'''
Fluid medium properties and shared unit infrastructure.
This module defines the unit registry used across fluidsolve and provides the
``Medium`` class, which encapsulates fluid-state properties required by
component and network calculations.
External dependencies:
* ``pint`` for unit-safe quantities,
* ``thermo`` for fluid property lookup from product names.
Main capabilities:
* expose shared unit aliases (``unitRegistry``, ``u``, ``Quantity``),
* define reference constants at normal conditions,
* create media from known thermo products,
* support user-defined media by overriding key properties (rho, mu, k).
Why this module is central:
Most hydraulic equations in the package convert between pressure, head,
velocity, and flow, all of which depend on medium properties and unit
consistency. This module provides that common foundation.
Typical usage::
water = Medium(prd='water')
custom = Medium(name='custom_mix', rho=950 * u.kg / u.m**3, mu=1.5e-3 * u.Pa * u.s)
rho = water.rho
References:
* https://thermo.readthedocs.io/thermo.chemical.html
* https://pint.readthedocs.io/en/stable/
'''
# =============================================================================
# PYLINT DIRECTIVES
# =============================================================================
# =============================================================================
# IMPORTS
# =============================================================================
from typing import Any
import fluids.units as fu
from pint import _DEFAULT_REGISTRY as pint_u
from pint import Quantity as pint_q
from thermo.chemical import Chemical
# module own
import fluidsolve.aux_tools as flsa
# =============================================================================
# UNITS (USED BY OTHER MODULES)
# =============================================================================
unitRegistry = pint_u
u = pint_u
Quantity = pint_q # type: ignore[misc]
# =============================================================================
# CONSTANTS
# =============================================================================
''' Gravitational acceleration. '''
CTE_G = 9.80665 * u.m/u.s**2
''' Normal temperature. '''
CTE_NT = 20.0 * u.degC
''' Normal pressure. '''
CTE_NP = (1.0 * u.atm).to(u.Pa)
''' Water at normal conditions. '''
CTE_WATER = Chemical('water', P=CTE_NP.magnitude, T=CTE_NT.to(u.degK).magnitude)
''' Water density. '''
CTE_RHO = CTE_WATER.rho * u.kg/u.m**3
''' Dynamic viscosity. '''
CTE_MU = CTE_WATER.mu * u.Pa*u.s
''' Kinematic viscosity. '''
CTE_NU = CTE_WATER.nu * u.m**2/u.s
''' Thermal conductivity of water. '''
CTE_K = CTE_WATER.k * u.W/u.m/u.degK
''' Absolute roughness (epsilon) of stainless steel. '''
CTE_E_RVS = 1.6 * u.um
# =============================================================================
# MEDIUM CLASS
# =============================================================================
[docs]
class Medium ():
''' Class representing a medium.
It can be created from a thermo product name.
It can also be user-defined by explicitly providing properties
such as rho, mu, and k.
Args:
prd (str, optional): Product name known to thermo.
name (str, optional): Medium name.
T (int | float | Quantity, optional): Temperature.
p (int | float | Quantity, optional): Pressure.
rho (int | float | Quantity, optional): Density.
mu (int | float | Quantity, optional): Dynamic viscosity.
k (int | float | Quantity, optional): Thermal conductivity.
Returns:
None
'''
# --------------------------------------------------------------------------
# INITIALIZE
[docs]
def __init__(self, **kwargs: int) -> None:
args = flsa.GetArgs(kwargs)
self._prd: str = args.getArg(
'prd',
[
flsa.vFun.default('water'),
flsa.vFun.istype(str),
]
)
self._name: str = args.getArg(
'name',
[
flsa.vFun.default(self._prd),
flsa.vFun.istype(str),
]
)
# conditions
self._T: Quantity = args.getArg(
'T',
[
flsa.vFun.default(CTE_NT),
flsa.vFun.istype((float, Quantity)),
flsa.vFun.tounits(u.degK)
]
)
self._p: Quantity = args.getArg(
'p',
[
flsa.vFun.default(CTE_NP),
flsa.vFun.istype((float, Quantity)),
flsa.vFun.tounits(u.bar)
]
)
# override rho, mu, k
# update the product with this conditions
self._rho_override = 'rho' in kwargs # pylint: disable=invalid-name
self._mu_override = 'mu' in kwargs # pylint: disable=invalid-name
self._k_override = 'k' in kwargs # pylint: disable=invalid-name
self._updateProduct()
if (self._cprd is None and not (self._rho_override and self._mu_override and self._k_override)):
raise ValueError('Medium must have a valid prd or have a rho, mu and k')
if self._rho_override:
self._rho: Quantity = args.getArg(
'rho',
[
flsa.vFun.istype(float, Quantity),
flsa.vFun.tounits(u.kg/u.m**3)
]
)
if self._mu_override:
self._mu: Quantity = args.getArg(
'mu',
[
flsa.vFun.istype(float, Quantity),
flsa.vFun.tounits(u.Pa*u.s)
]
)
if self._k_override:
self._k: Quantity = args.getArg(
'k',
[
flsa.vFun.istype(float, Quantity),
flsa.vFun.tounits(u.W/u.m/u.degK)
]
)
# --------------------------------------------------------------------------
# PROPERTIES
@property
def name(self) -> str:
''' Name property.
Returns:
str: Name property.
'''
return self._name
@name.setter
def name(self, value: str) -> None:
''' Set name property.
Args:
value (str): Name.
'''
self._name = value
self._updateProduct()
@property
def cprd(self) -> str:
''' underlying chemical object.
Returns:
Any: cprd property.
'''
return self._cprd
@property
def T(self) -> Quantity:
''' Temperature property.
Returns:
Quantity: Temperature in degC (internally stored in K).
'''
return self._T.to(u.degC)
@T.setter
def T(self, value: int | float | Quantity) -> None:
''' Set temperature.
Args:
value (int | float | Quantity): Temperature.
'''
self._T = flsa.toUnits(value, u.degK)
self._updateProduct()
@property
def p(self) -> Quantity:
''' Pressure property.
Returns:
Quantity: Pressure property (bar).
'''
return self._p
@p.setter
def p(self, value: int | float | Quantity) -> None:
''' Set pressure property.
Args:
value (int | float | Quantity): Pressure.
'''
self._p = flsa.toUnits(value, u.bar)
self._updateProduct()
@property
def rho(self) -> Quantity:
''' Density property.
Returns:
Quantity: Density property (kg/m3).
'''
return self._rho
@rho.setter
def rho(self, value: int | float | Quantity) -> None:
''' Set density property.
Args:
value (int | float | Quantity): Density (default in kg/m3).
'''
self._rho_override = True
self._rho = flsa.toUnits(value, u.kg/u.m**3)
@property
def mu(self) -> Quantity:
''' Dynamic viscosity property.
Returns:
Quantity: Dynamic viscosity property (Pa.s).
'''
return self._mu
@mu.setter
def mu(self, value: int | float | Quantity) -> None:
''' Set dynamic viscosity property.
Args:
value (int | float | Quantity): Dynamic viscosity.
'''
self._mu_override = True
self._mu = flsa.toUnits(value, u.Pa*u.s)
@property
def k(self) -> Quantity:
''' Thermal conductivity property.
Returns:
Quantity: Thermal conductivity property (W/m/K).
'''
return self._k
@k.setter
def k(self, value: int | float | Quantity) -> None:
''' Set thermal conductivity property.
Args:
value (int | float | Quantity): Thermal conductivity.
'''
self._k_override = True
self._k = flsa.toUnits(value, u.W/u.m/u.degK)
[docs]
def _updateProduct(self) -> Any:
''' Update derived properties from the thermo product model. '''
if len(self._prd)>0:
self._cprd = Chemical(self._prd, P=self._p.to(u.Pa).magnitude, T=self._T.to(u.degK).magnitude)
if not self._rho_override:
self._rho = self._cprd.rho * u.kg/u.m**3
if not self._mu_override:
self._mu = self._cprd.mu * u.Pa*u.s
if not self._k_override:
self._k = self._cprd.k * u.W/u.m/u.degK
else:
self._cprd = None
[docs]
def __str__(self) -> str:
''' String representation.
Returns:
str: String representation.
'''
return self.toString(0)
def __format__(self, format_spec: str) -> str:
if format_spec == '':
return str(self)
try:
detail = int(format_spec)
except ValueError as exc:
raise ValueError(f'Invalid format spec for {type(self).__name__}: {format_spec!r}') from exc
return self.toString(detail)
[docs]
def toString(self, detail: int=0) -> str:
''' Return string representation.
Args:
detail (int, optional): Detail level.
Returns:
str: String representation.
'''
name = self._name if self._name else '-'
if detail == 0:
return f'Medium {name}: rho: {self._rho:.2f~P}, mu: {self._mu:.2e~P}'
else:
return f'Medium {name} : T: {self._T:.2f~P}, p: {self._p:.2f~P}, rho: {self._rho:.2f~P}, mu: {self._mu:.2e~P}, k: {self._k:.2e~P}'
[docs]
def __repr__(self) -> str:
''' Representation of the medium object.
Returns:
str: Representation.
'''
if self._name=='':
return f'Medium(prd="water",rho={self._rho:.2f~P}, mu={self._mu:.2e~P})'
else:
return f'Medium(name="{self._name}", prd="{self._prd}",rho={self._rho:.2f~P}, mu={self._mu:.2e~P})'