Skip to content

visualdynamics.units

units

Unit handling for visualdynamics.

Values whose units are known are stored in SI (m, kg, s, N, Pa, K, ...). Values imported from a source that does not declare units are stored exactly as they appear in the file and tagged unknown until the user defines units; defining units converts them to SI once and records the unit chosen, so a wrong guess can be corrected without losing anything.

Conversions are affine — si = raw * scale + offset — so offset scales like degC and degF are handled correctly alongside purely multiplicative units.

pint provides the registry but is kept internal to this module: core objects carry plain numpy arrays plus a dimension tag (a string like 'length', 'acceleration', or a compound expression like 'acceleration/force').

Classes:

Name Description
UnitError

A unit was wrong: unreadable, or not the dimension asked for.

UnitsRequired

Raised when an operation needs units that have not been defined.

UnitSystem

A named mapping of dimension -> display unit.

Functions:

Name Description
unit_registry

The one pint registry, built on first use.

normalize_unit

A unit string as visualdynamics reads it, before pint sees it.

pretty_unit

A unit string with real exponents, for showing: 'm/s**2' -> 'm/s²'.

plain_unit

The inverse of pretty_unit, so a shown unit can be read back.

is_compound_unit

Does it need brackets before something is done to the whole of it?

parse_dimension

Parse a dimension expression into (base_dimension, power) parts.

si_transform

(scale, offset) such that si_value = value * scale + offset.

si_factor

Multiplier converting values in unit to SI.

to_si

values, read as unit, in SI. Handles offsets, so degrees

from_si

SI values expressed in unit — the inverse of to_si.

convert

values from one unit to another, through SI.

dimension_of

The visualdynamics base dimension matching a unit string, or None.

Classes

UnitError

Bases: ValueError

A unit was wrong: unreadable, or not the dimension asked for.

A ValueError, because that is what a bad value is, and a named one so the app can tell a unit problem it should explain from a programming error it should not swallow.

UnitsRequired

UnitsRequired(
    message: str, required: Sequence[str] = ("length_unit",)
)

Bases: UnitError

Raised when an operation needs units that have not been defined.

Source code in src/visualdynamics/units.py
def __init__(self, message: str,
             required: Sequence[str] = ('length_unit',)) -> None:
    super().__init__(message)
    self.required: tuple[str, ...] = tuple(required)

UnitSystem dataclass

UnitSystem(
    name: str,
    units: dict = dict(),
    base: UnitSystem | None = None,
)

A named mapping of dimension -> display unit.

base is the coherent system this one came from — the same object for a coherent system, and the parent for one carrying display-only overrides such as accelerations in g. Exports use it, because no foreign format can record "g" as a unit.

Methods:

Name Description
unit

Display unit for a dimension or dimension expression.

transform

(scale, offset) converting a display value to SI.

factor

SI-per-display-unit scale (offset-free dimensions only).

from_si

Convert SI values to this system's display unit for dimension.

to_si

Convert values from this system's units into SI.

label

Display unit text; empty when the dimension is undefined.

label_text

Display unit for plain-text output, with real exponents: in/s².

label_ascii

Display unit in plain ASCII, exponents as carets: in/s^2.

label_html

Display unit as HTML — a stacked fraction when it has one.

with_units

A copy of this system with per-dimension unit overrides.

Attributes:

Name Type Description
coherent UnitSystem

This system with display-only overrides stripped.

Attributes
coherent property
coherent: UnitSystem

This system with display-only overrides stripped.

Methods:
unit
unit(dimension: str) -> str

Display unit for a dimension or dimension expression.

Expressions compose from the base units: with in-lbf-s, 'acceleration/force' -> '(in/s**2)/lbf'.

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
str

The unit string for this dimension.

