Source code for tespy.tools.units

# -*- coding: utf-8

"""Module for Units class.

This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted
by the contributors recorded in the version control history of the file,
available from its original location tespy/tools/units.py

SPDX-License-Identifier: MIT
"""
import shutil
import sys
import warnings

import pint
import platformdirs

from tespy import __datapath__


[docs] class Units:
[docs] @classmethod def from_json(cls, default_units): instance = cls() instance.set_defaults(**default_units) return instance
def __init__(self): self.default = { "temperature": "kelvin", "temperature_difference": "delta_degC", "enthalpy": "J/kg", "specific_energy": "J/kg", "entropy": "J/kg/K", "pressure": "Pa", "pressure_difference": "Pa", "mass_flow": "kg/s", "volumetric_flow": "m3/s", "specific_volume": "m3/kg", "power": "W", "heat": "W", "quality": "1", "vapor_mass_fraction": "1", # backwards compatibility network import "efficiency": "1", "ratio": "1", "length": "m", "speed": "m/s", "area": "m2", "thermal_conductivity": "W/m/K", "heat_transfer_coefficient": "W/K", "heat_transfer_coefficient_per_area": "W/m**2/K", "thermal_resistance": "K/W", "angle": "degree", # the SI unit for angle would be radians, but that would break things in the compressor "frequency": "1/s", # None is the default if not quantity is supplied None: "1" } # necessary, because pint cannot auto detect environment changes and # pint version changes major = sys.version_info.major minor = sys.version_info.minor path = platformdirs.user_cache_dir( "tespy", False, f"py{major}{minor}pint{pint.__version__}" ) try: self._ureg = pint.UnitRegistry(cache_folder=path) except FileNotFoundError: # this is necessary, because inside the cache folder, pint points # to the pint installation inside (any) of the venvs (potentially # the first ever created?). If that venv moves or gets deleted, # then the link cannot be found any more and we have to recreated # the cache shutil.rmtree(path) self._ureg = pint.UnitRegistry(cache_folder=path) # cannot use the setter here because we have to define m3 first! self.ureg.define("m3 = m ** 3") self.ureg.define("m2 = m ** 2") self.ureg.define("kgK = kg * K") self._quantities = { k: self.ureg.Quantity(1, v) for k, v in self.default.items() } self._from_SI = {}
[docs] def value_from_SI(self, value_SI, base_unit, target_unit): """Convert a value in SI units to its magnitude in the target unit. Unit conversions are affine, so factor and offset are computed once per unit pair and cached, bypassing the pint conversion machinery for every subsequent value. """ try: factor, offset = self._from_SI[(base_unit, target_unit)] except KeyError: offset = self.ureg.Quantity(0.0, base_unit).m_as(target_unit) # probe at a large power of two: the division is exact and the # cancellation error of the offset subtraction becomes negligible probe = 2.0 ** 20 factor = ( self.ureg.Quantity(probe, base_unit).m_as(target_unit) - offset ) / probe self._from_SI[(base_unit, target_unit)] = (factor, offset) return value_SI * factor + offset
[docs] def quantity_from_SI(self, value_SI, base_unit, target_unit): return self.ureg.Quantity( self.value_from_SI(value_SI, base_unit, target_unit), target_unit )
[docs] def set_defaults(self, **kwargs): """Set the default units Parameters ---------- temperature : str Default unit: "kelvin" temperature_difference : str Default unit: "delta_degC" enthalpy : str Default unit: "J/kg" specific_energy : str Default unit: "J/kg" entropy : str Default unit: "J/kg/K" pressure : str Default unit: "Pa". For backwards compatibility, setting this also sets :code:`pressure_difference` to the same unit unless :code:`pressure_difference` is explicitly provided as well. pressure_difference : str Default unit: "Pa" mass_flow : str Default unit: "kg/s" volumetric_flow : str Default unit: "m3/s" specific_volume : str Default unit: "m3/kg" power : str Default unit: "W" heat : str Default unit: "W" quality : str Default unit: "1" efficiency : str Default unit: "1" ratio : str Default unit: "1" length : str Default unit: "m" speed : str Default unit: "m/s" area : str Default unit: "m2" thermal_conductivity : str Default unit: "W/m/K" heat_transfer_coefficient : str Default unit: "W/K" heat_transfer_coefficient_per_area : str Default unit: "W/m**2/K" thermal_resistance : str Default unit: "K/W" """ if "pressure" in kwargs and "pressure_difference" not in kwargs: msg = ( "Setting the 'pressure' unit currently also sets the " "'pressure_difference' unit for backwards compatibility. " "In version 0.12 this will no longer happen. Please " "explicitly set 'pressure_difference' in " "Network.units.set_defaults() to silence this warning." ) warnings.warn(msg, FutureWarning) kwargs["pressure_difference"] = kwargs["pressure"] for key, value in kwargs.items(): self._check_quantity_exists(key) if value == "-": value = "1" if self._is_compatible(key, value): self.default[key] = value else: msg = f"Unit {value} is not compatible with quantity {key}" raise ValueError(msg)
def _is_compatible(self, quantity, unit): if quantity == "temperature_difference": if unit.startswith("delta_"): return self._quantities[quantity].is_compatible_with(unit) else: _units = self.ureg._units kelvin = list(_units["K"].aliases) + [_units["K"].name] rankine = list(_units["rankine"].aliases) + [_units["rankine"].name] return unit in kelvin or unit in rankine else: return self._quantities[quantity].is_compatible_with(unit)
[docs] def get_default(self, quantity): self._check_quantity_exists(quantity) return self.default[quantity]
def _check_quantity_exists(self, quantity): if quantity not in self.default: msg = ( f"The quantity {quantity} is unknown. Please specify any of " f"the following: " f"{', '.join([key for key in self.default if key is not None])}." ) raise KeyError(msg)
[docs] def set_ureg(self, ureg): """Replace default ureg with a custom one Parameters ---------- ureg : pint.UnitRegistry Change the pint.UnitRegistry to a custom one """ self._ureg = ureg self.ureg.define("m3 = m ** 3") self.ureg.define("m2 = m ** 2") self._quantities = { k: self.ureg.Quantity(1, v) for k, v in self.default.items() }
[docs] def get_ureg(self): return self._ureg
def _serialize(self): return {k: v for k, v in self.default.items() if k is not None} ureg = property(get_ureg, set_ureg)
_UNITS = Units() SI_UNITS = _UNITS.default.copy()