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 338 additions and 2 deletions
Showing only changes of commit 8f14a3840f - Show all commits

No files matched your search

+10 -2
View File
@@ -86,6 +86,14 @@
- `tests/test_test_mql_lines.py`
- 保护管路数量、拓扑端口、管路模式、典型 `Data_Path` 和子模型分组。
- `PythonModels/systems/test_mql_mechanical.py`
- 把 AMESim 配置中的 `PNRP17`、`MECMAS21`、`LSTP00A`、`LMECHN1` 等机械相关组件装配成 Python 参数目录。
- 当前覆盖 8 个气动活塞、10 个质量/端止动、8 个弹性端止动、2 个机械节点、16 个零力源和 2 个力连接器。
- 保留 AMESim alias 和对应 `Data_Path`,并把直径、刚度、阻尼、穿透距离等参数换算到 SI 单位。
- `tests/test_test_mql_mechanical.py`
- 保护机械组件数量、活塞面积、质量端止动参数、载荷质量行程限制、弹性端止动参数、机械节点端口数和单位换算。
## 物性约定
AMESim 模型中 `test_mql` 使用氦气,Python 侧当前通过 `HELIUM_PR` 使用 Peng-Robinson 状态方程计算气体压缩因子和密度。当前物性层先覆盖状态方程相关量,完整焓/内能偏差函数后续在接气室能量方程时再补。
@@ -132,12 +140,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_pneumatic.py PythonModels/scripts/run_test_mql.py
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 PythonModels.scripts.run_test_mql
python3 -m unittest discover -s tests -t .
```
当前测试覆盖的是结构、配置解析、气动原语、气动件装配、管路连接装配、AMESim 结果读取、变量目录和 AMESim/Python 时序对齐工具,不代表已经完成 AMESim 物理结果复刻。
当前测试覆盖的是结构、配置解析、气动原语、气动件装配、管路连接装配、机械装配、AMESim 结果读取、变量目录和 AMESim/Python 时序对齐工具,不代表已经完成 AMESim 物理结果复刻。
## 后续方向
+226
View File
@@ -0,0 +1,226 @@
from __future__ import annotations
from dataclasses import dataclass
from math import pi
from PythonModels.reporting.amesim_results import AmesimResults
from PythonModels.reporting.test_mql_variables import (
TestMqlVariableCatalog,
build_test_mql_variable_catalog,
)
from PythonModels.systems.test_mql_config import TestMqlConfig, TestMqlResolvedComponent
MM_TO_M = 1.0e-3
N_PER_MM_TO_N_PER_M = 1.0e3
N_PER_MM_PER_S_TO_N_PER_M_PER_S = 1.0e3
@dataclass(frozen=True)
class TestMqlPistonSpec:
alias: str
piston_diameter_m: float
rod_diameter_m: float
zero_displacement_m: float
piston_area_m2: float
rod_area_m2: float
annulus_area_m2: float
data_paths: tuple[str, ...]
@dataclass(frozen=True)
class TestMqlMassEndstopSpec:
alias: str
mass_kg: float
xmin_m: float
xmax_m: float
min_stiffness_n_per_m: float
max_stiffness_n_per_m: float
min_damping_n_per_m_per_s: float
max_damping_n_per_m_per_s: float
min_penetration_m: float
max_penetration_m: float
rest_coeff: float
data_paths: tuple[str, ...]
@dataclass(frozen=True)
class TestMqlElasticEndstopSpec:
alias: str
gap_m: float
contact_stiffness_n_per_m: float
contact_damping_n_per_m_per_s: float
spring_diameter_m: float
wire_diameter_m: float
data_paths: tuple[str, ...]
@dataclass(frozen=True)
class TestMqlMechanicalNodeSpec:
alias: str
port_count: int
sum_mode: int
data_paths: tuple[str, ...]
@dataclass(frozen=True)
class TestMqlMechanicalAssembly:
pistons: dict[str, TestMqlPistonSpec]
masses: dict[str, TestMqlMassEndstopSpec]
elastic_endstops: dict[str, TestMqlElasticEndstopSpec]
mechanical_nodes: dict[str, TestMqlMechanicalNodeSpec]
zero_force_sources: tuple[str, ...]
force_connectors: tuple[str, ...]
@property
def component_count(self) -> int:
return (
len(self.pistons)
+ len(self.masses)
+ len(self.elastic_endstops)
+ len(self.mechanical_nodes)
+ len(self.zero_force_sources)
+ len(self.force_connectors)
)
@property
def aliases(self) -> tuple[str, ...]:
return tuple(
[
*self.pistons,
*self.masses,
*self.elastic_endstops,
*self.mechanical_nodes,
*self.zero_force_sources,
*self.force_connectors,
]
)
def build_test_mql_mechanical_assembly(
config: TestMqlConfig | None = None,
amesim_results: AmesimResults | None = None,
variable_catalog: TestMqlVariableCatalog | None = None,
) -> TestMqlMechanicalAssembly:
config = config or TestMqlConfig.from_amesim_specs()
if variable_catalog is None and amesim_results is not None:
variable_catalog = build_test_mql_variable_catalog(amesim_results)
pistons = {
component.alias: _build_piston(component, variable_catalog)
for component in config.components_by_submodel("PNRP17")
}
masses = {
component.alias: _build_mass(component, variable_catalog)
for component in config.components_by_submodel("MECMAS21")
}
elastic_endstops = {
component.alias: _build_elastic_endstop(component, variable_catalog)
for component in config.components_by_submodel("LSTP00A")
}
mechanical_nodes = {
component.alias: _build_mechanical_node(component, variable_catalog)
for component in config.components_by_submodel("LMECHN1")
}
zero_force_sources = tuple(component.alias for component in config.components_by_submodel("F000"))
force_connectors = tuple(component.alias for component in config.components_by_submodel("FORC"))
return TestMqlMechanicalAssembly(
pistons=pistons,
masses=masses,
elastic_endstops=elastic_endstops,
mechanical_nodes=mechanical_nodes,
zero_force_sources=zero_force_sources,
force_connectors=force_connectors,
)
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)
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,
data_paths=_data_paths(variable_catalog, component.alias),
)
def _build_mass(
component: TestMqlResolvedComponent,
variable_catalog: TestMqlVariableCatalog | None,
) -> TestMqlMassEndstopSpec:
return TestMqlMassEndstopSpec(
alias=component.alias,
mass_kg=component.parameter_value("mass"),
xmin_m=component.parameter_value("xmin"),
xmax_m=component.parameter_value("xmax"),
min_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmin")),
max_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmax")),
min_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmin")),
max_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmax")),
min_penetration_m=mm_to_m(component.parameter_value("Pdmin")),
max_penetration_m=mm_to_m(component.parameter_value("Pdmax")),
rest_coeff=component.parameter_value("restcoeff"),
data_paths=_data_paths(variable_catalog, component.alias),
)
def _build_elastic_endstop(
component: TestMqlResolvedComponent,
variable_catalog: TestMqlVariableCatalog | None,
) -> TestMqlElasticEndstopSpec:
return TestMqlElasticEndstopSpec(
alias=component.alias,
gap_m=mm_to_m(component.parameter_value("gap0")),
contact_stiffness_n_per_m=component.parameter_value("kcont"),
contact_damping_n_per_m_per_s=component.parameter_value("rcont"),
spring_diameter_m=mm_to_m(component.parameter_value("sdiam")),
wire_diameter_m=mm_to_m(component.parameter_value("wdiam")),
data_paths=_data_paths(variable_catalog, component.alias),
)
def _build_mechanical_node(
component: TestMqlResolvedComponent,
variable_catalog: TestMqlVariableCatalog | None,
) -> TestMqlMechanicalNodeSpec:
return TestMqlMechanicalNodeSpec(
alias=component.alias,
port_count=int(component.parameter_value("v1")),
sum_mode=int(component.parameter_value("sum")),
data_paths=_data_paths(variable_catalog, component.alias),
)
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
def n_per_mm_per_s_to_n_per_m_per_s(value: float) -> float:
return value * N_PER_MM_PER_S_TO_N_PER_M_PER_S
def _data_paths(
variable_catalog: TestMqlVariableCatalog | None,
alias: str,
) -> tuple[str, ...]:
if variable_catalog is None:
return ()
return variable_catalog.data_paths_for_owner(alias)
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import unittest
from pathlib import Path
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog
from PythonModels.systems.test_mql_mechanical import (
build_test_mql_mechanical_assembly,
circular_area,
mm_to_m,
n_per_mm_per_s_to_n_per_m_per_s,
n_per_mm_to_n_per_m,
)
REPO_ROOT = Path(__file__).resolve().parents[1]
TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame"
class TestMqlMechanicalAssemblyTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.amesim_results = load_test_mql_amesim_results(TEST_MQL_AME)
cls.variable_catalog = build_test_mql_variable_catalog(cls.amesim_results)
cls.assembly = build_test_mql_mechanical_assembly(
amesim_results=cls.amesim_results,
variable_catalog=cls.variable_catalog,
)
def test_assembles_expected_mechanical_component_counts(self) -> None:
self.assertEqual(len(self.assembly.pistons), 8)
self.assertEqual(len(self.assembly.masses), 10)
self.assertEqual(len(self.assembly.elastic_endstops), 8)
self.assertEqual(len(self.assembly.mechanical_nodes), 2)
self.assertEqual(len(self.assembly.zero_force_sources), 16)
self.assertEqual(len(self.assembly.force_connectors), 2)
self.assertEqual(self.assembly.component_count, 46)
def test_piston_geometry_is_converted_to_si_units(self) -> None:
piston = self.assembly.pistons["pn_brp2_8"]
self.assertAlmostEqual(piston.piston_diameter_m, 0.2)
self.assertAlmostEqual(piston.rod_diameter_m, 0.001)
self.assertAlmostEqual(piston.zero_displacement_m, 0.0)
self.assertAlmostEqual(piston.piston_area_m2, circular_area(0.2))
self.assertAlmostEqual(piston.rod_area_m2, circular_area(0.001))
self.assertAlmostEqual(piston.annulus_area_m2, piston.piston_area_m2 - piston.rod_area_m2)
self.assertIn("vvol1@pn_brp2_8", piston.data_paths)
self.assertIn("f2@pn_brp2_8", piston.data_paths)
def test_mass_endstop_parameters_are_converted_to_si_units(self) -> None:
mass = self.assembly.masses["mass_friction_endstops_10"]
self.assertEqual(mass.mass_kg, 50.0)
self.assertEqual(mass.xmin_m, -1.0)
self.assertEqual(mass.xmax_m, 0.8)
self.assertEqual(mass.min_stiffness_n_per_m, 1.0e9)
self.assertEqual(mass.max_stiffness_n_per_m, 1.0e9)
self.assertEqual(mass.min_damping_n_per_m_per_s, 1.0e4)
self.assertEqual(mass.max_damping_n_per_m_per_s, 1.0e4)
self.assertAlmostEqual(mass.min_penetration_m, 1.0e-4)
self.assertAlmostEqual(mass.max_penetration_m, 1.0e-4)
self.assertEqual(mass.rest_coeff, 0.65)
self.assertIn("v1@mass_friction_endstops_10", mass.data_paths)
self.assertIn("x1@mass_friction_endstops_10", mass.data_paths)
def test_large_load_masses_keep_distinct_limits(self) -> None:
front = self.assembly.masses["mass_friction_endstops_18"]
rear = self.assembly.masses["mass_friction_endstops_19"]
self.assertEqual(front.mass_kg, 170000.0)
self.assertEqual(front.xmin_m, 0.0)
self.assertEqual(front.xmax_m, 0.37)
self.assertEqual(rear.mass_kg, 90000.0)
self.assertEqual(rear.xmin_m, -0.72)
self.assertEqual(rear.xmax_m, 0.0)
def test_elastic_endstop_parameters_are_converted_to_si_units(self) -> None:
endstop = self.assembly.elastic_endstops["elasticendstop_8"]
self.assertEqual(endstop.gap_m, 0.0)
self.assertEqual(endstop.contact_stiffness_n_per_m, 1.0e11)
self.assertEqual(endstop.contact_damping_n_per_m_per_s, 1.0e11)
self.assertAlmostEqual(endstop.spring_diameter_m, 0.02)
self.assertAlmostEqual(endstop.wire_diameter_m, 0.002)
def test_mechanical_nodes_keep_port_count(self) -> None:
node = self.assembly.mechanical_nodes["dynamic_mechanical_node_alternative_2"]
self.assertEqual(node.port_count, 8)
self.assertEqual(node.sum_mode, 1)
self.assertIn("dynamic_mechanical_node_alternative_2", self.assembly.aliases)
def test_unit_helpers(self) -> None:
self.assertAlmostEqual(mm_to_m(20.0), 0.02)
self.assertAlmostEqual(n_per_mm_to_n_per_m(1.0e6), 1.0e9)
self.assertAlmostEqual(n_per_mm_per_s_to_n_per_m_per_s(10.0), 1.0e4)
if __name__ == "__main__":
unittest.main()