Source code in src/visualdynamics/units.py
def unit(self, dimension: str) -> str:
    """Display unit for a dimension or dimension expression.

    Expressions compose from the base units: with in-lbf-s,
    'acceleration/force' -> '(in/s**2)/lbf'.

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    str
        The unit string for this dimension.
    """
    if dimension == UNKNOWN:
        return ''
    if dimension in self.units:
        return self.units[dimension]
    numerator, denominator = [], []
    for name, power in parse_dimension(dimension):
        unit = self.units.get(name)
        if unit is None:
            raise UnitError(
                f"Unit system {self.name!r} has no unit for dimension {name!r}")
        label = f'({unit})' if ('/' in unit or '*' in unit) else unit
        if abs(power) != 1:
            label += f'**{abs(power)}'
        (numerator if power > 0 else denominator).append(label)
    out = '*'.join(numerator) if numerator else '1'
    for label in denominator:
        out += f'/{label}'
    return out
transform
transform(dimension: str) -> tuple[float, float]

(scale, offset) converting a display value to SI.

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
tuple of (float, float)

The scale and offset taking SI to display — an offset matters for temperature and nothing else.

Source code in src/visualdynamics/units.py
def transform(self, dimension: str) -> tuple[float, float]:
    """(scale, offset) converting a display value to SI.

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    tuple of (float, float)
        The scale and offset taking SI to display —
        an offset matters for temperature and nothing else.
    """
    if dimension == UNKNOWN:
        return 1.0, 0.0
    if dimension in self.units:
        return si_transform(self.units[dimension], dimension)
    scale = 1.0
    for name, power in parse_dimension(dimension):
        part_scale, part_offset = si_transform(self.units[name], name)
        if part_offset:
            raise UnitError(
                f"Dimension {dimension!r} combines the offset unit "
                f"{self.units[name]!r}; use an absolute unit instead")
        scale *= part_scale ** power
    return scale, 0.0
factor
factor(dimension: str) -> float

SI-per-display-unit scale (offset-free dimensions only).

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
float

What an SI value is multiplied by to display it.

Source code in src/visualdynamics/units.py
def factor(self, dimension: str) -> float:
    """SI-per-display-unit scale (offset-free dimensions only).

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    float
        What an SI value is multiplied by to display it.
    """
    scale, offset = self.transform(dimension)
    if offset:
        raise UnitError(
            f"Display unit for {dimension!r} has an offset; use "
            "from_si()/to_si()")
    return scale
from_si
from_si(values: ArrayLike, dimension: str) -> Any

Convert SI values to this system's display unit for dimension.

Parameters:

Name Type Description Default
values array_like

Values in SI.

required
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
array_like

The same values in this system's units.

Source code in src/visualdynamics/units.py
def from_si(self, values: ArrayLike, dimension: str) -> Any:
    """Convert SI values to this system's display unit for `dimension`.

    Parameters
    ----------
    values : array_like
        Values in SI.
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    array_like
        The same values in this system's units.
    """
    scale, offset = self.transform(dimension)
    return (values - offset) / scale
to_si
to_si(values: ArrayLike, dimension: str) -> Any

Convert values from this system's units into SI.

Parameters:

Name Type Description Default
values array_like

Values in this system's units.

required
dimension str

A dimension tag, such as 'acceleration'.

required

Returns:

Type Description
array_like

The same values in SI.

Source code in src/visualdynamics/units.py
def to_si(self, values: ArrayLike, dimension: str) -> Any:
    """Convert values from this system's units into SI.

    Parameters
    ----------
    values : array_like
        Values in this system's units.
    dimension : str
        A dimension tag, such as 'acceleration'.

    Returns
    -------
    array_like
        The same values in SI.
    """
    scale, offset = self.transform(dimension)
    return values * scale + offset
label
label(dimension: str) -> str

Display unit text; empty when the dimension is undefined.

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
str

The unit's name in this system.

Source code in src/visualdynamics/units.py
def label(self, dimension: str) -> str:
    """Display unit text; empty when the dimension is undefined.

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    str
        The unit's name in this system.
    """
    return self.unit(dimension)
