Skip to content

isqx (core, fmt)

The top-level isqx public API contains the re-exports of the following modules:

Restrictions for composing expressions with examples:

Inner
Outer
isqx.BaseDimension isqx.BaseUnit isqx.Dimensionless isqx.Exp isqx.Mul isqx.Scaled isqx.Aliased isqx.Tagged isqx.Translated isqx.Log
isqx.Exp
__pow__

\(\text{L}^2\)

\(\text{m}^2\)

\(\text{Re}^{1/5}\)

\((\text{m}^2)^2\)

\((\text{m}\cdot\text{s}^{-1})^2\)

\(\text{ft}^2\)
❌¹
\(\degree\text{C}^2\)
❌¹³
\(\text{dBV}^2\)
isqx.Mul
__mul__

\(\text{L}\cdot\text{T}^{-2}\)

\(\text{N}\cdot\text{m}\)

\(\text{rad}\cdot\text{s}^{-1}\)

\(\text{m}\cdot\text{s}^{-2}\)

\((\text{kg}\cdot\text{m}\cdot\text{s}^{-1})\cdot\text{s}^{-1}\)

\(\text{lbf}\cdot\text{ft}\)

\(\text{N}\cdot\text{m}\)

\(\text{m}_\text{geometric}\cdot\text{s}^{-2}\)
❌¹
\(\text{J}\cdot\degree\text{C}^{-1}\)
⚠️¹⁴
\(\text{dBV}\cdot\text{s}^{-1}\)
isqx.Scaled
__rmul__

\(1000\cdot\text{m}\)

\(100\cdot\text{m}^2\)

\(3.6\cdot(\text{m}\cdot\text{s}^{-1})\)

\(\frac{1}{12}\text{ft}\)

\(10^3\text{J}\)

\(\text{ft}_\text{geometric} = 0.3048\cdot\text{m}_\text{geometric}\)
❌¹
\(1.8\cdot\degree\text{C}\)

\(10\cdot \text{B}\)
isqx.Prefix^
__mul__
❌² ✅³
\(\text{km}\)
❌²
\(\text{kilo}\cdot\text{Re}\)
❌⁴
\(\text{kilo}\cdot\text{m}^2\)
❌⁴
\(\text{kilo}\cdot(\text{m}\cdot\text{s}^{-1})\)
❌⁴
\(\text{kilo}\cdot\text{ft}\)
✅⁶
\(\text{kJ}\)
⚠️⁷
\(\text{km}_\text{geometric}\)
❌¹
\(\text{kilo}\cdot\degree\text{C}\)
✅⁵
\(\text{mNp}\)
isqx.Aliased
.alias
❌⁸ ❌⁸ ❌⁸
\(\text{sqft} = \text{ft}^2\)

\(\text{J}=\text{N}\cdot\text{m}\)

\(\text{ft} = 0.3048\cdot\text{m}\)
❌⁹
\(\text{N}_\text{thrust}\)
❌¹
\(\text{B} = \log...\)
isqx.Tagged
__getitem__

\(\text{L}_\text{geometric}\)

\(\text{m}_\text{geometric}\)

\(\text{Re}_\text{chord}\)

\(({\text{m}^2})_\text{surface}\)

\((\text{N}\cdot\text{m})_\text{torque}\)

\(\text{ft}_\text{geometric}\)

\(\text{N}_\text{thrust}\)
⚠️¹⁰
\((\degree\text{C})_\text{surface}\)

\(\text{dB(A)}\)
isqx.Translated ❌¹¹
\(\text{L} + 13\)

\(\text{K} - 273.15\)
❌¹¹
\(\text{Re} - 13\)
❌¹¹
\(\text{m}^2 + 13\)
❌¹¹
\((\text{N}\cdot\text{m}) + 13\)

\(\degree\text{R} - 459.67\)
⚠️
\(\text{K}_\text{ambient} + 13\)
❌¹
\(\degree\text{C} + 13\)
❌¹
\(\text{dBV} + 10\)
isqx.Log ❌¹² ❌¹²
\(\text{B}\)
❌¹²
\(\log(\text{m}^2)\)
❌¹²
\(\log(\text{W}\cdot\text{m}^{-2}\cdot\text{Hz}^{-1})\)
❌¹²
\(\log(\text{mW})\)
❌¹²
\(\log(\text{W})\)

\(\log(\text{ratio}_\text{V/V})\)
❌¹
\(\log(\degree\text{C})\)
❌¹²
\(\log(\text{dBV})\)

^ a prefix is not an expression, but a factory that produces an aliased scaled unit.

  1. isqx.Translated is considered terminal and cannot be further composed with other expressions, except for being tagged. for example, ℃² is physically meaningless, as are kilo(). operations on intervals must use the absolute reference unit (e.g., J K⁻¹ instead of J ℃⁻¹).
  2. prefixes like kilo- or milli- can only be applied to units, not dimensionless numbers or dimensions.
  3. prefixes can be applied to any isqx.BaseUnit except for the kilogram.
  4. a prefix must be applied to a named unit, which is either a isqx.BaseUnit or an isqx.Aliased. it cannot be applied to structural expressions like Exp, Mul, or Scaled directly.
  5. a logarithmic can only be prefixed if its allow_prefix flag is True.
  6. a prefix can be applied to an isqx.Aliased if its allow_prefix flag is True (e.g., kilo * N).
  7. the legality of wrapping a isqx.Tagged type depends on the rules for its inner reference. for example, Prefix * N["thrust"] is legal because Prefix * N is legal.
  8. an isqx.Aliased must wrap a structural expression (Exp, Mul, Scaled). it cannot wrap fundamental, terminal or aliased expressions.
  9. nesting isqx.Aliased types is forbidden.
  10. nesting isqx.Tagged types is forbidden. repeated __getitem__ calls will "extend" the tag tuple.
  11. a translated unit (like Celsius) must have a isqx.BaseUnit or a isqx.Scaled unit (like Rankine) as its reference.
  12. a logarithmic expression must wrap a dimensionless expression. it cannot wrap another logarithmic expression or an expression with a physical dimension.
  13. a logarithmic expression is considered terminal and cannot be exponentiated.
  14. a product can contain at most one logarithmic expression. this is to allow common units like dB/m or dB/Hz while forbidding physically meaningless compositions like dB·m or dB².

_core ¤

slots ¤

slots: dict[str, bool] = {}

SupportsDecimal ¤

Bases: SupportsFloat, SupportsAbs[object], Protocol

to_decimal ¤

to_decimal(ctx: decimal.Context) -> Decimal
Source code in src/isqx/_core.py
61
def to_decimal(self, ctx: decimal.Context) -> Decimal: ...

ExprKind ¤

ExprKind: TypeAlias = Literal[
    "dimensionless", "unit", "dimension"
]

Name ¤

Name: TypeAlias = str

A unique slug to identify the expression. This is used by the default basic formatter to display the expression and hence should not contain any spaces to avoid ambiguity. For example, meter, newton, reynolds

Named ¤

Bases: Protocol

name ¤

name: Name

AliasMixin ¤

alias ¤

alias(name: Name, *, allow_prefix: bool = False) -> Aliased

Wrap this expression with a name.

Parameters:

Name Type Description Default
name Name

Name of this alias, e.g. newton

required
allow_prefix bool

Whether to allow prefixes to be attached. This should only be true for some units like liter

False
Source code in src/isqx/_core.py
85
86
87
88
89
90
91
92
def alias(self, name: Name, *, allow_prefix: bool = False) -> Aliased:
    """Wrap this expression with a name.

    :param name: Name of this alias, e.g. `newton`
    :param allow_prefix: Whether to allow [prefixes][isqx.Prefix] to be
    attached. This should only be true for some units like `liter`
    """
    return Aliased(self, name=name, allow_prefix=allow_prefix)  # type: ignore

FormatMixin ¤

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

ExprBase ¤

Bases: FormatMixin

A base class for a "unit-like" expression.

It may be of the following forms:

Type Example
dimensionless number¹ Reynolds, Prandtl
base dimension¹ length
base unit¹ meter
expression raised to a power² Exp(M, 2)
product of expressions² Mul(A, S)
scaled expression²⁴ Scaled(M, 0.3048)
aliased¹ newton = kg m s⁻²
translated expression³ Translated(K, -273.15)
logarithmic expression¹³ dB
tagged true vs ground speed

¹ these expressions are associated with a name. ² these expressions can be aliased with a name. ³ these expressions are terminal, meaning it cannot be further exponentiated, multiplied, scaled, translated or aliased to form a more complex expression. However, it can be further tagged (e.g. surface temperature vs ISA temperature). ⁴ can be created by multiplying a prefix (e.g. milli) ⁵ can be created by calling a quantity kind with a unit

Operator overloading is provided for ergonomic expression construction.

Note

While dividing expressions is supported, it is strongly discouraged as it can lead to operator precedence ambiguity. For example, while Python interprets J / KG / K as (J / KG) / K, it is often clearer to represent it as J * KG**-1 * K**-1.

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

Dimensionless ¤

Dimensionless(name: Name)

Bases: Named, ExprBase

name ¤

name: Name

Name for the dimensionless number, e.g. reynolds, prandtl

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

BaseDimension ¤

BaseDimension(name: Name)

Bases: Named, ExprBase

name ¤

name: Name

Name for the base dimension, e.g. L, M, T

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

BaseUnit ¤

BaseUnit(_dimension: BaseDimension, name: Name)

Bases: Named, ExprBase

name ¤

name: Name

Name for the unit, e.g. m, kg, s

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

Exponent ¤

Exponent: TypeAlias = Union[int, Fraction]

An exponent, generally small integers, which can be positive, negative, or a fraction, but not zero

Exp ¤

Exp(base: _ComposableExpr, exponent: Exponent)

Bases: AliasMixin, ExprBase

An expression raised to an exponent. For example, BaseUnit("meter", Dimension("L")), 2) is m². Can be recursively nested, e.g. Exp(Exp(METER, 2), Fraction(1, 2))

base ¤

base: _ComposableExpr

exponent ¤

exponent: Exponent

Exponent. Avoid using zero to represent dimensionless numbers: use isqx.Dimensionless with a name instead.

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def __post_init__(self) -> None:
    if not isinstance(self.exponent, (int, Fraction)):
        raise CompositionError(
            outer=Exp,
            inner=self.exponent,
            msg=(
                "exponent must be an integer or a fraction, "
                f"not {type(self.exponent).__name__}."
            ),
        )
    if self.exponent == 0:
        raise CompositionError(
            outer=Exp,
            inner=self.exponent,
            msg="exponent must not be zero.",
            help="use `Dimensionless` to represent a dimensionless quantity.",
        )
    ref = _unwrap_tagged_or_aliased(self.base)
    if isinstance(ref, Translated):
        raise CompositionError(
            outer=Exp,
            inner=ref,
            msg="translated units (like ℃) are terminal and cannot be exponentiated.",
            help=(
                "did you mean to exponentiate its "
                f"absolute reference `{ref.reference}` instead?"
            ),
        )  # prevent ℃². J ℃⁻¹ should be written as J K⁻¹

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

