DynamicQuantities defines a simple statically-typed Quantity type for Julia. Physical dimensions are stored as a value, as opposed to a parametric type, as in Unitful.jl. This can greatly improve both runtime performance, by avoiding type instabilities, and startup time, as it avoids overspecializing methods.
Performance
DynamicQuantities can greatly outperform Unitful when the compiler cannot infer dimensions in a function:
julia> using BenchmarkTools, DynamicQuantities; import Unitful
julia> dyn_uni = 0.2u"m/s"
0.2 m s⁻¹
julia> unitful = convert(Unitful.Quantity, dyn_uni)
0.2 m s⁻¹
julia> f(x, i) = x ^ i * 0.3;
julia> @btime f($dyn_uni, 1);
2.708 ns (0 allocations: 0 bytes)
julia> @btime f($unitful, 1);
2.597 μs (30 allocations: 1.33 KiB)Note the μ and n: this is a 1000x speedup! Here, the DynamicQuantities quantity object allows the compiler to build a function that is type stable, while the Unitful quantity object, which stores its dimensions in the type, requires type inference at runtime.
However, if the dimensions in your function can be inferred by the compiler, then you can get better speeds with Unitful:
julia> g(x) = x ^ 2 * 0.3;
julia> @btime g($dyn_uni);
1.791 ns (0 allocations: 0 bytes)
julia> @btime g($unitful);
1.500 ns (0 allocations: 0 bytes)While both of these are type stable, because Unitful parametrizes the type on the dimensions, functions can specialize to units and the compiler can optimize away units from the code.
Usage
You can create a Quantity object by using the convenience macro u"...":
julia> x = 0.3u"km/s"
300.0 m s⁻¹
julia> y = 42 * u"kg"
42.0 kgor by importing explicitly:
julia> using DynamicQuantities: kPa
julia> room_temp = 100kPa
100000.0 m⁻¹ kg s⁻²Note that Units is an exported submodule, so you can also access this as Units.kPa. You may like to define
julia> const U = Unitsso that you can simply write, say, U.kPa or C.m_e.
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> x = Quantity(300.0, length=1, time=-1)
300.0 m s⁻¹Elementary calculations with +, -, *, /, ^, sqrt, cbrt, abs are supported:
julia> x * y
12600.0 m kg s⁻¹
julia> x / y
7.142857142857143 m kg⁻¹ s⁻¹
julia> x ^ 3
2.7e7 m³ s⁻³
julia> x ^ -1
0.0033333333333333335 m⁻¹ s
julia> sqrt(x)
17.320508075688775 m¹ᐟ² s⁻¹ᐟ²
julia> x ^ 1.5
5196.152422706632 m³ᐟ² s⁻³ᐟ²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> x + 3 * x
1.2 m¹ᐟ² kg
julia> x + y
ERROR: DimensionError: 0.3 m¹ᐟ² kg and 10.2 kg² s⁻² have incompatible dimensionsThe dimensions of a Quantity can be accessed either with dimension(quantity) for the entire Dimensions object:
julia> dimension(x)
m¹ᐟ² kgor with umass, ulength, etc., for the various dimensions:
julia> umass(x)
1//1
julia> ulength(x)
1//2Finally, you can strip units with ustrip:
julia> ustrip(x)
0.2You can also convert a quantity to a desired unit and then strip the units using a two-argument version of ustrip:
julia> ustrip(u"km", 1000u"m")
1.0
julia> ustrip(u"minute", 60u"s")
1.0This is equivalent to ustrip(quantity / unit) but performs dimension checks first.
Constants
There are a variety of physical constants accessible via the Constants submodule:
julia> Constants.c
2.99792458e8 m s⁻¹which you may like to define as
julia> const C = ConstantsThese can also be used inside the u"..." macro:
julia> u"Constants.c * Hz"
2.99792458e8 m s⁻²Similarly, you can just import each individual constant:
julia> using DynamicQuantities.Constants: hFor the full list, see the docs.
Symbolic Units
You can also choose to not eagerly convert to SI base units, instead leaving the units as the user had written them. For example:
julia> q = 100us"cm * kPa"
100.0 cm kPa
julia> q^2
10000.0 cm² kPa²You can convert to regular SI base units with uexpand:
julia> uexpand(q^2)
1.0e6 kg² s⁻⁴This also works with constants:
julia> x = us"Constants.c * Hz"
1.0 Hz c
julia> x^2
1.0 Hz² c²
julia> uexpand(x^2)
8.987551787368176e16 m² s⁻⁴You can also convert a quantity in regular base SI units to symbolic units with the |> infix operator
julia> 5e-9u"m" |> us"nm"
5.0 nmYou can also convert between different symbolic units. (Note that you can write this more explicitly with uconvert(us"nm", 5e-9u"m").)
Finally, you can also import these directly:
julia> using DynamicQuantities.SymbolicUnits: cmor constants:
julia> using DynamicQuantities.SymbolicConstants: hNote that SymbolicUnits and SymbolicConstants are exported, so you can simply access these as SymbolicUnits.cm and SymbolicConstants.h, respectively.
Custom Units
You can create custom units with the @register_unit macro:
julia> @register_unit OneFiveV 1.5u"V"and then use it in calculations normally:
julia> x = us"OneFiveV"
1.0 OneFiveV
julia> x * 10u"A" |> us"W"
15.0 W
julia> 3us"V" |> us"OneFiveV"
2.0 OneFiveVAffine Units
You can also use "affine" units such as Celsius or Fahrenheit, using the ua"..." string macro:
julia> room_temp = 22ua"degC"
295.15 K
julia> freezing = 32ua"degF"
273.15 KThese are regular Quantity{Float64,Dimensions{...}} objects, meaning that you can use them in the same way as regular quantities, including taking differences.
To convert back, you can use the two-argument ustrip with the particular affine unit:
julia> ustrip(ua"degC", 295.15u"K")
22.0Arrays
For working with an array of quantities that have the same dimensions, you can use a QuantityArray:
julia> ar = rand(3)u"m/s"
3-element QuantityArray(::Vector{Float64}, ::Quantity{Float64, Dimensions{FRInt32}}):
0.2729202669351497 m s⁻¹
0.992546340360901 m s⁻¹
0.16863543422972482 m s⁻¹This QuantityArray is a subtype <:AbstractArray{Quantity{Float64,Dimensions{...}},1}, meaning that indexing a specific element will return a Quantity:
julia> ar[2]
0.992546340360901 m s⁻¹
julia> ar[2] *= 2
1.985092680721802 m s⁻¹
julia> ar[2] += 0.5u"m/s"
2.485092680721802 m s⁻¹This also has a custom broadcasting interface which allows the compiler to avoid redundant dimension calculations, relative to if you had simply used an array of quantities:
julia> f(v) = v^2 * 1.5;
julia> @btime $f.(xa) setup=(xa = randn(100000) .* u"km/s");
109.500 μs (2 allocations: 3.81 MiB)
julia> @btime $f.(qa) setup=(xa = randn(100000) .* u"km/s"; qa = QuantityArray(xa));
50.917 μs (3 allocations: 781.34 KiB)So we can see the QuantityArray version saves on both time and memory.
By default, DynamicQuantities will create a QuantityArray from an AbstractArray, similarly to how a Quantity is created from a scalar in the Usage examples:
julia> x = (1:3)us"km/h"
3-element QuantityArray(::StepRangeLen{Float64, ...}, ::Quantity{Float64, SymbolicDimensions{...}}):
1.0 km h⁻¹
2.0 km h⁻¹
3.0 km h⁻¹Unitful
DynamicQuantities allows you to convert back and forth from Unitful.jl:
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 m s⁻¹
julia> y2 = y^2 * 0.3
75000.0 m² s⁻²
julia> x2 = convert(Unitful.Quantity, y2)
75000.0 m² s⁻²
julia> x^2*0.3 == x2
trueTypes
Both a Quantity's values and dimensions are of arbitrary type. The default Dimensions (for the u"..." macro) performs exponent tracking for SI units, and SymbolicDimensions (for the us"..." macro) performs exponent tracking for all known unit and constant symbols, using a sparse array.
You can create custom spaces dimension spaces by simply creating a Julia struct subtyped to AbstractDimensions:
julia> struct CookiesAndMilk{R} <: AbstractDimensions{R}
cookies::R
milk::R
end
julia> cookie_rate = Quantity(0.9, CookiesAndMilk(cookies=1, milk=-1))
0.9 cookies milk⁻¹
julia> total_milk = Quantity(103, CookiesAndMilk(milk=1))
103 milk
julia> total_cookies = cookie_rate * total_milk
92.7 cookiesExponents are tracked by default with the type FRInt32 (alias for FixedRational{Int32, 25200}), which represents rational numbers with an integer numerator and fixed denominator. This is much faster than Rational.
julia> typeof(0.5u"kg")
Quantity{Float64, Dimensions{FRInt32}}You can change the type of the value field by initializing with a value explicitly of the desired type.
julia> typeof(Quantity(Float16(0.5), mass=1, length=1))
Quantity{Float16, Dimensions{FRInt32}}or by conversion:
julia> typeof(convert(Quantity{Float16}, 0.5u"m/s"))
Quantity{Float16, Dimensions{FRInt32}}For many applications, using FRInt8 (alias for FixedRational{Int8,12}) will suffice as the base dimensions type, and can be faster as it means the entire Dimensions struct will fit into 64 bits. You can change the type of the dimensions field by passing the type you wish to use as the second argument to Quantity:
julia> using DynamicQuantities
julia> R8 = Dimensions{FRInt8};
julia> R32 = Dimensions{FRInt32};
julia> q8 = [Quantity{Float64,R8}(randn(), length=rand(-2:2)) for i in 1:1000];
julia> q32 = [Quantity{Float64,R32}(randn(), length=rand(-2:2)) for i in 1:1000];
julia> f(x) = @. x ^ 2 * 0.5;
julia> @btime f($q8);
1.433 μs (3 allocations: 15.77 KiB)
julia> @btime f($q32);
1.883 μs (4 allocations: 39.12 KiB)