label_text
label_text(dimension: str) -> str

Display unit for plain-text output, with real exponents: in/s².

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
str

The label as plain text.

Source code in src/visualdynamics/units.py
def label_text(self, dimension: str) -> str:
    """Display unit for plain-text output, with real exponents: in/s².

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    str
        The label as plain text.
    """
    return self._slash_label(
        dimension, _exponents_to_unicode,
        lambda text, power: text + _superscript(str(power)))
label_ascii
label_ascii(dimension: str) -> str

Display unit in plain ASCII, exponents as carets: in/s^2.

For renderers that quietly drop what they cannot draw: VTK's 3-D axis titles lose unicode superscripts in every text mode, so label_text's (in/s²)²/Hz read (in/s)/Hz off the waterfall — wrong by two squarings, with nothing saying so.

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
str

The label with no unicode, for renderers that drop it.

Source code in src/visualdynamics/units.py
def label_ascii(self, dimension: str) -> str:
    """Display unit in plain ASCII, exponents as carets: in/s^2.

    For renderers that quietly drop what they cannot draw: VTK's
    3-D axis titles lose unicode superscripts in every text mode,
    so `label_text`'s (in/s²)²/Hz read (in/s)/Hz off the waterfall
    — wrong by two squarings, with nothing saying so.

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    str
        The label with no unicode, for renderers that drop it.
    """
    return self._slash_label(dimension, _exponents_to_ascii,
                             lambda text, power: f'{text}^{power}')
label_html
label_html(dimension: str) -> str

Display unit as HTML — a stacked fraction when it has one.

Parameters:

Name Type Description Default
dimension str

A dimension tag, such as 'acceleration' or 'force'.

required

Returns:

Type Description
str

The label with HTML superscripts.

Source code in src/visualdynamics/units.py
def label_html(self, dimension: str) -> str:
    """Display unit as HTML — a stacked fraction when it has one.

    Parameters
    ----------
    dimension : str
        A dimension tag, such as 'acceleration' or 'force'.

    Returns
    -------
    str
        The label with HTML superscripts.
    """
    parts = self._unit_parts(dimension)
    if not parts:
        return ''
    numerator, denominator = _render_parts(
        parts, _exponents_to_html,
        lambda text, power: f'{text}<sup>{power}</sup>')
    numerator, denominator = numerator[0] or '1', denominator[0]
    if not denominator:
        return numerator
    return (
        '<table cellpadding="0" cellspacing="0" style="display:inline">'
        f'<tr><td align="center" style="border-bottom:1px solid">'
        f'{numerator}</td></tr>'
        f'<tr><td align="center">{denominator}</td></tr></table>')
with_units
with_units(
    name: str | None = None, **overrides
) -> UnitSystem

A copy of this system with per-dimension unit overrides.

Example: IN_LBF_S.with_units(acceleration='g') displays acceleration in g while everything else stays inch-pound-second.

Parameters:

Name Type Description Default
name str

What to call the derived system.

None
**overrides

Dimension/unit pairs to change.

{}

Returns:

Type Description
UnitSystem

A copy with those units replaced.

Source code in src/visualdynamics/units.py
def with_units(self, name: str | None = None, **overrides) -> UnitSystem:
    """A copy of this system with per-dimension unit overrides.

    Example: IN_LBF_S.with_units(acceleration='g') displays
    acceleration in g while everything else stays inch-pound-second.

    Parameters
    ----------
    name : str, optional
        What to call the derived system.
    **overrides
        Dimension/unit pairs to change.

    Returns
    -------
    UnitSystem
        A copy with those units replaced.
    """
    for dimension, unit in overrides.items():
        si_transform(unit, dimension)  # validates dimension and unit
    return UnitSystem(name or f'{self.name} (custom)',
                      {**self.units, **overrides}, base=self.coherent)

Functions:

unit_registry

unit_registry() -> Any

The one pint registry, built on first use.

One, because pint units from two registries do not compare — and lazily, because building it costs a tenth of a second that a script doing no unit conversion should not pay.

Source code in src/visualdynamics/units.py
def unit_registry() -> Any:
    """The one pint registry, built on first use.

    One, because pint units from two registries do not compare — and
    lazily, because building it costs a tenth of a second that a script
    doing no unit conversion should not pay.
    """
    global _ureg
    if _ureg is None:
        import pint

        _ureg = pint.UnitRegistry()  # pint ships slinch (lbf*s^2/in) already
        _ureg.define('@alias standard_gravity = gn')
    return _ureg

normalize_unit

normalize_unit(unit: str) -> str

A unit string as visualdynamics reads it, before pint sees it.

Source code in src/visualdynamics/units.py
def normalize_unit(unit: str) -> str:
    """A unit string as visualdynamics reads it, before pint sees it."""
    if not isinstance(unit, str):
        return unit
    unit = _BARE_LBM.sub('lb', unit)
    return _BARE_MIL.sub('thou', _BARE_G.sub('standard_gravity', unit))

pretty_unit

pretty_unit(unit: str) -> str

A unit string with real exponents, for showing: 'm/s**2' -> 'm/s²'.

Source code in src/visualdynamics/units.py
def pretty_unit(unit: str) -> str:
    """A unit string with real exponents, for showing: 'm/s**2' -> 'm/s²'."""
    return _exponents_to_unicode(unit)

plain_unit

plain_unit(text: str) -> str

The inverse of pretty_unit, so a shown unit can be read back.

Source code in src/visualdynamics/units.py
def plain_unit(text: str) -> str:
    """The inverse of `pretty_unit`, so a shown unit can be read back."""
    text = text.replace('·', '*')
    return _PRETTY_POWER.sub(
        lambda m: f'{m.group(1)}**'
        + ('0.5' if m.group(2) == '½'
           else m.group(2).translate(_FROM_SUPERSCRIPT)),
        text)

is_compound_unit

is_compound_unit(unit: str) -> bool

Does it need brackets before something is done to the whole of it?

Source code in src/visualdynamics/units.py
def is_compound_unit(unit: str) -> bool:
    """Does it need brackets before something is done to the whole of it?"""
    return _is_compound(unit)

parse_dimension

parse_dimension(expression: str) -> list[tuple[str, int]]

Parse a dimension expression into (base_dimension, power) parts.

Memoized: this is a pure function of a short string, and the plot asks for the same handful of expressions once per record. Drawing one curve out of a 1356-record FRF used to reparse and reconvert 1356 times.

Source code in src/visualdynamics/units.py
def parse_dimension(expression: str) -> list[tuple[str, int]]:
    """Parse a dimension expression into (base_dimension, power) parts.

    Memoized: this is a pure function of a short string, and the plot asks
    for the same handful of expressions once per record. Drawing one curve
    out of a 1356-record FRF used to reparse and reconvert 1356 times.
    """
    return list(_parse_dimension(expression))

si_transform cached

si_transform(
    unit: str, dimension: str | None = None
) -> tuple[float, float]

(scale, offset) such that si_value = value * scale + offset.

Offset is non-zero only for affine units such as degC and degF.

Source code in src/visualdynamics/units.py
@lru_cache(maxsize=4096)
def si_transform(unit: str, dimension: str | None = None) -> tuple[float, float]:
    """(scale, offset) such that `si_value = value * scale + offset`.

    Offset is non-zero only for affine units such as degC and degF.
    """
    ureg = unit_registry()
    unit = normalize_unit(unit)
    try:
        zero = ureg.Quantity(0.0, unit).to_base_units().magnitude
        one = ureg.Quantity(1.0, unit).to_base_units().magnitude
    except Exception as e:  # noqa: BLE001 - surface any pint failure uniformly
        raise UnitError(f"Cannot interpret unit {unit!r}: {e}")
    scale = one - zero
    if dimension is not None and dimension != UNKNOWN:
        expected = _DIMENSIONALITY.get(dimension)
        if expected is None:
            raise UnitError(f"Unknown dimension {dimension!r}")
        actual = ureg.Quantity(1.0, unit).dimensionality
        if actual != ureg.get_dimensionality(expected):
            raise UnitError(
                f"Unit {unit!r} is not a {dimension} unit (got {actual})")
    return scale, zero

