97 lines
3.1 KiB
Python
97 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ThermodynamicProperties:
|
|
p: float
|
|
T: float
|
|
rho: float
|
|
u: float
|
|
h: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IdealGasMedium:
|
|
"""Temperature-dependent ideal-gas air approximation.
|
|
|
|
This is still not a strict clone of `Modelica.Media.Air.SimpleAir`.
|
|
The small linear `cp(T)` term is kept configurable for calibration, but the
|
|
current default is calibrated against the committed Testmodel baseline and
|
|
therefore falls back to the constant-heat-capacity limit.
|
|
"""
|
|
|
|
name: str = "SimpleAirApprox"
|
|
R_gas: float = 287.0
|
|
cp_ref: float = 1005.0
|
|
T_ref: float = 300.0
|
|
cp_slope: float = 0.0
|
|
|
|
@property
|
|
def cv(self) -> float:
|
|
return self.cv_at_temperature(self.T_ref)
|
|
|
|
@property
|
|
def gamma(self) -> float:
|
|
return self.cp_at_temperature(self.T_ref) / self.cv
|
|
|
|
def cp_at_temperature(self, T: float) -> float:
|
|
return self.cp_ref + self.cp_slope * (T - self.T_ref)
|
|
|
|
def cv_at_temperature(self, T: float) -> float:
|
|
return self.cp_at_temperature(T) - self.R_gas
|
|
|
|
def density(self, p: float, T: float) -> float:
|
|
return p / (self.R_gas * T)
|
|
|
|
def specific_internal_energy(self, T: float) -> float:
|
|
delta_T = T - self.T_ref
|
|
return (
|
|
self.cv * self.T_ref
|
|
+ self.cv * delta_T
|
|
+ 0.5 * self.cp_slope * delta_T * delta_T
|
|
)
|
|
|
|
def specific_enthalpy(self, T: float) -> float:
|
|
delta_T = T - self.T_ref
|
|
return (
|
|
self.cp_ref * self.T_ref
|
|
+ self.cp_ref * delta_T
|
|
+ 0.5 * self.cp_slope * delta_T * delta_T
|
|
)
|
|
|
|
def temperature_from_internal_energy(self, u: float) -> float:
|
|
reference_internal_energy = self.cv * self.T_ref
|
|
delta_u = u - reference_internal_energy
|
|
|
|
if abs(self.cp_slope) <= 1e-15:
|
|
return self.T_ref + delta_u / self.cv
|
|
|
|
a = 0.5 * self.cp_slope
|
|
b = self.cv
|
|
c = -delta_u
|
|
discriminant = max(b * b - 4.0 * a * c, 0.0)
|
|
positive_root = (-b + discriminant**0.5) / (2.0 * a)
|
|
negative_root = (-b - discriminant**0.5) / (2.0 * a)
|
|
delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root
|
|
return self.T_ref + delta_T
|
|
|
|
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
|
if m <= 0.0:
|
|
raise ValueError("Mass must stay positive when recovering temperature.")
|
|
return self.temperature_from_internal_energy(U / m)
|
|
|
|
def pressure(self, m: float, T: float, V: float) -> float:
|
|
if V <= 0.0:
|
|
raise ValueError("Volume must stay positive.")
|
|
return m * self.R_gas * T / V
|
|
|
|
def properties_from_mU(self, m: float, U: float, V: float) -> ThermodynamicProperties:
|
|
T = self.temperature_from_mass_internal_energy(m, U)
|
|
p = self.pressure(m, T, V)
|
|
rho = m / V
|
|
u = U / m
|
|
h = self.specific_enthalpy(T)
|
|
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
|