Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Quantities should be <: Real? #680

Open
aplavin opened this issue Sep 2, 2023 · 17 comments
Open

Quantities should be <: Real? #680

aplavin opened this issue Sep 2, 2023 · 17 comments
Labels

Comments

@aplavin
Copy link
Contributor

aplavin commented Sep 2, 2023

Currently, Unitful quantities are <: Number but not <: Real. This hinders composability with other libraries.

Just a few examples for methods that accept Reals and don't work with units - but actually should and totally make sense:

  • Base.searchsorted(::AbstractRange{<:Real}, ::Real) does division instead of performing binary search - so, it's faster than AbstractArray fallback
  • StatsBase.weights require AbstractVector{<:Real}

Surely there are much more.
Note that these methods are correctly restricted to reals: they don't make sense for complex numbers.

Why not make Quantity <: Real, and use Complex{Quantity} for complex unitful values?

@giordano
Copy link
Collaborator

giordano commented Sep 2, 2023

I'm sympathetic to this proposal, I also think Quantity <: Number instead of <: Real wasn't necessarily good.

@giordano giordano added the v2.0 label Sep 2, 2023
@timholy
Copy link
Contributor

timholy commented Sep 3, 2023

It goes both ways:

  • one of the most important properties of Real is "can be ordered," and I'm in the camp that thinks that is reasonable for quantities (although it has been debated in literature on this topic).
  • on the other hand, I'll wager you there are lots of places where one expects that Real can be converted to AbstractFloat, and there are extremely good reasons that doesn't work for Quantities.

@timholy
Copy link
Contributor

timholy commented Sep 3, 2023

The alternative, of course, is to weaken the type-constraints on the functions you want to call. I don't really understand why StatsBase has so many type-constraints.

@aplavin
Copy link
Contributor Author

aplavin commented Sep 3, 2023

Both methods I listed (from Base and StatsBase) are fundamentally right in their restriction to Reals: it's not clear what searchsorted should do on complex range at all, and stuff like quantile(X, weights(W)) is also hardly definable for complex W.
So I think these methods are correct in their Real restrictions - it's Unitful values that should Really be Real.

@timholy
Copy link
Contributor

timholy commented Sep 4, 2023

But we have lots of things that are duck-typed, why is that wrong? Often there is no decent way of specifying what types of arguments this should be expected to work on.

@aplavin
Copy link
Contributor Author

aplavin commented Sep 11, 2023

Maybe the same behavior and reliability is possible without Real restrictions, I don't know...
If that's the case, Base should really lead "duck-typing", so that searchsorted on a range of Unitful values is as performant as on a range of regular floats ($O(1)$ instead of $O(\log N)$ for unitful currently). Then other packages are more likely to follow.

@MilesCranmer
Copy link

MilesCranmer commented Nov 21, 2023

Was discussing this recently too here. Here's my very subjective take.

Is it semantically correct of a physical quantity as a real number? One could indeed argue that 1.0 meters is a type of number... But is it a real?

Let me give an example. Does the statement:

$0.5 \text{ km} \text{ s}^{-1} \in \mathbb{R}$

feel semantically correct? This is sort of what Quantity <: Real would be stating, if you think of Real as real numbers.

To me it feels more natural to instead state:

$0.5 \text{ km} \text{ s}^{-1} \in \mathbb{R} \times \mathbb{U}$