si_factor

si_factor(unit: str, dimension: str | None = None) -> float

Multiplier converting values in unit to SI.

Raises for affine units (degC, degF), which need to_si/from_si.

Source code in src/visualdynamics/units.py
def si_factor(unit: str, dimension: str | None = None) -> float:
    """Multiplier converting values in `unit` to SI.

    Raises for affine units (degC, degF), which need `to_si`/`from_si`.
    """
    scale, offset = si_transform(unit, dimension)
    if offset:
        raise UnitError(
            f"Unit {unit!r} has an offset; use to_si()/from_si() instead of a "
            "bare scale factor")
    return scale

to_si

to_si(
    values: ArrayLike,
    unit: str,
    dimension: str | None = None,
) -> Any

values, read as unit, in SI. Handles offsets, so degrees Celsius arrive as kelvin rather than as a scaled nonsense.

Source code in src/visualdynamics/units.py
def to_si(values: ArrayLike, unit: str,
          dimension: str | None = None) -> Any:
    """`values`, read as `unit`, in SI. Handles offsets, so degrees
    Celsius arrive as kelvin rather than as a scaled nonsense."""
    scale, offset = si_transform(unit, dimension)
    return values * scale + offset

from_si

from_si(
    values: ArrayLike,
    unit: str,
    dimension: str | None = None,
) -> Any

SI values expressed in unit — the inverse of to_si.

Source code in src/visualdynamics/units.py
def from_si(values: ArrayLike, unit: str,
            dimension: str | None = None) -> Any:
    """SI `values` expressed in `unit` — the inverse of `to_si`."""
    scale, offset = si_transform(unit, dimension)
    return (values - offset) / scale

convert

convert(
    values: ArrayLike, from_unit: str, to_unit: str
) -> Any

values from one unit to another, through SI.

Source code in src/visualdynamics/units.py
def convert(values: ArrayLike, from_unit: str, to_unit: str) -> Any:
    """`values` from one unit to another, through SI."""
    return from_si(to_si(values, from_unit), to_unit)

dimension_of

dimension_of(unit: str) -> str | None

The visualdynamics base dimension matching a unit string, or None.

Only single base dimensions are recognized ('m/s**2' -> 'acceleration'); compound quantities like FRFs carry their dimension expression explicitly.

Source code in src/visualdynamics/units.py
def dimension_of(unit: str) -> str | None:
    """The visualdynamics base dimension matching a unit string, or None.

    Only single base dimensions are recognized ('m/s**2' -> 'acceleration');
    compound quantities like FRFs carry their dimension expression explicitly.
    """
    ureg = unit_registry()
    unit = normalize_unit(unit)
    try:
        dimensionality = ureg.Quantity(1.0, unit).dimensionality
    except Exception:  # noqa: BLE001 - any unparseable unit is simply unknown
        return None
    # pint holds the radian dimensionless, so rad/s and Hz share a
    # dimensionality; a unit spelled with an angle is the angular one
    angular = bool(_ANGLE_TOKEN.search(unit))
    for tag, expr in _DIMENSIONALITY.items():
        if tag in ('dimensionless', 'strain', 'angle'):
            continue
        if (tag in ANGULAR_UNITS) != angular:
            continue
        if dimensionality == ureg.get_dimensionality(expr):
            return tag
    if not dimensionality:
        return 'angle' if angular else 'dimensionless'
    return None