UnitTypes.jl

This package provides physical units as Julia types.

julia> using UnitTypes

julia> x = Meter(3)
3m

julia> typeof(x)
Meter

julia> typeof(x) <: AbstractLength
true

julia> typeof(x) <: AbstractCapacitance
false

This allows you to easily write functions with arguments restricted to variables having certain types.

julia> function goFaster(a::T) where T<:AbstractAcceleration end

This leads to correctness and very clear error messages.

julia> goFaster(3u"m/s")
ERROR: MethodError: no method matching goFaster(::MeterPerSecond)

Closest candidates are:
  goFaster(::AbstractAcceleration)

Type hierarchy

UnitTypes introduces an abstract type hierarchy of:

AbstractMeasure
├─ AbstractAcceleration
│  └─ MeterPerSecond2
├─ AbstractAngle
│  ├─ Degree
│  └─ Radian
├─ AbstractArea
│  ├─ Acre
│  ├─ Foot2
│  ├─ Inch2
│  ├─ Meter2
│  └─ Mile2
├...and so on

See docs/unitTypesTree.md for the full tree of predefined types.

Internally, a Measure is represented by

struct Meter <: AbstractLength
  value::Float64  # the value as measured in this unit
end

The unit abbreviation and conversion function to/from the base unit are stored in UnitTypes.allUnitTypes, a package-level dictionary keyed by type.

A Dimension wraps a Measure to add geometric or physical context:

struct Diameter{T <: AbstractLength } <: AbstractDimension
  measure::T
end

The idea is that a Measure is some quantity bearing units, while a Dimension is some context-specific application of a Measure.

Within a Dimension multiple Measures may logically be used as long as they are dimensionally consistent. For instance, a circle may be described by its radius, diameter, or circumference, concepts that can be interchangeably converted, using any Measure of extent (<:AbstractLength). A function creating a circle can then internally store radii while accepting Radius, Diameter, or Circumference arguments as the user prefers, since the type system provides conversion between the argument and the function's internal convention.

Please open an issue with a minimal working example if you run into conversion errors or think additional units should be defined by the package.

Introducing new types

Macros are used to introduce and create relationships around new types:

  • @makeBaseMeasure Length Meter "m" - introduces a new basic Measure like Meter for Length or Meter3 for Volume, this should be rarely used!
  • @makeMeasure 1e3 Meter = 1 KiloMeter "km" - derives a new measure (KiloMeter) from an existing measure (Meter) with a conversion ratio (1000m = 1km)
  • @relateMeasures KiloGram*MeterPerSecond2=Newton - relates the product of types to another type, all types preexisting.

For working with Dimensions:

  • @makeDimension Diameter Meter - creates the Dimension Diameter measured in Meters
  • @relateDimensions Diameter = 2.0*Radius - relates the Dimensions Diameter and Radius by the scalar 2.0.

The macros in Measure.jl and Dimension.jl define the convert()s and other operators needed for common operations. If these are insufficient you will receive undefined method errors and can then work around the missing defitions, define them yourself, and/or open an issue.

Logical operations

Using units correctly requires distinguishing between valid and invalid operations, which in some cases means not allowing apparently convenient operations. Inches can be added, as can inch and millimeter, but only when computing area does inch*inch make sense. Inch * 3 is convenient while 3 / Inch is unlikely to be desirable. These conceptual gotchas are especially obvious in affine units like Temperature, where 0°C + 10°F is not 42°F but rather -12.2°C.

With use, patience, and issues, these coherence rules will become more clear and explained by example.

Naming conventions

Combining the names of units to get the resulting type name follows these simple rules:

  1. Multiplication is concatenation: Newton * Meter = NewtonMeter (==MeterNewton)
  2. Division and negative exponents are indicated by Per: N*m/s^2 = NewtonMeterPerSecond2
  3. Numeric powers are preferred over words: Meter2, not SquareMeter
  4. No plurals: Meter, not Meters

Comparison with other packages

Unitful.jl

Unitful leverages parametric types to store units, giving flexibility at the cost of compile-time type uncertainty. It's two major limitations are the avoidance of angular measures, as they are not first-class entities but rather ratios, and rather lengthy type unions that clutter outputs, especially on error:

julia> function goSlower(x<:Unitful.Acceleration) end
goSlower (generic function with 1 method)

julia> goSlower(1u"mm")
ERROR: MethodError: no method matching goSlower(::Quantity{Int64, 𝐋 , Unitful.FreeUnits{(mm,), 𝐋 , nothing}})

Closest candidates are:
  goSlower(::T) where T<:(Union{Quantity{T, 𝐋 𝐓^-2, U}, Level{L, S, Quantity{T, 𝐋 𝐓^-2, U}} where {L, S}} where {T, U})

As Unitful is the dominant unit package and has wide use and support, we provide a separate package ExchangeUnitful to enable interoperation with Unitful.

DynamicQuantities.jl

