DynamicQuantities.jl v0.4 is now a one-stop shop for physical quantities, with a built-in units parser available. Thanks to @odow@Oscar_Smith@mcabbott@j-fu for review and feedback.

You can create a quantity with the @u_str macro, like so:

julia> 0.2u"m^0.5 * kg * mol^3"
0.2 m¹ᐟ² kg mol³
julia> randn(3) .* u"GHz"
3-element Vector{Quantity{Float64, DynamicQuantities.FixedRational{Int32, 25200}}}:
1.558255538781646e9 s⁻¹
-1.0549637909362864e8 s⁻¹
1.392099679975791e9 s⁻¹
julia> u"m/s^0.5"
1.0 m s⁻¹ᐟ²

The “dimensions” are now printed in terms of the SI base units to avoid misinterpretation.

These are the available units:

Base:

m: Length in meters. Available variants: fm, pm, nm, μm (/um), cm, dm, mm, km, Mm, Gm.

g: Mass in grams. Available variants: μg (/ug), mg, kg.

s: Time in seconds. Available variants: fs, ps, ns, μs (/us), ms, min, h (/hr), day, yr, kyr, Myr, Gyr.

A: Current in Amperes. Available variants: nA, μA (/uA), mA, kA.

K: Temperature in Kelvin. Available variant: mK.

cd: Luminosity in candela. Available variant: mcd.

mol: Amount in moles. Available variant: mmol.

Derived:

Hz: Frequency in Hertz. Available variants: kHz, MHz, GHz.

N: Force in Newtons.

Pa: Pressure in Pascals. Available variant: kPa.

J: Energy in Joules. Available variant: kJ.

W: Power in Watts. Available variants: kW, MW, GW.

C: Charge in Coulombs.

V: Voltage in Volts. Available variants: kV, MV, GV.

F: Capacitance in Farads.

Ω: Resistance in Ohms. Available variant: mΩ. Also available is ASCII ohm (with variant mohm).

T: Magnetic flux density in Teslas.

L: Volume in liters. Available variants: mL, dL.

bar: Pressure in bars.

In addition to @u_str and uparse which evaluate expressions in this namespace, you can also access these explicitly with, e.g., DynamicQuantities.Units.kW. All of these “units” are of type Quantity{Float64,FixedRational{Int32,25200}}, and uparse will return quantities of this type, but you can convert as needed with convert(::Type{Quantity{T,R}}, ...).

The Unitful.jl integration is now just for convenience, but is unnecessary for parsing user input.

Regarding encoding units in the “type” domain vs the “value” domain, I wonder about a design that makes it possible to have the best of both worlds. For example, removing the constraint R <: Real in

struct Dimensions{R}
length::R
mass::R
time::R
current::R
temperature::R
luminosity::R
amount::R
end

You could define a dimension of length dynamically as Dimensions(1, 0, 0, 0, 0, 0, 0)or statically as e.g. Dimesnions(Val(1), Val(0), Val(0), Val(0), ...).

That could be interesting to try. As a package developer, you could test a function for type stability, and if it is, you could switch to using a TypedDimension object. As to “Why not use Unitful.jl”, perhaps it’s nicer to have a way of easily switching back and forth between typed and untyped quantities, in case you find your code turns out to be type stable? But maybe I should just try to maintain full API compatibility with Unitful in this case so you could use that instead of duplicating efforts.

For example, here’s a custom quantity with only length/mass/time dimensions:

julia> using DynamicQuantities
julia> struct MyDimensions{R} <: AbstractDimensions{R}
length::R
mass::R
time::R
scale::R
end
julia> struct MyQuantity{T,R} <: AbstractQuantity{T,R}
value::T
dimensions::MyDimensions{R}
end
julia> x = MyQuantity(1.2, time=5//2, scale=3)
1.2 s⁵ᐟ² scale³
julia> x^2
1.44 s⁵ scale⁶

All the math and utilities are defined on the abstract types (and use fieldnames to find the dimensions), so everything should just work.

The other thing you would need is overload getproperty to get dimensions from the type Val{length} instead of from the value, and everything else should work.

That is an awesome initiative, thanks for working on it.

I wonder what are your thoughts on scientific types besides the dimensions? The ScientificTypes.jl package has many issues that I am planning to address, but it has the scitype trait function that queries if a quantity is “Continuous”, “Count”, … This is very useful in statistical work and often ignored in type systems that absorb data from users.

If we could somehow integrate these worlds into a single world with dimensions and scientific interpretation, that would be great. Any chance a trait like scitype could be added to the dynamic quantities? If a quantity is m/s we automatically know it is a Continuous variable for example.

Sure, happy to explore putting it in as an extension! Would you want all quantity objects to be “Continuous”? Or would use of the mol be “Count”? What if it is a fraction of a mol? I guess if that would be the case, things get tricky, because the physical dimensions are not revealed in the type – so any trait would technically be a union of continuous/count.

I guess it’s also a bit difficult to nail down a single definition because it’s also context-dependent. s⁻¹ could be a “count” if you are counting discrete things per second. But it could also be the frequency of radiation, which is continuous.

That is an interesting observation. I would always assume that rates or frequencies are continuous variables. The count variable here would be the numerator of the rate variable.

Actually, good point, it would be continuous either way there.

I suppose if and only if uamount(::Quantity) > 0 (the dimension corresponding to mol) while every other dimension is exactly 0, it would it be a “Count”, while everything else would be Continuous? Or are there other types?

mol is 6e22 molecules and can be considered continuous in most practical cases. C is 6e18 elementary charges, and in most cases can be considered contimuous, too (though in many other cases physics deals with elementary charges). All other units are “even more” continuous, aren’t they?

I think I would agree with you. I guess it depends on what these terms are supposed to mean in the package. @juliohm?

Note also that the Quantity type has an arbitrary element type (i.e., Quantity{T}). So you could store integers in a Quantity for example. Maybe if you are counting 1 kg weights, then it makes sense to treat it as a count.

In other words, maybe

scitype(::Type{Q}) where {Q<:AbstractQuantity{<:Integer}} = Count
scitype(::Type{Q}) where {Q<:AbstractQuantity{<:AbstractFloat}} = Continuous

makes the most sense?

But this is difficult because quantities are defined in the base SI units. So you would not be able to “count” milliseconds if we take this type definition…

The general use of these scientific types is to decide whether or not we can take means, standard deviations, and other statistics that are only valid with continuous variables. Whenever we get categorical variables such as group labels we cannot perform certain types of analysis and need to do something else instead. If the type system could provide us with this information, we could then automate a bunch of transformations with mixed continuous and categorical variables.

@juliohm I finally got around to making a PR to add scitype; please take a look!

It just forwards scitype to the value. If you want to use integers for quantities then the scitype will be Count, which I think sort of feels semantically best. Thoughts?

using DynamicQuantities
using ScientificTypes
x = 1.0u"m/s"
@test scitype(x) <: Continuous
@test scitype([x]) <: AbstractVector{<:Continuous}
@test scitype(randn(32) .* u"m/s") <: AbstractVector{<:Continuous}
# Converted to integer 1 m/s => Count
@test scitype(Quantity{Int}(x)) <: Count
X = (; x=randn(32) .* u"m/s")
@test scitype(X) <: Table{<:AbstractVector{<:Continuous}}
sch = schema(X)
@test first(sch.names) == :x
@test first(sch.scitypes) == Continuous
@test first(sch.types) <: Quantity{Float64}