diff --git a/PythonModels/systems/test_mql_computed.py b/PythonModels/systems/test_mql_computed.py new file mode 100644 index 0000000..0c4e9a6 --- /dev/null +++ b/PythonModels/systems/test_mql_computed.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +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 ( + TestMqlObservationCatalog, + build_test_mql_observation_catalog, +) +from PythonModels.reporting.test_mql_output_schema import ( + TestMqlOutputSchema, + build_test_mql_output_schema, +) +from PythonModels.reporting.test_mql_output_validation import ( + TestMqlValidatedOutput, + compare_validated_test_mql_output, + validate_test_mql_output, +) +from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog +from PythonModels.systems.test_mql_mechanical import ( + TestMqlMechanicalAssembly, + build_test_mql_mechanical_assembly, +) + + +@dataclass(frozen=True) +class TestMqlComputedPistonGeometryRun: + 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: + 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_piston_geometry_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 TestMqlComputedPistonGeometryRun( + 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, +) -> dict[str, tuple[float, ...]]: + series_by_data_path: dict[str, tuple[float, ...]] = {} + for alias in sorted(mechanical_assembly.pistons): + piston = mechanical_assembly.pistons[alias] + geometry = piston.geometry() + x4 = amesim_results.series(f"x4@{alias}") + x5 = amesim_results.series(f"x5@{alias}") + v4 = amesim_results.series(f"v4@{alias}") + v5 = amesim_results.series(f"v5@{alias}") + series_by_data_path[f"length@{alias}"] = tuple( + geometry.chamber_length_mm(port4, port5) + for port4, port5 in zip(x4, x5) + ) + series_by_data_path[f"vol1@{alias}"] = tuple( + geometry.chamber_volume_cm3(port4, port5) + for port4, port5 in zip(x4, x5) + ) + series_by_data_path[f"vvol1@{alias}"] = tuple( + geometry.chamber_volume_rate_l_min(port4, port5) + for port4, port5 in zip(v4, v5) + ) + return series_by_data_path diff --git a/tests/test_test_mql_computed.py b/tests/test_test_mql_computed.py new file mode 100644 index 0000000..0bb7275 --- /dev/null +++ b/tests/test_test_mql_computed.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import unittest +from pathlib import Path + +from PythonModels.systems.test_mql_computed import run_test_mql_computed_piston_geometry + + +REPO_ROOT = Path(__file__).resolve().parents[1] +TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame" + + +class TestMqlComputedPistonGeometryTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.computed_run = run_test_mql_computed_piston_geometry(TEST_MQL_AME) + + def test_computed_piston_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(self.computed_run.signal_count, 24) + self.assertEqual(len(self.computed_run.comparison.metrics), 24) + self.assertEqual(self.computed_run.output_schema.signal_count, 858) + + def test_computed_piston_geometry_output_paths_are_expected(self) -> None: + paths = self.computed_run.output.data_paths + + self.assertIn("length@pn_brp2_8", paths) + self.assertIn("vol1@pn_brp2_8", paths) + self.assertIn("vvol1@pn_brp2_8", paths) + self.assertIn("length@pn_brp2_15", paths) + 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_piston_geometry_matches_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("length@"): + self.assertLessEqual(metric.max_abs_error, 3.0e-13) + elif data_path.startswith("vol1@"): + self.assertLessEqual(metric.max_abs_error, 8.0e-12) + elif data_path.startswith("vvol1@"): + self.assertLessEqual(metric.max_abs_error, 3.0e-11) + else: + self.fail(f"Unexpected computed piston geometry path: {data_path}") + + def test_computed_piston_geometry_final_samples_match_expected_values(self) -> None: + self.assertAlmostEqual( + self.computed_run.output.series("length@pn_brp2_8")[-1], + 1090.0013536465842, + ) + self.assertAlmostEqual( + self.computed_run.output.series("vol1@pn_brp2_8")[-1], + 34242.54636512914, + delta=1.0e-9, + ) + self.assertAlmostEqual( + self.computed_run.output.series("vvol1@pn_brp2_8")[-1], + 8.360141002531034e-07, + delta=1.0e-11, + ) + + +if __name__ == "__main__": + unittest.main()