How to use Parametric Structs with Unitful

I am trying to create a package that solves the kinematics of a closed mechanism. The mechanism type consists of a list of link types that I have also defined parametricly:

abstract type Link end

struct BarLink{T} <: Link
	r::T # Vector Length
	θ::T # Position of link (variable) units of rad
	ω::T # Velocity of link, units of rad/s
	α::T # Acceleration of link, units of rad/s/s
end

struct Mechanism
	n::Integer 
	input::Integer #index of input link in link vector
	variables::Tuple{Integer, Integer} #index of variable links (2)
	links::Vector{Link}
end

Using Unitful I tried to create a 4-bar Mechanism:

bar1 = BarLink{Quantity}(10u"inch", Float64(π)u"rad", 0u"m/m", 0u"m/m") # line throws error
bar2 = BarLink{Quantity}(4u"inch", Float64(2*π/3)u"rad", 45u"rad/s", 0u"rad/s/s")
bar3 = BarLink{Quantity}(10u"inch", deg2rad(47.26)u"rad", 0u"m/m", 0u"m/m")
bar4 = BarLink{Quantity}(12u"inch", deg2rad(295.75)u"rad", 0u"m/m", 0u"m/m")

fourbar = Mechanism(4, 2, (3, 4), [bar1; bar2; bar3; bar4])

The error thrown from the initialization of bar1:

MethodError: no method matching Unitful.Quantity(::Int64)

For reference, I have similar code that works for other subtypes of Number:

bar1 = BarLink{Real}(10, π, 0, 0)
bar2 = BarLink{Real}(4, deg2rad(120), 45, 0)
bar3 = BarLink{Real}(10, deg2rad(227.26-180), 0, 0)
bar4 = BarLink{Real}(12, deg2rad(115.75+180), 0, 0)

fourbar = Mechanism(4, 2, (3, 4), [bar1; bar2; bar3; bar4])

and with SymPy:

@syms r1 r2 r3 r4 θ1 θ2 θ3 θ4 ω2 α2

bar1 = BarLink{Sym}(r1, θ1, 0, 0)
bar2 = BarLink{Sym}(r2, θ2, 0, 0)
bar3 = BarLink{Sym}(r3, θ3, 0, 0)
bar4 = BarLink{Sym}(r4, θ4, 0, 0)

fourbar = Mechanism(4, 2, (3,4), [bar1; bar2; bar3; bar4])

I have no idea why a method error is thrown, since Quantity is a subtype of Number, and also is abstract enough to contain different concrete subtypes of Quantity, shouldn’t the constructor create the datatype?

There are some subtleties with Unitful. The unit m/m eagerly cancels the m to become dimensionless and actually a scalar Int. The following is something put together to avoid this:

# using Unitful and definition above

const mpm1 = Unitful.FreeUnits{(Unitful.Unit{:Meter, Unitful.𝐋}(0, 1),
  Unitful.Unit{:Meter, Unitful.𝐋}(0, -1)), NoDims, nothing}
const mpm = Quantity{Int64, NoDims, mpm1}(1)

bar1 = BarLink{Quantity}(10u"inch", Float64(π)u"rad", 0mpm, 0mpm) # line throws error
bar2 = BarLink{Quantity}(4u"inch", Float64(2*π/3)u"rad", 45u"rad/s", 0u"rad/s/s")
bar3 = BarLink{Quantity}(10u"inch", deg2rad(47.26)u"rad", 0mpm, 0mpm)
bar4 = BarLink{Quantity}(12u"inch", deg2rad(295.75)u"rad", 0mpm, 0mpm)

fourbar = Mechanism(4, 2, (3, 4), [bar1; bar2; bar3; bar4])

I’ll be happy to see more idiomatic ways of creating m/m unit.

1 Like

If you are looking for performance, then “Avoid fields with abstract type”: Performance Tips · The Julia Language
(Quantity, Integer, and Link are all abstract types)[1]

I.e, if you want to keep the types parametric, it should be sth like

abstract type Link end

struct BarLink{R,Θ,Ω,Α} <: Link
	r::R # Vector Length
	θ::Θ # Position of link (variable) units of rad
	ω::Ω # Velocity of link, units of rad/s
	α::Α # Acceleration of link, units of rad/s/s
end

struct Mechanism{L<:Link}
	n::Int
	input::Int #index of input link in link vector
	variables::Tuple{Int,Int} #index of variable links (2)
	links::Vector{L}
end

(Note e.g. the Int (== Int64) instead of Integer: concrete vs abstract type. You could also annotate those fields ::I and add ,I or ,I<:Integer in the parameter clause)


  1. Note that this only applies to annotations of struct fields. Function argument annotations can still be happily abstract (or not given at all) ↩︎

You don’t have to specify all those new type params on creation btw. They get filled in automatically. I.e:

bar1 = BarLink(10u"inch", Float64(π)u"rad", 0u"rad/s", 0u"rad/s/s")
bar1 = BarLink(10, π, 0, 0)
bar1 = BarLink(r1, θ1, 0, 0)

fourbar = Mechanism(4, 2, (3, 4), [bar1; bar2; bar3; bar4])