Dimensional analysis in Elixir

Aaron Brenzel
Software @ Fast Radius
2 min readNov 22, 2019

At Fast Radius we deal with a lot of quantities that come in units. Nearly all of the code that interacts with CAD files uploaded to our systems needs to know what units the CAD files were originally created in, and the units of any downstream calculations like the part’s volume and surface area.

The first pass implementation was to tag the file itself with a string that indicated whether it was in metric or imperial. Metric was assumed to be millimeters and imperial to be inches. Unfortunately, the quantities themselves did not carry their units with them, leading to a number of bugs as that data was passed along to the various services that make up the part analysis toolchain.

To solve this problem, we created ex_dimensions, a small library for ensuring correctness in dimensional calculations while trying to be as unobtrusive as possible. Here’s an example:

iex> q = ExDimensions.Spatial.inches(6)
iex> IO.puts(q)
"6 in"

Other than explicit string representation, the library tries to avoid working in strings as much as possible. Under the hood, quantities are tagged with what are effectively phantom types so they can be properly handled:

iex> IO.inspect(q)
%ExDimensions.Quantity{
value: 6,
units: [ExDimensions.Spatial.Inches],
denom: []
}

Things get much more interesting when doing unit math. Here’s a quick example:

defmodule Foo do 
use ExDimensions.Math def do_some_math() do
q1 = ExDimensions.Spatial.inches(6)
q2 = ExDimensions.Spatial.inches(3)
q1 + q2
end
end
iex> IO.puts(Foo.do_some_math())
"9 in"

If incompatible units are added together, you’ll get an ArithmeticError :

defmodule Foo do 
use ExDimensions.Math def do_some_math() do
q1 = ExDimensions.Spatial.inches(6)
q2 = ExDimensions.Temperature.fahrenheit(3)
q1 + q2
end
end
iex> Foo.do_some_math()
** (ArithmeticError) bad argument in arithmetic expression: %ExDimensions.Quantity{denom: [], units: [ExDimensions.Spatial.Inches], value: 6} + %ExDimensions.Quantity{denom: [], units: [ExDimensions.Temperature.Fahrenheit], value: 3}
:erlang.+(%ExDimensions.Quantity{denom: [], units: [ExDimensions.Spatial.Inches], value: 6}, %ExDimensions.Quantity{denom: [], units: [ExDimensions.Temperature.Fahrenheit], value: 3})
iex:2: Foo.+/2

When doing multiplication and division, units are combined or canceled as appropriate:

defmodule Bar do 
use ExDimensions.Math def do_some_multiplication() do
q1 = ExDimensions.Spatial.inches(6)
q2 = ExDimensions.Spatial.inches(3)
q1 * q2
end def do_some_division() do
q1 = ExDimensions.Spatial.inches(6)
q2 = ExDimensions.Spatial.inches(3)
q1 / q2
end
end
iex> IO.puts(Bar.do_some_multiplication())
"18.0 in^2"
iex> IO.puts(Bar.do_some_division())
2.0

There is also a small DSL provided for conversions:

iex> import ExDimensions.Conversions 
iex> IO.puts(
ExDimensions.Spatial.feet(1) ~> ExDimensions.Spatial.Inches
)
"12 in"

Finally, there is a custom Ecto type for those interested in persisting unit aware quantities to their databases. It transparently converts between a serialized format for the quantity and the Quantity type in the library.

Currently, only a few types of quantities relevant to the manufacturing process are supported (spatial, volume, temperature) but new types are fairly easy to add and we expect to be doing so in the coming months. Pull requests are of course welcome too! The code can be found on Github and Hex.

--

--