Source code for antupy.core.var

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Self

from enum import Enum

import math
import re

import numpy as np
from antupy.core.units import Unit, _assign_unit, _conv_temp, _mul_units, _div_units


[docs] def CF(unit1: str|Unit, unit2: str|Unit) -> Var: """ Conversion factor between two units. It returns a Var instance with the conversion factor and the unit label. If the units are not compatible, it raises a ValueError. This function computes the multiplicative factor needed to convert a value from unit1 to unit2. The result is returned as a Var object containing the conversion factor and a compound unit representing the ratio. Parameters ---------- unit1 : str or Unit The source unit (unit to convert from). Can be a unit string (e.g., "m", "kg/s") or a Unit object. unit2 : str or Unit The target unit (unit to convert to). Can be a unit string (e.g., "km", "g/s") or a Unit object. Returns ------- Var A Var object where: - value: the numerical conversion factor - unit: compound unit representing unit2/unit1 Raises ------ ValueError If the units are not dimensionally compatible (e.g., trying to convert between length and mass units). TypeError If unit1 or unit2 are not valid unit types. Examples -------- Basic unit conversions: >>> from antupy import CF >>> cf = CF("m", "km") >>> print(cf.v) # Get the numerical value 0.001 >>> print(cf) # Show the full conversion factor 0.001 [km/m] Energy unit conversion: >>> cf_energy = CF("J", "kWh") >>> print(cf_energy) 2.7777777777777776e-07 [kWh/J] Flow rate conversion: >>> cf_flow = CF("m3/s", "L/min") >>> print(cf_flow) 60000.0 [L-min/m3-s] Usage in unit conversion: >>> # Convert 5 meters to kilometers >>> distance_m = 5000 # meters >>> distance_km = distance_m * CF("m", "km").v >>> print(f"{distance_m} m = {distance_km} km") 5000 m = 5.0 km Notes ----- The conversion factor represents how many units of unit2 equal one unit of unit1. For example, CF("m", "km") returns 0.001 because 1 meter = 0.001 kilometers. The function only works with dimensionally compatible units. For example, you cannot convert between "kg" (mass) and "m" (length). See Also -------- Var.gv : Get value in different units antupy.units.Unit : The underlying unit representation """ if isinstance(unit1, Unit): u1 = unit1 else: u1 = Unit(unit1) if isinstance(unit2, Unit): u2 = unit2 else: u2 = Unit(unit2) if u1.base_units == u2.base_units: return Var( u1.base_factor / u2.base_factor, _div_units(u2.label_unit, u1.label_unit) ) else: raise ValueError(f"{unit1} and {unit2} are not compatible.")
[docs] @dataclass(frozen=True) class Var(): """ Class to represent parameters and variables in the system. It is used to store the values with their units. If you have a Var instance, you can obtain the value in different units with the gv([str]) method. In this way you make sure you are getting the value with the expected unit. "gv" internally converts unit if it is possible. Parameters ---------- value : float or None, optional The numerical value of the variable. If None, represents an undefined or uninitialized variable. Default is None. _unit : str, Unit, or None, optional The unit of the variable. Can be a unit string (e.g., "kg", "m/s2"), a Unit object, or None for dimensionless quantities. Default is None. unit : Unit The Unit object representing the variable's unit. Examples -------- Creating variables with units: >>> from antupy import Var >>> mass = Var(5.0, "kg") >>> velocity = Var(10, "m/s") Arithmetic operations with automatic unit handling: >>> v1 = Var(5.0, "kg") >>> v2 = Var(500, "g") >>> total_mass = v1 + v2 # Automatically converts g to kg >>> print(total_mass) 5.5 [kg] Unit conversions: >>> energy = Var(1000, "J") >>> energy_in_kj = energy.gv("kJ") >>> print(energy_in_kj) 1.0 Physical calculations: >>> force = mass * Var(9.81, "m/s2") # F = ma >>> print(force) 49.05 [kg-m/s2] Notes ----- - The class is immutable (frozen dataclass) to ensure variable integrity - Operations between incompatible units raise TypeError - Supports comparison operators with automatic unit conversion - Can be converted to float/int for numerical operations See Also -------- Array : For handling arrays of values with the same unit antupy.units.Unit : The underlying unit representation class """ _value: float|None|Var = None _unit: str|Unit|None = None value: float|None = field(init=False) unit: Unit = field(init=False) def __post_init__(self): if isinstance(self._value, Var) and self._unit is None: object.__setattr__(self, "value", self._value.v) object.__setattr__(self, "unit", self._value.u) elif isinstance(self._value, Var) and self._unit is not None: unit_ = _assign_unit(self._unit) object.__setattr__(self, "value", self._value.gv(unit_.label_unit)) object.__setattr__(self, "unit", unit_) else: object.__setattr__(self, "value", self._value) object.__setattr__(self, "unit", _assign_unit(self._unit)) def __add__(self, other: Self): """ Overloading the addition operator. """ if not isinstance(other, Var): return NotImplemented if self.value is None or other.value is None: return Var(None, self.unit) if self.unit == other.unit: return Var(self.value + other.value, self.unit) elif self.unit.base_units == other.unit.base_units: return Var(self.value + other.gv(self.unit.label_unit), self.unit) else: raise TypeError(f"Cannot add {self.unit} with {other.unit}. Units are not compatible.") def __sub__(self, other: Self): """ Overloading the subtraction operator. """ if not isinstance(other, Var): return NotImplemented if self.value is None or other.value is None: return Var(None, self.unit) if self.unit == other.unit: return Var(self.value - other.value, self.unit) elif self.unit.base_units == other.unit.base_units: return Var(self.value - other.gv(self.unit.u), self.unit) else: raise TypeError(f"Cannot subtract {self.unit} with {other.unit}. Units are not compatible.") def __radd__(self, other: Self): """ Overloading the addition operator. """ if not isinstance(other, Var): return NotImplemented if self.value is None or other.value is None: return Var(None, self.unit) if self.unit == other.unit: return Var(self.value + other.value, other.unit) elif self.unit.base_units == other.unit.base_units: return Var(other.value + self.gv(other.unit.u), other.unit) else: raise TypeError(f"Cannot add {self.unit} with {other.unit}. Units are not compatible.") def __mul__(self, other: Self|float|int): """ Overloading the multiplication operator. """ if isinstance(other, Var): if self.value is None or other.value is None: return Var(None, _mul_units(self.unit.u, other.unit.u)) return Var(self.value * other.value, _mul_units(self.unit.u, other.unit.u)) elif isinstance(other, (int, float)): if self.value is None: return Var(None, self.unit) return Var(self.value * other, self.unit) else: return NotImplemented def __rmul__(self, other: Self|float|int): """ Overloading the multiplication operator. """ if isinstance(other, Var): if self.value is None or other.value is None: return Var(None, _mul_units(other.unit.u, self.unit.u)) return Var(self.value * other.value, _mul_units(other.unit.u, self.unit.u)) elif isinstance(other, (int, float)): if self.value is None: return Var(None, self.unit) return Var(self.value * other, self.unit) else: return NotImplemented def __truediv__(self, other: Self|float|int): """ Overloading the division operator. """ if isinstance(other, Var): if self.value is None or other.value is None: return Var(None, _div_units(self.unit.u, other.unit.u)) return Var(self.value / other.value, _div_units(self.unit.u, other.unit.u)) elif isinstance(other, (int, float)): if self.value is None: return Var(None, self.unit) return Var(self.value / other, self.unit) else: return NotImplemented def __int__(self) -> int: return int(self.v) def __float__(self) -> float: return float(self.v) def __eq__(self, other) -> bool: """ Overloading the equality operator. """ if not isinstance(other, Var): return NotImplemented if other.value is None: return False return ( self.value == other.value * CF(other.unit.u, self.unit.u).v and self.unit.base_units == other.unit.base_units ) def __lt__(self, other) -> bool: if isinstance(other, Var): return self.v < other.gv(self.unit.u) elif isinstance(other, (int,float)): return self.v < other else: return NotImplemented def __le__(self, other) -> bool: if isinstance(other, Var): return self.v <= other.gv(self.unit.u) elif isinstance(other, (int,float)): return self.v <= other else: return NotImplemented def __gt__(self, other) -> bool: if isinstance(other, Var): return self.v > other.gv(self.unit.u) elif isinstance(other, (int,float)): return self.v > other else: return NotImplemented def __ge__(self, other) -> bool: if isinstance(other, Var): return self.v >= other.gv(self.unit.u) elif isinstance(other, (int,float)): return self.v >= other else: return NotImplemented def __neg__(self) -> Var: return Var(-self.value if self.value is not None else None, self.unit) def __pos__(self) -> Var: return Var(+self.value if self.value is not None else None, self.unit) def __abs__(self) -> Var: return Var(abs(self.value) if self.value is not None else None, self.unit) def __round__(self, ndigits=0) -> Var: return Var(round(self.value, ndigits) if self.value is not None else None, self.unit) def __trunc__(self) -> Var: return Var(math.trunc(self.value) if self.value is not None else None, self.unit) def __floor__(self) -> Var: return Var(math.floor(self.value) if self.value is not None else None, self.unit) def __ceil__(self) -> Var: return Var(math.ceil(self.value) if self.value is not None else None, self.unit) def __repr__(self) -> str: return f"{self.value:} [{self.unit.u}]" def __format__(self, format_spec: str) -> str: if self.value is None: base_str = f"None [{self.unit.u}]" if format_spec: width_match = re.match(r'([<>=^]?)(\d+)', format_spec) if width_match: align, width = width_match.groups() return format(base_str, f"{align}{width}") return base_str # Match format patterns like: [fill][align][width][.precision][type] match = re.match(r'([<>=^]?)(\d*)(?:\.(\d+))?([a-zA-Z%]?)', format_spec) if match: align, width, precision, type_spec = match.groups() # Build format for the numeric value value_format = "" if precision: value_format += f".{precision}" if type_spec: value_format += type_spec # Format the value if value_format: formatted_value = format(self.value, value_format) else: formatted_value = str(self.value) # Create the full string base_str = f"{formatted_value} [{self.unit.u}]" # Apply width/alignment to the full string if width: return format(base_str, f"{align}{width}") else: return base_str else: # Fallback for unrecognized format specs return f"{self.value} [{self.unit.u}]" def get_value(self, unit: str | None = None) -> float: """ Method to obtain the value of the variable in the requested unit. If the unit is not compatible with the variable unit, an error is raised. If the unit is None, the value is returned in the Var's label unit. """ if unit is None: unit = self.unit.u if self.value is None: raise ValueError("Var value is None.") if self.unit == unit: return self.value if self.unit.base_units == Unit(unit).base_units: if unit in ["°C", "degC","K"]: return float(_conv_temp(self, unit)) return self.value * CF(self.unit.u, unit).v else: raise ValueError( f"Var unit ({self.unit}) and wanted unit ({unit}) are not compatible.") def set_unit(self, unit: str | None = None) -> Var: """ Set the primary unit of the variable. """ unit = str(unit) if (self.unit.base_units == Unit(unit).base_units) and (self.value is not None): return Var(self.value * CF(self.unit, unit).v, Unit(unit)) else: raise ValueError( f"unit ({unit}) is not compatible with existing unit label ({self.unit})." ) @property def u(self) -> str: """ Property to obtain the label unit of the variable""" return self.unit.label_unit @property def v(self) -> float: """ Property to obtain the value of the variable in its label unit. """ return self.value if self.value is not None else np.nan def gv(self, unit: str|None = None) -> float: """Alias for self.get_value()""" return self.get_value(unit) def su(self, unit: str|None = None) -> Var: """Alias of self.set_unit""" return self.set_unit(unit) def compatible(self) -> list[str]: """ Return a list of compatible units for the variable unit. """ return self.unit.compatible()
class C(): c = Var(299792458, "m/s") # Speed of light G = Var(6.6743015e-11, "m3/kg-s2") # Gravitational constant delta_v_c = Var(9192631770, "Hz") # Hyperfine transition frequency of 133Cs h = Var(6.62607015e-34, "J-s") # Planck's constant eps_0 = Var(8.8541878188e-12, "F/m") # Vacuum permittivity mu_0 = Var(1.25663706127e-6, "N/A2") # Vacuum permeability e = Var(1.602176634e-19, "C") # Elementary charge m_e = Var(9.1093837139e-31, "kg") # Electron mass k = Var(1.380649e-23, "J/K") # Boltzmann constant sigma = Var(5.670374419e-8, "W/m2-K4") # Stefan-Boltzmann constant R = Var(8.314462618, "kJ/kmol-K") # Gas constant N_A = Var(6.02214076e23, "1/mol") # Avogadro constant K_cd = Var(683, "lm/W") # Luminous efficacy of 540 THz radiation pi = Var(math.pi, "-") euler = Var(math.e, "-") phi = Var((1 + math.sqrt(5)) / 2, "-") CONSTANTS: dict[str, Var] = { "c": Var(299792458, "m/s"), # Speed of light "G": Var(6.6743015e-11, "m3/kg-s2"), # Gravitational constant "delta_v_c": Var(9192631770, "Hz"), # Hyperfine transition frequency of 133Cs "h": Var(6.62607015e-34, "J-s"), # Planck's constant "eps_0": Var(8.8541878188e-12, "F/m"), # Vacuum permittivity "mu_0": Var(1.25663706127e-6, "N/A2"), # Vacuum permeability "e": Var(1.602176634e-19, "C"), # Elementary charge "m_e": Var(9.1093837139e-31, "kg"), # Electron mass "k": Var(1.380649e-23, "J/K"), # Boltzmann constant "sigma": Var(5.670374419e-8, "W/m2-K4"), # Stefan-Boltzmann constant "R": Var(8.314462618, "kJ/kmol-K"), # Gas constant "N_A": Var(6.02214076e23, "1/mol"), # Avogadro constant "K_cd": Var(683, "lm/W"), # Luminous efficacy of 540 THz radiation }