305 lines
9.7 KiB
Python
305 lines
9.7 KiB
Python
from __future__ import annotations
|
||
|
||
from collections.abc import Callable
|
||
from dataclasses import dataclass
|
||
from typing import ClassVar
|
||
|
||
from app.simulation.core.medium import (
|
||
GasMedium,
|
||
IdealGasMedium,
|
||
ThermodynamicProperties,
|
||
)
|
||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AmesimIdealAirMedium(IdealGasMedium):
|
||
"""AMESim air properties evaluated with the ideal-gas method.
|
||
|
||
Substance identity and property method are part of the concrete Python
|
||
type. A future air correlation or helium Peng-Robinson implementation can
|
||
therefore coexist as a sibling type without turning ``gi`` into a fluid
|
||
enumeration.
|
||
"""
|
||
|
||
SUBSTANCE_ID: ClassVar[str] = "air"
|
||
PROPERTY_METHOD_ID: ClassVar[str] = "ideal_gas"
|
||
|
||
name: str = "AMESimAirIdealGas"
|
||
R_gas: float = 287.0
|
||
cp_ref: float = 1005.0
|
||
T_ref: float = 300.0
|
||
cp_slope: float = 0.0
|
||
viscosity_ref: float = 1.82e-5
|
||
viscosity_T_ref: float = 293.15
|
||
sutherland_constant: float = 110.4
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
||
"""AMESim helium with a Peng-Robinson mechanical equation of state.
|
||
|
||
The pressure-density-temperature relation is evaluated by the shared
|
||
``HELIUM_PR`` fluid. The caloric reference follows the constant NASA
|
||
polynomial from Simcenter Amesim 2404 ``helium_cp_h_s.data``.
|
||
"""
|
||
|
||
SUBSTANCE_ID: ClassVar[str] = "helium"
|
||
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
|
||
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
|
||
nasa_cp_over_R: ClassVar[float] = 2.5
|
||
nasa_enthalpy_constant_K: ClassVar[float] = -745.375
|
||
|
||
name: str = "AMESimHeliumPengRobinson"
|
||
R_gas: float = HELIUM_PR.specific_gas_constant
|
||
cp_ref: float = nasa_cp_over_R * HELIUM_PR.specific_gas_constant
|
||
T_ref: float = 293.15
|
||
cp_slope: float = 0.0
|
||
viscosity_ref: float = 1.96e-5
|
||
viscosity_T_ref: float = 293.15
|
||
sutherland_constant: float = 79.4
|
||
|
||
@property
|
||
def cv(self) -> float:
|
||
return (self.nasa_cp_over_R - 1.0) * self.R_gas
|
||
|
||
def cv_at_temperature(self, T: float) -> float:
|
||
del T
|
||
return self.cv
|
||
|
||
def density(self, p: float, T: float) -> float:
|
||
return self.fluid.density(p, T)
|
||
|
||
def _real_heat_capacities(
|
||
self,
|
||
p: float,
|
||
T: float,
|
||
) -> tuple[float, float, float, float, float]:
|
||
density = self.density(p, T)
|
||
pressure_density_derivative = (
|
||
self.fluid.pressure_density_derivative_at_temperature(
|
||
T,
|
||
density,
|
||
)
|
||
)
|
||
pressure_temperature_derivative = (
|
||
self.fluid.pressure_temperature_derivative_at_density(
|
||
T,
|
||
density,
|
||
)
|
||
)
|
||
cv = (
|
||
self.cv_at_temperature(T)
|
||
+ self.fluid.residual_isochoric_heat_capacity_at_density(T, density)
|
||
)
|
||
cp = (
|
||
cv
|
||
+ T
|
||
* pressure_temperature_derivative
|
||
* pressure_temperature_derivative
|
||
/ (density * density * pressure_density_derivative)
|
||
)
|
||
if cp <= 0.0 or cv <= 0.0:
|
||
raise ValueError("Real-gas heat capacities must be positive.")
|
||
return (
|
||
cp,
|
||
cv,
|
||
density,
|
||
pressure_density_derivative,
|
||
pressure_temperature_derivative,
|
||
)
|
||
|
||
def _local_isentropic_density_pressure_factor(
|
||
self,
|
||
p: float,
|
||
T: float,
|
||
) -> tuple[float, float]:
|
||
cp, cv, density, pressure_density_derivative, pressure_temperature_derivative = (
|
||
self._real_heat_capacities(p, T)
|
||
)
|
||
heat_capacity_ratio = cp / cv
|
||
factor = p / (
|
||
density * pressure_density_derivative * heat_capacity_ratio
|
||
)
|
||
exponent = (
|
||
p
|
||
* (heat_capacity_ratio - 1.0)
|
||
/ (
|
||
heat_capacity_ratio
|
||
* T
|
||
* pressure_temperature_derivative
|
||
)
|
||
)
|
||
return factor, exponent
|
||
|
||
def isentropic_density_pressure_factor(
|
||
self,
|
||
p: float,
|
||
T: float,
|
||
downstream_pressure: float | None = None,
|
||
) -> float:
|
||
upstream_factor, isentropic_temperature_exponent = (
|
||
self._local_isentropic_density_pressure_factor(p, T)
|
||
)
|
||
if downstream_pressure is None or downstream_pressure >= p:
|
||
return upstream_factor
|
||
|
||
pressure_ratio = max(downstream_pressure / p, 1.0e-12)
|
||
isentropic_temperature = max(
|
||
T * pressure_ratio**isentropic_temperature_exponent,
|
||
2.2,
|
||
)
|
||
downstream_factor, _unused_exponent = (
|
||
self._local_isentropic_density_pressure_factor(
|
||
max(downstream_pressure, 1.0),
|
||
isentropic_temperature,
|
||
)
|
||
)
|
||
# AMESim 2404 saggs_ evaluates the local factor at the upstream
|
||
# state and at an approximate isentropic downstream state.
|
||
return 0.5 * (upstream_factor + downstream_factor)
|
||
|
||
def pressure(self, m: float, T: float, V: float) -> float:
|
||
if V <= 0.0:
|
||
raise ValueError("Volume must stay positive.")
|
||
return self.fluid.pressure_from_density(T, m / V)
|
||
|
||
def specific_internal_energy(self, T: float) -> float:
|
||
return self.R_gas * (
|
||
(self.nasa_cp_over_R - 1.0) * T
|
||
+ self.nasa_enthalpy_constant_K
|
||
)
|
||
|
||
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
|
||
density = self.density(p, T)
|
||
return (
|
||
self.specific_internal_energy(T)
|
||
+ self.fluid.residual_specific_internal_energy_at_density(T, density)
|
||
)
|
||
|
||
def specific_enthalpy(self, T: float) -> float:
|
||
return self.R_gas * (
|
||
self.nasa_cp_over_R * T
|
||
+ self.nasa_enthalpy_constant_K
|
||
)
|
||
|
||
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
|
||
return self.specific_enthalpy(T) + self.fluid.residual_specific_enthalpy(p, T)
|
||
|
||
def temperature_from_internal_energy(self, u: float) -> float:
|
||
return (
|
||
u / self.R_gas - self.nasa_enthalpy_constant_K
|
||
) / (self.nasa_cp_over_R - 1.0)
|
||
|
||
def temperature_from_enthalpy(self, h: float) -> float:
|
||
return (
|
||
h / self.R_gas - self.nasa_enthalpy_constant_K
|
||
) / self.nasa_cp_over_R
|
||
|
||
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
|
||
temperature = max(self.temperature_from_enthalpy(h), 2.2)
|
||
for _iteration in range(16):
|
||
residual_enthalpy = self.fluid.residual_specific_enthalpy(p, temperature)
|
||
next_temperature = max(
|
||
self.temperature_from_enthalpy(h - residual_enthalpy),
|
||
2.2,
|
||
)
|
||
if abs(next_temperature - temperature) <= 1.0e-10 * max(
|
||
temperature,
|
||
1.0,
|
||
):
|
||
return next_temperature
|
||
temperature = next_temperature
|
||
return temperature
|
||
|
||
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 properties_from_mU(
|
||
self,
|
||
m: float,
|
||
U: float,
|
||
V: float,
|
||
) -> ThermodynamicProperties:
|
||
if m <= 0.0:
|
||
raise ValueError("Mass must stay positive when recovering temperature.")
|
||
if V <= 0.0:
|
||
raise ValueError("Volume must stay positive.")
|
||
density = m / V
|
||
target_internal_energy = U / m
|
||
temperature = max(
|
||
self.temperature_from_internal_energy(target_internal_energy),
|
||
2.2,
|
||
)
|
||
for _iteration in range(16):
|
||
residual_internal_energy = (
|
||
self.fluid.residual_specific_internal_energy_at_density(
|
||
temperature,
|
||
density,
|
||
)
|
||
)
|
||
next_temperature = max(
|
||
self.temperature_from_internal_energy(
|
||
target_internal_energy - residual_internal_energy
|
||
),
|
||
2.2,
|
||
)
|
||
if abs(next_temperature - temperature) <= 1.0e-10 * max(
|
||
temperature,
|
||
1.0,
|
||
):
|
||
temperature = next_temperature
|
||
break
|
||
temperature = next_temperature
|
||
pressure = self.fluid.pressure_from_density(temperature, density)
|
||
return ThermodynamicProperties(
|
||
p=pressure,
|
||
T=temperature,
|
||
rho=density,
|
||
u=target_internal_energy,
|
||
h=self.specific_enthalpy_at_pressure(
|
||
pressure,
|
||
temperature,
|
||
),
|
||
)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class AmesimGasPropertyModelSpec:
|
||
"""A selectable calculation method for one AMESim gas substance."""
|
||
|
||
value: int
|
||
label: str
|
||
method_id: str
|
||
factory: Callable[[], GasMedium]
|
||
eos_type: int
|
||
|
||
def build_medium(self) -> GasMedium:
|
||
return self.factory()
|
||
|
||
|
||
AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL = 0
|
||
AMESIM_AIR_PROPERTY_MODELS = (
|
||
AmesimGasPropertyModelSpec(
|
||
value=AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL,
|
||
label="理想气体",
|
||
method_id=AmesimIdealAirMedium.PROPERTY_METHOD_ID,
|
||
factory=AmesimIdealAirMedium,
|
||
eos_type=1,
|
||
),
|
||
)
|
||
|
||
|
||
AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL = 0
|
||
AMESIM_HELIUM_PROPERTY_MODELS = (
|
||
AmesimGasPropertyModelSpec(
|
||
value=AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL,
|
||
label="Peng–Robinson",
|
||
method_id=AmesimHeliumPengRobinsonMedium.PROPERTY_METHOD_ID,
|
||
factory=AmesimHeliumPengRobinsonMedium,
|
||
eos_type=6,
|
||
),
|
||
)
|