FlexUnits.jl 0.3.0, Support for Unitful-like inference

After attempting compatiblity with DifferentialEquations.jl and achieving some successful results, I think it’s best if I follow Unitful’s convention to subtype Quantity to Number and add a new FlexQuantitiy type that can contain other types.

1 Like

After digging into some of the existing problems with unit packages and linear algebra, I figured out some interesting solutions which are planned for the next major release. I’ll be testing against some examples with DifferentialEquations.jl (I’m already successful with mixed units and the Tsit5() solver) and hopefully with better LinearAlgebra support, I’ll be able to support more solvers.

2 Likes

Just quickly dropping by to say that yes, as the maintainer of DynamicQuantities.jl, I’m happy to support this too, via making DQ.Quantity a subtype of some UnitsBase abstract type.

5 Likes

And abstact quantit supertype might be even better than a trait-based approach. We currently need to dispatch to RealOrRealQuantity quite often in several physics packages I’m involved in (lot’s of code that can take real numbers with and without units, be really shouldn’t be given complex numbers and so on). So having a kind of AbstractQuantity (enabling RealOrRealAbstrastQuantity and the like), supported by all unit packages, would be awesome.

Oh I wish the default/the only quantity type just was <: Real… Would solve quite a few inconveniences.
Don’t remember seeing strong arguments against, other than “unitful quantities have been <: Number for ages”.

E.g. Electrical Engineering commonly works with complex units, like complex impedance.

@Eben60 sure, complex numbers with units are often useful – same as vectors with units, or quaternions with units, or a bunch of other objects.
But how is that related to Quantity <: Real vs current Quantity <: Number? If anything, having Quantity <: Real would make it easier to support complex unitful numbers – it would just work automatically, with Complex{Quantity}.

1 Like

I actually tried making Quantity something like Quantity{<:Real, <:AbstractUnitLike} <: Real but it was way more trouble than it was worth in my experience (tons of ambiguities, conflicts with dual numbers) so I dropped it. I generally find that this isn’t an issue because there’s two patterns I use to deal with this:

  1. I can always make my code support “Number”, and when I do, I write it with the express intent to support quantities and complex numbers.
  2. If there’s a third party function that takes “Real” arguments, I simply ustrip the inputs to SI, run it on raw Real numbers, and then add the expected output dimensions at the end (because the result will be in SI). Even when I supported Real, attempting to put units into these functions failed 90% of the time, so I had to use this pattern anyway.

In fact, my next major release is going to add objects that make Pattern 2 widely used for

  • Linear algebra (because it’s significantly faster and more reliable)
  • Automatic differentiation (because it’s necessary to make it work)
  • Generic nonlinear functions (because it’s often necessary for black box models)

For most cases, Pattern 2 is the way. Is there any reason why this doesn’t work for you?

“My code” is totally fine indeed: it can be made as generic as needed.

That works, of course, but loses a huge part of Unitful appeal – “everything” just works with unitful values.
There is lots of Julia code in the wild that is restricted to Real. Two examples I remember out of my head, that would 100% make sense for Unitful:

  • Base.searchsorted(::AbstractRange{<:Real}, ::Real) is faster than the generic AbstractArray fallback because it does division instead of binary search
  • StatsBase.weights requires AbstractVector{<:Real}

That statement right there runs counter to most of my experience, especially with mixed units which are a nightmare to deal with in Unitful. Moreover, mixed units are the only case I really care about because when units are all the same, the answer is obvious. This is why I wrote FlexUnits; to be able to support more of Julia with mixed-unit functionality. Because even with algorithms being compatible with Number, they fail more often than not. Real is even worse, because when people write code in Real, they seldomly think about units. To most Julia programmers, the line between pure black-box numeric code and code that supports units is drawn between Real and Number, or even between Number and Any.

Sometimes interesting how different can experiences be :slight_smile:

