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
QuantityOpwill 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, themixed-unitslint gate (units_extra_available()) marks the spot, and declared magnitudes pass through unchanged exactly as today.OpenMDAO bridge.
add_input/add_outputcalls inlongeron.analysis.mdaogainunits=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 returnsNonewhere 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 oneconvert()at build time. Until then theanchor-dimension-mismatchlint 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:
objectAn exponent vector over a system’s base units.
expholds oneFractionpower 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:
objectOne unit’s derived semantics: vector, SI factor, scale tag.
factorandoffsetmap a magnitude in this unit to canonical SI:si = value * factor + offset(linear and offset scales; thelogscale needs real conversion, which is the boundary tier’s job).qnameis the qualified name keyed on long names (SI::kilogram);symbolis the short name (kg).
- class longeron.units.UnitTable(base_symbols=())[source]¶
Bases:
objectUnits 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_dimensionanswers 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.prefixescarries the model’s own prefix vocabulary (SIPrefixes: symbol and long name -> factor), whichprefix_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', ...))
- prefix_splits(ref)[source]¶
Model-derived prefix decompositions of a symbol the table does not name:
('m', 1e-3, <gram>)formg.Empty when the symbol IS a named unit (a name the model chose always wins –
mmis millimetre, never decomposed) or when nothing decomposes. Only linear-scale bases compose, matching the model’s ownConversionByPrefixpattern. More than one entry means the spelling is genuinely ambiguous in scope; callers refuse rather than guess.
- longeron.units.convert(value, from_unit, to_unit)[source]¶
Convert
valuebetween 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; raisesMissingExtraErrorwithout it.- Return type:
- 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:
- longeron.units.derive_units(model, *, base=None)[source]¶
Derive a
UnitTablefrommodel’s definitional algebra.Works on any model shaped like the vendored quantities library (finding 4 of the design): a
SystemOfUnitswithbaseUnitsseeds the basis,ConversionByPrefix/ConversionByConventionmembers inherit their reference unit’s vector, derived units evaluate their definitional expressions in unit space, andIntervalScale/ the dB family seed the scale tags.basesupplies an existing table (usually the standard one) whose basis and entries the new units may reference – so a user package declaringpound : MassUnitagainstkgderives with no mapping table.- Return type:
- longeron.units.format_quantity(value, unit, *, precision=3)[source]¶
valuein its declared display unit:format_quantity(0.254, "m") == '0.254 m'. Display only – no conversion, no pint needed.- Return type:
- longeron.units.om_unit(unit)[source]¶
The OpenMDAO dialect spelling of
unit, orNonewhen 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.
- 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.
dimmaps base-unit symbols to powers ({"m": 1, "s": -2}) or is a readyDimover the standard basis.pintnames the boundary-side spelling for the[units]facade (e.g."dBm"); for spellings pint does not know, pass a raw definition throughdefine()as well. The override is keyed onqname, the symbol, and every alias, and wins over derived entries everywhere (lint and facade alike).- Return type:
- 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, notkg*m**2/s**3); falls back to pint’s base-unit spelling for dimensions the table has no name for.- Return type:
- 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:
- 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:
- 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:
- longeron.units.units_extra_available()[source]¶
True when the
[units]extra (pint) is importable.The
mixed-unitslint 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:
- longeron.units.with_units(df, units)[source]¶
A copy of
dfwith 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