"""
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)