Source code for pyH2A.Utilities.Unit_Handler.quantity

import re
import numpy as np

from pyH2A.Utilities.Unit_Handler.config import FLAT_MULTIPLIERS, FLAT_BASES, FLAT_DIMENSIONS, ABSOLUTE_TEMPERATURE

# Regex pattern for lenient parsing. Splits by math operators and keeps them as tokens.
# Filters out spaces and empty strings automatically based on regex logic.
TOKEN_PATTERN = re.compile(r'([*/()])|\s+')

[docs] def parse_composite_unit(unit_str): """ Parse a composite unit (like 'kWh / cm2' or '(kWh * m)/m2') string into conversion multiplier, resulting composite base unit, and composite dimension. This function expands user-facing units (e.g., `kWh / cm2` or `(kWh * m)/m2`) into: - a numerical multiplier to convert to base units, - a composite base-unit expression, and - a composite dimension expression. Parameters ---------- unit_str : str Unit expression that may include `*`, `/`, and parentheses. Returns ------- combined_multiplier : float Multiplier that converts `unit_str` into its composite base unit. combined_base_str : str Composite base unit expression (e.g., `J / m2`). combined_dim_str : str Composite dimension expression in the same operator layout. Raises ------ ValueError If an unknown unit token is encountered or the expression cannot be evaluated. """ tokens = TOKEN_PATTERN.split(unit_str) # Remove empty/whitespace tokens tokens = [t.strip() for t in tokens if t and t.strip()] multiplier_expr = [] base_expr = [] dim_expr = [] for tok in tokens: if tok in ('*', '/', '(', ')'): multiplier_expr.append(tok) base_expr.append(tok) dim_expr.append(tok) elif tok.isdigit() or tok.replace('.', '', 1).isdigit(): # Support explicit coefficients like '1/month' where 1 is the quantity component multiplier_expr.append(tok) base_expr.append(tok) dim_expr.append(tok) else: if tok not in FLAT_MULTIPLIERS: raise ValueError(f"Unknown unit encountered during parsing: '{tok}'") val = FLAT_MULTIPLIERS[tok] base = FLAT_BASES[tok] dim = FLAT_DIMENSIONS[tok] multiplier_expr.append(str(val)) base_expr.append(base) dim_expr.append(dim) # Evaluate the multiplier using safe eval (restricting globals over a math string) multiplier_str = "".join(multiplier_expr) try: combined_multiplier = eval(multiplier_str, {"__builtins__": {}}) except Exception as e: raise ValueError(f"Could not compute composite factor for '{unit_str}': {str(e)}") # Join with spaces to generate clean standard form (e.g. 'J / m2') combined_base_str = " ".join(base_expr) combined_dim_str = " ".join(dim_expr) return combined_multiplier, combined_base_str, combined_dim_str
[docs] class UnitDictionary(dict): """ A custom dictionary class designed for lazy runtime unit evaluations. Takes memory and performance into consideration by not calculating all unit conversions upfront. Guarantees that the 'base_unit' and 'supplied_unit' values are immediately present. """ def __init__(self, quantity): """ Create a lazy unit dictionary for a given `Quantity`. Parameters ---------- quantity : Quantity Quantity instance providing base/supplied values and dimension. Returns ------- None : None This initializer populates the dictionary in-place. """ super().__init__() self._quantity = quantity # Populate guaranteed keys on init self[quantity.supplied_unit] = quantity.supplied_value self[quantity.base_unit] = quantity.base_value def __missing__(self, target_unit): """ Lazily compute a unit value when dict[target_unit] is accessed. Parameters ---------- target_unit : str Unit expression requested by the caller. Returns ------- value : float or np.ndarray Value expressed in `target_unit`, cached in the dictionary. Raises ------ KeyError If an absolute temperature conversion is requested for an unsupported unit. ValueError If the requested unit has a mismatched dimension. """ # 1. Absolute Temperature Handling Path if self._quantity.is_absolute_temp: if target_unit not in ABSOLUTE_TEMPERATURE["supported_units"]: raise KeyError(f"Unsupported absolute temperature unit: {target_unit}") from_base_func = ABSOLUTE_TEMPERATURE["from_base"][target_unit] val = from_base_func(self._quantity.base_value) self[target_unit] = val return val # 2. Standard / Composite Units Handling Path target_multiplier, target_base, target_dim = parse_composite_unit(target_unit) # Verify dimension logic (light validation by stripping spaces) if target_dim.replace(" ", "") != self._quantity.dimension.replace(" ", ""): raise ValueError( f"Dimension mismatch: original dimension '{self._quantity.dimension}', " f"but requested dimension '{target_dim}' when mapping '{target_unit}'" ) # Compute final target value seamlessly using numpy (if given) or scalar types val = self._quantity.base_value / target_multiplier self[target_unit] = val return val
[docs] class Quantity: """ Lightweight computational replacement for Pint in pyH2A. The constructor parses the supplied unit into base units and a dimension string. Unit conversion is provided lazily through a `UnitDictionary` stored on `self.unit`. """ __slots__ = ['supplied_value', 'supplied_unit', 'base_value', 'base_unit', 'dimension', 'unit', 'is_absolute_temp'] def __init__(self, value, unit_str): ''' Create a `Quantity` from a numeric value and unit expression. Parameters ---------- value : float, int, or np.ndarray Supplied numeric value. unit_str : str Unit expression compatible with the unit handler configuration. Returns ------- None : None The instance is initialized in-place. ''' self.supplied_value = value self.supplied_unit = unit_str.strip() self.is_absolute_temp = False # Detect hardcoded offset pathway if self.supplied_unit in ABSOLUTE_TEMPERATURE["supported_units"]: self.is_absolute_temp = True to_base_func = ABSOLUTE_TEMPERATURE["to_base"][self.supplied_unit] self.base_value = to_base_func(self.supplied_value) self.base_unit = ABSOLUTE_TEMPERATURE["base"] self.dimension = "absolute_temperature" else: # Handle multi-unit combinations (e.g. 'kWh/day' or '(J*m)/cm2') supplied_multiplier, base_unit_str, dim_str = parse_composite_unit(self.supplied_unit) self.base_value = self.supplied_value * supplied_multiplier self.base_unit = base_unit_str self.dimension = dim_str # Provide the required dictionary attribute for lazy multi-unit access self.unit = UnitDictionary(self) def __repr__(self): """ Provide a compact representation using base units. Returns ------- representation : str String form `Quantity(<base_value>, '<base_unit>')`. """ return f"Quantity({self.base_value}, '{self.base_unit}')"
[docs] def test_quantity(): """ Run a simple, manual sanity check of quantity parsing and conversion. Returns ------- None : None Prints example outputs to stdout. """ array_test = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) #array_test = 10 test_energy = Quantity(array_test, 'kWh / m2 / day') print(test_energy) # Should show the original value and unit print(test_energy.dimension) # test_frequency = Quantity(1, '1 / day') # print(test_frequency) # Should show the original value and unit test_energy = Quantity(10, 'J') print(test_energy.unit['eV']) # Should convert to electronvolts test_dimensionless = Quantity(0.99, '-') print(test_dimensionless.dimension)
# print(test_energy.unit['J / m2 / s']) # Should convert to Joules if __name__ == "__main__": test_quantity()