DynamicQuantities.jl v0.7.0: efficient and type-stable physical quantities

Some more updates as of v0.10

New typing system

There are now multiple quantity types which act identically, but let you work with different subtypes:

  • Quantity <: AbstractQuantity <: Number
    • The default quantity returned by 0.5u"m/s", which is subtyped to Number.
  • GenericQuantity <: AbstractGenericQuantity <: Any
    • Do you need to put units on some non-numerical type? Then you want GenericQuantity
  • RealQuantity <: AbstractRealQuantity <: Real
    • Many packages throughout Julia require Real input. Although physical quantities are not technically real, it can still be useful for compatibility to pass them as if they were. RealQuantity lets you do that.
  • All three of these are of course compatible with QuantityArray, and can take any AbstractDimensions to store the dimensions.
julia> x = 0.5u"m/s"
0.5 m s⁻¹

julia> x_re = RealQuantity(x)::Real
0.5 m s⁻¹

julia> y_gen = GenericQuantity("hello", length=1, time=-1)
(hello) m s⁻¹

We also get automatic promotion:

julia> x = RealQuantity(0.5u"m/s")
0.5 m s⁻¹

julia> y = x * (1 + 2im)
(0.5 + 1.0im) m s⁻¹

julia> typeof(y)
Quantity{ComplexF64, Dimensions{DynamicQuantities.FixedRational{Int32, 25200}}}

the behavior of which can be overloaded for any custom hierarchy of quantity types.

Thanks to Guarav Arya for his help on this.

New methods

Many other numerical methods which require dimensionless numbers, such as exp or sin, can now take a quantity as input (required because DynamicQuantities.Quantity can be dimensionless - it doesn’t automatically convert to Float64 due to the potential for type instability).

The overloaded method for dimensionless functions will simply check if the quantity is dimensionless or not, and then evaluate:

julia> x = 0.5u"m"
0.5 m

julia> y = 10u"Constants.Mpc"
3.085677581491367e23 m

julia> exp(x/y)
1.0

This means we can also have ranges of quantities now (via Base.rem)

julia> for x in 0u"km":1.5u"km":10u"km"
           @show x^2
       end
x ^ 2 = 0.0 m²
x ^ 2 = 2.25e6 m²
x ^ 2 = 9.0e6 m²
x ^ 2 = 2.025e7 m²
x ^ 2 = 3.6e7 m²
x ^ 2 = 5.625e7 m²
x ^ 2 = 8.1e7 m²

(Although this should probably use a QuantityArray… PRs always appreciated )

Better perf

QuantityArray performance seems pretty solid now, and lets you wrap arrays of arbitrary data with quantities via GenericQuantity. Here’s summation on a regular array compared to an array of physical quantities:

julia> struct Coords
           x::Float64
           y::Float64
       end

julia> Base.:+(a::Coords, b::Coords) = Coords(a.x+b.x, a.y+b.y)

julia> Base.:*(a::Coords, b::Number) = Coords(a.x*b, a.y*b)

julia> Base.:*(a::Number, b::Coords) = Coords(a*b.x, a*b.y)

julia> @btime(
    sum(array),
    setup=(N=1000; array=[Coords(rand(), rand()) for i=1:N]
)
  828.000 ns (0 allocations: 0 bytes)
Coords(509.5939555433635, 494.56896926222123)

julia> @btime(
    sum(array),
    setup=(N=1000; array=QuantityArray([GenericQuantity(Coords(rand(), rand()), dimension(u"m/s")) for i=1:N])
)
  843.750 ns (0 allocations: 0 bytes)
(Coords(504.7718424677944, 482.0449323023619)) m s⁻¹

Thanks to all the contributors thus far :smiley:

22 Likes