for some abstract space $\mathbb{U}$ of all possible physical dimensions (or, maybe it's like a measure). I could think of $\mathbb{R} \times \mathbb{U}$ as a space of "numbers", similar to how you can think of $\mathbb{C}$ as $\mathbb{R} \times \mathbb{R}$ – complex numbers are "numbers", but they are not "real".

All of this is to say that it's probably best to keep it Number, as it carries a sufficiently abstract connotation, and have downstream packages open up their type requirements to allow more abstract inputs.

Otherwise it is a bit like creating a complex number type that is <:Real for the purpose of passing complex numbers to methods that are ::Real. I think it's just a bit dangerous in general, as there could be a variety of assumptions that don't work for more abstract numbers like complex or physical quantities.

@aplavin
Copy link
Contributor Author

aplavin commented Nov 21, 2023

This may be true from the mathematical purity PoV, but pragmatically in Julia:

  • Some methods only make sense for "real-ish" numbers, not complex numbers, and have signatures defined as such (two examples in this thread). Still they are totally reasonable to apply to "real number + unit".
  • Lots of types are <: Real that are mathematically not – but it can be a better choice for actual usage. Stuff like dual numbers, intervals, infinities, numbers with uncertainties (some such types aren't <: Real though). Unitful numbers would be a great fit here :)

@MilesCranmer
Copy link

One other option is to basically have Unitful do what DynamicQuantities does with respect to the type hierarchy to have pseudo multiple-inheritance.

After SymbolicML/DynamicQuantities.jl#85 merges, you will be able to have quantities which are either <:Real, <:Number, or <:Any, by using a RealQuantity, Quantity (default), or GenericQuantity, respectively. Promotion rules are defined here so that RealQuantity * Complex -> Quantity, etc.

So the default is the (imo, safer) Quantity<:AbstractQuantity<:Number type hierarchy. But for DynamicQuantites users who really know what they are doing, they can now use RealQuantity<:AbstractRealQuantity<:Real — thus allowing packages which require Real inputs. So maybe Unitful could do something similar?

This works by defining all quantity methods on Union{AbstractQuantity,AbstractRealQuantity,…}, rather than on an single abstract type. However there are many method ambiguities that pop out of this, so for some methods, I need to loop over the abstract types and call @eval.

However it's a lot refactoring, and a lot of disambiguating methods. And start time seems to takes a hit (haven’t figured out how to “heal” method invalidations coming from within a @eval yet… not sure it’s possible). So may not be possible or practical for a mature package like Unitful without a lot of work. But I think it’s preferable to making Quantity <: Real which imo tilts the balance between convenience and safety too far in one directions (subjective).

It’s not so much the mathematical purity directly, it’s more the indirect effects that matters. If people are used to thinking of Real input as a literal real number, they may make assumptions (like any Real can be strictly compared with any other Real), and breaking those may introduce some sort of silent bugs through ecosystems.

@sostock
Copy link
Collaborator

sostock commented Nov 21, 2023

Lots of types are <: Real that are mathematically not – but it can be a better choice for actual usage. Stuff like dual numbers, intervals, infinities, numbers with uncertainties (some such types aren't <: Real though). Unitful numbers would be a great fit here :)

In my opinion, these examples are different from quantities and it makes more sense that they are <:Real than it does for quantities. The difference is that they actually lie somewhere on the real number line, e.g.:

  • DualNumbers: Every dual number corresponds to a real number. Dual(1, 2) occupies the same spot as 1 on the real number line, it just adds an infinitesimally small part 2ε that can be discarded to get a real number. Indeed, Dual(1,2) == 1 is true.
  • IntervalArithmetic: 5 ± 2 doesn’t correspond to a single number, but an interval. But it is still an interval on the real number line. For example, I can add a real number to an interval: (5 ± 2) + 1 == (6 ± 2).

Dimensionful quantities on the other hand are not a part of the real number line and some things that you can do with real numbers are not possible with quantities: What should 1m + 1s return? Is 1m < 5s?

If we make AbstractQuantity <: Real, we significantly reduce the number of properties that people can expect to hold for Real numbers. But that doesn’t mean that we cannot do it. In some places we loosen the notion of real numbers already when it is convenient (e.g., Base.isreal(1m) == true, even though 1m is not equal to any real number).

@aplavin
Copy link
Contributor Author

aplavin commented Nov 21, 2023

In my opinion, these examples are different from quantities and it makes more sense that they are <:Real than it does for quantities. The difference is that they actually lie somewhere on the real number line, <...>

That's one specific property - choose another and unitful numbers start looking closer to reals than those other types.

If we make AbstractQuantity <: Real, we significantly reduce the number of properties that people can expect to hold for Real numbers.

It's pretty hard to further reduce the number of properties given the wide range of types that are already Real :)

Anyway, I see there's no consensus on unitful numbers being Real - unfortunately.
This is one of the two pain points I have with Unitful: that quantities cannot be used with functions/methods taking Reals (there are many, and for a good reason). Another is that angles are considered dimensionless and it very easily leads to errors.

@sostock
Copy link
Collaborator

sostock commented Nov 22, 2023

That's one specific property - choose another and unitful numbers start looking closer to reals than those other types.

What other properties do you have in mind here? I’m genuinely curious.

It's pretty hard to further reduce the number of properties given the wide range of types that are already Real :)

Do you have some examples for those types as well? I think DualNumbers and Infinities don’t really reduce the properties of real numbers that much. I can’t think of an operation that I can do with real numbers but not with dual numbers or infinity.

@sostock
Copy link
Collaborator

sostock commented Nov 22, 2023

In my opinion, if we want to make AbstractQuantity <: Real, we need to ensure that all Base operations that take ::Real arguments are either invariant under unit conversions or throw an error. This is because people write functions with ::Real arguments without thinking about Unitful numbers at all. Such a function might then silently return an unexpected result if a quantity is passed as argument. If it throws an error instead, that is not a problem.

For example, we currently allow mod(5m, 2). We have 5m == 500cm but mod(5m, 2) != mod(500cm, 2). A user who writes a foo(x::Real, y::Real) function that calls mod(x, y) at some point will not necessarily think about unitful numbers. Some other user, who calls foo(5m, 2) or foo(500cm, 2) will get wildly different results depending on the unit of the number that is passed as argument, even though the numbers are equal. 1 It would be more safe to error in this case.

Footnotes

  1. To be fair, in my opinion mod(5m, 2) should throw an error anyway, even if we don’t make 5m a Real, because I think all arithmetic should be invariant under unit conversion. But that is another discussion. My point is that making quantities <:Real would increase the risk of users running into these kinds of problems.

@aplavin
Copy link
Contributor Author

aplavin commented Nov 22, 2023

What other properties do you have in mind here? I’m genuinely curious.

Unitful quantities can be subtracted and compared in a consistent way, unlike some of other <: Real types.

Do you have some examples for those types as well? <...> I can’t think of an operation that I can do with real numbers but not with dual numbers or infinity.

Oh where do I start... Even looking only at those that directly subtype <: Real, here are some operations that anyone would expect working consistently for real numbers, but that don't:

julia> using Infinities
julia> RealInfinity() - RealInfinity()
ERROR: ArgumentError

julia> using ForwardDiff
julia> a = 0
julia> b = ForwardDiff.Dual(0, 1)
julia> a == b
true
julia> isinteger(b)
true
julia> Int(a)
0
# but:
julia> Int(b)
ERROR: InexactError

julia> using LogarithmicNumbers
julia> ULogarithmic(2) - ULogarithmic(3)
ERROR: DomainError

julia> using IntervalArithmetic
julia> a = 1..2
julia> b = 1..3
julia> isless(a, b)
false
julia> isless(b, a)
false
julia> isequal(a, b)
false
Of course, these inconsistencies don't make the types less useful.

And importantly, there is no fundamental difference between <: Real and <: Number here! Almost all of these properties one would expect to hold for a "mathematical number" as well.

This is because people write functions with ::Real arguments without thinking about Unitful numbers at all.

Same here: this is also an argument against subtyping Number, because people writing ::Number functions don't typically think about Unitful.

@brainandforce
Copy link

brainandforce commented Feb 1, 2024

I'm going to chime in with a different perspective: I don't think Quantity should subtype Number or restrict the values it wraps to Real entries.

The reason I'm putting this perspective forward is because of the way that arrays of unitful quantities are handled. Although the manual shows that you can construct arrays with elements of mixed dimension it's not some thing that you'd want to do in practice because the array elements are multiple types, and performance suffers. The design of the type system should discourage users from doing this, and this can be accomplished by allowing Quantity to wrap arrays directly: that way you could have Quantity{SVector{3,Float64}} for 3D position vectors with associated length units. This also extends to types that are not arrays: I'd like to use this package with geometric algebra packages that provide multivectors.

If I'm missing some important reason why this would be a bad idea, I'd love to know.

@aplavin
Copy link
Contributor Author

aplavin commented Feb 2, 2024

I can imagine usecases when non-numeric quantities can be needed. And there's nothing wrong to have a type for that!

Although, I don't see how Quantity{<:AbstractArray} would be generally useful. For one, it would require something like QuantityArray <: AbstractArray to be supported by array functions. And how it would work in cases like map(x -> x*u"m", 1:10)? Presumably, it would still return a regular Vector{Quantity}.

Anyway, my original point was only about numeric quantities (the only kind supported now). To handle numbers, it's typically more straightforward and composable to have Quantity <: Real than Quantity <: Number. It would work with any functions/methods restricted to Real, and would cleanly support stuff like Complex{Quantity} and Quaternion{Quantity}.

I'd like to use this package with geometric algebra packages that provide multivectors

And this would also work without any special handling – if Quantity was <: Real. Assuming your multivectors can contain Real values of course.

@brainandforce
Copy link

brainandforce commented Jun 18, 2024

I forgot to respond here, but now that I remembered:

Although, I don't see how Quantity{<:AbstractArray} would be generally useful. For one, it would require something like QuantityArray <: AbstractArray to be supported by array functions.

I think the solution is not a QuantityArray type, but a function that exposes the underlying value of type T wrapped by Quantity{T}. Functions that take arrays or any other type would just need to have their input stripped of quantity information, or preferably, the packages containing them should define a package extension for Unitful interoperability.

And how it would work in cases like map(x -> x*u"m", 1:10)? Presumably, it would still return a regular Vector{Quantity}.

Correct, that would return a Vector{<:Quantity}: but why would you do that if you could just multiply a Vector{T} or Matrix{T} by u"m" to get a Quantity{Vector{T}} or Quantity{Matrix{T}}?

There is an answer: I would read Quantity{Vector{T}} as a single quantity represented by a Vector{T}, such as a point in space with coordinates. On the other hand, I would read Vector{Quantity{T}} as a series of scalar quantities of type T placed in an array: perhaps these are the outputs of a function or data points that are fed into some statistical analysis.

There is a critical semantic distinction between the two scenarios that the package currently does not allow users to express: sometimes you work with arrays of separate quantities, and sometimes the array itself is a quantity. My proposal would allow that.

I'd like to use this package with geometric algebra packages that provide multivectors

And this would also work without any special handling – if Quantity was <: Real. Assuming your multivectors can contain Real values of course.

I've published a package, CliffordNumbers.jl providing a multivector (in package, referred to as CliffordNumber) implementation that necessarily constrains the types of scalars to Union{Real,Complex} because Clifford algebras are only defined over the real or complex numbers (because, for example, users shouldn't use Quaternion as the scalar type). So at the moment, it's not composable with this package. I'm personally in favor of making Quantity subtype Real, as it's better than the status quo and would make this package composable with mine, but for the reasons above I think the more generic behavior of Quantity I describe above is preferable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants