From 7569917924dff762b477dffdd860b10127e4d1ca Mon Sep 17 00:00:00 2001 From: huojiarong Date: Fri, 17 Jul 2026 09:25:35 +0000 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0test=5Fmql=20P4=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E4=BB=A3=E6=95=B0=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PythonModels/systems/test_mql.py | 11 +++ PythonModels/systems/test_mql_nodes.py | 98 ++++++++++++++++++++++++++ tests/test_test_mql_nodes.py | 88 ++++++++++++++++++++++- 3 files changed, 196 insertions(+), 1 deletion(-) diff --git a/PythonModels/systems/test_mql.py b/PythonModels/systems/test_mql.py index 84a721e..35d28de 100644 --- a/PythonModels/systems/test_mql.py +++ b/PythonModels/systems/test_mql.py @@ -3863,6 +3863,7 @@ class TestMqlSystem: self.pnl0003_assembly = self._build_pnl0003_assembly() self.pnl00r_assembly = self._build_pnl00r_assembly() self.node3_assembly = self._build_node3_assembly() + self.node4_assembly = self._build_node4_assembly() self.pneumatic_assembly = self._build_pneumatic_assembly() pneumatic_components = self._pneumatic_components_by_alias() for spec in COMPONENT_SPECS: @@ -3912,6 +3913,12 @@ class TestMqlSystem: return build_test_mql_node3_assembly() + @staticmethod + def _build_node4_assembly(): + from PythonModels.systems.test_mql_nodes import build_test_mql_node4_assembly + + return build_test_mql_node4_assembly() + def _pneumatic_components_by_alias(self) -> dict[str, Component]: return { **self.pneumatic_assembly.fixed_chambers, @@ -3940,6 +3947,10 @@ class TestMqlSystem: def typed_node3_count(self) -> int: return len(self.node3_assembly) + @property + def typed_node4_count(self) -> int: + return len(self.node4_assembly) + def _pnl0003_spec_for_node_port(self, node_alias: str, port_name: str): for spec in self.pnl0003_assembly.specs: if spec.source_component == node_alias and spec.source_port == port_name: diff --git a/PythonModels/systems/test_mql_nodes.py b/PythonModels/systems/test_mql_nodes.py index 6bff89d..596e8d0 100644 --- a/PythonModels/systems/test_mql_nodes.py +++ b/PythonModels/systems/test_mql_nodes.py @@ -74,9 +74,107 @@ class TestMqlPneumaticNode3: ) +@dataclass(frozen=True) +class TestMqlPneumaticNode4Balance: + temperature_k: float + pressure_pa: float + port_1_enthalpy_flow_w: float + port_1_mass_flow_g_s: float + port_1_volume_derivative_l_min: float + port_1_volume_cm3: float + port_2_enthalpy_flow_w: float + port_2_mass_flow_g_s: float + port_2_volume_derivative_l_min: float + port_2_volume_cm3: float + port_3_enthalpy_flow_w: float + port_3_mass_flow_g_s: float + port_3_volume_derivative_l_min: float + port_3_volume_cm3: float + port_4_enthalpy_flow_w: float + port_4_mass_flow_g_s: float + port_4_volume_derivative_l_min: float + port_4_volume_cm3: float + + +@dataclass(frozen=True) +class TestMqlPneumaticNode4: + """Exact algebraic contract of AMESim ``P4NODE2``. + + Pressure and temperature are fixed by port 2 and duplicated to ports 1, 3, + and 4. Flow and volume signals at port 2 are the sums of ports 1, 3, and + 4, matching the saved AMESim variables for ``pnnode4_*`` instances. + """ + + alias: str + + def balance( + self, + *, + port_2_temperature_k: float, + port_2_pressure_pa: float, + port_1_enthalpy_flow_w: float, + port_1_mass_flow_g_s: float, + port_3_enthalpy_flow_w: float, + port_3_mass_flow_g_s: float, + port_4_enthalpy_flow_w: float, + port_4_mass_flow_g_s: float, + port_1_volume_derivative_l_min: float = 0.0, + port_1_volume_cm3: float = 0.0, + port_3_volume_derivative_l_min: float = 0.0, + port_3_volume_cm3: float = 0.0, + port_4_volume_derivative_l_min: float = 0.0, + port_4_volume_cm3: float = 0.0, + ) -> TestMqlPneumaticNode4Balance: + if port_2_temperature_k <= 0.0: + raise ValueError("port_2_temperature_k must be positive") + if port_2_pressure_pa <= 0.0: + raise ValueError("port_2_pressure_pa must be positive") + return TestMqlPneumaticNode4Balance( + temperature_k=port_2_temperature_k, + pressure_pa=port_2_pressure_pa, + port_1_enthalpy_flow_w=port_1_enthalpy_flow_w, + port_1_mass_flow_g_s=port_1_mass_flow_g_s, + port_1_volume_derivative_l_min=port_1_volume_derivative_l_min, + port_1_volume_cm3=port_1_volume_cm3, + port_2_enthalpy_flow_w=( + port_1_enthalpy_flow_w + + port_3_enthalpy_flow_w + + port_4_enthalpy_flow_w + ), + port_2_mass_flow_g_s=( + port_1_mass_flow_g_s + + port_3_mass_flow_g_s + + port_4_mass_flow_g_s + ), + port_2_volume_derivative_l_min=( + port_1_volume_derivative_l_min + + port_3_volume_derivative_l_min + + port_4_volume_derivative_l_min + ), + port_2_volume_cm3=( + port_1_volume_cm3 + port_3_volume_cm3 + port_4_volume_cm3 + ), + port_3_enthalpy_flow_w=port_3_enthalpy_flow_w, + port_3_mass_flow_g_s=port_3_mass_flow_g_s, + port_3_volume_derivative_l_min=port_3_volume_derivative_l_min, + port_3_volume_cm3=port_3_volume_cm3, + port_4_enthalpy_flow_w=port_4_enthalpy_flow_w, + port_4_mass_flow_g_s=port_4_mass_flow_g_s, + port_4_volume_derivative_l_min=port_4_volume_derivative_l_min, + port_4_volume_cm3=port_4_volume_cm3, + ) + + def build_test_mql_node3_assembly() -> dict[str, TestMqlPneumaticNode3]: return { str(spec["alias"]): TestMqlPneumaticNode3(alias=str(spec["alias"])) for spec in COMPONENT_SPECS if spec["submodel"] == "PN3NODE2" } + +def build_test_mql_node4_assembly() -> dict[str, TestMqlPneumaticNode4]: + return { + str(spec["alias"]): TestMqlPneumaticNode4(alias=str(spec["alias"])) + for spec in COMPONENT_SPECS + if spec["submodel"] == "P4NODE2" + } diff --git a/tests/test_test_mql_nodes.py b/tests/test_test_mql_nodes.py index 8c4bea0..e1df079 100644 --- a/tests/test_test_mql_nodes.py +++ b/tests/test_test_mql_nodes.py @@ -5,7 +5,10 @@ from pathlib import Path from PythonModels.reporting.amesim_results import load_test_mql_amesim_results from PythonModels.systems.test_mql import TestMqlSystem -from PythonModels.systems.test_mql_nodes import build_test_mql_node3_assembly +from PythonModels.systems.test_mql_nodes import ( + build_test_mql_node3_assembly, + build_test_mql_node4_assembly, +) REPO_ROOT = Path(__file__).resolve().parents[1] @@ -83,5 +86,88 @@ class TestMqlPneumaticNode3Tests(unittest.TestCase): ) +class TestMqlPneumaticNode4Tests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.nodes = build_test_mql_node4_assembly() + cls.results = load_test_mql_amesim_results(TEST_MQL_AME) + + def test_builds_all_eight_p4node2_instances(self) -> None: + self.assertEqual(len(self.nodes), 8) + self.assertIn("pnnode4_17", self.nodes) + system = TestMqlSystem() + self.assertEqual(system.typed_node4_count, 8) + self.assertEqual(system.node4_assembly["pnnode4_17"].alias, "pnnode4_17") + + def test_port_two_flow_is_exact_sum_of_ports_one_three_and_four(self) -> None: + balance = self.nodes["pnnode4_17"].balance( + port_2_temperature_k=281.0, + port_2_pressure_pa=4.4e6, + port_1_enthalpy_flow_w=2.0, + port_1_mass_flow_g_s=3.0, + port_3_enthalpy_flow_w=-0.5, + port_3_mass_flow_g_s=-1.0, + port_4_enthalpy_flow_w=4.0, + port_4_mass_flow_g_s=5.0, + port_1_volume_derivative_l_min=4.0, + port_1_volume_cm3=5.0, + port_3_volume_derivative_l_min=-1.0, + port_3_volume_cm3=6.0, + port_4_volume_derivative_l_min=2.0, + port_4_volume_cm3=7.0, + ) + + self.assertEqual(balance.temperature_k, 281.0) + self.assertEqual(balance.pressure_pa, 4.4e6) + self.assertEqual(balance.port_2_enthalpy_flow_w, 5.5) + self.assertEqual(balance.port_2_mass_flow_g_s, 7.0) + self.assertEqual(balance.port_2_volume_derivative_l_min, 5.0) + self.assertEqual(balance.port_2_volume_cm3, 18.0) + + def test_pnnode4_17_baseline_matches_adjacent_flow_sum(self) -> None: + index = 500 + balance = self.nodes["pnnode4_17"].balance( + port_2_temperature_k=self.results.series("temp1@pnnode4_17")[index], + port_2_pressure_pa=( + self.results.series("press1@pnnode4_17")[index] + 101_300.0 + ), + port_1_enthalpy_flow_w=self.results.series("dh1@pneumatic_86")[index], + port_1_mass_flow_g_s=self.results.series("dm1@pneumatic_86")[index], + port_3_enthalpy_flow_w=self.results.series("dh2@pneumatic_83")[index], + port_3_mass_flow_g_s=self.results.series("dm2@pneumatic_83")[index], + port_4_enthalpy_flow_w=self.results.series("dh2@pn_morifice_9")[index], + port_4_mass_flow_g_s=self.results.series("dm2@pn_morifice_9")[index], + ) + + self.assertAlmostEqual( + balance.port_2_mass_flow_g_s, + self.results.series("dm2@pnnode4_17")[index], + ) + self.assertAlmostEqual( + balance.port_2_enthalpy_flow_w, + self.results.series("dh2@pnnode4_17")[index], + ) + self.assertEqual(self.results.series("vol2@pnnode4_17")[index], 0.0) + self.assertEqual(self.results.series("dvol2@pnnode4_17")[index], 0.0) + self.assertAlmostEqual( + self.results.series("press1@pnnode4_17")[index], + self.results.series("p2@pneumatic_68")[index], + ) + + def test_rejects_nonphysical_primary_pressure_or_temperature(self) -> None: + node = self.nodes["pnnode4_17"] + with self.assertRaises(ValueError): + node.balance( + port_2_temperature_k=0.0, + port_2_pressure_pa=1.0e5, + port_1_enthalpy_flow_w=0.0, + port_1_mass_flow_g_s=0.0, + port_3_enthalpy_flow_w=0.0, + port_3_mass_flow_g_s=0.0, + port_4_enthalpy_flow_w=0.0, + port_4_mass_flow_g_s=0.0, + ) + + if __name__ == "__main__": unittest.main()