# Copyright 2026 BrainX Ecosystem Limited. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
from __future__ import annotations
import re
from copy import deepcopy
from typing import TYPE_CHECKING
from ._base_dimension import (
Dimension,
DIMENSIONLESS,
_is_tracer,
)
from ._typing import ArrayLike
if TYPE_CHECKING:
from ._base_quantity import Quantity
__all__ = [
'Unit',
'UNITLESS',
'add_standard_unit',
'parse_unit',
]
# SI unit _prefixes as integer exponents of 10, see table at end of file.
_siprefixes = {
"y": -24,
"z": -21,
"a": -18,
"f": -15,
"p": -12,
"n": -9,
"u": -6,
"m": -3,
"c": -2,
"d": -1,
"": 0,
"da": 1,
"h": 2,
"k": 3,
"M": 6,
"G": 9,
"T": 12,
"P": 15,
"E": 18,
"Z": 21,
"Y": 24,
}
# ---------------------------------------------------------------------------
# Display-parts helpers – canonical, sorted factored-unit representation
# ---------------------------------------------------------------------------
def _assert_same_base(u1, u2):
if not u1.has_same_base(u2):
raise TypeError(f"Cannot operate on units with different bases. Got {u1.base} != {u2.base}.")
def _find_standard_unit(
dim: Dimension,
base,
scale,
factor,
for_composition: bool = False,
) -> tuple[str | None, str | None, bool, bool]:
"""
Find a standard unit for the given dimension, base, scale, and factor.
Parameters
----------
for_composition : bool
When True, keys that are *ambiguous* (i.e. have >=2 registered
aliases with distinct display names, e.g. hertz vs becquerel)
are skipped so that they are never auto-substituted during
unit arithmetic. This is detected automatically at
registration time—no hardcoded list.
Returns
-------
(name, dispname, is_fullname, is_dimensionless)
"""
if dim == DIMENSIONLESS:
# Dimensionless aliases (radian, steradian, percent, ...) intentionally
# drop their display name in arithmetic results: ``radian / radian``
# is bare 1, not "rad". Callers needing to preserve a dimensionless
# alias (e.g. ``factorless()`` on radian) consult ``_standard_units``
# directly before invoking this helper.
return None, None, False, True
if isinstance(base, (int, float)):
if isinstance(scale, (int, float)):
if isinstance(factor, (int, float)):
key = (dim, scale, base, factor)
if key in _standard_units:
if for_composition and key in _ambiguous_keys:
pass # skip – ambiguous, fall through
else:
u = _standard_units[key]
return u.name, u.dispname, True, False
key = (dim, 0, base, 1.0)
if key in _standard_units:
if for_composition and key in _ambiguous_keys:
return None, None, False, False
u = _standard_units[key]
return u.name, u.dispname, False, False
return None, None, False, False
def _format_dim_parser_compatible(dim: Dimension, python_code: bool = False) -> str:
"""Render *dim* in parser-compatible canonical form.
``Dimension.__str__`` emits space-separated SI factors (``"m kg s^-2"``)
which cannot be round-tripped through :func:`parse_unit`. This helper
produces the same content with the canonical `` * `` / ``^`` / `` / ``
grammar used by :func:`_format_display_parts`, so anonymous Units have
a name/dispname that the parser can read back.
When ``python_code`` is True, the full-name SI labels (``metre``,
``kilogram``, ...) are used in place of the short symbols.
"""
from ._base_dimension import _ilabel, _iclass_label
labels = _iclass_label if python_code else _ilabel
dims = dim._dims
parts = []
for i in range(len(dims)):
if dims[i]:
parts.append((labels[i], labels[i], dims[i]))
if not parts:
return "1"
return _format_display_parts(parts)
def _find_a_name(dim: Dimension, base, scale, factor) -> tuple[str | None, bool]:
if dim == DIMENSIONLESS:
u_name = f"Unit({base}^{scale})"
return u_name, False
if isinstance(base, (int, float)):
if isinstance(scale, (int, float)):
if isinstance(factor, (int, float)):
key = (dim, scale, base, factor)
if key in _standard_units:
u_name = _standard_units[key].name
return u_name, True
if isinstance(factor, (int, float)):
key = (dim, 0, base, factor)
if key in _standard_units:
u_name = _standard_units[key].name
if factor == 1.:
return f"{base}^{scale} * {u_name}", False
else:
return f"{factor} * {base}^{scale} * {u_name}", False
key = (dim, 0, base, 1.)
if key in _standard_units:
u_name = _standard_units[key].name
if _is_tracer(scale):
return u_name, False
else:
return f"{base}^{scale} * {u_name}", False
return None, True
_standard_units: 'dict[tuple, Unit]' = {}
_standard_unit_aliases: 'dict[tuple, list[Unit]]' = {}
_unit_name_registry: 'dict[str, Unit]' = {}
# Monotonically-increasing registration index for each registered Unit
# identity. Used by :func:`_select_preferred_standard_unit` to prefer
# units that were registered earlier (i.e. library built-ins) over
# user-added aliases for the same physical key.
_unit_registration_index: 'dict[int, int]' = {}
_next_registration_index: 'list[int]' = [0]
# ---------------------------------------------------------------------------
# Ambiguous-key detection
#
# A dimension key is "ambiguous" when >=2 registered aliases have
# **different display names** (dispname). Spelling variants like
# meter/metre share the same dispname ("m") so they are NOT flagged.
# Genuine semantic collisions like hertz/becquerel ("Hz" vs "Bq") ARE
# flagged automatically—no hardcoded list required.
#
# Ambiguous keys are never auto-substituted during unit composition
# (mul / div / pow / reverse) so that e.g. joule/kg never silently
# becomes sievert.
# ---------------------------------------------------------------------------
_ambiguous_keys: set = set()
def _standard_unit_preference_score(unit: 'Unit') -> int:
"""
Return a preference score for choosing canonical display aliases.
Lower is better. Deterministic: on ties the name that sorts first
alphabetically wins (via ``_select_preferred_standard_unit``).
"""
name = unit.name.lower() if isinstance(unit.name, str) else ""
score = 0
# Prefer frequency over radioactivity for s^-1
if "hertz" in name:
score -= 10
return score
def _select_preferred_standard_unit(units: 'list[Unit]') -> 'Unit':
"""Pick the preferred alias.
Order of preference (lower is better):
1. ``_standard_unit_preference_score`` (e.g. prefer hertz over
becquerel for s^-1).
2. Registration index — library built-ins win over user
additions made via :func:`add_standard_unit` after import,
so user aliases cannot hijack canonical display.
3. Alphabetical, as a final deterministic tie-breaker for units
registered in the same call.
"""
def _key(u):
idx = _unit_registration_index.get(id(u), float('inf'))
return (
_standard_unit_preference_score(u),
idx,
u.name.lower() if isinstance(u.name, str) else "",
)
return min(units, key=_key)
[docs]
def add_standard_unit(u: 'Unit'):
"""
Register a unit as a standard unit for display purposes.
Once registered, this unit will be used when formatting quantities
whose dimensions, scale, base, and factor match. If multiple units
are registered for the same key, the preferred alias is selected
automatically. Keys with two or more distinct display names are
flagged as *ambiguous* and will not be auto-substituted during unit
composition.
Parameters
----------
u : Unit
The unit to register. Its ``base``, ``scale``, and ``factor``
must all be plain Python ``int`` or ``float`` values (not JAX
tracers) for registration to take effect.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> my_unit = u.Unit(
... dim=u.joule.dim,
... name='my_energy',
... dispname='myE',
... is_fullname=True,
... )
>>> u.add_standard_unit(my_unit)
"""
if (
isinstance(u.base, (int, float)) and
isinstance(u.scale, (int, float)) and
isinstance(u.factor, (int, float))
):
key = (u.dim, u.scale, u.base, u.factor)
aliases = _standard_unit_aliases.setdefault(key, [])
# Dedup by identity: a Unit instance is registered at most once.
# Without this, repeated calls grow the alias list unboundedly
# and poison the ambiguity heuristic below.
if not any(existing is u for existing in aliases):
aliases.append(u)
# Stamp a monotonic registration index so that later
# additions cannot hijack the canonical display.
_unit_registration_index[id(u)] = _next_registration_index[0]
_next_registration_index[0] += 1
_standard_units[key] = _select_preferred_standard_unit(aliases)
# Auto-detect ambiguity: >=2 distinct display names → ambiguous
dispnames = {a.dispname for a in aliases if isinstance(a.dispname, str)}
if len(dispnames) >= 2:
_ambiguous_keys.add(key)
# Register by dispname and name for string-based lookup
if isinstance(u.dispname, str) and u.dispname:
_unit_name_registry.setdefault(u.dispname, u)
if isinstance(u.name, str) and u.name:
_unit_name_registry.setdefault(u.name, u)
def _get_display_parts(unit: 'Unit'):
"""Return the display-parts list for *unit*.
Each element is ``(name, dispname, exponent)``.
"""
if unit._display_parts is not None:
return list(unit._display_parts)
return [(unit.name, unit.dispname, 1)]
def _merge_display_parts(parts_a, parts_b):
"""Merge two part-lists, combine same-name entries, drop zeros, sort."""
merged: dict[str, tuple] = {}
for name, disp, exp in list(parts_a) + list(parts_b):
if name in merged:
_, old_disp, old_exp = merged[name]
merged[name] = (name, disp, old_exp + exp)
else:
merged[name] = (name, disp, exp)
result = [(n, d, e) for n, d, e in merged.values() if e != 0]
# positive exponents first (alphabetical), then negative (alphabetical)
result.sort(key=lambda x: (0 if x[2] > 0 else 1, x[0].lower()))
return result
_RE_DISPNAME_EXP = re.compile(r'^(.+)\^(-?\d+(?:\.\d+)?)$')
def _normalise_display_parts(parts):
"""Normalise display parts: decompose stacked exponents, drop zeros, sort.
If a dispname already contains an exponent (e.g. ``'m^2'``), fold that
exponent into the part's own exponent so that ``('meter2', 'm^2', 3)``
becomes ``('meter2', 'm', 6)`` instead of rendering as ``m^2^3``.
"""
result = []
for name, disp, exp in parts:
if exp == 0:
continue
m = _RE_DISPNAME_EXP.match(disp)
if m:
base_disp = m.group(1)
inner_exp = float(m.group(2))
disp = base_disp
exp = inner_exp * exp
result.append((name, disp, exp))
# Merge entries that now share the same base dispname
merged: dict[str, tuple] = {}
for name, disp, exp in result:
if disp in merged:
_, old_disp, old_exp = merged[disp]
merged[disp] = (name, disp, old_exp + exp)
else:
merged[disp] = (name, disp, exp)
result = [(n, d, e) for n, d, e in merged.values() if e != 0]
result.sort(key=lambda x: (0 if x[2] > 0 else 1, x[0].lower()))
return result
def _fmt_exp(exp):
"""Format an exponent value, using int form when possible."""
return str(int(exp)) if exp == int(exp) else str(exp)
def _format_display_parts(parts) -> str:
"""Render a parts-list as a canonical unit string.
The canonical format uses dispname symbols (e.g. ``mV``, ``Hz``),
``^`` for exponentiation, `` * `` for multiplication, and `` / ``
for division. This single format is both human-readable and
machine-parseable:
mV
J / kg
nA / cm^2
mS * nA / cm^2
m / (kg * s^2)
"""
if not parts:
return "1"
numerator = [(n, d, e) for n, d, e in parts if e > 0]
denominator = [(n, d, -e) for n, d, e in parts if e < 0]
def _fmt_term(name, dispname, exp):
if exp == 1:
return dispname
return f"{dispname}^{_fmt_exp(exp)}"
num_str = " * ".join(_fmt_term(n, d, e) for n, d, e in numerator) if numerator else "1"
if not denominator:
return num_str
if len(denominator) == 1:
den_str = _fmt_term(*denominator[0])
else:
inner = " * ".join(_fmt_term(n, d, e) for n, d, e in denominator)
den_str = f"({inner})"
return f"{num_str} / {den_str}"
# ---------------------------------------------------------------------------
# String → Unit parser
# ---------------------------------------------------------------------------
def _split_fraction(s: str):
"""Split ``'A / B'`` into ``('A', 'B')``, respecting parentheses.
Returns ``(s, None)`` when there is no top-level ``/``.
"""
depth = 0
i = 0
while i < len(s):
ch = s[i]
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
elif depth == 0 and s[i:i + 3] == ' / ':
num = s[:i].strip()
den = s[i + 3:].strip()
if den.startswith('(') and den.endswith(')'):
den = den[1:-1].strip()
return num, den
i += 1
return s, None
def _split_product(s: str):
"""Split on ``' * '`` respecting parentheses."""
parts = []
depth = 0
start = 0
i = 0
while i < len(s):
ch = s[i]
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
elif depth == 0 and s[i:i + 3] == ' * ':
parts.append(s[start:i])
start = i + 3
i = start
continue
i += 1
parts.append(s[start:])
return parts
def _parse_product(s: str):
"""Parse ``'A * B * C'`` into a product of :class:`Unit` objects."""
terms = _split_product(s)
result = None
for term in terms:
u = _parse_term(term.strip())
result = u if result is None else result * u
return result
def _parse_term(s: str):
"""Parse a single term like ``'cm^2'``, ``'mV'``, or ``'10^3'``.
Parenthesised sub-expressions are recursively parsed as full
fraction/product expressions; this lets ``parse_unit("(m * s) / A")``
succeed.
"""
s = s.strip()
# Strip a single outer paren wrapping the whole term and recurse.
if s.startswith('(') and s.endswith(')'):
depth = 0
balanced_at_zero_only_at_end = True
for i, ch in enumerate(s):
if ch == '(':
depth += 1
elif ch == ')':
depth -= 1
if depth == 0 and i != len(s) - 1:
balanced_at_zero_only_at_end = False
break
if balanced_at_zero_only_at_end:
inner = s[1:-1].strip()
if not inner:
raise ValueError(f"Empty parenthesised group in {s!r}")
num_str, den_str = _split_fraction(inner)
numerator = _parse_product(num_str)
if den_str is not None:
return numerator / _parse_product(den_str)
return numerator
caret_idx = s.rfind('^')
if caret_idx > 0:
atom = s[:caret_idx].strip()
exp_str = s[caret_idx + 1:].strip()
try:
exp = float(exp_str)
if exp == int(exp):
exp = int(exp)
except ValueError:
raise ValueError(f"Invalid exponent in unit string: {s!r}")
# Numeric base → dimensionless scaled unit (e.g. "10^3").
# ``Unit`` is base=10-only, so encode ``base_num ** exp`` either
# in ``scale`` (when base_num==10) or in ``factor``.
try:
base_num = float(atom)
if base_num == 10.0:
return Unit(DIMENSIONLESS, scale=exp)
return Unit(DIMENSIONLESS, factor=float(base_num) ** exp)
except ValueError:
pass
if atom in _unit_name_registry:
return _unit_name_registry[atom] ** exp
raise ValueError(f"Unknown unit token: {atom!r} in {s!r}")
# No exponent — direct lookup
if s in _unit_name_registry:
return _unit_name_registry[s]
# Numeric literal (rare: anonymous factor)
try:
num = float(s)
return Unit(DIMENSIONLESS, scale=0, base=10., factor=num)
except ValueError:
pass
raise ValueError(
f"Unknown unit: {s!r}. Use a registered unit name or display name."
)
def parse_unit(s: str) -> 'Unit':
"""Parse a canonical unit string into a :class:`Unit`.
Accepts strings in the format produced by ``str(unit)`` or
``repr(unit)``, e.g. ``"mV"``, ``"J / kg"``, ``"nA / cm^2"``.
Both display names (``"mV"``) and full names (``"mvolt"``) are
recognised.
Parameters
----------
s : str
The unit string to parse.
Returns
-------
Unit
Raises
------
ValueError
If the string cannot be parsed into a known unit.
Examples
--------
>>> parse_unit("mV")
Unit("mV")
>>> parse_unit("J / kg")
Unit("J / kg")
"""
s = s.strip()
# Strip the Unit("...") repr wrapper if present
if s.startswith('Unit("') and s.endswith('")'):
s = s[6:-2]
elif s.startswith("Unit('") and s.endswith("')"):
s = s[6:-2]
if not s:
raise ValueError("Cannot parse an empty unit string.")
# Dimensionless
if s == '1':
return UNITLESS
# Fast path: direct registry lookup
if s in _unit_name_registry:
return _unit_name_registry[s]
# Compound expression
num_str, den_str = _split_fraction(s)
numerator = _parse_product(num_str)
if den_str is not None:
denominator = _parse_product(den_str)
return numerator / denominator
return numerator
class Unit:
r"""
A physical unit.
Basically, a unit is just a number with given dimensions, e.g.
mvolt = 0.001 with the dimensions of voltage. The units module
defines a large number of standard units, and you can also define
your own (see below).
Mathematically, a unit represents:
.. math::
\text{{factor}} \times \text{{base}}^{\text{{scale}}} \times \text{{dimension}}
where the ``factor`` is the conversion factor of the unit (e.g.
``1 calorie = 4.18400 Joule``, so the factor is 4.18400), the ``base``
is the base of the exponent (e.g. 10 for the kilo prefix), the ``scale``
is the exponent of the base (e.g. 3 for the kilo prefix), and the
``dimension`` is the physical dimensions of the unit (e.g. ``joule`` for
energy).
The unit class also keeps track of various things that were used
to define it so as to generate a nice string representation of it.
See below.
Parameters
----------
dim : Dimension, optional
The physical dimensions of the unit. Defaults to ``DIMENSIONLESS``.
scale : array_like, optional
The scale exponent, e.g. 3 for a "k" (kilo) prefix. Defaults to 0.
base : array_like, optional
The base of the exponent, e.g. 10 for SI prefixes. Defaults to 10.
factor : array_like, optional
The conversion factor of the unit. Defaults to 1.
name : str, optional
The full name of the unit, e.g. ``'volt'``.
dispname : str, optional
The display name, e.g. ``'V'``.
is_fullname : bool, optional
Whether ``name`` is the canonical full name. Defaults to ``True``.
display_parts : list of tuple, optional
Canonical display components for compound units.
Notes
-----
When creating scaled units, you can use the following prefixes:
====== ====== ==============
Factor Name Prefix
====== ====== ==============
10^24 yotta Y
10^21 zetta Z
10^18 exa E
10^15 peta P
10^12 tera T
10^9 giga G
10^6 mega M
10^3 kilo k
10^2 hecto h
10^1 deka da
1
10^-1 deci d
10^-2 centi c
10^-3 milli m
10^-6 micro u (\mu in SI)
10^-9 nano n
10^-12 pico p
10^-15 femto f
10^-18 atto a
10^-21 zepto z
10^-24 yocto y
====== ====== ==============
**Defining your own**
It can be useful to define your own units for printing
purposes. So for example, to define the newton metre, you
write:
.. code-block:: python
>>> import saiunit as u
>>> Nm = u.newton * u.metre
You can then do:
.. code-block:: python
>>> (1 * Nm).in_unit(Nm)
'1. N m'
New "compound units", i.e. units that are composed of other units will be
automatically registered and from then on used for display. For example,
imagine you define total conductance for a membrane, and the total area of
that membrane:
.. code-block:: python
>>> import saiunit as u
>>> conductance = 10. * u.nS
>>> area = 20000 * u.um ** 2
If you now ask for the conductance density, you will get an "ugly" display
in basic SI dimensions, as saiunit does not know of a corresponding unit:
.. code-block:: python
>>> conductance / area
0.5 * metre ** -4 * kilogram ** -1 * second ** 3 * amp ** 2
By using an appropriate unit once, it will be registered and from then on
used for display when appropriate:
.. code-block:: python
>>> u.usiemens / u.cm ** 2
usiemens / (cmetre ** 2)
>>> conductance / area # same as before, but now knows about uS/cm^2
50. * usiemens / (cmetre ** 2)
Note that user-defined units cannot override the standard units (``volt``,
``second``, etc.) that are predefined. For example, the unit ``Nm`` has the
dimensions "length^2 * mass / time^2", and therefore the same dimensions as
the standard unit ``joule``. The latter will be used for display purposes:
.. code-block:: python
>>> 3 * u.joule
3. * joule
>>> 3 * Nm
3. * joule
Examples
--------
Create a simple unit:
.. code-block:: python
>>> import saiunit as u
>>> u.volt
Unit("V")
>>> u.mvolt
Unit("mV")
Combine units:
.. code-block:: python
>>> import saiunit as u
>>> u.volt / u.amp
Unit("V / A")
"""
__module__ = "saiunit"
__slots__ = ["_dim", "_base", "_scale", "_factor", "_dispname", "_name", "is_fullname", "_hash", "_display_parts"]
__array_priority__ = 1000
_dim: 'Dimension'
_base: int | float
_scale: int | float
_factor: int | float
_dispname: str
_name: str
is_fullname: bool
_hash: int | None
_display_parts: 'list[tuple[str, str, int]] | None'
def __init__(
self,
dim: 'Dimension | str | None' = None,
scale: int | float = 0,
base: int | float = 10.,
factor: int | float = 1.,
name: str | None = None,
dispname: str | None = None,
is_fullname: bool = True,
display_parts=None,
):
# String-based construction: Unit("mV"), Unit("J / kg"), etc.
if isinstance(dim, str):
# The string form ignores every other constructor argument —
# silently dropping ``Unit("mV", scale=99, factor=99)`` was a
# source of confusing bugs. Reject any non-default secondary
# argument explicitly so the caller knows.
extras = []
if scale != 0:
extras.append("scale")
if base != 10.:
extras.append("base")
if factor != 1.:
extras.append("factor")
if name is not None:
extras.append("name")
if dispname is not None:
extras.append("dispname")
if display_parts is not None:
extras.append("display_parts")
if extras:
raise TypeError(
"Unit(str, ...) does not accept additional arguments: "
+ ", ".join(extras)
+ ". Use parse_unit() and modify the result, or construct "
"the Unit from a Dimension instead."
)
parsed = parse_unit(dim)
self._base = parsed._base
self._scale = parsed._scale
self._factor = parsed._factor
self._dim = parsed._dim
self._name = parsed._name
self._dispname = parsed._dispname
self.is_fullname = parsed.is_fullname
self._hash = None
self._display_parts = parsed._display_parts
return
# ``base`` is fixed at 10 for now — Units canonicalize to base=10
# internally, and accepting other bases silently rewrote them,
# losing information. Raise so callers can not be surprised.
if base != 10.:
raise ValueError(
f"Unit currently only supports base=10; got base={base!r}. "
"Encode non-decimal scales in ``factor`` instead."
)
# Reject NaN/inf factors — these poison arithmetic downstream and
# cannot represent a valid physical conversion.
if isinstance(factor, (int, float)):
import math
if math.isnan(factor) or math.isinf(factor):
raise ValueError(
f"Unit factor must be a finite real number; got factor={factor!r}."
)
self._base = base
self._scale = scale
self._factor = factor
# The physical unit dimensions of this unit
if dim is None:
dim = DIMENSIONLESS
if not isinstance(dim, Dimension):
raise TypeError(f'Expected instance of Dimension, but got {dim}')
self._dim = dim
# The name of this unit
if name is None:
is_fullname = False
if dim == DIMENSIONLESS:
name = f"Unit({base}^{scale})"
else:
# Anonymous Units must produce parser-compatible
# name/dispname so that ``parse_unit(repr(u))`` round-trips.
# ``Dimension.__str__`` uses space separation which the
# parser cannot read. ``_canonical_str`` (called by
# ``__repr__``/``__str__``) prefixes the factor/scale on
# its own for anonymous units, so we only encode the
# dimensional part here.
name = _format_dim_parser_compatible(dim, python_code=True)
if dispname is None:
dispname = _format_dim_parser_compatible(dim, python_code=False)
self._name = name
# The display name of this unit
self._dispname = (name if dispname is None else dispname)
# whether the name is the full name
self.is_fullname = is_fullname
# cached hash (computed lazily)
self._hash = None
# Canonical display components: list of (name, dispname, exponent).
# None for simple (non-compound) units.
self._display_parts = display_parts
@property
def factor(self) -> float:
"""
Return the conversion factor of the unit.
The factor represents a multiplicative constant that converts a
quantity expressed in this unit to its base-unit equivalent. For
example, 1 calorie = 4.184 joule, so ``calorie.factor == 4.184``.
Returns
-------
float
The conversion factor.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.factor
1.0
"""
return self._factor
@factor.setter
def factor(self, factor):
raise NotImplementedError(
"Cannot set the factor of a Unit object directly,"
"Please create a new Unit object with the factor you want."
)
@property
def base(self) -> float:
"""
Return the base of the unit's scale exponent.
The base is the number that is raised to the ``scale`` power to
produce the unit's magnitude. For SI-prefixed units this is 10
(e.g. ``kilo`` means ``10 ** 3``).
Returns
-------
float
The base of the exponent.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.kvolt.base
10.0
"""
return self._base
@base.setter
def base(self, base):
raise NotImplementedError(
"Cannot set the base of a Unit object directly,"
"Please create a new Unit object with the base you want."
)
@property
def scale(self) -> float | int:
"""
Return the scale exponent of the unit.
The scale is the integer exponent applied to :attr:`base` to
produce the unit's magnitude relative to the base unit. For
example, ``mvolt`` has ``scale == -3`` (i.e. ``10 ** -3``).
Returns
-------
float or int
The scale exponent.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.mvolt.scale
-3
"""
return self._scale
@scale.setter
def scale(self, scale):
raise NotImplementedError(
"Cannot set the scale of a Unit object directly,"
"Please create a new Unit object with the scale you want."
)
@property
def magnitude(self) -> float:
"""
Return the absolute magnitude of the unit.
The magnitude is computed as ``factor * base ** scale`` and
represents the overall multiplicative factor that converts a
value in this unit to the corresponding base-unit value.
Returns
-------
float
The absolute magnitude of the unit.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.mvolt.magnitude
0.001
>>> u.kvolt.magnitude
1000.0
"""
# magnitude = factor * base ** scale
return self.factor * self.base ** self.scale
@magnitude.setter
def magnitude(self, scale):
raise NotImplementedError(
"Cannot set the magnitude of a Unit object."
)
@property
def dim(self) -> Dimension:
"""
Return the physical unit dimensions of this unit.
Returns
-------
Dimension
The :class:`~saiunit.Dimension` instance describing the
physical dimensions (e.g. length, mass, time, ...).
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.dim
metre ** 2 * kilogram * second ** -3 * amp ** -1
"""
return self._dim
@dim.setter
def dim(self, value):
# Do not support setting the unit directly
raise NotImplementedError(
"Cannot set the dimension of a Quantity object directly,"
"Please create a new Quantity object with the dimension you want."
)
@property
def is_unitless(self) -> bool:
"""
Whether the unit is dimensionless with no scaling.
A unit is considered unitless when its dimension is dimensionless,
its scale exponent is 0, and its factor is 1.0.
Returns
-------
bool
``True`` if the unit is unitless, ``False`` otherwise.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.UNITLESS.is_unitless
True
>>> u.volt.is_unitless
False
"""
return self.dim.is_dimensionless and self.scale == 0 and self.factor == 1.0
@property
def should_display_unit(self) -> bool:
"""
Whether the unit should be shown in formatted output.
Returns ``True`` for all non-unitless units, and also for
dimensionless units that carry a meaningful registered name
(e.g. radian, steradian).
Returns
-------
bool
``True`` if the unit should be displayed, ``False`` otherwise.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.should_display_unit
True
>>> u.UNITLESS.should_display_unit
False
"""
if not self.is_unitless:
return True
# Dimensionless but with a registered display name (e.g. rad, sr)
return self.is_fullname and self._canonical_str() != '1'
@property
def name(self):
"""
Return the full name of the unit.
Returns
-------
str or None
The full name of the unit (e.g. ``'volt'``, ``'mvolt'``),
or ``None`` if no name was assigned.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.name
'volt'
>>> u.mvolt.name
'mvolt'
"""
return self._name
@name.setter
def name(self, name):
raise NotImplementedError(
"Cannot set the name of a Unit object directly,"
"Please create a new Unit object with the name you want."
)
@property
def dispname(self):
"""
Return the display name of the unit.
The display name is the short symbol used when rendering the
unit in string output (e.g. ``'V'`` for volt, ``'mV'`` for
millivolt).
Returns
-------
str or None
The display name of the unit.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.dispname
'V'
>>> u.mvolt.dispname
'mV'
"""
return self._dispname
@dispname.setter
def dispname(self, dispname):
raise NotImplementedError(
"Cannot set the dispname of a Unit object directly,"
"Please create a new Unit object with the dispname you want."
)
[docs]
def factorless(self) -> 'Unit':
"""
Return a copy of this Unit with the factor set to 1.
Returns
-------
Unit
A new Unit object with the factor set to 1.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u = u.Unit.create(u.Dimension(kg=1), 'pound', 'lb', factor=0.453592)
>>> u.factor
0.453592
>>> u.factorless().factor
1.0
"""
# using standard units
key = (self.dim, self.scale, self.base, 1.)
if key in _standard_units:
return _standard_units[key]
# using temporary units
name, dispname, is_fullname, dimless = _find_standard_unit(self.dim, self.base, self.scale, 1.0)
return Unit(
dim=self.dim,
scale=self.scale,
base=self.base,
factor=1.,
name=name,
dispname=dispname,
is_fullname=is_fullname,
)
[docs]
def copy(self):
"""
Return a copy of this Unit.
Returns
-------
Unit
A new Unit object with the same attributes.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u = u.volt.copy()
>>> u == u.volt
True
>>> u is u.volt
False
"""
return Unit(
dim=self.dim,
scale=self.scale,
base=self.base,
factor=self.factor,
name=self.name,
dispname=self.dispname,
is_fullname=self.is_fullname,
display_parts=(
list(self._display_parts)
if self._display_parts is not None else None
),
)
def __deepcopy__(self, memodict):
return Unit(
dim=self.dim.__deepcopy__(memodict),
scale=deepcopy(self.scale),
base=deepcopy(self.base),
factor=deepcopy(self.factor),
name=deepcopy(self.name),
dispname=deepcopy(self.dispname),
is_fullname=deepcopy(self.is_fullname),
display_parts=deepcopy(self._display_parts, memodict),
)
def __hash__(self):
if self._hash is None:
# Equality is *physical*: two units that resolve to the same
# ``(dim, factor, base, scale)`` must hash equal regardless
# of name spelling (e.g. ``metre`` vs ``meter``).
self._hash = hash(
(
self.dim,
self.factor,
self.base,
self.scale,
)
)
return self._hash
[docs]
def has_same_magnitude(self, other: 'Unit') -> bool:
"""
Whether this Unit has the same magnitude as another Unit.
Two units have the same magnitude when they share the same
``scale``, ``base``, and ``factor``.
Parameters
----------
other : Unit
The other Unit to compare with.
Returns
-------
bool
Whether the two Units have the same magnitude.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.mvolt.has_same_magnitude(u.mamp)
True
>>> u.mvolt.has_same_magnitude(u.volt)
False
"""
return self.scale == other.scale and self.base == other.base and self.factor == other.factor
[docs]
def has_same_base(self, other: 'Unit') -> bool:
"""
Whether this Unit has the same ``base`` as another Unit.
Parameters
----------
other : Unit
The other Unit to compare with.
Returns
-------
bool
Whether the two Units have the same base.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.has_same_base(u.amp)
True
>>> u.volt.has_same_base(u.mvolt)
True
"""
return self.base == other.base
[docs]
def has_same_dim(self, other: 'Unit') -> bool:
"""
Whether this Unit has the same unit dimensions as another Unit.
Parameters
----------
other : Unit
The other Unit to compare with.
Returns
-------
bool
Whether the two Units have the same unit dimensions.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.volt.has_same_dim(u.mvolt)
True
>>> u.volt.has_same_dim(u.amp)
False
"""
from ._base_getters import get_dim
other_dim = get_dim(other)
return get_dim(self) == other_dim
[docs]
@staticmethod
def create(
dim: Dimension,
name: str,
dispname: str,
scale: int | float = 0,
base: float = 10.,
factor: float = 1.,
) -> 'Unit':
"""
Create a new named unit.
Parameters
----------
dim : Dimension
The dimensions of the unit.
name : `str`
The full name of the unit, e.g. ``'volt'``
dispname : `str`
The display name, e.g. ``'V'``
scale : int, optional
The scale of this unit as an exponent of 10, e.g. -3 for a unit that
is 1/1000 of the base scale. Defaults to 0 (i.e. a base unit).
base: float, optional
The base for this unit (as the base of the exponent), i.e.
a base of 10 means 10^3, for a "k" prefix. Defaults to 10.
factor: float, optional
The factor for this unit (as the conversion factor), e.g.
a factor of 1 cal = 4.18400 J, where 4.18400 is the factor.
Defaults to 1.
Returns
-------
u : Unit
The new unit.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> from saiunit import Dimension
>>> energy_dim = u.joule.dim
>>> cal = u.Unit.create(energy_dim, 'calorie', 'cal', factor=4.184)
>>> cal
Unit("cal")
"""
u = Unit(
dim=dim,
scale=scale,
base=base,
factor=factor,
name=name,
dispname=dispname,
is_fullname=True,
)
add_standard_unit(u)
return u
[docs]
@staticmethod
def create_scaled_unit(baseunit: 'Unit', scalefactor: str) -> 'Unit':
"""
Create a scaled unit from a base unit.
Parameters
----------
baseunit : `Unit`
The unit of which to create a scaled version, e.g. ``volt``,
``amp``.
scalefactor : `str`
The scaling factor, e.g. ``"m"`` for mvolt, mamp
Returns
-------
u : Unit
The new unit.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> uvolt = u.Unit.create_scaled_unit(u.volt, 'u')
>>> uvolt.name
'uvolt'
>>> uvolt.scale
-6
"""
if scalefactor not in _siprefixes:
raise ValueError(
f"Unknown SI prefix {scalefactor!r}. "
f"Valid prefixes are: {list(_siprefixes.keys())}"
)
name = scalefactor + baseunit.name
dispname = scalefactor + baseunit.dispname
scale = _siprefixes[scalefactor] + baseunit.scale
u = Unit(
dim=baseunit.dim,
name=name,
dispname=dispname,
scale=scale,
base=baseunit.base,
is_fullname=True,
)
add_standard_unit(u)
return u
def _canonical_str(self) -> str:
"""Return the canonical display string for this unit.
Uses dispname symbols (``mV``, ``Hz``, ``kg``), ``^`` for
exponentiation, `` * `` for multiplication, and `` / `` for
division. The result is both human-readable and
machine-parseable.
The standard-unit substitution is resolved eagerly at
construction time (in ``__mul__``/``__div__``/``__pow__``/
``reverse``) and stored on ``_name``/``_dispname``, so this
method simply returns the stored canonical name when
``is_fullname`` is set. This keeps ``unit.name`` consistent
with ``str(unit)`` and survives pickle/copy.
"""
if self.is_fullname:
return self.dispname
if self._display_parts is not None:
return _format_display_parts(self._display_parts)
if self.dim.is_dimensionless:
if self.scale == 0 and self.factor == 1.:
return '1'
elif self.factor == 1.:
return f'{self.base}^{_fmt_exp(self.scale)}'
elif self.scale == 0:
return str(self.factor)
else:
return f'{self.factor} * {self.base}^{_fmt_exp(self.scale)}'
# Anonymous unit — build a descriptive string from components
if self.factor == 1.:
if self.scale == 0:
return f'{self.dispname}'
else:
return f'{self.base}^{self.scale} * {self.dispname}'
else:
if self.scale == 0:
return f'{self.factor} * {self.dispname}'
else:
return f'{self.factor} * {self.base}^{self.scale} * {self.dispname}'
def __repr__(self) -> str:
s = self._canonical_str()
return f"Unit(\"{s}\")"
def __str__(self) -> str:
return self._canonical_str()
def __mul__(self, other) -> 'Unit | Quantity':
# self * other
if isinstance(other, Unit):
_assert_same_base(self, other)
scale = self.scale + other.scale
dim = self.dim * other.dim
factor = self.factor * other.factor
# Dimensionless result. When neither operand carries a named
# dimensionless display (radian, steradian, ...), the result is
# bare ``Unit("1")`` as before. When at least one operand is a
# named dimensionless unit, merge its display parts so the
# name survives ``radian * UNITLESS`` and compounds such as
# ``radian * radian`` render as ``rad^2`` rather than ``1``.
if dim == DIMENSIONLESS:
self_named_dimless = self.is_fullname and self.dim.is_dimensionless
other_named_dimless = other.is_fullname and other.dim.is_dimensionless
if self_named_dimless or other_named_dimless:
parts_a = _get_display_parts(self) if self_named_dimless else []
parts_b = _get_display_parts(other) if other_named_dimless else []
parts = _normalise_display_parts(_merge_display_parts(parts_a, parts_b))
if parts:
canonical = _format_display_parts(parts)
return Unit(
dim, scale=scale, base=self.base, factor=factor,
name=canonical, dispname=canonical,
is_fullname=True, display_parts=parts,
)
return Unit(dim, scale=scale, base=self.base, factor=factor)
# Both named → deterministic compound via display_parts
if self.is_fullname and other.is_fullname:
parts = _merge_display_parts(
_get_display_parts(self),
_get_display_parts(other),
)
parts = _normalise_display_parts(parts)
# Eagerly resolve a registered standard name for the
# composed quantity so that ``name``/``dispname`` stay in
# sync with ``str(self)``. Falls back to the parts-based
# canonical string for ambiguous keys or anonymous results.
std_name, std_disp, std_is_full, _ = _find_standard_unit(
dim, self.base, scale, factor, for_composition=True,
)
if std_is_full:
name, dispname, is_fullname = std_name, std_disp, True
else:
canonical = _format_display_parts(parts)
name, dispname, is_fullname = canonical, canonical, True
return Unit(
dim, scale=scale, base=self.base, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
display_parts=parts,
)
# Fallback: standard-unit lookup
name, dispname, is_fullname, _ = _find_standard_unit(
dim, self.base, scale, factor
)
return Unit(
dim, scale=scale, base=self.base, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
)
elif isinstance(other, Dimension):
raise TypeError(f"unit {self} cannot multiply by a Dimension {other}.")
else:
from ._base_quantity import Quantity
if isinstance(other, Quantity):
return Quantity(other.mantissa, unit=(self * other.unit)) # type: ignore[arg-type]
return Quantity(other, unit=self)
def __rmul__(self, other) -> 'Unit | Quantity':
# other * self
if isinstance(other, Unit):
return other.__mul__(self)
from ._base_quantity import Quantity
if isinstance(other, Quantity):
return Quantity(other.mantissa, unit=(other.unit * self)) # type: ignore[arg-type]
return Quantity(other, unit=self)
def __imul__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __div__(self, other) -> 'Unit':
# self / other
if isinstance(other, Unit):
_assert_same_base(self, other)
scale = self.scale - other.scale
dim = self.dim / other.dim
factor = self.factor / other.factor
# Dimensionless result — preserve named-dimensionless display
# (radian, steradian, ...) so ``rad / UNITLESS`` stays ``rad``.
if dim == DIMENSIONLESS:
self_named_dimless = self.is_fullname and self.dim.is_dimensionless
other_named_dimless = other.is_fullname and other.dim.is_dimensionless
if self_named_dimless or other_named_dimless:
parts_a = _get_display_parts(self) if self_named_dimless else []
parts_b = (
[(n, d, -e) for n, d, e in _get_display_parts(other)]
if other_named_dimless else []
)
parts = _normalise_display_parts(_merge_display_parts(parts_a, parts_b))
if parts:
canonical = _format_display_parts(parts)
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=canonical, dispname=canonical,
is_fullname=True, display_parts=parts,
)
return Unit(dim, scale=scale, base=self.base, factor=factor)
# Both named → deterministic compound via display_parts
if self.is_fullname and other.is_fullname:
other_parts = [(n, d, -e) for n, d, e in _get_display_parts(other)]
parts = _merge_display_parts(
_get_display_parts(self), other_parts,
)
parts = _normalise_display_parts(parts)
std_name, std_disp, std_is_full, _ = _find_standard_unit(
dim, self.base, scale, factor, for_composition=True,
)
if std_is_full:
name, dispname, is_fullname = std_name, std_disp, True
else:
canonical = _format_display_parts(parts)
name, dispname, is_fullname = canonical, canonical, True
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
display_parts=parts,
)
# Fallback: standard-unit lookup
name, dispname, is_fullname, _ = _find_standard_unit(
dim, self.base, scale, factor
)
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
)
else:
raise TypeError(f"unit {self} cannot divide by a non-unit {other}")
def __rdiv__(self, other) -> 'Unit | Quantity':
# other / self
if isinstance(other, Unit):
return other.__div__(self)
from ._base_quantity import Quantity
if isinstance(other, Quantity):
return Quantity(other.mantissa, unit=(other.unit / self))
return Quantity(other, unit=self.reverse())
[docs]
def reverse(self):
"""
Return the multiplicative inverse of this unit.
Computes ``1 / self``, producing a new unit with negated scale,
inverted factor, and reciprocal dimensions.
Returns
-------
Unit
A new Unit representing the reciprocal of this unit.
Examples
--------
.. code-block:: python
>>> import saiunit as u
>>> u.second.reverse()
Unit("Hz")
>>> u.metre.reverse()
Unit("1 / m")
"""
dim = self.dim ** -1
scale = -self.scale
factor = 1. / self.factor
# Standard-unit lookup — allowed for reverse() because it is a
# single-operand transform where the preference system correctly
# picks hertz over becquerel, etc.
name, dispname, is_fullname, dimless = _find_standard_unit(
dim, self.base, scale, factor
)
if is_fullname:
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=name, dispname=dispname,
is_fullname=True,
)
# Build from display_parts (negate exponents)
if self.is_fullname:
parts = [(n, d, -e) for n, d, e in _get_display_parts(self)]
parts = _normalise_display_parts(parts)
# reverse() already handles the unambiguous standard-unit
# case at the top; here we just render the parts.
canonical = _format_display_parts(parts)
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=canonical, dispname=canonical,
is_fullname=True,
display_parts=parts,
)
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
)
def __idiv__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __truediv__(self, oc):
# self / oc
return self.__div__(oc)
def __rtruediv__(self, oc):
# oc / self
return self.__rdiv__(oc)
def __itruediv__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __floordiv__(self, oc):
raise NotImplementedError("Units cannot be performed floor division")
def __rfloordiv__(self, oc):
raise NotImplementedError("Units cannot be performed floor division")
def __ifloordiv__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __pow__(self, other):
# self ** other
from ._base_getters import is_scalar_type
if is_scalar_type(other):
dim = self.dim ** other
scale = self.scale * other
factor = self.factor ** other
if dim == DIMENSIONLESS:
# Preserve a named-dimensionless display (radian, steradian)
# through powers: ``radian ** 1`` is ``rad``, ``rad ** 2``
# is ``rad^2``; ``rad ** 0`` collapses to ``1`` naturally.
if self.is_fullname and self.dim.is_dimensionless:
src_parts = _get_display_parts(self)
parts = _normalise_display_parts(
[(n, d, e * other) for n, d, e in src_parts]
)
if parts:
canonical = _format_display_parts(parts)
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=canonical, dispname=canonical,
is_fullname=True, display_parts=parts,
)
return Unit(dim, scale=scale, base=self.base, factor=factor)
# Named source → build from display_parts (multiply exponents).
# This avoids ambiguous standard-unit aliases (e.g. m^3→kl,
# (m/s)^2→Gy) and keeps display consistent with __mul__/__div__.
if self.is_fullname:
src_parts = _get_display_parts(self)
parts = [(n, d, e * other) for n, d, e in src_parts]
parts = _normalise_display_parts(parts)
std_name, std_disp, std_is_full, _ = _find_standard_unit(
dim, self.base, scale, factor, for_composition=True,
)
if std_is_full:
name, dispname, is_fullname = std_name, std_disp, True
else:
canonical = _format_display_parts(parts)
name, dispname, is_fullname = canonical, canonical, True
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
display_parts=parts,
)
# Fallback: standard-unit lookup (for anonymous units)
name, dispname, is_fullname, dimless = _find_standard_unit(
dim, self.base, scale, factor
)
return Unit(
dim, base=self.base, scale=scale, factor=factor,
name=name, dispname=dispname,
is_fullname=is_fullname,
)
else:
raise TypeError(
f"unit cannot perform an exponentiation (unit ** other) with a non-scalar, "
f"since one unit cannot contain multiple units. \n"
f"But we got unit={self}, other={other}"
)
def __ipow__(self, other, modulo=None):
raise NotImplementedError("Units cannot be modified in-place")
def __add__(self, other):
raise TypeError(
"Units cannot be added: addition is defined on quantities, not units. "
"To add quantities, attach mantissas first (e.g. 1*ms + 2*ms)."
)
def __radd__(self, other):
raise TypeError(
"Units cannot be added: addition is defined on quantities, not units. "
"To add quantities, attach mantissas first (e.g. 1*ms + 2*ms)."
)
def __iadd__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __sub__(self, other):
raise TypeError(
"Units cannot be subtracted: subtraction is defined on quantities, not "
"units. To subtract quantities, attach mantissas first (e.g. 2*ms - 1*ms)."
)
def __rsub__(self, other):
raise TypeError(
"Units cannot be subtracted: subtraction is defined on quantities, not "
"units. To subtract quantities, attach mantissas first (e.g. 2*ms - 1*ms)."
)
def __isub__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __mod__(self, oc):
raise NotImplementedError("Units cannot be performed modulo")
def __rmod__(self, oc):
raise NotImplementedError("Units cannot be performed modulo")
def __imod__(self, other):
raise NotImplementedError("Units cannot be modified in-place")
def __eq__(self, other) -> bool:
# Two Units are equal when they represent the same physical
# quantity (matching dim/scale/base/factor). Names and display
# strings are *not* part of equality: spelling aliases such as
# ``metre`` vs ``meter`` and registry-canonical compounds such
# as ``A * ohm`` vs ``V`` compare equal, and the corresponding
# hashes collide as required by the ``__hash__`` contract.
if not isinstance(other, Unit):
return False
return (
(other.dim == self.dim)
and (other.scale == self.scale)
and (other.base == self.base)
and (other.factor == self.factor)
)
def __ne__(self, other) -> bool:
return not self.__eq__(other)
def __abs__(self) -> 'Unit':
"""Return the unit itself — units are always non-negative."""
return self
def __reduce__(self):
# For pickling. ``display_parts`` is forwarded so that compound
# units (e.g. ``mS * nA / cm^2``) preserve their canonical
# rendering across pickle round-trips.
return (
_to_unit,
(
self.dim,
self.scale,
self.base,
self.factor,
self.name,
self.dispname,
self.is_fullname,
(list(self._display_parts)
if self._display_parts is not None else None),
)
)
def _to_unit(dim, scale, base, factor, name, dispname, is_fullname,
display_parts=None):
"""Private pickle reconstruction shim for Unit."""
return Unit(
dim=dim, scale=scale, base=base, factor=factor,
name=name, dispname=dispname, is_fullname=is_fullname,
display_parts=display_parts,
)
_to_unit.__module__ = 'saiunit._base_unit'
UNITLESS = Unit()
"""
The canonical unitless (dimensionless) unit.
``UNITLESS`` is a singleton-like :class:`Unit` with no physical dimensions,
a scale of 0, a base of 10, and a factor of 1. It is returned by default
when a :class:`Unit` is constructed with no arguments, and is used
internally as the neutral element of unit arithmetic.
.. code-block:: python
>>> import saiunit as u
>>> u.UNITLESS.is_unitless
True
>>> u.UNITLESS.dim.is_dimensionless
True
"""