Automatic Physical Units in Mathematica
I just published a Mathematica package that provides an alternative, richer implementation of units and dimensional analysis than the built-in units package. You can get it here. Aside from being a really nice extension to Mathematica, it is also an interesting case study in adding a custom data “type” to Mathematica and extending the knowledge of the built-in functions to handle the new “type”.
First I have to explain the point by answering the question, “What’s wrong with the built-in units package?” Well, there is nothing actually wrong with it, it just doesn’t apply Mathematica‘s automation principles. It can convert between several hundred units and warn if a requested conversion is dimensionally inconsistent. But give it an input like…
and it does nothing with it until you specify that you want the result in a specific unit. The core reason is that it doesn’t teach the system, as a whole, anything about units, or even that the symbol “Meter” is any different than the symbol “x”. All of the knowledge about units and Meter in particular is contained in the Convert command.
So in this package I want as many of Mathematica‘s commands as possible to do sensible things automatically when they encounter unit-based quantities. In order to do this in an extensible way, I don’t want to teach all these commands about Meter specifically, but I will create a new data type for units, so that I can teach the Mathematica commands about units abstractly. Just as in the existing version, the only function that needs to know about the ratio of Meter to, say, Inch, is Convert, though that might be invoked, in an abstracted way, by other commands.
My new data type is going to look (internally) like this:
And I am going to represent composite units with composites of the name in the second argument. For example:
So having created this type in my mind, what do I need to do to make it available to Mathematica? Well, nothing. Really, nothing! I can already enter and store data in that type and write functions that recognize that type because “type” is just an arbitrary symbol used as a wrapper for a collection of contents, and Mathematica eats symbolic data for breakfast!
We can proceed directly to teaching Mathematica what is special about the new type. Here is a typical rule to teach it what happens when you get powers of units.
The TagSetDelayed (shortcut notation /:) associates the rule with the symbol Unit. Generally it is good organization to keep the rules together, but it is also more efficient. More practically, it means that I don’t need to Unprotect Power. The rest is a pretty straightforward pattern that says if we apply a numeric power to a unit, the operation slips inside the Unit object and applies to each of the components of the object. The PowerExpand prevents us getting into silly units like Sqrt[Meter^2]], which is correct for complex variables, but not useful for positive real valued units.
Once evaluated, powers of unit data simplify automatically.
When you think about the range of mathematical functions that can be applied to units, they mostly fall into a fairly small number of categories that are roughly all the permutations of: single vs. multiple argument, unit discarding or preserving, operate on quantity or quantity and unit type, and so on. Once you collect them into groups of the same type, then you can usually come up with a single rule that can be used for each function in the group. Here is the collection for “functions of one argument that apply directly to the quantity only and remain in the original unit.” I use it to extend each function in this group (Abs through to Round) to handle units in the same way.
Here is one such example in action:
Of course, rules such as how the automatic axes labels work on visualizations of a grid of data in different units compatible only by column are a bit more complicated. None of these rules need any knowledge of the actual units; they can just make abstracted references to whether the input units are compatible and to a “common” unit for the output. For example, Min can only operate on units if they are all dimensionally compatible:
Once you have taught all the relevant functions in Mathematica how to handle Unit data in general, you are just left with the relatively simple task of describing how each unit behaves individually. That comes down to establishing a relationship chain between the definitions. In practice this can be simplified to a set of rules for each unit to and from a base unit representation, for example, that Unit[1, "Atmosphere"] Unit[101325, "Kilogram"/("Meter"*"Second"^2)]. Then any conversion can be reduced to converting to the base unit and then back to the new unit. Since unit values are now abstracted from all the logical code, we can just keep adding data. Over the course of a couple of very tedious days I added over 1,000 units, and you can add your own to the system too.
Finally, the internal structure isn’t necessarily what we want to enter or see. Fortunately, the parser and form matter in Mathematica are just two more commands that can be given rules for our new data type. We can hide the object structure in the same way that you never look at the copious data hidden inside InterpolatingFunction or CompiledFunction. So with a couple of rules for MakeBoxes and MakeExpression, any symbol for which there is a unit value will be parsed as a Unit expression, for example, the input “Meter” parses as Unit[1, "Meter"], but displays as “Meter”.
There are lots of details to doing a good job: efficiency considerations, syntactic sugar, auto-error correction or graceful fails under misuse, coverage of reals vs. exact numbers, and so on that take the package up to about 1,000 lines of code plus the 1,000 lines of unit definitions. But all of that is essentially handled in the same way as the three examples above—with additional rules for using this newly imagined symbolic data object under different circumstances.
I think the result is the most automated handling of units in any system. If it isn’t, let me know and I will add whatever rules are missing.
Here are some examples of the package in action.
Calculations involving units will automatically convert arguments into a suitable common default unit. In most cases the default is SI units.
Where input units are not compatible, no computation will be performed.
Composite units can be used by multiplying powers of units, and will also be handled automatically.
SimplifyUnits will try to find simpler equivalents to composite units.
Most operators, for which unit-based quantities make sense, will automatically handle units.
Some data constructors can support ranges or intervals using unit-based quantities. For example, here we generate 10 random quantities between 1 Yard and 1 Meter.
Or generate values between 10 Meters and 13 Yards in 1 Foot steps.
Many plotting routines will handle units, and will usually display the resulting units as AxesLabel.
Unit-based quantities have a head Unit, which is not shown in StandardForm.
In TraditionalForm, traditional short labels, with full-name tooltips, are used for the units.
You can convert quantities to specific units.
Or to the most appropriate unit from a list.
A large collection of pre-defined sets of units is available.
The available named unit sets is given by:
You can see or change the members of a particular set or create new sets.
You can declare new units.
Temperatures are assumed to represent temperature differences for standard unit conversions.
But their different origins are respected using ConvertTemperature.