From c4c1b35dee82e687caee063e7a69705793d39523 Mon Sep 17 00:00:00 2001 From: huojiarong Date: Fri, 17 Jul 2026 03:39:14 +0000 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5test=5Fmql=E5=8F=8C=E7=AE=A1?= =?UTF-8?q?=E8=B7=AF=E6=B0=94=E5=AE=A4=E4=B8=8EPN3=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PythonModels/systems/test_mql.py | 91 ++++++++++ PythonModels/systems/test_mql_closure.py | 219 +++++++++++++++++++++++ PythonModels/systems/test_mql_nodes.py | 82 +++++++++ tests/test_test_mql_nodes.py | 87 +++++++++ tests/test_test_mql_pnl0001_segment.py | 73 ++++++++ 5 files changed, 552 insertions(+) create mode 100644 PythonModels/systems/test_mql_nodes.py create mode 100644 tests/test_test_mql_nodes.py diff --git a/PythonModels/systems/test_mql.py b/PythonModels/systems/test_mql.py index 42eb77c..3b26199 100644 --- a/PythonModels/systems/test_mql.py +++ b/PythonModels/systems/test_mql.py @@ -3860,6 +3860,7 @@ class TestMqlSystem: self.archive_path = archive_path or Path(__file__).resolve().parents[2] / AMESIM_ARCHIVE_RELATIVE_PATH self.network = SimulationNetwork(name=MODEL_NAME) self.pnl0001_assembly = self._build_pnl0001_assembly() + self.node3_assembly = self._build_node3_assembly() self.pneumatic_assembly = self._build_pneumatic_assembly() pneumatic_components = self._pneumatic_components_by_alias() for spec in COMPONENT_SPECS: @@ -3889,6 +3890,12 @@ class TestMqlSystem: return build_test_mql_pnl0001_assembly(self.archive_path) + @staticmethod + def _build_node3_assembly(): + from PythonModels.systems.test_mql_nodes import build_test_mql_node3_assembly + + return build_test_mql_node3_assembly() + def _pneumatic_components_by_alias(self) -> dict[str, Component]: return { **self.pneumatic_assembly.fixed_chambers, @@ -3905,6 +3912,10 @@ class TestMqlSystem: def typed_pnl0001_line_count(self) -> int: return len(self.pnl0001_assembly.lines) + @property + def typed_node3_count(self) -> int: + return len(self.node3_assembly) + def pneumatic_state_vector(self) -> list[float]: return self.network.initial_state_vector() @@ -4161,6 +4172,86 @@ class TestMqlSystem: t_eval=t_eval, ) + def pnl0001_pair_chamber_segment_closure_from_spec( + self, + spec, + *, + inlet_node_pressure_pa: float, + outlet_node_pressure_pa: float, + inlet_node_temperature_k: float = 293.15, + outlet_node_temperature_k: float = 293.15, + ): + """Insert both topology-derived PNL0001 states into the segment.""" + from PythonModels.components.amesim_pneumatic import ( + AmesimPneumaticOrifice, + AmesimPneumaticVolume, + ) + from PythonModels.systems.test_mql_closure import ( + TestMqlPneumaticBoundaryCondition, + TestMqlPnl0001PairChamberSegmentClosure, + TestMqlPnl0001PairChamberSegmentComponents, + ) + + inlet_line = self.pnl0001_assembly.lines[spec.inlet_line_alias] + outlet_line = self.pnl0001_assembly.lines[spec.outlet_line_alias] + volume = self.network.components[spec.volume_alias] + inlet_orifice = self.network.components[spec.inlet_orifice_alias] + outlet_orifice = self.network.components[spec.outlet_orifice_alias] + if not isinstance(volume, AmesimPneumaticVolume): + raise TypeError(f"{spec.volume_alias} is not an AMESim pneumatic volume") + if not isinstance(inlet_orifice, AmesimPneumaticOrifice): + raise TypeError( + f"{spec.inlet_orifice_alias} is not an AMESim pneumatic orifice" + ) + if not isinstance(outlet_orifice, AmesimPneumaticOrifice): + raise TypeError( + f"{spec.outlet_orifice_alias} is not an AMESim pneumatic orifice" + ) + return TestMqlPnl0001PairChamberSegmentClosure( + components=TestMqlPnl0001PairChamberSegmentComponents( + inlet_line=inlet_line, + outlet_line=outlet_line, + volume=volume, + inlet_orifice=inlet_orifice, + outlet_orifice=outlet_orifice, + spec=spec, + ), + inlet_node=TestMqlPneumaticBoundaryCondition( + pressure_pa=inlet_node_pressure_pa, + temperature_k=inlet_node_temperature_k, + ), + outlet_node=TestMqlPneumaticBoundaryCondition( + pressure_pa=outlet_node_pressure_pa, + temperature_k=outlet_node_temperature_k, + ), + ) + + def simulate_pnl0001_pair_chamber_segment_from_spec( + self, + spec, + *, + inlet_node_pressure_pa: float, + outlet_node_pressure_pa: float, + inlet_node_temperature_k: float = 293.15, + outlet_node_temperature_k: float = 293.15, + config: SolveIVPConfig | None = None, + t_eval: list[float] | None = None, + ): + closure = self.pnl0001_pair_chamber_segment_closure_from_spec( + spec, + inlet_node_pressure_pa=inlet_node_pressure_pa, + outlet_node_pressure_pa=outlet_node_pressure_pa, + inlet_node_temperature_k=inlet_node_temperature_k, + outlet_node_temperature_k=outlet_node_temperature_k, + ) + run_config = config or SolveIVPConfig(t_stop=1.0e-4, max_step=1.0e-5) + return integrate_ode( + rhs=lambda t, state: closure.rhs(state), + initial_state=closure.initial_state_vector(), + config=run_config, + t_eval=t_eval, + ) + def pneumatic_branch_closure_from_spec(self, spec): return self.pneumatic_branch_closure( name=spec.name, diff --git a/PythonModels/systems/test_mql_closure.py b/PythonModels/systems/test_mql_closure.py index af948ef..bbda93e 100644 --- a/PythonModels/systems/test_mql_closure.py +++ b/PythonModels/systems/test_mql_closure.py @@ -119,6 +119,29 @@ class TestMqlPnl0001ChamberSegmentSnapshot: outlet_flow: float +@dataclass(frozen=True) +class TestMqlPnl0001PairChamberSegmentComponents: + inlet_line: AmesimPnl0001Pipe + outlet_line: AmesimPnl0001Pipe + volume: AmesimPneumaticVolume + inlet_orifice: AmesimPneumaticOrifice + outlet_orifice: AmesimPneumaticOrifice + spec: TestMqlPneumaticChamberSegmentSpec + + +@dataclass(frozen=True) +class TestMqlPnl0001PairChamberSegmentSnapshot: + inlet_line: ThermodynamicProperties + chamber: ThermodynamicProperties + outlet_line: ThermodynamicProperties + inlet_node: ThermodynamicProperties + outlet_node: ThermodynamicProperties + inlet_node_to_line_flow: float + inlet_line_to_chamber_flow: float + outlet_node_to_line_flow: float + outlet_line_to_chamber_flow: float + + @dataclass(frozen=True) class TestMqlPneumaticBranchComponents: name: str @@ -555,3 +578,199 @@ class TestMqlPnl0001ChamberSegmentClosure: internal_h=snapshot.chamber.h, ) return [*line_derivative.as_vector(), *chamber_derivative.as_vector()] + + +class TestMqlPnl0001PairChamberSegmentClosure: + """Six-state fixed chamber segment with both PNL0001 compliance states. + + Generated C shows that both line instances receive ``t1/p1`` from their + PN3 nodes and send their explicit ``t2/p2`` states to the fixed orifices. + The reduced closure therefore prescribes node pressure/temperature at both + resistive port-1 boundaries while keeping both line storage states native. + """ + + def __init__( + self, + *, + components: TestMqlPnl0001PairChamberSegmentComponents, + inlet_node: TestMqlPneumaticBoundaryCondition, + outlet_node: TestMqlPneumaticBoundaryCondition, + ) -> None: + self.components = components + self.inlet_node = inlet_node + self.outlet_node = outlet_node + + def initial_state_vector(self) -> list[float]: + return [ + *self.components.inlet_line.get_state_vector(), + *self.components.volume.get_state_vector(), + *self.components.outlet_line.get_state_vector(), + ] + + def apply_state_vector(self, values: list[float]) -> None: + if len(values) != 6: + raise ValueError("PNL0001 pair/chamber state vector requires six values") + self.components.inlet_line.set_state_vector(values[:2]) + self.components.volume.set_state_vector(values[2:4]) + self.components.outlet_line.set_state_vector(values[4:]) + + def snapshot( + self, + state_vector: list[float] | None = None, + ) -> TestMqlPnl0001PairChamberSegmentSnapshot: + if state_vector is not None: + self.apply_state_vector(state_vector) + inlet_line = self.components.inlet_line.properties() + chamber = self.components.volume.properties() + outlet_line = self.components.outlet_line.properties() + inlet_node = self.inlet_node.properties(self.components.inlet_line.gas) + outlet_node = self.outlet_node.properties(self.components.outlet_line.gas) + inlet_node_to_line_flow = self.components.inlet_line.resistance_mass_flow( + port_1_pressure_pa=inlet_node.p, + port_1_temperature_k=inlet_node.T, + ) + outlet_node_to_line_flow = self.components.outlet_line.resistance_mass_flow( + port_1_pressure_pa=outlet_node.p, + port_1_temperature_k=outlet_node.T, + ) + inlet_line_to_chamber_flow = self._line_to_chamber_flow( + line=inlet_line, + chamber=chamber, + orifice=self.components.inlet_orifice, + ) + outlet_line_to_chamber_flow = self._line_to_chamber_flow( + line=outlet_line, + chamber=chamber, + orifice=self.components.outlet_orifice, + ) + snapshot = TestMqlPnl0001PairChamberSegmentSnapshot( + inlet_line=inlet_line, + chamber=chamber, + outlet_line=outlet_line, + inlet_node=inlet_node, + outlet_node=outlet_node, + inlet_node_to_line_flow=inlet_node_to_line_flow, + inlet_line_to_chamber_flow=inlet_line_to_chamber_flow, + outlet_node_to_line_flow=outlet_node_to_line_flow, + outlet_line_to_chamber_flow=outlet_line_to_chamber_flow, + ) + self._write_port_states(snapshot) + return snapshot + + @staticmethod + def _line_to_chamber_flow( + *, + line: ThermodynamicProperties, + chamber: ThermodynamicProperties, + orifice: AmesimPneumaticOrifice, + ) -> float: + upstream_temperature = line.T if line.p >= chamber.p else chamber.T + return orifice.mass_flow(line.p, chamber.p, upstream_temperature) + + @staticmethod + def _port( + component: AmesimPneumaticVolume | AmesimPneumaticOrifice, + port_name: str, + ) -> PortState: + return TestMqlPneumaticChamberSegmentClosure._port(component, port_name) + + def _write_port_states( + self, + snapshot: TestMqlPnl0001PairChamberSegmentSnapshot, + ) -> None: + spec = self.components.spec + chamber = self.components.volume + chamber_inlet_port = self._port(chamber, spec.volume_inlet_port) + chamber_outlet_port = self._port(chamber, spec.volume_outlet_port) + + self._write_line_side( + line=self.components.inlet_line, + line_properties=snapshot.inlet_line, + node_properties=snapshot.inlet_node, + node_to_line_flow=snapshot.inlet_node_to_line_flow, + line_to_chamber_flow=snapshot.inlet_line_to_chamber_flow, + orifice=self.components.inlet_orifice, + orifice_boundary_port=spec.inlet_orifice_boundary_port, + orifice_volume_port=spec.inlet_orifice_volume_port, + chamber_port=chamber_inlet_port, + chamber_properties=snapshot.chamber, + ) + self._write_line_side( + line=self.components.outlet_line, + line_properties=snapshot.outlet_line, + node_properties=snapshot.outlet_node, + node_to_line_flow=snapshot.outlet_node_to_line_flow, + line_to_chamber_flow=snapshot.outlet_line_to_chamber_flow, + orifice=self.components.outlet_orifice, + orifice_boundary_port=spec.outlet_orifice_boundary_port, + orifice_volume_port=spec.outlet_orifice_volume_port, + chamber_port=chamber_outlet_port, + chamber_properties=snapshot.chamber, + ) + + def _write_line_side( + self, + *, + line: AmesimPnl0001Pipe, + line_properties: ThermodynamicProperties, + node_properties: ThermodynamicProperties, + node_to_line_flow: float, + line_to_chamber_flow: float, + orifice: AmesimPneumaticOrifice, + orifice_boundary_port: str, + orifice_volume_port: str, + chamber_port: PortState, + chamber_properties: ThermodynamicProperties, + ) -> None: + line.port_1.p = node_properties.p + line.port_1.m_flow = node_to_line_flow + line.port_1.h_outflow = line_properties.h + line.port_2.p = line_properties.p + line.port_2.m_flow = -line_to_chamber_flow + + boundary_port = self._port(orifice, orifice_boundary_port) + volume_port = self._port(orifice, orifice_volume_port) + boundary_port.p = line_properties.p + boundary_port.m_flow = line_to_chamber_flow + boundary_port.h_outflow = line_properties.h + volume_port.p = chamber_properties.p + volume_port.m_flow = -line_to_chamber_flow + volume_port.h_outflow = chamber_properties.h + chamber_port.m_flow = line_to_chamber_flow + + def rhs(self, state_vector: list[float]) -> list[float]: + snapshot = self.snapshot(state_vector) + inlet_derivative = self.components.inlet_line.derivatives_from_connections( + port_1_m_flow=snapshot.inlet_node_to_line_flow, + connected_h_1=snapshot.inlet_node.h, + port_2_m_flow=-snapshot.inlet_line_to_chamber_flow, + connected_h_2=snapshot.chamber.h, + ) + outlet_derivative = self.components.outlet_line.derivatives_from_connections( + port_1_m_flow=snapshot.outlet_node_to_line_flow, + connected_h_1=snapshot.outlet_node.h, + port_2_m_flow=-snapshot.outlet_line_to_chamber_flow, + connected_h_2=snapshot.chamber.h, + ) + chamber = self.components.volume + spec = self.components.spec + chamber_derivative = chamber.derivatives_from_two_connections( + port_a_m_flow=self._port(chamber, "port_1").m_flow, + connected_h_a=( + snapshot.inlet_line.h + if spec.volume_inlet_port == "port_1" + else snapshot.outlet_line.h + ), + port_b_m_flow=self._port(chamber, "port_2").m_flow, + connected_h_b=( + snapshot.inlet_line.h + if spec.volume_inlet_port == "port_2" + else snapshot.outlet_line.h + ), + internal_h=snapshot.chamber.h, + ) + return [ + *inlet_derivative.as_vector(), + *chamber_derivative.as_vector(), + *outlet_derivative.as_vector(), + ] diff --git a/PythonModels/systems/test_mql_nodes.py b/PythonModels/systems/test_mql_nodes.py new file mode 100644 index 0000000..6bff89d --- /dev/null +++ b/PythonModels/systems/test_mql_nodes.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from PythonModels.systems.test_mql import COMPONENT_SPECS + + +@dataclass(frozen=True) +class TestMqlPneumaticNode3Balance: + 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 + + +@dataclass(frozen=True) +class TestMqlPneumaticNode3: + """Exact algebraic contract of AMESim ``PN3NODE2``. + + Pressure and temperature are fixed by port 2 and duplicated to ports 1 and + 3. Flow and volume signals at port 2 are the sums of ports 1 and 3, matching + the ``EXPRESS2`` equations stored in ``test_mql_.cir``. + """ + + 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_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, + ) -> TestMqlPneumaticNode3Balance: + 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 TestMqlPneumaticNode3Balance( + 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_2_mass_flow_g_s=port_1_mass_flow_g_s + port_3_mass_flow_g_s, + port_2_volume_derivative_l_min=( + port_1_volume_derivative_l_min + port_3_volume_derivative_l_min + ), + port_2_volume_cm3=port_1_volume_cm3 + port_3_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, + ) + + +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" + } diff --git a/tests/test_test_mql_nodes.py b/tests/test_test_mql_nodes.py new file mode 100644 index 0000000..8c4bea0 --- /dev/null +++ b/tests/test_test_mql_nodes.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import unittest +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 + + +REPO_ROOT = Path(__file__).resolve().parents[1] +TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame" + + +class TestMqlPneumaticNode3Tests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.nodes = build_test_mql_node3_assembly() + cls.results = load_test_mql_amesim_results(TEST_MQL_AME) + + def test_builds_all_eight_pn3node2_instances(self) -> None: + self.assertEqual(len(self.nodes), 8) + self.assertIn("pn_node3_9", self.nodes) + system = TestMqlSystem() + self.assertEqual(system.typed_node3_count, 8) + self.assertEqual(system.node3_assembly["pn_node3_9"].alias, "pn_node3_9") + + def test_port_two_flow_is_exact_sum_of_ports_one_and_three(self) -> None: + balance = self.nodes["pn_node3_9"].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_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, + ) + + self.assertEqual(balance.temperature_k, 281.0) + self.assertEqual(balance.pressure_pa, 4.4e6) + self.assertEqual(balance.port_2_enthalpy_flow_w, 1.5) + self.assertEqual(balance.port_2_mass_flow_g_s, 2.0) + self.assertEqual(balance.port_2_volume_derivative_l_min, 3.0) + self.assertEqual(balance.port_2_volume_cm3, 11.0) + + def test_pn_node3_9_baseline_matches_adjacent_line_flow_sum(self) -> None: + index = 500 + balance = self.nodes["pn_node3_9"].balance( + port_2_temperature_k=self.results.series("t1@pneumatic_88")[index], + port_2_pressure_pa=( + self.results.series("p1@pneumatic_88")[index] + 101_300.0 + ), + port_1_enthalpy_flow_w=self.results.series("dh1@pneumatic_100")[index], + port_1_mass_flow_g_s=self.results.series("dm1@pneumatic_100")[index], + port_3_enthalpy_flow_w=self.results.series("dh1@pneumatic_97")[index], + port_3_mass_flow_g_s=self.results.series("dm1@pneumatic_97")[index], + ) + + self.assertAlmostEqual( + balance.port_2_mass_flow_g_s, + self.results.series("dm2@pn_node3_9")[index], + ) + self.assertAlmostEqual( + balance.port_2_enthalpy_flow_w, + self.results.series("dh2@pn_node3_9")[index], + ) + self.assertEqual(self.results.series("vol2@pn_node3_9")[index], 0.0) + self.assertEqual(self.results.series("dvol2@pn_node3_9")[index], 0.0) + + def test_rejects_nonphysical_primary_pressure_or_temperature(self) -> None: + node = self.nodes["pn_node3_9"] + 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, + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_test_mql_pnl0001_segment.py b/tests/test_test_mql_pnl0001_segment.py index 3e251ee..f91f7f9 100644 --- a/tests/test_test_mql_pnl0001_segment.py +++ b/tests/test_test_mql_pnl0001_segment.py @@ -76,5 +76,78 @@ class TestMqlPnl0001ChamberSegmentTests(unittest.TestCase): self.assertEqual([len(row) for row in solution.y], [3, 3, 3, 3]) +class TestMqlPnl0001PairChamberSegmentTests(unittest.TestCase): + def setUp(self) -> None: + self.system = TestMqlSystem() + self.spec = self.system.discover_pneumatic_branch_topology().chamber_segment_specs[0] + + def test_pair_closure_uses_both_topology_derived_lines(self) -> None: + closure = self.system.pnl0001_pair_chamber_segment_closure_from_spec( + self.spec, + inlet_node_pressure_pa=16.0e6, + outlet_node_pressure_pa=14.0e6, + ) + + snapshot = closure.snapshot() + rhs = closure.rhs(closure.initial_state_vector()) + + self.assertEqual(closure.components.inlet_line.name, "pneumatic_96") + self.assertEqual(closure.components.outlet_line.name, "pneumatic_97") + self.assertEqual(len(closure.initial_state_vector()), 6) + self.assertEqual(len(rhs), 6) + self.assertGreater(snapshot.inlet_node_to_line_flow, 0.0) + self.assertLess(snapshot.outlet_node_to_line_flow, 0.0) + self.assertAlmostEqual( + rhs[0] + rhs[2] + rhs[4], + snapshot.inlet_node_to_line_flow + snapshot.outlet_node_to_line_flow, + delta=1.0e-12, + ) + + def test_both_internal_orifice_flows_cancel_from_total_mass(self) -> None: + closure = self.system.pnl0001_pair_chamber_segment_closure_from_spec( + self.spec, + inlet_node_pressure_pa=15.3e6, + outlet_node_pressure_pa=15.3e6, + ) + state = closure.initial_state_vector() + state[0] *= 1.01 + state[1] *= 1.01 + state[4] *= 0.99 + state[5] *= 0.99 + + snapshot = closure.snapshot(state) + rhs = closure.rhs(state) + + self.assertGreater(snapshot.inlet_line_to_chamber_flow, 0.0) + self.assertLess(snapshot.outlet_line_to_chamber_flow, 0.0) + self.assertAlmostEqual( + rhs[0] + rhs[2] + rhs[4], + snapshot.inlet_node_to_line_flow + snapshot.outlet_node_to_line_flow, + delta=1.0e-12, + ) + self.assertAlmostEqual( + closure.components.outlet_line.port_2.m_flow, + -snapshot.outlet_line_to_chamber_flow, + ) + + def test_simulates_six_state_pair_line_chamber_segment(self) -> None: + solution = self.system.simulate_pnl0001_pair_chamber_segment_from_spec( + self.spec, + inlet_node_pressure_pa=15.31e6, + outlet_node_pressure_pa=15.29e6, + config=SolveIVPConfig( + t_start=0.0, + t_stop=1.0e-7, + max_step=1.0e-8, + ), + t_eval=[0.0, 5.0e-8, 1.0e-7], + ) + + self.assertTrue(solution.success) + self.assertEqual(len(solution.t), 3) + self.assertEqual(len(solution.y), 6) + self.assertEqual([len(row) for row in solution.y], [3, 3, 3, 3, 3, 3]) + + if __name__ == "__main__": unittest.main()