Unitful.jl icon indicating copy to clipboard operation
Unitful.jl copied to clipboard

Define helpers for unitful interfaces.

Open RomeoV opened this issue 1 year ago • 4 comments
trafficstars

Summary

This commit adds two simple functions WithDims (and WithUnits) that return a Quantity type with the dimensions (and units) constrained to those of the parameters

With this PR, we can write

circumference_of_circle(r::WithDims(u"m")) = pi*r^2
# or
circumference_of_square(s::WithUnits(u"m")) = 4*side

circumference_of_circle(1.0m)  # works
circumference_of_circle((1//1)m)  # works
import ForwardDiff: Dual
circumference_of_circle(Dual(1.0)m)  # also works (!), see e.g. https://github.com/JuliaDiff/ForwardDiff.jl/issues/328

The difference between WithDims and WithUnits the two is that the first one only constrains the dimension, and the latter constrains both dimension and unit (i.e. doesn't allow e.g. km) -- see Units vs Dims.

Rationale

I found myself trying to write simulation code with strongly typed interfaces, i.e. including information about the units. Initially I wrote my interfaces like so:

const Meters = typeof(1.0m);
circumference_of_circle(r::Meters) = pi*r^2

However, when trying to autodiff through this code, I run into a problem, because Meters has the numerical type Float64 baked in, and autodiff evaluates on a type Quantity{Dual{Float64}} (roughly).

We can instead define Meters like so:

const Meters{T<:Real} = Quantity{T, dimension(1.0m), unit(1.0m)}
circumference_of_circle(r::Meters{T}) where {T} = pi*r^2
# or
circumference_of_circle(r::Quantity{T, dimension(1.0m), unit(1.0m)}) where {T} = pi*r^2

but I thought a better approach would be to provide some syntactic sugar to this "unit constraint". This led to the construction of the syntactic sugar described in the summary.

I'm happy to receive any feedback on the idea and the naming. Other names could be e.g. quantity_with_dims (but too long for my taste), or dims_as etc., but similar is already Julia lingo and feels appropriate in this context.

RomeoV avatar Nov 20 '23 02:11 RomeoV

Instead of WithDims(u"m") one can also use Unitful.Length, although it is different in two ways:

  • WithDims only allows <:Real numbers as values:
    julia> (1+2im)u"V" isa Unitful.Voltage
    true
    
    julia> (1+2im)u"V" isa WithDims(u"V")
    false
    
  • WithDims doesn’t include Levels (i.e., logarithmic quantities):
    julia> 1u"dBm" isa Unitful.Power
    true
    
    julia> 1u"dBm" isa WithDims(u"W")
    false
    

Is there a reason for only allowing <:Real values in WithDims and WithUnits?

sostock avatar Dec 02 '23 14:12 sostock

Thanks for the review.

To your points:

Instead of WithDims(u"m") one can also use Unitful.Length

That is true for "atomic" dimensions, but becomes much more difficult for any composite dimensions (e.g. energy). Consider the example I added to the docstring in 6d28e4c:

julia> kinetic_energy(mass::WithUnits(kg), velocity::WithUnits(m/s))::WithUnits(J) = mass*velocity^2 |> x->uconvert(J, x)
julia> kinetic_energy(1000kg, uconvert(m/s, 100km/hr))
62500000//81 J

~With this PR, we can request for instance m/s, and "promise" to return Joule (J), in a very succinct way. Notice that something like this doesn't work:~

import Uniful: Length, Time
kinetic_energy(mass::Mass, velocity::Length/Time) = mass*velocity^2

~because Length/Time is not defined. Finally, even if it would work, constraining a type as Mass*Length^2/Time^2 is harder to mentally parse than WithUnits(J), and only carries dimensional information, not choice of specific units.~ EDIT: I have only now noticed all the different options for pre-defined unit combinations like Unitful.Energy, Unitful.Velocity and so on, which indeed cover a lot of the use-cases that made me write this PR. I do think that having the ability to quickly use "arithmetic" like in the example above can be useful if the required unit is not yet defined, and also being able to specify units (rather than just dimensions) can be nice, in particular for return types.

WithDims only allows <:Real numbers as values

That is true, and until I read your comment I have also never considered non-real unitful quantities. I have since consulted some of my friends, and apparently Voltage is indeed sometimes associated with complex numbers, as you have also suggested. Let me share three thoughts why I think constraining to reals is nonetheless a good design choice:

  • The seven SI base units are all real-valued. Therefore, any units derived through product and division of the former will also have a real value.
  • Since this PR is only syntactic sugar, my main concern is /clarity/ for the users of the interfaces defined using WithDims, e.g. kinetic_energy above. Therefore, I think the interface should be as restrictive as possible, to provide the smallest possible amount of "misunderstanding".
  • It seems to me that electrical impedance is represented as a complex number only for mathematical convenience, not out of a "natural" justification. Regardless, this seems to be a rare case. In these rare cases it is not difficult for the interface author to pass either a tuple (real and imaginary part) or to define WithDimsComplex themselves (one line of code). I find this a better trade-off rather than to weaken the clarity of all other interfaces.

RomeoV avatar Dec 04 '23 07:12 RomeoV

WithDims doesn’t include Levels (i.e., logarithmic quantities)

That is true, I can get to that still. There seems to be Gain and Level, which are related to MixedUnits as far as I can see. Should be easy to support if we want to move forward with this.

RomeoV avatar Dec 04 '23 07:12 RomeoV

Regardless, this seems to be a rare case.

Using complex numbers for electrical units like voltage, current, or impedance is a very common case in electrical engineering.

It seems to me that electrical impedance is represented as a complex number only for mathematical convenience

Sure. It is "only" a way of concise representation of complex relations. But then, most of the mathematics is "only" a way of concise representation of complex relations.

The seven SI base units are all real-valued.

Complex numbers can be equally used to represent oscillation processes of any nature, be it mechanical or electromagnetic. Both length (displacement) and current can be "complex-valued" in this sense.

Eben60 avatar Dec 23 '23 22:12 Eben60