diff --git a/PythonModels/components/amesim_pneumatic.py b/PythonModels/components/amesim_pneumatic.py index 44f5684..d705c0e 100644 --- a/PythonModels/components/amesim_pneumatic.py +++ b/PythonModels/components/amesim_pneumatic.py @@ -70,6 +70,24 @@ class AmesimPneumaticGas: ) ) + def pressure_transport_enthalpy( + self, + pressure: float, + temperature: float, + reference_pressure: float = 101_300.0, + reference_temperature: float = 298.15, + ) -> float: + """Convert AMESim reference enthalpy to the absolute-energy state basis.""" + return ( + self.pressure_reference_enthalpy( + pressure, + temperature, + reference_pressure, + reference_temperature, + ) + + self.cp * reference_temperature + ) + def temperature_from_internal_energy(self, specific_internal_energy: float) -> float: if self.cv <= 0.0: raise ValueError("cv must be positive.") diff --git a/PythonModels/scripts/run_test_mql_full_state_comparison.py b/PythonModels/scripts/run_test_mql_full_state_comparison.py index f41aa23..fa2e70e 100644 --- a/PythonModels/scripts/run_test_mql_full_state_comparison.py +++ b/PythonModels/scripts/run_test_mql_full_state_comparison.py @@ -1,6 +1,7 @@ from __future__ import annotations import argparse +from bisect import bisect_left from dataclasses import dataclass, field from datetime import UTC, datetime from math import nextafter, sqrt @@ -492,6 +493,8 @@ class TestMqlPnvoEventWindowSampleDiagnostic: data_paths: tuple[str, ...] python_values_by_data_path: dict[str, float] amesim_values_by_data_path: dict[str, float] + amesim_sample_left_time_s: float + amesim_sample_right_time_s: float pnl0001_rhs_diagnostics: tuple[TestMqlPnl0001LineRhsDiagnostic, ...] = field( default_factory=tuple ) @@ -517,6 +520,10 @@ class TestMqlPnvoEventWindowSampleDiagnostic: TestMqlPnch012EnergyEquationDiagnostic, ... ] = field(default_factory=tuple) + @property + def amesim_values_are_interpolated(self) -> bool: + return self.amesim_sample_left_time_s != self.amesim_sample_right_time_s + def abs_error(self, data_path: str) -> float: return abs( self.python_values_by_data_path[data_path] @@ -929,6 +936,27 @@ def _series_one_sided_differences_at( return backward, forward +def _series_sample_bracket( + *, + times: tuple[float, ...] | list[float], + time_s: float, +) -> tuple[float, float]: + if not times: + raise ValueError("at least one sample time is required") + index = bisect_left(times, time_s) + tolerance = 1.0e-12 * max(1.0, abs(time_s)) + for candidate_index in (index - 1, index): + if 0 <= candidate_index < len(times): + candidate_time = times[candidate_index] + if abs(candidate_time - time_s) <= tolerance: + return candidate_time, candidate_time + if index <= 0: + return times[0], times[0] + if index >= len(times): + return times[-1], times[-1] + return times[index - 1], times[index] + + def _reference_enthalpy_flow_for_node_port( *, gas: AmesimPneumaticGas, @@ -2157,12 +2185,18 @@ def run_test_mql_pnvo_event_window_diagnostic( time_s=sample_time, ), ) + ( + amesim_sample_left_time_s, + amesim_sample_right_time_s, + ) = _series_sample_bracket(times=amesim_results.times, time_s=sample_time) sample_diagnostics.append( TestMqlPnvoEventWindowSampleDiagnostic( time_s=sample_time, data_paths=data_paths, python_values_by_data_path=python_sample_values, amesim_values_by_data_path=amesim_sample_values, + amesim_sample_left_time_s=amesim_sample_left_time_s, + amesim_sample_right_time_s=amesim_sample_right_time_s, pnl0001_rhs_diagnostics=pnl0001_rhs_diagnostics, pnl0001_pressure_loss_diagnostics=pnl0001_pressure_loss_diagnostics, pnvo_flow_parameter_diagnostics=pnvo_flow_parameter_diagnostics, @@ -2224,7 +2258,17 @@ def format_test_mql_pnvo_event_window_summary( if diagnostic.sample_diagnostics: lines.append("Sample comparisons:") for sample in diagnostic.sample_diagnostics: - lines.append(f" t={sample.time_s}") + if sample.amesim_values_are_interpolated: + amesim_sample_source = ( + "linear-interpolation@" + f"{sample.amesim_sample_left_time_s}" + f"..{sample.amesim_sample_right_time_s}" + ) + else: + amesim_sample_source = f"exact@{sample.amesim_sample_left_time_s}" + lines.append( + f" t={sample.time_s} (amesim={amesim_sample_source})" + ) for data_path in sample.data_paths: lines.append( f" - {data_path}: " diff --git a/tests/test_amesim_pneumatic_components.py b/tests/test_amesim_pneumatic_components.py index b5f2dc7..fe8f876 100644 --- a/tests/test_amesim_pneumatic_components.py +++ b/tests/test_amesim_pneumatic_components.py @@ -55,6 +55,17 @@ class AmesimPneumaticComponentsTest(unittest.TestCase): delta=0.1, ) + def test_pressure_transport_enthalpy_restores_absolute_energy_offset(self) -> None: + gas = HELIUM_PNEUMATIC_GAS + pressure = 13_839_965.0 + temperature = 287.7322 + + self.assertAlmostEqual( + gas.pressure_transport_enthalpy(pressure, temperature), + gas.pressure_reference_enthalpy(pressure, temperature) + + gas.cp * 298.15, + ) + def test_reference_temperature_from_enthalpy_rejects_non_positive_cp(self) -> None: gas = AmesimPneumaticGas(cp=0.0, cv=3116.0) diff --git a/tests/test_run_test_mql_full_state_comparison.py b/tests/test_run_test_mql_full_state_comparison.py index 4b33274..464891b 100644 --- a/tests/test_run_test_mql_full_state_comparison.py +++ b/tests/test_run_test_mql_full_state_comparison.py @@ -26,6 +26,7 @@ from PythonModels.scripts.run_test_mql_full_state_comparison import ( format_test_mql_pnvo_event_boundary_summary, format_test_mql_pnvo_event_window_summary, run_test_mql_full_state_comparison, + _series_sample_bracket, ) @@ -34,6 +35,23 @@ TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame" class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): + def test_series_sample_bracket_distinguishes_exact_and_interpolated_times(self) -> None: + times = (0.04000000000000009, 0.050000000000000114) + + exact_left, exact_right = _series_sample_bracket( + times=times, + time_s=0.04, + ) + interpolated_left, interpolated_right = _series_sample_bracket( + times=times, + time_s=0.041, + ) + + self.assertEqual(exact_left, exact_right) + self.assertAlmostEqual(exact_left, 0.04) + self.assertAlmostEqual(interpolated_left, 0.04) + self.assertAlmostEqual(interpolated_right, 0.05) + def test_full_state_script_writes_summary_and_comparison_csv(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: output_dir = Path(tmpdir) @@ -231,6 +249,8 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): "xv@pn_morifice_1": 1.0, "dm2@pn_morifice_1": 456.8, }, + amesim_sample_left_time_s=0.05, + amesim_sample_right_time_s=0.05, pnl0001_rhs_diagnostics=( TestMqlPnl0001LineRhsDiagnostic( line_alias="pneumatic_69", @@ -497,7 +517,8 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase): 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.assertFalse(sample.amesim_values_are_interpolated) + self.assertIn("t=0.05 (amesim=exact@0.05)", summary) self.assertIn("rhs=8154", summary) self.assertIn("rhs@pneumatic_69", summary) self.assertIn("dm_dt=-0.0222", summary) diff --git a/tests/test_test_mql_orifice_observations.py b/tests/test_test_mql_orifice_observations.py index 2544d32..bb03c10 100644 --- a/tests/test_test_mql_orifice_observations.py +++ b/tests/test_test_mql_orifice_observations.py @@ -8,6 +8,9 @@ from PythonModels.reporting.test_mql_orifice_observations import ( G_PER_S_TO_KG_PER_S, build_test_mql_orifice_observation_catalog, ) +from PythonModels.systems.test_mql_pneumatic import ( + TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER, +) REPO_ROOT = Path(__file__).resolve().parents[1] @@ -55,7 +58,10 @@ class TestMqlOrificeObservationCatalogTests(unittest.TestCase): last = binding.observation_at(self.amesim_results, -1) self.assertAlmostEqual(binding.nominal_area_m2, 78.5e-6) - self.assertAlmostEqual(binding.flow_coefficient, 0.45) + self.assertAlmostEqual( + binding.flow_coefficient, + 0.45 * TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER, + ) self.assertEqual(first.opening, 0.0) self.assertEqual(first.effective_area_m2, 0.0) self.assertEqual(last.opening, 1.0)