alias ¤

alias(name: Name, *, allow_prefix: bool = False) -> Aliased

Wrap this expression with a name.

Parameters:

Name Type Description Default
name Name

Name of this alias, e.g. newton

required
allow_prefix bool

Whether to allow prefixes to be attached. This should only be true for some units like liter

False
Source code in src/isqx/_core.py
85
86
87
88
89
90
91
92
def alias(self, name: Name, *, allow_prefix: bool = False) -> Aliased:
    """Wrap this expression with a name.

    :param name: Name of this alias, e.g. `newton`
    :param allow_prefix: Whether to allow [prefixes][isqx.Prefix] to be
    attached. This should only be true for some units like `liter`
    """
    return Aliased(self, name=name, allow_prefix=allow_prefix)  # type: ignore

Mul ¤

Mul(terms: tuple[_ComposableExpr, ...])

Bases: AliasMixin, ExprBase

Products of powers of an expression.

terms ¤

terms: tuple[_ComposableExpr, ...]

A tuple of expressions to be multiplied, preserving the order.

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def __post_init__(self) -> None:
    if not self.terms:
        raise CompositionError(
            outer=Mul,
            inner=self.terms,
            msg="`Mul` terms must not be empty.",
            help="use `Dimensionless` to represent a dimensionless quantity.",
        )
    current_kind: ExprKind | None = None
    for term in self.terms:
        ref = _unwrap_tagged_or_aliased(term)
        if isinstance(ref, Translated):
            raise CompositionError(
                outer=Mul,
                inner=ref,
                msg="`Translated` units (like ℃) are terminal and cannot be part of a product.",
                help=f"use its absolute reference `{ref.reference}` instead.",
            )  # prevent ℃ * ℃
        k = kind(term)
        if current_kind is None and k != "dimensionless":
            current_kind = k
        elif current_kind != k and k != "dimensionless":
            raise MixedKindError(terms=self.terms)  # prevent time * seconds

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

alias ¤

alias(name: Name, *, allow_prefix: bool = False) -> Aliased

Wrap this expression with a name.

Parameters:

Name Type Description Default
name Name

Name of this alias, e.g. newton

required
allow_prefix bool

Whether to allow prefixes to be attached. This should only be true for some units like liter

False
Source code in src/isqx/_core.py
85
86
87
88
89
90
91
92
def alias(self, name: Name, *, allow_prefix: bool = False) -> Aliased:
    """Wrap this expression with a name.

    :param name: Name of this alias, e.g. `newton`
    :param allow_prefix: Whether to allow [prefixes][isqx.Prefix] to be
    attached. This should only be true for some units like `liter`
    """
    return Aliased(self, name=name, allow_prefix=allow_prefix)  # type: ignore

Scaled ¤

Scaled(
    reference: BaseUnit
    | Exp
    | Mul
    | Scaled
    | Aliased
    | Tagged
    | Log,
    factor: Number | LazyProduct | Prefix,
)

Bases: AliasMixin, ExprBase

reference ¤

reference: (
    BaseUnit | Exp | Mul | Scaled | Aliased | Tagged | Log
)

The unit or dimension that this unit or dimension is based on.

factor ¤

The exact factor to multiply to this unit to convert it to the reference. For example, 1 ft = 0.3048 m, so the factor is 0.3048.

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
def __post_init__(self) -> None:
    if not isinstance(
        self.factor,
        (
            LazyProduct,
            SupportsDecimal,
            Decimal,
            Fraction,
            float,
            int,
            Prefix,
        ),
    ):
        raise CompositionError(
            outer=Scaled,
            inner=self.factor,
            msg=f"factor must be a number, not {type(self.factor).__name__}.",
        )
    ref = _unwrap_tagged_or_aliased(self.reference)
    if isinstance(ref, Translated):
        raise CompositionError(
            outer=Scaled,
            inner=self.reference,
            msg=f"`{type(self.factor).__name__} cannot be scaled.",
        )  # prevent 13 * ℃

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

alias ¤

alias(name: Name, *, allow_prefix: bool = False) -> Aliased

Wrap this expression with a name.

Parameters:

Name Type Description Default
name Name

Name of this alias, e.g. newton

required
allow_prefix bool

Whether to allow prefixes to be attached. This should only be true for some units like liter

False
Source code in src/isqx/_core.py
85
86
87
88
89
90
91
92
def alias(self, name: Name, *, allow_prefix: bool = False) -> Aliased:
    """Wrap this expression with a name.

    :param name: Name of this alias, e.g. `newton`
    :param allow_prefix: Whether to allow [prefixes][isqx.Prefix] to be
    attached. This should only be true for some units like `liter`
    """
    return Aliased(self, name=name, allow_prefix=allow_prefix)  # type: ignore

Log ¤

Log(reference: Dimensionless | Tagged, base: Number)

Bases: AliasMixin, ExprBase

The logarithm of a dimensionless expression [ISO 80000-2:2019 2-13.4].

reference ¤

reference: Dimensionless | Tagged

A dimensionless expression

base ¤

base: Number

The base of the logarithm, e.g. 10 for bel, e for neper.

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
341
342
343
344
345
346
347
348
349
350
351
352
def __post_init__(self) -> None:
    ref = self.reference
    is_valid_ref = isinstance(ref, Dimensionless) or (
        isinstance(ref, Tagged) and isinstance(ref.reference, Dimensionless)
    )
    if not is_valid_ref:
        raise CompositionError(
            outer=Log,
            inner=ref,
            msg="`Log` can only wrap a `Dimensionless` expression",
            help="use the `isqx.ratio()` helper for logarithmic units like dB.",
        )

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

alias ¤

alias(name: Name, *, allow_prefix: bool = False) -> Aliased

Wrap this expression with a name.

Parameters:

Name Type Description Default
name Name

Name of this alias, e.g. newton

required
allow_prefix bool

Whether to allow prefixes to be attached. This should only be true for some units like liter

False
Source code in src/isqx/_core.py
85
86
87
88
89
90
91
92
def alias(self, name: Name, *, allow_prefix: bool = False) -> Aliased:
    """Wrap this expression with a name.

    :param name: Name of this alias, e.g. `newton`
    :param allow_prefix: Whether to allow [prefixes][isqx.Prefix] to be
    attached. This should only be true for some units like `liter`
    """
    return Aliased(self, name=name, allow_prefix=allow_prefix)  # type: ignore

Aliased ¤

Aliased(
    reference: Exp | Mul | Scaled | Tagged | Log,
    name: Name,
    allow_prefix: bool = False,
)

Bases: Named, ExprBase

An alias for an expression, used to give a more readable name.

Note that unlike a tagged expression, simplification will effectively elide this class.

reference ¤

reference: Exp | Mul | Scaled | Tagged | Log

Expression to be aliased, e.g. Mul((KG, M, Exp(S, -2)))

name ¤

name: Name

Name of this alias, e.g. newton

allow_prefix ¤

allow_prefix: bool = False

Whether to allow prefixes to be attached. This should only be true for some units like liter.

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
371
372
373
374
375
376
377
378
379
def __post_init__(self) -> None:
    if not isinstance(
        ref := self.reference, (Exp, Mul, Scaled, Tagged, Log)
    ):
        raise CompositionError(
            outer=Aliased,
            inner=ref,
            msg="`Aliased` can only wrap an `Exp`, `Mul`, `Scaled`, `Tagged`, or `Log` expression.",
        )

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

Translated ¤

Translated(
    reference: BaseUnit | Scaled | Aliased | Tagged,
    offset: Number,
    name: Name,
)

Bases: Named, ExprBase

An expression offsetted from some reference unit.

reference ¤

reference: BaseUnit | Scaled | Aliased | Tagged

The expression that this expression is based on (e.g., K for DEGC)

offset ¤

offset: Number

The exact offset to add to the reference to get this unit. For example, = K - 273.15, so the offset is -273.15.

name ¤

name: Name

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def __post_init__(self) -> None:
    ref = _unwrap_tagged_or_aliased(self.reference)
    if isinstance(ref, Translated):
        raise CompositionError(
            outer=Translated,
            inner=ref,
            msg="nesting `Translated` expressions is not allowed.",
        )
    if not isinstance(ref, (BaseUnit, Scaled)):
        raise CompositionError(
            outer=Translated,
            inner=ref,
            msg="`Translated` must have a `BaseUnit` or `Scaled` expression as its reference.",
        )

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

Tagged ¤

Tagged(
    reference: _TaggedAllowedExpr, tags: tuple[Tag, ...]
)

Bases: AliasMixin, ExprBase

An expression decorated with one of more semantic context tag.

Similar to how:

  • Annotated[T, M1, M2, ...] attaches metadata to some type T,
  • expr[C1, C2, ...] attaches context to an expression.

This allows one to "disambiguate" between quantities that share the same physical dimension, but have different meanings, e.g. geopotential altitude vs. geometric altitude.

reference ¤

reference: _TaggedAllowedExpr

tags ¤

tags: tuple[Tag, ...]

__post_init__ ¤

__post_init__() -> None
Source code in src/isqx/_core.py
426
427
428
429
430
431
432
433
434
435
def __post_init__(self) -> None:
    if isinstance(self.reference, Tagged):
        raise CompositionError(
            outer=Tagged,
            inner=self.reference,
            msg="nesting `Tagged` expressions is not allowed. use the `unit[tags]` syntax.",
        )
    for tag in self.tags:
        if isinstance(tag, HasTagValidation):
            tag.__validate_tag__(self.reference, self.tags)

__format__ ¤

__format__(fmt: _FormatSpec | str | Formatter) -> str
Source code in src/isqx/_core.py
96
97
98
99
def __format__(self, fmt: _FormatSpec | str | Formatter) -> str:
    from ._fmt import fmt as format_

    return format_(self, formatter=fmt)  # type: ignore

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
101
102
def __str__(self) -> str:
    return self.__format__("basic")

__pow__ ¤

__pow__(exponent: Exponent) -> Exp
Source code in src/isqx/_core.py
142
143
def __pow__(self, exponent: Exponent) -> Exp:
    return Exp(self, exponent)  # type: ignore

__mul__ ¤

