Units

The two Python-side tiers of the units design: the in-house derived unit table that powers the dimensional lint, and the typed conversion facade behind the [units] extra:

pip install "longeron[units]"   # pint + pint-pandas

The core tier needs no extra: dimension vectors, SI factors, and scale tags are derived from the vendored quantities library’s own definitional algebra (newton = kg*m/s^2 survives in the model), and user packages shaped like the standard library derive the same way. The facade wraps pint so its Quantity never appears in a public signature – floats and unit strings in, floats out:

from longeron import units

units.convert(25.0, "°C", "K")  # 298.15
units.convert(3.0, "dBm", "mW")  # ~1.995
units.si_value(30.0, "min")  # 1800.0
units.si_unit("dBm")  # 'W'
units.format_quantity(0.254, "m")  # '0.254 m'  (no pint needed)
units.om_unit("min")  # 'min'      (no pint needed)
units.with_units(df, {"mass": "kg", "flightTime": "min"})

Unit strings are the model’s vocabulary – "SI::kg", "kg", "°C", "dBm" – resolved through the derived table first and mapped to pint spellings automatically (table-derived definitions cover units pint does not know). register_unit() overrides or extends the table; define() passes a raw pint definition through for boundary-side spellings.

Conversion seams reserved for 0.11

Per the units design, the conversion hooks into the analysis bridges are seams this release – documented here, wired next:

  • Declaration-boundary normalization. With the extra installed, evaluating a QuantityOp will multiply the declared magnitude through to canonical SI (si_value()), so instance slots hold SI floats and mixed same-dimension arithmetic becomes correct automatically. Until it lands, the mixed-units lint gate (units_extra_available()) marks the spot, and declared magnitudes pass through unchanged exactly as today.

  • OpenMDAO bridge. add_input / add_output calls in longeron.analysis.mdao gain units=om_unit(...) – values crossing the bridge are canonical SI after normalization, so the kwarg is the SI unit’s OM spelling ("kg", "m/s", "degK"). om_unit() is ready today and returns None where OM has no spelling (the dB family), leaving those variables unitless exactly as now.

  • Scoreboard. Tooltips render through format_quantity() in the declared display unit; ramp anchors declared in one unit score measures computed in another through one convert() at build time. Until then the anchor-dimension-mismatch lint flags the disagreement statically.

The interpreter invariant survives all three: evaluation, instance slots, M0 populations, and compute() bodies see only plain floats.

Units: dimension vectors, scale tags, and the [units] facade.

Two tiers live in this module (design: docs/design/units.md):

Core tier (stdlib only). Dimension vectors over the SI base – tuples of Fraction powers – each tagged with a scale (linear / offset / log). The unit table is derived from the vendored standard library’s own definitional algebra: base units come from the model’s SystemOfUnits declaration, derived units evaluate their definitional expressions (newton = kg*m/s^2) in unit space, prefixed units follow ConversionByPrefix, conventional units follow ConversionByConvention, IntervalScale seeds the offset tag (and marks its display unit, so a Celsius value can never pose as a linear kelvin), and the dB family seeds log. Any user or third-party unit package shaped like the standard library derives the same way, with no mapping table; register_unit() covers the rest. The dimensional lint in longeron.validation runs entirely on this tier – no third-party dependency.

Boundary tier (``pip install “longeron[units]”`` = pint). A typed facade over pint for everything that actually converts or pretty-prints: convert(), si_value(), si_unit(), format_quantity(), om_unit(), with_units(). pint’s Quantity never appears in a public signature – floats and unit strings in, floats out. The pint registry is a lazy module-level singleton seeded from the same derived unit table the lint uses; define() passes a raw pint definition through for boundary-side spellings the derivation cannot reach.

The interpreter invariant is untouched: evaluation, instance slots, M0 populations, and compute() bodies see only plain floats. Everything here is validation-time or boundary-time.

class longeron.units.Dim(exp)[source]

Bases: object

An exponent vector over a system’s base units.

exp holds one Fraction power per base unit, in the order the system of units declares them (for the vendored SI: m, kg, s, A, K, mol, cd). Closed under multiply / divide / rational power – exactly the quantity-dimension arithmetic of SysML v2 §9.8.9.

class longeron.units.UnitInfo(qname, dim, factor=1.0, offset=0.0, scale='linear', symbol=None, name=None)[source]

Bases: object

One unit’s derived semantics: vector, SI factor, scale tag.

factor and offset map a magnitude in this unit to canonical SI: si = value * factor + offset (linear and offset scales; the log scale needs real conversion, which is the boundary tier’s job). qname is the qualified name keyed on long names (SI::kilogram); symbol is the short name (kg).

class longeron.units.UnitTable(base_symbols=())[source]

Bases: object

Units and quantity dimensions derived from a model.

Lookup accepts qualified names on either the long name or the symbol (SI::kilogram, SI::kg) and bare names (kg, kilogram). quantity_dimension answers for quantity vocabulary – quantity attributes (ISQBase::mass), quantity value definitions (MassValue), and unit definitions (MassUnit) – which the lint uses to type attributes declared by quantity subsetting. User-registered overrides (register_unit()) are consulted first. prefixes carries the model’s own prefix vocabulary (SIPrefixes: symbol and long name -> factor), which prefix_splits() composes onto named units for symbols the model never names itself (mg).