DynamicQuantities is newer and faster than Unitful because it "defines a simple statically-typed Quantity type for storing physical units." It does this by storing the exponents on the basic units, allowing any unit traceable to SI to be used. But this performant representation hurts readability, and while the unit representation may be able to be hidden behind overrides of show(), Julia is designed for types to be read and manipulated directly by users.

UnitTypes.jl

In the presence of Julia's type-heavy UI, these two, good attempts feel misdirected and motivate this package's literal typing of units. Only units that have been defined by one of the macros may be represented with a named type. Arithmetic that produces a novel unit combination falls back to Catchall, which carries a dimension map but lacks a named type. Complex units may also need additional methods to correctly convert between units — see Temperature.jl for an example of manual unit conversion.

Package macros and functions

Measure.jl

UnitTypes.@makeBaseMeasureMacro

macro makeBaseMeasure(quantityName, unitName, abbreviation::String, isAffine=false)

Make a new base measure which has no relationship to an existing unit. For example, in @makeBaseMeasure Length Meter "m":

  • quantityName is the name of the measure, 'Length' above.
  • unitName is the name of the unit which will be used to make measures bearing that unit, 'Meter' above.
  • abbreviation is the abbreviation of the unit name, used in all string presentations of the measure.

The macro will introduce AbstractLength <: AbstractMeasure and Meter() into the current scope.

To get the measure's value in the base unit as a float, see toBaseFloat().

source
UnitTypes.@makeMeasureMacro

macro makeMeasure(xFactor, relation, newType, newAbbreviation)

Creates a new Measure from an existing base measure using leading conversion factors. The ExistingUnitType must already exist; NewUnitType will be created. The equation xFactor * ExistingUnitType = yFactor * NewUnitType defines the conversion. @makeMeasure 1e-3 Meter = 1 MilliMeter "mm"

Affine units (like Temperature) provide parenthesized anonymous functions as factors. xFactor converts NewUnitType values to ExistingUnitType; yFactor is the inverse. @makeMeasure (f->(f+459.67)*5/9) Kelvin = (k->k*9/5-459.67) Fahrenheit "°F"

source
UnitTypes.@relateMeasuresMacro

macro relateMeasures(relation)

Adds a multiplicative relationship between the left and right sides of the equation, allowing units to be multiplied and divided with consistent units. All types must already be defined; the right side is the resultant type.

Single binary operations: @relateMeasures Meter*Newton = NewtonMeter @relateMeasures Newton/Meter2 = Pascal @relateMeasures 1/Second = Hertz # inverse: sets Hertz dims to {AbstractTime=>-1}

Compound expressions with multiple * and / are also supported. Intermediate types are created automatically, named by concatenating type names (*) or inserting "Per" (/): @makeBaseMeasure Force Newton "N" @relateMeasures KiloGram*Meter/Second/Second = Newton # creates KiloGramMeter and KiloGramMeterPerSecond as intermediate types

source
UnitTypes.@u_strMacro

macro u_str(unit::String)

Macro to provide the 1.2u"cm" inline unit assignment. a = 1.2u"cm"

This works by looking up the unit string in allUnitTypes and returning the corresponding type. See https://docs.julialang.org/en/v1/manual/metaprogramming/#meta-non-standard-string-literals

source

Catchall.jl

UnitTypes.CatchallType

struct Catchall <: AbstractMeasure

Catch-all for unit expressions with no defined named type. The stored value is in SI base units; dimensions maps abstract dimension types to their integer exponents, e.g. {AbstractLength=>1, AbstractTime=>-1} for velocity.

Prefer registered named types (Meter, Second, Newton …) wherever possible. Catchall is produced only when arithmetic or u_str parsing yields a combination that has no matching entry in allUnitTypes.

source
UnitTypes.parseCatchallFunction

parseCatchall(str) -> Union{AbstractMeasure, Nothing}

Parses a compound unit string such as "mm*s/kg" or "m^2" into a measure. Each token is looked up by abbreviation in allUnitTypes; the combined scale factor and dimension map are computed, and resolveOrExpr is applied so a named type is returned whenever one matches.

Returns nothing if any token is unrecognised or affine (Temperature-style).

Syntax supported:

  • * separates numerator factors
  • / introduces denominator factors (applies to all tokens after the first / per *-group)
  • ^N applies an integer exponent (may be negative) to the preceding abbreviation
source
UnitTypes.UnitStepRangeType

struct UnitStepRange{T<:AbstractMeasure}

A lazy, allocation-free stepped range over a consistent unit type. Constructed by the start:step:stop colon syntax when all three share the same abstract dimension. The element type is determined by start; step and stop are converted to match.

source

Dimension.jl

UnitTypes.@makeDimensionMacro

macro makeDimension(dimName, measure)

Make a new dimension dimName of measure; also creates 'AbstractdimName'

``` @makeDimension Diameter Meter

d = Diameter(MilliMeter(3.4))
r = Radius(d)

```

source
UnitTypes.@relateDimensionsMacro

macro relateDimensions(relation)

Defines various Base.: functions that facilitate the given (linear) relationship. All types must already be defined and written in the form type1 = factor * type2, as in: @relateDimensions Diameter = 2.0*Radius

source