Dimensional analysis in Rust: how not to crash "Mars Climate Orbiter"
Software is complicated: ensuring that old features keep working, while holding the bugs away, is an every day’s fight. Thankfully, there are many (too often unknown) gems, either built in the language/compiler or as third-party libraries.
Some time ago, I introduced SymPy, which allows one to use symbolic maths in Python, and do things such as solving equations or integrating functions. In this article, I will introduce you another nice library, for Rust this time: uom (units of measurement). This crate helps performing dimensional analysis. In short, it ensures that adding meters with feet will give the correct result (by converting everything in the same base unit), while adding meters with seconds will not be allowed.
uom
, it did not end well… - Wikipedia, CC BYUom
uom performs dimensional analysis, automatically, with type-safety and zero cost.
In layman’s terms, it means that instead of working with measurement units (meter, kilometer, foot, mile, etc), uom
works with quantities (in this example: length).
It uses the compiler’s type system (at compile-time, without run-time overhead), to ensure that quantities and units are not be mixed up.
If you try to add meters, kilometers and feet, uom will convert everything in the base unit (meters).
If you try to add meters with seconds, the compiler will error out because the types are not compatible.
As uom’s documentation puts it:
No more crashing your climate orbiter!
Source: uom’s README
Here is a short example, to demonstrate how it is used:
|
|
Why and how I use it
In the code of my Moon lander guidance software, I manipulate a lot of different types of data: position, velocity and acceleration, in both translation and rotation, but also times, masses, forces, and many others. They all have their own unit, and sometimes even several units (degrees and radians, depending on what I am doing). Some calculations can mix many different units: for instance, when computing aerodynamic drag, you start with a density and a velocity to get a pressure, which you then use with a length to obtain a a force, which you finally express as an acceleration using a mass. All of this is very error prone and a disaster is bound to happen (spoiler: it did, and was discovered thanks to uom).
Using uom ensures that there are no such easily detectable and fixable errors as mixed units. Letting the compiler work allows me to focus on better value-adding tasks: I can spend less time checking that the code is correct which allows me to add features faster. It increases my confidence in the code, which allows me to use my full brain power to focus on actual features and not second guessing everything I do.
Here are a few examples. First a simple one (acc/vel/pos) and second is a bit more complex (aerodynamic drag).
|
|
|
|
Pros & cons
Pros
I already had a lot of code, and I had to convert it to use uom. The library is rather easy and transparent to use, so it took me only a few hours to convert everything. It was worth it, as I found a bug: I was trying to compute the angular acceleration, from the pitch, but I went to fast and derived only once instead of two times. Uom caught the error with a clear error message.
The diff looks as follows (full code). It is part of a function which reads KSP’s sensors and convert the pitch into the angular acceleration:
|
|
Cons
Regarding cons, uom can sometimes be missing some seldom used quantities and units. For example, it does not have the moment of inertia unit. Thankfully, that’s an easy fix, and the maintainer welcomes Pull Requests.
Another minor con, that you might have noticed in the code examples, is that uom is a bit very verbose.
Measurements and quantities could have alternative short names: for example mps
instead of meter_per_second
.
The biggest downside is when wrapping uom quantities in structs:
I wanted to create a vector with two fields, to represent a vertical and horizontal position/velocity/acceleration.
It proved to be impossible to implements methods on this struct, such as adding a delta position to a position (vecpos += vecvel * dt
).
As a workaround, I use macros, but it does not seems very clean, and makes the compiler’s error messages more complicated.
Conclusion
As with everything in life, uom is not perfect (usage is a bit verbose, vectors are clunky, etc), but is nonetheless very useful (I found and fixed a bug after all): It is a way to improve the state of software and avoid some errors that can be made without this library.
Although a project using this library is safe on its own, there is one thing to watch out for: interfaces. That’s always where issues are, and in this case uom will not help: for example, if a Rust module using uom is interfaced with an Ada or C (embedded) or Python (ground station) module, uom is of no help in ensuring that the interfaces match. Software developers will not be jobless anytime soon…