From c84a809f7721b2589fbf75698b55b791abdd16df Mon Sep 17 00:00:00 2001 From: huojiarong Date: Thu, 16 Jul 2026 02:15:47 +0000 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90test=5Fmql=E6=9C=BA=E6=A2=B0?= =?UTF-8?q?=E8=A7=82=E6=B5=8B=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_mql_mechanical_observations.py | 138 +++++++++++++++++- .../test_test_mql_mechanical_observations.py | 67 ++++++++- 2 files changed, 203 insertions(+), 2 deletions(-) diff --git a/PythonModels/reporting/test_mql_mechanical_observations.py b/PythonModels/reporting/test_mql_mechanical_observations.py index e3221ca..645be29 100644 --- a/PythonModels/reporting/test_mql_mechanical_observations.py +++ b/PythonModels/reporting/test_mql_mechanical_observations.py @@ -50,6 +50,26 @@ class TestMqlElasticEndstopObservation: stiffness_n_m: float +@dataclass(frozen=True) +class TestMqlForceSourceObservation: + time: float + force_n: float + + +@dataclass(frozen=True) +class TestMqlForceConnectorObservation: + time: float + force_n: float + + +@dataclass(frozen=True) +class TestMqlMechanicalNodeObservation: + time: float + velocities_m_s: dict[int, float] + displacements_m: dict[int, float] + total_force_n: float + + @dataclass(frozen=True) class TestMqlPistonObservationBinding: alias: str @@ -125,15 +145,71 @@ class TestMqlElasticEndstopObservationBinding: ) +@dataclass(frozen=True) +class TestMqlForceSourceObservationBinding: + alias: str + force_path: str + + def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceSourceObservation: + return TestMqlForceSourceObservation( + time=results.times[index], + force_n=results.series(self.force_path)[index], + ) + + +@dataclass(frozen=True) +class TestMqlForceConnectorObservationBinding: + alias: str + force_path: str + + def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceConnectorObservation: + return TestMqlForceConnectorObservation( + time=results.times[index], + force_n=results.series(self.force_path)[index], + ) + + +@dataclass(frozen=True) +class TestMqlMechanicalNodeObservationBinding: + alias: str + velocity_paths_by_port: dict[int, str] + displacement_paths_by_port: dict[int, str] + total_force_path: str + + def observation_at(self, results: AmesimResults, index: int) -> TestMqlMechanicalNodeObservation: + return TestMqlMechanicalNodeObservation( + time=results.times[index], + velocities_m_s={ + port: results.series(path)[index] + for port, path in self.velocity_paths_by_port.items() + }, + displacements_m={ + port: results.series(path)[index] + for port, path in self.displacement_paths_by_port.items() + }, + total_force_n=results.series(self.total_force_path)[index], + ) + + @dataclass(frozen=True) class TestMqlMechanicalObservationCatalog: pistons: dict[str, TestMqlPistonObservationBinding] masses: dict[str, TestMqlMassEndstopObservationBinding] elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding] + zero_force_sources: dict[str, TestMqlForceSourceObservationBinding] + force_connectors: dict[str, TestMqlForceConnectorObservationBinding] + mechanical_nodes: dict[str, TestMqlMechanicalNodeObservationBinding] @property def binding_count(self) -> int: - return len(self.pistons) + len(self.masses) + len(self.elastic_endstops) + return ( + len(self.pistons) + + len(self.masses) + + len(self.elastic_endstops) + + len(self.zero_force_sources) + + len(self.force_connectors) + + len(self.mechanical_nodes) + ) def build_test_mql_mechanical_observation_catalog( @@ -160,6 +236,18 @@ def build_test_mql_mechanical_observation_catalog( alias: _build_elastic_endstop_binding(alias, variable_catalog) for alias in mechanical_assembly.elastic_endstops }, + zero_force_sources={ + alias: _build_zero_force_source_binding(alias, variable_catalog) + for alias in mechanical_assembly.zero_force_sources + }, + force_connectors={ + alias: _build_force_connector_binding(alias, variable_catalog) + for alias in mechanical_assembly.force_connectors + }, + mechanical_nodes={ + alias: _build_mechanical_node_binding(alias, variable_catalog) + for alias in mechanical_assembly.mechanical_nodes + }, ) @@ -217,6 +305,54 @@ def _build_elastic_endstop_binding( ) +def _build_zero_force_source_binding( + alias: str, + variable_catalog: TestMqlVariableCatalog, +) -> TestMqlForceSourceObservationBinding: + variables = _owner_variables(variable_catalog, alias) + return TestMqlForceSourceObservationBinding( + alias=alias, + force_path=_required_path(variables, "fzero", expected_units="N"), + ) + + +def _build_force_connector_binding( + alias: str, + variable_catalog: TestMqlVariableCatalog, +) -> TestMqlForceConnectorObservationBinding: + variables = _owner_variables(variable_catalog, alias) + return TestMqlForceConnectorObservationBinding( + alias=alias, + force_path=_required_path(variables, "force", expected_units="N"), + ) + + +def _build_mechanical_node_binding( + alias: str, + variable_catalog: TestMqlVariableCatalog, +) -> TestMqlMechanicalNodeObservationBinding: + variables = _owner_variables(variable_catalog, alias) + velocity_paths_by_port = {} + displacement_paths_by_port = {} + for port in range(1, 9): + velocity_paths_by_port[port] = _required_path( + variables, + f"p{port}__vt", + expected_units="m/s", + ) + displacement_paths_by_port[port] = _required_path( + variables, + f"p{port}__xt", + expected_units="m", + ) + return TestMqlMechanicalNodeObservationBinding( + alias=alias, + velocity_paths_by_port=velocity_paths_by_port, + displacement_paths_by_port=displacement_paths_by_port, + total_force_path=_required_path(variables, "tforce", expected_units="N"), + ) + + def _owner_variables( variable_catalog: TestMqlVariableCatalog, alias: str, diff --git a/tests/test_test_mql_mechanical_observations.py b/tests/test_test_mql_mechanical_observations.py index 06a8c62..9cb83f1 100644 --- a/tests/test_test_mql_mechanical_observations.py +++ b/tests/test_test_mql_mechanical_observations.py @@ -23,7 +23,10 @@ class TestMqlMechanicalObservationCatalogTests(unittest.TestCase): self.assertEqual(len(self.catalog.pistons), 8) self.assertEqual(len(self.catalog.masses), 10) self.assertEqual(len(self.catalog.elastic_endstops), 8) - self.assertEqual(self.catalog.binding_count, 26) + self.assertEqual(len(self.catalog.zero_force_sources), 16) + self.assertEqual(len(self.catalog.force_connectors), 2) + self.assertEqual(len(self.catalog.mechanical_nodes), 2) + self.assertEqual(self.catalog.binding_count, 46) def test_piston_binding_exposes_amesim_data_paths(self) -> None: binding = self.catalog.pistons["pn_brp2_8"] @@ -119,6 +122,68 @@ class TestMqlMechanicalObservationCatalogTests(unittest.TestCase): 1.0e-9, ) + def test_zero_force_source_observations_are_zero(self) -> None: + for binding in self.catalog.zero_force_sources.values(): + force = self.amesim_results.series(binding.force_path) + with self.subTest(alias=binding.alias): + self.assertEqual(binding.force_path, f"fzero@{binding.alias}") + self.assertEqual(min(force), 0.0) + self.assertEqual(max(force), 0.0) + self.assertEqual(binding.observation_at(self.amesim_results, -1).force_n, 0.0) + + def test_force_connector_observations_match_amesim_samples(self) -> None: + force_1 = self.catalog.force_connectors["forcecon_1"].observation_at( + self.amesim_results, + -1, + ) + force_2 = self.catalog.force_connectors["forcecon_2"].observation_at( + self.amesim_results, + -1, + ) + + self.assertEqual(self.catalog.force_connectors["forcecon_1"].force_path, "force@forcecon_1") + self.assertEqual(self.catalog.force_connectors["forcecon_2"].force_path, "force@forcecon_2") + self.assertAlmostEqual(force_1.force_n, 49000.0) + self.assertAlmostEqual(force_2.force_n, 0.0) + self.assertAlmostEqual( + self.amesim_results.series("force@forcecon_1")[0], + 1.0e17, + ) + self.assertAlmostEqual( + self.amesim_results.series("force@forcecon_2")[0], + 1.0e12, + ) + + def test_mechanical_node_bindings_expose_all_ports(self) -> None: + node = self.catalog.mechanical_nodes["dynamic_mechanical_node_alternative_2"] + + self.assertEqual(sorted(node.velocity_paths_by_port), list(range(1, 9))) + self.assertEqual(sorted(node.displacement_paths_by_port), list(range(1, 9))) + self.assertEqual(node.velocity_paths_by_port[1], "p1__vt@dynamic_mechanical_node_alternative_2") + self.assertEqual(node.displacement_paths_by_port[8], "p8__xt@dynamic_mechanical_node_alternative_2") + self.assertEqual(node.total_force_path, "tforce@dynamic_mechanical_node_alternative_2") + + def test_mechanical_node_observations_match_amesim_final_samples(self) -> None: + front = self.catalog.mechanical_nodes["dynamic_mechanical_node_alternative_2"].observation_at( + self.amesim_results, + -1, + ) + rear = self.catalog.mechanical_nodes["dynamic_mechanical_node_alternative_3"].observation_at( + self.amesim_results, + -1, + ) + + self.assertEqual(set(front.velocities_m_s), set(range(1, 9))) + self.assertEqual(set(front.displacements_m), set(range(1, 9))) + self.assertAlmostEqual(front.velocities_m_s[1], -0.0) + self.assertAlmostEqual(front.displacements_m[1], -0.3700000000000001) + self.assertAlmostEqual(front.displacements_m[8], -0.3700000000000001) + self.assertAlmostEqual(front.total_force_n, 1083272.2763054767) + self.assertAlmostEqual(rear.velocities_m_s[1], 0.0) + self.assertAlmostEqual(rear.displacements_m[1], -0.72) + self.assertAlmostEqual(rear.displacements_m[8], -0.72) + self.assertAlmostEqual(rear.total_force_n, 1083272.2762592242) + if __name__ == "__main__": unittest.main()