From bb4a71c6120d2e104b4cfbca7fc9a41a66531271 Mon Sep 17 00:00:00 2001 From: huojiarong Date: Mon, 20 Jul 2026 09:18:05 +0000 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5test=5Fmql=E5=A4=96=E5=8A=9B?= =?UTF-8?q?=E4=BF=A1=E5=8F=B7=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PythonModels/systems/test_mql_mechanical.py | 107 +++++++++++++++++++- tests/test_test_mql_mechanical.py | 36 ++++++- 2 files changed, 136 insertions(+), 7 deletions(-) diff --git a/PythonModels/systems/test_mql_mechanical.py b/PythonModels/systems/test_mql_mechanical.py index 9c52ee1..a6c696a 100644 --- a/PythonModels/systems/test_mql_mechanical.py +++ b/PythonModels/systems/test_mql_mechanical.py @@ -109,14 +109,62 @@ class TestMqlMechanicalNodeSpec: data_paths: tuple[str, ...] +@dataclass(frozen=True) +class TestMqlPiecewiseLinearSignalSpec: + alias: str + t_start_s: float + starts: tuple[float, ...] + ends: tuple[float, ...] + durations_s: tuple[float, ...] + stage_count: int + is_cyclic: bool + data_paths: tuple[str, ...] + + def output_at(self, time_s: float) -> float: + if self.stage_count <= 0: + return 0.0 + elapsed = max(time_s - self.t_start_s, 0.0) + active_durations = self.durations_s[: self.stage_count] + total_duration = sum(active_durations) + if self.is_cyclic and total_duration > 0.0: + elapsed = elapsed % total_duration + + stage_start_time = 0.0 + for index, duration in enumerate(active_durations): + stage_end_time = stage_start_time + duration + if elapsed < stage_end_time or index == self.stage_count - 1: + if duration <= 0.0: + return self.ends[index] + fraction = (elapsed - stage_start_time) / duration + return self.starts[index] + fraction * (self.ends[index] - self.starts[index]) + stage_start_time = stage_end_time + return self.ends[self.stage_count - 1] + + +@dataclass(frozen=True) +class TestMqlForceConnectorSpec: + alias: str + signal_alias: str + target_mass_alias: str + data_paths: tuple[str, ...] + + def force_at( + self, + time_s: float, + signals: dict[str, TestMqlPiecewiseLinearSignalSpec], + ) -> float: + return signals[self.signal_alias].output_at(time_s) + + @dataclass(frozen=True) class TestMqlMechanicalAssembly: pistons: dict[str, TestMqlPistonSpec] masses: dict[str, TestMqlMassEndstopSpec] elastic_endstops: dict[str, TestMqlElasticEndstopSpec] mechanical_nodes: dict[str, TestMqlMechanicalNodeSpec] + piecewise_signals: dict[str, TestMqlPiecewiseLinearSignalSpec] + force_connectors: dict[str, TestMqlForceConnectorSpec] zero_force_sources: tuple[str, ...] - force_connectors: tuple[str, ...] @property def component_count(self) -> int: @@ -125,8 +173,9 @@ class TestMqlMechanicalAssembly: + len(self.masses) + len(self.elastic_endstops) + len(self.mechanical_nodes) - + len(self.zero_force_sources) + + len(self.piecewise_signals) + len(self.force_connectors) + + len(self.zero_force_sources) ) @property @@ -137,8 +186,9 @@ class TestMqlMechanicalAssembly: *self.masses, *self.elastic_endstops, *self.mechanical_nodes, - *self.zero_force_sources, + *self.piecewise_signals, *self.force_connectors, + *self.zero_force_sources, ] ) @@ -264,15 +314,23 @@ def build_test_mql_mechanical_assembly( component.alias: _build_mechanical_node(component, variable_catalog) for component in config.components_by_submodel("LMECHN1") } + piecewise_signals = { + component.alias: _build_piecewise_signal(component, variable_catalog) + for component in config.components_by_submodel("UD00") + } + force_connectors = { + component.alias: _build_force_connector(component, variable_catalog) + for component in config.components_by_submodel("FORC") + } 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, + piecewise_signals=piecewise_signals, force_connectors=force_connectors, + zero_force_sources=zero_force_sources, ) @@ -356,6 +414,45 @@ def _build_mechanical_node( ) +def _build_piecewise_signal( + component: TestMqlResolvedComponent, + variable_catalog: TestMqlVariableCatalog | None, +) -> TestMqlPiecewiseLinearSignalSpec: + starts = tuple(component.parameter_value(f"start{index}") for index in range(1, 9)) + ends = tuple(component.parameter_value(f"end{index}") for index in range(1, 9)) + durations = tuple(component.parameter_value(f"t{index}") for index in range(1, 9)) + return TestMqlPiecewiseLinearSignalSpec( + alias=component.alias, + t_start_s=component.parameter_value("tstart"), + starts=starts, + ends=ends, + durations_s=durations, + stage_count=int(component.parameter_value("nstages")), + is_cyclic=bool(int(component.parameter_value("iscyclic"))), + data_paths=_data_paths(variable_catalog, component.alias), + ) + + +def _build_force_connector( + component: TestMqlResolvedComponent, + variable_catalog: TestMqlVariableCatalog | None, +) -> TestMqlForceConnectorSpec: + signal_alias_by_force_connector = { + "forcecon_1": "piecewiselinear", + "forcecon_2": "piecewiselinear_1", + } + target_mass_by_force_connector = { + "forcecon_1": "mass_friction_endstops_19", + "forcecon_2": "mass_friction_endstops_18", + } + return TestMqlForceConnectorSpec( + alias=component.alias, + signal_alias=signal_alias_by_force_connector[component.alias], + target_mass_alias=target_mass_by_force_connector[component.alias], + data_paths=_data_paths(variable_catalog, component.alias), + ) + + 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_test_mql_mechanical.py b/tests/test_test_mql_mechanical.py index 5e1da5f..b6c4681 100644 --- a/tests/test_test_mql_mechanical.py +++ b/tests/test_test_mql_mechanical.py @@ -36,9 +36,41 @@ class TestMqlMechanicalAssemblyTests(unittest.TestCase): 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.piecewise_signals), 2) self.assertEqual(len(self.assembly.force_connectors), 2) - self.assertEqual(self.assembly.component_count, 46) + self.assertEqual(len(self.assembly.zero_force_sources), 16) + self.assertEqual(self.assembly.component_count, 48) + + def test_piecewise_force_sources_match_amesim_samples(self) -> None: + front_signal = self.assembly.piecewise_signals["piecewiselinear_1"] + rear_signal = self.assembly.piecewise_signals["piecewiselinear"] + + self.assertAlmostEqual(front_signal.output_at(0.0), 1.0e12) + self.assertAlmostEqual(front_signal.output_at(0.8000000000000009), 0.0) + self.assertAlmostEqual(rear_signal.output_at(0.0), 1.0e17) + self.assertAlmostEqual(rear_signal.output_at(0.8000000000000009), 49000.0) + self.assertEqual( + self.assembly.force_connectors["forcecon_2"].target_mass_alias, + "mass_friction_endstops_18", + ) + self.assertEqual( + self.assembly.force_connectors["forcecon_1"].target_mass_alias, + "mass_friction_endstops_19", + ) + self.assertAlmostEqual( + self.assembly.force_connectors["forcecon_2"].force_at( + 0.0, + self.assembly.piecewise_signals, + ), + self.amesim_results.series("force@forcecon_2")[0], + ) + self.assertAlmostEqual( + self.assembly.force_connectors["forcecon_1"].force_at( + 0.8000000000000009, + self.assembly.piecewise_signals, + ), + self.amesim_results.series("force@forcecon_1")[80], + ) def test_mechanical_mass_closure_exposes_all_mecmas21_states(self) -> None: closure = TestMqlMechanicalMassClosure(self.assembly)