Incorporate units into custom types

I have a package that works fine. First all my types and function were hard-coded to Float64. Then I evolved into parameterizing everything to {T <: AbstractFloat}. Now I’m looking into allowing for Unitful units, but I’m stumped on how to best do this.

It would be great if there was some simple way of making everything unit-friendly while retaining the concreteness of the types/functions. How do you people do it?

As an example, here is how my Ray type looks like now:

using LinearAlgebra, StaticArrays

mutable struct Ray{T <: AbstractFloat}
    origin::SVector{3, T}
    direction::SVector{3, T}
    intensity::T
    Ray(o::SVector{3, T}, d::SVector{3, T}) where {T <: AbstractFloat} = new{T}(o, normalize(d), one(T))
end

While its origin can be unitful, its direction shouldn’t, and its intensity can remain unitless (though that too could get upgraded to a unitful representation).

julia> Ray(SVector(1.0, 2.0, 3.0), SVector(1.0, 2.0., 3.0))
Ray{Float64}([1.0, 2.0, 3.0], [0.267261, 0.534522, 0.801784], 1.0)

You can see how currently this won’t work with units:

julia> using Unitful

julia> import Unitful: μm

julia> Ray(SVector(1.0μm, 2.0μm, 3.0μm), SVector(1.0μm, 2.0μm, 3.0μm))
ERROR: MethodError: no method matching Ray(::SArray{Tuple{3},Quantity{Float64,Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)},Unitful.FreeUnits{(Unitful.Unit{:Meter,Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}}(-6, 1//1),),Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}}},1,3}, ::SArray{Tuple{3},Quantity{Float64,Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)},Unitful.FreeUnits{(Unitful.Unit{:Meter,Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}}(-6, 1//1),),Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}}},1,3})

This works:

using LinearAlgebra, StaticArrays
using Unitful: μm, unit

mutable struct Ray{T, S}
    origin::SVector{3, T}
    direction::SVector{3, S}
    intensity::T
end

Ray(o::SVector{3, T}, d::SVector{3, T}) where {T} = Ray(o, normalize(d), one(T)*unit(T))

Ray(SVector(1.0μm, 2.0μm, 3.0μm), SVector(1.0μm, 2.0μm, 3.0μm))
1 Like

You’d want to make the change to replace one(T)*unit(T) with oneunit(t) which is in Base.

using LinearAlgebra, StaticArrays
using Unitful
using Unitful: μm

mutable struct Ray{T, S}
    origin::SVector{3, T}
    direction::SVector{3, S}
    intensity::T
end
Ray(o::SVector{3, T}, d::SVector{3, T}) where {T} = Ray(o, normalize(d), oneunit(T))

I’d also point out there is a potential pitfall to assigning Unitful units. For instance, the rays a and b are equivalent, but the promotion will cause issues.

Ray(o::SVector{3},d::SVector{3}) = Ray(promote(o,d)...)

a=SVector(1.0μm, 2.0μm, 3.0μm)
b=SVector(1e-6u"m", 2e-6u"m", 3e-6u"m")

a==b #true
A=Ray(a,a)
B=Ray(a,b)

A==B #false
A.origin==B.origin #true
A.direction==B.direction #true
A.intensity==B.intensity #false intensity of 1μm ≠ 1m