From 6f34ac16bf6098ec2100d6c48aabd100e60725ab Mon Sep 17 00:00:00 2001 From: huojiarong Date: Wed, 15 Jul 2026 10:02:53 +0000 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85test=5Fmql=E6=9C=BA=E6=A2=B0?= =?UTF-8?q?=E5=8E=9F=E8=AF=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AmesimModels/test_mql/README.md | 12 ++- PythonModels/components/amesim_mechanical.py | 94 ++++++++++++++++++++ PythonModels/systems/test_mql_mechanical.py | 35 ++++---- tests/test_amesim_mechanical_components.py | 81 +++++++++++++++++ 4 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 PythonModels/components/amesim_mechanical.py create mode 100644 tests/test_amesim_mechanical_components.py diff --git a/AmesimModels/test_mql/README.md b/AmesimModels/test_mql/README.md index 30bd77a..0ee0adc 100644 --- a/AmesimModels/test_mql/README.md +++ b/AmesimModels/test_mql/README.md @@ -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 物理结果复刻。 ## 后续方向 diff --git a/PythonModels/components/amesim_mechanical.py b/PythonModels/components/amesim_mechanical.py new file mode 100644 index 0000000..b294a33 --- /dev/null +++ b/PythonModels/components/amesim_mechanical.py @@ -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) diff --git a/PythonModels/systems/test_mql_mechanical.py b/PythonModels/systems/test_mql_mechanical.py index b555931..b1dbf93 100644 --- a/PythonModels/systems/test_mql_mechanical.py +++ b/PythonModels/systems/test_mql_mechanical.py @@ -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 diff --git a/tests/test_amesim_mechanical_components.py b/tests/test_amesim_mechanical_components.py new file mode 100644 index 0000000..dab4563 --- /dev/null +++ b/tests/test_amesim_mechanical_components.py @@ -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()