skills/uncertainty-and-units/references/pint-recipes.md
Verified against pint 0.25.3 with NumPy 2.5.1. Every output below was produced by running the snippet.
A Quantity belongs to the registry that created it. Two registries produce quantities
that cannot interact, and the failure is a bare ValueError far from the cause:
import pint
first = pint.UnitRegistry()
second = pint.UnitRegistry()
first.Quantity(1, "m") + second.Quantity(1, "m")
# ValueError: Cannot operate with Quantity and Quantity of different registries.
This bites hardest across module boundaries, where each module innocently creates its own registry at import time, and after unpickling, because a pickled quantity is restored against the application registry rather than the one that created it.
Build one registry and share it, or use the application registry everywhere:
import pint
ureg = pint.UnitRegistry()
pint.set_application_registry(ureg)
# in every other module
ureg = pint.get_application_registry()
Degrees Celsius and Fahrenheit measure a point on a scale, not an amount, so multiplication and addition are undefined for them. Pint refuses rather than guessing:
Q = ureg.Quantity
Q(20, "degC") * 2
# OffsetUnitCalculusError: Ambiguous operation with offset unit (degree_Celsius).
Q(20, "degC") + Q(5, "degC")
# OffsetUnitCalculusError: Ambiguous operation with offset unit (...).
The delta units carry temperature differences, and mixed arithmetic works:
Q(20, "degC") + Q(5, "delta_degC") # 25 degree_Celsius
Q(25, "degC") - Q(20, "degC") # 5 delta_degree_Celsius
Note the second line: subtracting two absolute temperatures yields a delta unit automatically, which is correct and often surprising downstream.
An uncertainty on a temperature is always a difference. u = 0.5 degC means
0.5 delta_degC; converting it to Fahrenheit multiplies by 9/5 and applies no offset,
giving 0.9 delta_degF. Converting the value 20 degC to Fahrenheit applies the
offset and gives 68 degF. Two different conversions on the same line of a report.
pint.UnitRegistry(autoconvert_offset_to_baseunit=True) makes arithmetic proceed by
converting to kelvin first. It removes the exception, not the ambiguity; enable it
deliberately, not to silence an error.
Pint models dB, dBm, and friends as non-multiplicative units, and + on them means
what it means in log space — multiplication of the underlying linear quantities:
Q(10, "dBm").to("mW") # 10.000000000000002 milliwatt
Q(10, "dBm") + Q(10, "dBm") # 0.00010000000000000005 kilogram**2 * meter**4 / second**6
The second line is 10 mW × 10 mW = 10^-4 W², not 20 mW and not 13 dBm. Nothing raises. Convert to a linear unit, do the arithmetic, convert back.
Some conversions are physical relations rather than dimensional identities. Pint performs them only inside a named context, which is a feature: it forces the physics to be stated.
Q(532, "nm").to("THz", "sp") # 563.5196578947367 terahertz
Q(532, "nm").to("1/cm", "sp") # 18796.992481203004 / centimeter
Q(532, "nm").to("eV", "sp") # 2.3305300457368467 electron_volt
Q(532, "nm").to("THz") # DimensionalityError
Q(1, "g").to("mol", "chemistry", mw=Q(180.156, "g/mol")) # 0.005550744909966918 mole
Q(298.15, "K").to("eV", "boltzmann") # 0.02569257912108585 electron_volt
Q(1, "gauss").to("T", "Gaussian") # 9.999999999338245e-05 tesla
The registry ships spectroscopy (sp), chemistry (chem), boltzmann, energy,
textile, Gaussian (Gau), and ESU (esu). ureg.enable_contexts("sp") turns one
on for every subsequent conversion and ureg.disable_contexts() turns it off again;
prefer passing the context per call so the assumption stays visible at the point of use.
The spectroscopy context accepts a refractive index n, defaulting to 1 (vacuum). It
matters more than it looks:
Q(532, "nm").to("THz", "sp") # 563.5196578947367 terahertz
Q(532, "nm").to("THz", "sp", n=1.33) # 423.69899089829823 terahertz
Note that gauss fails without the Gaussian context: CGS electromagnetic units have
different dimensions from SI ones, not merely different scales.
.magnitude returns whatever number the quantity happens to be carrying, in whatever
unit it happens to be in. That is the single most common way a unit error enters a
correct-looking program:
length = (12.7 * ureg.mm).magnitude # 12.7 -- but of what?
length = (12.7 * ureg.mm).to("m").magnitude # 0.0127 metres, stated
length = (12.7 * ureg.mm).m_as("m") # same, shorter
Always name the unit at the point of extraction. m_as exists precisely so there is no
excuse.
Rather than sprinkling conversions through a function, convert once at its boundary:
@ureg.wraps("J", ("N", "m"))
def work(force, distance):
return force * distance
work(ureg.Quantity(2, "N"), ureg.Quantity(300, "cm")) # 6.0 joule
wraps strips the declared units on the way in and reattaches the result unit on the
way out, so the body is plain floats and stays fast. It defaults to strict=True,
which rejects bare numbers:
work(2.0, 3.0)
# ValueError: A wrapped function using strict=True requires quantity or a string
# for all arguments with not None units.
strict=False accepts bare numbers and assumes they are already in the declared units.
That is convenient and it is also exactly the assumption that unit tracking exists to
avoid; use it only at an edge you control.
check validates dimensionality without converting:
@ureg.check("[length]", "[time]")
def speed(distance, elapsed):
return distance / elapsed
speed(ureg.Quantity(10, "kg"), ureg.Quantity(2, "s"))
# DimensionalityError: Cannot convert from '10 kilogram' ([mass]) to 'a quantity of' ([length])
A quantity can wrap an array, and most ufuncs and many array functions are supported:
import numpy as np
a = ureg.Quantity(np.array([1.0, 2.0, 3.0]), "m")
np.mean(a) # 2.0 meter
np.concatenate([a, ureg.Quantity(np.array([100.0]), "cm")]) # [1.0 2.0 3.0 1.0] meter
np.concatenate([a, np.array([1.0])])
# DimensionalityError: Cannot convert from 'dimensionless' to 'meter'
Note that the mixed concatenation converted centimetres to metres correctly, and the bare array was rejected rather than assumed. Both behaviours are what you want.
Wrapped arrays carry per-operation overhead. In an inner loop, convert at the boundary
with wraps or m_as and compute on raw arrays.
ureg.define("cell = [cell_count] = cells")
ureg.define("od600 = [optical_density]")
ureg.define("percent_v_v = 0.01 = %v/v")
Defining a new base dimension in square brackets makes it dimensionally distinct from
everything else, which is the point: cells / mL will then refuse to be added to
particles / mL. Load a whole file of them with ureg.load_definitions("units.txt").
q = ureg.Quantity(1.2345, "kg*m/s**2")
f"{q}" # 1.2345 kilogram * meter / second ** 2
f"{q:~}" # 1.2345 kg * m / s ** 2
f"{q:.3f~P}" # 1.234 kg·m/s²
f"{q:~L}" # 1.2345\ \frac{\mathrm{kg} \cdot \mathrm{m}}{\mathrm{s}^{2}}
~ gives short unit symbols, P pretty Unicode, L LaTeX, C compact ASCII. Numeric
format specs come first and behave as usual.
The two libraries compose: a ufloat magnitude inside a pint quantity converts and
formats correctly.
from uncertainties import ufloat
q = ufloat(2.5, 0.1) * ureg.meter
q.to("cm") # 250+/-10 centimeter
f"{q:.2uS}" # 2.50(10) meter
pint-pandas provides a pandas extension dtype so a DataFrame column carries a unit;
pint-xarray does the same for xarray. Both are separate installs and both inherit the
one-registry rule.