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

std.units

import std.units as u provides the SI base dimensions, named derived dimensions, units, physical constants and functions over the built-in Quantity[T, D]. The module is ordinary embedded Plenty: dimensions are pub dimension declarations and units are pub const quantities. Nothing in it allocates.

import std.units as u
from std.units import Velocity, km, hour, m, s

def main() -> Result[(), IoError | AllocError]:
    speed: Quantity[f64, Velocity] = 90.0 * km / hour
    braking = 6.0 * m / (s * s)
    distance = speed * speed / (2.0 * braking)
    print(speed, speed.to(u.knot))?
    print(f"{distance.to(m):.1f} m, {(speed / braking).to(s):.2f} s"?)?
    Ok(())
25.0 m/s 48.59611231101511
52.1 m, 4.17 s

Every unit is a Quantity[f64, D] holding its size in the coherent SI unit, so km stores 1000.0. Construction multiplies by the unit and to divides by it; both are folded at compile time when the operands are constants. A conversion between two non-coherent units divides two rounded values, so it can round: u.foot.to(u.inch) is 12.000000000000002. The stored values themselves are the nearest f64 to each exact definition.

Printing shows the coherent value and SI symbols in SI order (kg, m, s, A, K, mol, cd), so a newton prints as kg*m/s^2; see formatting.

Dimensions

BaseCoherent unit
Masskg
Lengthm
Times
CurrentA
TemperatureK
Amountmol
Luminositycd

Derived dimensions are aliases; each names a different dimension, so diagnostics can name it:

GroupDimensions
Geometry and motionArea, Volume, Wavenumber, Velocity, Acceleration, Frequency, Momentum
MechanicsForce, Pressure, Energy, Power, Action, Density, SurfaceTension, Gravitation
FlowMassFlowRate, VolumeFlowRate, DynamicViscosity, KinematicViscosity
HeatHeatFlux, SpecificEnergy, Entropy, SpecificHeatCapacity, ThermalConductivity, StefanBoltzmann
ChemistryMolarMass, MolarEnergy, MolarHeatCapacity, Concentration, PerAmount, CatalyticActivity
ElectromagnetismCharge, Voltage, ElectricField, Capacitance, Permittivity, Resistance, Conductance, MagneticFlux, MagneticFluxDensity, Inductance, Permeability
LightIlluminance, LuminousEfficacy

SI gives some different kinds of quantity one dimension. Energy also describes torque, Frequency also describes activity in becquerels, and SpecificEnergy also describes absorbed and equivalent dose. A program that needs to keep such kinds apart declares its own base dimension.

Units

SI units, their prefixed forms, eV and Wh use their symbols. Other units use names. Micro is written u: um, us, uF.

DimensionUnits
Massug, mg, g, kg, tonne, dalton, pound, ounce
Lengthpm, nm, um, mm, cm, m, km, inch, foot, yard, mile, nautical_mile, angstrom, au, light_year, parsec
Timeps, ns, us, ms, s, minute, hour, day, week, year
CurrentuA, mA, A, kA
TemperaturemK, K, rankine
Amountumol, mmol, mol, kmol
Luminositycd, lm
Onerad, sr, degree, arcminute, arcsecond
Area, Volumehectare, acre; litre, millilitre, cubic_metre, gallon (US liquid)
Velocitykm_per_hour, mph, knot
FrequencyHz, kHz, MHz, GHz, Bq
ForcemN, N, kN, MN, pound_force
PressurePa, hPa, kPa, MPa, GPa, mbar, bar, atm, torr, mmHg, psi
EnergymJ, J, kJ, MJ, GJ, Wh, kWh, MWh, GWh, eV, keV, MeV, GeV, calorie, kilocalorie, btu
PoweruW, mW, W, kW, MW, GW, horsepower
ChargenC, uC, mC, C
VoltageuV, mV, V, kV, MV
CapacitancepF, nF, uF, mF, F
Resistance, Conductanceohm, kohm, Mohm; uS, mS, S
MagnetismWb; uT, mT, T; nH, uH, mH, H
Otherlx, Gy, Sv, kat

