Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Units

One of Oneil’s defining features is its unit-based type system. Oneil tracks units, disallows invalid operations between different physical properties, and automatically converts between differing units of the same physical property.

For example, Oneil will throw an error if you try to add a time and a distance or compare a mass and a temperature. But it will automatically convert a length in meters and a length in feet to a common base before adding them together.

This simplifies expressions to focus on relationships between physical properties while preventing unit conversion errors that might crash your spacecraft.

Units, dimensions, and magnitude

Before we get into how units work in Oneil, we’re going to take a quick detour to delve into what makes units compatible. Why can you add meters and kilometers, but not meters and kilograms? Why is a Joule equivalent to a Watt-second?

The answer is dimensions. A dimension can be defined as an aspect of something that can be measured. That definition is hard to understand on its own, though, so lets consider the dimension of time as an example.

A trip to the store (and more)

When measuring how long a car takes to get from your house to the store, it doesn’t matter whether you measure in seconds, minutes, or even millennia. They all are measurements of the dimension of time.

You can also measure how long it takes for you to get from the store to work, and you can measure that in any unit of time as well. Then, you could add the values together because they both measure the dimension of time.

However, it wouldn’t make sense to add the mass of your car to the measured travel time, since travel time is measured in the dimension of time, while car mass is measured in the dimension of mass.

Supported dimensions

Oneil supports the following dimensions, listed here with their associated base unit.

  • mass: kilogram
  • distance: meter
  • time: second
  • temperature: Kelvin
  • current: ampere
  • information: bit
  • currency: $ (USD)
  • substance: mole
  • luminous intensity: candela

Base units convey a single dimension, like the unit kilometer with its dimension distance. Derived units convey 0 to many dimensions, like the unit degree which is dimensionless or the unit Joule with its dimensions of of mass, distance^2, and time^-2. Dimensionless units are discussed in more detail later in this chapter.

Each unit has 0 or more dimensions associated with it. The kilometer is defined as having a dimension of distance, while a Joule would have the dimensions of mass, distance^2, and time^-2.

There are also dimensionless units such %. These are discussed later in this chapter.

If you haven’t quite wrapped your head around dimensions yet, don’t worry. You don’t need to fully understand it to use Oneil.

Magnitudes

So if kilometers and millimeters are the same dimensions, then what makes them different? The difference is in the magnitudes.

A magnitude is the relative size of a unit compared to the base unit. Relative to the base unit of meters, kilometers has a magnitude of 1000 since 1 km == 1000 m. Meanwhile, millimeters has a magnitude of 0.001 because 1 mm == 0.001 m.

Oneil tracks magnitudes and performs automatic conversions to handle units with different magnitudes. So when Oneil sees 1 m + 1 km, it knows that it needs to convert 1 km to 1000 m before adding. The result would therefore end up being 1001 m.

This automatic conversion also applies to units such as feet and meters, as well as more complex units like ft*lb/s^2 and Newtons, which could save your climate orbiter from a devastating crash.

Assigning units

Now that we’ve reviewed the motivation behind tracking units, let’s get into the practical application. For parameters with a literal value, units can be assigned with the :<unit> syntax:

# velocity.on
Distance: d = 100 :meters
Travel time: t = 20 :seconds

This will assign d to a value of 100 :meters and t to a value of 20 :seconds. These values are now measured numbers, or numbers with units.

Note

There are often multiple synonyms for a given unit. For example, the above model could also be written as

Distance: d = 100 :m
Travel time: t = 20 :s

To see a list of all builtin units and their synonyms, run oneil builtins unit. Also, if you would like to search for a given unit, run oneil builtins unit <unit>.

Annotating expected units

For calculated parameters, the :<unit> syntax declares the expected units of a calculation, which Oneil checks.

For example, using d and t from the previous section, we can then define velocity as

# velocity.on (continued)
$ Velocity: v = d/t :m/s

defining a parameter with a measured value with the units m/s.

Running the model with oneil eval velocity.on produces

v = 5 :m/s  # Velocity

If we wanted to, we could just as easily define velocity in kilometers per hour.

$ Velocity: v = d/t :km/hr
#                    ^^^^^ `km/hr` instead of `m/s`
oneil eval velocity.on
v = 18 :km/hr  # Velocity

Note that we did not have to do any conversions. Oneil handles that for us. However, if we try to use incorrect units, Oneil will produce an error.

$ Velocity: v = d/t :kg/hr
#                    ^^ `kg` instead of `km`
oneil eval velocity.on
error: calculated unit does not match expected unit
 --> velocity.on:3:17
  | 
