diff --git a/PythonModels/systems/test_mql.py b/PythonModels/systems/test_mql.py index 86ab151..b1a1572 100644 --- a/PythonModels/systems/test_mql.py +++ b/PythonModels/systems/test_mql.py @@ -3915,6 +3915,49 @@ class TestMqlSystem: source=source, ) + def discover_pneumatic_branch_topology(self): + from PythonModels.systems.test_mql_closure import ( + TestMqlPneumaticTopologyCandidate, + TestMqlPneumaticTopologyDiscovery, + ) + + typed_aliases = set(self._pneumatic_components_by_alias()) + blocked_candidates = [] + for spec in CONNECTION_SPECS: + source_component = str(spec["source_component"]) + target_component = str(spec["target_component"]) + source_is_typed = source_component in typed_aliases + target_is_typed = target_component in typed_aliases + if not (source_is_typed or target_is_typed): + continue + + line_submodel = str(spec["submodel"]) + if source_is_typed and target_is_typed: + reason = "direct typed pneumatic connection is not a complete volume-orifice-volume branch" + elif line_submodel == "DIRECT": + reason = "requires AMESim boundary component interpretation before closure construction" + else: + reason = "requires AMESim node/line submodel interpretation before closure construction" + + blocked_candidates.append( + TestMqlPneumaticTopologyCandidate( + connection_alias=str(spec["alias"]), + line_submodel=line_submodel, + source_component=source_component, + source_port=str(spec["source_port"]), + target_component=target_component, + target_port=str(spec["target_port"]), + source_is_typed_pneumatic=source_is_typed, + target_is_typed_pneumatic=target_is_typed, + reason=reason, + ) + ) + + return TestMqlPneumaticTopologyDiscovery( + branch_specs=(), + blocked_candidates=tuple(blocked_candidates), + ) + 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 7c84ac2..d7c0488 100644 --- a/PythonModels/systems/test_mql_closure.py +++ b/PythonModels/systems/test_mql_closure.py @@ -20,6 +20,25 @@ class TestMqlPneumaticBranchSpec: source: str = "manual" +@dataclass(frozen=True) +class TestMqlPneumaticTopologyCandidate: + connection_alias: str + line_submodel: str + source_component: str + source_port: str + target_component: str + target_port: str + source_is_typed_pneumatic: bool + target_is_typed_pneumatic: bool + reason: str + + +@dataclass(frozen=True) +class TestMqlPneumaticTopologyDiscovery: + branch_specs: tuple[TestMqlPneumaticBranchSpec, ...] + blocked_candidates: tuple[TestMqlPneumaticTopologyCandidate, ...] + + @dataclass(frozen=True) class TestMqlPneumaticBranchComponents: name: str diff --git a/tests/test_test_mql_branch_simulation.py b/tests/test_test_mql_branch_simulation.py index 2002d74..9869c22 100644 --- a/tests/test_test_mql_branch_simulation.py +++ b/tests/test_test_mql_branch_simulation.py @@ -86,6 +86,33 @@ class TestMqlPneumaticBranchSimulationTests(unittest.TestCase): self.assertGreater(series["pn_general_chamber.p"][0], series["pn_general_chamber.p"][-1]) self.assertLess(series["pn_c1_8.p"][0], series["pn_c1_8.p"][-1]) + def test_discover_pneumatic_branch_topology_reports_blocked_candidates(self) -> None: + system = TestMqlSystem() + + discovery = system.discover_pneumatic_branch_topology() + + self.assertEqual(discovery.branch_specs, ()) + self.assertEqual(len(discovery.blocked_candidates), 48) + candidates = { + candidate.connection_alias: candidate + for candidate in discovery.blocked_candidates + } + chamber_candidate = candidates["pneumatic_69"] + self.assertEqual(chamber_candidate.line_submodel, "PNL0001") + self.assertEqual(chamber_candidate.source_component, "pnnode4_16") + self.assertEqual(chamber_candidate.target_component, "pn_c1_8") + self.assertFalse(chamber_candidate.source_is_typed_pneumatic) + self.assertTrue(chamber_candidate.target_is_typed_pneumatic) + self.assertIn("node/line", chamber_candidate.reason) + + orifice_candidate = candidates["pneumatic_96"] + self.assertEqual(orifice_candidate.line_submodel, "PNL0001") + self.assertEqual(orifice_candidate.source_component, "pn_node3_8") + self.assertEqual(orifice_candidate.target_component, "pn_orifice_18") + self.assertFalse(orifice_candidate.source_is_typed_pneumatic) + self.assertTrue(orifice_candidate.target_is_typed_pneumatic) + self.assertIn("node/line", orifice_candidate.reason) + def test_simulate_pneumatic_branch_rejects_invalid_component_aliases(self) -> None: system = TestMqlSystem()