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 UnitError at 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/s and km/h share a dimension while m and s do not. Exponents are rationals in principle; they are stored as floats so that sqrt(m) (which appears in spectral density units) remains representable.

Parameters:
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 EUR never silently becomes USD.

Parameters:
  • symbol (str)

  • dimension (Dimension)

  • scale (float)

  • offset (float)

  • tag (str | None)

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. A Unit passes 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.

Parameters:
  • value (Any)

  • from_ (str | Unit | None)

  • to (str | Unit | None)

Return type:

Any

prophys.units.compatible(a, b)[source]

Whether two units share a dimension and a tag, and so convert into each other.

Parameters:
  • a (str | Unit | None)

  • b (str | Unit | None)

Return type:

bool

prophys.units.check_compatible(a, b, context='')[source]

Raise UnitError unless 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.

Parameters:
  • a (str | Unit | None)

  • b (str | Unit | None)

  • context (str)

Return type:

None

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.

Parameters:
  • from_ (str | Unit | None)

  • to (str | Unit | None)

Return type:

float

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"))
Parameters:
Return type:

None

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.

Parameters:

text (str | Unit | None)

Return type:

str

prophys.units.same_unit(units)[source]

The single unit shared by a collection, or raise.

Used where several annotated objects must agree before being combined — the attributes of one interaction, the fields of one frame.

Parameters:

units (Mapping[str, Any] | Iterable[Any])

Return type:

Unit