__mul__(rhs: Expr) -> Mul
__mul__(rhs: LazyProduct | Number) -> Scaled
__mul__(rhs: Expr | LazyProduct | Number) -> Mul | Scaled
Source code in src/isqx/_core.py
152
153
154
155
156
157
158
159
160
def __mul__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return Scaled(self, rhs)  # type: ignore
    # make sure KG * M * S becomes flat, not Mul((Mul((KG, M)), S))
    terms_self = self.terms if isinstance(self, Mul) else (self,)
    terms_other = rhs.terms if isinstance(rhs, Mul) else (rhs,)
    return Mul(tuple([*terms_self, *terms_other]))

__rmul__ ¤

__rmul__(lhs: LazyProduct | Number | Prefix) -> Scaled
Source code in src/isqx/_core.py
162
163
164
165
def __rmul__(self, lhs: LazyProduct | Number | Prefix) -> Scaled:
    if isinstance(lhs, Prefix):
        return lhs.mul(self)  # type: ignore
    return Scaled(self, lhs)  # type: ignore

__truediv__ ¤

__truediv__(rhs: Expr) -> Mul
__truediv__(rhs: LazyProduct | Number) -> Scaled
__truediv__(
    rhs: Expr | LazyProduct | Number,
) -> Mul | Scaled
Source code in src/isqx/_core.py
173
174
175
176
177
178
179
180
181
182
def __truediv__(self, rhs: Expr | LazyProduct | Number) -> Mul | Scaled:
    if not isinstance(
        rhs, (LazyProduct, SupportsDecimal, Decimal, Fraction, float, int)
    ):
        return self * rhs**-1

    return Scaled(
        self,  # type: ignore
        LazyProduct(tuple(f for f in _products_inverse(rhs))),
    )  # M / 2 => Scaled(M, Fraction(1, 2))

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> Tagged

Attach tags to this expresson.

Source code in src/isqx/_core.py
184
185
186
187
188
189
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> Tagged:
    """Attach tags to this expresson."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if isinstance(self, Tagged):
        return Tagged(self.reference, self.tags + t)
    return Tagged(self, t)  # type: ignore

alias ¤

alias(name: Name, *, allow_prefix: bool = False) -> Aliased

Wrap this expression with a name.

Parameters:

Name Type Description Default
name Name

Name of this alias, e.g. newton

required
allow_prefix bool

Whether to allow prefixes to be attached. This should only be true for some units like liter

False
Source code in src/isqx/_core.py
85
86
87
88
89
90
91
92
def alias(self, name: Name, *, allow_prefix: bool = False) -> Aliased:
    """Wrap this expression with a name.

    :param name: Name of this alias, e.g. `newton`
    :param allow_prefix: Whether to allow [prefixes][isqx.Prefix] to be
    attached. This should only be true for some units like `liter`
    """
    return Aliased(self, name=name, allow_prefix=allow_prefix)  # type: ignore

HasTagValidation ¤

Bases: Hashable, Protocol

__validate_tag__ ¤

__validate_tag__(
    reference: Expr, tags: tuple[Tag, ...]
) -> None

Check that this tag can be applied to the given expression.

For example, this can be used to ensure:

  • decibel (log level) with log reference unit voltage but not
  • reynolds number with log reference unit voltage.

Parameters:

Name Type Description Default
reference Expr

The expression to apply the tags to.

required
tags tuple[Tag, ...]

The tags being applied to the expression. This can be used to enforce complex rules (e.g. no duplicates)

required

Raises:

Type Description
CompositionError

if the tag cannot be applied to the expression

Source code in src/isqx/_core.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def __validate_tag__(
    self,
    reference: Expr,
    tags: tuple[Tag, ...],
) -> None:
    """Check that this tag can be applied to the given expression.

    For example, this can be used to ensure:

    - `decibel` (log level) with log reference unit `voltage` but not
    - `reynolds number` with log reference unit `voltage`.

    :param reference: The expression to apply the tags to.
    :param tags: The tags being applied to the expression.
        This can be used to enforce complex rules (e.g. no duplicates)
    :raises CompositionError: if the tag cannot be applied to the expression
    """
    ...

PhysicalUnit ¤

PhysicalUnit: TypeAlias = Union[
    BaseUnit, Exp, Mul, Scaled, Aliased, Tagged
]

Prefix ¤

Prefix(value: Number, name: str)

A prefix, which when multiplied by a base unit or aliased unit, returns a scaled unit.

Note that this is not an isqx.Expr.

value ¤

value: Number

name ¤

name: str

Name of this prefix, e.g. milli, kibi

mul ¤

mul(rhs: BaseUnit | Aliased | Tagged) -> Scaled
Source code in src/isqx/_core.py
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
def mul(self, rhs: BaseUnit | Aliased | Tagged) -> Scaled:
    if not isinstance(rhs, (BaseUnit, Aliased, Tagged)):
        raise CompositionError(
            outer=Scaled,
            inner=rhs,
            msg=f"prefixes cannot be applied to `{type(rhs).__name__}`.",
        )  # this will prevent double prefixing

    if isinstance(rhs, BaseUnit):
        if rhs.name == "kilogram":
            raise CompositionError(
                outer=Scaled,
                inner=rhs,
                msg="cannot prefix `kilogram`.",
                help="apply the prefix to `gram` instead.",
            )
    elif isinstance(rhs, Aliased):
        if not rhs.allow_prefix:
            raise CompositionError(
                outer=Scaled,
                inner=rhs,
                msg=f"The aliased unit `{rhs.name}` does not allow prefixes.",
            )
        if self.name == "kilo" and rhs.name == "gram":
            raise CompositionError(
                outer=Scaled,
                inner=rhs,
                msg="cannot apply prefix `kilo` to `gram`.",
                help="use the `KG` unit directly.",
            )
    elif isinstance(rhs, Tagged) and not isinstance(
        (ref := _unwrap_tagged_or_aliased(rhs)),
        (BaseUnit, Aliased),
    ):
        raise CompositionError(
            outer=Scaled,
            inner=rhs,
            msg="prefixes cannot be applied to this type of tagged expression.",
            help=f"the inner reference is `{type(ref).__name__}`, which cannot be prefixed.",
        )
    # TODO: robustly handle Scaled(Log())
    return Scaled(rhs, self)

QtyKind ¤

QtyKind(
    unit_si_coherent: _TaggedAllowedExpr,
    tags: tuple[Tag, ...] | None = None,
)

An abstract kind of quantity (ISO 80000-1) represents a "concept" (e.g. speed) without a specific unit tied to it.

When called with a unit, it becomes a concrete unit with tagged context.

unit_si_coherent ¤

unit_si_coherent: _TaggedAllowedExpr

The coherent SI unit (i.e. no conversion factors involved, e.g. M_PERS for speed, not KM_PERHOUR)

tags ¤

tags: tuple[Tag, ...] | None = None

si_coherent ¤

si_coherent() -> Expr

Return the SI coherent unit with tags.

Source code in src/isqx/_core.py
578
579
580
581
582
def si_coherent(self) -> Expr:
    """Return the SI coherent unit with tags."""
    if self.tags is None:
        return self.unit_si_coherent
    return self.unit_si_coherent[self.tags]

__getitem__ ¤

__getitem__(tags: tuple[Tag, ...] | Tag) -> QtyKind

Attach additional tags to this quantity kind.

Source code in src/isqx/_core.py
584
585
586
587
588
589
def __getitem__(self, tags: tuple[Tag, ...] | Tag) -> QtyKind:
    """Attach additional tags to this quantity kind."""
    t = tags if isinstance(tags, tuple) else (tags,)
    if self.tags is not None:
        t = self.tags + t
    return replace(self, tags=t)

__call__ ¤

__call__(unit: _TaggedAllowedExpr) -> Expr

Create a tagged unit from this quantity kind.

Source code in src/isqx/_core.py
591
592
593
594
595
596
597
598
599
600
601
602
def __call__(self, unit: _TaggedAllowedExpr) -> Expr:
    """Create a tagged unit from this quantity kind."""
    if unit is self.unit_si_coherent:
        return self.si_coherent()

    dim_unit = dimension(simplify(unit))
    dim_unit_self = dimension(simplify(self.unit_si_coherent))
    if dim_unit != dim_unit_self:
        raise UnitKindMismatchError(self, unit, dim_unit_self, dim_unit)
    if self.tags is None:
        return unit
    return unit[self.tags]

DELTA ¤

DELTA = _Delta('finite')

A tag to indicate a finite change in a quantity \(\Delta Q\).

Example: duration in time

DIFFERENTIAL ¤

DIFFERENTIAL = _Delta('differential')

A tag to indicate an exact differential \(dQ\) (whose integral is path-independent).

Example: volume element

INEXACT_DIFFERENTIAL ¤

INEXACT_DIFFERENTIAL = _Delta('inexact_differential')

A tag to indicate an inexact differential \(\delta Q\) (whose integral is path-dependent).

Example: inexact differential in heat

Quantity ¤

Quantity(value: Number | LazyProduct, unit: PhysicalUnit)

A simple data container for a value and its unit, for use in specifying the origin of a point on the scale.

Warning

This is not an isqx.Expr should not be further composed.

value ¤

unit ¤

OriginAt ¤

OriginAt(location: Quantity | Hashable)

Bases: HasTagValidation

A tag to specify the origin for a point on the scale. Commonly combined with isqx.DELTA to also represent a change in the quantity.

Examples:

  • Unix epoch: S[OriginAt("unix epoch")]
  • difference in actual temperature and ISA temperature: K[DELTA, OriginAt((15, CELSIUS))]
  • difference in pressure: Pa[DELTA, OriginAt((100, PSI))]

location ¤

location: Quantity | Hashable

The location of the "zero point" of the measurement.

Can be a quantity (value + unit, e.g. (100, PSI)) or simply a hashable object (e.g. the string "unix epoch"). Hashable objects are useful for scenarios where the origin itself is contextual, for example:

  • the height above the ground is dependent on the geographic location
  • the stock price relative to yesterday's close
  • time elapsed relative to the engine ignition

__validate_tag__ ¤

__validate_tag__(
    reference: Expr, tags: tuple[Tag, ...]
) -> None
Source code in src/isqx/_core.py
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
def __validate_tag__(self, reference: Expr, tags: tuple[Tag, ...]) -> None:
    if (
        isinstance(q := self.location, Quantity)
        and not isinstance(reference, Dimensionless)
        and q.unit != reference
    ):
        raise CompositionError(
            outer=OriginAt,
            inner=reference,
            msg=(
                f"expression {reference} is not compatible with "
                f"`OriginAt.location` of unit {q.unit}"
            ),
            help=f"`OriginAt.location` must be of unit {reference}",
        )
    _check_duplicate_tags(self, tags)

__hash__ ¤

__hash__() -> int
Source code in src/isqx/_core.py
709
710
def __hash__(self) -> int:
    return hash((self.__class__.__name__, self.location))

ratio ¤

ratio(
    numerator: Expr, denominator: Expr | Quantity
) -> Tagged

Returns a dimensionless between two expressions of the same dimension.

The denominator can be a quantity (value + unit) to represent logarithmic units like isqx.DBW.

See: https://en.wikipedia.org/wiki/Dimensionless_quantity#Ratios,_proportions,_and_angles

Source code in src/isqx/_core.py
720
721
722
723
724
725
726
727
def ratio(numerator: Expr, denominator: Expr | Quantity) -> Tagged:
    """Returns a dimensionless between two expressions of the same dimension.

    The denominator can be a [quantity (value + unit)][isqx.Quantity] to
    represent logarithmic units like [`isqx.DBW`][].

    See: https://en.wikipedia.org/wiki/Dimensionless_quantity#Ratios,_proportions,_and_angles"""
    return _RATIO[_RatioBetween(numerator, denominator)]