3 | $ Velocity: v = d/t :kg/hr
  |               ^--
  = note: calculated unit is `meters/seconds` but expected unit is `kg/hr`

In addition, Oneil requires units on any parameters whose calculations are expected to produce a measured value. If we leave out the unit, we get an error.

Likewise, the calculation for a unitless parameter should not have a measured result. In that case, we do leave the units out (see dimensionless values).

$ Velocity: v = d/t
#                   ^ No unit
oneil eval velocity.on
error: parameter is missing a unit
 --> velocity.on:3:17
  | 
3 | $ Velocity: v = d/t
  |                 ^--
  = note: parameter value has unit `meters/seconds`
  = help: add a unit annotation `:meters/seconds` to the parameter

Composing units in a unit expression

A unit expression is built from one or more units separated by * or /. Each unit can be raised to a numeric power with ^, such as s^2.

Unit expressions can also use the literal 1 as a dimensionless unit. This is used in rates such as 1/s.

Warning

Multiplication and division operate left to right. So J/kg*K is treated as (J/kg)*K rather than J/(kg*K).

To express J/(kg*K), explicit parentheses are required.

Unit casting

Imagine that you have a test that takes time to start up before it runs. The full time of the test is 5 minutes and start-up time is 10 seconds. To calculate what the actual run time of the test is, you might write the following model.

# testing.on
Full time: t_full = 5 :min
$ Run time: t_run = t_full - 10 :min

However, this will produce an error.

oneil eval testing.on
error: expected scalar with unit `min` but found scalar
 --> testing.on:2:30
  | 
4 | Run time: t_r = t_f - 10 :min
  |                       ^-

In other words, Oneil can’t determine whether 10 is supposed to be 10 seconds, 10 minutes, or 10 hours.

The first recommended solution is to create another parameter to hold this “magic number”. You can then define a unit on that parameter.

# testing.on
Full time: t_full = 5 :min
Startup time: t_start = 10 :s
$ Run time: t_run = t_full - t_start :min
oneil eval testing.on
t_run = 4.833 :min  # Run time

However, there are some situations where you may just want to label a unitless number with a unit.

To do so, you can use unit casting. Unit casting takes the form of (<expression> : <unit>). This allows a unitless value to be assigned a unit.

Using this, the model could be rewritten as

# testing.on
Full time: t_full = 5 :min
$ Run time: t_run = t_full - (10:s) :min
oneil eval testing.on
t_run = 4.833 :min  # Run time

Arithmetic and comparison operators

Arithmetic and comparison operator rules and behavior are defined by the following table. The unit of a given value x is indicated by x_unit.

OperationInput RulesUnit Output
a + b, a - b, a % ba_unit and b_unit must have the same dimensionsa_unit
a * bNonea_unit * b_unit
a / bNonea_unit / b_unit
a ^ bb cannot have any dimensionsa_unit ^ b
comparison (<, >, <=, >=, ==, !=)a_unit and b_unit must have the same dimensionsN/A (produces true or false)

Examples

Note

empty.on is just an empty model, since we don’t reference any model parameters.

# addition, subtraction, modulo
oneil eval empty.on \
  -x "(1000:m) + (1:km)" \
  -x "(1:km) + (1000:m)" \
  -x "(5:min) - (30:s)" \
  -x "(80:s) % (1:min)"
(1000:m) + (1:km) = 2e3 :m
(1:km) + (1000:m) = 2 :km
(5:min) - (30:s) = 4.5 :min
(80:s) % (1:min) = 20 :s
# multiplication, division, exponentiation
oneil eval empty.on \
  -x "(1:m) * (1:s)" \
  -x "(1:m) * (1:m)" \
  -x "(1:m) * 1" \
  -x "(1:m) / (1:s)" \
  -x "(1:m) / 1" \
  -x "(1:m)^2"
(1:m) * (1:s) = 1 :m*s
(1:m) * (1:m) = 1 :m*m
(1:m) * 1 = 1 :m
(1:m) / (1:s) = 1 :m/s
(1:m) / 1 = 1 :m
(1:m)^2 = 1 :m^2
# comparison
oneil eval empty.on \
  -x "(1:kg) < (2000:g)" \
  -x "(1:kg) > (1:g)" \
  -x "(1:kg) <= (1000:g)" \
  -x "(1:kg) >= (900:g)" \
  -x "(1:kg) == (1000:g)" \
  -x "(1:kg) != (1:g)"
(1:kg) < (2000:g) = true
(1:kg) > (1:g) = true
(1:kg) <= (1000:g) = true
(1:kg) >= (900:g) = true
(1:kg) == (1000:g) = true
(1:kg) != (1:g) = true

