Units & Dimensions#
Frame and
UncertainAttribute have always carried a unit
label. prophys.units makes those labels checkable, so a metre frame
combined with a kilometre field is an error rather than a silently wrong
answer.
A Unit is a symbol, a
Dimension over the seven SI base exponents, and a
scale to the SI base. Two rules follow: compatible units share a
dimension and convert into each other; incompatible units cannot be
combined at all.
Checking happens at Python model-build time, never inside the JAX trace, so
it costs nothing at run time — the same discipline Frame compatibility
already follows.
Parsing and converting#
from prophys import units
units.parse("kW") # kW [m^2*kg*s^-3] x1000
units.parse("kg*m/s^2") # the same dimension as N
units.convert(36.0, "km/h", "m/s") # 10.0
units.convert(20.0, "degC", "degF") # 68.0
units.conversion_factor("km", "m") # 1000.0
SI prefixes apply to a fixed list of base units, so min stays minutes
rather than becoming milli-inches. Compound expressions use *, / and
^; a repeated or trailing operator is a parse error rather than being
quietly ignored.
convert() works elementwise on anything supporting
arithmetic — a float, a NumPy array, a JAX array, or a symbolic
Expr — so a converted quantity can go straight
back into a model graph.
Affine temperature scales#
degC and degF convert by a scale and an offset, so they have no
single conversion factor and cannot appear in a compound unit. Both are
refused explicitly rather than silently dropping the offset:
units.conversion_factor("degC", "K") # UnitError
units.parse("W/degC") # UnitError
Use K for temperature differences.
Tagged dimensionless units#
Currency, counts and index scales are dimensionless but not interchangeable. A tag keeps them distinct:
units.compatible("EUR", "USD") # False
units.parse("EUR") / units.parse("EUR") # dimensionless, untagged
Register a domain unit this package does not ship:
from prophys.units import Unit, register_unit
register_unit(Unit("CHF", tag="currency-chf"), aliases=("franc",))
Frames#
A frame is a spatial coordinate system, so its unit must be a length — checked where the frame is declared:
prp.Frame("site", units="m") # fine
prp.Frame("site", units="kg") # UnitError
A FrameTransform with no explicit matrix applies the
unit conversion between its two frames rather than an identity:
plan = prp.Frame("plan", units="km")
site = prp.Frame("site", units="m")
transform = prp.FrameTransform(plan, site)
transform.apply(np.array([[1.0, 2.0]])) # [[1000., 2000.]]
Supplying a matrix takes responsibility for the whole map, unit scaling included.
Attributes#
An attribute validates its unit at construction and can convert its own values, refusing a conversion to a different quantity:
power = prp.UncertainAttribute("power", dist, unit="kW")
power.convert(1000.0, "MW") # 1.0
power.convert(1.0, "kWh") # UnitError: power is not energy
The unit also travels into an export, expanded into its dimension and SI scale, so a downstream consumer can check compatibility without reimplementing this parser — see Export Format.
Physical units and dimensional checking.
Frame and UncertainAttribute carry a unit string. This module turns
those labels into checkable quantities, so a metre frame is never silently
combined with a kilometre field, and an attribute declared in "kW" is
never calibrated against observations in "MW" without conversion.
A Unit is a symbol, a Dimension (the seven SI base
exponents), and a scale factor to the SI base. So km is
(length=1, scale=1000), kW is (mass=1, length=2, time=-3,
scale=1000), and m/s is a compound of the two. parse() builds
one from a string, accepting SI prefixes and the usual *, / and
^ composition.
Two rules follow:
Compatible units share a dimension and can be converted between (
convert()).Incompatible units cannot be combined at all, and the attempt raises
UnitErrorwhen the model is built rather than producing a number.
Checking happens in Python when the model is built, never inside the JAX trace, so it costs nothing at run time, the same discipline Frame compatibility already follows.
Units the SI does not define (currency, counts, arbitrary index scales) are
supported as dimensionless tagged units: two different tags never
convert into each other, so EUR and USD stay distinct while both
remain dimensionless.
- class prophys.units.Dimension(length=0.0, mass=0.0, time=0.0, current=0.0, temperature=0.0, amount=0.0, luminosity=0.0)[source]
Exponents of the seven SI base dimensions.
Compared by value, so
m/sandkm/hshare a dimension whilemandsdo not. Exponents are rationals in principle but are stored as floats so thatsqrt(m)(which appears in spectral density units) remains representable.- Parameters:
- property is_dimensionless: bool
True when every base dimension exponent is zero (a pure number).
- class prophys.units.Unit(symbol, dimension=Dimension(length=0.0, mass=0.0, time=0.0, current=0.0, temperature=0.0, amount=0.0, luminosity=0.0), scale=1.0, offset=0.0, tag=None)[source]
A unit of measurement: a symbol, a dimension, and a scale to SI.
scale is the multiplier taking a value in this unit to the SI base unit of its dimension: km has scale 1000, mm has scale 0.001. offset handles the two affine temperature scales, where conversion is not a pure multiplication. It is zero for every other unit.
tag distinguishes dimensionless units that must not be interchanged, such as currencies, counts and arbitrary indices. Two tagged units are compatible only when their tags match, so
EURnever silently becomesUSD.- property is_affine: bool
Whether conversion involves an offset as well as a scale, which makes the unit unusable in a product or a ratio (there is no meaningful “degrees Celsius per metre”).
- exception prophys.units.UnitError[source]
Raised when two quantities’ units are incompatible, or a unit string cannot be parsed.
- prophys.units.parse(text)[source]
Parse a unit expression such as
"m","km/h","kg*m/s^2".An empty string,
None,"1"and"-"all give the dimensionless unit, so an unannotated model keeps working unchanged. AUnitpasses through, which lets every API accept either form.
- prophys.units.convert(value, from_, to)[source]
Convert value from unit from_ to unit to.
Works elementwise on anything supporting arithmetic (a Python float, a NumPy array, a JAX array or a symbolic Expr), so a converted quantity can go straight back into a model graph. Affine temperature scales are converted through kelvin.
- prophys.units.compatible(a, b)[source]
Return whether two units share a dimension and a tag, and so convert into each other.
Unparseable units are reported as incompatible rather than raising.
- prophys.units.check_compatible(a, b, context='')[source]
Raise
UnitErrorunless a and b are compatible.context is prefixed to the message. Pass what was being combined so the error names the model construct rather than only the units.
- prophys.units.conversion_factor(from_, to)[source]
Return the multiplier taking a value from unit from_ into unit to.
Defined only for purely multiplicative units. An affine scale (degC, degF) has no single factor, so
convert()must be used for those.
- prophys.units.register_unit(unit, *, aliases=())[source]
Add a unit to the registry under its symbol and any aliases.
Use it for domain units this module does not ship, such as a currency, a counting unit or an index specific to a site:
register_unit(Unit("CHF", tag="currency"))
- prophys.units.describe(text)[source]
Return a description of a unit on one line: its symbol, dimension and SI scale, plus tag and offset if any.
Used in error messages and in ModelPackage metadata.
- prophys.units.same_unit(units)[source]
Return the first of a collection of unit labels after checking that all are compatible with it.
units may be a mapping, whose values are the labels, or an iterable. An empty collection gives the dimensionless unit.