VECTOR ¤

VECTOR = _Tensor(rank=1)

A tag to indicate that the quantity can be a vector. [ISO 80000-2:2019 2-18.1]. It is not restricted to three dimensions.

Examples:

  • velocity vector: M_PERS[VECTOR]
  • force vector: F[VECTOR]

TENSOR_SECOND_ORDER ¤

TENSOR_SECOND_ORDER = _Tensor(rank=2)

A tag to indicate that the quantity can be a second-order tensor.

Examples:

  • stress tensor: (N * M**-2)["stress", TENSOR_SECOND_ORDER]

CARTESIAN ¤

CARTESIAN = _CoordinateSystem(name='cartesian')

A tag to indicate that the quantity is expressed in Cartesian coordinates (\(x\), \(y\), \(z\)) [ISO 80000-2:2019 2-17.1].

The position vector is \(\mathbf{r} = x \mathbf{e}_x + y \mathbf{e}_y + z \mathbf{e}_z\), and its differential is \(d\mathbf{r} = dx \mathbf{e}_x + dy \mathbf{e}_y + dz \mathbf{e}_z\).

CYLINDRICAL ¤

CYLINDRICAL = _CoordinateSystem(name='cylindrical')

A tag to indicate that the quantity is expressed in cylindrical coordinates (radial distance \(\rho\), azimuth \(\varphi\), axial coordinate or height \(z\)) [ISO 80000-2:2019 2-17.2].

The position vector is \(\mathbf{r} = \rho \mathbf{e}_\rho(\varphi) + z \mathbf{e}_z\), and its differential is \(d\mathbf{r} = d\rho \mathbf{e}_\rho(\varphi) + \rho d\varphi \mathbf{e}_\varphi(\varphi) + dz \mathbf{e}_z\).

If \(z = 0\), it is equivalent to polar coordinates.

SPHERICAL ¤

SPHERICAL = _CoordinateSystem(name='spherical')

A tag to indicate that the quantity is expressed in spherical coordinates (radial distance \(r\), polar angle \(\theta\), azimuthal angle \(\varphi\)) [ISO 80000-2:2019 2-17.3].

The position vector is \(\mathbf{r} = r \mathbf{e}_r(\theta, \varphi)\), and its differential is \(d\mathbf{r} = dr \mathbf{e}_r(\theta, \varphi) + r d\theta \mathbf{e}_\theta(\theta, \varphi) + r \sin(\theta) d\varphi \mathbf{e}_\varphi(\theta, \varphi)\).

COMPLEX ¤

COMPLEX = _Complex()

A tag to indicate that the quantity is complex-valued.

PhotometricCondition ¤

PhotometricCondition(
    kind: Literal[
        "photopic",
        "scotopic",
        "mesopic",
        "photopic_cie_1964_10degree",
        "photopic_cie_1988_mod_2degree",
    ]
    | str,
)

Bases: HasTagValidation

Photometric condition [ISO 80000-7]. Can be:

kind ¤

kind: (
    Literal[
        "photopic",
        "scotopic",
        "mesopic",
        "photopic_cie_1964_10degree",
        "photopic_cie_1988_mod_2degree",
    ]
    | str
)

__validate_tag__ ¤

__validate_tag__(
    reference: Expr, tags: tuple[Tag, ...]
) -> None
Source code in src/isqx/_core.py
876
877
def __validate_tag__(self, reference: Expr, tags: tuple[Tag, ...]) -> None:
    _check_duplicate_tags(self, tags)

__hash__ ¤

__hash__() -> int
Source code in src/isqx/_core.py
879
880
def __hash__(self) -> int:
    return hash((self.__class__.__name__, self.kind))

Tag ¤

Tag: TypeAlias = Union[
    OriginAt,
    _RatioBetween,
    _Delta,
    _Tensor,
    _CoordinateSystem,
    _Complex,
    PhotometricCondition,
    HasTagValidation,
    Hashable,
]

A tag can be any hashable object (e.g. frozen dataclasses or strings).

dimension ¤

dimension(expr: Dimensionless) -> Dimensionless
dimension(expr: BaseDimension) -> BaseDimension
dimension(expr: BaseUnit) -> BaseDimension
dimension(expr: Exp) -> Exp
dimension(expr: Mul) -> Mul
dimension(expr: Log) -> Dimensionless
dimension(expr: Tagged) -> Tagged | Expr
dimension(expr: Aliased | Scaled | Translated) -> Expr
dimension(expr: Expr) -> Expr

Return the dimension of this "unit-like" expression. Note that it does not perform simplification.

Examples:

  • Exp(M, 2) -> Exp(DIM_LENGTH, 2)
  • Mul(M, Exp(S, -1)) -> Mul(DIM_LENGTH, Exp(DIM_TIME, -1))
Source code in src/isqx/_core.py
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
@cache
def dimension(expr: Expr) -> Expr:
    """Return the dimension of this "unit-like" expression.
    Note that it does not perform simplification.

    Examples:

    - `Exp(M, 2)` -> `Exp(DIM_LENGTH, 2)`
    - `Mul(M, Exp(S, -1))` -> `Mul(DIM_LENGTH, Exp(DIM_TIME, -1))`
    """
    if isinstance(expr, (Dimensionless, BaseDimension)):
        return expr
    if isinstance(expr, BaseUnit):
        return expr._dimension
    if isinstance(expr, (Aliased, Scaled, Translated)):
        return dimension(expr.reference)
    if isinstance(expr, Exp):
        return Exp(dimension(expr.base), expr.exponent)  # type: ignore
    if isinstance(expr, Mul):
        return Mul(tuple(dimension(term) for term in expr.terms))  # type: ignore
    if isinstance(expr, Tagged):
        ref_dim = dimension(expr.reference)
        # note: log level's dimension is always dimensionless
        # so we need to strip away tags that dont make any more sense
        tags = []
        for tag in expr.tags:
            if isinstance(tag, HasTagValidation):
                try:
                    tag.__validate_tag__(ref_dim, expr.tags)
                except CompositionError:
                    continue
            tags.append(tag)
        if not tags:
            return ref_dim
        return ref_dim[tuple(tags)]
    if isinstance(expr, Log):
        return Dimensionless(f"log_{repr(expr.reference)}")
    assert_never(expr)

kind ¤

kind(expr: Dimensionless | Log) -> Literal['dimensionless']
kind(expr: BaseDimension) -> Literal['dimension']
kind(expr: BaseUnit) -> Literal['unit']
kind(
    expr: Exp
    | Mul
    | Scaled
    | Aliased
    | Translated
    | Tagged,
) -> ExprKind
kind(expr: Expr) -> ExprKind

Whether this expression is a unit, dimension, or dimensionless.

Source code in src/isqx/_core.py
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
@cache
def kind(expr: Expr) -> ExprKind:
    """Whether this expression is a unit, dimension, or dimensionless."""
    if isinstance(expr, Dimensionless):
        return "dimensionless"
    if isinstance(expr, BaseDimension):
        return "dimension"
    if isinstance(expr, BaseUnit):
        return "unit"
    if isinstance(expr, Exp):
        return kind(expr.base)
    if isinstance(expr, (Scaled, Aliased, Translated, Tagged)):
        return kind(expr.reference)
    if isinstance(expr, Mul):
        # all terms have a consistent underlying kind (unit/dimension)
        for term in expr.terms:
            term_kind = kind(term)
            if term_kind != "dimensionless":
                return term_kind
        return "dimensionless"
        # everything left are dimensionless to the power of something
    if isinstance(expr, Log):
        return "dimensionless"
    assert_never(expr)

SimplifiedExpr ¤

simplify ¤

simplify(expr: Dimensionless) -> Dimensionless
simplify(expr: BaseDimension) -> BaseDimension
simplify(expr: BaseUnit) -> BaseUnit
simplify(expr: Aliased) -> SimplifiedExpr
simplify(expr: Translated) -> Translated
simplify(expr: Log) -> Log
simplify(expr: Tagged) -> Tagged
simplify(expr: Expr) -> SimplifiedExpr
Source code in src/isqx/_core.py
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
@cache
def simplify(expr: Expr) -> SimplifiedExpr:
    if isinstance(expr, (Dimensionless, BaseDimension, BaseUnit)):
        return expr
    if isinstance(expr, Aliased):
        return simplify(expr.reference)
    if isinstance(expr, Translated):
        return Translated(
            simplify(expr.reference),  # type: ignore
            expr.offset,
            expr.name,
        )
    if isinstance(expr, Tagged):
        return Tagged(simplify(expr.reference), expr.tags)  # type: ignore
    base_exponent_pairs: dict[SimplifiedExpr, Exponent] = {}
    scaled_conversions: list[tuple[Scaled, Exponent]] = []
    _decompose_expr(expr, 1, base_exponent_pairs, scaled_conversions)
    return _build_canonical_expr(base_exponent_pairs, scaled_conversions)

SupportsConversion ¤

Bases: Protocol

__call__ ¤

__call__(value)

Convert a value in the origin unit to the target unit.

Parameters:

Name Type Description Default
value

An int, float, fractions.Fraction, or any other numeric type^. ^ If the converter was created with exact=True, the scale will be fractions and thus may not be compatible with many numeric libraries (e.g. fractions.Fraction * numpy.array fails). decimal.Decimal inputs should be converted into fractions.Fraction.

required
Source code in src/isqx/_core.py
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
def __call__(self, value):  # type: ignore
    """Convert a value in the origin unit to the target unit.

    :param value: An `int`, `float`, [fractions.Fraction][],
        or any other numeric type^.

        ^ If the converter was created with `exact=True`, the `scale`
        will be fractions and thus may not be compatible with many numeric
        libraries (e.g. `fractions.Fraction * numpy.array` fails).
        [decimal.Decimal][] inputs should be converted into
        [fractions.Fraction][].
    """

Converter ¤

Converter(scale: Number)

Bases: SupportsConversion

scale ¤

scale: Number

__call__ ¤

