Unitful.jl without parametrizing types on units?

Thanks for doing this! I plan to test it out and recommend it over unitful for anything in SciML. I’ve been waiting for something like this for a long time and never got around to it, and so I’m very happy to see it finally exist.

3 Likes

Awesome, let me know if you have any suggestions for it :slightly_smiling_face:

1 Like

Just added a Unitful.jl extension too. So one can use Unitful.jl as a user-facing interface with its catalogue of units, and then DynamicUnits.jl for internal calculations:

julia> using Unitful: Unitful, @u_str

julia> x = 0.5u"km/s"
0.5 km s⁻¹

julia> y = convert(DynamicUnits.Quantity, x)
500.0 𝐋^1 𝐓^(-1)

julia> y2 = y^2 * 0.3
75000.0 𝐋^2 𝐓^(-2)

julia> x2 = convert(Unitful.Quantity, y2)
75000.0 m² s⁻²

julia> x^2*0.3 == x2
true

(It uses Unitful.upreferred to reduce a quantity to a standard form before stripping units, which seems to be fairly robust)

3 Likes

Awesome package, thanks! As long as it is not registered, we may wonder for a better meaningful name, eg I think of DimensionalUnits as you mention it works with Dimensions?

2 Likes

Thanks!

I am open to name suggestions.

To give backstory, here’s my original motivation for “DynamicUnits”:

Dynamic:

  1. Unitful.jl has dimensions as well; what differentiates this package is that the dimensions/units can change during runtime. “Dynamic” seemed like a good way to describe this.
  2. DynamicExpressions.jl is the cousin package and basically does the same thing for equations: it creates type stable symbolic expressions that can change during runtime without additional evaluations.

Units:

  1. Though it deals with physical dimensions, so long as you use a standardized unit system (such as I use when converting to/from Unitful - with upreferred), it is essentially the same thing. i.e., I guess I would argue that a “unit” is a physical dimension simply attached to a normalization factor.
    • It should be noted if not obvious from above that DynamicUnits tracks the quantity value as well as the dimensions. So you can totally convert to a unit system afterwards without losing any information.
  2. Subjectively, “dimension” seems like overloaded of a term, whereas “Unit” is associated to physical quantities. It also perhaps makes it easier “DynamicUnits” join into the Unitful.jl ecosystem of packages (which are pretty much all *Unit*.jl)

What do you think?

Regarding the name, I was thinking DimensionalQuantities.jl makes more sense for what it currently offers, given that there are no actual units. Obviously, this could evolve into something bigger though.

Thanks for putting this together.

2 Likes

That’s a good idea too. Actually I might change it to that…

I guess if the name is “Units,” people might assume it stores a catalog of units (maybe units that associated to dynamical systems…), like other packages in the Unitful.jl ecosystem. Whereas DynamicQuantities.jl is more obvious that it is an entire quantity type altogether.

Will think more about this

Given that a quantity is a more fundament concept for which units are based, it makes sense to me to have a package that defines a quantity type(s) that a separate unit package would build upon.

The use of “units” in the title is a double edge sword. It would make it easy for people to find, but I think most users would expect to find something that propagates units like Unitful or Pint in python

1 Like

Good point!

Actually I think I misread your suggestion initially. I had read it as “DynamicQuantities.jl” (which I do quite like and think I might switch to…). “DimensionalQuantities.jl” sounds like a bit of a tautology, maybe? But I think the “Quantities” part of the title is good.

How do error messages look like with your package? Less monstrous than those produced by using Unitful ?

I don’t do much for errors. The one thing I’d note is that instead of Unitful.DimensionError, I return the quantity as normal and set the (q::Quantity).valid flag to false. This is to remain consistent with the theme of type stability. That’s it though; it’s a pretty lightweight package: only ~300 SLOC.

(MethodError will still occur if a method is not defined for the Quantity type.)

This error return inspired by GitHub - JuliaPreludes/Try.jl: Zero-overhead and debuggable error handling. From the discussion on Performance of hasmethod vs try-catch on MethodError it seems like try-catch on a DimensionError is bound to be slower than tracking an extra value for errors in calculation.

This looks great! One question I have is whether Dimensions should have a type parameter which would let users with simple units use an Int8 per dimension rather than a SimpleRatio{Int}. Doing this would shrink a Dimensions object down from 896 bits to 56 bits (64 with padding), and would also let you compare Dimensions in a single instruction. For similar reasons, I think that Dimmensions should probably default to SimpleRatio{Int8}. I don’t think people commonly need more than 127 for their SI units

1 Like

If there is interest I would definitely welcome a PR on this. It may make the code a bit heavier but I think it is worth it:

I think exploring other types for the dimensions struct is a great idea in general. Even switching to @tim.holy’s SimpleRatio{Int} results in massive speedups compared to Rational{Int}. Now a benchmark against a compiled Unitful.jl method is only a 3x factor slower:

julia> dyn_uni = Quantity(0.2, mass=1, length=0.5, amount=3)
0.2 𝐋 ¹ᐟ² 𝐌 ¹ 𝐍 ³

julia> unitful = convert(Unitful.Quantity, dyn_uni)
0.2 kg m¹ᐟ² mol³

julia> g(x) = x ^ 2 * 0.3;

julia> @btime g($dyn_uni);
  6.083 ns (0 allocations: 0 bytes)

julia> @btime g($unitful);
  1.958 ns (0 allocations: 0 bytes)

And the non-compilable version is now >3000x faster than Unitful:

julia> f(x, i) = x ^ i * 0.3;

julia> @btime f($dyn_uni, 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.)

The package looks really nice - great initiative. I personally like the name DimensionalQuantities.jl, I don’t think it’s much of a tautology.

1 Like

Using @btime randn(5) .* Dimensions(mass=2/5, length=2) as a benchmark, I see a pretty big improvement going to Int8 instead of Int. It won’t make much difference for scalars, but for vectors, it cuts allocations by 75% and time by 19%.

#before:
julia> @btime randn(500) .* Dimensions(mass=2/5, length=2);
  17.086 μs (1012 allocations: 145.22 KiB)
# after:
julia> @btime randn(500) .* Dimensions(mass=2/5, length=2);
  13.984 μs (1011 allocations: 39.77 KiB)
1 Like

Is there a way to do type-stable units without constraining the dimensions to a fixed set? I like to create dimensions like NumberOfBooks and calculate

(10 book / shelf) * (8 shelf / case) * 3 case

with little syntactic overhead.

That would require a Dict rather than a struct and would as a result be a good bit slower.

1 Like

Very nice!


Thanks for the comments everyone. I put up a new thread specifically on the package here: [ANN] DynamicQuantities.jl: type stable physical quantities

1 Like

Any chance we can get a moderator to move the messages about DynamicQuantities.jl to the new thread?

1 Like