PainterQubits / Unitful.jl

Physical quantities with arbitrary units
Other
602 stars 110 forks source link

Addition of degree + radian is unitless #537

Open bc0n opened 2 years ago

bc0n commented 2 years ago
using Unitful
a = 180u"°" + 3.141u"rad"
@show unit(a)    # blank
@show typeof(a) # Float64

I expect to have to explicitly ustrip() when I want a unitless quantity. This behavior makes certain operations type-unstable, which the docs describe and dismiss. Much of the point of having typed functions goes away if I also have to accept floats because their input units were automatically, silently canceled. UnitfulAngles.jl does not fix this.

sostock commented 2 years ago

Just to clarify, which of the following is the issue here:

This behavior makes certain operations type-unstable, which the docs describe and dismiss.

What operation is type-unstable because of this?

Much of the point of having typed functions goes away if I also have to accept floats because their input units were automatically, silently canceled.

Yes, it is unfortunate that dispatching on DimensionlessQuantity is not useful because of this behavior. Dispatching on Union{DimensionlessQuantity,Real} may be an option if you only expect real numbers. Unfortunately, Union{DimensionlessQuantity,Number} would also match dimensionful quantities, since AbstractQuantity <: Number.

bc0n commented 2 years ago

Thanks for the follow-up, I'd like to say both are the same issue, which is that Radians are sometimes treated as an angle and sometimes number and this inconsistency leads to confusion. The type instability is simply:

ulia> Angle{T} = Union{Quantity{T,NoDims,typeof(u"rad")}, Quantity{T,NoDims,typeof(u"°")}} where T
Union{Quantity{T, NoDims, Unitful.FreeUnits{(rad,), NoDims, nothing}}, Quantity{T, NoDims, Unitful.FreeUnits{(°,), NoDims, nothing}}} where T

julia> g(a::Angle) = @show a
g (generic function with 1 method)

julia> g(1°)
a = 1°
1°

julia> g(1u"rad")
a = 1 rad
1 rad

julia> g(1°+2°)
a = 3°
3°

julia> g(1u"rad" + 2u"rad")
a = 3 rad
3 rad

julia> g(1° + 2u"rad")
ERROR: MethodError: no method matching g(::Float64)
Closest candidates are:
  g(::Union{Quantity{T, NoDims, Unitful.FreeUnits{(rad,), NoDims, nothing}}, Quantity{T, NoDims, Unitful.FreeUnits{(°,), NoDims, nothing}}} where T) at REPL[49]:1
Stacktrace:
 [1] top-level scope
   @ REPL[54]:1

Again I think the value of unit systems is that they help to distinguish variables and guide users in creating consistent, physically-intelligible models. While angles are, I guess, a mathematical convention rather than traceable to a physical quantity, it strains the conceptual model to define

julia> AngleOrNumber = Union{Angle, Real}
Union{Real, Union{Quantity{T, NoDims, Unitful.FreeUnits{(rad,), NoDims, nothing}}, Quantity{T, NoDims, Unitful.FreeUnits{(°,), NoDims, nothing}}} where T}

julia> typeof(1) <: AngleOrNumber
true

julia> typeof(1°) <: AngleOrNumber
true

That is I would prefer to treat angles as a fully-contrived unit that is not mapped to dimensionless/Real at all. Alternately, a fallback promotion from Real to Radian might enable g(1) to succeed?

cmichelenstrofer commented 1 year ago

@bc0n checkout DimensionfulAngles.jl, it treats angles as a dimension.

RomeoV commented 11 months ago

Here's a two-line hack I used to solve this problem:

AngularUnits = Union{typeof(unit(1.0°)), typeof(unit(1.0rad))};
# without this there's a bug when adding different angular units...
Unitful.promote_unit(lhs::T, rhs::S) where {T<:AngularUnits, S<:AngularUnits} = rad

This always returns the result of addition/subtraction in rad. Actually I would have preferred

Unitful.promote_unit(lhs::T, rhs::S) where {T<:AngularUnits, S<:AngularUnits} = lhs

but that somehow yields a Stackoverflow error.

RomeoV commented 11 months ago

Btw I think it would be really good if this issue would be dealt with properly, or at least throw an error of some kind.

I had a pretty painful debugging session trying to find this, especially since ustrip(\degree, myangle) doesn't complain if myangle is already a Float64. So for me the typed angle became a regular Float, further angle addition was incorrect, and the numerical error propagated silently from the simulation into the optimizatio, resulting in misleading numerical results without any warnings / errors.