__call__(value)
Source code in src/isqx/_core.py
1232
1233
def __call__(self, value):  # type: ignore
    return self.scale * value

NonAffineConverter ¤

NonAffineConverter(scale: Number, offset: Number)

Bases: SupportsConversion

scale ¤

scale: Number

offset ¤

offset: Number

__call__ ¤

__call__(value)
Source code in src/isqx/_core.py
1241
1242
def __call__(self, value):  # type: ignore
    return value * self.scale + self.offset

convert ¤

convert(
    origin: Expr,
    target: Expr,
    *,
    exact: bool = False,
    ctx: decimal.Context | None = None,
) -> Converter | NonAffineConverter

Create a new unit converter from one unit to another.

Checks that the underlying dimension are compatible (e.g. USD/year and HKD/hour) and computes the total scaling factor.

Source code in src/isqx/_core.py
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
def convert(
    origin: Expr,
    target: Expr,
    *,
    exact: bool = False,
    ctx: decimal.Context | None = None,
) -> Converter | NonAffineConverter:
    """Create a new unit converter from one unit to another.

    Checks that the underlying dimension are compatible
    (e.g. `USD/year` and `HKD/hour`) and computes the total scaling factor.
    """
    ctx = ctx or decimal.getcontext()
    origin_simpl = simplify(origin)
    target_simpl = simplify(target)

    info_origin = _flatten(origin_simpl)
    info_target = _flatten(target_simpl)

    log_info_origin = _get_log_info(info_origin)
    log_info_target = _get_log_info(info_target)

    if log_info_origin and log_info_target:
        if not (
            log_info_origin.b == log_info_target.b
            and log_info_origin.q_measured_unit
            == log_info_target.q_measured_unit
            and log_info_origin.q_ref.unit == log_info_target.q_ref.unit
            and log_info_origin.other_tags == log_info_target.other_tags
        ):
            return _convert_logarithmic(
                log_info_origin, log_info_target, exact=exact, ctx=ctx
            )
        # if the underlying log unit is the same (same base and reference qty type),
        # then it's a simple linear scaling (e.g. Np -> dNp)
    elif log_info_origin or log_info_target:
        raise NonLinearConversionError(
            origin=origin,
            target=target,
        )  # e.g. V = V_ref * b**(L_dBV / k), L_dbV = k * log_b(V / V_ref)

    if (origin_kind := kind(origin_simpl)) != (
        target_kind := kind(target_simpl)
    ):
        raise KindMismatchError(
            origin_kind=origin_kind, target_kind=target_kind
        )

    origin_dim = dimension(info_origin.expr)
    target_dim = dimension(info_target.expr)
    origin_dim_terms = (
        origin_dim.terms if isinstance(origin_dim, Mul) else (origin_dim,)
    )
    target_dim_terms = (
        target_dim.terms if isinstance(target_dim, Mul) else (target_dim,)
    )
    if origin_dim_terms != target_dim_terms:
        raise DimensionMismatchError(origin, target, origin_dim, target_dim)
    # we have:
    #   v_abs = scale_origin * v_origin + offset_origin
    #   v_abs = scale_target * v_target + offset_target
    # then:
    #   v_target = (scale_origin / scale_target) * v_origin +
    #              (offset_origin - offset_target) / scale_target
    scale_origin = list(_products(info_origin.factor))

    inv_scale_target = tuple(f for f in _products_inverse(info_target.factor))
    scale = LazyProduct(tuple([*scale_origin, *inv_scale_target]))

    offset_numerator = _factor_to_fraction(
        info_origin.offset, ctx=ctx
    ) - _factor_to_fraction(info_target.offset, ctx=ctx)
    offset = LazyProduct(tuple([offset_numerator, *inv_scale_target]))

    return _converter_new(
        scale=scale.to_exact(ctx=ctx) if exact else scale.to_approx(),
        offset=offset.to_exact(ctx=ctx) if exact else offset.to_approx(),
    )

LazyProduct ¤

LazyProduct(
    products: tuple[tuple[Number, Exponent] | Number, ...],
)

Bases: SupportsFloat

Represents a lazy product of a sequence of numbers raised to an optional exponent, i.e. \(\prod_i x_i\), or \(\prod_i x_i^{e_i}\).

Lazy evaluation allows the choice between evaluating it to an exact value (taking longer to compute, useful for financial calculations) or an approximate float.

products ¤

products: tuple[tuple[Number, Exponent] | Number, ...]

from_derived_conversions ¤

from_derived_conversions(
    derived_conversions: Sequence[tuple[Scaled, Exponent]],
) -> LazyProduct
Source code in src/isqx/_core.py
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
@classmethod
def from_derived_conversions(
    cls,
    derived_conversions: Sequence[tuple[Scaled, Exponent]],
) -> LazyProduct:
    products: list[tuple[Number, Exponent] | Number] = []
    for scaled, exponent in derived_conversions:
        for inner_item in _products(scaled.factor):
            if isinstance(inner_item, tuple):
                base, inner_exp = inner_item
                products.append((base, inner_exp * exponent))
            else:
                products.append((inner_item, exponent))
    return cls(tuple(products))

to_approx ¤

to_approx() -> float

Reduce it to an approximate float value. Good enough for most applications.

Source code in src/isqx/_core.py
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
def to_approx(self) -> float:
    """Reduce it to an approximate float value. Good enough for most
    applications."""
    product = 1.0
    for item in self.products:
        base, exponent = item if isinstance(item, tuple) else (item, 1)
        if base == 0:
            if exponent > 0:
                return 0.0
            if exponent == 0:
                continue  # 0 ** 0 = 1
        if base == 1:
            continue
        product *= float(base) ** float(exponent)
    return product

__float__ ¤

__float__() -> float
Source code in src/isqx/_core.py
1582
1583
def __float__(self) -> float:
    return self.to_approx()

to_exact ¤

to_exact(ctx: decimal.Context) -> Fraction | Decimal

Reduce it to an exact fraction or decimal.

Parameters:

Name Type Description Default
ctx decimal.Context

The decimal context (precision, rounding, etc.) to use. decimal.getcontext() can be used to get the current context. The return type depends on the items of each product: +--------+-------+----------------+ | None | int | Fraction(p, q) | <- exponent +----------------+--------+-------+----------------+ | Decimal | . | . | x | | Fraction(a, b) | . | . | * | | float | . | x | x | | int | . | . | ^ | +----------------+--------+-------+----------------+ ^ |_ base Can `base ** exponent` be represented exactly a `Fraction`? . Yes * Only if q > 0 and a^p and b^q are the perfect q-th power of an integer e.g. (8/27) ** (1/3) = 2/3 ^ Sometimes, e.g. 4 ** (1/2) x No, decimal only For simplicity, only cases that can definitively be represented as a Fraction are returned as such. A Decimal is returned otherwise.

required
Source code in src/isqx/_core.py
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
def to_exact(self, ctx: decimal.Context) -> Fraction | Decimal:
    """Reduce it to an *exact* fraction or decimal.

    :param ctx: The decimal context (precision, rounding, etc.) to use.
        `decimal.getcontext()` can be used to get the current context.

    The return type depends on the items of each product:

    ```
                     +--------+-------+----------------+
                     |  None  |  int  | Fraction(p, q) | <- exponent
    +----------------+--------+-------+----------------+
    | Decimal        |   .    |   .   |       x        |
    | Fraction(a, b) |   .    |   .   |       *        |
    | float          |   .    |   x   |       x        |
    | int            |   .    |   .   |       ^        |
    +----------------+--------+-------+----------------+
      ^
      |_ base

    Can `base ** exponent` be represented exactly a `Fraction`?

    . Yes
    * Only if q > 0 and a^p and b^q are the perfect q-th power of an integer
      e.g. (8/27) ** (1/3) = 2/3
    ^ Sometimes, e.g. 4 ** (1/2)
    x No, decimal only
    ```

    For simplicity, only cases that can definitively be represented as a
    `Fraction` are returned as such. A `Decimal` is returned otherwise.
    """
    # accumulate products in two streams: if the "tripwire" for decimal
    # is hit, we must return Decimal.
    product_fraction = Fraction(1)
    product_decimal = Decimal(1)
    for item in self.products:
        if not isinstance(item, tuple):  # no exponent
            product_fraction *= _factor_to_fraction(item, ctx=ctx)
            continue
        base, exponent = item
        if base == 0:
            if exponent > 0:
                return Fraction(0)
            if exponent < 0:
                raise ZeroDivisionError
            continue  # 0 ** 0 = 1
        if base == 1:
            continue
        if isinstance(exponent, int):
            # most of the time, we can represent it as Fraction
            if isinstance(base, float):
                base_decimal = ctx.create_decimal_from_float(base)
                product_decimal *= base_decimal**exponent
            else:
                base_fraction = _factor_to_fraction(base, ctx=ctx)
                product_fraction *= base_fraction**exponent
        elif isinstance(exponent, Fraction):
            # but raising to a Fraction exponent requires decimal
            if isinstance(base, SupportsDecimal):
                base_decimal = base.to_decimal(ctx=ctx)
            elif isinstance(base, Decimal):
                base_decimal = base
            elif isinstance(base, Fraction):  # *
                base_decimal = _fraction_to_decimal(base)
            elif isinstance(base, float):
                base_decimal = ctx.create_decimal_from_float(base)
            elif isinstance(base, int):  # ^
                base_decimal = Decimal(base, context=ctx)
            else:
                assert_never(base)
            exponent_decimal = _fraction_to_decimal(exponent)
            product_decimal *= ctx.power(base_decimal, exponent_decimal)
        else:
            assert_never(exponent)
    if product_decimal == Decimal(1):
        return product_fraction
    return _fraction_to_decimal(product_fraction) * product_decimal

StdUncertainty ¤

StdUncertainty(value: int)

Concise notation for the one-standard-deviation uncertainty of a numerical value.

For example, the parentheses in 12.3456(89) kg means:

     numerical value = 12.3456
standard uncertainty =  0.0089
Use typing.Annotated to attach the uncertainty information to the value:
from decimal import Decimal
from typing import Annotated
from isqx import KG, StdUncertainty

CONST_FOO: Annotated[Decimal, KG, StdUncertainty(89)] = Decimal("12.3456")

value ¤

value: int

The one-standard-deviation uncertainty expressed in the least significant digit(s) of the numerical value. Must be greater than zero.

E ¤

E: Final[_E] = _E()

The Euler number ISO 80000-2:2019 2-13.1.

PI ¤

PI: Final[_PI] = _PI()

The ratio of the circumference of a circle to its diameter ISO 80000-2:2019 2-14.1.

AnnotatedMetadata ¤

AnnotatedMetadata(
    unit: Expr | None,
    std_uncertainty: StdUncertainty | None,
)

