diff --git a/README.md b/README.md index b7e3838a..2e43ddcc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ This is done to allow for calculations where physical dimensions are not known a - [Performance](#performance) - [Usage](#usage) -- [Units](#units) - [Types](#types) - [Vectors](#vectors) @@ -26,8 +25,8 @@ when the compiler cannot infer dimensions in a function: ```julia julia> using BenchmarkTools, DynamicQuantities; import Unitful -julia> dyn_uni = Quantity(0.2, mass=1, length=0.5, amount=3) -0.2 𝐋 ¹ᐟ² 𝐌 ¹ 𝐍 ³ +julia> dyn_uni = 0.2u"m^0.5 * kg * mol^3" +0.2 m¹ᐟ² kg mol³ julia> unitful = convert(Unitful.Quantity, dyn_uni) 0.2 kg m¹ᐟ² mol³ @@ -64,56 +63,71 @@ to units and the compiler can optimize away units from the code. ## Usage -You can create a `Quantity` object with a value and keyword arguments for the powers of the physical dimensions -(`mass`, `length`, `time`, `current`, `temperature`, `luminosity`, `amount`): +You can create a `Quantity` object +by using the convenience macro `u"..."`: ```julia -julia> x = Quantity(0.3, mass=1, length=0.5) -0.3 𝐋 ¹ᐟ² 𝐌 ¹ +julia> x = 0.3u"km/s" +300.0 m s⁻¹ + +julia> y = 42 * u"kg" +42.0 kg + +julia> room_temp = 100u"kPa" +100000.0 m⁻¹ kg s⁻² +``` + +This supports a wide range of SI base and derived units, with common +prefixes. + +You can also construct values explicitly with the `Quantity` type, +with a value and keyword arguments for the powers of the physical dimensions +(`mass`, `length`, `time`, `current`, `temperature`, `luminosity`, `amount`): -julia> y = Quantity(10.2, mass=2, time=-2) -10.2 𝐌 ² 𝐓 ⁻² +```julia +julia> x = Quantity(300.0, length=1, time=-1) +300.0 m s⁻¹ ``` -Elementary calculations with `+, -, *, /, ^, sqrt, cbrt` are supported: +Elementary calculations with `+, -, *, /, ^, sqrt, cbrt, abs` are supported: ```julia julia> x * y -3.0599999999999996 𝐋 ¹ᐟ² 𝐌 ³ 𝐓 ⁻² +12600.0 m kg s⁻¹ julia> x / y -0.029411764705882353 𝐋 ¹ᐟ² 𝐌 ⁻¹ 𝐓 ² +7.142857142857143 m kg⁻¹ s⁻¹ julia> x ^ 3 -0.027 𝐋 ³ᐟ² 𝐌 ³ +2.7e7 m³ s⁻³ julia> x ^ -1 -3.3333333333333335 𝐋 ⁻¹ᐟ² 𝐌 ⁻¹ +0.0033333333333333335 m⁻¹ s julia> sqrt(x) -0.5477225575051661 𝐋 ¹ᐟ⁴ 𝐌 ¹ᐟ² +17.320508075688775 m¹ᐟ² s⁻¹ᐟ² julia> x ^ 1.5 -0.1643167672515498 𝐋 ³ᐟ⁴ 𝐌 ³ᐟ² +5196.152422706632 m³ᐟ² s⁻³ᐟ² ``` -Each of these values has the same type, thus obviating the need for type inference at runtime. +Each of these values has the same type, which means we don't need to perform type inference at runtime. Furthermore, we can do dimensional analysis by detecting `DimensionError`: ```julia julia> x + 3 * x -1.2 𝐋 ¹ᐟ² 𝐌 ¹ +1.2 m¹ᐟ² kg julia> x + y -ERROR: DimensionError: 0.3 𝐋 ¹ᐟ² 𝐌 ¹ and 10.2 𝐌 ² 𝐓 ⁻² have different dimensions +ERROR: DimensionError: 0.3 m¹ᐟ² kg and 10.2 kg² s⁻² have incompatible dimensions ``` The dimensions of a `Quantity` can be accessed either with `dimension(quantity)` for the entire `Dimensions` object: ```julia julia> dimension(x) -𝐋 ¹ᐟ² 𝐌 ¹ +m¹ᐟ² kg ``` or with `umass`, `ulength`, etc., for the various dimensions: @@ -133,26 +147,28 @@ julia> ustrip(x) 0.2 ``` -## Units +### Unitful + +DynamicQuantities works with quantities that are exclusively +represented by their SI base units. This gives us type stability +and greatly improves performance. -DynamicQuantities works with quantities which store physical dimensions and a value, -and does not directly provide a unit system. However, performing calculations with physical dimensions is actually equivalent to working with a standardized unit system. Thus, you can use Unitful to parse units, and then use the DynamicQuantities->Unitful extension for conversion: ```julia -julia> using Unitful: Unitful, @u_str +julia> using Unitful: Unitful, @u_str; import DynamicQuantities julia> x = 0.5u"km/s" 0.5 km s⁻¹ julia> y = convert(DynamicQuantities.Quantity, x) -500.0 𝐋 ¹ 𝐓 ⁻¹ +500.0 m s⁻¹ julia> y2 = y^2 * 0.3 -75000.0 𝐋 ² 𝐓 ⁻² +75000.0 m² s⁻² julia> x2 = convert(Unitful.Quantity, y2) 75000.0 m² s⁻² @@ -163,24 +179,31 @@ true ## Types -Both the `Quantity`'s values and dimensions are of arbitrary type. +Both a `Quantity`'s values and dimensions are of arbitrary type. By default, dimensions are stored as a `DynamicQuantities.FixedRational{Int32,C}` object, which represents a rational number with a fixed denominator `C`. This is much faster than `Rational`. ```julia -julia> typeof(Quantity(0.5, mass=1)) +julia> typeof(0.5u"kg") Quantity{Float64, FixedRational{Int32, 25200} ``` You can change the type of the value field by initializing with a value -of the desired type. +explicitly of the desired type. ```julia julia> typeof(Quantity(Float16(0.5), mass=1, length=1)) Quantity{Float16, FixedRational{Int32, 25200}} ``` +or by conversion: + +```julia +julia> typeof(convert(Quantity{Float16}, 0.5u"m/s")) +Quantity{Float16, DynamicQuantities.FixedRational{Int32, 25200}} +``` + For many applications, `FixedRational{Int8,6}` will suffice, and can be faster as it means the entire `Dimensions` struct will fit into 64 bits. @@ -213,23 +236,23 @@ There is not a separate class for vectors, but you can create units like so: ```julia -julia> randn(5) .* Dimensions(mass=2/5, length=2) -5-element Vector{Quantity{Float64, FixedRational{Int32, 25200}}}: - -0.6450221578668845 𝐋 ² 𝐌 ²ᐟ⁵ - 0.4024829670050946 𝐋 ² 𝐌 ²ᐟ⁵ - 0.21478863605789672 𝐋 ² 𝐌 ²ᐟ⁵ - 0.0719774550969669 𝐋 ² 𝐌 ²ᐟ⁵ - -1.4231241943420674 𝐋 ² 𝐌 ²ᐟ⁵ +julia> randn(5) .* u"m/s" +5-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}: + 1.1762086954956399 m s⁻¹ + 1.320811324040591 m s⁻¹ + 0.6519033652437799 m s⁻¹ + 0.7424822374423569 m s⁻¹ + 0.33536928068133726 m s⁻¹ ``` Because it is type stable, you can have mixed units in a vector too: ```julia julia> v = [Quantity(randn(), mass=rand(0:5), length=rand(0:5)) for _=1:5] -5-element Vector{Quantity{Float64, FixedRational{Int32, 25200}}}: - 2.2054411324716865 𝐌 ³ - -0.01603602425887379 𝐋 ⁴ 𝐌 ³ - 1.4388184352393647 - 2.382303019892503 𝐋 ² 𝐌 ¹ - 0.6071392594021706 𝐋 ⁴ 𝐌 ⁴ +5-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}: + 0.4309293892461158 kg⁵ + 1.415520139801276 + 1.2179414706524276 m³ kg⁴ + -0.18804207255117408 m³ kg⁵ + 0.52123911329638 m³ kg² ``` diff --git a/docs/Project.toml b/docs/Project.toml index 14f45c27..dfa65cd1 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,2 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -DynamicQuantities = "06fc5a27-2a28-4c7c-a15d-362465fb6821" diff --git a/docs/make.jl b/docs/make.jl index c0d62263..d604307f 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,5 @@ using DynamicQuantities +import DynamicQuantities.Units using Documenter DocMeta.setdocmeta!(DynamicQuantities, :DocTestSetup, :(using DynamicQuantities); recursive=true) @@ -26,7 +27,7 @@ open(dirname(@__FILE__) * "/src/index.md", "w") do io end makedocs(; - modules=[DynamicQuantities], + modules=[DynamicQuantities, DynamicQuantities.Units], authors="MilesCranmer and contributors", repo="https://github.com/SymbolicML/DynamicQuantities.jl/blob/{commit}{path}#{line}", sitename="DynamicQuantities.jl", diff --git a/docs/src/api.md b/docs/src/api.md index 6cab9efe..21309a72 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,15 +1,91 @@ -```@meta -CurrentModule = DynamicQuantities +# Usage + +## Types + +```@docs +Quantity +Dimensions +``` + +## Utilities + +The two main general utilities for working +with quantities are `ustrip` and `dimension`: + +```@docs +ustrip +dimension ``` -# API Reference +### Accessing dimensions -API Reference for [DynamicQuantities](https://github.com/SymbolicML/DynamicQuantities.jl). +Utility functions to extract specific dimensions are as follows: -```@index +```@docs +ulength +umass +utime +ucurrent +utemperature +uluminosity +uamount ``` ```@autodocs Modules = [DynamicQuantities] -Order = [:type, :function] -``` \ No newline at end of file +Pages = ["utils.jl"] +Filter = t -> !(t in [ustrip, dimension, ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount]) +``` + +## Units + +The two main functions for working with units are `uparse` and `u_str`: + +```@docs +@u_str +uparse +``` + +### Available units + +The base SI units are as follows. +Instead of calling directly, it is recommended to access them via +the `@u_str` macro, which evaluates the expression +in a namespace with all the units available. + +```@docs +Units.m +Units.g +Units.s +Units.A +Units.K +Units.cd +Units.mol +``` + +Several derived SI units are available as well: + +```@docs +Units.Hz +Units.N +Units.Pa +Units.J +Units.W +Units.C +Units.V +Units.F +Units.Ω +Units.T +Units.L +Units.bar +``` + +## Internals + +### FixedRational + +```@docs +DynamicQuantities.FixedRational +DynamicQuantities.denom +``` + diff --git a/src/DynamicQuantities.jl b/src/DynamicQuantities.jl index f0024575..0fe164fc 100644 --- a/src/DynamicQuantities.jl +++ b/src/DynamicQuantities.jl @@ -2,13 +2,16 @@ module DynamicQuantities export Quantity, Dimensions, DimensionError, ustrip, dimension, valid export ulength, umass, utime, ucurrent, utemperature, uluminosity, uamount +export uparse, @u_str include("fixed_rational.jl") include("types.jl") include("utils.jl") include("math.jl") +include("units.jl") import Requires: @init, @require +import .Units: uparse, @u_str if !isdefined(Base, :get_extension) @init @require Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d" include("../ext/DynamicQuantitiesUnitfulExt.jl") diff --git a/src/fixed_rational.jl b/src/fixed_rational.jl index 807501e1..2d914ac9 100644 --- a/src/fixed_rational.jl +++ b/src/fixed_rational.jl @@ -4,6 +4,11 @@ A rational number with a fixed denominator. Significantly faster than `Rational{T}`, as it never needs to compute the `gcd` apart from when printing. +Access the denominator with `denom(F)` (which converts to `T`). + +# Fields + +- `num`: numerator of type `T`. The denominator is fixed to the type parameter `den`. """ struct FixedRational{T<:Integer,den} <: Real num::T @@ -35,14 +40,18 @@ Base.inv(x::F) where {F<:FixedRational} = unsafe_fixed_rational(widemul(denom(F) Base.:(==)(x::F, y::F) where {F<:FixedRational} = x.num == y.num Base.iszero(x::FixedRational) = iszero(x.num) +Base.isone(x::F) where {F<:FixedRational} = x.num == denom(F) Base.isinteger(x::F) where {F<:FixedRational} = iszero(x.num % denom(F)) Base.convert(::Type{F}, x::Integer) where {F<:FixedRational} = unsafe_fixed_rational(x * denom(F), eltype(F), val_denom(F)) Base.convert(::Type{F}, x::Rational) where {F<:FixedRational} = F(x) +Base.convert(::Type{Rational{R}}, x::F) where {R,F<:FixedRational} = Rational{R}(x.num, denom(F)) Base.convert(::Type{Rational}, x::F) where {F<:FixedRational} = Rational{eltype(F)}(x.num, denom(F)) Base.convert(::Type{AF}, x::F) where {AF<:AbstractFloat,F<:FixedRational} = convert(AF, x.num) / convert(AF, denom(F)) Base.round(::Type{T}, x::F) where {T,F<:FixedRational} = div(convert(T, x.num), convert(T, denom(F)), RoundNearest) +Base.promote(x::Integer, y::F) where {F<:FixedRational} = (F(x), y) +Base.promote(x::F, y::Integer) where {F<:FixedRational} = reverse(promote(y, x)) Base.promote(x, y::F) where {F<:FixedRational} = promote(x, convert(Rational, y)) -Base.promote(x::F, y) where {F<:FixedRational} = promote(convert(Rational, x), y) +Base.promote(x::F, y) where {F<:FixedRational} = reverse(promote(y, x)) Base.show(io::IO, x::F) where {F<:FixedRational} = show(io, convert(Rational, x)) Base.zero(::Type{F}) where {F<:FixedRational} = unsafe_fixed_rational(0, eltype(F), val_denom(F)) diff --git a/src/math.jl b/src/math.jl index 4662ab02..39195dae 100644 --- a/src/math.jl +++ b/src/math.jl @@ -19,10 +19,13 @@ Base.:/(l::Number, r::Dimensions) = Quantity(l, inv(r)) Base.:+(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value + r.value, l.dimensions) : throw(DimensionError(l, r)) Base.:-(l::Quantity, r::Quantity) = dimension(l) == dimension(r) ? Quantity(l.value - r.value, l.dimensions) : throw(DimensionError(l, r)) -_pow(l::Dimensions{R}, r::R) where {R} = @map_dimensions(Base.Fix1(*, r), l) -_pow(l::Quantity{T,R}, r::R) where {T,R} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r)) +_pow(l::Dimensions, r) = @map_dimensions(Base.Fix1(*, r), l) +_pow(l::Quantity{T}, r) where {T} = Quantity(l.value^r, _pow(l.dimensions, r)) +_pow_as_T(l::Quantity{T}, r) where {T} = Quantity(l.value^convert(T, r), _pow(l.dimensions, r)) +Base.:^(l::Dimensions{R}, r::Integer) where {R} = _pow(l, r) Base.:^(l::Dimensions{R}, r::Number) where {R} = _pow(l, tryrationalize(R, r)) -Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow(l, tryrationalize(R, r)) +Base.:^(l::Quantity{T,R}, r::Integer) where {T,R} = _pow(l, r) +Base.:^(l::Quantity{T,R}, r::Number) where {T,R} = _pow_as_T(l, tryrationalize(R, r)) Base.inv(d::Dimensions) = @map_dimensions(-, d) Base.inv(q::Quantity) = Quantity(inv(q.value), inv(q.dimensions)) diff --git a/src/types.jl b/src/types.jl index 7cd03482..823d3cd3 100644 --- a/src/types.jl +++ b/src/types.jl @@ -2,11 +2,13 @@ const DEFAULT_DIM_TYPE = FixedRational{Int32, 2^4 * 3^2 * 5^2 * 7} const DEFAULT_VALUE_TYPE = Float64 """ - Dimensions + Dimensions{R} A type representing the dimensions of a quantity, with each field giving the power of the corresponding dimension. For example, the dimensions of velocity are `Dimensions(length=1, time=-1)`. +Each of the 7 dimensions are stored using the type `R`, +which is by default a rational number. # Fields @@ -17,6 +19,15 @@ example, the dimensions of velocity are `Dimensions(length=1, time=-1)`. - `temperature`: temperature dimension (i.e., K^(temperature)) - `luminosity`: luminosity dimension (i.e., cd^(luminosity)) - `amount`: amount dimension (i.e., mol^(amount)) + +# Constructors + +- `Dimensions(args...)`: Pass all the dimensions as arguments. `R` is set to `DEFAULT_DIM_TYPE`. +- `Dimensions(; kws...)`: Pass a subset of dimensions as keyword arguments. `R` is set to `DEFAULT_DIM_TYPE`. +- `Dimensions(::Type{R}; kws...)` or `Dimensions{R}(; kws...)`: Pass a subset of dimensions as keyword arguments, with the output type set to `Dimensions{R}`. +- `Dimensions{R}(args...)`: Pass all the dimensions as arguments, with the output type set to `Dimensions{R}`. +- `Dimensions{R}(d::Dimensions)`: Copy the dimensions from another `Dimensions` object, with the output type set to `Dimensions{R}`. + """ struct Dimensions{R <: Real} length::R @@ -48,16 +59,17 @@ struct Dimensions{R <: Real} ) Dimensions{_R}(; kws...) where {_R} = Dimensions(_R; kws...) Dimensions{_R}(args...) where {_R} = Dimensions(Base.Fix1(convert, _R).(args)...) + Dimensions{_R}(d::Dimensions) where {_R} = Dimensions{_R}(d.length, d.mass, d.time, d.current, d.temperature, d.luminosity, d.amount) end const DIMENSION_NAMES = Base.fieldnames(Dimensions) -const DIMENSION_SYNONYMS = (:𝐋, :𝐌, :𝐓, :𝐈, :𝚯, :𝐉, :𝐍) +const DIMENSION_SYNONYMS = (:m, :kg, :s, :A, :K, :cd, :mol) const SYNONYM_MAPPING = NamedTuple(DIMENSION_NAMES .=> DIMENSION_SYNONYMS) """ - Quantity{T} + Quantity{T,R} -Physical quantity with value `value` of type `T` and dimensions `dimensions`. +Physical quantity with value `value` of type `T` and dimensions `dimensions` of type `Dimensions{R}`. For example, the velocity of an object with mass 1 kg and velocity 2 m/s is `Quantity(2, mass=1, length=1, time=-1)`. You should access these fields with `ustrip(q)`, and `dimensions(q)`. @@ -65,12 +77,21 @@ You can access specific dimensions with `ulength(q)`, `umass(q)`, `utime(q)`, `ucurrent(q)`, `utemperature(q)`, `uluminosity(q)`, and `uamount(q)`. Severals operators in `Base` are extended to work with `Quantity` objects, -including `*`, `+`, `-`, `/`, `^`, `sqrt`, and `cbrt`. +including `*`, `+`, `-`, `/`, `abs`, `^`, `sqrt`, and `cbrt`, which manipulate +dimensions according to the operation. # Fields -- `value::T`: value of the quantity of some type `T` -- `dimensions::Dimensions`: dimensions of the quantity +- `value::T`: value of the quantity of some type `T`. Access with `ustrip(::Quantity)` +- `dimensions::Dimensions{R}`: dimensions of the quantity with dimension type `R`. Access with `dimension(::Quantity)` + +# Constructors + +- `Quantity(x; kws...)`: Construct a quantity with value `x` and dimensions given by the keyword arguments. The value type is inferred from `x`. `R` is set to `DEFAULT_DIM_TYPE`. +- `Quantity(x, ::Type{R}; kws...)`: Construct a quantity with value `x`. The dimensions parametric type is set to `R`. +- `Quantity(x, d::Dimensions{R})`: Construct a quantity with value `x` and dimensions `d`. +- `Quantity{T}(q::Quantity)`: Construct a quantity with value `q.value` and dimensions `q.dimensions`, but with value type converted to `T`. +- `Quantity{T,R}(q::Quantity)`: Construct a quantity with value `q.value` and dimensions `q.dimensions`, but with value type converted to `T` and dimensions parametric type set to `R`. """ struct Quantity{T, R} value::T @@ -79,6 +100,8 @@ struct Quantity{T, R} Quantity(x; kws...) = new{typeof(x), DEFAULT_DIM_TYPE}(x, Dimensions(; kws...)) Quantity(x, ::Type{_R}; kws...) where {_R} = new{typeof(x), _R}(x, Dimensions(_R; kws...)) Quantity(x, d::Dimensions{_R}) where {_R} = new{typeof(x), _R}(x, d) + Quantity{T}(q::Quantity) where {T} = Quantity(convert(T, q.value), dimension(q)) + Quantity{T,R}(q::Quantity) where {T,R} = Quantity(convert(T, q.value), Dimensions{R}(dimension(q))) end struct DimensionError{Q1,Q2} <: Exception diff --git a/src/units.jl b/src/units.jl new file mode 100644 index 00000000..bbba5ffa --- /dev/null +++ b/src/units.jl @@ -0,0 +1,148 @@ +module Units + +export uparse, @u_str + +import ..DEFAULT_DIM_TYPE +import ..DEFAULT_VALUE_TYPE +import ..Quantity + +@assert DEFAULT_VALUE_TYPE == Float64 "`units.jl` must be updated to support a different default value type." + +macro add_prefixes(base_unit, prefixes) + @assert prefixes.head == :tuple + expr = _add_prefixes(base_unit, prefixes.args) + return expr |> esc +end + +function _add_prefixes(base_unit::Symbol, prefixes) + all_prefixes = ( + f=1e-15, p=1e-12, n=1e-9, μ=1e-6, u=1e-6, m=1e-3, c=1e-2, d=1e-1, + k=1e3, M=1e6, G=1e9 + ) + expr = Expr(:block) + for (prefix, value) in zip(keys(all_prefixes), values(all_prefixes)) + prefix in prefixes || continue + new_unit = Symbol(prefix, base_unit) + push!(expr.args, :(const $new_unit = $value * $base_unit)) + end + return expr +end + +# SI base units +"Length in meters. Available variants: `fm`, `pm`, `nm`, `μm` (/`um`), `cm`, `dm`, `mm`, `km`, `Mm`, `Gm`." +const m = Quantity(1.0, length=1) +"Mass in grams. Available variants: `μg` (/`ug`), `mg`, `kg`." +const g = Quantity(1e-3, mass=1) +"Time in seconds. Available variants: `fs`, `ps`, `ns`, `μs` (/`us`), `ms`, `min`, `h` (/`hr`), `day`, `yr`, `kyr`, `Myr`, `Gyr`." +const s = Quantity(1.0, time=1) +"Current in Amperes. Available variants: `nA`, `μA` (/`uA`), `mA`, `kA`." +const A = Quantity(1.0, current=1) +"Temperature in Kelvin. Available variant: `mK`." +const K = Quantity(1.0, temperature=1) +"Luminosity in candela. Available variant: `mcd`." +const cd = Quantity(1.0, luminosity=1) +"Amount in moles. Available variant: `mmol`." +const mol = Quantity(1.0, amount=1) + +@add_prefixes m (f, p, n, μ, u, c, d, m, k, M, G) +@add_prefixes g (μ, u, m, k) +@add_prefixes s (f, p, n, μ, u, m) +@add_prefixes A (n, μ, u, m, k) +@add_prefixes K (m,) +@add_prefixes cd (m,) +@add_prefixes mol (m,) + +# SI derived units +"Frequency in Hertz. Available variants: `kHz`, `MHz`, `GHz`." +const Hz = inv(s) +"Force in Newtons." +const N = kg * m / s^2 +"Pressure in Pascals. Available variant: `kPa`." +const Pa = N / m^2 +"Energy in Joules. Available variant: `kJ`." +const J = N * m +"Power in Watts. Available variants: `kW`, `MW`, `GW`." +const W = J / s +"Charge in Coulombs." +const C = A * s +"Voltage in Volts. Available variants: `kV`, `MV`, `GV`." +const V = W / A +"Capacitance in Farads." +const F = C / V +"Resistance in Ohms. Available variant: `mΩ`. Also available is ASCII `ohm` (with variant `mohm`)." +const Ω = V / A +const ohm = Ω +"Magnetic flux density in Teslas." +const T = N / (A * m) + +@add_prefixes Hz (k, M, G) +@add_prefixes N () +@add_prefixes Pa (k,) +@add_prefixes J (k,) +@add_prefixes W (k, M, G) +@add_prefixes C () +@add_prefixes V (m, k, M, G) +@add_prefixes F () +@add_prefixes Ω (m,) +@add_prefixes ohm (m,) +@add_prefixes T () + +# Common assorted units +## Time +const min = 60 * s +const h = 60 * min +const hr = h +const day = 24 * h +const yr = 365.25 * day + +@add_prefixes min () +@add_prefixes h () +@add_prefixes hr () +@add_prefixes day () +@add_prefixes yr (k, M, G) + +## Volume +"Volume in liters. Available variants: `mL`, `dL`." +const L = dm^3 + +@add_prefixes L (m, d) + +## Pressure +"Pressure in bars." +const bar = 100 * kPa + +@add_prefixes bar () + +# Do not wish to define Gaussian units, as it changes +# some formulas. Safer to force user to work exclusively in one unit system. + +# Do not wish to define physical constants, as the number of symbols might lead to ambiguity. +# The user should define these instead. + +""" + uparse(s::AbstractString) + +Parse a string containing an expression of units and return the +corresponding `Quantity` object with `Float64` value. For example, +`uparse("m/s")` would be parsed to `Quantity(1.0, length=1, time=-1)`. +""" +function uparse(s::AbstractString) + return as_quantity(eval(Meta.parse(s)))::Quantity{DEFAULT_VALUE_TYPE,DEFAULT_DIM_TYPE} +end + +as_quantity(q::Quantity) = q +as_quantity(x::Number) = Quantity(convert(DEFAULT_VALUE_TYPE, x), DEFAULT_DIM_TYPE) +as_quantity(x) = error("Unexpected type evaluated: $(typeof(x))") + +""" + u"[unit expression]" + +Parse a string containing an expression of units and return the +corresponding `Quantity` object with `Float64` value. For example, +`u"km/s^2"` would be parsed to `Quantity(1000.0, length=1, time=-2)`. +""" +macro u_str(s) + return esc(uparse(s)) +end + +end diff --git a/src/utils.jl b/src/utils.jl index 5aa903b0..b73001ce 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -61,7 +61,7 @@ Base.show(io::IO, d::Dimensions) = for k in keys(d) if !iszero(d[k]) print(tmp_io, SYNONYM_MAPPING[k]) - pretty_print_exponent(tmp_io, d[k]) + isone(d[k]) || pretty_print_exponent(tmp_io, d[k]) print(tmp_io, " ") end end @@ -73,7 +73,7 @@ Base.show(io::IO, d::Dimensions) = Base.show(io::IO, q::Quantity) = print(io, q.value, " ", q.dimensions) string_rational(x) = isinteger(x) ? string(round(Int, x)) : string(x) -pretty_print_exponent(io::IO, x) = print(io, " ", to_superscript(string_rational(x))) +pretty_print_exponent(io::IO, x) = print(io, to_superscript(string_rational(x))) const SUPERSCRIPT_MAPPING = ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'] const INTCHARS = ['0' + i for i = 0:9] to_superscript(s::AbstractString) = join( @@ -88,6 +88,13 @@ tryrationalize(::Type{R}, x) where {R} = isinteger(x) ? convert(R, round(Int, x) Base.showerror(io::IO, e::DimensionError) = print(io, "DimensionError: ", e.q1, " and ", e.q2, " have incompatible dimensions") +Base.convert(::Type{Quantity}, q::Quantity) = q +Base.convert(::Type{Quantity{T}}, q::Quantity) where {T} = Quantity{T}(q) +Base.convert(::Type{Quantity{T,R}}, q::Quantity) where {T,R} = Quantity{T,R}(q) + +Base.convert(::Type{Dimensions}, d::Dimensions) = d +Base.convert(::Type{Dimensions{R}}, d::Dimensions) where {R} = Dimensions{R}(d) + """ ustrip(q::Quantity) @@ -106,49 +113,63 @@ dimension(::Number) = Dimensions() """ ulength(q::Quantity) + ulength(d::Dimensions) Get the length dimension of a quantity (e.g., meters^(ulength)). """ -ulength(q::Quantity) = q.dimensions.length +ulength(q::Quantity) = ulength(dimension(q)) +ulength(d::Dimensions) = d.length """ umass(q::Quantity) + umass(d::Dimensions) Get the mass dimension of a quantity (e.g., kg^(umass)). """ -umass(q::Quantity) = q.dimensions.mass +umass(q::Quantity) = umass(dimension(q)) +umass(d::Dimensions) = d.mass """ utime(q::Quantity) + utime(d::Dimensions) Get the time dimension of a quantity (e.g., s^(utime)) """ -utime(q::Quantity) = q.dimensions.time +utime(q::Quantity) = utime(dimension(q)) +utime(d::Dimensions) = d.time """ ucurrent(q::Quantity) + ucurrent(d::Dimensions) Get the current dimension of a quantity (e.g., A^(ucurrent)). """ -ucurrent(q::Quantity) = q.dimensions.current +ucurrent(q::Quantity) = ucurrent(dimension(q)) +ucurrent(d::Dimensions) = d.current """ utemperature(q::Quantity) + utemperature(d::Dimensions) Get the temperature dimension of a quantity (e.g., K^(utemperature)). """ -utemperature(q::Quantity) = q.dimensions.temperature +utemperature(q::Quantity) = utemperature(dimension(q)) +utemperature(d::Dimensions) = d.temperature """ uluminosity(q::Quantity) + uluminosity(d::Dimensions) Get the luminosity dimension of a quantity (e.g., cd^(uluminosity)). """ -uluminosity(q::Quantity) = q.dimensions.luminosity +uluminosity(q::Quantity) = uluminosity(dimension(q)) +uluminosity(d::Dimensions) = d.luminosity """ uamount(q::Quantity) + uamount(d::Dimensions) Get the amount dimension of a quantity (e.g., mol^(uamount)). """ -uamount(q::Quantity) = q.dimensions.amount +uamount(q::Quantity) = uamount(dimension(q)) +uamount(d::Dimensions) = d.amount diff --git a/test/unittests.jl b/test/unittests.jl index ce2c22e8..e9e0558c 100644 --- a/test/unittests.jl +++ b/test/unittests.jl @@ -35,8 +35,8 @@ using Test @test ustrip(y) ≈ T(0.4) if R <: Rational - @test string(x) == "0.2 𝐋 ¹ 𝐌 ⁵ᐟ²" - @test string(inv(x)) == "5.0 𝐋 ⁻¹ 𝐌 ⁻⁵ᐟ²" + @test string(x) == "0.2 m kg⁵ᐟ²" + @test string(inv(x)) == "5.0 m⁻¹ kg⁻⁵ᐟ²" end @test_throws DimensionError x^2 + x @@ -176,3 +176,68 @@ end q = Quantity(0.5, inv(d)) @test q == Quantity(0.5, length=0.2, luminosity=-2) end + +@testset "Conversions" begin + d = Dimensions(Rational{Int16}, mass=2) + d32 = convert(Dimensions{Rational{Int32}}, d) + @test typeof(d) == Dimensions{Rational{Int16}} + @test typeof(d32) == Dimensions{Rational{Int32}} + @test umass(d) == 2 + @test umass(d32) == 2 + @test typeof(umass(d32)) == Rational{Int32} + + # Should not change: + @test convert(Dimensions, d) === d + + q = Quantity(0.5, d) + q32_32 = convert(Quantity{Float32,Rational{Int32}}, q) + @test typeof(q) == Quantity{Float64,Rational{Int16}} + @test typeof(q32_32) == Quantity{Float32,Rational{Int32}} + @test ustrip(q) == 0.5 + @test ustrip(q32_32) == 0.5 + @test typeof(ustrip(q)) == Float64 + @test typeof(ustrip(q32_32)) == Float32 + @test dimension(q32_32) == dimension(q) + @test umass(q) == 2 + @test umass(q32_32) == 2 + @test typeof(umass(q32_32)) == Rational{Int32} + @test typeof(convert(Quantity{Float16}, q)) == Quantity{Float16,Rational{Int16}} + @test convert(Quantity, q) === q +end + +@testset "Units" begin + x = 1.3u"km/s^2" + @test ustrip(x) == 1300 # SI base units + @test ulength(x) == 1 + @test utime(x) == -2 + + y = 0.9u"sqrt(mΩ)" + @test typeof(y) == Quantity{Float64,DEFAULT_DIM_TYPE} + @test ustrip(y) ≈ 0.02846049894151541 + @test ucurrent(y) == -1 + @test ulength(y) == 1 + + y = BigFloat(0.3) * u"mΩ" + @test typeof(y) == Quantity{BigFloat,DEFAULT_DIM_TYPE} + @test ustrip(y) ≈ 0.0003 + @test ulength(y) == 2 + + y32 = convert(Quantity{Float32,Rational{Int16}}, y) + @test typeof(y32) == Quantity{Float32,Rational{Int16}} + @test ustrip(y32) ≈ 0.0003 + + z = u"yr" + @test utime(z) == 1 + @test ustrip(z) ≈ 60 * 60 * 24 * 365.25 + + # Test type stability of extreme range of units + @test typeof(u"1") == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"1f0") == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"s"^2) == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"Ω") == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"Gyr") == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"fm") == Quantity{Float64,DEFAULT_DIM_TYPE} + @test typeof(u"fm"^2) == Quantity{Float64,DEFAULT_DIM_TYPE} + + @test_throws LoadError eval(:(u":x")) +end