对齐Amesim氦气PR物性与PNVO流量
This commit is contained in:
1 parent
18d9802f03
commit
046aa49814
13 files changed
+728
-71
No files matched your search
@@ -206,9 +206,13 @@ class AmesimPnor001(AlgebraicComponent):
|
|||||||
|
|
||||||
def _upstream_temperature(self, port_name: str) -> float:
|
def _upstream_temperature(self, port_name: str) -> float:
|
||||||
port = self.get_port(port_name)
|
port = self.get_port(port_name)
|
||||||
if port.h_outflow > 0.0:
|
return max(
|
||||||
return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0)
|
self.medium.temperature_from_pressure_enthalpy(
|
||||||
return self.medium.T_ref
|
max(port.p, 1.0),
|
||||||
|
port.h_outflow,
|
||||||
|
),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
|
||||||
def mass_flow(self, p_1: float, p_2: float) -> float:
|
def mass_flow(self, p_1: float, p_2: float) -> float:
|
||||||
if p_1 == p_2 or self.effective_area == 0.0:
|
if p_1 == p_2 or self.effective_area == 0.0:
|
||||||
@@ -461,6 +465,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
|||||||
self.port_2.h_outflow = initial_h
|
self.port_2.h_outflow = initial_h
|
||||||
self.port_3 = self.register_declared_port("port_3")
|
self.port_3 = self.register_declared_port("port_3")
|
||||||
self.port_3.h_outflow = initial_h
|
self.port_3.h_outflow = initial_h
|
||||||
|
self._connected_h: dict[str, float] = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _integer_parameter(name: str, value: float) -> int:
|
def _integer_parameter(name: str, value: float) -> int:
|
||||||
@@ -507,9 +512,16 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
|||||||
|
|
||||||
def _upstream_temperature(self, port_name: str) -> float:
|
def _upstream_temperature(self, port_name: str) -> float:
|
||||||
port = self.get_port(port_name)
|
port = self.get_port(port_name)
|
||||||
if port.h_outflow > 0.0:
|
# A component port's h_outflow describes fluid leaving the valve; the
|
||||||
return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0)
|
# upstream state comes from the connection on that same physical side.
|
||||||
return self.medium.T_ref
|
inlet_h = self._connected_h.get(port_name, port.h_outflow)
|
||||||
|
return max(
|
||||||
|
self.medium.temperature_from_pressure_enthalpy(
|
||||||
|
max(port.p, 1.0),
|
||||||
|
inlet_h,
|
||||||
|
),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
|
||||||
def mass_flow(self, p_2: float, p_3: float) -> float:
|
def mass_flow(self, p_2: float, p_3: float) -> float:
|
||||||
if p_2 == p_3 or self.effective_area == 0.0:
|
if p_2 == p_3 or self.effective_area == 0.0:
|
||||||
@@ -526,6 +538,64 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
|||||||
upstream_temperature=self._upstream_temperature("port_3"),
|
upstream_temperature=self._upstream_temperature("port_3"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _one_way_flow_characteristics(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
upstream_pressure: float,
|
||||||
|
downstream_pressure: float,
|
||||||
|
upstream_temperature: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
p_up = max(upstream_pressure, 1.0)
|
||||||
|
p_down = max(min(downstream_pressure, p_up), 0.0)
|
||||||
|
T_up = max(upstream_temperature, 1.0)
|
||||||
|
gamma_s = self.medium.isentropic_density_pressure_factor(
|
||||||
|
p_up,
|
||||||
|
T_up,
|
||||||
|
p_down,
|
||||||
|
)
|
||||||
|
gamma_s = min(max(gamma_s, 1.0e-9), 1.0 - 1.0e-9)
|
||||||
|
density = max(self.medium.density(p_up, T_up), 1.0e-12)
|
||||||
|
pressure_ratio = max(p_down / p_up, 0.0)
|
||||||
|
critical_ratio = (2.0 * gamma_s / (gamma_s + 1.0)) ** (
|
||||||
|
1.0 / (1.0 - gamma_s)
|
||||||
|
)
|
||||||
|
if pressure_ratio <= critical_ratio:
|
||||||
|
mass_flow_parameter = (
|
||||||
|
sqrt(2.0 / (1.0 + gamma_s) * density * T_up / p_up)
|
||||||
|
* (2.0 * gamma_s / (gamma_s + 1.0))
|
||||||
|
** (gamma_s / (1.0 - gamma_s))
|
||||||
|
)
|
||||||
|
gas_velocity = sqrt(
|
||||||
|
2.0 / (1.0 + gamma_s) * p_up / density
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
expansion = (
|
||||||
|
pressure_ratio ** (2.0 * gamma_s)
|
||||||
|
- pressure_ratio ** (1.0 + gamma_s)
|
||||||
|
)
|
||||||
|
mass_flow_parameter = sqrt(
|
||||||
|
max(
|
||||||
|
2.0
|
||||||
|
/ (1.0 - gamma_s)
|
||||||
|
* density
|
||||||
|
* T_up
|
||||||
|
/ p_up
|
||||||
|
* expansion,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
gas_velocity = sqrt(
|
||||||
|
max(
|
||||||
|
2.0
|
||||||
|
/ (1.0 - gamma_s)
|
||||||
|
* p_up
|
||||||
|
/ density
|
||||||
|
* (1.0 - pressure_ratio ** (1.0 - gamma_s)),
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return mass_flow_parameter, gas_velocity
|
||||||
|
|
||||||
def _one_way_mass_flow(
|
def _one_way_mass_flow(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -534,44 +604,45 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
|||||||
upstream_temperature: float,
|
upstream_temperature: float,
|
||||||
) -> float:
|
) -> float:
|
||||||
p_up = max(upstream_pressure, 1.0)
|
p_up = max(upstream_pressure, 1.0)
|
||||||
p_down = max(min(downstream_pressure, p_up), 0.0)
|
|
||||||
T_up = max(upstream_temperature, 1.0)
|
T_up = max(upstream_temperature, 1.0)
|
||||||
gamma = max(self.medium.gamma, 1.000001)
|
mass_flow_parameter, _gas_velocity = self._one_way_flow_characteristics(
|
||||||
pressure_ratio = max(p_down / p_up, 0.0)
|
upstream_pressure=p_up,
|
||||||
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
downstream_pressure=downstream_pressure,
|
||||||
if pressure_ratio <= critical_ratio:
|
upstream_temperature=T_up,
|
||||||
flow_factor = sqrt(gamma / (self.medium.R_gas * T_up)) * (
|
|
||||||
2.0 / (gamma + 1.0)
|
|
||||||
) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
|
||||||
else:
|
|
||||||
expansion = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** (
|
|
||||||
(gamma + 1.0) / gamma
|
|
||||||
)
|
)
|
||||||
flow_factor = sqrt(
|
return (
|
||||||
max(
|
self.effective_cq
|
||||||
2.0
|
* self.effective_area
|
||||||
* gamma
|
* p_up
|
||||||
* expansion
|
* mass_flow_parameter
|
||||||
/ (self.medium.R_gas * T_up * (gamma - 1.0)),
|
/ sqrt(T_up)
|
||||||
0.0,
|
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return self.effective_cq * self.effective_area * p_up * flow_factor
|
|
||||||
|
|
||||||
def component_result_values(self) -> Mapping[str, float]:
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
p_2 = max(self.port_2.p, 1.0)
|
p_2 = max(self.port_2.p, 1.0)
|
||||||
p_3 = max(self.port_3.p, 1.0)
|
p_3 = max(self.port_3.p, 1.0)
|
||||||
m_flow = abs(self.mass_flow(self.port_2.p, self.port_3.p))
|
if p_2 >= p_3:
|
||||||
upstream_pressure = max(p_2, p_3)
|
upstream_port_name = "port_2"
|
||||||
|
upstream_pressure = p_2
|
||||||
|
downstream_pressure = p_3
|
||||||
|
flow_direction = 1.0
|
||||||
|
else:
|
||||||
|
upstream_port_name = "port_3"
|
||||||
|
upstream_pressure = p_3
|
||||||
|
downstream_pressure = p_2
|
||||||
|
flow_direction = -1.0
|
||||||
upstream_temperature = self._upstream_temperature(
|
upstream_temperature = self._upstream_temperature(
|
||||||
"port_2" if p_2 >= p_3 else "port_3"
|
upstream_port_name
|
||||||
|
)
|
||||||
|
mass_flow_parameter, gas_velocity = self._one_way_flow_characteristics(
|
||||||
|
upstream_pressure=upstream_pressure,
|
||||||
|
downstream_pressure=downstream_pressure,
|
||||||
|
upstream_temperature=upstream_temperature,
|
||||||
)
|
)
|
||||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
|
||||||
area = max(self.effective_area, 1.0e-18)
|
|
||||||
return {
|
return {
|
||||||
"xv": self.opening,
|
"xv": self.opening,
|
||||||
"cm": m_flow / (self.effective_cq * area * upstream_pressure),
|
"cm": mass_flow_parameter,
|
||||||
"gasvel": m_flow / (density * area),
|
"gasvel": flow_direction * gas_velocity,
|
||||||
}
|
}
|
||||||
|
|
||||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
@@ -605,6 +676,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
|
self._connected_h = dict(connected_h)
|
||||||
self.port_2.h_outflow = connected_h["port_3"]
|
self.port_2.h_outflow = connected_h["port_3"]
|
||||||
self.port_3.h_outflow = connected_h["port_2"]
|
self.port_3.h_outflow = connected_h["port_2"]
|
||||||
|
|
||||||
@@ -684,6 +756,7 @@ class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening):
|
|||||||
self.port_2.h_outflow = initial_h
|
self.port_2.h_outflow = initial_h
|
||||||
self.port_3 = self.register_declared_port("port_3")
|
self.port_3 = self.register_declared_port("port_3")
|
||||||
self.port_3.h_outflow = initial_h
|
self.port_3.h_outflow = initial_h
|
||||||
|
self._connected_h: dict[str, float] = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(
|
def create(
|
||||||
|
|||||||
@@ -164,9 +164,13 @@ class AmesimPnl00r(AlgebraicComponent):
|
|||||||
|
|
||||||
def _port_temperature(self, port_name: str) -> float:
|
def _port_temperature(self, port_name: str) -> float:
|
||||||
port = self.get_port(port_name)
|
port = self.get_port(port_name)
|
||||||
if port.h_outflow > 0.0:
|
return max(
|
||||||
return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0)
|
self.medium.temperature_from_pressure_enthalpy(
|
||||||
return self.medium.T_ref
|
max(port.p, 1.0),
|
||||||
|
port.h_outflow,
|
||||||
|
),
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
|
||||||
def _dynamic_viscosity(self, temperature_k: float) -> float:
|
def _dynamic_viscosity(self, temperature_k: float) -> float:
|
||||||
return self.medium.dynamic_viscosity(temperature_k)
|
return self.medium.dynamic_viscosity(temperature_k)
|
||||||
@@ -494,9 +498,9 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
|
|||||||
self.volume = self.area * self.le
|
self.volume = self.area * self.le
|
||||||
self.exchange_area = pi * self.diam * self.le
|
self.exchange_area = pi * self.diam * self.le
|
||||||
m0 = medium.density(self.p0, self.T0) * self.volume
|
m0 = medium.density(self.p0, self.T0) * self.volume
|
||||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
||||||
self.state = VolumeState(m=m0, U=U0)
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
initial_h = medium.specific_enthalpy(self.T0)
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
||||||
self.port_1 = self.register_declared_port("port_1")
|
self.port_1 = self.register_declared_port("port_1")
|
||||||
self.port_1.p = self.p0
|
self.port_1.p = self.p0
|
||||||
self.port_1.h_outflow = initial_h
|
self.port_1.h_outflow = initial_h
|
||||||
@@ -978,8 +982,8 @@ class AmesimPnl0003(DynamicComponent):
|
|||||||
self.exchange_area = pi * self.diam * self.le
|
self.exchange_area = pi * self.diam * self.le
|
||||||
self.state_1 = self._initial_state(float(p1_0), float(T1_0))
|
self.state_1 = self._initial_state(float(p1_0), float(T1_0))
|
||||||
self.state_2 = self._initial_state(float(p2_0), float(T2_0))
|
self.state_2 = self._initial_state(float(p2_0), float(T2_0))
|
||||||
h1 = medium.specific_enthalpy(float(T1_0))
|
h1 = medium.specific_enthalpy_at_pressure(float(p1_0), float(T1_0))
|
||||||
h2 = medium.specific_enthalpy(float(T2_0))
|
h2 = medium.specific_enthalpy_at_pressure(float(p2_0), float(T2_0))
|
||||||
self.port_1 = self.register_declared_port("port_1")
|
self.port_1 = self.register_declared_port("port_1")
|
||||||
self.port_1.p = float(p1_0)
|
self.port_1.p = float(p1_0)
|
||||||
self.port_1.h_outflow = h1
|
self.port_1.h_outflow = h1
|
||||||
@@ -999,7 +1003,13 @@ class AmesimPnl0003(DynamicComponent):
|
|||||||
|
|
||||||
def _initial_state(self, pressure: float, temperature: float) -> VolumeState:
|
def _initial_state(self, pressure: float, temperature: float) -> VolumeState:
|
||||||
mass = self.medium.density(pressure, temperature) * self.compliance_volume
|
mass = self.medium.density(pressure, temperature) * self.compliance_volume
|
||||||
return VolumeState(m=mass, U=mass * self.medium.specific_internal_energy(temperature))
|
return VolumeState(
|
||||||
|
m=mass,
|
||||||
|
U=mass * self.medium.specific_internal_energy_at_pressure(
|
||||||
|
pressure,
|
||||||
|
temperature,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def get_state_vector(self) -> list[float]:
|
def get_state_vector(self) -> list[float]:
|
||||||
return [*self.state_1.as_vector(), *self.state_2.as_vector()]
|
return [*self.state_1.as_vector(), *self.state_2.as_vector()]
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
|
|
||||||
from app.simulation.core.medium import GasMedium, IdealGasMedium
|
from app.simulation.core.medium import (
|
||||||
|
GasMedium,
|
||||||
|
IdealGasMedium,
|
||||||
|
ThermodynamicProperties,
|
||||||
|
)
|
||||||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||||
|
|
||||||
|
|
||||||
@@ -36,18 +40,19 @@ class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
|||||||
"""AMESim helium with a Peng-Robinson mechanical equation of state.
|
"""AMESim helium with a Peng-Robinson mechanical equation of state.
|
||||||
|
|
||||||
The pressure-density-temperature relation is evaluated by the shared
|
The pressure-density-temperature relation is evaluated by the shared
|
||||||
``HELIUM_PR`` fluid. The first public AMESim port keeps the committed
|
``HELIUM_PR`` fluid. The caloric reference follows the constant NASA
|
||||||
constant-heat-capacity caloric model so it can be consumed through the
|
polynomial from Simcenter Amesim 2404 ``helium_cp_h_s.data``.
|
||||||
same :class:`GasMedium` contract as ideal-gas air.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SUBSTANCE_ID: ClassVar[str] = "helium"
|
SUBSTANCE_ID: ClassVar[str] = "helium"
|
||||||
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
|
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
|
||||||
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
|
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
|
||||||
|
nasa_cp_over_R: ClassVar[float] = 2.5
|
||||||
|
nasa_enthalpy_constant_K: ClassVar[float] = -745.375
|
||||||
|
|
||||||
name: str = "AMESimHeliumPengRobinson"
|
name: str = "AMESimHeliumPengRobinson"
|
||||||
R_gas: float = HELIUM_PR.specific_gas_constant
|
R_gas: float = HELIUM_PR.specific_gas_constant
|
||||||
cp_ref: float = 5193.0
|
cp_ref: float = nasa_cp_over_R * HELIUM_PR.specific_gas_constant
|
||||||
T_ref: float = 293.15
|
T_ref: float = 293.15
|
||||||
cp_slope: float = 0.0
|
cp_slope: float = 0.0
|
||||||
viscosity_ref: float = 1.96e-5
|
viscosity_ref: float = 1.96e-5
|
||||||
@@ -56,7 +61,7 @@ class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def cv(self) -> float:
|
def cv(self) -> float:
|
||||||
return 3116.0
|
return (self.nasa_cp_over_R - 1.0) * self.R_gas
|
||||||
|
|
||||||
def cv_at_temperature(self, T: float) -> float:
|
def cv_at_temperature(self, T: float) -> float:
|
||||||
del T
|
del T
|
||||||
@@ -65,11 +70,201 @@ class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
|||||||
def density(self, p: float, T: float) -> float:
|
def density(self, p: float, T: float) -> float:
|
||||||
return self.fluid.density(p, T)
|
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:
|
def pressure(self, m: float, T: float, V: float) -> float:
|
||||||
if V <= 0.0:
|
if V <= 0.0:
|
||||||
raise ValueError("Volume must stay positive.")
|
raise ValueError("Volume must stay positive.")
|
||||||
return self.fluid.pressure_from_density(T, m / V)
|
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)
|
@dataclass(frozen=True)
|
||||||
class AmesimGasPropertyModelSpec:
|
class AmesimGasPropertyModelSpec:
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ class AmesimPnch023(ThermodynamicVolumeComponent):
|
|||||||
self.p0 = float(p0)
|
self.p0 = float(p0)
|
||||||
self.T0 = float(T0)
|
self.T0 = float(T0)
|
||||||
m0 = medium.density(self.p0, self.T0) * self.cvol
|
m0 = medium.density(self.p0, self.T0) * self.cvol
|
||||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
||||||
self.state = VolumeState(m=m0, U=U0)
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
initial_h = medium.specific_enthalpy(self.T0)
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
||||||
self.port_1 = self.register_declared_port("port_1")
|
self.port_1 = self.register_declared_port("port_1")
|
||||||
self.port_1.p = self.p0
|
self.port_1.p = self.p0
|
||||||
self.port_1.h_outflow = initial_h
|
self.port_1.h_outflow = initial_h
|
||||||
@@ -413,9 +413,9 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
|||||||
if self.total_volume() <= 0.0:
|
if self.total_volume() <= 0.0:
|
||||||
raise ValueError("PNCH012 total volume must be positive.")
|
raise ValueError("PNCH012 total volume must be positive.")
|
||||||
m0 = medium.density(self.p0, self.T0) * self.total_volume()
|
m0 = medium.density(self.p0, self.T0) * self.total_volume()
|
||||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
U0 = m0 * medium.specific_internal_energy_at_pressure(self.p0, self.T0)
|
||||||
self.state = VolumeState(m=m0, U=U0)
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
initial_h = medium.specific_enthalpy(self.T0)
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, self.T0)
|
||||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||||
port = self.register_declared_port(port_name)
|
port = self.register_declared_port(port_name)
|
||||||
port.p = self.p0
|
port.p = self.p0
|
||||||
|
|||||||
@@ -38,16 +38,29 @@ class GasMedium(Protocol):
|
|||||||
|
|
||||||
def density(self, p: float, T: float) -> float: ...
|
def density(self, p: float, T: float) -> float: ...
|
||||||
|
|
||||||
|
def isentropic_density_pressure_factor(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
downstream_pressure: float | None = None,
|
||||||
|
) -> float: ...
|
||||||
|
|
||||||
def dynamic_viscosity(self, T: float) -> float: ...
|
def dynamic_viscosity(self, T: float) -> float: ...
|
||||||
|
|
||||||
def specific_internal_energy(self, T: float) -> float: ...
|
def specific_internal_energy(self, T: float) -> float: ...
|
||||||
|
|
||||||
|
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float: ...
|
||||||
|
|
||||||
def specific_enthalpy(self, T: float) -> float: ...
|
def specific_enthalpy(self, T: float) -> float: ...
|
||||||
|
|
||||||
|
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float: ...
|
||||||
|
|
||||||
def temperature_from_internal_energy(self, u: float) -> float: ...
|
def temperature_from_internal_energy(self, u: float) -> float: ...
|
||||||
|
|
||||||
def temperature_from_enthalpy(self, h: float) -> float: ...
|
def temperature_from_enthalpy(self, h: float) -> float: ...
|
||||||
|
|
||||||
|
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float: ...
|
||||||
|
|
||||||
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: ...
|
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: ...
|
||||||
|
|
||||||
def pressure(self, m: float, T: float, V: float) -> float: ...
|
def pressure(self, m: float, T: float, V: float) -> float: ...
|
||||||
@@ -96,6 +109,18 @@ class IdealGasMedium:
|
|||||||
def density(self, p: float, T: float) -> float:
|
def density(self, p: float, T: float) -> float:
|
||||||
return p / (self.R_gas * T)
|
return p / (self.R_gas * T)
|
||||||
|
|
||||||
|
def isentropic_density_pressure_factor(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
downstream_pressure: float | None = None,
|
||||||
|
) -> float:
|
||||||
|
del p
|
||||||
|
del downstream_pressure
|
||||||
|
cp = self.cp_at_temperature(T)
|
||||||
|
cv = self.cv_at_temperature(T)
|
||||||
|
return cv / cp
|
||||||
|
|
||||||
def dynamic_viscosity(self, T: float) -> float:
|
def dynamic_viscosity(self, T: float) -> float:
|
||||||
"""Return dynamic viscosity using the default air Sutherland law."""
|
"""Return dynamic viscosity using the default air Sutherland law."""
|
||||||
|
|
||||||
@@ -116,6 +141,10 @@ class IdealGasMedium:
|
|||||||
+ 0.5 * self.cp_slope * delta_T * delta_T
|
+ 0.5 * self.cp_slope * delta_T * delta_T
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
|
||||||
|
del p
|
||||||
|
return self.specific_internal_energy(T)
|
||||||
|
|
||||||
def specific_enthalpy(self, T: float) -> float:
|
def specific_enthalpy(self, T: float) -> float:
|
||||||
delta_T = T - self.T_ref
|
delta_T = T - self.T_ref
|
||||||
return (
|
return (
|
||||||
@@ -124,6 +153,10 @@ class IdealGasMedium:
|
|||||||
+ 0.5 * self.cp_slope * delta_T * delta_T
|
+ 0.5 * self.cp_slope * delta_T * delta_T
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
|
||||||
|
del p
|
||||||
|
return self.specific_enthalpy(T)
|
||||||
|
|
||||||
def temperature_from_internal_energy(self, u: float) -> float:
|
def temperature_from_internal_energy(self, u: float) -> float:
|
||||||
reference_internal_energy = self.cv * self.T_ref
|
reference_internal_energy = self.cv * self.T_ref
|
||||||
delta_u = u - reference_internal_energy
|
delta_u = u - reference_internal_energy
|
||||||
@@ -156,6 +189,10 @@ class IdealGasMedium:
|
|||||||
delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root
|
delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root
|
||||||
return self.T_ref + delta_T
|
return self.T_ref + delta_T
|
||||||
|
|
||||||
|
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
|
||||||
|
del p
|
||||||
|
return self.temperature_from_enthalpy(h)
|
||||||
|
|
||||||
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
||||||
if m <= 0.0:
|
if m <= 0.0:
|
||||||
raise ValueError("Mass must stay positive when recovering temperature.")
|
raise ValueError("Mass must stay positive when recovering temperature.")
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ from dataclasses import dataclass
|
|||||||
from math import acos, cos, isfinite, log, pi, sqrt
|
from math import acos, cos, isfinite, log, pi, sqrt
|
||||||
|
|
||||||
UNIVERSAL_GAS_CONSTANT = 8.31446261815324
|
UNIVERSAL_GAS_CONSTANT = 8.31446261815324
|
||||||
|
# Simcenter Amesim 2404 ``sag_reinit_eos_`` keeps more digits than the
|
||||||
|
# commonly printed Peng-Robinson constants 0.45724 and 0.07780.
|
||||||
|
PENG_ROBINSON_A_COEFFICIENT = 0.457235583
|
||||||
|
PENG_ROBINSON_B_COEFFICIENT = 0.07779607
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -29,7 +33,7 @@ class PengRobinsonFluid:
|
|||||||
@property
|
@property
|
||||||
def a_parameter(self) -> float:
|
def a_parameter(self) -> float:
|
||||||
return (
|
return (
|
||||||
0.45724
|
PENG_ROBINSON_A_COEFFICIENT
|
||||||
* UNIVERSAL_GAS_CONSTANT
|
* UNIVERSAL_GAS_CONSTANT
|
||||||
* UNIVERSAL_GAS_CONSTANT
|
* UNIVERSAL_GAS_CONSTANT
|
||||||
* self.critical_temperature
|
* self.critical_temperature
|
||||||
@@ -39,7 +43,12 @@ class PengRobinsonFluid:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def b_parameter(self) -> float:
|
def b_parameter(self) -> float:
|
||||||
return 0.07780 * UNIVERSAL_GAS_CONSTANT * self.critical_temperature / self.critical_pressure
|
return (
|
||||||
|
PENG_ROBINSON_B_COEFFICIENT
|
||||||
|
* UNIVERSAL_GAS_CONSTANT
|
||||||
|
* self.critical_temperature
|
||||||
|
/ self.critical_pressure
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def kappa(self) -> float:
|
def kappa(self) -> float:
|
||||||
@@ -62,12 +71,32 @@ class PengRobinsonFluid:
|
|||||||
/ (self.critical_temperature * sqrt_reduced_temperature)
|
/ (self.critical_temperature * sqrt_reduced_temperature)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def alpha_temperature_second_derivative(self, temperature: float) -> float:
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
reduced_temperature = temperature / self.critical_temperature
|
||||||
|
sqrt_reduced_temperature = sqrt(reduced_temperature)
|
||||||
|
alpha_base = 1.0 + self.kappa * (1.0 - sqrt_reduced_temperature)
|
||||||
|
return (
|
||||||
|
self.kappa
|
||||||
|
/ (2.0 * self.critical_temperature * self.critical_temperature)
|
||||||
|
* (
|
||||||
|
self.kappa / reduced_temperature
|
||||||
|
+ alpha_base / (reduced_temperature * sqrt_reduced_temperature)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def attractive_parameter(self, temperature: float) -> float:
|
def attractive_parameter(self, temperature: float) -> float:
|
||||||
return self.a_parameter * self.alpha(temperature)
|
return self.a_parameter * self.alpha(temperature)
|
||||||
|
|
||||||
def attractive_parameter_temperature_derivative(self, temperature: float) -> float:
|
def attractive_parameter_temperature_derivative(self, temperature: float) -> float:
|
||||||
return self.a_parameter * self.alpha_temperature_derivative(temperature)
|
return self.a_parameter * self.alpha_temperature_derivative(temperature)
|
||||||
|
|
||||||
|
def attractive_parameter_temperature_second_derivative(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
) -> float:
|
||||||
|
return self.a_parameter * self.alpha_temperature_second_derivative(temperature)
|
||||||
|
|
||||||
def pressure_from_molar_volume(self, temperature: float, molar_volume: float) -> float:
|
def pressure_from_molar_volume(self, temperature: float, molar_volume: float) -> float:
|
||||||
self._validate_temperature(temperature)
|
self._validate_temperature(temperature)
|
||||||
if molar_volume <= self.b_parameter:
|
if molar_volume <= self.b_parameter:
|
||||||
@@ -83,6 +112,51 @@ class PengRobinsonFluid:
|
|||||||
raise ValueError("Density must be positive.")
|
raise ValueError("Density must be positive.")
|
||||||
return self.pressure_from_molar_volume(temperature, self.molar_mass / density)
|
return self.pressure_from_molar_volume(temperature, self.molar_mass / density)
|
||||||
|
|
||||||
|
def pressure_temperature_derivative_at_density(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
if molar_volume <= self.b_parameter:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
b = self.b_parameter
|
||||||
|
denominator = molar_volume * (molar_volume + b) + b * (molar_volume - b)
|
||||||
|
return (
|
||||||
|
UNIVERSAL_GAS_CONSTANT / (molar_volume - b)
|
||||||
|
- self.attractive_parameter_temperature_derivative(temperature) / denominator
|
||||||
|
)
|
||||||
|
|
||||||
|
def pressure_density_derivative_at_temperature(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
if molar_volume <= self.b_parameter:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
b = self.b_parameter
|
||||||
|
denominator = molar_volume * (molar_volume + b) + b * (molar_volume - b)
|
||||||
|
pressure_molar_volume_derivative = (
|
||||||
|
-UNIVERSAL_GAS_CONSTANT * temperature / (molar_volume - b) ** 2
|
||||||
|
+ self.attractive_parameter(temperature)
|
||||||
|
* 2.0
|
||||||
|
* (molar_volume + b)
|
||||||
|
/ denominator**2
|
||||||
|
)
|
||||||
|
molar_volume_density_derivative = -self.molar_mass / (density * density)
|
||||||
|
return pressure_molar_volume_derivative * molar_volume_density_derivative
|
||||||
|
|
||||||
def reduced_parameters(self, pressure: float, temperature: float) -> tuple[float, float]:
|
def reduced_parameters(self, pressure: float, temperature: float) -> tuple[float, float]:
|
||||||
self._validate_pressure_temperature(pressure, temperature)
|
self._validate_pressure_temperature(pressure, temperature)
|
||||||
a_alpha = self.attractive_parameter(temperature)
|
a_alpha = self.attractive_parameter(temperature)
|
||||||
@@ -165,6 +239,63 @@ class PengRobinsonFluid:
|
|||||||
)
|
)
|
||||||
return residual_molar_enthalpy / self.molar_mass
|
return residual_molar_enthalpy / self.molar_mass
|
||||||
|
|
||||||
|
def residual_specific_internal_energy_at_density(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
"""Return Peng-Robinson internal-energy departure, J/kg."""
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
b = self.b_parameter
|
||||||
|
if molar_volume <= b:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
attractive = self.attractive_parameter(temperature)
|
||||||
|
d_attractive_d_temperature = (
|
||||||
|
self.attractive_parameter_temperature_derivative(temperature)
|
||||||
|
)
|
||||||
|
log_argument = (
|
||||||
|
molar_volume + (1.0 + sqrt(2.0)) * b
|
||||||
|
) / (
|
||||||
|
molar_volume + (1.0 - sqrt(2.0)) * b
|
||||||
|
)
|
||||||
|
residual_molar_internal_energy = (
|
||||||
|
temperature * d_attractive_d_temperature - attractive
|
||||||
|
) * log(log_argument) / (2.0 * sqrt(2.0) * b)
|
||||||
|
return residual_molar_internal_energy / self.molar_mass
|
||||||
|
|
||||||
|
def residual_isochoric_heat_capacity_at_density(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
"""Return the constant-volume heat-capacity departure, J/kg/K."""
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
b = self.b_parameter
|
||||||
|
if molar_volume <= b:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
log_argument = (
|
||||||
|
molar_volume + (1.0 + sqrt(2.0)) * b
|
||||||
|
) / (
|
||||||
|
molar_volume + (1.0 - sqrt(2.0)) * b
|
||||||
|
)
|
||||||
|
residual_molar_cv = (
|
||||||
|
temperature
|
||||||
|
* self.attractive_parameter_temperature_second_derivative(temperature)
|
||||||
|
* log(log_argument)
|
||||||
|
/ (2.0 * sqrt(2.0) * b)
|
||||||
|
)
|
||||||
|
return residual_molar_cv / self.molar_mass
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _validate_temperature(temperature: float) -> None:
|
def _validate_temperature(temperature: float) -> None:
|
||||||
if temperature <= 0.0:
|
if temperature <= 0.0:
|
||||||
@@ -181,7 +312,8 @@ HELIUM_PR = PengRobinsonFluid(
|
|||||||
molar_mass=0.004002602,
|
molar_mass=0.004002602,
|
||||||
critical_temperature=5.1953,
|
critical_temperature=5.1953,
|
||||||
critical_pressure=227_460.0,
|
critical_pressure=227_460.0,
|
||||||
acentric_factor=-0.385,
|
# Simcenter Amesim 2404 helium_eos.data.
|
||||||
|
acentric_factor=-0.382,
|
||||||
)
|
)
|
||||||
|
|
||||||
NITROGEN_PR = PengRobinsonFluid(
|
NITROGEN_PR = PengRobinsonFluid(
|
||||||
|
|||||||
@@ -268,8 +268,13 @@ class GenericFluidSystem:
|
|||||||
component.refresh_thermodynamic_ports()
|
component.refresh_thermodynamic_ports()
|
||||||
algebraic = self.pressure_flow_solver.solve()
|
algebraic = self.pressure_flow_solver.solve()
|
||||||
stream, connected_h = self.stream_resolver.solve()
|
stream, connected_h = self.stream_resolver.solve()
|
||||||
|
# Some constitutive flow laws recover their upstream temperature from
|
||||||
|
# the connected stream enthalpy. Stream propagation updates that
|
||||||
|
# cache after the first pressure-flow pass, so refresh explicit flows
|
||||||
|
# once more before evaluating state derivatives and result variables.
|
||||||
|
algebraic = self.pressure_flow_solver.solve()
|
||||||
self.mechanical_state_reducer.update_constraint_accelerations()
|
self.mechanical_state_reducer.update_constraint_accelerations()
|
||||||
self.algebraic_solve_count += 1 + int(bool(pneumatic_volume.propagated))
|
self.algebraic_solve_count += 2 + int(bool(pneumatic_volume.propagated))
|
||||||
self.max_algebraic_residual = max(
|
self.max_algebraic_residual = max(
|
||||||
self.max_algebraic_residual,
|
self.max_algebraic_residual,
|
||||||
algebraic.max_scaled_residual,
|
algebraic.max_scaled_residual,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from tests.test_system_xml_protocol import physical_port
|
|||||||
|
|
||||||
|
|
||||||
class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
||||||
def test_uses_shared_peng_robinson_eos_and_committed_caloric_constants(
|
def test_uses_amesim_2404_peng_robinson_and_nasa_constants(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
medium = AmesimHeliumPengRobinsonMedium()
|
medium = AmesimHeliumPengRobinsonMedium()
|
||||||
@@ -32,11 +32,11 @@ class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
|||||||
self.assertEqual(medium.SUBSTANCE_ID, "helium")
|
self.assertEqual(medium.SUBSTANCE_ID, "helium")
|
||||||
self.assertEqual(medium.PROPERTY_METHOD_ID, "peng_robinson")
|
self.assertEqual(medium.PROPERTY_METHOD_ID, "peng_robinson")
|
||||||
self.assertAlmostEqual(medium.R_gas, HELIUM_PR.specific_gas_constant)
|
self.assertAlmostEqual(medium.R_gas, HELIUM_PR.specific_gas_constant)
|
||||||
self.assertEqual(medium.cp_ref, 5193.0)
|
self.assertEqual(medium.cp_ref, 2.5 * medium.R_gas)
|
||||||
self.assertEqual(medium.cv, 3116.0)
|
self.assertEqual(medium.cv, 1.5 * medium.R_gas)
|
||||||
self.assertEqual(medium.cp_at_temperature(400.0), 5193.0)
|
self.assertEqual(medium.cp_at_temperature(400.0), 2.5 * medium.R_gas)
|
||||||
self.assertEqual(medium.cv_at_temperature(400.0), 3116.0)
|
self.assertEqual(medium.cv_at_temperature(400.0), 1.5 * medium.R_gas)
|
||||||
self.assertAlmostEqual(medium.gamma, 5193.0 / 3116.0)
|
self.assertAlmostEqual(medium.gamma, 5.0 / 3.0)
|
||||||
self.assertAlmostEqual(density, HELIUM_PR.density(pressure, temperature))
|
self.assertAlmostEqual(density, HELIUM_PR.density(pressure, temperature))
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
medium.pressure(density * volume, temperature, volume),
|
medium.pressure(density * volume, temperature, volume),
|
||||||
@@ -44,7 +44,7 @@ class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
|||||||
delta=pressure * 1.0e-12,
|
delta=pressure * 1.0e-12,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_constant_caloric_model_and_sutherland_viscosity_complete_contract(
|
def test_nasa_caloric_reference_and_sutherland_viscosity_complete_contract(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
medium = AmesimHeliumPengRobinsonMedium()
|
medium = AmesimHeliumPengRobinsonMedium()
|
||||||
@@ -53,8 +53,14 @@ class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
|||||||
internal_energy = medium.specific_internal_energy(temperature)
|
internal_energy = medium.specific_internal_energy(temperature)
|
||||||
enthalpy = medium.specific_enthalpy(temperature)
|
enthalpy = medium.specific_enthalpy(temperature)
|
||||||
|
|
||||||
self.assertEqual(internal_energy, 3116.0 * temperature)
|
self.assertEqual(
|
||||||
self.assertEqual(enthalpy, 5193.0 * temperature)
|
internal_energy,
|
||||||
|
medium.R_gas * (1.5 * temperature - 745.375),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
enthalpy,
|
||||||
|
medium.R_gas * (2.5 * temperature - 745.375),
|
||||||
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
medium.temperature_from_internal_energy(internal_energy),
|
medium.temperature_from_internal_energy(internal_energy),
|
||||||
temperature,
|
temperature,
|
||||||
@@ -66,6 +72,56 @@ class AmesimHeliumPengRobinsonMediumTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(medium.dynamic_viscosity(293.15), 1.96e-5)
|
self.assertAlmostEqual(medium.dynamic_viscosity(293.15), 1.96e-5)
|
||||||
self.assertEqual(medium.sutherland_constant, 79.4)
|
self.assertEqual(medium.sutherland_constant, 79.4)
|
||||||
|
|
||||||
|
def test_pressure_transport_enthalpy_includes_peng_robinson_departure(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
medium = AmesimHeliumPengRobinsonMedium()
|
||||||
|
pressure = 13_839_965.0
|
||||||
|
temperature = 287.7322
|
||||||
|
|
||||||
|
ideal_enthalpy = medium.specific_enthalpy(temperature)
|
||||||
|
transport_enthalpy = medium.specific_enthalpy_at_pressure(pressure, temperature)
|
||||||
|
|
||||||
|
self.assertGreater(
|
||||||
|
transport_enthalpy,
|
||||||
|
ideal_enthalpy,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
medium.temperature_from_pressure_enthalpy(pressure, transport_enthalpy),
|
||||||
|
temperature,
|
||||||
|
delta=1.0e-8,
|
||||||
|
)
|
||||||
|
|
||||||
|
density = medium.density(pressure, temperature)
|
||||||
|
volume = 0.01
|
||||||
|
properties = medium.properties_from_mU(
|
||||||
|
density * volume,
|
||||||
|
density
|
||||||
|
* volume
|
||||||
|
* medium.specific_internal_energy_at_pressure(pressure, temperature),
|
||||||
|
volume,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(properties.p, pressure, delta=pressure * 1.0e-10)
|
||||||
|
self.assertAlmostEqual(properties.T, temperature, delta=1.0e-8)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
properties.h,
|
||||||
|
transport_enthalpy,
|
||||||
|
delta=2.0e-8,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_amesim_2404_real_gas_isentropic_factor_reference(self) -> None:
|
||||||
|
medium = AmesimHeliumPengRobinsonMedium()
|
||||||
|
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
medium.isentropic_density_pressure_factor(
|
||||||
|
15_201_996.778497815,
|
||||||
|
292.3997890954483,
|
||||||
|
405_072.8123335606,
|
||||||
|
),
|
||||||
|
0.5807873871273339,
|
||||||
|
delta=1.0e-12,
|
||||||
|
)
|
||||||
|
|
||||||
def test_definition_maps_local_selector_to_amesim_fluid_and_eos_codes(
|
def test_definition_maps_local_selector_to_amesim_fluid_and_eos_codes(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ class AmesimPneumaticComponentsTest(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(ideal_reference_h, -54099.6354, delta=0.001)
|
self.assertAlmostEqual(ideal_reference_h, -54099.6354, delta=0.001)
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
pressure_reference_h - ideal_reference_h,
|
pressure_reference_h - ideal_reference_h,
|
||||||
11936.1,
|
12762.690087540526,
|
||||||
delta=0.1,
|
delta=1.0e-4,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_pressure_transport_enthalpy_restores_absolute_energy_offset(self) -> None:
|
def test_pressure_transport_enthalpy_restores_absolute_energy_offset(self) -> None:
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ class AmesimPnl0001PipeTests(unittest.TestCase):
|
|||||||
self.assertEqual(len(self.pipe.get_state_vector()), 2)
|
self.assertEqual(len(self.pipe.get_state_vector()), 2)
|
||||||
self.assertAlmostEqual(properties.p, 15.3e6, delta=1.0e-5)
|
self.assertAlmostEqual(properties.p, 15.3e6, delta=1.0e-5)
|
||||||
self.assertAlmostEqual(properties.T, 293.15)
|
self.assertAlmostEqual(properties.T, 293.15)
|
||||||
self.assertAlmostEqual(self.pipe.gas_mass_g(), 3.716965219188, places=10)
|
self.assertAlmostEqual(
|
||||||
|
self.pipe.gas_mass_g(),
|
||||||
|
self.pipe.gas.density(15.3e6, 293.15) * self.pipe.volume * 1000.0,
|
||||||
|
places=10,
|
||||||
|
)
|
||||||
|
|
||||||
def test_resistance_flow_follows_pressure_gradient(self) -> None:
|
def test_resistance_flow_follows_pressure_gradient(self) -> None:
|
||||||
forward = self.pipe.resistance_mass_flow(
|
forward = self.pipe.resistance_mass_flow(
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from app.simulation.components.amesim.flow.orifices import AmesimPnvo001FixedOpening
|
from app.simulation.components.amesim.flow.orifices import AmesimPnvo001FixedOpening
|
||||||
|
from app.simulation.components.amesim.media.mediums import (
|
||||||
|
AmesimHeliumPengRobinsonMedium,
|
||||||
|
)
|
||||||
from app.simulation.core.medium import IdealGasMedium
|
from app.simulation.core.medium import IdealGasMedium
|
||||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||||
|
|
||||||
@@ -61,6 +64,86 @@ class AmesimPnvo001FixedOpeningComponentTests(unittest.TestCase):
|
|||||||
self.assertLess(reverse, 0.0)
|
self.assertLess(reverse, 0.0)
|
||||||
self.assertAlmostEqual(forward, -reverse)
|
self.assertAlmostEqual(forward, -reverse)
|
||||||
|
|
||||||
|
def test_mass_flow_uses_connected_enthalpy_from_the_upstream_side(self) -> None:
|
||||||
|
valve = AmesimPnvo001FixedOpening("valve_1", self.medium, opening=0.5)
|
||||||
|
hot_h = self.medium.specific_enthalpy(600.0)
|
||||||
|
cold_h = self.medium.specific_enthalpy(200.0)
|
||||||
|
|
||||||
|
valve.update_stream_outflows({"port_2": hot_h, "port_3": cold_h})
|
||||||
|
|
||||||
|
self.assertEqual(valve.port_2.h_outflow, cold_h)
|
||||||
|
self.assertEqual(valve.port_3.h_outflow, hot_h)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
valve.mass_flow(500000.0, 100000.0),
|
||||||
|
valve._one_way_mass_flow(
|
||||||
|
upstream_pressure=500000.0,
|
||||||
|
downstream_pressure=100000.0,
|
||||||
|
upstream_temperature=600.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
valve.mass_flow(100000.0, 500000.0),
|
||||||
|
-valve._one_way_mass_flow(
|
||||||
|
upstream_pressure=500000.0,
|
||||||
|
downstream_pressure=100000.0,
|
||||||
|
upstream_temperature=200.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_helium_flow_uses_pressure_enthalpy_and_real_gas_factor(self) -> None:
|
||||||
|
medium = AmesimHeliumPengRobinsonMedium()
|
||||||
|
valve = AmesimPnvo001FixedOpening(
|
||||||
|
"valve_1",
|
||||||
|
medium,
|
||||||
|
cq=0.45,
|
||||||
|
area0=78.5e-6,
|
||||||
|
opening=1.0,
|
||||||
|
)
|
||||||
|
upstream_pressure = 15_201_996.778497815
|
||||||
|
downstream_pressure = 405_072.8123335606
|
||||||
|
upstream_temperature = 292.3997890954483
|
||||||
|
valve.port_2.p = upstream_pressure
|
||||||
|
valve.port_3.p = downstream_pressure
|
||||||
|
upstream_enthalpy = medium.specific_enthalpy_at_pressure(
|
||||||
|
upstream_pressure,
|
||||||
|
upstream_temperature,
|
||||||
|
)
|
||||||
|
valve.update_stream_outflows(
|
||||||
|
{
|
||||||
|
"port_2": upstream_enthalpy,
|
||||||
|
"port_3": medium.specific_enthalpy_at_pressure(
|
||||||
|
downstream_pressure,
|
||||||
|
393.46713105173205,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertLess(upstream_enthalpy, 0.0)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
valve._upstream_temperature("port_2"),
|
||||||
|
upstream_temperature,
|
||||||
|
delta=1.0e-8,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
valve.mass_flow(upstream_pressure, downstream_pressure),
|
||||||
|
0.49551308906777447,
|
||||||
|
delta=1.0e-9,
|
||||||
|
)
|
||||||
|
results = valve.component_result_values()
|
||||||
|
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
results["cm"],
|
||||||
|
0.015778323343746598,
|
||||||
|
delta=1.0e-11,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
results["gasvel"],
|
||||||
|
894.7011595213406,
|
||||||
|
delta=1.0e-8,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -19,6 +19,11 @@ class PengRobinsonTest(unittest.TestCase):
|
|||||||
density = HELIUM_PR.density(pressure, temperature)
|
density = HELIUM_PR.density(pressure, temperature)
|
||||||
|
|
||||||
self.assertGreater(HELIUM_PR.compressibility_factor(pressure, temperature), 1.0)
|
self.assertGreater(HELIUM_PR.compressibility_factor(pressure, temperature), 1.0)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
density,
|
||||||
|
24.114225444477153,
|
||||||
|
delta=1.0e-12,
|
||||||
|
)
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
HELIUM_PR.pressure_from_density(temperature, density),
|
HELIUM_PR.pressure_from_density(temperature, density),
|
||||||
pressure,
|
pressure,
|
||||||
@@ -28,17 +33,43 @@ class PengRobinsonTest(unittest.TestCase):
|
|||||||
def test_helium_residual_enthalpy_is_small_at_atmosphere(self) -> None:
|
def test_helium_residual_enthalpy_is_small_at_atmosphere(self) -> None:
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
HELIUM_PR.residual_specific_enthalpy(101_300.0, 298.15),
|
HELIUM_PR.residual_specific_enthalpy(101_300.0, 298.15),
|
||||||
17.834,
|
25.3556466754,
|
||||||
delta=0.01,
|
delta=0.01,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_helium_residual_enthalpy_captures_high_pressure_departure(self) -> None:
|
def test_helium_residual_enthalpy_captures_high_pressure_departure(self) -> None:
|
||||||
self.assertAlmostEqual(
|
self.assertAlmostEqual(
|
||||||
HELIUM_PR.residual_specific_enthalpy(13_839_965.0, 287.7322),
|
HELIUM_PR.residual_specific_enthalpy(13_839_965.0, 287.7322),
|
||||||
11953.9,
|
12788.0457342,
|
||||||
delta=0.1,
|
delta=0.1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_residual_isochoric_heat_capacity_is_internal_energy_derivative(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
temperature = 292.3997890954483
|
||||||
|
density = 24.02697515640726
|
||||||
|
temperature_step = 1.0e-3
|
||||||
|
numerical_derivative = (
|
||||||
|
HELIUM_PR.residual_specific_internal_energy_at_density(
|
||||||
|
temperature + temperature_step,
|
||||||
|
density,
|
||||||
|
)
|
||||||
|
- HELIUM_PR.residual_specific_internal_energy_at_density(
|
||||||
|
temperature - temperature_step,
|
||||||
|
density,
|
||||||
|
)
|
||||||
|
) / (2.0 * temperature_step)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
HELIUM_PR.residual_isochoric_heat_capacity_at_density(
|
||||||
|
temperature,
|
||||||
|
density,
|
||||||
|
),
|
||||||
|
numerical_derivative,
|
||||||
|
delta=1.0e-6,
|
||||||
|
)
|
||||||
|
|
||||||
def test_air_reference_remains_available_for_other_models(self) -> None:
|
def test_air_reference_remains_available_for_other_models(self) -> None:
|
||||||
z = AIR_PR.compressibility_factor(101_325.0, 300.0)
|
z = AIR_PR.compressibility_factor(101_325.0, 300.0)
|
||||||
density = AIR_PR.density(101_325.0, 300.0)
|
density = AIR_PR.density(101_325.0, 300.0)
|
||||||
@@ -60,7 +91,7 @@ class PengRobinsonTest(unittest.TestCase):
|
|||||||
molar_mass=0.004002602,
|
molar_mass=0.004002602,
|
||||||
critical_temperature=5.1953,
|
critical_temperature=5.1953,
|
||||||
critical_pressure=227_460.0,
|
critical_pressure=227_460.0,
|
||||||
acentric_factor=-0.385,
|
acentric_factor=-0.382,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertAlmostEqual(fluid.specific_gas_constant, 2077.3, delta=0.5)
|
self.assertAlmostEqual(fluid.specific_gas_constant, 2077.3, delta=0.5)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from app.simulation.components.experimental.storage.tank import Tank
|
|||||||
from app.simulation.core.medium import IdealGasMedium
|
from app.simulation.core.medium import IdealGasMedium
|
||||||
from app.simulation.core.state import VolumeState
|
from app.simulation.core.state import VolumeState
|
||||||
from app.simulation.solvers.algebraic import AlgebraicSolveError, PressureFlowSolver
|
from app.simulation.solvers.algebraic import AlgebraicSolveError, PressureFlowSolver
|
||||||
|
from app.simulation.systems.generic import GenericFluidSystem
|
||||||
from app.simulation.systems.network import SimulationNetwork
|
from app.simulation.systems.network import SimulationNetwork
|
||||||
|
|
||||||
|
|
||||||
@@ -59,10 +60,40 @@ class PressureFlowSolverInitializationTests(unittest.TestCase):
|
|||||||
mass = medium.density(pressure, temperature) * component.V
|
mass = medium.density(pressure, temperature) * component.V
|
||||||
component.state = VolumeState(
|
component.state = VolumeState(
|
||||||
m=mass,
|
m=mass,
|
||||||
U=mass * medium.specific_internal_energy(temperature),
|
U=mass
|
||||||
|
* medium.specific_internal_energy_at_pressure(
|
||||||
|
pressure,
|
||||||
|
temperature,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
component.refresh_thermodynamic_ports()
|
component.refresh_thermodynamic_ports()
|
||||||
|
|
||||||
|
def test_stream_enthalpy_refreshes_explicit_orifice_flow(self) -> None:
|
||||||
|
network, medium, high, low, valve = self._near_equal_pressure_network()
|
||||||
|
self._set_pressure_temperature(
|
||||||
|
high,
|
||||||
|
medium,
|
||||||
|
15_201_996.778497815,
|
||||||
|
292.3997890954483,
|
||||||
|
)
|
||||||
|
self._set_pressure_temperature(
|
||||||
|
low,
|
||||||
|
medium,
|
||||||
|
405_072.8123335606,
|
||||||
|
393.46713105173205,
|
||||||
|
)
|
||||||
|
|
||||||
|
system = GenericFluidSystem(network)
|
||||||
|
system.consistent_initial_state_vector()
|
||||||
|
|
||||||
|
self.assertLess(valve._connected_h["port_2"], 0.0)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
valve.port_2.m_flow,
|
||||||
|
valve.mass_flow(valve.port_2.p, valve.port_3.p),
|
||||||
|
places=12,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(valve.port_3.m_flow, -valve.port_2.m_flow, places=12)
|
||||||
|
|
||||||
def test_current_storage_pressure_reseeds_stale_orifice_ports_and_flow(self) -> None:
|
def test_current_storage_pressure_reseeds_stale_orifice_ports_and_flow(self) -> None:
|
||||||
network, medium, high, low, valve = self._near_equal_pressure_network()
|
network, medium, high, low, valve = self._near_equal_pressure_network()
|
||||||
solver = PressureFlowSolver(network, max_evaluations=10)
|
solver = PressureFlowSolver(network, max_evaluations=10)
|
||||||
|
|||||||
Reference in new issue
Block a user