unit ¤

unit: Expr | None

std_uncertainty ¤

std_uncertainty: StdUncertainty | None

from_args ¤

from_args(args: Sequence[Any]) -> AnnotatedMetadata

Extract metadata from the args of an Annotated type.

Example:

>>> from typing import Annotated
>>> from isqx import KG, StdUncertainty
>>> from dataclasses import dataclass
>>> @dataclass
... class Foo:
...     class_member: Annotated[float, KG, StdUncertainty(13)]
...
>>> def bar(function_arg: Annotated[float, KG]) -> None:
...     ...
...
>>> from isqx import AnnotatedMetadata
>>> def print_metadata(obj) -> None:
...     for name, ann in get_type_hints(obj, include_extras=True).items():
...         print(f"{name}: {AnnotatedMetadata.from_args(get_args(ann))}")
...
>>> print_metadata(Foo)
class_member: AnnotatedMetadata(unit=kilogram, std_uncertainty=StdUncertainty(value=13))
>>> print_metadata(bar)
function_arg: AnnotatedMetadata(unit=kilogram)
return: AnnotatedMetadata(unit=None)

Source code in src/isqx/_core.py
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
@classmethod
def from_args(cls, args: Sequence[Any]) -> AnnotatedMetadata:
    """Extract metadata from the args of an `Annotated` type.

    Example:
    ```pycon
    >>> from typing import Annotated
    >>> from isqx import KG, StdUncertainty
    >>> from dataclasses import dataclass
    >>> @dataclass
    ... class Foo:
    ...     class_member: Annotated[float, KG, StdUncertainty(13)]
    ...
    >>> def bar(function_arg: Annotated[float, KG]) -> None:
    ...     ...
    ...
    >>> from isqx import AnnotatedMetadata
    >>> def print_metadata(obj) -> None:
    ...     for name, ann in get_type_hints(obj, include_extras=True).items():
    ...         print(f"{name}: {AnnotatedMetadata.from_args(get_args(ann))}")
    ...
    >>> print_metadata(Foo)
    class_member: AnnotatedMetadata(unit=kilogram, std_uncertainty=StdUncertainty(value=13))
    >>> print_metadata(bar)
    function_arg: AnnotatedMetadata(unit=kilogram)
    return: AnnotatedMetadata(unit=None)
    ```
    """
    unit: _TaggedAllowedExpr | None = None
    std_uncertainty: StdUncertainty | None = None
    for arg in args:
        if isinstance(arg, _ARGS_EXPR):
            if unit is not None:
                raise DuplicateAnnotationError(args, arg)
            unit = arg
        elif isinstance(arg, StdUncertainty):
            if std_uncertainty is not None:
                raise DuplicateAnnotationError(args, arg)
            std_uncertainty = arg
    return cls(unit=unit, std_uncertainty=std_uncertainty)

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1799
1800
1801
1802
1803
1804
1805
def __str__(self) -> str:
    kv = []
    for field in fields(self):
        if (value := getattr(self, field.name)) is None:
            continue
        kv.append(f"{field.name}={value}")
    return f"{type(self).__name__}({', '.join(kv)})"

module_attribute_metadata ¤

module_attribute_metadata(
    module_rt: ModuleType,
) -> Generator[tuple[str, AnnotatedMetadata], None, None]

Yields all (name, metadata) pairs for module attributes that are annotated with valid unit expressions and/or standard uncertainties in the given module.

Effectively returns the units and uncertainties of all constants. Example:

>>> from isqx import module_attribute_metadata, iso80000
>>> for name, metadata in module_attribute_metadata(iso80000):
...     print(f"{name}: {metadata}")
...
CONST_SPEED_OF_LIGHT_VACUUM: AnnotatedMetadata(unit=meter · second⁻¹)
CONST_PLANCK: AnnotatedMetadata(unit=joule · second
- joule = newton · meter
- newton = kilogram · meter · second⁻²)
CONST_REDUCED_PLANCK: AnnotatedMetadata(unit=joule · second
- joule = newton · meter
- newton = kilogram · meter · second⁻²)
...

Source code in src/isqx/_core.py
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
def module_attribute_metadata(
    module_rt: ModuleType,
) -> Generator[tuple[str, AnnotatedMetadata], None, None]:
    """Yields all (name, metadata) pairs for module attributes that are
    annotated with valid [unit expressions][isqx.Expr] and/or
    [standard uncertainties][isqx.StdUncertainty] in the given module.

    Effectively returns the units and uncertainties of all constants. Example:
    ```pycon
    >>> from isqx import module_attribute_metadata, iso80000
    >>> for name, metadata in module_attribute_metadata(iso80000):
    ...     print(f"{name}: {metadata}")
    ...
    CONST_SPEED_OF_LIGHT_VACUUM: AnnotatedMetadata(unit=meter · second⁻¹)
    CONST_PLANCK: AnnotatedMetadata(unit=joule · second
    - joule = newton · meter
    - newton = kilogram · meter · second⁻²)
    CONST_REDUCED_PLANCK: AnnotatedMetadata(unit=joule · second
    - joule = newton · meter
    - newton = kilogram · meter · second⁻²)
    ...
    ```
    """
    for name, anno in get_type_hints(module_rt, include_extras=True).items():
        if get_origin(anno) is not Annotated:
            continue
        metadata = AnnotatedMetadata.from_args(get_args(anno))
        yield name, metadata

Anchor ¤

Anchor(text: str, path: str)

A simple wrapper over a text with an external link.

It is used to:

text ¤

text: str

path ¤

path: str

StrFragment ¤

StrFragment: TypeAlias = Union[str, Anchor]

IsqxError ¤

Bases: Exception

Base exception for all errors raised by the isqx library.

CompositionError ¤

CompositionError(
    outer: type,
    inner: Any,
    msg: str,
    help: str | None = None,
)

Bases: IsqxError

outer ¤

outer: type

inner ¤

inner: Any

msg ¤

msg: str

help ¤

help: str | None = None

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
def __str__(self) -> str:  # pragma: no cover
    outer_name = (
        self.outer.name
        if isinstance(self.outer, Prefix)
        else self.outer.__name__
    )
    inner_repr = f"`{self.inner}` (of type `{type(self.inner).__name__}`)"

    message = (
        f"invalid composition: cannot apply `{outer_name}` to {inner_repr}."
        f"\nreason: {self.msg}"
    )
    if self.help:
        message += f"\n= help: {self.help}"
    return message

MixedKindError ¤

MixedKindError(terms: tuple[Expr, ...])

Bases: IsqxError

terms ¤

terms: tuple[Expr, ...]

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1892
1893
1894
1895
1896
1897
1898
1899
def __str__(self) -> str:  # pragma: no cover
    return (
        "cannot mix expressions of different kinds in a product."
        f"\n  found kinds: {', '.join(f'`{kind(t)}`' for t in self.terms)}"
        "\n= help: all terms in a `Mul` expression must be of the same kind "
        "(e.g., all units like `M` and `S`, or all dimensions like "
        "`DIM_LENGTH` and `DIM_TIME`)."
    )

KindMismatchError ¤

KindMismatchError(
    origin_kind: ExprKind, target_kind: ExprKind
)

Bases: IsqxError

origin_kind ¤

origin_kind: ExprKind

target_kind ¤

target_kind: ExprKind

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1908
1909
1910
1911
1912
1913
def __str__(self) -> str:  # pragma: no cover
    return (
        "cannot convert between expressions of different kinds."
        f"\nnote: origin kind: `{self.origin_kind}`"
        f"\n      target kind: `{self.target_kind}`"
    )

DimensionMismatchError ¤

DimensionMismatchError(
    origin: Expr,
    target: Expr,
    dim_origin: Expr,
    dim_target: Expr,
)

Bases: IsqxError

origin ¤

origin: Expr

target ¤

target: Expr

dim_origin ¤

dim_origin: Expr

dim_target ¤

dim_target: Expr

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1923
1924
1925
1926
1927
1928
1929
def __str__(self) -> str:  # pragma: no cover
    return (
        f"cannot convert from `{self.origin}` to `{self.target}`."
        "\n= help: expected compatible dimensions, but found:"
        f"\ndimension of origin: `{self.dim_origin}`"
        f"\ndimension of target: `{self.dim_target}`"
    )

UnitKindMismatchError ¤

UnitKindMismatchError(
    qtykind: QtyKind,
    unit: Expr,
    dim_kind: Expr,
    dim_unit: Expr,
)

Bases: IsqxError

qtykind ¤

qtykind: QtyKind

unit ¤

unit: Expr

dim_kind ¤

dim_kind: Expr

dim_unit ¤

dim_unit: Expr

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1939
1940
1941
1942
1943
1944
1945
1946
1947
def __str__(self) -> str:  # pragma: no cover
    return (
        f"cannot create tagged unit for kind `{self.qtykind.tags}` with "
        f"unit `{self.unit}`."
        f"\nexpected dimension of kind: `{self.dim_kind}`"
        f" (`{self.qtykind.unit_si_coherent}`)"
        f"\n   found dimension of unit: `{self.dim_unit}`"
        f" (`{self.unit}`)"
    )

NonLinearConversionError ¤

NonLinearConversionError(origin: Expr, target: Expr)

Bases: IsqxError

origin ¤

origin: Expr

target ¤

target: Expr

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1955
1956
1957
1958
1959
1960
1961
1962
1963
def __str__(self) -> str:  # pragma: no cover
    return (
        f"cannot create a value-agnostic converter from `{self.origin}` "
        f"to `{self.target}`."
        "conversion between a logarithmic and a linear unit is non-linear."
        "\n= help: this requires a reference value (e.g., 1V for dBV), "
        "but the `isqx` library only performs value-agnostic conversions. "
        "perform the calculation manually instead."
    )

DuplicateAnnotationError ¤

DuplicateAnnotationError(
    annotation_args: Sequence[Any],
    conflicting_arg: _TaggedAllowedExpr | StdUncertainty,
)

Bases: IsqxError

annotation_args ¤

annotation_args: Sequence[Any]

conflicting_arg ¤

conflicting_arg: _TaggedAllowedExpr | StdUncertainty

__str__ ¤

__str__() -> str
Source code in src/isqx/_core.py
1971
1972
1973
1974
1975
1976
1977
def __str__(self) -> str:  # pragma: no cover
    return (
        f"duplicate annotation `{self.conflicting_arg}` found in "
        f"{', '.join(repr(arg) for arg in self.annotation_args)}."
        "\n= help: only one unit and one standard uncertainty can be "
        "applied."
    )

_fmt ¤

fmt ¤

