Skip to content

Data units

A tensor's values can carry a physical unit — the .unit property (a constructor unit= kwarg or a settable attribute) — that rides through operations the way names and coordinates do. By default a unit is an opaque string; with a units backend it also gains validation, conversion, and dimensional algebra:

v = xtensor(data, names=("b", "t"), unit="V")
v.unit                 # "V"
v.T.unit               # "V"  — carried through reshaping/reduction/indexing
v.unit = "mV"          # annotate: never changes the data

By default (unit_backend=None) a unit is an opaque string — stored and carried, never inspected. Selecting a backend turns on validation, conversion, and dimensional algebra:

from fiery.xtensor import set_options
with set_options(unit_backend="pint"):     # needs fiery-xtensor[units]
    volts = xtensor(v, unit="V")
    amps  = xtensor(a, unit="A")
    secs  = xtensor(t, unit="s")
    volts.to_unit("mV")           # converts: rescales the data ×1000
    (volts * amps).unit           # "ampere * volt"  — units multiply
    (volts / secs).unit           # "volt / second"
    (volts ** 2).unit             # "volt ** 2"
    (volts @ amps).unit           # "ampere * volt"  — matmul multiplies too

You can also attach a unit by multiplying with the backend's own unit objects, the way pint builds a Quantity from 5 * ureg.metre:

import pint
u = pint.UnitRegistry()
with set_options(unit_backend="pint"):
    (x * u.mm).unit              # "millimeter"          — bare unit; data unchanged
    (x * (3 * u.mm)).unit        # "millimeter", data ×3 — a quantity scales too
    (v / u.s).unit               # "volt / second"

(Write the unit on the rightx * u.mm, not u.mm * x — so the XTensor handles it before pint's own reflected operator does.)

Whenever a step is dimensionally invalid or ambiguous — adding incompatible units, or a transcendental like exp/log of a united value — the result silently drops the unit; set_options(unit_policy="strict") makes those same steps raise instead:

(volts + amps).unit               # None    — incompatible, dropped
torch.exp(volts).unit             # None    — exp needs a dimensionless argument
with set_options(unit_policy="strict"):
    volts + amps                  # ValueError: incompatible units 'volt' and 'ampere'

Compatible units are reconciled automatically: adding or comparing V and mV converts the right operand to the left's unit first (only incompatible dimensions drop/raise). And .magnitude drops the unit when you want the bare values:

with set_options(unit_backend="pint"):
    (xtensor(v, unit="V") + xtensor(mv, unit="mV")).unit   # "volt" — mV converted
    xtensor(v, unit="V").magnitude.unit                    # None   — unit dropped, still an XTensor

Heterogeneous (per-axis) units

Units may also vary along an axis: give a structured coordinate (Proposal 0002) a unit field per position, and each position stacks a different quantity (a voltage/current/power channel stack). The effective element unit is base · Π(coord units):

with set_options(unit_backend="pint"):
    x = xtensor(data, names=("q", "t"), coords={"q": [
        {"name": "voltage", "unit": "V"},
        {"name": "current", "unit": "A"},
        {"name": "power",   "unit": "W"},
    ]})
    x.unit                     # None    — no single base unit (heterogeneous)
    x.sel(q="voltage").unit    # "V"     — selecting one position folds its unit in
    x.sum(dim="q").unit        # None    — V/A/W incompatible -> dropped

    # a *uniform* axis (every position in the same unit) folds cleanly instead:
    uniform = xtensor(data, names=("q", "t"), coords={"q": [
        {"name": "vx", "unit": "V"},
        {"name": "vy", "unit": "V"},
        {"name": "vz", "unit": "V"},
    ]})
    uniform.sum(dim="q").unit  # "V"     — the shared unit folds into the base

Reducing over such an axis folds a uniform unit into the base or, on incompatible per-position units, drops it (or raises under unit_policy="strict").

The data itself is never wrapped in a pint.Quantity, so autograd, GPU, and __torch_function__ keep working throughout.