From fafce0af873550e21e342c99f7f7c8a5c8d97679 Mon Sep 17 00:00:00 2001 From: huojiarong Date: Fri, 17 Jul 2026 09:51:54 +0000 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5test=5Fmql=20P4=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=B1=80=E9=83=A8=E9=97=AD=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PythonModels/systems/test_mql.py | 186 ++++++++++++ PythonModels/systems/test_mql_closure.py | 366 +++++++++++++++++++++++ tests/test_test_mql_pnl0001_segment.py | 95 ++++++ 3 files changed, 647 insertions(+) diff --git a/PythonModels/systems/test_mql.py b/PythonModels/systems/test_mql.py index ffff67a..d949fa1 100644 --- a/PythonModels/systems/test_mql.py +++ b/PythonModels/systems/test_mql.py @@ -3963,6 +3963,48 @@ class TestMqlSystem: def typed_node4_count(self) -> int: return len(self.node4_assembly) + def _pnl0001_spec_for_node_port(self, node_alias: str, port_name: str): + for spec in self.pnl0001_assembly.specs: + if spec.source_component == node_alias and spec.source_port == port_name: + return spec + if spec.target_component == node_alias and spec.target_port == port_name: + return spec + raise KeyError(f"No PNL0001 line attached to {node_alias}.{port_name}") + + def _pnl0001_line_for_node_port(self, node_alias: str, port_name: str): + spec = self._pnl0001_spec_for_node_port(node_alias, port_name) + return self.pnl0001_assembly.lines[spec.alias] + + def _pnl0002_spec_for_node_port(self, node_alias: str, port_name: str): + for spec in self.pnl0002_assembly.specs: + if spec.source_component == node_alias and spec.source_port == port_name: + return spec + if spec.target_component == node_alias and spec.target_port == port_name: + return spec + raise KeyError(f"No PNL0002 line attached to {node_alias}.{port_name}") + + def _pnl0002_line_for_node_port(self, node_alias: str, port_name: str): + spec = self._pnl0002_spec_for_node_port(node_alias, port_name) + return self.pnl0002_assembly.lines[spec.alias] + + def _p4_node_alias_for_orifice_port_2(self, orifice_alias: str) -> str: + for spec in CONNECTION_SPECS: + if spec["submodel"] != "DIRECT": + continue + if ( + spec["source_component"] == orifice_alias + and spec["source_port"] == "port_2" + and str(spec["target_component"]) in self.node4_assembly + ): + return str(spec["target_component"]) + if ( + spec["target_component"] == orifice_alias + and spec["target_port"] == "port_2" + and str(spec["source_component"]) in self.node4_assembly + ): + return str(spec["source_component"]) + raise KeyError(f"No P4 node attached to {orifice_alias}.port_2") + 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: @@ -4430,6 +4472,150 @@ class TestMqlSystem: t_eval=t_eval, ) + def pn3_p4_node_chamber_segment_closure_from_spec( + self, + spec, + *, + inlet_node_pressure_pa: float, + resistance_boundary_pressure_pa: float, + inlet_node_temperature_k: float = 293.15, + resistance_boundary_temperature_k: float = 293.15, + p4_primary_boundary_pressure_pa: float = 100_000.0, + p4_primary_boundary_temperature_k: float = 293.15, + p4_port1_boundary_pressure_pa: float = 100_000.0, + p4_port1_boundary_temperature_k: float = 293.15, + p4_port3_boundary_pressure_pa: float = 100_000.0, + p4_port3_boundary_temperature_k: float = 293.15, + ): + """Close a PN3 chamber segment through the adjacent real P4 node. + + The P4 primary pressure/temperature is supplied by the PNL0001 line on + P4 port 2. Ports 1 and 3 use the adjacent PNL0002 center compliances; + their far P4 nodes remain prescribed boundaries in this local closure. + """ + from PythonModels.components.amesim_pneumatic import ( + AmesimPneumaticOrifice, + AmesimPneumaticVolume, + ) + from PythonModels.systems.test_mql_closure import ( + TestMqlPneumaticBoundaryCondition, + TestMqlPn3P4NodeChamberSegmentClosure, + TestMqlPn3P4NodeChamberSegmentComponents, + ) + + inlet_line = self.pnl0001_assembly.lines[spec.inlet_line_alias] + outlet_line = self.pnl0001_assembly.lines[spec.outlet_line_alias] + node_line_spec = self._pnl0003_spec_for_node_port( + spec.outlet_node_alias, + "port_2", + ) + node_line = self.pnl0003_assembly.lines[node_line_spec.alias] + node_orifice_alias = ( + node_line_spec.target_component + if node_line_spec.source_component == spec.outlet_node_alias + else node_line_spec.source_component + ) + p4_node_alias = self._p4_node_alias_for_orifice_port_2(node_orifice_alias) + node_orifice = self.pneumatic_assembly.variable_orifices[node_orifice_alias] + node_resistance = self._pnl00r_line_for_node_port( + spec.outlet_node_alias, + "port_1", + ) + node = self.node3_assembly[spec.outlet_node_alias] + p4_node = self.node4_assembly[p4_node_alias] + p4_primary_line = self._pnl0001_line_for_node_port(p4_node_alias, "port_2") + p4_port1_line = self._pnl0002_line_for_node_port(p4_node_alias, "port_1") + p4_port3_line = self._pnl0002_line_for_node_port(p4_node_alias, "port_3") + 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 TestMqlPn3P4NodeChamberSegmentClosure( + components=TestMqlPn3P4NodeChamberSegmentComponents( + inlet_line=inlet_line, + outlet_line=outlet_line, + node_line=node_line, + node_resistance=node_resistance, + node_orifice=node_orifice, + node=node, + p4_node=p4_node, + p4_primary_line=p4_primary_line, + p4_port1_line=p4_port1_line, + p4_port3_line=p4_port3_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, + ), + resistance_boundary=TestMqlPneumaticBoundaryCondition( + pressure_pa=resistance_boundary_pressure_pa, + temperature_k=resistance_boundary_temperature_k, + ), + p4_primary_boundary=TestMqlPneumaticBoundaryCondition( + pressure_pa=p4_primary_boundary_pressure_pa, + temperature_k=p4_primary_boundary_temperature_k, + ), + p4_port1_boundary=TestMqlPneumaticBoundaryCondition( + pressure_pa=p4_port1_boundary_pressure_pa, + temperature_k=p4_port1_boundary_temperature_k, + ), + p4_port3_boundary=TestMqlPneumaticBoundaryCondition( + pressure_pa=p4_port3_boundary_pressure_pa, + temperature_k=p4_port3_boundary_temperature_k, + ), + ) + + def simulate_pn3_p4_node_chamber_segment_from_spec( + self, + spec, + *, + inlet_node_pressure_pa: float, + resistance_boundary_pressure_pa: float, + inlet_node_temperature_k: float = 293.15, + resistance_boundary_temperature_k: float = 293.15, + p4_primary_boundary_pressure_pa: float = 100_000.0, + p4_primary_boundary_temperature_k: float = 293.15, + p4_port1_boundary_pressure_pa: float = 100_000.0, + p4_port1_boundary_temperature_k: float = 293.15, + p4_port3_boundary_pressure_pa: float = 100_000.0, + p4_port3_boundary_temperature_k: float = 293.15, + config: SolveIVPConfig | None = None, + t_eval: list[float] | None = None, + ): + closure = self.pn3_p4_node_chamber_segment_closure_from_spec( + spec, + inlet_node_pressure_pa=inlet_node_pressure_pa, + resistance_boundary_pressure_pa=resistance_boundary_pressure_pa, + inlet_node_temperature_k=inlet_node_temperature_k, + resistance_boundary_temperature_k=resistance_boundary_temperature_k, + p4_primary_boundary_pressure_pa=p4_primary_boundary_pressure_pa, + p4_primary_boundary_temperature_k=p4_primary_boundary_temperature_k, + p4_port1_boundary_pressure_pa=p4_port1_boundary_pressure_pa, + p4_port1_boundary_temperature_k=p4_port1_boundary_temperature_k, + p4_port3_boundary_pressure_pa=p4_port3_boundary_pressure_pa, + p4_port3_boundary_temperature_k=p4_port3_boundary_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 baac37f..3637405 100644 --- a/PythonModels/systems/test_mql_closure.py +++ b/PythonModels/systems/test_mql_closure.py @@ -10,6 +10,7 @@ from PythonModels.components.amesim_pneumatic import ( ) from PythonModels.components.amesim_pneumatic_line import ( AmesimPnl0001Pipe, + AmesimPnl0002Pipe, AmesimPnl0003Pipe, AmesimPnl00rPipe, ) @@ -19,6 +20,8 @@ from PythonModels.core.state import VolumeState from PythonModels.systems.test_mql_nodes import ( TestMqlPneumaticNode3, TestMqlPneumaticNode3Balance, + TestMqlPneumaticNode4, + TestMqlPneumaticNode4Balance, ) @@ -185,6 +188,369 @@ class TestMqlPn3NodeChamberSegmentSnapshot: pnl0003_to_p4_flow: float +@dataclass(frozen=True) +class TestMqlPn3P4NodeChamberSegmentComponents: + inlet_line: AmesimPnl0001Pipe + outlet_line: AmesimPnl0001Pipe + node_line: AmesimPnl0003Pipe + node_resistance: AmesimPnl00rPipe + node_orifice: AmesimPneumaticOrifice + node: TestMqlPneumaticNode3 + p4_node: TestMqlPneumaticNode4 + p4_primary_line: AmesimPnl0001Pipe + p4_port1_line: AmesimPnl0002Pipe + p4_port3_line: AmesimPnl0002Pipe + volume: AmesimPneumaticVolume + inlet_orifice: AmesimPneumaticOrifice + outlet_orifice: AmesimPneumaticOrifice + spec: TestMqlPneumaticChamberSegmentSpec + + +@dataclass(frozen=True) +class TestMqlPn3P4NodeChamberSegmentSnapshot: + inlet_line: ThermodynamicProperties + chamber: ThermodynamicProperties + outlet_line: ThermodynamicProperties + node_line_port_1: ThermodynamicProperties + node_line_port_2: ThermodynamicProperties + p4_primary_line: ThermodynamicProperties + p4_boundary: ThermodynamicProperties + p4_port1_line: ThermodynamicProperties + p4_port3_line: ThermodynamicProperties + inlet_node: ThermodynamicProperties + resistance_boundary: ThermodynamicProperties + p4_primary_boundary: ThermodynamicProperties + p4_port1_boundary: ThermodynamicProperties + p4_port3_boundary: ThermodynamicProperties + node_balance: TestMqlPneumaticNode3Balance + p4_balance: TestMqlPneumaticNode4Balance + inlet_node_to_line_flow: float + inlet_line_to_chamber_flow: float + outlet_node_to_line_flow: float + outlet_line_to_chamber_flow: float + node_to_resistance_flow: float + node_to_pnl0003_flow: float + pnl0003_center_flow: float + pnl0003_to_p4_flow: float + p4_boundary_to_primary_line_flow: float + p4_node_to_primary_line_flow: float + p4_to_port1_line_flow: float + p4_port1_boundary_to_line_flow: float + p4_to_port3_line_flow: float + p4_port3_boundary_to_line_flow: float + + +class TestMqlPn3P4NodeChamberSegmentClosure: + """PN3 chamber segment closed through the adjacent real P4 node. + + The P4 primary pressure/temperature comes from the PNL0001 storage on + ``port_2``. P4 ports 1 and 3 are represented by the adjacent PNL0002 center + compliances; their far nodes remain prescribed pressure boundaries. + """ + + def __init__( + self, + *, + components: TestMqlPn3P4NodeChamberSegmentComponents, + inlet_node: TestMqlPneumaticBoundaryCondition, + resistance_boundary: TestMqlPneumaticBoundaryCondition, + p4_primary_boundary: TestMqlPneumaticBoundaryCondition, + p4_port1_boundary: TestMqlPneumaticBoundaryCondition, + p4_port3_boundary: TestMqlPneumaticBoundaryCondition, + ) -> None: + self.components = components + self.inlet_node = inlet_node + self.resistance_boundary = resistance_boundary + self.p4_primary_boundary = p4_primary_boundary + self.p4_port1_boundary = p4_port1_boundary + self.p4_port3_boundary = p4_port3_boundary + + 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(), + *self.components.node_line.get_state_vector(), + *self.components.p4_primary_line.get_state_vector(), + *self.components.p4_port1_line.get_state_vector(), + *self.components.p4_port3_line.get_state_vector(), + ] + + def apply_state_vector(self, values: list[float]) -> None: + if len(values) != 16: + raise ValueError("PN3/P4 node chamber segment state vector requires sixteen 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:6]) + self.components.node_line.set_state_vector(values[6:10]) + self.components.p4_primary_line.set_state_vector(values[10:12]) + self.components.p4_port1_line.set_state_vector(values[12:14]) + self.components.p4_port3_line.set_state_vector(values[14:]) + + def snapshot( + self, + state_vector: list[float] | None = None, + ) -> TestMqlPn3P4NodeChamberSegmentSnapshot: + 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() + node_line_port_1 = self.components.node_line.properties_1() + node_line_port_2 = self.components.node_line.properties_2() + p4_primary_line = self.components.p4_primary_line.properties() + p4_port1_line = self.components.p4_port1_line.properties() + p4_port3_line = self.components.p4_port3_line.properties() + inlet_node = self.inlet_node.properties(self.components.inlet_line.gas) + resistance_boundary = self.resistance_boundary.properties( + self.components.node_resistance.gas + ) + p4_primary_boundary = self.p4_primary_boundary.properties( + self.components.p4_primary_line.gas + ) + p4_port1_boundary = self.p4_port1_boundary.properties( + self.components.p4_port1_line.gas + ) + p4_port3_boundary = self.p4_port3_boundary.properties( + self.components.p4_port3_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=node_line_port_1.p, + port_1_temperature_k=node_line_port_1.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, + ) + node_to_resistance_flow = self.components.node_resistance.mass_flow( + port_1_pressure_pa=node_line_port_1.p, + port_1_temperature_k=node_line_port_1.T, + port_2_pressure_pa=resistance_boundary.p, + port_2_temperature_k=resistance_boundary.T, + ) + node_balance = self.components.node.balance( + port_2_temperature_k=node_line_port_1.T, + port_2_pressure_pa=node_line_port_1.p, + port_1_enthalpy_flow_w=self._enthalpy_flow_from_node( + flow_kg_s=node_to_resistance_flow, + node_h=node_line_port_1.h, + connected_h=resistance_boundary.h, + ), + port_1_mass_flow_g_s=node_to_resistance_flow * 1.0e3, + port_3_enthalpy_flow_w=self._enthalpy_flow_from_node( + flow_kg_s=outlet_node_to_line_flow, + node_h=node_line_port_1.h, + connected_h=outlet_line.h, + ), + port_3_mass_flow_g_s=outlet_node_to_line_flow * 1.0e3, + ) + node_to_pnl0003_flow = -node_balance.port_2_mass_flow_g_s * 1.0e-3 + pnl0003_center_flow = self.components.node_line.resistance_mass_flow() + pnl0003_to_p4_flow = self._line_to_p4_flow( + line=node_line_port_2, + p4_boundary=p4_primary_line, + orifice=self.components.node_orifice, + ) + + p4_boundary_to_primary_line_flow = ( + self.components.p4_primary_line.resistance_mass_flow( + port_1_pressure_pa=p4_primary_boundary.p, + port_1_temperature_k=p4_primary_boundary.T, + ) + ) + p4_to_port1_line_flow = self.components.p4_port1_line.port_mass_flow( + port_pressure_pa=p4_primary_line.p, + port_temperature_k=p4_primary_line.T, + ) + p4_port1_boundary_to_line_flow = self.components.p4_port1_line.port_mass_flow( + port_pressure_pa=p4_port1_boundary.p, + port_temperature_k=p4_port1_boundary.T, + ) + p4_to_port3_line_flow = self.components.p4_port3_line.port_mass_flow( + port_pressure_pa=p4_primary_line.p, + port_temperature_k=p4_primary_line.T, + ) + p4_port3_boundary_to_line_flow = self.components.p4_port3_line.port_mass_flow( + port_pressure_pa=p4_port3_boundary.p, + port_temperature_k=p4_port3_boundary.T, + ) + p4_balance = self.components.p4_node.balance( + port_2_temperature_k=p4_primary_line.T, + port_2_pressure_pa=p4_primary_line.p, + port_1_enthalpy_flow_w=self._enthalpy_flow_from_node( + flow_kg_s=p4_to_port1_line_flow, + node_h=p4_primary_line.h, + connected_h=p4_port1_line.h, + ), + port_1_mass_flow_g_s=-p4_to_port1_line_flow * 1.0e3, + port_3_enthalpy_flow_w=self._enthalpy_flow_from_node( + flow_kg_s=p4_to_port3_line_flow, + node_h=p4_primary_line.h, + connected_h=p4_port3_line.h, + ), + port_3_mass_flow_g_s=-p4_to_port3_line_flow * 1.0e3, + port_4_enthalpy_flow_w=self._enthalpy_flow_from_node( + flow_kg_s=pnl0003_to_p4_flow, + node_h=node_line_port_2.h, + connected_h=p4_primary_line.h, + ), + port_4_mass_flow_g_s=pnl0003_to_p4_flow * 1.0e3, + ) + p4_node_to_primary_line_flow = p4_balance.port_2_mass_flow_g_s * 1.0e-3 + + snapshot = TestMqlPn3P4NodeChamberSegmentSnapshot( + inlet_line=inlet_line, + chamber=chamber, + outlet_line=outlet_line, + node_line_port_1=node_line_port_1, + node_line_port_2=node_line_port_2, + p4_primary_line=p4_primary_line, + p4_boundary=p4_primary_line, + p4_port1_line=p4_port1_line, + p4_port3_line=p4_port3_line, + inlet_node=inlet_node, + resistance_boundary=resistance_boundary, + p4_primary_boundary=p4_primary_boundary, + p4_port1_boundary=p4_port1_boundary, + p4_port3_boundary=p4_port3_boundary, + node_balance=node_balance, + p4_balance=p4_balance, + 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, + node_to_resistance_flow=node_to_resistance_flow, + node_to_pnl0003_flow=node_to_pnl0003_flow, + pnl0003_center_flow=pnl0003_center_flow, + pnl0003_to_p4_flow=pnl0003_to_p4_flow, + p4_boundary_to_primary_line_flow=p4_boundary_to_primary_line_flow, + p4_node_to_primary_line_flow=p4_node_to_primary_line_flow, + p4_to_port1_line_flow=p4_to_port1_line_flow, + p4_port1_boundary_to_line_flow=p4_port1_boundary_to_line_flow, + p4_to_port3_line_flow=p4_to_port3_line_flow, + p4_port3_boundary_to_line_flow=p4_port3_boundary_to_line_flow, + ) + self._write_port_states(snapshot) + return snapshot + + @staticmethod + def _line_to_chamber_flow( + *, + line: ThermodynamicProperties, + chamber: ThermodynamicProperties, + orifice: AmesimPneumaticOrifice, + ) -> float: + return TestMqlPn3NodeChamberSegmentClosure._line_to_chamber_flow( + line=line, + chamber=chamber, + orifice=orifice, + ) + + @staticmethod + def _line_to_p4_flow( + *, + line: ThermodynamicProperties, + p4_boundary: ThermodynamicProperties, + orifice: AmesimPneumaticOrifice, + ) -> float: + return TestMqlPn3NodeChamberSegmentClosure._line_to_p4_flow( + line=line, + p4_boundary=p4_boundary, + orifice=orifice, + ) + + @staticmethod + def _port( + component: AmesimPneumaticVolume | AmesimPneumaticOrifice, + port_name: str, + ) -> PortState: + return TestMqlPn3NodeChamberSegmentClosure._port(component, port_name) + + @staticmethod + def _enthalpy_flow_from_node( + *, + flow_kg_s: float, + node_h: float, + connected_h: float, + ) -> float: + return TestMqlPn3NodeChamberSegmentClosure._enthalpy_flow_from_node( + flow_kg_s=flow_kg_s, + node_h=node_h, + connected_h=connected_h, + ) + + def _write_port_states( + self, + snapshot: TestMqlPn3P4NodeChamberSegmentSnapshot, + ) -> None: + TestMqlPn3NodeChamberSegmentClosure._write_port_states(self, snapshot) + + p4_primary_line = self.components.p4_primary_line + p4_primary_line.port_1.p = snapshot.p4_primary_boundary.p + p4_primary_line.port_1.m_flow = snapshot.p4_boundary_to_primary_line_flow + p4_primary_line.port_1.h_outflow = snapshot.p4_primary_line.h + p4_primary_line.port_2.p = snapshot.p4_primary_line.p + p4_primary_line.port_2.m_flow = snapshot.p4_node_to_primary_line_flow + p4_primary_line.port_2.h_outflow = snapshot.p4_primary_line.h + + p4_port1_line = self.components.p4_port1_line + p4_port1_line.port_1.p = snapshot.p4_primary_line.p + p4_port1_line.port_1.m_flow = snapshot.p4_to_port1_line_flow + p4_port1_line.port_1.h_outflow = snapshot.p4_port1_line.h + p4_port1_line.port_2.p = snapshot.p4_port1_boundary.p + p4_port1_line.port_2.m_flow = snapshot.p4_port1_boundary_to_line_flow + p4_port1_line.port_2.h_outflow = snapshot.p4_port1_line.h + + p4_port3_line = self.components.p4_port3_line + p4_port3_line.port_1.p = snapshot.p4_port3_boundary.p + p4_port3_line.port_1.m_flow = snapshot.p4_port3_boundary_to_line_flow + p4_port3_line.port_1.h_outflow = snapshot.p4_port3_line.h + p4_port3_line.port_2.p = snapshot.p4_primary_line.p + p4_port3_line.port_2.m_flow = snapshot.p4_to_port3_line_flow + p4_port3_line.port_2.h_outflow = snapshot.p4_port3_line.h + + def rhs(self, state_vector: list[float]) -> list[float]: + snapshot = self.snapshot(state_vector) + base_rhs = TestMqlPn3NodeChamberSegmentClosure.rhs(self, state_vector)[:10] + p4_primary_derivative = ( + self.components.p4_primary_line.derivatives_from_connections( + port_1_m_flow=snapshot.p4_boundary_to_primary_line_flow, + connected_h_1=snapshot.p4_primary_boundary.h, + port_2_m_flow=snapshot.p4_node_to_primary_line_flow, + connected_h_2=snapshot.p4_primary_line.h, + ) + ) + p4_port1_derivative = self.components.p4_port1_line.derivatives_from_connections( + port_1_m_flow=snapshot.p4_to_port1_line_flow, + connected_h_1=snapshot.p4_primary_line.h, + port_2_m_flow=snapshot.p4_port1_boundary_to_line_flow, + connected_h_2=snapshot.p4_port1_boundary.h, + ) + p4_port3_derivative = self.components.p4_port3_line.derivatives_from_connections( + port_1_m_flow=snapshot.p4_port3_boundary_to_line_flow, + connected_h_1=snapshot.p4_port3_boundary.h, + port_2_m_flow=snapshot.p4_to_port3_line_flow, + connected_h_2=snapshot.p4_primary_line.h, + ) + return [ + *base_rhs, + *p4_primary_derivative.as_vector(), + *p4_port1_derivative.as_vector(), + *p4_port3_derivative.as_vector(), + ] + + @dataclass(frozen=True) class TestMqlPneumaticBranchComponents: name: str diff --git a/tests/test_test_mql_pnl0001_segment.py b/tests/test_test_mql_pnl0001_segment.py index e4d10dc..dec16fd 100644 --- a/tests/test_test_mql_pnl0001_segment.py +++ b/tests/test_test_mql_pnl0001_segment.py @@ -246,5 +246,100 @@ class TestMqlPn3NodeChamberSegmentTests(unittest.TestCase): self.assertEqual([len(row) for row in solution.y], [3] * 10) +class TestMqlPn3P4NodeChamberSegmentTests(unittest.TestCase): + def setUp(self) -> None: + self.system = TestMqlSystem() + self.spec = self.system.discover_pneumatic_branch_topology().chamber_segment_specs[0] + + def test_closure_replaces_p4_pressure_boundary_with_real_node_and_lines(self) -> None: + closure = self.system.pn3_p4_node_chamber_segment_closure_from_spec( + self.spec, + inlet_node_pressure_pa=15.31e6, + resistance_boundary_pressure_pa=15.29e6, + ) + + snapshot = closure.snapshot() + rhs = closure.rhs(closure.initial_state_vector()) + + self.assertEqual(closure.components.node_orifice.name, "pn_morifice_9") + self.assertEqual(closure.components.p4_node.alias, "pnnode4_17") + self.assertEqual(closure.components.p4_primary_line.name, "pneumatic_68") + self.assertEqual(closure.components.p4_port1_line.name, "pneumatic_86") + self.assertEqual(closure.components.p4_port3_line.name, "pneumatic_83") + self.assertEqual(len(closure.initial_state_vector()), 16) + self.assertEqual(len(rhs), 16) + self.assertAlmostEqual( + snapshot.p4_balance.pressure_pa, + snapshot.p4_primary_line.p, + ) + self.assertAlmostEqual( + snapshot.p4_balance.temperature_k, + snapshot.p4_primary_line.T, + ) + + def test_p4_node_flow_cancels_between_pnvo_pnl0002_and_primary_line(self) -> None: + closure = self.system.pn3_p4_node_chamber_segment_closure_from_spec( + self.spec, + inlet_node_pressure_pa=15.31e6, + resistance_boundary_pressure_pa=15.29e6, + ) + + snapshot = closure.snapshot() + rhs = closure.rhs(closure.initial_state_vector()) + + self.assertGreater(snapshot.pnl0003_to_p4_flow, 0.0) + self.assertAlmostEqual( + snapshot.p4_balance.port_2_mass_flow_g_s, + ( + -snapshot.p4_to_port1_line_flow + - snapshot.p4_to_port3_line_flow + + snapshot.pnl0003_to_p4_flow + ) + * 1.0e3, + ) + self.assertAlmostEqual( + snapshot.p4_node_to_primary_line_flow, + snapshot.p4_balance.port_2_mass_flow_g_s * 1.0e-3, + ) + self.assertAlmostEqual( + snapshot.p4_node_to_primary_line_flow + + snapshot.p4_to_port1_line_flow + + snapshot.p4_to_port3_line_flow + - snapshot.pnl0003_to_p4_flow, + 0.0, + delta=1.0e-12, + ) + mass_derivative_sum = sum(rhs[index] for index in range(0, 16, 2)) + self.assertAlmostEqual( + mass_derivative_sum, + ( + snapshot.inlet_node_to_line_flow + - snapshot.node_to_resistance_flow + + snapshot.p4_boundary_to_primary_line_flow + + snapshot.p4_port1_boundary_to_line_flow + + snapshot.p4_port3_boundary_to_line_flow + ), + delta=1.0e-12, + ) + + def test_simulates_sixteen_state_pn3_p4_node_chamber_segment(self) -> None: + solution = self.system.simulate_pn3_p4_node_chamber_segment_from_spec( + self.spec, + inlet_node_pressure_pa=15.31e6, + resistance_boundary_pressure_pa=15.29e6, + config=SolveIVPConfig( + t_start=0.0, + t_stop=1.0e-10, + max_step=1.0e-11, + ), + t_eval=[0.0, 5.0e-11, 1.0e-10], + ) + + self.assertTrue(solution.success) + self.assertEqual(len(solution.t), 3) + self.assertEqual(len(solution.y), 16) + self.assertEqual([len(row) for row in solution.y], [3] * 16) + + if __name__ == "__main__": unittest.main()