fmt(
    expr: Expr,
    formatter: Formatter | _FormatSpec | str = "basic",
) -> str
Source code in src/isqx/_fmt.py
54
55
56
57
58
59
60
61
62
def fmt(expr: Expr, formatter: Formatter | _FormatSpec | str = "basic") -> str:
    if isinstance(formatter, Formatter):
        return "".join(
            item.text if isinstance(item, Anchor) else item
            for item in formatter.fmt(expr)
        )
    if formatter == "" or formatter == "basic":
        return fmt(expr, DEFAULT_FORMATTER)
    raise NotImplementedError(f"unknown format {formatter=}")

Formatter ¤

Bases: Protocol

fmt ¤

fmt(expr: Expr) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
67
def fmt(self, expr: Expr) -> Generator[StrFragment, None, None]: ...

Precedence ¤

Bases: IntEnum

NONE ¤

NONE = auto()

Virtual precedence, not a Python expression.

Use this when the parent already is in a parenthesised group and you want to guarantee that the child will not add any additional parentheses.

MUL ¤

MUL = auto()

SCALED ¤

SCALED = auto()

LOG ¤

LOG = auto()

TAGGED ¤

TAGGED = auto()

EXP ¤

EXP = auto()

ATOM ¤

ATOM = auto()

Visitor ¤

Bases: Protocol[_VisitorState, _VisitorResult]

visit_named ¤

