Create custom units in Unitful

Hey together!

Im creating custom units with Unitful and am stumbling across a difficulty for which I havent yet found a solution for. I created a little module in which I am able to define new unitful dimensions and create custom units:

module MyUnits

using Unitful
export @u_str

function create_unit(name::String)
	name = lowercase(name)

	Dimension = uppercasefirst(name) * "Dimension" |> Meta.parse
	DimensionQuote = uppercasefirst(name) * "Dimension"
	dimension = name * "Dimension" |> Meta.parse

	Unit = uppercasefirst(name) |> Meta.parse
	UnitQuote = uppercasefirst(name)
	unit = name |> Meta.parse
	UNIT = uppercase(name) |> Meta.parse

	return eval(
		quote
			@dimension $Dimension $DimensionQuote $dimension
			@refunit $Unit $UnitQuote $unit $Dimension true
			const $UNIT = Quantity{<:Real, $Dimension}
			export $UNIT, $Unit, $unit, $dimension, $Dimension
		end
	)
end

create_unit("tomate")
create_unit("salad")
create_unit("euro")

function __init__()
	Unitful.register(MyUnits)
end

end

This for now works fine. What I want to do with this is add units in form of vegetables like tomate, salad (later more), money like euro, and later many more very custom and unique dimesional units. In the create_unit function I am defining multiple “namings”, which are fed into the @dimension and @refunit macros to name and define the new unitful dimensions and units. Then I export some of the names so I can use it lateron outside the package. There are some case conventions I use here for the namings which are not very relevant for my problem, like uppercase, firstuppercase, no uppercase and so on.

If I load this module, I can use my newly created units like:

price(e::EURO, t::TOMATE) = e/t
price(5u"Euro", 2u"Tomate") # 2.5 Euro Tomate^-1
5u"Euro/Tomate" * 7u"Tomate" # 35 Euro
isa(5u"Euro/Tomate" * 7u"Tomate", EURO) # true

So until here everything is fine. What I now would like to do is the following. I want to “typecheck” my calculations in the form of:

tom::TOMATE = 7u"Tomate"
cost::EURO = 3u"Euro"
tomatoprice::WHATTYPE = cost / tom

I want to define calculations and type check them by putting ::TYPE next to the resulting variable. This is just a basic example, lateron there will be many combined types flying around, which is actually my initial motivation to implement this kind of “typechecking” with Unitful.

So, what do I have to put into the missing space at WHATTYPE? Of course there shall be something like EURO/TOMATO, but how does it get created automatically, or how can I access it if already existing? I know that there is the Quantity type in Unitful, but I havent yet understood it very well and how to use it for this purpose.

Thanks very much for help here! I am enjoying Unitful, but am still at the beginning of my learning. If you have some critics on my module and code structures above, please let me know aswell. Im still somehow new to Julia too.

I have not worked with Unitful.jl so I don’t know the answer but I can tell you how to check. Simply use typeof to ask Julia what the type is :slight_smile:
Like:

julia> typeof(cost/tom)
1 Like

Thanks for your quick answer! :slight_smile: I forgot to mention that I tried that but it doesnt seem to solve it for two reasons. First, if I do

typeof(1u"Euro/Tomate")

I get

Unitful.Quantity{Int64, EuroDimension TomateDimension^-1, Unitful.FreeUnits{(Euro, Tomate^-1), EuroDimension TomateDimension^-1, nothing}}

but

isa(1u"Euro/Tomate", Unitful.Quantity{Int64, EuroDimension TomateDimension^-1, Unitful.FreeUnits{(Euro, Tomate^-1), EuroDimension TomateDimension^-1, nothing}})

produces the error

syntax: missing comma or } in argument list

which I do not understand. I tried some similar things with the Quantity type thing, but it returned false.

But even if this would work, the Quantity… type is just too long to be used directly like that, so Im looking for a short and handy way of using quotient types in that sense.

The @derived_dimension macro is useful for that:

@derived_dimension TomatoPrice EuroDimension/TomateDimension

This defines TomatoPrice (and TomatoPriceUnits, TomatoPriceFreeUnits) so that

tomatoprice::TomatoPrice = cost / tom

does what you want.

2 Likes

Thanks much, that is a first step. Do you know if there is a automatic way of defining such derived dimensions for all dimensions I have created? Later I will for example have units like Container, Year, Shoes, etc and will want to “spontaneously” do

shoesPerContainerPerYear::SHOE/CONTAINER/YEAR = 100000u"Shoe" / (10u"Container"*1u"Year")

without having to explicitly define a derived dimension for that. Do you understand what I mean?

This is because the dimensions are printed as EuroDimension TomateDimension^-1 (with a space instead of *, which isn’t valid Julia syntax) in the type parameters. You can instead write

Quantity{<:Real, EuroDimension/TomateDimension}
1 Like

Oh ok, I wonder how I didnt try that out. That is now at least a worst case solution so to say, because it will still make my code kind of long if I always type ::Quantity{<:Real, ...}, especially if many units are combined together, which will happen. But at least it gives a solution, thanks for that.

Ive been trying out right now to write some formating or macro to convert something like EURO/TOMATE into Quantiy{<:Real, EuroDimension/TomateDimension, such that it consistently extends to more combinations, like EURO/TOMATE/CONTAINER. Not yet successfull, also because of that EuroDimension TomateDimension^-1 formating thing.

Do you see a simple way to do this?

I found a workaround with which I may be able to do everyhing I want to do. Im doing the following:

module MyUnits

using Reexport
@reexport using Unitful
export @u_str, TT

TT(x) = Quantity{<:Real, x}

function create_unit(name::String)
	name = lowercase(name)

	DIMENSION = uppercase(name) |> Meta.parse
	DimensionQuote = uppercasefirst(name) * "Dimension"
	dimension = name * "Dimension" |> Meta.parse

	Unit = uppercasefirst(name) |> Meta.parse
	UnitQuote = uppercasefirst(name)
	unit = name |> Meta.parse

	return eval(
		quote
			@dimension $DIMENSION $DimensionQuote $dimension
			@refunit $Unit $UnitQuote $unit $DIMENSION true
			export $Unit, $DIMENSION
		end
	)
end

create_unit("tomato")
create_unit("euro")
create_unit("year")

function __init__()
	Unitful.register(MyUnits)
end

end

Then with the help of the TT function I can handyly do the following stuff:

using .MyUnits
isa(2.5u"Euro", TT(EURO)) # true
isa(4u"Euro/Tomato/Tomato", TT(EURO/TOMATO/TOMATO)) # true
price(e::TT(EURO), t::TT(TOMATO)) = e/t
price(2u"Euro", 8u"Tomato") # 0.25 Euro Tomato^-1
2u"Tomato"*5u"Euro/Tomato/Tomato"/3u"Euro" # 3.3333333333333335 Tomato^-1
tom::TT(TOMATO) = 7u"Tomato"
cost::TT(EURO) = 3u"Euro"
time::TT(YEAR) = 5u"Year"
tomatoprice::TT(EURO * YEAR / TOMATO / YEAR * TOMATO) = cost / 3.1*tom / time/tom*2*time # 1.935483870967742 Euro

And so on. So wrapping around this TT function is the very handy thing here. Final version would be to not need this function, but for now its working well for me.

If anyone has one idea how to build this without having to use such a helper function, I appreciate your comments! And also any further critics and how you find my approach here. Thanks!