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 have always carried a units string, but nothing read it: a metre frame combined with a kilometre field produced a silently wrong answer, and an attribute declared in “kW” could be calibrated against observations in “MW” without complaint. This module turns those labels into checkable quantities.
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
UnitErrorat model-build time rather than producing a number.
Checking is at Python model-build time, 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; they are stored as floats so thatsqrt(m)(which appears in spectral density units) remains representable.
- 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 — currencies, counts, 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.- Parameters:
text (str | Unit | None)
- Return type:
Unit
- prophys.units.convert(value, from_, to)[source]
Convert value from one unit to another.
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.
- prophys.units.compatible(a, b)[source]
Whether two units share a dimension and a tag, and so convert into each other.
- 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]
Multiplier taking a value from from_ into 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 — a currency, a counting unit, a site-specific index:
register_unit(Unit("CHF", tag="currency"))
- prophys.units.describe(text)[source]
A one-line description of a unit: its symbol, dimension and SI scale. Used in error messages and in ModelPackage metadata.