How To Properly Use Unitful

I’m using Unitful for the first time and would like direction on the proper way to use it. For my use case, I am creating a struct that I would like to store a specific unit because the value is being passed to such things as xml file creation and cli programs as only a number. I’d like conversion of the to happen at construction, rather than having to do something like uconvert(u"output-unit", someQuantity) every place the raw number has to be passed to the external applications.

In my first implementation that works, I had a struct similar to the following:

struct MyStruct 
	value::typeof(1.0u"cm^-3")
end

I thought this was too restricting because I may want to allow Int or something for the quantity, so I changed my struct to the following:

struct MyStruct 
	value::Unitful.Quantity{<:Real, Unitful.𝐋^-3, u"cm^-3"}
end

But when I try to construct an instance, I get the following error:

ERROR: MethodError: no method matching Quantity{#s92495,𝐋^-3,cm^-3} where #s92495<:Real(::Quantity{Int64,𝐋^-3,Unitful.FreeUnits{(cm^-3,),𝐋^-3,nothing}})

To experiment with different types, I discovered the following

julia> typeof(1.0u"cm^-3")
Quantity{Float64,𝐋^-3,Unitful.FreeUnits{(cm^-3,),𝐋^-3,nothing}}

julia> typeof(1.0u"cm^-3") <: typeof(1.0u"cm^-3")
true

julia> typeof(1.0u"cm^-3") <: Unitful.Quantity{Float64, Unitful.𝐋^-3,Unitful.FreeUnits{( Unitful.cm^-3,), Unitful.𝐋^-3,nothing}}
false

That is a surprising result that I don’t understand.

Which leads me to why I asking a question. What is the proper way to force a specific Unit in the struct or a function signature? Also, what is best practice? I would normally expect to use something like Quantity{<:Real, 𝐋^-3}, but for my specific case it seems safer to force the Unit as well to avoid having to remember do uconvert with the correct type everywhere when I am interfacing with external applications.

4 Likes

I’m not sure it’s helpful, but I think you should not worry about the unit in structs, unless I’m missing something. So I would just go with

struct MyStruct{T}
	value::T
end

Also, I don’t see why using a struct removes the necessity to do uconvert. Maybe you can clarify that?

Side notes:
Visually, I personally prefer to use the pipe for conversions, e.g., 3u"m" |> u"cm" will give 300u"cm". I also like that you can strip the unit and convert at the same time, e.g., ustrip(u"cm", 3u"m") will give 300.

1 Like

The idea of the struct is I can use the value stored in the struct directly and know it is the correct unit for use with external applications I am interfacing with (the struct is used for other stuff too, not just storing the Quantity). There are lines of code like the following that use the numeric value in the filename:

outputpath = joinpath(directory, "data$(mystruct.value.val).txt")

which probably would need a unconvert call if I don’t know the units in mystruct:

outputpath = joinpath(directory, "data$(uconvert(u"m", mystruct.value).val).txt")

The value is used as data inside xml files and as cli parameter to other applications too though. So I’m trying to avoid having to call uconvert to be sure of the unit.

The code needs to be refactored because I can put the logic in a function and do the conversion in the function, then the unit in MyStruct won’t matter. So there are better ways, but I am still curious how to force units with the function signature.

julia> using Unitful

julia> struct MyStruct{L<:Unitful.Length}
           x::L
       end

julia> MyStruct(3.0u"mm")
MyStruct{Quantity{Float64,𝐋,Unitful.FreeUnits{(mm,),𝐋,nothing}}}(3.0 mm)

julia> MyStruct(3u"km")
MyStruct{Quantity{Int64,𝐋,Unitful.FreeUnits{(km,),𝐋,nothing}}}(3 km)

julia> MyStruct(3u"s")
ERROR: MethodError: no method matching MyStruct(::Quantity{Int64,𝐓,Unitful.FreeUnits{(s,),𝐓,nothing}})
Closest candidates are:
...

You can either use Unitful’s defaults or define your own:

@derived_dimension PerVol inv(Unitful.𝐋^3)
6 Likes

The OP specifically said

so it seems they want to constrain the struct to only hold something with units of inverse centimeters cubed. They don’t want to allow inverse kilometers cubed or whatever.

What I find surprising is this part:

julia> using Unitful

julia> typeof(1.0u"cm^-3")
Quantity{Float64,𝐋^-3,Unitful.FreeUnits{(cm^-3,),𝐋^-3,nothing}}

julia> Unitful.Quantity{Float64, Unitful.𝐋^-3,Unitful.FreeUnits{( Unitful.cm^-3,), Unitful.𝐋^-3,nothing}}
Quantity{Float64,𝐋^-3,Unitful.FreeUnits{(cm^-3,),𝐋^-3,nothing}}

julia> typeof(1.0u"cm^-3") == Unitful.Quantity{Float64, Unitful.𝐋^-3,Unitful.FreeUnits{( Unitful.cm^-3,), Unitful.𝐋^-3,nothing}}
false

julia> typeof(1.0u"cm^-3") <: Unitful.Quantity{Float64, Unitful.𝐋^-3,Unitful.FreeUnits{( Unitful.cm^-3,), Unitful.𝐋^-3,nothing}}
false

I can’t spot a single difference between the second and third input yet they are apparently not the same type. Seems like a bug to me, but maybe I’m missing something.

3 Likes

@Mason, yup, that is exactly correct. And your findings are what have me puzzled as well.

I would suggest opening and issue in Unitful.jl regarding those types and asking why they’re not the same.

Continuing @tim.holy 's example, you could still force the unit on creation with an inner constructor if I understand correctly

using Unitful

@derived_dimension PerVol inv(Unitful.𝐋^3)

struct MyStruct{T <: PerVol}
    x::T
    function MyStruct(x::T) where {T <: PerVol}
        y = uconvert(u"cm^-3", x)
        new{eltype(y)}(y)
    end
end
julia> MyStruct(1u"mm^-3")
MyStruct{Quantity{Int64,�^-3,Unitful.FreeUnits{(cm^-3,),�^-3,nothing}}}(1000 cm^-3)
1 Like

@Mason, I will open an issue. @kevinczimmerman, I didn’t think to use an inner constructor, I will try that as a work around. It would be nice to know why the function signature is not working though, or if that can’t be done at all for some reason.

2 Likes

I opened an issue about the typeof weirdness:
https://github.com/PainterQubits/Unitful.jl/issues/339

1 Like

I didn’t get to the bottom of this, but Unitful does have all sorts of show methods for these types, which might be hiding things:

julia> @which show(stdout, typeof(1.0u"cm^-3"))
show(io::IO, x::Type{T}) where T<:Quantity in Unitful at /Users/me/.julia/packages/Unitful/9XsxK/src/display.jl:81
1 Like

Late to the thread, but fyi, there’s something sneaky being done here to keep Unitful types from being exceedingly long when printed, which is something people have disliked in the past. The cm^-3 inside the type signature is actually this:

julia> Unitful.Unit{:Meter, u"𝐋"}(-2,-3)
cm⁻³

The Unitful.Unit type is never manipulated by the user and is for internal use only. Having Unitful types show in a succinct way that still yields enough information to construct the type at the REPL has proven challenging. I think a step in the right direction would be to at least distinguish the two cm objects by having the internal one display differently, to avoid pulling a fast one on everyone. Sorry for that.

If you find yourself wanting to manipulate Unitful types directly, there’s almost always a better way. I would follow Tim’s advice here. You could consider restricting to cm^-3 in the constructor if desired, though I’m not sure why you would need to do that.

3 Likes

Hello;

just to say that this puzzles me greatly as well. I’m a novice, but if I happen to discover anything relevant I will post an update.

Cheers!

P.S. I don’t know if it is considered ‘good practice’ to bump old threads like this (when they are still open) - if it is not please let me know and I will refrain in the future.

I ran into the same issue. Below I describe how I inverstigated the problem and fixed the type definition (spoiler: solution is at the end).

julia> using Unitful

julia> a = typeof(1.0u"m")
Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}

