FlexUnits.jl The flexible units package

A units package that uses a very similar mechanism for unit tracking as DynamicQuantities.jl, but has an implementation that is more similar to Unitful.jl. The main difference comes from design decisions centered around flexibility and ambiguity avoidance, which are issues that sometimes come up when I build industrial solutions with existing units packages.

The main design choice that enables flexibility comes from its implementation which is similar to DynamicQuantities.jl, where the fundamental building block is a Dimension object that tracks the (SI) dimensions of the unit. In this package, one level above a Dimension is an AffineUnit which adds a scale and an offset and can represent all commonly used units (except logscale units like decibels).

Another core design choice is to eagerly convert all values to SI units when applying mathematical operations. This is both for simplicity and performance, as all dimensions only have one SI unit representation (simplicity) and no conversion factors need to occur between units (performance). This also means that if a single quantity is used in multiple calculations, a performance boost can be achieved by calling x_si = ubase(x) and using that value, as conversions don’t need to be repeated. Results can always be converted back to desired units, and if the conversion fails due to a dimension mismatch, a helpful error is given with respect to the dimensional difference.

Another degree of flexibility is achieved in how it is unopinionated with respect to unit registries; you can easily define your own registry and use it for macros as opposed to the .UnitRegistry module (the original UnitRegistry module is only ~30 lines of code). This can be helpful if there is a conflict in the symbolic unit representation (which can happen when applying a single solution codebase to multiple clients). Because of this, the default registry isn’t automatically exported. In order to use it, UnitRegistry must be explicitly imported in order to use the common string macros:

using FlexUnits.jl, .UnitRegistry

Finally, all Quantity instances are generic and do not subtype into Real or even Number. This allows me to have generic unitful behavior on custom objects or Number types without running into ambiguities.

Major differences between FlexUnits.jl and DynamicQuantities.jl

  1. Fully supports affine units (like °C and °F) and can potentially support logarithmic units (like dB) in a separate registry
  2. The string macro u_str and parsing function uparse are not automatically exported (allowing users to export their own registries)
  3. Easier to build your own unit registry (allowing differnet behaviour for u_str and uparse)
  4. 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
  5. More closely resembles the Unitful.jl API in many ways
  6. No symbolic units (everything eagerly evaluates to SI units)
  7. The function uexpand is replaced by ubase
  8. There is no Quantity type that subtypes to Number or Real. A Quantity in FlexUnits.jl is equivalent to a GenericQuantity in DynamicQuantities.jl This is a deliberate design decision to avoid ambiguities on numerical operations with user-defined types.

Major differences between FlexUnits.jl and Unitful.jl

  1. Units are not specialized (u"m/s" returns the same concrete type as u"°C") which is much faster when units cannot be inferred
  2. The string macro u_str and parsing function uparse are not automatically exported (allowing users to export their own registries)
  3. Units are not dynamically tracked, quantities are displayed as though upreferred was called on them
  4. The function upreferred is replaced by ubase which converts to raw dimensions (like SI) which are not configurable
  5. Operations on affine units do not produce errors (due to automatic conversion to dimensional form). This may may yield unituitive (but more consistent) results.
  6. 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.
  7. Quantity in FlexUnits.jl does not subtype to Number in order to support more value types (such as a Distribution or Array)

That is awesome @Deduction42 ! :100: :juliaspinner:

What are your recommendations for porting packages from Unitful.jl to FlexUnits.jl?

We chose Unitful.jl because we needed to dispatch on specific quantities like

const Len{T} = Quantity{T,u"š‹"}
const Met{T} = Quantity{T,u"š‹",typeof(m)}
const Deg{T} = Quantity{T,NoDims,typeof(°)}
const Rad{T} = Quantity{T,NoDims,typeof(rad)}

Can we do something similar with FlexUnits.jl?

I do that all the time with dimensions actually. FlexUnits provides the @D_str macro to specify a dimension type. However, you can’t statically specify units in FlexUnits (this maintains flexibility, prevents over-specialization, and reduces compile times), units will be automatically converted to SI fundamental units.

display_simplified_units(true)

const Pressure{T} = Quantity{T, D"psi"} #Only uses dimensions of psi, converts to Pa
const Temperature{T} = Quantity{T, D"°C"} #Only uses dimensisons of Celsius, converts to K

julia> Pressure{Float64}(5u"psi")
34473.8 Pa

julia> Temperature{Float64}(5u"°C")
278.15 K

Now forcing everything to be SI units seems a bit clunky, but in a lot of my projects, I’m applying the same algorithm to different customers that use different units (especially in Canada where we’re notoriously inconsistent). One customer might have pressure in kPa while others have ounces per square inch; so I don’t know units, just dimensions. This is problematic for Unitful, because in order for a type to be concrete, you need dimensions AND units. Thus any object that contains your ā€œLenā€ object above will take a dynamic dispatch performance hit, but ā€œMetā€ will not.

julia> isconcretetype(Len{Float64})
false

julia> isconcretetype(Met{Float64})
true

However for the FlexUnits types, all you need in order to make the type concrete is the dimension

julia> isconcretetype(Temperature{Float64})
true

This allows me to achieve compiled performance with less information upfront. In reality, dimensions are more important than units. Resolving dimensions takes much more computational effort than scaling and offsetting; moreover, if you’re using base SI units, you don’t have to scale or offset anything. If you know the dimension, you can get any unit you want.

It makes a lot of sense to dispatch on dimensions only. I will explore this idea.

Other than that, I think we only rely on ustrip and unit, which I assume exist in FlexUnits.jl as well. If you would like to help with PRs, CoordRefSystems.jl and Meshes.jl are the most challenging candidates I have in mind.

Let’s see if the port Unitful.jl → FlexUnits.jl brings some improvements :slight_smile:

FlexUnits tries to match the Unitful API as much as possible. It includes ustrip and units, and you can use them in similar manners. The main difference is object types:

  • Quantity{T,U}, where U can be a dimension or a unit (AbstractUnitLike)
  • Units{D,T} where D is a dimension and T is a transform (usually AffineTransform{Float64}) the transform is a callable object that converts the units to SI base units (the transform is under the ā€œtobaseā€ field, and can be accessed via the tobase function). When converting back to units, the inverse of the transform is called. Calling FlexUnits.tobase on a Dimension produces a NoTransform. Logarithmic units have an ExpAffTransform.
  • Dimensions{FixRat32} is a dynamic dimension, you can use Quantity{T,Dimensions{FixRat32}} if you don’t know the dimensions beforehand. This is useful for matrices or dictionaries that have heterogeneous units.

Most operations give you dimensional quantities as output Quantity{T, <:AbstractDimLike}. Quantities with units usually pop up when you use uconvert. Also, make sure you use using .UnitRegistry to enable string macros and uparse etc.

Let me know if you’re having any problems with your port. I have some more examples with dispatch patterns up in the documentation now. There’s also a differential equation solver example in advanced examples that uses a clever dispatch pattern to make a vector that is unitful when fields are accessed but produces raw numerical values when indexed by number (so that the solver doesn’t require knowing units).