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
4 changed files with 201 additions and 21 deletions
Showing only changes of commit 6f34ac16bf - Show all commits

No files matched your search

+10 -2
View File
@@ -94,6 +94,14 @@
- `tests/test_test_mql_mechanical.py`
- 保护机械组件数量、活塞面积、质量端止动参数、载荷质量行程限制、弹性端止动参数、机械节点端口数和单位换算。
- `PythonModels/components/amesim_mechanical.py`
- 提供 AMESim 机械侧小原语,当前覆盖 `PNRP17` 活塞腔几何和 `LSTP00A` 弹性端止动接触力。
- 活塞腔长度、体积、体积变化率已用 `length@pn_brp2_8`、`vol1@pn_brp2_8`、`vvol1@pn_brp2_8` 的 AMESim 样本校验。
- 端止动接触力已按 `gap@elasticendstop_8`、`f1@elasticendstop_8` 和阻尼速度样本校验。
- `tests/test_amesim_mechanical_components.py`
- 保护机械原语单位换算、活塞几何、活塞体积变化率和弹性端止动接触力。
## 物性约定
AMESim 模型中 `test_mql` 使用氦气,Python 侧当前通过 `HELIUM_PR` 使用 Peng-Robinson 状态方程计算气体压缩因子和密度。当前物性层先覆盖状态方程相关量,完整焓/内能偏差函数后续在接气室能量方程时再补。
@@ -140,12 +148,12 @@ AMESim 模型中 `test_mql` 使用氦气,Python 侧当前通过 `HELIUM_PR`
## 验证方式
```bash
python3 -m py_compile PythonModels/components/amesim_pneumatic.py PythonModels/core/peng_robinson.py PythonModels/reporting/amesim_results.py PythonModels/reporting/test_mql_comparison.py PythonModels/reporting/test_mql_variables.py PythonModels/systems/test_mql.py PythonModels/systems/test_mql_config.py PythonModels/systems/test_mql_lines.py PythonModels/systems/test_mql_mechanical.py PythonModels/systems/test_mql_pneumatic.py PythonModels/scripts/run_test_mql.py
python3 -m py_compile PythonModels/components/amesim_mechanical.py PythonModels/components/amesim_pneumatic.py PythonModels/core/peng_robinson.py PythonModels/reporting/amesim_results.py PythonModels/reporting/test_mql_comparison.py PythonModels/reporting/test_mql_variables.py PythonModels/systems/test_mql.py PythonModels/systems/test_mql_config.py PythonModels/systems/test_mql_lines.py PythonModels/systems/test_mql_mechanical.py PythonModels/systems/test_mql_pneumatic.py PythonModels/scripts/run_test_mql.py
python3 -m PythonModels.scripts.run_test_mql
python3 -m unittest discover -s tests -t .
```
当前测试覆盖的是结构、配置解析、气动原语、气动件装配、管路连接装配、机械装配、AMESim 结果读取、变量目录和 AMESim/Python 时序对齐工具,不代表已经完成 AMESim 物理结果复刻。
当前测试覆盖的是结构、配置解析、气动原语、机械原语、气动件装配、管路连接装配、机械装配、AMESim 结果读取、变量目录和 AMESim/Python 时序对齐工具,不代表已经完成 AMESim 物理结果复刻。
## 后续方向
@@ -0,0 +1,94 @@
from __future__ import annotations
from dataclasses import dataclass
from math import pi
MM_TO_M = 1.0e-3
M_TO_MM = 1.0e3
M3_TO_CM3 = 1.0e6
M3_PER_S_TO_L_PER_MIN = 60_000.0
def circular_area(diameter_m: float) -> float:
if diameter_m < 0.0:
raise ValueError("diameter_m must be non-negative.")
return pi * diameter_m * diameter_m / 4.0
def mm_to_m(value: float) -> float:
return value * MM_TO_M
def m_to_mm(value: float) -> float:
return value * M_TO_MM
@dataclass(frozen=True)
class AmesimPistonGeometry:
"""Geometry relations used by AMESim PNRP17 pneumatic piston variables."""
piston_diameter_m: float
rod_diameter_m: float = 0.0
zero_length_m: float = 0.0
@property
def piston_area_m2(self) -> float:
return circular_area(self.piston_diameter_m)
@property
def rod_area_m2(self) -> float:
return circular_area(self.rod_diameter_m)
@property
def annulus_area_m2(self) -> float:
return self.piston_area_m2 - self.rod_area_m2
def chamber_length_m(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return self.zero_length_m + port5_displacement_m - port4_displacement_m
def chamber_length_mm(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return m_to_mm(self.chamber_length_m(port4_displacement_m, port5_displacement_m))
@property
def chamber_area_m2(self) -> float:
return self.annulus_area_m2
def chamber_volume_m3(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return self.chamber_area_m2 * self.chamber_length_m(
port4_displacement_m,
port5_displacement_m,
)
def chamber_volume_cm3(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
return self.chamber_volume_m3(port4_displacement_m, port5_displacement_m) * M3_TO_CM3
def chamber_volume_rate_m3_s(self, port4_velocity_m_s: float, port5_velocity_m_s: float) -> float:
return self.chamber_area_m2 * (port5_velocity_m_s - port4_velocity_m_s)
def chamber_volume_rate_l_min(self, port4_velocity_m_s: float, port5_velocity_m_s: float) -> float:
return self.chamber_volume_rate_m3_s(
port4_velocity_m_s,
port5_velocity_m_s,
) * M3_PER_S_TO_L_PER_MIN
@dataclass(frozen=True)
class AmesimElasticEndstop:
"""Contact force part of AMESim LSTP00A elastic endstop."""
contact_stiffness_n_per_m: float
contact_damping_n_per_m_per_s: float = 0.0
gap0_m: float = 0.0
def penetration_m_from_gap_mm(self, gap_mm: float) -> float:
return max(-(mm_to_m(gap_mm) - self.gap0_m), 0.0)
def static_contact_force(self, gap_mm: float) -> float:
return self.contact_stiffness_n_per_m * self.penetration_m_from_gap_mm(gap_mm)
def contact_force(self, gap_mm: float, penetration_velocity_m_s: float = 0.0) -> float:
if self.penetration_m_from_gap_mm(gap_mm) <= 0.0:
return 0.0
damping_force = self.contact_damping_n_per_m_per_s * penetration_velocity_m_s
return max(self.static_contact_force(gap_mm) + damping_force, 0.0)
+16 -19
View File
@@ -1,8 +1,12 @@
from __future__ import annotations
from dataclasses import dataclass
from math import pi
from PythonModels.components.amesim_mechanical import (
AmesimPistonGeometry,
circular_area,
mm_to_m,
)
from PythonModels.reporting.amesim_results import AmesimResults
from PythonModels.reporting.test_mql_variables import (
TestMqlVariableCatalog,
@@ -138,18 +142,19 @@ def _build_piston(
component: TestMqlResolvedComponent,
variable_catalog: TestMqlVariableCatalog | None,
) -> TestMqlPistonSpec:
piston_diameter_m = mm_to_m(component.parameter_value("dp"))
rod_diameter_m = mm_to_m(component.parameter_value("dr"))
piston_area_m2 = circular_area(piston_diameter_m)
rod_area_m2 = circular_area(rod_diameter_m)
geometry = AmesimPistonGeometry(
piston_diameter_m=mm_to_m(component.parameter_value("dp")),
rod_diameter_m=mm_to_m(component.parameter_value("dr")),
zero_length_m=mm_to_m(component.parameter_value("x0")),
)
return TestMqlPistonSpec(
alias=component.alias,
piston_diameter_m=piston_diameter_m,
rod_diameter_m=rod_diameter_m,
zero_displacement_m=mm_to_m(component.parameter_value("x0")),
piston_area_m2=piston_area_m2,
rod_area_m2=rod_area_m2,
annulus_area_m2=piston_area_m2 - rod_area_m2,
piston_diameter_m=geometry.piston_diameter_m,
rod_diameter_m=geometry.rod_diameter_m,
zero_displacement_m=geometry.zero_length_m,
piston_area_m2=geometry.piston_area_m2,
rod_area_m2=geometry.rod_area_m2,
annulus_area_m2=geometry.annulus_area_m2,
data_paths=_data_paths(variable_catalog, component.alias),
)
@@ -201,14 +206,6 @@ def _build_mechanical_node(
)
def circular_area(diameter_m: float) -> float:
return pi * diameter_m * diameter_m / 4.0
def mm_to_m(value: float) -> float:
return value * MM_TO_M
def n_per_mm_to_n_per_m(value: float) -> float:
return value * N_PER_MM_TO_N_PER_M
@@ -0,0 +1,81 @@
from __future__ import annotations
import unittest
from pathlib import Path
from PythonModels.components.amesim_mechanical import (
AmesimElasticEndstop,
AmesimPistonGeometry,
circular_area,
m_to_mm,
mm_to_m,
)
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
REPO_ROOT = Path(__file__).resolve().parents[1]
TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame"
class AmesimMechanicalComponentsTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.amesim_results = load_test_mql_amesim_results(TEST_MQL_AME)
def test_unit_helpers_and_area(self) -> None:
self.assertAlmostEqual(mm_to_m(200.0), 0.2)
self.assertAlmostEqual(m_to_mm(0.37), 370.0)
self.assertAlmostEqual(circular_area(0.2), 0.031415926535897934)
def test_piston_geometry_matches_amesim_length_and_volume(self) -> None:
geometry = AmesimPistonGeometry(piston_diameter_m=0.2, rod_diameter_m=0.001)
index = -1
x4 = self.amesim_results.series("x4@pn_brp2_8")[index]
x5 = self.amesim_results.series("x5@pn_brp2_8")[index]
self.assertAlmostEqual(
geometry.chamber_length_mm(x4, x5),
self.amesim_results.series("length@pn_brp2_8")[index],
)
self.assertAlmostEqual(
geometry.chamber_volume_cm3(x4, x5),
self.amesim_results.series("vol1@pn_brp2_8")[index],
delta=1.0e-3,
)
def test_piston_geometry_matches_amesim_volume_rate(self) -> None:
geometry = AmesimPistonGeometry(piston_diameter_m=0.2, rod_diameter_m=0.001)
index = 100
v4 = self.amesim_results.series("v4@pn_brp2_8")[index]
v5 = self.amesim_results.series("v5@pn_brp2_8")[index]
self.assertAlmostEqual(
geometry.chamber_volume_rate_l_min(v4, v5),
self.amesim_results.series("vvol1@pn_brp2_8")[index],
delta=1.0e-8,
)
def test_elastic_endstop_contact_force_matches_amesim_final_sample(self) -> None:
endstop = AmesimElasticEndstop(
contact_stiffness_n_per_m=1.0e11,
contact_damping_n_per_m_per_s=1.0e11,
)
index = -1
gap_mm = self.amesim_results.series("gap@elasticendstop_8")[index]
penetration_velocity_m_s = self.amesim_results.series("v5@pn_brp2_8")[index]
self.assertAlmostEqual(
endstop.contact_force(gap_mm, penetration_velocity_m_s),
self.amesim_results.series("f1@elasticendstop_8")[index],
delta=1.0e-3,
)
def test_elastic_endstop_returns_zero_before_contact(self) -> None:
endstop = AmesimElasticEndstop(contact_stiffness_n_per_m=1.0e11)
self.assertEqual(endstop.static_contact_force(0.1), 0.0)
self.assertEqual(endstop.penetration_m_from_gap_mm(0.1), 0.0)
if __name__ == "__main__":
unittest.main()