from __future__ import annotations
import numpy as np
from typing import TYPE_CHECKING, TypedDict
if TYPE_CHECKING:
from antupy import Array, Var
BASE_UNITS: dict[str, tuple[float, str,str]] = {
"-": (1e0, "adimensional", "adim"),
"s": (1e0, "second", "time"),
"m": (1e0, "meter", "length"),
"g": (1e0, "gram", "mass"),
"K": (1e0, "kelvin", "temperature"),
"A": (1e0, "ampere", "current"),
"mol": (1e0, "mole", "substance"),
"cd": (1e0, "candela", "luminous_intensity"),
"USD": (1e0, "us_dollar", "money")
}
DERIVED_UNITS: dict[str, tuple[float,str,str,str]] = {
"rad": (1e0, "-", "radian", "plane_angle"),
"sr": (1e0, "-", "steradian", "solid_angle"),
"Hz": (1e0, "1/s", "hertz", "frequency"),
"N": (1e0, "kg-m/s2", "newton", "force"),
"Pa": (1e0, "kg/m-s2", "pascal", "pressure"),
"J": (1e0, "kg-m2/s2", "joule", "energy"),
"W": (1e0, "kg-m2/s3", "watt", "power"),
"C": (1e0, "s-A", "coulomb", "electric_charge"),
"V": (1e0, "kg-m2/s3-A", "volt", "electric_potential"),
"F": (1e0, "s4-A2/kg-m2", "farad", "capacitance"),
"Ω": (1e0, "kg-m2/s3-A2", "ohm", "electrical_resistance"),
"S": (1e0, "s3-A2/kg-m2", "siemens", "electrical_conductance"),
"Wb": (1e0, "kg-m2/s2-A", "weber", "magnetic_flux"),
"T": (1e0, "kg/s2-A", "tesla", "magnetic_flux_density"),
"H": (1e0, "kg-m2/s2-A2", "henry", "inductance"),
"lm": (1e0, "cd-sr", "lumen", "luminous flux"),
"lx": (1e0, "cd-sr/m2", "lux", "illuminance"),
"Bq": (1e0, "1/s", "becquerel", "radioactivity"),
"Gy": (1e0, "m2/s2", "gray", "absorbed_dose"),
"Sv": (1e0, "m2/s2", "sievert", "dose_equivalent"),
"kat": (1e0, "mol/s", "katal", "catalytic_activity"),
}
RELATED_UNITS: dict[str, tuple[float,str,str,str]] = {
"L": (1e-3, "m3", "liter", "volume"),
"l": (1e-3, "m3", "liter", "volume"),
"sec": (1e0, "s", "second", "time"),
"min": (60., "s", "minute", "time"),
"hr": (3600., "s", "hour", "time"),
"day": (86400., "s", "day", "time"),
"wk": (24*3600*7, "s", "year", "time"),
"week": (24*3600*7, "s", "year", "time"),
"mo": (24*3600*30, "s", "year", "time"),
"month": (24*3600*30, "s", "year", "time"),
"yr": (31536000, "s", "year", "time"),
"year": (31536000, "s", "year", "time"),
"au": (149597870700, "m", "astronomic_unit", "length"),
"mi": (1e0/1609.34,"m", "mile", "length"),
"ft": (3.28084,"m", "foot", "length"),
"'": (39.3701,"m", "inch", "length"),
"ton": (1e3,"kg", "tonne", "mass"),
"lb": (2.20462,"kg", "pound", "mass"),
"oz": (35.274,"kg", "ounce", "mass"),
"lm": (1.0, "cd-sr", "lumens", "luminous_flux"),
"Wh": (3600, "J", "watt-hour", "energy"),
"Wp": (1e0, "W", "watt-peak", "power"),
"cal": (4184, "J", "calorie", "energy"),
"ha": (1e4, "m2", "hectar", "surface"),
"°C": (1e0, "K", "celcius", "temperature"),
"degC": (1e0, "K", "celcius", "temperature"),
"bar": (1e5, "Pa", "bar", "pressure"),
"psi": (6894.76, "Pa", "psi", "pressure"),
"atm": (101325, "Pa", "atmosphere", "pressure"),
"mmHg": (133.322, "Pa", "mm_of_mercury", "pressure"),
"ppm": (1000, "mL/L", "parts_per_million", "concentration"),
"deg": (np.pi/180., "rad", "degree", "plane_angle"),
"AUD": (1.4, "USD", "AU_dollar", "money"),
"CLP": (1e-3, "USD", "CL_pesos", "money"),
}
PREFIXES: dict[str, float] = {
"q": 1e-30, # "quecto"
"r": 1e-27, # "ronto"
"y": 1e-24, # "yocto"
"z": 1e-21, # "zepto"
"a": 1e-18, # "atto"
"f": 1e-15, # "femto"
"p": 1e-12, # "pico"
"n": 1e-9, # "nano"
"μ": 1e-6, # "micro"
"m": 1e-3, # "milli"
"c": 1e-2, # "centi"
"d": 1e-1, # "deci"
"": 1.0,
"k": 1e3, # "kilo"
"M": 1e6, # "mega"
"G": 1e9, # "giga"
"T": 1e12, # "tera"
"P": 1e15, # "peta"
"E": 1e18, # "exa"
"Z": 1e21, # "zetta"
"Y": 1e24, # "yotta"
"R": 1e27, # "ronna"
"Q": 1e30, # "quetta"
}
UnitDict = TypedDict(
"UnitDict",
{
"s": int,
"m": int,
"g": int,
"K": int,
"A": int,
"mol": int,
"cd": int,
"USD": int,
"-": int,
},
)
BASE_ADIM: UnitDict = {
"s": 0,
"m": 0,
"g": 0,
"K": 0,
"A": 0,
"mol": 0,
"cd": 0,
"USD": 0,
"-": 0,
}
UnitPool = list[tuple[str, int]]
[docs]
class Unit():
"""
Class containing any unit valid with SI unit system.
To initiate it, pass a "unit label", which correspond to a valid str.
It converts it internally to a base representation, which is a dictionary
with the 7 base SI units as keys and their respective exponents as values.
Parameters
----------
unit : str, optional
Valid string with the unit label, e.g. "kg-m/s2". Default is "-" (dimensionless).
base_factor : float, optional
Multiplicative factor to convert to base SI units. Default is 1.0.
Attributes
----------
label_unit : str
The original unit label string.
base_factor : float
Multiplicative factor to convert to base SI units.
base_units : UnitDict
Dictionary with the 7 base SI units as keys and their respective exponents as values.
Examples
--------
Creating units from labels:
>>> u1 = Unit("kg-m/s2")
>>> print(u1)
[kg-m/s2]
>>> print(u1.si)
1.00e+03[m-g/s2]
Unit equivalence:
>>> u2 = Unit("N")
>>> print(u2)
[N]
>>> print(u2.si)
1.00e+03[m-g/s2]
>>> u1 == u2
True
Notes
-----
The base representation uses seven SI base units: meter (m), gram (g), second (s),
ampere (A), kelvin (K), mole (mol), and candela (cd). Note that gram is used instead
of kilogram to simplify prefix handling.
See Also
--------
Var : Variable class that uses Unit for dimensional consistency
Array : Array class that uses Unit for dimensional consistency
"""
[docs]
def __init__(self, unit: str = "-", base_factor: float = 1e0):
self.base_units: UnitDict = BASE_ADIM.copy()
self.base_factor: float = base_factor
self.label_unit: str = unit
self._translate_to_base()
def __repr__(self) -> str:
return f"[{self.label_unit}]"
def __eq__(self, other) -> bool:
if isinstance(other, Unit):
return (
(self.base_factor==other.base_factor)
and (self.base_units == other.base_units)
)
return False
@property
def si(self) -> str:
"""
Returns the unit in base SI representation.
The base SI representation is a string with the base factor and the base units in integer exponents
"""
top_str = ""
bottom_str = ""
d = [(k,int(v)) for (k,v) in self.base_units.items()] #type: ignore
for (comp,exp) in d:
if exp>0:
expr = f"{comp}{abs(exp)}" if exp>1 else f"{comp}"
if top_str == "":
top_str = expr
else:
top_str = top_str + f"-{expr}"
elif exp<0:
expr = f"{comp}{abs(exp)}" if exp<-1 else f"{comp}"
if bottom_str == "":
bottom_str = expr
else:
bottom_str = bottom_str + f"-{expr}"
else:
continue
if bottom_str == "":
return f"{self.base_factor:.2e}[{top_str}]" if top_str != "" else "-"
else:
return f"{self.base_factor:.2e}[{top_str if top_str != "" else "1"}/{bottom_str}]"
@property
def u(self)->str:
"""
Returns the unit label.
This is just a shorter alias for label_unit."""
return self.label_unit
def compatible(self) -> list[str]:
return [
label for label in (BASE_UNITS | DERIVED_UNITS | RELATED_UNITS)
if self.base_units == Unit(label).base_units
]
def _update_base_repr(self, name: str, exponent: int):
exponent_prev = self.base_units.get(name,0)
self.base_units[name] = exponent+exponent_prev
return
@staticmethod
def _parse_unit_comps(
unit_pool: UnitPool,
comps: list[str],
exp_sign: int
) -> tuple[UnitPool, float]:
UNITS = BASE_UNITS | DERIVED_UNITS | RELATED_UNITS
factor_ = 1.0
for comp in comps:
if comp == "":
name = comp
exponent = exp_sign
elif comp[-1].isdigit():
name = comp[:-1]
exponent = exp_sign * int(comp[-1])
else:
name = comp
exponent = exp_sign
if name in UNITS:
factor = 1.0
elif name == "":
factor = 1.0
elif name[0] in PREFIXES and name[1:] in UNITS:
factor = PREFIXES[name[0]] ** (exponent*exp_sign)
name = name[1:]
else:
raise ValueError(f"Unit '{name}' not recognized.")
unit_pool.append((name, exponent))
factor_ *= factor
return unit_pool, factor_
@classmethod
def _split_unit(cls, unit: str) -> tuple[float, UnitPool]:
"""
Split a unit label into its components, their factors and exponents.
For example, "kg-m/s2" becomes [("kg", 1), ("m", 1), ("s", -2)].
"""
unit_pool: UnitPool = []
if unit in ["-", "", "adim"]:
return 1.0, [("-", 0)]
if "/" in unit:
top, bottom = unit.split("/", 1)
top_units = top.split("-") if "-" in top else [top,]
bottom_units = bottom.split("-") if "-" in bottom else [bottom,]
else:
top_units = unit.split("-") if "-" in unit else [unit,]
bottom_units = []
unit_pool, factor_top = cls._parse_unit_comps(unit_pool, top_units, 1)
unit_pool, factor_bot = cls._parse_unit_comps(unit_pool, bottom_units, -1)
return (factor_top/factor_bot, unit_pool)
def _translate_to_base(self) -> None:
factor_, unit_pool_ = self._split_unit(self.label_unit)
factor_ = self.base_factor * factor_
while len(unit_pool_)>0:
(name, exponent) = unit_pool_.pop(0)
if name in BASE_UNITS:
self._update_base_repr(name, exponent)
if name in DERIVED_UNITS|RELATED_UNITS:
new_label = (DERIVED_UNITS|RELATED_UNITS)[name][1]
new_factor1 = (DERIVED_UNITS|RELATED_UNITS)[name][0]
new_factor2, new_pool = self._split_unit(new_label)
for comp in new_pool:
unit_pool_.append((comp[0], exponent*comp[1]))
factor_ *= (new_factor2*new_factor1)**np.sign(exponent)
self.base_factor = factor_
return None
def _conv_temp(temp: Var|Array, unit: str|None) -> float|np.ndarray:
if temp.value is None or unit is None:
raise ValueError("Value or unit is None")
if temp.unit.u == "K" and unit in ["°C", "degC"]:
return temp.value - 273.15
elif temp.unit.u in ["°C", "degC"] and unit == "K":
return temp.value + 273.15
elif (temp.unit.u in ["°C", "degC"] and unit in ["°C", "degC"]):
return temp.value
elif temp.unit.u == unit:
return temp.value
else:
raise ValueError(f"either {temp.unit.u} and/or {unit} is/are incompatible.")
def _assign_unit(unit: str|Unit|None = None) -> Unit:
if isinstance(unit, str):
return Unit(unit)
elif isinstance(unit, Unit):
return unit
else:
raise TypeError(f"{type(unit)} is not a valid type for unit.")
def _mul_units(unit1: str|None, unit2: str|None) -> str:
""" Function to merge two units into a single unit by multiplication.
"""
adim_units = ["", "-", "adim"]
if unit1 is None:
return unit2 if unit2 is not None else ""
if unit2 is None:
return unit1
if unit1 in adim_units and unit2 not in adim_units:
return unit2
if unit2 in adim_units and unit1 not in adim_units:
return unit1
if unit1 in adim_units and unit2 in adim_units:
return "-"
top = []
bottom = []
if "/" in unit1:
top1, bottom1 = unit1.split("/")
top = top + top1.split("-")
bottom = bottom + bottom1.split("-")
else:
top = top + unit1.split("-")
if "/" in unit2:
top2, bottom2 = unit2.split("/")
top = top + top2.split("-")
bottom = bottom + bottom2.split("-")
else:
top = top + unit2.split("-")
for unit in top:
if unit in bottom:
top.remove(unit)
bottom.remove(unit)
if len(bottom) > 0:
if len(top) == 0:
return f"1/{'-'.join(bottom)}"
return f"{'-'.join(top)}/{'-'.join(bottom)}"
else:
return f"{'-'.join(top)}"
def _div_units(unit1: str|None, unit2: str|None) -> str:
""" Function to merge two units into a single unit by division
"""
admin_units = ["", "-", "adim"]
if unit1 is None:
return unit2 if unit2 is not None else ""
if unit2 is None:
return unit1
if unit2 in admin_units and unit1 not in admin_units:
return unit1
if unit1 in admin_units and unit2 in admin_units:
return "-"
top = []
bottom = []
if "/" in unit1:
top1, bottom1 = unit1.split("/")
top = top + top1.split("-")
bottom = bottom + bottom1.split("-")
else:
top = top + unit1.split("-")
if "/" in unit2:
top2, bottom2 = unit2.split("/")
top = top + bottom2.split("-")
bottom = bottom + top2.split("-")
else:
bottom = bottom + unit2.split("-")
for unit in top:
if unit in bottom:
top.remove(unit)
bottom.remove(unit)
if len(bottom) > 0:
if len(top) == 0:
return f"1/{'-'.join(bottom)}"
return f"{'-'.join(top)}/{'-'.join(bottom)}"
else:
return f"{'-'.join(top)}"
CONSTANTS: dict[str, tuple[float, str]] = {
"delta_v_c": (9192631770, "Hz"), # Hyperfine transition frequency of 133Cs
"c": (299792458, "m/s"), # Speed of light
"h": (6.62607015e-34, "J*s"), # Planck's constant
"e": (1.602176634e-19, "C"), # Elementary charge
"k": (1.380649e-23, "J/K"), # Boltzmann constant
"sigma": (5.670374419e-8, "W/m2-K4"), # Stefan-Boltzmann constant
"R": (8.314462618, "J/mol-K"), # Gas constant
"N_A": (6.02214076e23, "1/mol"), # Avogadro constant
"K_cd": (683, "lm/W"), # Luminous efficacy of 540 THz radiation
}
USEFUL_QUANTITIES = {
"density": {
"kg/m3": 1e0,
"g/cm3": 1e-3,
},
"specific_heat": {
"J/kgK": 1e0, "J/kg-K": 1e0,
"kJ/kgK": 1e-3, "kJ/kg-K": 1e-3,
},
"thermal_conductivity": {
"W/mK": 1e0, "W/m-K": 1e0,
"kW/mK": 1e-3, "kW/m-K": 1e-3,
"J/s-m-K": 1e0, "J/s-mK": 1e0,
},
"viscosity": {
"Pa-s": 1e0,
"mPa-s": 1e3,
"kg/m-s": 1e0
}
}
def main():
return
if __name__=="__main__":
main()
pass