Units & Dimensions ==================== :class:`~prophys.frames.Frame` and :class:`~prophys.domain.UncertainAttribute` have always carried a unit label. :mod:`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 :class:`~prophys.units.Unit` is a symbol, a :class:`~prophys.units.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 ----------------------- .. code-block:: python 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. :func:`~prophys.units.convert` works elementwise on anything supporting arithmetic — a float, a NumPy array, a JAX array, or a symbolic :class:`~prophys.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: .. code-block:: python 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: .. code-block:: python units.compatible("EUR", "USD") # False units.parse("EUR") / units.parse("EUR") # dimensionless, untagged Register a domain unit this package does not ship: .. code-block:: python 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: .. code-block:: python prp.Frame("site", units="m") # fine prp.Frame("site", units="kg") # UnitError A :class:`~prophys.frames.FrameTransform` with no explicit matrix applies the **unit conversion** between its two frames rather than an identity: .. code-block:: python 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: .. code-block:: python 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 :doc:`export_format`. .. Rendered here rather than only in the API reference, since this page is where the module is explained. ``:no-index:`` keeps the autosummary page as the single indexed home for the module object itself. .. automodule:: prophys.units :no-index: :members: Dimension, Unit, UnitError, parse, convert, compatible, check_compatible, conversion_factor, register_unit, describe, same_unit