Unitful.jl
Unitful.jl copied to clipboard
Addition of degree + radian is unitless
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.
Just to clarify, which of the following is the issue here:
- that Unitful.jl returns plain numbers whenever the result of an operation would be a quantity with units
NoUnitsor - that the result of adding a quantity with units
radand a quantity with units°does not have unitsrador°, butNoUnits?
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.
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?
@bc0n checkout DimensionfulAngles.jl, it treats angles as a dimension.
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.
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.