装配test_mql机械组件
This commit is contained in:
1 parent
03a6db10ad
commit
8f14a3840f
3 files changed
+338
-2
No files matched your search
@@ -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 物理结果复刻。
|
||||
|
||||
## 后续方向
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user