diff --git a/PythonModels/systems/test_mql_computed.py b/PythonModels/systems/test_mql_computed.py index 0c4e9a6..8a02520 100644 --- a/PythonModels/systems/test_mql_computed.py +++ b/PythonModels/systems/test_mql_computed.py @@ -19,10 +19,15 @@ 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, ) +from PythonModels.systems.test_mql_pneumatic import ( + TestMqlPneumaticAssembly, + build_test_mql_pneumatic_assembly, +) @dataclass(frozen=True) @@ -43,6 +48,25 @@ class TestMqlComputedPistonGeometryRun: return len(self.output.data_paths) +@dataclass(frozen=True) +class TestMqlComputedGeometryRun: + amesim_results: AmesimResults + observation_catalog: TestMqlObservationCatalog + output_schema: TestMqlOutputSchema + mechanical_assembly: TestMqlMechanicalAssembly + pneumatic_assembly: TestMqlPneumaticAssembly + 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: @@ -82,6 +106,54 @@ def run_test_mql_computed_piston_geometry( ) +def run_test_mql_computed_geometry( + archive_path: Path, +) -> TestMqlComputedGeometryRun: + 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, + ) + pneumatic_assembly = build_test_mql_pneumatic_assembly() + output_series = { + **_compute_piston_geometry_series(amesim_results, mechanical_assembly), + **_compute_variable_chamber_volume_series( + amesim_results, + mechanical_assembly, + pneumatic_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 TestMqlComputedGeometryRun( + amesim_results=amesim_results, + observation_catalog=observation_catalog, + output_schema=output_schema, + mechanical_assembly=mechanical_assembly, + pneumatic_assembly=pneumatic_assembly, + output=output, + comparison=comparison, + ) + + def _compute_piston_geometry_series( amesim_results: AmesimResults, mechanical_assembly: TestMqlMechanicalAssembly, @@ -107,3 +179,30 @@ def _compute_piston_geometry_series( for port4, port5 in zip(v4, v5) ) return series_by_data_path + + +def _compute_variable_chamber_volume_series( + amesim_results: AmesimResults, + mechanical_assembly: TestMqlMechanicalAssembly, + pneumatic_assembly: TestMqlPneumaticAssembly, +) -> dict[str, tuple[float, ...]]: + series_by_data_path: dict[str, tuple[float, ...]] = {} + for chamber_alias in sorted(pneumatic_assembly.variable_chambers): + chamber = pneumatic_assembly.variable_chambers[chamber_alias] + piston_alias = _piston_alias_for_variable_chamber(chamber_alias) + piston = mechanical_assembly.pistons[piston_alias] + geometry = piston.geometry() + x4 = amesim_results.series(f"x4@{piston_alias}") + x5 = amesim_results.series(f"x5@{piston_alias}") + dead_volume_cm3 = m3_to_cm3(chamber.dead_volume) + series_by_data_path[f"vol@{chamber_alias}"] = tuple( + dead_volume_cm3 + geometry.chamber_volume_cm3(port4, port5) + for port4, port5 in zip(x4, x5) + ) + return series_by_data_path + + +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) diff --git a/tests/test_test_mql_computed.py b/tests/test_test_mql_computed.py index 0bb7275..4d06e0d 100644 --- a/tests/test_test_mql_computed.py +++ b/tests/test_test_mql_computed.py @@ -3,7 +3,10 @@ from __future__ import annotations import unittest from pathlib import Path -from PythonModels.systems.test_mql_computed import run_test_mql_computed_piston_geometry +from PythonModels.systems.test_mql_computed import ( + run_test_mql_computed_geometry, + run_test_mql_computed_piston_geometry, +) REPO_ROOT = Path(__file__).resolve().parents[1] @@ -63,5 +66,48 @@ class TestMqlComputedPistonGeometryTests(unittest.TestCase): ) +class TestMqlComputedGeometryTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.computed_run = run_test_mql_computed_geometry(TEST_MQL_AME) + + def test_computed_geometry_outputs_schema_valid_series(self) -> None: + self.assertEqual(self.computed_run.sample_count, 1002) + self.assertEqual(len(self.computed_run.mechanical_assembly.pistons), 8) + self.assertEqual(len(self.computed_run.pneumatic_assembly.variable_chambers), 8) + self.assertEqual(self.computed_run.signal_count, 32) + self.assertEqual(len(self.computed_run.comparison.metrics), 32) + self.assertEqual(self.computed_run.output_schema.signal_count, 858) + + def test_computed_geometry_includes_variable_chamber_volume_paths(self) -> None: + paths = self.computed_run.output.data_paths + + self.assertIn("vol@pn_c1_8", paths) + self.assertIn("vol@pn_c1_15", paths) + self.assertEqual(len([path for path in paths if path.startswith("vol@pn_c1")]), 8) + self.assertEqual(len([path for path in paths if path.startswith("length@")]), 8) + self.assertEqual(len([path for path in paths if path.startswith("vol1@")]), 8) + self.assertEqual(len([path for path in paths if path.startswith("vvol1@")]), 8) + + def test_computed_variable_chamber_volumes_match_amesim_with_tight_error_bounds(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): + if data_path.startswith("vol@pn_c1"): + self.assertLessEqual(metric.max_abs_error, 8.0e-12) + + def test_computed_variable_chamber_volume_final_samples_match_expected_values(self) -> None: + self.assertAlmostEqual( + self.computed_run.output.series("vol@pn_c1_8")[-1], + 49242.54636512914, + delta=1.0e-9, + ) + self.assertAlmostEqual( + self.computed_run.output.series("vol@pn_c1_15")[-1], + 49242.546365149814, + delta=1.0e-9, + ) + + if __name__ == "__main__": unittest.main()