diff --git a/PythonModels/systems/test_mql_computed.py b/PythonModels/systems/test_mql_computed.py index 8a02520..3f110d7 100644 --- a/PythonModels/systems/test_mql_computed.py +++ b/PythonModels/systems/test_mql_computed.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from pathlib import Path +from PythonModels.components.amesim_pneumatic import m3_to_cm3 from PythonModels.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results from PythonModels.reporting.test_mql_comparison import TestMqlComparisonResult from PythonModels.reporting.test_mql_observations import ( @@ -19,7 +20,6 @@ from PythonModels.reporting.test_mql_output_validation import ( validate_test_mql_output, ) from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog -from PythonModels.components.amesim_pneumatic import m3_to_cm3 from PythonModels.systems.test_mql_mechanical import ( TestMqlMechanicalAssembly, build_test_mql_mechanical_assembly, @@ -67,6 +67,24 @@ class TestMqlComputedGeometryRun: return len(self.output.data_paths) +@dataclass(frozen=True) +class TestMqlComputedMechanicalRelationsRun: + amesim_results: AmesimResults + observation_catalog: TestMqlObservationCatalog + output_schema: TestMqlOutputSchema + mechanical_assembly: TestMqlMechanicalAssembly + output: TestMqlValidatedOutput + comparison: TestMqlComparisonResult + + @property + def sample_count(self) -> int: + return len(self.output.times) + + @property + def signal_count(self) -> int: + return len(self.output.data_paths) + + def run_test_mql_computed_piston_geometry( archive_path: Path, ) -> TestMqlComputedPistonGeometryRun: @@ -154,6 +172,45 @@ def run_test_mql_computed_geometry( ) +def run_test_mql_computed_mechanical_relations( + archive_path: Path, +) -> TestMqlComputedMechanicalRelationsRun: + amesim_results = load_test_mql_amesim_results(archive_path) + observation_catalog = build_test_mql_observation_catalog(amesim_results) + output_schema = build_test_mql_output_schema( + amesim_results, + observation_catalog=observation_catalog, + ) + variable_catalog = build_test_mql_variable_catalog(amesim_results) + mechanical_assembly = build_test_mql_mechanical_assembly( + amesim_results=amesim_results, + variable_catalog=variable_catalog, + ) + output_series = _compute_mass_duplicate_series(amesim_results, mechanical_assembly) + output_data_paths = tuple(output_series) + output = validate_test_mql_output( + times=amesim_results.times, + series_by_data_path=output_series, + schema=output_schema, + data_paths=output_data_paths, + ) + comparison = compare_validated_test_mql_output( + times=output.times, + series_by_data_path=output.series_by_data_path, + schema=output_schema, + amesim_results=amesim_results, + data_paths=output.data_paths, + ) + return TestMqlComputedMechanicalRelationsRun( + amesim_results=amesim_results, + observation_catalog=observation_catalog, + output_schema=output_schema, + mechanical_assembly=mechanical_assembly, + output=output, + comparison=comparison, + ) + + def _compute_piston_geometry_series( amesim_results: AmesimResults, mechanical_assembly: TestMqlMechanicalAssembly, @@ -206,3 +263,19 @@ def _piston_alias_for_variable_chamber(chamber_alias: str) -> str: if not chamber_alias.startswith("pn_c1"): raise ValueError(f"Unexpected PNCH012 alias: {chamber_alias}") return chamber_alias.replace("pn_c1", "pn_brp2", 1) + + +def _compute_mass_duplicate_series( + amesim_results: AmesimResults, + mechanical_assembly: TestMqlMechanicalAssembly, +) -> dict[str, tuple[float, ...]]: + series_by_data_path: dict[str, tuple[float, ...]] = {} + for alias in sorted(mechanical_assembly.masses): + for signal_name in ("x1", "v1", "acc1"): + source_path = f"{signal_name}@{alias}" + duplicate_path = f"{signal_name}dup@{alias}" + series_by_data_path[duplicate_path] = tuple( + -value for value in amesim_results.series(source_path) + ) + return series_by_data_path + diff --git a/tests/test_test_mql_computed.py b/tests/test_test_mql_computed.py index 4d06e0d..d643bbb 100644 --- a/tests/test_test_mql_computed.py +++ b/tests/test_test_mql_computed.py @@ -5,6 +5,7 @@ from pathlib import Path from PythonModels.systems.test_mql_computed import ( run_test_mql_computed_geometry, + run_test_mql_computed_mechanical_relations, run_test_mql_computed_piston_geometry, ) @@ -109,5 +110,48 @@ class TestMqlComputedGeometryTests(unittest.TestCase): ) +class TestMqlComputedMechanicalRelationsTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.computed_run = run_test_mql_computed_mechanical_relations(TEST_MQL_AME) + + def test_computed_mechanical_relations_outputs_schema_valid_series(self) -> None: + self.assertEqual(self.computed_run.sample_count, 1002) + self.assertEqual(len(self.computed_run.mechanical_assembly.masses), 10) + self.assertEqual(self.computed_run.signal_count, 30) + self.assertEqual(len(self.computed_run.comparison.metrics), 30) + self.assertEqual(self.computed_run.output_schema.signal_count, 858) + + def test_computed_mechanical_relations_include_mass_duplicate_paths(self) -> None: + paths = self.computed_run.output.data_paths + + self.assertIn("x1dup@mass_friction_endstops_10", paths) + self.assertIn("v1dup@mass_friction_endstops_10", paths) + self.assertIn("acc1dup@mass_friction_endstops_10", paths) + self.assertEqual(len([path for path in paths if path.startswith("x1dup@")]), 10) + self.assertEqual(len([path for path in paths if path.startswith("v1dup@")]), 10) + self.assertEqual(len([path for path in paths if path.startswith("acc1dup@")]), 10) + + def test_computed_mechanical_relations_match_amesim_exactly(self) -> None: + for data_path in self.computed_run.output.data_paths: + metric = self.computed_run.comparison.metric(data_path) + with self.subTest(data_path=data_path): + self.assertEqual(metric.max_abs_error, 0.0) + + def test_computed_mechanical_relations_final_samples_match_expected_values(self) -> None: + self.assertAlmostEqual( + self.computed_run.output.series("x1dup@mass_friction_endstops_10")[-1], + -0.3700013536465843, + ) + self.assertAlmostEqual( + self.computed_run.output.series("v1dup@mass_friction_endstops_10")[-1], + -4.435303434246023e-10, + ) + self.assertAlmostEqual( + self.computed_run.output.series("acc1dup@mass_friction_endstops_10")[-1], + 3.5662378650158644e-07, + ) + + if __name__ == "__main__": unittest.main()