"""Unit conversion helpers used by parser and devices.
This module is the canonical conversion backend in mpylab. The public API
supports both directions:
- unit/value -> Quantity (`to_quantity`)
- Quantity -> target unit/value (`from_quantity`)
The mapping is case-insensitive for unit names.
"""
from __future__ import annotations
import math
from numpy import array, log10, ndarray, power
from scuq import si, units
from scuq.quantities import Quantity
from mpylab.tools.aunits import AMPLITUDERATIO, EFIELD, HFIELD, POWERRATIO, POYNTING
def _ident(v):
"""Identity mapping."""
return v
def _from_dBfac(fac):
"""Convert dB values to linear values with factor `fac`."""
def lin(db):
return 10 ** (db / fac)
return lin
def _to_dBfac(fac):
"""Convert linear values to dB values with factor `fac`."""
def db(lin):
return fac * log10(lin)
return db
def _addsum(method, summand):
"""Compose `method` and add a constant afterwards."""
def new_m(v):
return summand + method(v)
return new_m
def _mulfac(method, fac):
"""Compose `method` and multiply by a constant afterwards."""
def new_m(v):
return fac * method(v)
return new_m
def lin2dB(dBfac=None, sifac=None):
"""Create a converter from linear input to dB output.
Example:
`W2dBm = lin2dB(10, 1e3)`
"""
if dBfac is None:
dBfac = 10
if sifac is None:
sifac = 1.0
def m(inp):
if type(inp) in (int, float):
inp = [inp]
if not isinstance(inp, ndarray):
inp = array(inp, dtype=float)
ans = dBfac * log10(inp * sifac)
if ans.size == 1:
return ans[0]
return ans
return m
def dB2lin(dBfac=None, sifac=None):
"""Create a converter from dB input to linear output.
Example:
`dBm2W = dB2lin(10, 1e-3)`
"""
if dBfac is None:
dBfac = 10
if sifac is None:
sifac = 1.0
def m(inp):
if type(inp) in (int, float):
inp = [inp]
if not isinstance(inp, ndarray):
inp = array(inp, dtype=float)
ans = power(10.0, inp / float(dBfac)) * sifac
if ans.size == 1:
return ans[0]
return ans
return m
class UConv:
"""Central registry for unit conversion methods.
`uconv` maps input unit strings to tuples `(target_scuq_unit, converter)`.
`uconv_from_quantity` maps scuq unit-string representation to outbound
converters keyed by destination unit string.
"""
uconv = {}
uconv_from_quantity = {}
@classmethod
def normalize_unit(cls, unit):
"""Normalize a unit name to its dictionary key representation."""
if not isinstance(unit, str):
raise TypeError("unit must be a string")
return unit.strip().lower()
@classmethod
def get(cls, unit):
"""Return `(scuq_unit, converter)` for input unit string."""
key = cls.normalize_unit(unit)
try:
return cls.uconv[key]
except KeyError as exc:
raise ValueError(f"Unknown unit: {unit}") from exc
@classmethod
def convert(cls, unit, value):
"""Convert a numeric value from `unit` to canonical scuq base."""
dim, fn = cls.get(unit)
return dim, fn(value)
@classmethod
def to_quantity(cls, fromunit, value):
"""Create a scuq `Quantity` from unit string and value."""
dim, converted = cls.convert(fromunit, value)
return Quantity(dim, converted)
@classmethod
def _quantity_unit_key(cls, obj):
"""Return the normalized unit key used by reverse mapping."""
try:
unit = obj._unit
except AttributeError as exc:
raise TypeError("obj must be a scuq Quantity-like object with '_unit'") from exc
return str(unit)
@classmethod
def from_quantity(cls, tounit, obj):
"""Convert a scuq `Quantity` to a float in destination unit `tounit`."""
tounit_norm = cls.normalize_unit(tounit)
unit_key = cls._quantity_unit_key(obj)
try:
unit_map = cls.uconv_from_quantity[unit_key]
except KeyError as exc:
raise ValueError(f"No outbound conversion mapping for quantity unit: {unit_key}") from exc
try:
method = unit_map[tounit_norm]
except KeyError as exc:
raise ValueError(f"Cannot convert quantity unit '{unit_key}' to '{tounit}'") from exc
value = obj.get_expectation_value_as_float()
return method(value)
@classmethod
def unit_exists(cls, unit):
"""Return `True` if the input unit is known."""
key = cls.normalize_unit(unit)
return key in cls.uconv
@classmethod
def available_units(cls):
"""Return all supported input units."""
return tuple(sorted(cls.uconv.keys()))
@classmethod
def available_output_units(cls, obj):
"""Return outbound unit strings available for a Quantity object."""
unit_key = cls._quantity_unit_key(obj)
return tuple(sorted(cls.uconv_from_quantity.get(unit_key, {}).keys()))
UConv.uconv = {
"1": (units.ONE, _ident),
"dimensionless": (units.ONE, _ident),
"dbm": (si.WATT, _mulfac(_from_dBfac(10), 1e-3)),
"w": (si.WATT, _ident),
"dbuv": (si.VOLT, _mulfac(_from_dBfac(20), 1e-6)),
"v": (si.VOLT, _ident),
"db": (POWERRATIO, _from_dBfac(10)),
"hz": (si.HERTZ, _ident),
"khz": (si.HERTZ, _mulfac(_ident, 1e3)),
"mhz": (si.HERTZ, _mulfac(_ident, 1e6)),
"ghz": (si.HERTZ, _mulfac(_ident, 1e9)),
"v/m": (EFIELD, _ident),
"dbv/m": (EFIELD, _from_dBfac(20)),
"m": (si.METER, _ident),
"cm": (si.METER, _mulfac(_ident, 1e-2)),
"mm": (si.METER, _mulfac(_ident, 1e-3)),
"deg": (si.RADIAN, _mulfac(_ident, math.pi / 180.0)),
"rad": (si.RADIAN, _ident),
"steps": (units.ONE, _ident),
"db1/m": (EFIELD / si.VOLT, _from_dBfac(20)),
"dbi": (POWERRATIO, _from_dBfac(10)),
"dbd": (POWERRATIO, _mulfac(_from_dBfac(10), 1.64)),
"1/m": (EFIELD / si.VOLT, _ident),
"a/m": (HFIELD, _ident),
"dba/m": (HFIELD, _from_dBfac(20)),
"w/m2": (POYNTING, _ident),
"dbw/m2": (POYNTING, _from_dBfac(20)),
"s/m": (HFIELD / si.VOLT, _ident),
"dbs/m": (HFIELD / si.VOLT, _from_dBfac(20)),
"amplituderatio": (AMPLITUDERATIO, _ident),
"powerratio": (POWERRATIO, _ident),
"h": (si.HENRY, _ident),
"f": (si.FARAD, _ident),
}
UConv.uconv_from_quantity = {
"1": {"1": _ident, "dimensionless": _ident, "steps": _ident},
"W": {"dbm": _addsum(_to_dBfac(10), 30), "w": _ident},
"V": {"dbuv": _addsum(_to_dBfac(20), 120), "v": _ident},
"(W/W)": {
"db": _to_dBfac(10),
"dbi": _to_dBfac(10),
"dbd": _addsum(_to_dBfac(10), -2.15),
"powerratio": _ident,
"powerration": _ident,
},
"Hz": {"hz": _ident, "khz": _mulfac(_ident, 1e-3), "mhz": _mulfac(_ident, 1e-6), "ghz": _mulfac(_ident, 1e-9)},
"V/m": {"v/m": _ident, "dbv/m": _to_dBfac(20)},
"m": {"m": _ident, "cm": _mulfac(_ident, 1e2), "mm": _mulfac(_ident, 1e3)},
"rad": {"rad": _ident, "deg": _mulfac(_ident, 180.0 / math.pi)},
"m^(-1)": {"db1/m": _to_dBfac(20), "1/m": _ident},
"(V/V)": {"amplituderatio": _ident},
"H": {"h": _ident},
"F": {"f": _ident},
"A*m^(-1)": {"a/m": _ident, "dba/m": _to_dBfac(20)},
"W*m^(-2)": {"w/m2": _ident, "dbw/m2": _to_dBfac(20)},
"A*m^(-1)*V^(-1)": {"s/m": _ident, "a/m": _ident, "dbs/m": _to_dBfac(20)},
}
def to_quantity(fromunit, value):
"""Backward-compatible convenience wrapper around `UConv.to_quantity`."""
return UConv.to_quantity(fromunit, value)
[docs]
def from_quantity(tounit, obj):
"""Backward-compatible convenience wrapper around `UConv.from_quantity`."""
return UConv.from_quantity(tounit, obj)
W2dBm = lin2dB(10, 1e3)
dBm2W = dB2lin(10, 1e-3)
mW2dBm = lin2dB(10, 1)
dBm2mW = dB2lin(10, 1)
V2dBuV = lin2dB(20, 1e6)
dBuV2V = dB2lin(20, 1e-6)
uV2dBuV = lin2dB(20, 1)
dBuV2uV = dB2lin(20, 1)
if __name__ == "__main__":
import math as _math
def _assert_close(name, actual, expected, tol=1e-12):
if abs(actual - expected) > tol:
raise AssertionError(f"{name}: expected {expected}, got {actual}")
print("Running uconv self-test...")
# 1) Forward conversion and case-insensitivity.
dim, w0 = UConv.convert("dBm", 0.0)
_assert_close("0 dBm -> W", w0, 1e-3)
print(f"OK: 0 dBm -> {w0} [{dim}]")
# 2) Quantity creation and reverse conversion.
q_f = UConv.to_quantity("MHz", 123.456)
hz_val = UConv.from_quantity("Hz", q_f)
mhz_val = UConv.from_quantity("mhz", q_f)
_assert_close("MHz roundtrip", mhz_val, 123.456, tol=1e-9)
_assert_close("MHz to Hz", hz_val, 123.456e6, tol=1e-3)
print("OK: Quantity roundtrip for frequency units")
# 3) Angle conversion.
q_a = UConv.to_quantity("deg", 180.0)
rad_val = UConv.from_quantity("rad", q_a)
_assert_close("180 deg -> rad", rad_val, _math.pi)
print("OK: Angle conversions")
# 4) Vector/scalar helper compatibility.
_assert_close("W2dBm(1e-3)", W2dBm(1e-3), 0.0)
_assert_close("dBm2W(0)", dBm2W(0.0), 1e-3)
vec = dBm2W([0, 10, 20])
if tuple(vec.shape) != (3,):
raise AssertionError(f"Unexpected vector shape: {vec.shape}")
print("OK: lin2dB/dB2lin scalar and vector paths")
# 5) Error-path checks.
try:
UConv.get("this_unit_does_not_exist")
raise AssertionError("Expected ValueError for unknown unit")
except ValueError:
pass
try:
UConv.from_quantity("dbm", 1.234)
raise AssertionError("Expected TypeError for invalid quantity object")
except TypeError:
pass
print("OK: Error-path checks")
print("uconv self-test passed.")