OK so let us define type b (a supposed alias for type a) as (note that you need the Unitful. prefix unless you import symbols m and 𝐋, a bold L, from Unitful):

julia> b = Quantity{Float64, Unitful.𝐋, Unitful.FreeUnits{(Unitful.m,), Unitful.𝐋, nothing}}
Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}

a and b are printed exactly the same however they are different as shown by:

julia> a == b
false

Exactly what was observed by @acdupont at the start of this discussion.

Since a and b are parametric types, let us find out which of their parameters is/are different by introspection:

julia> a.parameters .== b.parameters
3-element BitVector:
 1
 1
 0

So the culprit is the 3rd one, a Unitful.FreeUnits structure, we use the same trick as above to compare its parameters:

julia> a.parameters[3].parameters .== b.parameters[3].parameters
3-element BitVector:
 0
 1
 1

So, the 1st parameter is now the one which is different. However:

julia> a.parameters[3].parameters[1]
(m,)

julia> b.parameters[3].parameters[1]
(m,)

which (wrongly) seems to indicate that both are 1-tuple with single element m. Let us check the type of the element:

julia> typeof(a.parameters[3].parameters[1][1])
Unitful.Unit{:Meter, 𝐋}

julia> typeof(b.parameters[3].parameters[1][1])
Unitful.FreeUnits{(m,), 𝐋, nothing}

Hooray, we finally found a difference!

Unrolling all this, the correct definition for b is:

julia> b = Quantity{Float64, Unitful.𝐋, Unitful.FreeUnits{(Unitful.Unit{:Meter,Unitful.𝐋}(0,1//1),), Unitful.𝐋, nothing}}
Quantity{Float64, 𝐋, Unitful.FreeUnits{(m,), 𝐋, nothing}}

Let us check this:

julia> a == b
true

julia> a === b
true

To give a practical example that can be used to define precisely the type of a field in a structure (the original need), we can define the following type alias:

const Meters{T<:Real} = Quantity{T, Unitful.𝐋, Unitful.FreeUnits{(Unitful.Unit{:Meter,Unitful.𝐋}(0,1//1),), Unitful.𝐋, nothing}}

and check that:

julia> typeof(1.0u"m") === Meters{Float64}
true

julia> typeof(1u"m") === Meters{Int}
true

julia> convert(Meters{Float32}, 2.3u"mm")
0.0023f0 m
6 Likes