merge/model-development-into-main #2

Merged
lujingze merged 126 commits from merge/model-development-into-main into main 2026-07-31 09:52:44 +08:00
3 changed files with 314 additions and 1 deletions
Showing only changes of commit a598fcd5c3 - Show all commits

No files matched your search

+6 -1
View File
@@ -42,6 +42,11 @@
- 当前覆盖压缩因子、摩尔体积、密度和由密度反算压力。
- 已内置 `HELIUM_PR`,供 `test_mql` 默认使用。
- `PythonModels/components/amesim_pneumatic.py`
- 提供 `test_mql` 后续会用到的 AMESim 气动组件原语。
- 当前包含氦气 PR 压力闭合的气动容腔、标准可压缩孔口流量、单位换算 helper。
- 这层仍是首版近似原语,后续必须通过 AMESim CSV 对齐修正。
## 物性约定
AMESim 模型中 `test_mql` 使用氦气,Python 侧当前通过 `HELIUM_PR` 使用 Peng-Robinson 状态方程计算气体压缩因子和密度。当前物性层先覆盖状态方程相关量,完整焓/内能偏差函数后续在接气室能量方程时再补。
@@ -88,7 +93,7 @@ AMESim 模型中 `test_mql` 使用氦气,Python 侧当前通过 `HELIUM_PR`
## 验证方式
```bash
python3 -m py_compile PythonModels/core/peng_robinson.py PythonModels/systems/test_mql.py PythonModels/systems/test_mql_config.py PythonModels/scripts/run_test_mql.py
python3 -m py_compile PythonModels/components/amesim_pneumatic.py PythonModels/core/peng_robinson.py PythonModels/systems/test_mql.py PythonModels/systems/test_mql_config.py PythonModels/scripts/run_test_mql.py
python3 -m PythonModels.scripts.run_test_mql
python3 -m unittest discover -s tests -t .
```
+216
View File
@@ -0,0 +1,216 @@
from __future__ import annotations
from dataclasses import dataclass
from math import pi, sqrt
from PythonModels.core.base import AlgebraicComponent, DynamicComponent
from PythonModels.core.medium import ThermodynamicProperties
from PythonModels.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
from PythonModels.core.ports import PortState
from PythonModels.core.state import VolumeState
@dataclass(frozen=True)
class AmesimPneumaticGas:
"""Caloric constants plus Peng-Robinson EOS for AMESim pneumatic components."""
fluid: PengRobinsonFluid = HELIUM_PR
cp: float = 5193.0
cv: float = 3116.0
@property
def gamma(self) -> float:
return self.cp / self.cv
@property
def R_gas(self) -> float:
return self.fluid.specific_gas_constant
def density(self, pressure: float, temperature: float) -> float:
return self.fluid.density(pressure, temperature)
def pressure(self, density: float, temperature: float) -> float:
return self.fluid.pressure_from_density(temperature, density)
def specific_internal_energy(self, temperature: float) -> float:
return self.cv * temperature
def specific_enthalpy(self, temperature: float) -> float:
return self.cp * temperature
def temperature_from_internal_energy(self, specific_internal_energy: float) -> float:
if self.cv <= 0.0:
raise ValueError("cv must be positive.")
return specific_internal_energy / self.cv
HELIUM_PNEUMATIC_GAS = AmesimPneumaticGas()
def liters_to_m3(value: float) -> float:
return value * 1.0e-3
def mm2_to_m2(value: float) -> float:
return value * 1.0e-6
def diameter_mm_to_area_m2(diameter_mm: float) -> float:
diameter_m = diameter_mm * 1.0e-3
return pi * diameter_m * diameter_m / 4.0
class AmesimPneumaticVolume(DynamicComponent):
"""First-pass AMESim pneumatic control volume using helium PR pressure closure."""
def __init__(
self,
name: str,
volume: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
) -> None:
if volume <= 0.0:
raise ValueError("volume must be positive.")
super().__init__(name=name)
self.volume = volume
self.gas = gas
rho0 = gas.density(p0, T0)
m0 = rho0 * volume
U0 = m0 * gas.specific_internal_energy(T0)
self.state = VolumeState(m=m0, U=U0)
self.port_a = PortState()
@classmethod
def from_liters(
cls,
name: str,
volume_liters: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
p0: float = 101_325.0,
T0: float = 293.15,
) -> "AmesimPneumaticVolume":
return cls(name=name, volume=liters_to_m3(volume_liters), gas=gas, p0=p0, T0=T0)
def get_state_vector(self) -> list[float]:
return self.state.as_vector()
def set_state_vector(self, values: list[float]) -> None:
self.state = VolumeState.from_vector(values)
def properties(self) -> ThermodynamicProperties:
if self.state.m <= 0.0:
raise ValueError("volume mass must stay positive.")
T = self.gas.temperature_from_internal_energy(self.state.U / self.state.m)
rho = self.state.m / self.volume
p = self.gas.pressure(rho, T)
u = self.state.U / self.state.m
h = self.gas.specific_enthalpy(T)
self.port_a.p = p
self.port_a.h_outflow = h
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
return VolumeState(m=m_flow, U=m_flow * inlet_h)
class AmesimPneumaticOrifice(AlgebraicComponent):
"""First-pass PNOR001/PNVO001-style compressible helium orifice.
This is a calibrated placeholder boundary for the Python port. It preserves
AMESim-style area and coefficient inputs, but final parity must be checked
against AMESim CSV results before treating it as numerically equivalent.
"""
def __init__(
self,
name: str,
area: float,
flow_coefficient: float = 1.0,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
opening: float = 1.0,
) -> None:
if area < 0.0:
raise ValueError("area must be non-negative.")
if flow_coefficient < 0.0:
raise ValueError("flow_coefficient must be non-negative.")
super().__init__(name=name)
self.area = area
self.flow_coefficient = flow_coefficient
self.gas = gas
self.opening = opening
self.port_a = PortState()
self.port_b = PortState()
@classmethod
def from_mm2(
cls,
name: str,
area_mm2: float,
flow_coefficient: float = 1.0,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
opening: float = 1.0,
) -> "AmesimPneumaticOrifice":
return cls(
name=name,
area=mm2_to_m2(area_mm2),
flow_coefficient=flow_coefficient,
gas=gas,
opening=opening,
)
@property
def effective_area(self) -> float:
return self.area * max(self.opening, 0.0)
def mass_flow(self, p_a: float, p_b: float, upstream_temperature: float) -> float:
if p_a == p_b or self.effective_area == 0.0 or self.flow_coefficient == 0.0:
return 0.0
if p_a > p_b:
return compressible_orifice_mass_flow(
upstream_pressure=p_a,
downstream_pressure=p_b,
upstream_temperature=upstream_temperature,
area=self.effective_area,
flow_coefficient=self.flow_coefficient,
gas=self.gas,
)
return -compressible_orifice_mass_flow(
upstream_pressure=p_b,
downstream_pressure=p_a,
upstream_temperature=upstream_temperature,
area=self.effective_area,
flow_coefficient=self.flow_coefficient,
gas=self.gas,
)
def compressible_orifice_mass_flow(
*,
upstream_pressure: float,
downstream_pressure: float,
upstream_temperature: float,
area: float,
flow_coefficient: float,
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
) -> float:
if upstream_pressure <= 0.0 or downstream_pressure < 0.0:
raise ValueError("pressures must be non-negative and upstream pressure must be positive.")
if upstream_temperature <= 0.0:
raise ValueError("upstream_temperature must be positive.")
if area < 0.0 or flow_coefficient < 0.0:
raise ValueError("area and flow_coefficient must be non-negative.")
if downstream_pressure >= upstream_pressure or area == 0.0 or flow_coefficient == 0.0:
return 0.0
gamma = gas.gamma
pressure_ratio = max(downstream_pressure / upstream_pressure, 0.0)
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
coefficient = flow_coefficient * area * upstream_pressure / sqrt(gas.R_gas * upstream_temperature)
if pressure_ratio <= critical_ratio:
flow_function = sqrt(gamma) * (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
else:
term = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** ((gamma + 1.0) / gamma)
flow_function = sqrt((2.0 * gamma / (gamma - 1.0)) * max(term, 0.0))
return coefficient * flow_function
+92
View File
@@ -0,0 +1,92 @@
from __future__ import annotations
import unittest
from PythonModels.components.amesim_pneumatic import (
HELIUM_PNEUMATIC_GAS,
AmesimPneumaticOrifice,
AmesimPneumaticVolume,
compressible_orifice_mass_flow,
diameter_mm_to_area_m2,
liters_to_m3,
mm2_to_m2,
)
class AmesimPneumaticComponentsTest(unittest.TestCase):
def test_unit_conversions(self) -> None:
self.assertAlmostEqual(liters_to_m3(15.0), 0.015)
self.assertAlmostEqual(mm2_to_m2(78.5), 78.5e-6)
self.assertAlmostEqual(diameter_mm_to_area_m2(10.0), 7.853981633974483e-5)
def test_volume_initial_state_matches_requested_pressure_temperature(self) -> None:
volume = AmesimPneumaticVolume.from_liters(
name="pn_general_chamber",
volume_liters=57.0,
p0=15.3e6,
T0=293.15,
)
props = volume.properties()
self.assertAlmostEqual(props.p, 15.3e6, delta=15.3e6 * 1.0e-12)
self.assertAlmostEqual(props.T, 293.15)
self.assertGreater(props.rho, 20.0)
self.assertLess(props.rho, 30.0)
def test_orifice_returns_signed_mass_flow(self) -> None:
orifice = AmesimPneumaticOrifice.from_mm2(
name="pn_orifice_18",
area_mm2=78.5,
flow_coefficient=0.9,
)
forward = orifice.mass_flow(15.3e6, 1.0e6, 293.15)
reverse = orifice.mass_flow(1.0e6, 15.3e6, 293.15)
self.assertGreater(forward, 0.0)
self.assertAlmostEqual(reverse, -forward)
self.assertEqual(orifice.mass_flow(1.0e6, 1.0e6, 293.15), 0.0)
def test_choked_flow_is_independent_of_lower_downstream_pressure(self) -> None:
base = compressible_orifice_mass_flow(
upstream_pressure=15.3e6,
downstream_pressure=1.0e6,
upstream_temperature=293.15,
area=78.5e-6,
flow_coefficient=0.9,
gas=HELIUM_PNEUMATIC_GAS,
)
lower_back_pressure = compressible_orifice_mass_flow(
upstream_pressure=15.3e6,
downstream_pressure=0.1e6,
upstream_temperature=293.15,
area=78.5e-6,
flow_coefficient=0.9,
gas=HELIUM_PNEUMATIC_GAS,
)
self.assertGreater(base, 0.0)
self.assertAlmostEqual(lower_back_pressure, base)
def test_subcritical_flow_decreases_as_back_pressure_rises(self) -> None:
low_back_pressure = compressible_orifice_mass_flow(
upstream_pressure=1.0e6,
downstream_pressure=0.6e6,
upstream_temperature=293.15,
area=78.5e-6,
flow_coefficient=0.9,
)
high_back_pressure = compressible_orifice_mass_flow(
upstream_pressure=1.0e6,
downstream_pressure=0.9e6,
upstream_temperature=293.15,
area=78.5e-6,
flow_coefficient=0.9,
)
self.assertGreater(low_back_pressure, high_back_pressure)
self.assertGreater(high_back_pressure, 0.0)
if __name__ == "__main__":
unittest.main()