# -*- 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()