Source code for jitx.units

"""
JITX uses the `Pint <https://pint.readthedocs.io/en/stable/>`_ library for
units where applicable. Currently the support is limited, and primarily used for
component values, but will be expanded over time.

The unit registry and shorthand units are available here.
"""

import math

import pint
from pint.facets.plain import PlainQuantity as PlainQuantity
from collections.abc import Callable
from typing import overload


registry = pint.UnitRegistry()
Quantity = registry.Quantity

mm = registry.mm
um = registry.um
nm = registry.nm
pm = registry.pm

# mil = registry.mil
# mils = registry.mils

percent = pct = registry.percent
ppm = registry.ppm

kV = registry.kV
V = registry.V
mV = registry.mV
uV = registry.uV

A = registry.A
mA = registry.mA
uA = registry.uA

kW = registry.kW
W = registry.W
mW = registry.mW
uW = registry.uW

mohm = registry.mohm
ohm = registry.ohm
kohm = registry.kohm
Mohm = registry.Mohm

F = registry.F
mF = registry.mF
uF = registry.uF
nF = registry.nF
pF = registry.pF

H = registry.H
mH = registry.mH
uH = registry.uH
nH = registry.nH

s = registry.s
ms = registry.ms
us = registry.us
ns = registry.ns
ps = registry.ps

Hz = registry.Hz
kHz = registry.kHz
MHz = registry.MHz
GHz = registry.GHz

dB = registry.dB


# RKM value codes (IEC 60062). The names are prefixed so they are not mistaken
# for unit shorthands; `F`, `H`, `V` and friends above are units, these are
# letters in a marking code.

# base-unit letter per dimension; the first dimensional match wins
_RKM_BASE_LETTER = {
    "ohm": "R",
    "farad": "F",
    "henry": "H",
    "volt": "V",
    "ampere": "A",
    "watt": "W",
    "second": "s",
    "hertz": "Hz",
}

# ASCII SI prefix symbols; `u` is the standard's sanctioned stand-in for micro
_RKM_SI = {
    12: "T",
    9: "G",
    6: "M",
    3: "k",
    0: "",
    -3: "m",
    -6: "u",
    -9: "n",
    -12: "p",
    -15: "f",
}

# IEC keeps resistance codes uppercase so milli cannot be misread as mega,
# which costs it `m` and is why the standard spells milliohm `L`
_RKM_IEC_OHM = {
    12: "T",
    9: "G",
    6: "M",
    3: "K",
    0: "",
    -3: "L",
    -6: "U",
    -9: "N",
    -12: "P",
    -15: "F",
}


def _rkm_canonical(value: PlainQuantity) -> str | None:
    for name in _RKM_BASE_LETTER:
        if registry.Unit(name).dimensionality == value.dimensionality:
            return name
    return None


def _rkm_round(value: float, digits: int) -> float:
    return round(value, (digits - 1) - int(math.floor(math.log10(abs(value)))))


@overload
def rkm_code(value: PlainQuantity, /, *, digits: int = 3, iec: bool = False) -> str: ...
@overload
def rkm_code(
    *, digits: int = 3, iec: bool = False
) -> Callable[[PlainQuantity], str]: ...


[docs] def rkm_code( value: PlainQuantity | None = None, /, *, digits: int = 3, iec: bool = False ): """Render a quantity as an RKM code (IEC 60062): `4k7`, `2n2`, `R47`, `3V3`. The prefix letter takes the place of the decimal point, so the value survives a marking or a scan that would lose the point. Note that the unit is implied by context whenever a prefix letter is used: `1m` is a milliohm, a milliamp or a millihenry depending on the part. `digits` is significant digits; `iec` switches resistance to the standard's uppercase spelling, `4K7` and `4L7`. A dimension with no letter of its own falls back to the default value-string rendering. If given no value, only options, a function is returned accepting a quantity, which can be used with :py:func:`valuestring` for example. """ if value is None: def rkm_coded(value: PlainQuantity, /) -> str: return rkm_code(value, digits=digits, iec=iec) return rkm_coded canonical = _rkm_canonical(value) if canonical is None: return f"{value:g~P}" base = _RKM_BASE_LETTER[canonical] prefixes = _RKM_IEC_OHM if (iec and canonical == "ohm") else _RKM_SI magnitude = float(value.to(canonical).magnitude) sign = "-" if magnitude < 0 else "" magnitude = abs(magnitude) if magnitude == 0.0: return f"{sign}0{base}" if not math.isfinite(magnitude): return f"{value:g~P}" # round before picking the decade, so 999.6 ohm lands on 1k not 1000R exponent = 3 * math.floor(math.log10(_rkm_round(magnitude, digits)) / 3) if canonical == "ohm" and -2 <= math.log10(magnitude) < 0: exponent = 0 # a sub-ohm resistor is marked R47, not 470m exponent = max(min(exponent, max(prefixes)), min(prefixes)) mantissa = _rkm_round(magnitude / 10.0**exponent, digits) letter = prefixes[exponent] or base whole, _, fraction = f"{mantissa:f}".rstrip("0").rstrip(".").partition(".") if not fraction: return f"{sign}{whole}{letter}" if len(letter) > 1: # "Hz" will not sit infix return f"{sign}{whole}.{fraction} {letter}" if whole == "0": return f"{sign}{letter}{fraction}" return f"{sign}{whole}{letter}{fraction}"
[docs] def valuestring( value: str | PlainQuantity, quantity: Callable[[PlainQuantity], str] | None = None ): """Common best-practice component-value-to-string conversion. Args: value: the to component value to convert to a string quantity: optional rendering mechanism, e.g. :py:func:`rkm_code` """ if isinstance(value, PlainQuantity): if quantity: return quantity(value) return f"{value:g~P}" return str(value)