What exactly do you mean by “mixed units”? Unitful preserves units as-is – stuff like “erg/s” remains in these units as long as possible even though it mixes si/non-si; and even “cm^3/pc” remain, which isn’t just si + nonsi, but also the same dimension in both numerator and denominator.

Citation needed :slight_smile: A lot of Number code just works with Unitful (that’s a major part of what makes Unitful a big success story of Julia).
And there are quite a few functions/types in the wild that are restricted to <: Real and cannot take Quantities – but otherwise would just work, if not for this type constraint.

I wonder if you have some examples of code that silently fails with Unitful – ie, without throwing an error?

It isn’t about silently throwing errors, it’s about effort vs. reward. Basically, disambiguating “Number” and “Real” is much easier than disambiguating “Real” and all its subtypes, especially since there’s a lot of other important Real types that are not present in Base (like dual numbers). If I simply subtype “Number”, I only have to case out Complex; otherwise, I have to case out all the subtypes of Real and repeat a lot of code for all of these cases in additional extensions (especially with dual numbers) to get rid of ambiguities and excessive invalidations. It truly is a lot of work to do this, and considering that most code which restricts to “Real” isn’t written to support quantities with units anyway, most of the time you don’t get any benefit for this additional work and maintenance burden.

Even in my own code I tend to do follow this convention for Real; for example, in polynomial regression. I do find it comforting to be able to draw the line at Real when it comes to expectations toward supporting units.

If this is really important to you, maybe you should fork Unitful.jl and prove out that it can be done without arduous amounts of effort. I tried this and failed. Maybe you can succeed.

“Silently failing” and “throwing errors” are really the opposite ends of a behavior spectrum, not the same thing :slight_smile:

Thanks, that can be a totally reasonable argument for “why not make units <: real”. Do you know why this disambiguation is easier for numbers vs reals? There are clearly more Number subtypes in the ecosystem than Real subtypes…

Not direct subtypes. There is only two direct subtypes of Number in Base, which are “Complex” and “Real”. Moreover, I don’t care about supporting “Number” subtypes outside of “Base”, and FlexUnits.jl won’t work with those unless someone builds an extension. There are a number of direct “Real” subtypes outside of Base that I care about, particularly “Dual”; this is particularly difficult to step around. This is precisely the reason I dropped Real support. If I simply make Quantity a Number, and disambiguate it from all Real numbers, then “Dual” numbers simply work with little to no effort. Note however, you can’t autodiff quantities, you can only autodiff THROUGH quantities by modifying the values. When trying to support Real, I still had to do this PLUS define all the disambiguating methods which are a LOT.

I see, basically the reason you found it easier to disambiguate vs Number is just because there are fewer subtypes of Number that you care about – than similar subtypes of Real.
Makes sense practically, of course!

The other thing to note is that if all quantities are generic, it’s actually pretty easy to wrap these methods as well. For example, in the current iteration of FlexUnits, this works:

Base.searchsorted(v::Quantity{<:AbstractVector{<:Real}}, x::Quantity{<:Real}) = searchsorted(ustrip(v), ustrip(unit(v), x))

julia> searchsorted((1:5)*u"m/s", 3*u"m/s")
3:3

Yes, this means I need to build functions to wrap all possible functions, but I’d have to do that anyway if I subtyped to Real to disambiguate. There’s no free lunch for me and it’s far easier for end users to patch individual functions in their code than it is to disambiguate someone else’s code.

To make things worse, in future versions, the first argument would need to be FlexQuant or QuantUnion because I ended up having to subtype Quantity to Number in order to make a lot of other things (like DifferentialEquations) work. This is why I wish the convention was not to subtype Quantity to anything at all or some way to split the supertyping so that

Quantity{<:Any} <: Any
Quantity{<:Number} <: Number
Quantity{<:AbstractArray{T}} <: AbstractArray{T}

but I don’t think splitting the supertyping like this would be easy to implement.