from __future__ import annotations import tempfile import unittest from pathlib import Path from PythonModels.core.solver import SolveIVPConfig from PythonModels.systems.test_mql import TestMqlPnl0001LineRhsDiagnostic from PythonModels.scripts.run_test_mql_full_state_comparison import ( TestMqlFullStateComparisonExecutionConfig, TestMqlFullStateComparisonPathConfig, TestMqlFullStateComparisonScriptConfig, TestMqlPnl0001PressureLossCalibrationDiagnostic, TestMqlPnvoEventBoundaryDiagnostic, TestMqlPnvoEventWindowDiagnostic, TestMqlPnvoEventWindowSampleDiagnostic, TestMqlPnvoFlowParameterDiagnostic, TestMqlPnvoEventWindowSegmentDiagnostic, format_test_mql_full_state_comparison_summary, format_test_mql_pnvo_event_boundary_summary, format_test_mql_pnvo_event_window_summary, run_test_mql_full_state_comparison, ) REPO_ROOT = Path(__file__).resolve().parents[1] TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame" class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): def test_full_state_script_writes_summary_and_comparison_csv(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: output_dir = Path(tmpdir) run, returned_output_dir = run_test_mql_full_state_comparison( TestMqlFullStateComparisonScriptConfig( paths=TestMqlFullStateComparisonPathConfig( archive_path=TEST_MQL_AME, output_dir=output_dir, ), execution=TestMqlFullStateComparisonExecutionConfig( data_paths=( "press@pn_c1_8", "vol1@pn_brp2_8", "dm1@pneumatic_69", "xv@pn_morifice_1", "dm2@pn_morifice_1", ), solver=SolveIVPConfig(t_start=0.0, t_stop=0.0), t_eval=(0.0,), ), ) ) summary_path = output_dir / "test_mql_full_state_comparison_summary.txt" csv_path = output_dir / "test_mql_amesim_comparison.csv" csv_summary_path = output_dir / "test_mql_amesim_comparison_summary.txt" summary = summary_path.read_text(encoding="utf-8") csv_header = csv_path.read_text(encoding="utf-8").splitlines()[0] csv_summary_exists = csv_summary_path.exists() self.assertEqual(returned_output_dir, output_dir) self.assertEqual(run.sample_count, 1) self.assertEqual(run.signal_count, 5) self.assertAlmostEqual(run.comparison.max_abs_error, 0.0, delta=1.0e-8) self.assertEqual( run.metrics_by_max_abs_error()[0].data_path, "press@pn_c1_8", ) diagnostic = run.signal_diagnostic("press@pn_c1_8") self.assertEqual(diagnostic.data_path, "press@pn_c1_8") self.assertAlmostEqual(diagnostic.initial_python_value, -1300.0) self.assertAlmostEqual(diagnostic.final_python_value, -1300.0) self.assertAlmostEqual(diagnostic.final_abs_error, 0.0, delta=1.0e-8) self.assertEqual( run.diagnostics_by_final_abs_error()[0].data_path, "press@pn_c1_8", ) flow_diagnostic = run.pnl0001_mass_flow_diagnostic() self.assertEqual(flow_diagnostic.data_path, "dm1@pneumatic_69") self.assertAlmostEqual(flow_diagnostic.initial_python_dm1_g_s, 0.0) self.assertAlmostEqual(flow_diagnostic.initial_amesim_dm1_g_s, 0.0) self.assertAlmostEqual( flow_diagnostic.initial_python_canonical_kg_s, -flow_diagnostic.initial_python_dm1_g_s * 1.0e-3, ) pnvo_diagnostic = run.pnvo_diagnostic() self.assertEqual(pnvo_diagnostic.alias, "pn_morifice_1") self.assertAlmostEqual(pnvo_diagnostic.initial_python_opening, 0.0) self.assertAlmostEqual(pnvo_diagnostic.initial_amesim_opening, 0.0) self.assertAlmostEqual(pnvo_diagnostic.initial_python_mass_flow_kg_s, 0.0) self.assertAlmostEqual(pnvo_diagnostic.initial_amesim_mass_flow_kg_s, 0.0) chamber_diagnostic = run.chamber_rhs_diagnostic("pn_c1_8") self.assertEqual(chamber_diagnostic.chamber_alias, "pn_c1_8") self.assertEqual(chamber_diagnostic.piston_alias, "pn_brp2_8") self.assertAlmostEqual(chamber_diagnostic.chamber_volume_m3, 0.015) self.assertAlmostEqual( chamber_diagnostic.energy_derivative_w, chamber_diagnostic.port_a_energy_flow_w + chamber_diagnostic.port_b_energy_flow_w + chamber_diagnostic.boundary_work_w + chamber_diagnostic.thermal_energy_flow_w, ) self.assertTrue(csv_summary_exists) self.assertIn("Mode: Python 132 full-state closure comparison", summary) self.assertIn("Compared signals: 5", summary) self.assertIn("AMESim first saved interval: 0.010000000000000023", summary) self.assertIn("Final time aligns with AMESim sample: True", summary) self.assertIn("Largest absolute error: press@pn_c1_8=", summary) self.assertIn("Largest final endpoint error: press@pn_c1_8=", summary) self.assertIn("Metrics by max absolute error:", summary) self.assertIn("Endpoint diagnostics by final absolute error:", summary) self.assertIn( "PNL0001 canonical mass-flow diagnostic: dm1@pneumatic_69", summary, ) self.assertIn("equals -AMESim dm1 * 1e-3", summary) self.assertIn("PNVO diagnostic: pn_morifice_1", summary) self.assertIn("final_python_opening=", summary) self.assertIn("final_python_mass_flow_kg_s=", summary) self.assertIn("Largest endpoint chamber RHS breakdown: pn_c1_8", summary) self.assertIn("piston_alias=pn_brp2_8", summary) self.assertIn("mass_derivative_kg_s=", summary) self.assertIn("boundary_work_w=", summary) self.assertIn("thermal_energy_flow_w=", summary) self.assertIn("final_python=", summary) self.assertIn("final_amesim=", summary) self.assertIn("python.press@pn_c1_8", csv_header) self.assertIn("amesim.vol1@pn_brp2_8", csv_header) self.assertIn("python.dm1@pneumatic_69", csv_header) self.assertIn("python.xv@pn_morifice_1", csv_header) self.assertIn("python.dm2@pn_morifice_1", csv_header) def test_default_execution_aligns_first_amesim_saved_sample(self) -> None: execution = TestMqlFullStateComparisonExecutionConfig() self.assertEqual(execution.solver.t_stop, 0.01) self.assertEqual(execution.solver.max_step, 1.0e-3) self.assertEqual(execution.t_eval, (0.0, 0.01)) self.assertIn("xv@pn_morifice_1", execution.data_paths) self.assertIn("dm2@pn_morifice_1", execution.data_paths) def test_formats_pnvo_event_boundary_diagnostic(self) -> None: data_paths = ( "press@pn_c1_8", "dm1@pneumatic_69", "xv@pn_morifice_1", "dm2@pn_morifice_1", ) diagnostic = TestMqlPnvoEventBoundaryDiagnostic( orifice_alias="pn_morifice_1", event_time_s=0.04, integration_stop_time_s=0.039999999999999994, data_paths=data_paths, python_values_by_data_path={ "press@pn_c1_8": -1090.9, "dm1@pneumatic_69": -0.00949, "xv@pn_morifice_1": 1.0, "dm2@pn_morifice_1": 502.945, }, amesim_values_by_data_path={ "press@pn_c1_8": -1146.38, "dm1@pneumatic_69": -0.00667, "xv@pn_morifice_1": 1.0, "dm2@pn_morifice_1": 497.824, }, ) self.assertEqual(diagnostic.orifice_alias, "pn_morifice_1") self.assertLess(diagnostic.integration_stop_time_s, diagnostic.event_time_s) self.assertEqual(diagnostic.abs_error("xv@pn_morifice_1"), 0.0) self.assertAlmostEqual( diagnostic.abs_error("dm2@pn_morifice_1"), 5.121, ) summary = format_test_mql_pnvo_event_boundary_summary(diagnostic) self.assertIn("PNVO event boundary diagnostic (pn_morifice_1)", summary) self.assertIn("Observation side: right-continuous STEP0 opening", summary) self.assertTrue(summary.endswith("\n")) def test_formats_pnvo_event_window_diagnostic_with_samples(self) -> None: data_paths = ( "press@pn_c1_8", "temp@pn_c1_8", "vol@pn_c1_8", "mgas1@pn_c1_8", "p2@pneumatic_69", "t2@pneumatic_69", "mgas@pneumatic_69", "re@pneumatic_69", "v@pneumatic_69", "ff@pneumatic_69", "dm1@pneumatic_69", "xv@pn_morifice_1", "dm2@pn_morifice_1", ) sample = TestMqlPnvoEventWindowSampleDiagnostic( time_s=0.05, data_paths=data_paths, python_values_by_data_path={ "press@pn_c1_8": 219127.9, "temp@pn_c1_8": 372.3, "vol@pn_c1_8": 15000.0, "mgas1@pn_c1_8": 5.46, "p2@pneumatic_69": 93250.0, "t2@pneumatic_69": 293.15, "mgas@pneumatic_69": 0.53, "re@pneumatic_69": 1.41e6, "v@pneumatic_69": 832.0, "ff@pneumatic_69": 0.0243, "dm1@pneumatic_69": 443.2, "xv@pn_morifice_1": 1.0, "dm2@pn_morifice_1": 463.7, }, amesim_values_by_data_path={ "press@pn_c1_8": 187060.0, "temp@pn_c1_8": 296.1, "vol@pn_c1_8": 15000.0, "mgas1@pn_c1_8": 5.47, "p2@pneumatic_69": 1484689.6, "t2@pneumatic_69": 296.1, "mgas@pneumatic_69": 0.81, "re@pneumatic_69": 1.35e6, "v@pneumatic_69": 519.8, "ff@pneumatic_69": 0.0241, "dm1@pneumatic_69": 419.9, "xv@pn_morifice_1": 1.0, "dm2@pn_morifice_1": 456.8, }, pnl0001_rhs_diagnostics=( TestMqlPnl0001LineRhsDiagnostic( line_alias="pneumatic_69", chamber_alias="pn_c1_8", line_pressure_pa=1034250.0, line_temperature_k=293.15, chamber_pressure_pa=320427.9, chamber_temperature_k=379.9, chamber_to_line_flow_kg_s=-0.4432, node_to_line_flow_kg_s=0.4210, mass_derivative_kg_s=-0.0222, port_1_energy_flow_w=-404000.0, port_2_energy_flow_w=384000.0, thermal_energy_flow_w=0.0, energy_derivative_w=-20000.0, ), ), pnl0001_pressure_loss_diagnostics=( TestMqlPnl0001PressureLossCalibrationDiagnostic( line_alias="pneumatic_69", chamber_alias="pn_c1_8", time_s=0.05, amesim_cm=0.0159, amesim_dm1_g_s=419.9, amesim_mass_flow_magnitude_kg_s=0.4199, amesim_line_gauge_pressure_pa=1484689.6, amesim_chamber_gauge_pressure_pa=187060.0, amesim_line_temperature_k=296.1, amesim_pressure_drop_pa=1297629.6, current_darcy_pressure_drop_pa=422633.2, pressure_drop_multiplier=3.07, amesim_linear_conductance_kg_s_sqrt_k_per_pa=5.56e-6, python_linear_conductance_kg_s_sqrt_k_per_pa=1.06e-5, python_to_amesim_linear_conductance_ratio=1.91, ), ), pnvo_flow_parameter_diagnostics=( TestMqlPnvoFlowParameterDiagnostic( orifice_alias="pn_morifice_1", time_s=0.05, amesim_cm=0.0158, amesim_dm2_g_s=456.8, amesim_opening=1.0, amesim_gas_velocity_m_s=885.9, python_opening=1.0, python_flow_coefficient=0.45, python_effective_area_m2=78.5e-6, python_line_pressure_pa=15310000.0, python_boundary_pressure_pa=1586000.0, python_upstream_pressure_pa=15310000.0, python_upstream_temperature_k=293.15, python_mass_flow_kg_s=0.4637, python_cm=0.0147, python_to_amesim_cm_ratio=0.93, ), ), ) diagnostic = TestMqlPnvoEventWindowDiagnostic( orifice_alias="pn_morifice_1", event_time_s=0.04, final_time_s=0.05, data_paths=data_paths, segment_diagnostics=( TestMqlPnvoEventWindowSegmentDiagnostic( t_start=0.048, t_stop=0.05, method="BDF", rtol=1.0e-5, atol=1.0e-8, max_step=1.0e-5, rhs_evaluations=8154, success=True, message="success", ), ), sample_diagnostics=(sample,), python_values_by_data_path=sample.python_values_by_data_path, amesim_values_by_data_path=sample.amesim_values_by_data_path, ) summary = format_test_mql_pnvo_event_window_summary(diagnostic) self.assertAlmostEqual(diagnostic.abs_error("dm2@pn_morifice_1"), 6.9) self.assertAlmostEqual(sample.abs_error("press@pn_c1_8"), 32067.9) self.assertIn("PNVO event window diagnostic (pn_morifice_1)", summary) self.assertIn("Sample comparisons:", summary) self.assertIn("t=0.05", summary) self.assertIn("rhs=8154", summary) self.assertIn("rhs@pneumatic_69", summary) self.assertIn("dm_dt=-0.0222", summary) self.assertIn("pressure_loss@pneumatic_69", summary) self.assertIn("cm=0.0159", summary) self.assertIn("dp_multiplier=3.07", summary) self.assertIn("amesim_linear_k=5.56e-06", summary) self.assertIn("python_linear_k=1.06e-05", summary) self.assertIn("linear_k_ratio=1.91", summary) self.assertIn("flow_parameter@pn_morifice_1", summary) self.assertIn("amesim_cm=0.0158", summary) self.assertIn("python_cm=0.0147", summary) self.assertIn("cm_ratio=0.93", summary) self.assertIn("amesim_gasvel=885.9", summary) self.assertIn("Final comparison:", summary) self.assertTrue(summary.endswith("\n")) def test_full_state_script_can_skip_artifact_files(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: output_dir = Path(tmpdir) run, returned_output_dir = run_test_mql_full_state_comparison( TestMqlFullStateComparisonScriptConfig( paths=TestMqlFullStateComparisonPathConfig( archive_path=TEST_MQL_AME, output_dir=output_dir, ), execution=TestMqlFullStateComparisonExecutionConfig( write_summary=False, write_comparison_csv=False, data_paths=("press@pn_c1_8",), solver=SolveIVPConfig(t_start=0.0, t_stop=0.0), t_eval=(0.0,), ), ) ) self.assertEqual(returned_output_dir, output_dir) self.assertEqual(run.signal_count, 1) self.assertFalse((output_dir / "test_mql_full_state_comparison_summary.txt").exists()) self.assertFalse((output_dir / "test_mql_amesim_comparison.csv").exists()) def test_format_test_mql_full_state_comparison_summary(self) -> None: run, _ = run_test_mql_full_state_comparison( TestMqlFullStateComparisonScriptConfig( paths=TestMqlFullStateComparisonPathConfig(archive_path=TEST_MQL_AME), execution=TestMqlFullStateComparisonExecutionConfig( write_summary=False, write_comparison_csv=False, data_paths=("press@pn_c1_8",), solver=SolveIVPConfig(t_start=0.0, t_stop=0.0), t_eval=(0.0,), ), ) ) summary = format_test_mql_full_state_comparison_summary(run) self.assertIn("Model: test_mql", summary) self.assertIn("Output schema signals: 858", summary) self.assertIn("Compared signals: 1", summary) self.assertIn("Largest absolute error: press@pn_c1_8=", summary) self.assertIn("Largest final endpoint error: press@pn_c1_8=", summary) self.assertIn("Metrics by max absolute error:", summary) self.assertIn("Endpoint diagnostics by final absolute error:", summary) self.assertIn("Largest endpoint chamber RHS breakdown: pn_c1_8", summary) self.assertTrue(summary.endswith("\n")) if __name__ == "__main__": unittest.main()