There is no milliohm constant, because mohm could be read as milli or mega; write 1e-3 * u.ohm. Angles are dimensionless, as in SI, so u.degree is Quantity[f64, One] holding π/180. calorie is the thermochemical calorie (4.184 J), btu the International Table BTU, horsepower the mechanical horsepower (550 ft·lbf/s), mmHg the conventional millimetre of mercury and year 365 days. Celsius and Fahrenheit are not units here; see temperature.

std.units.catalogue adds every SI prefix on every SI unit and the other named units of NIST SP 811. It is a separate module so that only programs importing it pay for its 768 constants.

Constants

ConstantValueStatus
delta_nu_Cs9192631770 Hz, caesium hyperfine frequencyExact SI defining constant
c299792458 m/s, speed of lightExact SI defining constant
h6.62607015e-34 J·s, Planck constantExact SI defining constant
e1.602176634e-19 C, elementary chargeExact SI defining constant
k_B1.380649e-23 J/K, Boltzmann constantExact SI defining constant
N_A6.02214076e23 /mol, Avogadro constantExact SI defining constant
K_cd683 lm/W, luminous efficacyExact SI defining constant
hbarh / (2π)Exact, rounded to f64
RN_A * k_B, molar gas constantExact, rounded to f64
sigma5.670374419e-8 W/(m²·K⁴), Stefan–Boltzmann constantExact value truncated as CODATA lists it
g_n9.80665 m/s², standard gravityExact by definition
atm101325 Pa, standard atmosphereExact by definition
G6.67430e-11 m³/(kg·s²), gravitational constantCODATA 2022
m_e, m_p, m_uElectron, proton and atomic mass constantCODATA 2022
epsilon_0, mu_0Vacuum permittivity and permeabilityCODATA 2022

Functions

FunctionResultContract
abs[T: IntType, D](x)Result[Quantity[T, D], OverflowError[T]]std.math.abs of integer storage; a signed minimum returns its overflow
abs_float[T: FloatType, D](x)Quantity[T, D]std.math.abs_float of floating storage
clamp[Num, D](x, low, high)Quantity[Num, D]math.min(math.max(x, low), high), so low > high gives high; any storage
sum[D](items: &list[Quantity[f64, D]])Quantity[f64, D]Neumaier compensated sum; an empty list gives zero
mean[D](items: &list[Quantity[f64, D]])Option[Quantity[f64, D]]sum(items) / len(items); Nothing for an empty list
sqrt[D](x: Quantity[f64, D])Result[Quantity[f64, Root[D, 2]], MathError]std.math.sqrt, with its errors
cbrt[D](x: Quantity[f64, D])Result[Quantity[f64, Root[D, 3]], MathError]std.math.cbrt, with its errors
hypot[D](a, b)Quantity[f64, D]sqrt(a*a + b*b) scaled to avoid intermediate overflow; infinity if either side is infinite
exp, log, log2, log10Result[Quantity[f64, One], MathError]Take Quantity[f64, One]; std.math errors
sin, cos, tanResult[Quantity[f64, One], MathError]Take an angle as Quantity[f64, One] in radians
asin, acos, atanResult[Quantity[f64, One], MathError]Return an angle in radians
atan2[D](y, x)Result[Quantity[f64, One], MathError]Both arguments have dimension D; the angle of (x, y)

Every argument is a copied quantity, except sum and mean, which borrow the list. Functions built on std.math take f64 storage, as std.math does. abs takes integer storage and abs_float floating storage, as in std.math; clamp accepts any storage. The generic std.math functions min, max, min_all and max_all already accept quantities, so this module does not repeat them. math.sum and math.prod take plain integers and math.abs a plain integer, so they do not accept quantities. A square or cube root whose dimension is not whole is a compile error at the call that needs it:

import std.units as u
import std.math as math

def main() -> Result[(), math.MathError]:
    side = u.sqrt(2.0 * u.litre)?
    Ok(())
`Root[Pow[std.units.Length, 3], 2]`: not a whole dimension

A root derives its dimension, so a formula’s result is checked. Falling through a tunnel across a uniform-density Earth is simple harmonic motion: the trip to the centre takes a quarter period, (π/2)·sqrt(R/g), and the speed there is sqrt(g·R):

import std.units as u
from std.units import km, m, s, minute
import std.math as math