visit_named(
    expr: NamedExpr, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
103
104
105
def visit_named(
    self, expr: NamedExpr, state: _VisitorState
) -> _VisitorResult: ...

visit_exp ¤

visit_exp(
    expr: Exp, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
107
def visit_exp(self, expr: Exp, state: _VisitorState) -> _VisitorResult: ...

visit_mul ¤

visit_mul(
    expr: Mul, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
109
def visit_mul(self, expr: Mul, state: _VisitorState) -> _VisitorResult: ...

visit_scaled ¤

visit_scaled(
    expr: Scaled, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
111
112
113
def visit_scaled(
    self, expr: Scaled, state: _VisitorState
) -> _VisitorResult: ...

visit_tagged ¤

visit_tagged(
    expr: Tagged, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
115
116
117
def visit_tagged(
    self, expr: Tagged, state: _VisitorState
) -> _VisitorResult: ...

visit_translated ¤

visit_translated(
    expr: Translated, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
119
120
121
def visit_translated(
    self, expr: Translated, state: _VisitorState
) -> _VisitorResult: ...

visit_log ¤

visit_log(
    expr: Log, state: _VisitorState
) -> _VisitorResult
Source code in src/isqx/_fmt.py
123
def visit_log(self, expr: Log, state: _VisitorState) -> _VisitorResult: ...

visit_expr ¤

visit_expr(
    visitor: Visitor[_VisitorState, _VisitorResult],
    expr: Expr,
    state: _VisitorState,
) -> _VisitorResult
Source code in src/isqx/_fmt.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def visit_expr(
    visitor: Visitor[_VisitorState, _VisitorResult],
    expr: Expr,
    state: _VisitorState,
) -> _VisitorResult:
    if isinstance(expr, (Dimensionless, BaseDimension, BaseUnit, Aliased)):
        return visitor.visit_named(expr, state)
    elif isinstance(expr, Exp):
        return visitor.visit_exp(expr, state)
    elif isinstance(expr, Mul):
        return visitor.visit_mul(expr, state)
    elif isinstance(expr, Scaled):
        return visitor.visit_scaled(expr, state)
    elif isinstance(expr, Tagged):
        return visitor.visit_tagged(expr, state)
    elif isinstance(expr, Translated):
        return visitor.visit_translated(expr, state)
    elif isinstance(expr, Log):
        return visitor.visit_log(expr, state)
    else:
        assert_never(expr)

BasicFormatter ¤

BasicFormatter(
    overrides: dict[Name, str] = dict(),
    verbose: bool = False,
    infix_mul: str = " · ",
)

Bases: Visitor[_BasicFormatterState, Generator[StrFragment, None, None]], Formatter

overrides ¤

overrides: dict[Name, str] = field(default_factory=dict)

verbose ¤

verbose: bool = False

infix_mul ¤

infix_mul: str = ' · '

fmt ¤

fmt(expr: Expr) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
179
180
181
182
183
184
185
186
187
188
189
190
191
def fmt(self, expr: Expr) -> Generator[StrFragment, None, None]:
    state = _BasicFormatterState()
    yield from self.visit(expr, state)

    if self.verbose and state.definitions:
        seen_definitions: set[str] = set()
        for name, expr in state.definitions.items():
            yield from self._fmt_definition(
                name,
                expr,
                seen_definitions=seen_definitions,
                depth=0,
            )

visit ¤

visit(
    expr: Expr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
223
224
225
226
227
228
229
230
231
232
def visit(
    self, expr: Expr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    precedence_expr = PRECEDENCE[type(expr)]
    needs_parentheses = state.parent_precedence >= precedence_expr
    if needs_parentheses:
        yield "("
    yield from visit_expr(self, expr, state)
    if needs_parentheses:
        yield ")"

visit_named ¤

visit_named(
    expr: NamedExpr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
236
237
238
239
240
241
242
243
244
245
246
def visit_named(
    self, expr: NamedExpr, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    name = expr.name
    name_formatted = self.overrides.get(name, name)
    yield name_formatted
    if (
        isinstance(expr, (Aliased, Translated))
        and name_formatted not in state.definitions
    ):
        state.definitions[name_formatted] = expr

visit_tagged ¤

visit_tagged(
    expr: Tagged, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
def visit_tagged(
    self, expr: Tagged, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    precedence_expr = PRECEDENCE[Tagged]
    with state._set_parent_precedence(precedence_expr):
        yield from self.visit(expr.reference, state)
    yield "["
    num_tags = len(expr.tags)
    for i, tag in enumerate(expr.tags):
        if isinstance(tag, _RatioBetween):
            yield "`"
            with state._set_parent_precedence(Precedence.NONE):
                yield from self.visit(tag.numerator, state)
            yield "` to `"
            if isinstance(q := tag.denominator, Quantity):
                yield from _format_factor(
                    q.value,
                    infix_mul=self.infix_mul,
                    format_product=self._fmt_product,
                )
                with state._set_parent_precedence(Precedence.NONE):
                    yield from self.visit(q.unit, state)
            else:
                with state._set_parent_precedence(Precedence.NONE):
                    yield from self.visit(q, state)
            yield "`"
        elif isinstance(tag, OriginAt):
            yield "relative to `"
            if isinstance(loc := tag.location, Quantity):
                yield from _format_factor(
                    loc.value,
                    infix_mul=self.infix_mul,
                    format_product=self._fmt_product,
                )
                with state._set_parent_precedence(precedence_expr):
                    yield from self.visit(loc.unit, state)
            else:  # hashable
                yield repr(tag.location)
            yield "`"
        elif tag is DELTA:
            yield "Δ"
        elif tag is DIFFERENTIAL:
            yield "differential"
        elif tag is INEXACT_DIFFERENTIAL:
            yield "inexact differential"
        else:
            yield repr(tag)
        if i < num_tags - 1:
            yield ", "
    yield "]"

visit_exp ¤

visit_exp(
    expr: Exp, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
299
300
301
302
303
304
def visit_exp(
    self, expr: Exp, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    with state._set_parent_precedence(PRECEDENCE[Exp]):
        yield from self.visit(expr.base, state)
    yield str(expr.exponent).translate(_BASIC_EXPONENT_MAP)

visit_mul ¤

visit_mul(
    expr: Mul, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
306
307
308
309
310
311
312
313
314
def visit_mul(
    self, expr: Mul, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    precedence_expr = PRECEDENCE[Mul]
    for i, term in enumerate(expr.terms):
        with state._set_parent_precedence(precedence_expr):
            yield from self.visit(term, state)
        if i < len(expr.terms) - 1:
            yield self.infix_mul

visit_scaled ¤

visit_scaled(
    expr: Scaled, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
316
317
318
319
320
321
322
323
324
325
def visit_scaled(
    self, expr: Scaled, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    yield from _format_factor(
        expr.factor,
        infix_mul=self.infix_mul,
        format_product=self._fmt_product,
    )
    with state._set_parent_precedence(PRECEDENCE[Scaled]):
        yield from self.visit(expr.reference, state)

visit_translated ¤

visit_translated(
    expr: Translated, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
327
328
329
330
def visit_translated(
    self, expr: Translated, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    yield from self.visit_named(expr, state)

visit_log ¤

visit_log(
    expr: Log, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]
Source code in src/isqx/_fmt.py
332
333
334
335
336
337
338
339
340
341
342
343
def visit_log(
    self, expr: Log, state: _BasicFormatterState
) -> Generator[StrFragment, None, None]:
    if expr.base is _E:
        yield "ln"
    else:
        yield "log"
        yield str(expr.base).translate(_BASIC_SUBSCRIPT_MAP)
    yield "("
    with state._set_parent_precedence(PRECEDENCE[Log]):
        yield from self.visit(expr.reference, state)
    yield ")"

DEFAULT_FORMATTER ¤

DEFAULT_FORMATTER = BasicFormatter(verbose=True)

_citations ¤

Citations for isqx.

They can be used for cross-references in the documentation, e.g. see [special publication 811][isqx._citations.SP811]. A tiny griffe extension is used to copy the runtime value into the docstring.

SI ¤

SI = "```bibtex\n@techreport{si,\n    author = {{Bureau International des Poids et Mesures}},\n    title = {The International System of Units ({SI})},\n    edition = {9th},\n    year = {2019},\n    note = {Version 3.01, updated August 2024},\n    institution = {Bureau International des Poids et Mesures (BIPM)},\n    address = {Sèvres, France},\n    url = {https://www.bipm.org/documents/20126/41483022/SI-Brochure-9-EN.pdf}\n}\n```"
@techreport{si,
    author = {{Bureau International des Poids et Mesures}},
    title = {The International System of Units ({SI})},
    edition = {9th},
    year = {2019},
    note = {Version 3.01, updated August 2024},
    institution = {Bureau International des Poids et Mesures (BIPM)},
    address = {Sèvres, France},
    url = {https://www.bipm.org/documents/20126/41483022/SI-Brochure-9-EN.pdf}
}

IUPAP1 ¤

IUPAP1 = "```bibtex\n@techreport{iupap1,\n    author = {{IUPAP Commission C2 on Symbols, Units, Nomenclature, Atomic Masses and Fundamental Constants}},\n    title = {Symbols, Units, Nomenclature and Fundamental Constants in Physics},\n    year = {1987},\n    institution = {International Union of Pure and Applied Physics (IUPAP)},\n    url = {https://archive2.iupap.org/wp-content/uploads/2014/05/A4.pdf}\n}\n```"
@techreport{iupap1,
    author = {{IUPAP Commission C2 on Symbols, Units, Nomenclature, Atomic Masses and Fundamental Constants}},
    title = {Symbols, Units, Nomenclature and Fundamental Constants in Physics},
    year = {1987},
    institution = {International Union of Pure and Applied Physics (IUPAP)},
    url = {https://archive2.iupap.org/wp-content/uploads/2014/05/A4.pdf}
}

SP811 ¤

SP811 = "```bibtex\n@techreport{sp811,\n    title = {Guide for the Use of the International System of Units ({SI})},\n    year = {2008},\n    edition = {2008},\n    institution = {National Institute of Standards and Technology},\n    type = {Special Publication},\n    number = {811},\n    address = {Gaithersburg, MD},\n    url = {https://www.nist.gov/pml/special-publication-811}\n}\n```"
@techreport{sp811,
    title = {Guide for the Use of the International System of Units ({SI})},
    year = {2008},
    edition = {2008},
    institution = {National Institute of Standards and Technology},
    type = {Special Publication},
    number = {811},
    address = {Gaithersburg, MD},
    url = {https://www.nist.gov/pml/special-publication-811}
}

CODATA2022 ¤

CODATA2022 = "```bibtex\n@techreport{codata2022,\n    title = {{CODATA} Recommended Values of the Fundamental Physical Constants: 2022},\n    year = {2024},\n    month = {May},\n    institution = {National Institute of Standards and Technology},\n    type = {Special Publication},\n    number = {961},\n    url = {https://physics.nist.gov/cuu/pdf/sp961.pdf}\n}\n```"
@techreport{codata2022,
    title = {{CODATA} Recommended Values of the Fundamental Physical Constants: 2022},
    year = {2024},
    month = {May},
    institution = {National Institute of Standards and Technology},
    type = {Special Publication},
    number = {961},
    url = {https://physics.nist.gov/cuu/pdf/sp961.pdf}
}

ISO_80000_2 ¤

ISO_80000_2 = "```bibtex\n@standard{iso_80000_2,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 2: Mathematics},\n    year = {2019},\n    number = {ISO 80000-2:2019},\n    url = {https://www.iso.org/standard/76921.html}\n}\n```"
@standard{iso_80000_2,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 2: Mathematics},
    year = {2019},
    number = {ISO 80000-2:2019},
    url = {https://www.iso.org/standard/76921.html}
}

ISO_80000_3 ¤

ISO_80000_3 = "```bibtex\n@standard{iso_80000_3,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 3: Space and time},\n    year = {2019},\n    number = {ISO 80000-3:2019},\n    url = {https://www.iso.org/standard/64974.html}\n}\n```"
@standard{iso_80000_3,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 3: Space and time},
    year = {2019},
    number = {ISO 80000-3:2019},
    url = {https://www.iso.org/standard/64974.html}
}

ISO_80000_4 ¤

ISO_80000_4 = "```bibtex\n@standard{iso_80000_4,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 4: Mechanics},\n    year = {2019},\n    number = {ISO 80000-4:2019},\n    url = {https://www.iso.org/standard/64975.html}\n}\n```"
@standard{iso_80000_4,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 4: Mechanics},
    year = {2019},
    number = {ISO 80000-4:2019},
    url = {https://www.iso.org/standard/64975.html}
}

ISO_80000_5 ¤

ISO_80000_5 = "```bibtex\n@standard{iso_80000_5,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 5: Thermodynamics},\n    year = {2019},\n    number = {ISO 80000-5:2019},\n    url = {https://www.iso.org/standard/64976.html}\n}\n```"
@standard{iso_80000_5,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 5: Thermodynamics},
    year = {2019},
    number = {ISO 80000-5:2019},
    url = {https://www.iso.org/standard/64976.html}
}

IEC_80000_6 ¤

IEC_80000_6 = "```bibtex\n@standard{iec_80000_6,\n    organization = {International Electrotechnical Commission},\n    title = {Quantities and units - Part 6: Electromagnetism},\n    year = {2022},\n    number = {IEC 80000-6:2022},\n    url = {https://www.iso.org/standard/77846.html}\n}\n```"
@standard{iec_80000_6,
    organization = {International Electrotechnical Commission},
    title = {Quantities and units - Part 6: Electromagnetism},
    year = {2022},
    number = {IEC 80000-6:2022},
    url = {https://www.iso.org/standard/77846.html}
}

IEV ¤

IEV = "```bibtex\n@misc{iev,\n    author = {{IEC}},\n    title = {Electropedia - International Electrotechnical Vocabulary},\n    organization = {International Electrotechnical Commission},\n    url = {https://www.electropedia.org/}\n}\n```"
@misc{iev,
    author = {{IEC}},
    title = {Electropedia - International Electrotechnical Vocabulary},
    organization = {International Electrotechnical Commission},
    url = {https://www.electropedia.org/}
}

ISO_80000_7 ¤

ISO_80000_7 = "```bibtex\n@standard{iso_80000_7,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 7: Light and radiation},\n    year = {2019},\n    number = {ISO 80000-7:2019},\n    url = {https://www.iso.org/standard/64977.html}\n}\n```"
@standard{iso_80000_7,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 7: Light and radiation},
    year = {2019},
    number = {ISO 80000-7:2019},
    url = {https://www.iso.org/standard/64977.html}
}

ISO_80000_8 ¤

ISO_80000_8 = "```bibtex\n@standard{iso_80000_8,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 8: Acoustics},\n    year = {2020},\n    number = {ISO 80000-8:2020},\n    url = {https://www.iso.org/standard/64978.html}\n}\n```"
@standard{iso_80000_8,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 8: Acoustics},
    year = {2020},
    number = {ISO 80000-8:2020},
    url = {https://www.iso.org/standard/64978.html}
}

ISO_80000_9 ¤

ISO_80000_9 = "```bibtex\n@standard{iso_80000_9,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 9: Physical chemistry and molecular physics},\n    year = {2019},\n    number = {ISO 80000-9:2019},\n    url = {https://www.iso.org/standard/64979.html}\n}\n```"
@standard{iso_80000_9,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 9: Physical chemistry and molecular physics},
    year = {2019},
    number = {ISO 80000-9:2019},
    url = {https://www.iso.org/standard/64979.html}
}

ISO_80000_10 ¤

ISO_80000_10 = "```bibtex\n@standard{iso_80000_10,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 10: Atomic and nuclear physics},\n    year = {2019},\n    number = {ISO 80000-10:2019},\n    url = {https://www.iso.org/standard/64980.html}\n}\n```"
@standard{iso_80000_10,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 10: Atomic and nuclear physics},
    year = {2019},
    number = {ISO 80000-10:2019},
    url = {https://www.iso.org/standard/64980.html}
}

ISO_80000_11 ¤

ISO_80000_11 = "```bibtex\n@standard{iso_80000_11,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 11: Characteristic numbers},\n    year = {2019},\n    number = {ISO 80000-11:2019},\n    url = {https://www.iso.org/standard/64982.html}\n}\n```"
@standard{iso_80000_11,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 11: Characteristic numbers},
    year = {2019},
    number = {ISO 80000-11:2019},
    url = {https://www.iso.org/standard/64982.html}
}

ISO_80000_12 ¤

ISO_80000_12 = "```bibtex\n@standard{iso_80000_12,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 12: Solid state physics},\n    year = {2019},\n    number = {ISO 80000-12:2019},\n    url = {https://www.iso.org/standard/63480.html}\n}\n```"
@standard{iso_80000_12,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 12: Solid state physics},
    year = {2019},
    number = {ISO 80000-12:2019},
    url = {https://www.iso.org/standard/63480.html}
}

ISO_80000_13 ¤

ISO_80000_13 = "```bibtex\n@standard{iso_80000_13,\n    organization = {International Organization for Standardization},\n    title = {Quantities and units - Part 13: Information science and technology},\n    year = {2025},\n    number = {ISO 80000-13},\n    url = {https://www.iso.org/standard/87648.html}\n}\n```"
@standard{iso_80000_13,
    organization = {International Organization for Standardization},
    title = {Quantities and units - Part 13: Information science and technology},
    year = {2025},
    number = {ISO 80000-13},
    url = {https://www.iso.org/standard/87648.html}
}

MP_UNITS ¤

MP_UNITS = "```bibtex\n@software{mp_units,\n    author = {Pusz, Mateusz and Guerrero Peña, Johel Ernesto and Hogg, Chip and {The mp-units project team}},\n    license = {MIT},\n    month = nov,\n    title = {{mp-units}},\n    url = {https://github.com/mpusz/mp-units},\n    version = {2.4.0},\n    year = {2024}\n}\n```"
@software{mp_units,
    author = {Pusz, Mateusz and Guerrero Peña, Johel Ernesto and Hogg, Chip and {The mp-units project team}},
    license = {MIT},
    month = nov,
    title = {{mp-units}},
    url = {https://github.com/mpusz/mp-units},
    version = {2.4.0},
    year = {2024}
}

H44 ¤

H44 = "```bibtex\n@techreport{h44,\n    author = {{NIST}},\n    title = {NIST Handbook 44 - 2024 - Appendix C. General Tables of Units of Measurement},\n    year = {2024},\n    institution = {National Institute of Standards and Technology},\n    url = {https://www.nist.gov/document/nist-handbook-44-2024-appendix-c-pdf}\n}\n```"
@techreport{h44,
    author = {{NIST}},
    title = {NIST Handbook 44 - 2024 - Appendix C. General Tables of Units of Measurement},
    year = {2024},
    institution = {National Institute of Standards and Technology},
    url = {https://www.nist.gov/document/nist-handbook-44-2024-appendix-c-pdf}
}

WMA1985 ¤

WMA1985 = "```bibtex\n@misc{wma1985,\n    title = {Weights and Measures Act 1985},\n    year = {1985},\n    organization = {{legislation.gov.uk}},\n    url = {https://www.legislation.gov.uk/ukpga/1985/72/contents}\n}\n```"
@misc{wma1985,
    title = {Weights and Measures Act 1985},
    year = {1985},
    organization = {{legislation.gov.uk}},
    url = {https://www.legislation.gov.uk/ukpga/1985/72/contents}
}

ICAO ¤

ICAO = "```bibtex\n@misc{icao,\n    author = {{ICAO}},\n    title = {Annex 5 - Units of Measurement to be Used in the Air and Ground Services},\n    organization = {International Civil Aviation Organization},\n    url = {https://store.icao.int/en/annex-5-units-of-measurement-to-be-used-in-the-air-and-ground-services}\n}\n```"
@misc{icao,
    author = {{ICAO}},
    title = {Annex 5 - Units of Measurement to be Used in the Air and Ground Services},
    organization = {International Civil Aviation Organization},
    url = {https://store.icao.int/en/annex-5-units-of-measurement-to-be-used-in-the-air-and-ground-services}
}