strip

In the case that you would like to treat a measured value as unitless, Oneil provides the strip function. The strip function removes any units from a value.

# adc.on
ADC bit resolution: S_adc = 10 :b
$ ADC step count: n_adc = 2^(strip(S_adc)-1)
oneil eval adc.on
n_adc = 512  # ADC step count

The places where this should be used are rare and should be treated cautiously since strip effectively disables unit checking.

In addition, it is important to realize that strip strips the unit that is currently associated with a value.

# length.on
Length in meters: l_m = 1000 :m
Length in kilometers: l_km = 1 :km
oneil eval length.on \
  -x "strip(l_m)" \
  -x "strip(l_km)"
strip(l_m) = 1e3
strip(l_km) = 1

For this reason, when using strip, it is recommended that you first cast the value to the unit that you expect it to be.

oneil eval length.on \
  -x "strip((l_m :m))" \
  -x "strip((l_km :m))"
strip((l_m :m)) = 1e3
strip((l_km :m)) = 1e3

Non-linear units

On top of linear units, Oneil supports decibel (dB) units. You form a decibel unit by prefixing dB directly to a built-in unit name, for example dBmW (decibels relative to one milliwatt) or dBV. The bare name dB (with no following unit) is also valid; it behaves as a dimensionless logarithmic unit.

Support for other non-linear units is on the roadmap.

When any unit is specified with prefix dB, Oneil internally converts the parameter to the corresponding linear value, performs all calculations in linear terms, and reconverts the value to dB for display. This means that equations that contain parameters with dB units should use linear math. For example, when calculating the signal to noise ratio by hand, you might subtract the noise (dB) from the signal (dB), but in Oneil, you divide the signal by the noise:

# power.on
Noise power: P_n = -100 :dBmW
Signal power: P_s = -90 :dBmW
$ Signal-to-noise ratio: S_N = P_s/P_n
oneil eval power.on
S_N = 10  # Signal-to-noise ratio

Dimensionless units

There are some units that don’t have any dimensions, such as % or ppm (parts per million). These units are referred to as dimensionless units, and values with dimensionless units are referred to as dimensionless values.

Unitless equivalence

Dimensionless values can be treated as if they have no unit. The following demonstrates this with the % unit.

Note

empty.on is just an empty model, since we don’t reference any model parameters.

# `100%` is treated as equal to `1`
oneil eval empty.on \
  -x "(100:%) == 1" \
(100:%) == 1 = true
# the `1` is equal to `100%`, not `1%`
oneil eval empty.on \
  -x "(100:%) + 1"
(100:%) + 1 = 200 :%

Angular Units

The lack of distinction between dimensionless values and unitless values is especially important when it comes to units involving radians. The International System of Units treats radians as dimensionless, and Oneil has opted to follow this convention. Therefore, all angular units (such as radians, degrees, and revolutions) are specified in radians. Therefore, when adding a unitless number to an angular value, the unitless number is treated as if it is specified in radians.

oneil eval empty.on \
  -x "(1:rad) == 1" \
  -x "(360:deg) == 2*pi" \
  -x "(1:rad) + 1" \
  -x "(360:deg) + 2*pi"
(1:rad) == 1 = true
(360:deg) == 2*pi = true
(1:rad) + 1 = 2
(360:deg) + 2*pi = 720 :deg

Hz and rad/s

There is one place where Oneil’s automatic conversions might cause confusion. That is with the Hz unit. In order to solve the problem described in this article and make Hz compatible with rad/s, Oneil defines Hz as

1 Hz == 1 cycle/s == 2*pi rad/s.

Note that both cycles and radians are both dimensionless values, but 1 cycle == 2*pi radians.

# freq.on
Frequency: f = 1 :Hz
oneil eval freq.on \
  -x "f" \
  -x "(f :cycle/s)" \
  -x "(f :rad/s)" \
f = 1 :Hz
(f :cycle/s) = 1 :cycle/s
(f :rad/s) = 6.283 :rad/s

By default, Oneil treats dimensionless values as if they are in radians. Because of this, anytime you would like dimensionless values to be in cycles, you need to manually convert from radians to cycles by dividing by 2*pi.

# freq2.on
Frequency: f = 5 :GHz
Speed of light: c = 299792458 :m/s

$ Wavelength: lambda = c/(f/2*pi) :cm
#                          ^^^^^ Need to divide by 2*pi to convert radians to cycles
oneil eval freq2.on
lambda = 0.6075 :cm  # Wavelength