def main() -> Result[(), IoError | AllocError | math.MathError]:
    radius = 6371.0 * km
    to_centre = f64.PI / 2.0 * u.sqrt(radius / u.g_n)?
    top_speed = u.sqrt(u.g_n * radius)?
    print(f"{to_centre.to(minute):.1f} min, {top_speed.to(km / s):.2f} km/s"?)?
    Ok(())
21.1 min, 7.90 km/s

Dimensionless results stay quantities, so an angle or a ratio combines with other quantities without .get():

import std.units as u
from std.units import m
import std.math as math

def main() -> Result[(), IoError | AllocError | math.MathError]:
    rise = 3.0 * m
    run = 4.0 * m
    angle = u.atan2(rise, run)?
    slope = u.hypot(rise, run)
    print(f"{angle.to(u.degree):.2f} degrees, {slope}"?)?
    print(slope * u.cos(angle)?, u.sqrt(rise * run)?)?
    lengths = [rise, run, slope]?
    print(u.sum(&lengths), u.mean(&lengths))?
    Ok(())
36.87 degrees, 5.0 m
4.0 m 3.4641016151377544 m
12.0 m Option[Quantity[f64, std.units.Length]].Some(Quantity[f64, std.units.Length](4.0))

Lists

FunctionResultContract
wrap_list[D](values: list[f64])list[Quantity[f64, D]]Consumes values and returns the same list with quantity elements
strip_list[D](items: list[Quantity[f64, D]])list[f64]Consumes items and returns the same list with plain elements

Use these at the boundary with code that produces or expects plain numbers. Both are infallible and run in constant time: a quantity row is stored exactly as its f64, so the conversion replaces only the list’s type descriptor. No element is read or copied, and nothing is allocated; see representation. D is given explicitly to wrap_list and inferred by strip_list:

import std.units as u
from std.units import Length, km

def main() -> Result[(), IoError | AllocError]:
    laps = u.wrap_list[Length]([400.0, 400.0, 1609.344]?)
    total = u.sum(&laps)
    print(f"{total.to(km):.3f} km"?)?
    metres = u.strip_list(laps)
    print(metres)?
    Ok(())
2.409 km
[400.0, 400.0, 1609.344]

Temperature and time

FunctionResultContract
from_celsius(degrees: f64)Quantity[f64, Temperature]degrees + 273.15 kelvin
to_celsius(t)f64t in kelvin minus 273.15
from_fahrenheit(degrees: f64)Quantity[f64, Temperature]Through Celsius: (degrees - 32) * 5/9 + 273.15
to_fahrenheit(t)f64Through Celsius: (t - 273.15) * 9/5 + 32
from_duration(span: &Duration)Quantity[f64, Time]Seconds in f64
to_duration(t: Quantity[f64, Time])Result[Duration, RangeError]Rounds to the nearest nanosecond; RangeError.OutOfRange for a negative, NaN or too-large time

Celsius and Fahrenheit have offset zeros, so they are not multiplicative units. They convert at the boundary to an absolute temperature; a temperature difference in degrees Celsius has the same size as one in kelvin. Going through Celsius keeps whole Celsius values exact: from_fahrenheit(212.0) is 373.15 K.

A std.time.Duration counts nanoseconds as u64. from_duration converts whole seconds and the remainder separately, but the result is still an f64: spans longer than about 104 days (2^53 nanoseconds) no longer resolve single nanoseconds.

import std.units as u
import std.time as time

def main() -> Result[(), IoError | RangeError]:
    boil = u.from_fahrenheit(212.0)
    print(boil, u.to_celsius(boil), u.to_fahrenheit(u.from_celsius(-40.0)))?
    timeout = u.to_duration(1.5 * u.minute)?
    print(timeout.as_seconds(), u.from_duration(&time.milliseconds(250)))?
    print(u.to_duration(-1.0 * u.s).is_err())?
    Ok(())
373.15 K 100.0 -40.0
90 0.25 s
True

Limits

  • Units and functions use f64 storage. A quantity with f32 storage converts with Quantity[f32, D].from(q).
  • wrap_list and strip_list take f64 storage only.
  • Named units are not printed: a joule prints as kg*m^2/s^2.

Acknowledgements

  • Exact unit definitions are from the SI Brochure (9th edition) and NIST SP 811, appendix B.
  • Measured constants are the CODATA 2022 recommended values.
  • misu and Frink informed the choice of units; Frink’s samples suggested the tunnel example.