Happy to share DynamicQuantities.jl, a library that defines a simple, statically-typed Quantity
object for working with physical units in Julia.
Physical quantities are stored as values, as opposed to the parametrically-typed units in Unitful.jl. This is done to allow for calculations where physical dimensions are not inferrable at compile time.
Performance
These type-stable quantities can greatly outperform those in Unitful when the compiler cannot infer dimensions:
julia> using BenchmarkTools, DynamicQuantities; import Unitful
julia> dynamic_q = Quantity(0.2, mass=1, length=0.5, amount=3)
0.2 𝐋 ¹ᐟ² 𝐌 ¹ 𝐍 ³
julia> unitful = convert(Unitful.Quantity, dynamic_q)
0.2 kg m¹ᐟ² mol³
julia> f(x, i) = x ^ i * 0.3;
julia> @btime f($dynamic_q, i) setup=(i=rand(1:10));
9.384 ns (0 allocations: 0 bytes)
julia> @btime f($unitful, i) setup=(i=rand(1:10));
29.667 μs (42 allocations: 1.91 KiB)
Note the μ and n: this example gets a >3000x speedup. DynamicQuantities 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, note 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($dynamic_q);
6.083 ns (0 allocations: 0 bytes)
julia> @btime g($unitful);
1.958 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 entirely.
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
):
julia> x = Quantity(0.3, mass=1, length=0.5)
0.3 𝐋 ¹ᐟ² 𝐌 ¹
Elementary calculations with +, -, *, /, ^, sqrt, cbrt, abs
are supported, letting you perform calculations on quantities just as you would with Unitful.
julia> x ^ 1.5
0.1643167672515498 𝐋 ³ᐟ⁴ 𝐌 ³ᐟ²
julia> x * Quantity(10.2, mass=2, time=-2)
3.0599999999999996 𝐋 ¹ᐟ² 𝐌 ³ 𝐓 ⁻²
julia> x ^ 3
0.027 𝐋 ³ᐟ² 𝐌 ³
julia> x ^ -1
3.3333333333333335 𝐋 ⁻¹ᐟ² 𝐌 ⁻¹
julia> sqrt(x)
0.5477225575051661 𝐋 ¹ᐟ⁴ 𝐌 ¹ᐟ²
Each return value has the same type as x
, which means no type inference is needed.
The dimensions of a Quantity
can be accessed either with dimension(quantity)
for the entire Dimensions
object:
julia> dimension(x)
𝐋 ¹ᐟ² 𝐌 ¹
or with umass
, ulength
, etc., for specific dimensions. You can strip dimensions with ustrip
. You can check for dimensional analysis errors with valid(x)
- avoiding a try/catch.
Units
DynamicQuantities works with quantities which store physical dimensions (think of this as using the SI standard units — and only those) and a value, and does not directly provide a unit system. However, DynamicQuantities has an interface with Unitful: you can use Unitful to parse units, and then use the DynamicQuantities->Unitful extension for conversion:
julia> using Unitful: Unitful, @u_str
julia> x = 0.5u"km/s"
0.5 km s⁻¹
julia> y = convert(DynamicQuantities.Quantity, x) # Auto reduces to SI
500.0 𝐋 ¹ 𝐓 ⁻¹
julia> y2 = y^2 * 0.3
75000.0 𝐋 ² 𝐓 ⁻²
julia> x2 = convert(Unitful.Quantity, y2) # Back to SI
75000.0 m² s⁻²
julia> x^2*0.3 == x2 # Same as if we did it in Unitful
true
This means you could use Unitful as a user interface, for taking inputs from its vast catalog of physical units, and then DynamicQuantities.jl for type-stable calculations where needed.
Vectors
Because the types are stable you can have mixed units in a vector:
julia> v = [Quantity(randn(), mass=rand(0:5), length=rand(0:5)) for _=1:5]
5-element Vector{Quantity{Float64}}:
2.2054411324716865 𝐌 ³
-0.01603602425887379 𝐋 ⁴ 𝐌 ³
1.4388184352393647
2.382303019892503 𝐋 ² 𝐌 ¹
0.6071392594021706 𝐋 ⁴ 𝐌 ⁴
Internals
DynamicQuantities.jl is only ~300 lines of code. The main two types, Dimensions
, and Quantities
, are simple structs:
import Ratios: SimpleRatio
const R = SimpleRatio{Int} # Faster Rational{Int}
struct Dimensions
length::R
mass::R
time::R
current::R
temperature::R
luminosity::R
amount::R
end
struct Quantity{T}
value::T
dimensions::Dimensions
valid::Bool
end
The fields of Dimensions
store the powers of each physical dimension of the 7 used in the SI system.
The rest of the library is just setting up helper utilities and overloading math operators.
The library resulted from encouragement from others in this discussion. I thought I’d share it broadly now that it seems to be working. Try it out with
] add https://github.com/SymbolicML/DynamicQuantities.jl
(Pkg registration t-3 days)
I want to also warmly welcome contributors to this package. These sorts of units packages seem pretty general so I am happy to consider suggestions/PRs.