base_symbols

symbols of the base units, in vector order (('m', 'kg', ...))

prefixes: dict[str, float]

prefix spelling -> conversion factor ('m'/'milli' -> 1e-3)

lookup(ref)[source]

The unit named ref (qualified or bare), or None.

Return type:

UnitInfo | None

quantity_dimension(qname)[source]

Dimension of a quantity attribute / value def / unit def.

Return type:

Dim | None

prefix_splits(ref)[source]

Model-derived prefix decompositions of a symbol the table does not name: ('m', 1e-3, <gram>) for mg.

Empty when the symbol IS a named unit (a name the model chose always wins – mm is millimetre, never decomposed) or when nothing decomposes. Only linear-scale bases compose, matching the model’s own ConversionByPrefix pattern. More than one entry means the spelling is genuinely ambiguous in scope; callers refuse rather than guess.

Return type:

list[tuple[str, float, UnitInfo]]

format_dim(dim)[source]

Render a vector as an SI-base formula: kg·m/s^2; 1.

Return type:

str

longeron.units.convert(value, from_unit, to_unit)[source]

Convert value between units of the model’s vocabulary.

Handles linear, offset, and logarithmic scales through pint: convert(25.0, "°C", "K") == 298.15, convert(3.0, "dBm", "mW") 1.995. Requires the [units] extra; raises MissingExtraError without it.

Return type:

float

longeron.units.define(definition)[source]

Pass a raw pint unit definition through to the facade’s registry.

The boundary-side escape hatch of the foreign-packages ruling: where derivation cannot reach and pint has no spelling, e.g. define("furlong = 201.168 * meter = fur"). Queued if the lazy registry is not built yet.

Return type:

None

longeron.units.derive_units(model, *, base=None)[source]

Derive a UnitTable from model’s definitional algebra.

Works on any model shaped like the vendored quantities library (finding 4 of the design): a SystemOfUnits with baseUnits seeds the basis, ConversionByPrefix / ConversionByConvention members inherit their reference unit’s vector, derived units evaluate their definitional expressions in unit space, and IntervalScale / the dB family seed the scale tags. base supplies an existing table (usually the standard one) whose basis and entries the new units may reference – so a user package declaring pound : MassUnit against kg derives with no mapping table.

Return type:

UnitTable

longeron.units.format_quantity(value, unit, *, precision=3)[source]

value in its declared display unit: format_quantity(0.254, "m") == '0.254 m'. Display only – no conversion, no pint needed.

Return type:

str

longeron.units.om_unit(unit)[source]

The OpenMDAO dialect spelling of unit, or None when OM has no equivalent (log-scale units, dimensionless, unknowns) – the variable then stays unitless, exactly as today. Pure table lookup: needs neither pint nor OpenMDAO installed.

Return type:

str | None

longeron.units.register_unit(qname, *, dim=None, factor=1.0, offset=0.0, scale='linear', symbol=None, aliases=(), pint=None)[source]

Register (or override) a unit the derivation cannot reach.

dim maps base-unit symbols to powers ({"m": 1, "s": -2}) or is a ready Dim over the standard basis. pint names the boundary-side spelling for the [units] facade (e.g. "dBm"); for spellings pint does not know, pass a raw definition through define() as well. The override is keyed on qname, the symbol, and every alias, and wins over derived entries everywhere (lint and facade alike).

Return type:

UnitInfo

longeron.units.si_unit(unit)[source]

The canonical SI spelling of unit’s dimension: si_unit("min") == "s"; si_unit("dBm") == "W".

Prefers the shortest named coherent SI unit from the derived table (W, not kg*m**2/s**3); falls back to pint’s base-unit spelling for dimensions the table has no name for.

Return type:

str

longeron.units.si_value(value, unit)[source]

The magnitude of value [unit] in canonical SI: si_value(25.0, "°C") == 298.15, si_value(30.0, "min") == 1800.0.

Return type:

float

longeron.units.standard_unit_table()[source]

The unit table derived from the vendored standard library (cached).

Returns an empty table when the standard library cannot load – callers degrade exactly like validation does.

Return type:

UnitTable

longeron.units.unit_table(model=None, *, include_standard=True)[source]

The table for validating model: the standard table extended with whatever unit packages the model itself carries.

Return type:

UnitTable

longeron.units.units_extra_available()[source]

True when the [units] extra (pint) is importable.

The mixed-units lint gates on this per the ratified kg + lbm ruling: with the extra, declaration-boundary normalization makes mixed same-dimension arithmetic correct (the normalization hook is the 0.11 interpreter seam); without it, the core tier converts nothing by design, so the lint warns.

Return type:

bool

longeron.units.with_units(df, units)[source]

A copy of df with pint-pandas dtypes applied per column: with_units(frame, {"mass": "kg", "flightTime": "min"}).

Column arithmetic then carries units (and raises on dimensional nonsense). Requires the [units] extra (pint + pint-pandas).

Return type:

DataFrame