The flexible units package. FlexUnits.jl was heavily inspired by DynamicQuantities.jl and Unitful.jl and can be seen as a hybridization of the two. Under the hood, operations on quantities are executed similarly to DynamicQuantities.jl, allowing a single concrete unit type to represent a variety of units, but the API and unit support is designed to more closely relate to Unitful.jl. This means multiple quantities with different units can be stored inside a concretely-typed Array or Dict (which is not possible with Unitful.jl). However, unlike DynamicQuantities.jl, this package fully supports affine units (like °C and °F) and fully supports custom unit registries.
- Fully supports affine units (like °C and °F) and can potentially support logarithmic units (like dB) in a separate registry
- The string macro
u_strand parsing functionuparseare not automatically exported (allowing users to export their own registries) - Easier to build your own unit registry (allowing differnet behaviour for
u_stranduparse) - While units are pretty-printed by default, you can enable parseable unit outputs by setting
pretty_print_units(false)which is useful for outputting unit information in JSON - More closely resembles the Unitful.jl API in many ways
- No symbolic units (everything eagerly evaluates to SI units)
- The function
uexpandis replaced byubase - There is no
Quantitytype that subtypes toNumberorReal. AQuantityin FlexUnits.jl is equivalent to aGenericQuantityin DynamicQuantities.jl This is a deliberate design decision to avoid ambiguities on numerical operations with user-defined types.
- Units are not specialized (u"m/s" returns the same concrete type as u"°C") which is much faster when units cannot be inferred
- The string macro
u_strand parsing functionuparseare not automatically exported (allowing users to export their own registries) - Units are not dynamically tracked, quantities are displayed as though
upreferredwas called on them - The function
upreferredis replaced byubasewhich converts to raw dimensions (like SI) which are not configurable - Operations on affine units do not produce errors (due to automatic conversion to dimensional form). This may may yield unituitive (but more consistent) results.
- Unit registries are much simpler; a registry is simply a dict of units, all of the same type, living inside a module. Custom registries inside user-defined modules are not neccessary, but are still supported.
Quantityin FlexUnits.jl does not subtype toNumberin order to support more value types (such as a Distribution or Array)
Much like other unit packages, you can use string macros to build units and quantities. Unlike other packages, you must manually "use" the default registry UnitRegistry, this is done so as to not be overly opinionated as to what registry to use (users can create and use their own registries instead).
julia> using FlexUnits, .UnitRegistry
julia> u = u"J/(mol*K)"
J/(mol*K)
julia> R = 8.314*u
8.314 J/(mol*K)
julia> v_satp = R*(25u"°C")/(101.3u"kPa") #Temperature is auto-converted to Kelvin
0.024470079960513324 m³/mol
You can register units using other units or quantities as follows:
julia> register_unit("bbl" => 0.158987*u"m^3")
FlexUnits.RegistryTools.PermanentDict{Symbol, AffineUnits{Dimensions{FixedRational{Int32, 25200}}}} with 150 entries:
:Ω => Ω
:μs => μs
:μV => μV
However, due to the nature of macros, these dictionaries are permanent. You can re-register units with the same values (so that you can re-run scripts) but changing them is not allowed.
julia> register_unit("bbl" => 0.158987*u"m^3")
FlexUnits.RegistryTools.PermanentDict{Symbol, AffineUnits{Dimensions{FixedRational{Int32, 25200}}}} with 150 entries:
:Ω => Ω
:μs => μs
:μV => μV
julia> register_unit("bbl" => 22.5*u"m^3")
ERROR: PermanentDictError: Key bbl already exists. Cannot assign a different value.
FlexUnits.jl and Unitful.jl focus on different use cases and can be considered complementary. Unitful.jl shines when explicitly referring to units inside the code (especially at low-level operations) while FlexUnits.jl is much better at high-level operations when units tend to be unknown (for example, when parsing strings). Because of this, FlexUnits (as of version v0.2.10) provides an extension to Unitful that allows for converting between types. A useful example is converting FlexUnits.Quanity to a Unitful equivalent through uconvert. Note that because both packages have significant overlap in their function/object names, you will have to use import on at least one of the packages.
julia> using Unitful
julia> import FlexUnits
julia> import FlexUnits.UnitRegistry
julia> import FlexUnits.uconvert
julia> x = UnitRegistry.qparse.(["5.0 km/hr", "2.0 N", "10 °C"])
julia> velocity = uconvert(u"km/hr", x[1])
18.0 km hr^-1
julia> force = uconvert(u"N", x[2])
2.0 N
julia> temperature = uconvert(u"°F", x[3])
49.99999999999994 °F
This pattern would be useful when performing low-level calculations on force, velocity and temperature inside a function that takes a mixed-unit vector. Similarly, if one wishes to collect results of dissimilar units, one can simply output them as a type-stable FlexUnit vector
julia> x_out = [FlexUnits.Quantity(velocity), FlexUnits.Quantity(force), FlexUnits.Quantity(temperature)]
3-element Vector{FlexUnits.Quantity{Float64, FlexUnits.Dimensions{FlexUnits.FixedRational{25200, Int32}}}}:
1.3888888888888888 m/s
2.0 (m kg)/s²
283.15 K
Using both packages together should feel natural due to their similar API and can provide the best of both worlds. However, these similarities also means that care must be taken to manually import any functions that needed from both packages.
FlexUnits.jl and DynamicQuantities.jl both greatly outperform Unitful.jl when the compiler cannot infer the units.
using FlexUnits
using .UnitRegistry
import DynamicQuantities
import Unitful
using BenchmarkTools
v1uni = [1.0*Unitful.u"m/s", 1.0*Unitful.u"J/kg", 1.0*Unitful.u"A/V"]
v1dyn = [1.0*DynamicQuantities.u"m/s", 1.0*DynamicQuantities.u"J/kg", 1.0*DynamicQuantities.u"A/V"]
v1flex = ubase.([1.0u"m/s", 1.0u"J/kg", 1.0u"A/V"])
@btime sum(x->x^0.0, v1uni)
7.850 μs (86 allocations: 3.92 KiB)
@btime sum(x->x^0.0, v1dyn)
105.173 ns (1 allocation: 48 bytes)
@btime sum(x->x^0.0, v1flex)
106.882 ns (1 allocation: 48 bytes)
Notice the 'μ' instead of the 'n' on the Unitful result, FlexUnits and DynamicQuantities both offer a ~75x speedup in this case (where unit type cannot be inferred). In the case where all types can be inferred, performance is more or less the same in terms of execution time (but Unitful allocates fewer bytes).
t1uni = [1.0*Unitful.u"m/s", 1.0*Unitful.u"m/s", 1.0*Unitful.u"m/s"]
t1dyn = [1.0*DynamicQuantities.u"m/s", 1.0*DynamicQuantities.u"m/s", 1.0*DynamicQuantities.u"m/s"]
t1flex = ubase.([1.0u"m/s", 1.0u"m/s", 1.0u"m/s"])
@btime sum(x->x*x, t1uni)
86.902 ns (1 allocation: 16 bytes)
@btime sum(x->x*x, t1dyn)
86.472 ns (1 allocation: 48 bytes)
@btime sum(x->x*x, t1flex)
86.260 ns (1 allocation: 48 bytes)