# Pure Python SPQR-Tree implementation. # Authors: # imacat@mail.imacat.idv.tw (imacat), 2026/3/2 # AI assistance: Claude Code (Anthropic) # Copyright (c) 2026 imacat. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for the SPQR-tree (_spqr.py). Tests cover: triangle K3 (S-node), K4 (R-node), C4 (S-node), two parallel edges (Q-node), and three parallel edges (P-node). """ import os import subprocess import sys import time import unittest from collections import deque from collections.abc import Hashable from spqrtree import ( Edge, MultiGraph, NodeType, SPQRNode, build_spqr_tree, ) def _make_k3() -> MultiGraph: """Build the triangle graph K3 (vertices 1,2,3). :return: A MultiGraph representing K3. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(1, 3) return g def _make_c4() -> MultiGraph: """Build the 4-cycle C4 (vertices 1,2,3,4). :return: A MultiGraph representing C4. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(3, 4) g.add_edge(4, 1) return g def _make_k4() -> MultiGraph: """Build the complete graph K4 (6 edges among vertices 1,2,3,4). :return: A MultiGraph representing K4. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(1, 3) g.add_edge(1, 4) g.add_edge(2, 3) g.add_edge(2, 4) g.add_edge(3, 4) return g def _make_two_parallel() -> MultiGraph: """Build a graph with 2 parallel edges between vertices 1 and 2. :return: A MultiGraph with two parallel edges. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(1, 2) return g def _make_three_parallel() -> MultiGraph: """Build a graph with 3 parallel edges between vertices 1 and 2. :return: A MultiGraph with three parallel edges. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(1, 2) g.add_edge(1, 2) return g def _collect_all_nodes(root: SPQRNode) -> list[SPQRNode]: """Collect all nodes in the SPQR-tree by BFS. :param root: The root SPQRNode. :return: List of all SPQRNode objects in BFS order. """ result: list[SPQRNode] = [] bfs_queue: deque[SPQRNode] = deque([root]) while bfs_queue: node: SPQRNode = bfs_queue.popleft() result.append(node) for child in node.children: bfs_queue.append(child) return result class TestSPQRNodeStructure(unittest.TestCase): """Tests for SPQRNode structure and attributes.""" def test_spqrnode_has_type(self) -> None: """Test that SPQRNode has a type attribute.""" root: SPQRNode = build_spqr_tree(_make_k3()) self.assertIsInstance(root.type, NodeType) def test_spqrnode_has_skeleton(self) -> None: """Test that SPQRNode has a skeleton graph.""" root: SPQRNode = build_spqr_tree(_make_k3()) self.assertIsInstance(root.skeleton, MultiGraph) def test_spqrnode_has_poles(self) -> None: """Test that SPQRNode has a poles attribute.""" root: SPQRNode = build_spqr_tree(_make_k3()) self.assertIsInstance(root.poles, tuple) self.assertEqual(len(root.poles), 2) def test_spqrnode_has_children(self) -> None: """Test that SPQRNode has a children list.""" root: SPQRNode = build_spqr_tree(_make_k3()) self.assertIsInstance(root.children, list) def test_spqrnode_parent_is_none_for_root(self) -> None: """Test that the root SPQRNode has parent = None.""" root: SPQRNode = build_spqr_tree(_make_k3()) self.assertIsNone(root.parent) def test_children_parent_links(self) -> None: """Test that children have correct parent links.""" root: SPQRNode = build_spqr_tree(_make_k3()) for child in root.children: self.assertIs(child.parent, root) class TestSPQRK3(unittest.TestCase): """Tests for the SPQR-tree of the triangle K3.""" def setUp(self) -> None: """Build the SPQR-tree for K3.""" self.root: SPQRNode = build_spqr_tree(_make_k3()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_returns_spqrnode(self) -> None: """Test that build_spqr_tree returns an SPQRNode.""" self.assertIsInstance(self.root, SPQRNode) def test_root_is_s_node(self) -> None: """Test that K3 produces an S-node (POLYGON) as root.""" self.assertEqual(self.root.type, NodeType.S) def test_node_types_are_valid(self) -> None: """Test that all node types are valid NodeType values.""" for node in self.all_nodes: self.assertIsInstance(node.type, NodeType) self.assertIn(node.type, list(NodeType)) class TestSPQRK4(unittest.TestCase): """Tests for the SPQR-tree of the complete graph K4.""" def setUp(self) -> None: """Build the SPQR-tree for K4.""" self.root: SPQRNode = build_spqr_tree(_make_k4()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_r_node(self) -> None: """Test that K4 produces a single R-node.""" self.assertEqual(self.root.type, NodeType.R) def test_skeleton_has_vertices(self) -> None: """Test that the R-node skeleton has vertices.""" self.assertGreater(self.root.skeleton.num_vertices(), 0) class TestSPQRC4(unittest.TestCase): """Tests for the SPQR-tree of the 4-cycle C4.""" def setUp(self) -> None: """Build the SPQR-tree for C4.""" self.root: SPQRNode = build_spqr_tree(_make_c4()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_s_node(self) -> None: """Test that C4 produces an S-node (POLYGON) as root.""" self.assertEqual(self.root.type, NodeType.S) def test_node_types_are_valid(self) -> None: """Test that all node types are valid NodeType values.""" for node in self.all_nodes: self.assertIsInstance(node.type, NodeType) class TestSPQRTwoParallel(unittest.TestCase): """Tests for the SPQR-tree of two parallel edges.""" def setUp(self) -> None: """Build the SPQR-tree for 2 parallel edges.""" self.root: SPQRNode = \ build_spqr_tree(_make_two_parallel()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_q_node(self) -> None: """Test that 2 parallel edges produce a single Q-node.""" # 2 parallel edges: BOND with 2 edges -> Q-node. self.assertEqual(self.root.type, NodeType.Q) def test_no_children(self) -> None: """Test that a Q-node has no children.""" self.assertEqual(len(self.root.children), 0) class TestSPQRThreeParallel(unittest.TestCase): """Tests for the SPQR-tree of three parallel edges.""" def setUp(self) -> None: """Build the SPQR-tree for 3 parallel edges.""" self.root: SPQRNode = \ build_spqr_tree(_make_three_parallel()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_p_node(self) -> None: """Test that 3 parallel edges produce a single P-node.""" self.assertEqual(self.root.type, NodeType.P) def test_node_types_are_valid(self) -> None: """Test that all node types are valid NodeType values.""" for node in self.all_nodes: self.assertIsInstance(node.type, NodeType) class TestSPQRInvariants(unittest.TestCase): """Tests for global SPQR-tree invariants across all graphs.""" def _check_parent_links(self, root: SPQRNode) -> None: """Check that parent-child links are consistent. :param root: The SPQR-tree root. :return: None """ for node in _collect_all_nodes(root): for child in node.children: self.assertIs( child.parent, node, f"Child {child.type} has wrong parent", ) def _check_skeleton_edges(self, root: SPQRNode) -> None: """Check that each node's skeleton has at least 1 edge. :param root: The SPQR-tree root. :return: None """ for node in _collect_all_nodes(root): self.assertGreater( node.skeleton.num_edges(), 0, f"Node {node.type} has empty skeleton", ) def test_k3_parent_links(self) -> None: """Test parent link invariant for K3.""" self._check_parent_links(build_spqr_tree(_make_k3())) def test_c4_parent_links(self) -> None: """Test parent link invariant for C4.""" self._check_parent_links(build_spqr_tree(_make_c4())) def test_k4_parent_links(self) -> None: """Test parent link invariant for K4.""" self._check_parent_links(build_spqr_tree(_make_k4())) def test_two_parallel_parent_links(self) -> None: """Test parent link invariant for 2 parallel edges.""" self._check_parent_links(build_spqr_tree(_make_two_parallel())) def test_three_parallel_parent_links(self) -> None: """Test parent link invariant for 3 parallel edges.""" self._check_parent_links( build_spqr_tree(_make_three_parallel()) ) def test_k3_skeleton_edges(self) -> None: """Test skeleton edge invariant for K3.""" self._check_skeleton_edges(build_spqr_tree(_make_k3())) def test_c4_skeleton_edges(self) -> None: """Test skeleton edge invariant for C4.""" self._check_skeleton_edges(build_spqr_tree(_make_c4())) def test_k4_skeleton_edges(self) -> None: """Test skeleton edge invariant for K4.""" self._check_skeleton_edges(build_spqr_tree(_make_k4())) def test_two_parallel_skeleton_edges(self) -> None: """Test skeleton edge invariant for 2 parallel edges.""" self._check_skeleton_edges( build_spqr_tree(_make_two_parallel()) ) def test_three_parallel_skeleton_edges(self) -> None: """Test skeleton edge invariant for 3 parallel edges.""" self._check_skeleton_edges( build_spqr_tree(_make_three_parallel()) ) def _make_diamond() -> MultiGraph: """Build the diamond graph (K4 minus one edge). Vertices 1,2,3,4; edges: 1-2, 1-3, 2-3, 2-4, 3-4. :return: A MultiGraph representing the diamond graph. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(1, 3) g.add_edge(2, 3) g.add_edge(2, 4) g.add_edge(3, 4) return g def _make_theta() -> MultiGraph: """Build the theta graph: two vertices connected by 3 paths. Vertices 1-5; edges: 1-3, 3-2, 1-4, 4-2, 1-5, 5-2. :return: A MultiGraph representing the theta graph. """ g: MultiGraph = MultiGraph() g.add_edge(1, 3) g.add_edge(3, 2) g.add_edge(1, 4) g.add_edge(4, 2) g.add_edge(1, 5) g.add_edge(5, 2) return g def _make_prism() -> MultiGraph: """Build the triangular prism graph. Two triangles connected by 3 edges. This graph is 3-connected. :return: A MultiGraph representing the triangular prism. """ g: MultiGraph = MultiGraph() # Top triangle g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(1, 3) # Bottom triangle g.add_edge(4, 5) g.add_edge(5, 6) g.add_edge(4, 6) # Connectors g.add_edge(1, 4) g.add_edge(2, 5) g.add_edge(3, 6) return g class TestSPQRDiamond(unittest.TestCase): """Tests for the SPQR-tree of the diamond graph.""" def setUp(self) -> None: """Build the SPQR-tree for the diamond graph.""" self.root: SPQRNode = \ build_spqr_tree(_make_diamond()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_at_least_two_nodes(self) -> None: """Test that diamond produces at least 2 SPQR-tree nodes.""" self.assertGreaterEqual( len(self.all_nodes), 2, "Diamond has a separation pair, expect >=2 SPQR nodes", ) def test_node_types_are_valid(self) -> None: """Test that all node types are valid NodeType values.""" for node in self.all_nodes: self.assertIsInstance(node.type, NodeType) self.assertIn(node.type, list(NodeType)) def test_no_ss_adjacency(self) -> None: """Test that no S-node is adjacent to another S-node.""" _assert_no_ss_pp(self, self.root, NodeType.S) def test_no_pp_adjacency(self) -> None: """Test that no P-node is adjacent to another P-node.""" _assert_no_ss_pp(self, self.root, NodeType.P) def test_parent_links(self) -> None: """Test that all parent-child links are consistent.""" for node in self.all_nodes: for child in node.children: self.assertIs(child.parent, node) class TestSPQRTheta(unittest.TestCase): """Tests for the SPQR-tree of the theta graph.""" def setUp(self) -> None: """Build the SPQR-tree for the theta graph.""" self.root: SPQRNode = \ build_spqr_tree(_make_theta()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_p_node(self) -> None: """Test that the theta graph produces a P-node as root. The theta graph has a separation pair {1,2} with 3 paths between them, so the root should be a P-node. """ self.assertEqual( self.root.type, NodeType.P, "Theta graph has 3 parallel paths between 1 and 2, " "expect P-node at root", ) def test_node_types_are_valid(self) -> None: """Test that all node types are valid NodeType values.""" for node in self.all_nodes: self.assertIsInstance(node.type, NodeType) def test_no_ss_adjacency(self) -> None: """Test that no S-node is adjacent to another S-node.""" _assert_no_ss_pp(self, self.root, NodeType.S) def test_no_pp_adjacency(self) -> None: """Test that no P-node is adjacent to another P-node.""" _assert_no_ss_pp(self, self.root, NodeType.P) def test_parent_links(self) -> None: """Test that all parent-child links are consistent.""" for node in self.all_nodes: for child in node.children: self.assertIs(child.parent, node) class TestSPQRPrism(unittest.TestCase): """Tests for the SPQR-tree of the triangular prism graph.""" def setUp(self) -> None: """Build the SPQR-tree for the triangular prism.""" self.root: SPQRNode = \ build_spqr_tree(_make_prism()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_single_r_node(self) -> None: """Test that the prism produces a single R-node.""" self.assertEqual( len(self.all_nodes), 1, "Prism is 3-connected, expect single R-node", ) self.assertEqual(self.root.type, NodeType.R) def test_no_children(self) -> None: """Test that the single R-node has no children.""" self.assertEqual(len(self.root.children), 0) def test_skeleton_has_nine_edges(self) -> None: """Test that the R-node skeleton contains 9 edges.""" self.assertEqual(self.root.skeleton.num_edges(), 9) def _assert_no_ss_pp( tc: unittest.TestCase, root: SPQRNode, ntype: NodeType, ) -> None: """Assert that no node of *ntype* is adjacent to another of same type. In the SPQR-tree, S-nodes must not be adjacent to S-nodes, and P-nodes must not be adjacent to P-nodes. :param tc: The TestCase instance for assertions. :param root: The SPQR-tree root node. :param ntype: The NodeType to check (S or P). :return: None """ for node in _collect_all_nodes(root): if node.type == ntype: for child in node.children: tc.assertNotEqual( child.type, ntype, f"{ntype.value}-{ntype.value} adjacency found " f"in SPQR-tree (not allowed)", ) if node.parent is not None: tc.assertNotEqual( node.parent.type, ntype, f"{ntype.value}-{ntype.value} adjacency found " f"in SPQR-tree (not allowed)", ) class TestSPQRNoSSPPInvariants(unittest.TestCase): """Tests that no S-S or P-P adjacency occurs for all graphs.""" def _check_tree(self, g: MultiGraph) -> None: """Build SPQR-tree and check S-S and P-P invariants. :param g: The input multigraph. :return: None """ root: SPQRNode = build_spqr_tree(g) _assert_no_ss_pp(self, root, NodeType.S) _assert_no_ss_pp(self, root, NodeType.P) def test_k3_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for K3.""" self._check_tree(_make_k3()) def test_c4_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for C4.""" self._check_tree(_make_c4()) def test_k4_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for K4.""" self._check_tree(_make_k4()) def test_two_parallel_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for 2 parallel edges.""" self._check_tree(_make_two_parallel()) def test_three_parallel_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for 3 parallel edges.""" self._check_tree(_make_three_parallel()) def test_diamond_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for the diamond graph.""" self._check_tree(_make_diamond()) def test_theta_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for the theta graph.""" self._check_tree(_make_theta()) def test_prism_no_ss_pp(self) -> None: """Test no S-S or P-P adjacency for the triangular prism.""" self._check_tree(_make_prism()) def _count_real_edges_in_tree(root: SPQRNode) -> int: """Count real (non-virtual) edges across all SPQR-tree skeletons. Each real edge should appear in exactly one node's skeleton. :param root: The SPQR-tree root. :return: Total count of real edges summed over all nodes. """ total: int = 0 for node in _collect_all_nodes(root): for e in node.skeleton.edges: if not e.virtual: total += 1 return total def _check_spqr_invariants( tc: unittest.TestCase, g: MultiGraph, root: SPQRNode, ) -> None: """Check all SPQR-tree invariants for a given graph. Verifies: parent-child links consistent, each node has >= 1 skeleton edge, real edge count preserved, no S-S or P-P adjacency. :param tc: The TestCase instance for assertions. :param g: The original input graph. :param root: The SPQR-tree root. :return: None """ nodes: list[SPQRNode] = _collect_all_nodes(root) # Parent-child links. for node in nodes: for child in node.children: tc.assertIs(child.parent, node) # Each node skeleton has at least 1 edge. for node in nodes: tc.assertGreater(node.skeleton.num_edges(), 0) # Real edge count. tc.assertEqual( _count_real_edges_in_tree(root), g.num_edges(), "Real edge count mismatch between SPQR-tree and original", ) # No S-S adjacency. _assert_no_ss_pp(tc, root, NodeType.S) # No P-P adjacency. _assert_no_ss_pp(tc, root, NodeType.P) def _make_wikipedia_example() -> MultiGraph: """Build the Wikipedia SPQR-tree example graph. 21 edges, 13 vertices. Used in the Wikipedia SPQR_tree article. :return: A MultiGraph representing the Wikipedia example. """ g: MultiGraph = MultiGraph() edges: list[tuple[int, int]] = [ (1, 2), (1, 4), (1, 8), (1, 12), (3, 4), (2, 3), (2, 13), (3, 13), (4, 5), (4, 7), (5, 6), (5, 8), (5, 7), (6, 7), (8, 11), (8, 9), (8, 12), (9, 10), (9, 11), (9, 12), (10, 12), ] for u, v in edges: g.add_edge(u, v) return g def _make_ht_example() -> MultiGraph: """Build the Hopcroft-Tarjan (1973) example graph. 23 edges, 13 vertices. Used in [HT1973]. :return: A MultiGraph representing the HT1973 example. """ g: MultiGraph = MultiGraph() edges: list[tuple[int, int]] = [ (1, 2), (1, 4), (1, 8), (1, 12), (1, 13), (2, 3), (2, 13), (3, 4), (3, 13), (4, 5), (4, 7), (5, 6), (5, 7), (5, 8), (6, 7), (8, 9), (8, 11), (8, 12), (9, 10), (9, 11), (9, 12), (10, 11), (10, 12), ] for u, v in edges: g.add_edge(u, v) return g def _make_gm_example() -> MultiGraph: """Build the Gutwenger-Mutzel (2001) example graph. 28 edges, 16 vertices. Used in [GM2001]. :return: A MultiGraph representing the GM2001 example. """ g: MultiGraph = MultiGraph() edges: list[tuple[int, int]] = [ (1, 2), (1, 4), (2, 3), (2, 5), (3, 4), (3, 5), (4, 5), (4, 6), (5, 7), (5, 8), (5, 14), (6, 8), (7, 14), (8, 9), (8, 10), (8, 11), (8, 12), (9, 10), (10, 13), (10, 14), (10, 15), (10, 16), (11, 12), (11, 13), (12, 13), (14, 15), (14, 16), (15, 16), ] for u, v in edges: g.add_edge(u, v) return g def _make_multiedge_complex() -> MultiGraph: """Build a complex graph with multi-edges embedded in a larger graph. 5 vertices, 7 edges; two pairs of parallel edges (1-5 and 2-3) embedded in a cycle. :return: A MultiGraph with embedded parallel edges. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(2, 3) g.add_edge(3, 4) g.add_edge(4, 5) g.add_edge(1, 5) g.add_edge(1, 5) return g class TestSPQRWikipediaExample(unittest.TestCase): """Tests for the SPQR-tree of the Wikipedia example graph.""" def setUp(self) -> None: """Build the SPQR-tree for the Wikipedia example.""" self.g: MultiGraph = _make_wikipedia_example() """The Wikipedia example graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for the Wikipedia example.""" _check_spqr_invariants(self, self.g, self.root) def test_at_least_two_nodes(self) -> None: """Test that the Wikipedia example produces multiple nodes.""" self.assertGreaterEqual( len(self.all_nodes), 2, "Wikipedia example has separation pairs, expect >=2 nodes", ) class TestSPQRHTExample(unittest.TestCase): """Tests for the SPQR-tree of the Hopcroft-Tarjan 1973 example.""" def setUp(self) -> None: """Build the SPQR-tree for the HT1973 example.""" self.g: MultiGraph = _make_ht_example() """The HT1973 example graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for the HT1973 example.""" _check_spqr_invariants(self, self.g, self.root) def test_at_least_two_nodes(self) -> None: """Test that the HT1973 example produces multiple nodes.""" self.assertGreaterEqual( len(self.all_nodes), 2, "HT1973 example has separation pairs, expect >=2 nodes", ) class TestSPQRGMExample(unittest.TestCase): """Tests for the SPQR-tree of the Gutwenger-Mutzel 2001 example.""" def setUp(self) -> None: """Build the SPQR-tree for the GM2001 example.""" self.g: MultiGraph = _make_gm_example() """The GM2001 example graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for the GM2001 example.""" _check_spqr_invariants(self, self.g, self.root) def test_at_least_two_nodes(self) -> None: """Test that the GM2001 example produces multiple nodes.""" self.assertGreaterEqual( len(self.all_nodes), 2, "GM2001 example has separation pairs, expect >=2 nodes", ) class TestSPQRMultiEdgeComplex(unittest.TestCase): """Tests for the SPQR-tree of a complex multi-edge graph. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the complex multi-edge graph.""" self.g: MultiGraph = _make_multiedge_complex() """The complex multi-edge graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for the multi-edge graph.""" _check_spqr_invariants(self, self.g, self.root) def test_has_p_node(self) -> None: """Test that multi-edges produce P-nodes in the tree.""" p_nodes: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertGreaterEqual( len(p_nodes), 1, "Multi-edge graph should have at least one P-node", ) def test_has_s_node(self) -> None: """Test that the cycle backbone produces an S-node.""" s_nodes: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] self.assertGreaterEqual( len(s_nodes), 1, "Multi-edge graph should have at least one S-node", ) def test_exact_node_structure(self) -> None: """Test exact SPQR-tree node counts: 2 P-nodes, 1 S-node. Each parallel pair forms a BOND: 2 real + 1 virtual = 3 edges -> P-node. The backbone cycle forms an S-node. """ p_count: int = sum( 1 for n in self.all_nodes if n.type == NodeType.P ) s_count: int = sum( 1 for n in self.all_nodes if n.type == NodeType.S ) self.assertEqual( p_count, 2, f"Expected 2 P-nodes, got {p_count}", ) self.assertEqual( s_count, 1, f"Expected 1 S-node, got {s_count}", ) def _make_single_edge() -> MultiGraph: """Build a graph with a single edge (vertices 0 and 1). This is the minimal biconnected graph. Expected: Q-node. :return: A MultiGraph with one edge. """ g: MultiGraph = MultiGraph() g.add_edge(0, 1) return g def _make_c5() -> MultiGraph: """Build the 5-cycle C5 (vertices 0-4). :return: A MultiGraph representing C5. """ g: MultiGraph = MultiGraph() for i in range(5): g.add_edge(i, (i + 1) % 5) return g def _make_c6() -> MultiGraph: """Build the 6-cycle C6 (vertices 0-5). :return: A MultiGraph representing C6. """ g: MultiGraph = MultiGraph() for i in range(6): g.add_edge(i, (i + 1) % 6) return g def _make_c6_with_chord() -> MultiGraph: """Build C6 plus chord (0,3): 7 edges, 6 vertices. The chord creates separation pair {0,3} yielding 3 SPQR nodes. :return: A MultiGraph representing C6 plus a chord. """ g: MultiGraph = MultiGraph() for i in range(6): g.add_edge(i, (i + 1) % 6) g.add_edge(0, 3) return g def _make_k5() -> MultiGraph: """Build the complete graph K5 (10 edges, vertices 0-4). K5 is 4-connected, hence a single R-node in the SPQR-tree. :return: A MultiGraph representing K5. """ g: MultiGraph = MultiGraph() for i in range(5): for j in range(i + 1, 5): g.add_edge(i, j) return g def _make_petersen() -> MultiGraph: """Build the Petersen graph (10 vertices, 15 edges). The Petersen graph is 3-connected, so it yields a single R-node. :return: A MultiGraph representing the Petersen graph. """ g: MultiGraph = MultiGraph() for i in range(5): g.add_edge(i, (i + 1) % 5) for i in range(5): g.add_edge(i, i + 5) for u, v in [(5, 7), (7, 9), (9, 6), (6, 8), (8, 5)]: g.add_edge(u, v) return g def _make_petersen_augmented() -> MultiGraph: """Build the Petersen graph with each edge subdivided by a path. For each original Petersen edge (u,v), two intermediate vertices w1 and w2 are added and a path u-w1-w2-v is inserted alongside the original edge. Result: 40 vertices, 60 edges. Expected: 31 nodes (15 P + 15 S + 1 R). :return: The augmented Petersen multigraph. """ g: MultiGraph = _make_petersen() petersen_edges: list[tuple[int, int]] = [ (0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 5), (1, 6), (2, 7), (3, 8), (4, 9), (5, 7), (7, 9), (9, 6), (6, 8), (8, 5), ] next_v: int = 10 for u, v in petersen_edges: w1: int = next_v w2: int = next_v + 1 next_v += 2 g.add_edge(u, w1) g.add_edge(w1, w2) g.add_edge(w2, v) return g def _make_three_k4_cliques() -> MultiGraph: """Build graph: 3 K4 cliques sharing poles {0, 1}. Vertices 0-7; poles are 0 and 1; each clique K4(0,1,a,b) adds 6 edges among {0,1,a,b}. The edge (0,1) appears 3 times. Expected: 4 nodes (1 P + 3 R). :return: A MultiGraph with three K4 cliques sharing poles. """ g: MultiGraph = MultiGraph() for a, b in [(2, 3), (4, 5), (6, 7)]: for u, v in [ (0, 1), (0, a), (0, b), (1, a), (1, b), (a, b) ]: g.add_edge(u, v) return g def _make_three_long_paths() -> MultiGraph: """Build graph: 3 paths of length 3 between vertices 0 and 1. Vertices 0-7; paths: 0-2-3-1, 0-4-5-1, 0-6-7-1. Expected: 4 nodes (1 P + 3 S). :return: A MultiGraph with three length-3 paths. """ g: MultiGraph = MultiGraph() for a, b in [(2, 3), (4, 5), (6, 7)]: g.add_edge(0, a) g.add_edge(a, b) g.add_edge(b, 1) return g def _count_nodes_by_type(root: SPQRNode) -> dict[str, int]: """Count SPQR-tree nodes by type. :param root: The SPQR-tree root. :return: Dict mapping type value string to count. """ counts: dict[str, int] = {} for node in _collect_all_nodes(root): key: str = node.type.value counts[key] = counts.get(key, 0) + 1 return counts class TestSPQRSingleEdge(unittest.TestCase): """Tests for the SPQR-tree of a single-edge graph. A single edge is the degenerate case: one Q-node, no children. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for a single edge.""" self.root: SPQRNode = \ build_spqr_tree(_make_single_edge()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_q_node(self) -> None: """Test that a single edge produces a Q-node root.""" self.assertEqual(self.root.type, NodeType.Q) def test_single_node_total(self) -> None: """Test that there is exactly 1 node in the tree.""" self.assertEqual(len(self.all_nodes), 1) def test_no_children(self) -> None: """Test that the Q-node has no children.""" self.assertEqual(len(self.root.children), 0) def test_skeleton_has_one_edge(self) -> None: """Test that the Q-node skeleton has exactly 1 edge.""" self.assertEqual(self.root.skeleton.num_edges(), 1) class TestSPQRC5(unittest.TestCase): """Tests for the SPQR-tree of the 5-cycle C5.""" def setUp(self) -> None: """Build the SPQR-tree for C5.""" self.root: SPQRNode = \ build_spqr_tree(_make_c5()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_s_node(self) -> None: """Test that C5 produces a single S-node.""" self.assertEqual(self.root.type, NodeType.S) def test_single_node_total(self) -> None: """Test that there is exactly 1 node in the tree.""" self.assertEqual(len(self.all_nodes), 1) def test_skeleton_has_five_edges(self) -> None: """Test that the S-node skeleton has 5 edges.""" self.assertEqual(self.root.skeleton.num_edges(), 5) class TestSPQRC6(unittest.TestCase): """Tests for the SPQR-tree of the 6-cycle C6. Expected: single S-node (the entire cycle). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for C6.""" self.root: SPQRNode = \ build_spqr_tree(_make_c6()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_s_node(self) -> None: """Test that C6 produces a single S-node.""" self.assertEqual(self.root.type, NodeType.S) def test_single_node_total(self) -> None: """Test that C6 yields exactly 1 SPQR node.""" self.assertEqual(len(self.all_nodes), 1) def test_no_children(self) -> None: """Test that the root S-node has no children.""" self.assertEqual(len(self.root.children), 0) def test_skeleton_has_six_edges(self) -> None: """Test that the S-node skeleton has 6 edges.""" self.assertEqual(self.root.skeleton.num_edges(), 6) class TestSPQRC6Chord(unittest.TestCase): """Tests for the SPQR-tree of C6 plus chord (0,3). The chord (0,3) creates separation pair {0,3} yielding 3 nodes: 1 P-node (chord bond) + 2 S-nodes (the two 4-cycle halves). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for C6 with chord.""" self.g: MultiGraph = _make_c6_with_chord() """The C6 with chord graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for C6 plus chord.""" _check_spqr_invariants(self, self.g, self.root) def test_three_nodes_total(self) -> None: """Test that C6 plus chord yields exactly 3 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 3, f"C6+chord should have 3 SPQR nodes, " f"got {len(self.all_nodes)}", ) def test_one_p_node(self) -> None: """Test that there is exactly 1 P-node (the chord bond). The chord (0,3) forms a BOND: 1 real edge + 2 virtual edges (one to each polygon side) = 3 total edges -> P-node. """ p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(p), 1, f"Expected 1 P-node, got {len(p)}", ) def test_two_s_nodes(self) -> None: """Test that there are exactly 2 S-nodes (the two paths).""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] self.assertEqual( len(s), 2, f"Expected 2 S-nodes, got {len(s)}", ) class TestSPQRK5(unittest.TestCase): """Tests for the SPQR-tree of the complete graph K5. K5 is 4-connected, so it yields a single R-node. """ def setUp(self) -> None: """Build the SPQR-tree for K5.""" self.root: SPQRNode = \ build_spqr_tree(_make_k5()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_r_node(self) -> None: """Test that K5 produces a single R-node.""" self.assertEqual(self.root.type, NodeType.R) def test_single_node_total(self) -> None: """Test that there is exactly 1 node in the tree.""" self.assertEqual(len(self.all_nodes), 1) def test_skeleton_has_ten_edges(self) -> None: """Test that the R-node skeleton has 10 edges.""" self.assertEqual(self.root.skeleton.num_edges(), 10) class TestSPQRPetersen(unittest.TestCase): """Tests for the SPQR-tree of the Petersen graph. The Petersen graph is 3-connected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the Petersen graph.""" self.root: SPQRNode = \ build_spqr_tree(_make_petersen()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_root_is_r_node(self) -> None: """Test that the Petersen graph produces a single R-node.""" self.assertEqual(self.root.type, NodeType.R) def test_single_node_total(self) -> None: """Test that there is exactly 1 node in the tree.""" self.assertEqual(len(self.all_nodes), 1) def test_no_children(self) -> None: """Test that the R-node has no children.""" self.assertEqual(len(self.root.children), 0) def test_skeleton_has_fifteen_edges(self) -> None: """Test that the R-node skeleton has 15 edges.""" self.assertEqual(self.root.skeleton.num_edges(), 15) class TestSPQRThreeK4Cliques(unittest.TestCase): """Tests for the SPQR-tree of 3 K4 cliques sharing poles {0,1}. Expected: 4 nodes: 1 P-node (3-way parallel at 0-1) and 3 R-nodes (one per K4 clique). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the three-K4-cliques graph.""" self.g: MultiGraph = _make_three_k4_cliques() """The three-K4-cliques graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for three K4 cliques.""" _check_spqr_invariants(self, self.g, self.root) def test_four_nodes_total(self) -> None: """Test that 3 K4 cliques yield exactly 4 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 4, f"Expected 4 SPQR nodes, got {len(self.all_nodes)}", ) def test_one_p_node(self) -> None: """Test that there is exactly 1 P-node (3+ parallel at 0-1).""" p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(p), 1, f"Expected 1 P-node, got {len(p)}", ) def test_three_r_nodes(self) -> None: """Test that there are exactly 3 R-nodes (one per K4).""" r: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.R ] self.assertEqual( len(r), 3, f"Expected 3 R-nodes, got {len(r)}", ) class TestSPQRThreeLongPaths(unittest.TestCase): """Tests for the SPQR-tree of 3 length-3 paths between 0 and 1. Expected: 4 nodes: 1 P-node (3-way connection at poles) and 3 S-nodes (one per length-3 path). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the three-long-paths graph.""" self.g: MultiGraph = _make_three_long_paths() """The three-long-paths graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for three long paths.""" _check_spqr_invariants(self, self.g, self.root) def test_four_nodes_total(self) -> None: """Test that three length-3 paths yield exactly 4 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 4, f"Expected 4 SPQR nodes, got {len(self.all_nodes)}", ) def test_one_p_node(self) -> None: """Test that there is exactly 1 P-node (3-way connection).""" p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(p), 1, f"Expected 1 P-node, got {len(p)}", ) def test_three_s_nodes(self) -> None: """Test that there are exactly 3 S-nodes (one per path).""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] self.assertEqual( len(s), 3, f"Expected 3 S-nodes, got {len(s)}", ) def test_s_node_skeletons_have_four_edges(self) -> None: """Test that each S-node skeleton has 4 edges. Each length-3 path has 3 real edges plus 1 virtual edge connecting its poles = 4 total edges in the skeleton. """ for node in self.all_nodes: if node.type == NodeType.S: self.assertEqual( node.skeleton.num_edges(), 4, f"S-node skeleton should have 4 edges " f"(3 real + 1 virtual), got " f"{node.skeleton.num_edges()}", ) class TestSPQRPetersenAugmented(unittest.TestCase): """Tests for the SPQR-tree of the augmented Petersen graph. Each Petersen edge (u,v) gets a parallel path u-w1-w2-v added. Expected: 31 nodes (15 P + 15 S + 1 R). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the augmented Petersen graph.""" self.g: MultiGraph = _make_petersen_augmented() """The augmented Petersen graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for augmented Petersen.""" _check_spqr_invariants(self, self.g, self.root) def test_thirty_one_nodes_total(self) -> None: """Test that the tree has 31 nodes total. Expected: 15 P + 15 S + 1 R = 31 total. """ self.assertEqual( len(self.all_nodes), 31, f"Augmented Petersen should have 31 nodes, " f"got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_one_r_node(self) -> None: """Test that there is exactly 1 R-node (Petersen skeleton).""" r: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.R ] self.assertEqual( len(r), 1, f"Expected 1 R-node, got {len(r)}", ) def test_fifteen_s_nodes(self) -> None: """Test that there are exactly 15 S-nodes.""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] self.assertEqual( len(s), 15, f"Expected 15 S-nodes, got {len(s)}", ) def test_fifteen_p_nodes(self) -> None: """Test that there are exactly 15 P-nodes. Each original Petersen edge (u,v) forms a BOND with the parallel path: 1 real + 2 virtual = 3 edges -> P-node. """ p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(p), 15, f"Expected 15 P-nodes, got {len(p)}", ) def _make_k33() -> MultiGraph: """Build K_{3,3} (9 edges, vertices 0-5). K_{3,3} is 3-connected (triconnected), so its SPQR-tree is a single R-node. :return: A MultiGraph representing K_{3,3}. """ g: MultiGraph = MultiGraph() for i in range(3): for j in range(3, 6): g.add_edge(i, j) return g def _make_w4() -> MultiGraph: """Build wheel W4: hub vertex 0, rim vertices 1-4. W4 has 8 edges (4 spokes + 4 rim) and is 3-connected, so its SPQR-tree is a single R-node. :return: A MultiGraph representing W4. """ g: MultiGraph = MultiGraph() for i in range(1, 5): g.add_edge(0, i) g.add_edge(i, i % 4 + 1) return g def _make_k3_doubled() -> MultiGraph: """Build K3 with each edge doubled (6 edges, vertices 1-3). Each pair of parallel edges forms a BOND. The triangle forms a POLYGON. Expected: 4 nodes (3 P + 1 S). :return: A MultiGraph representing doubled K3. """ g: MultiGraph = MultiGraph() for u, v in [(1, 2), (2, 3), (1, 3)]: g.add_edge(u, v) g.add_edge(u, v) return g def _make_four_parallel() -> MultiGraph: """Build 4 parallel edges between vertices 1 and 2. Four parallel edges form a single BOND component with 4 real edges -> P-node. :return: A MultiGraph with 4 parallel edges. """ g: MultiGraph = MultiGraph() for _ in range(4): g.add_edge(1, 2) return g def _make_five_parallel() -> MultiGraph: """Build 5 parallel edges between vertices 1 and 2. Five parallel edges form a single BOND component with 5 real edges -> P-node. :return: A MultiGraph with 5 parallel edges. """ g: MultiGraph = MultiGraph() for _ in range(5): g.add_edge(1, 2) return g def _make_three_long_paths_doubled() -> MultiGraph: """Build 3 length-3 paths with all edges doubled. Vertices 0-7; paths: 0-2-3-1, 0-4-5-1, 0-6-7-1, each edge doubled. Expected: 13 nodes (3 S + 10 P). :return: A MultiGraph with doubled three-length-3 paths. """ g: MultiGraph = MultiGraph() for a, b in [(2, 3), (4, 5), (6, 7)]: for u, v in [(0, a), (a, b), (b, 1)]: g.add_edge(u, v) g.add_edge(u, v) return g def _make_graph6_sage_docstring() -> MultiGraph: """Build the 13-vertex, 23-edge graph from graph6 'LlCG{O@?GBoMw?'. This biconnected graph has multiple separation pairs. Expected: 12 nodes (2 R + 5 S + 5 P). :return: A MultiGraph with 13 vertices and 23 edges. """ g: MultiGraph = MultiGraph() edges: list[tuple[int, int]] = [ (0, 1), (1, 2), (0, 3), (2, 3), (3, 4), (4, 5), (3, 6), (4, 6), (5, 6), (0, 7), (4, 7), (7, 8), (8, 9), (7, 10), (8, 10), (9, 10), (0, 11), (7, 11), (8, 11), (9, 11), (0, 12), (1, 12), (2, 12), ] for u, v in edges: g.add_edge(u, v) return g def _make_petersen_augmented_twice() -> MultiGraph: """Build Petersen graph with two rounds of path augmentation. Round 1: for each of the 15 Petersen edges (u,v), add a parallel path u-w1-w2-v alongside. Round 2: for each of the 60 round-1 edges, add another parallel path alongside. Result: 160 vertices, 240 edges. Expected: 136 nodes (60 P + 75 S + 1 R). :return: The doubly-augmented Petersen multigraph. """ g: MultiGraph = MultiGraph() for i in range(5): g.add_edge(i, (i + 1) % 5) for i in range(5): g.add_edge(i, i + 5) for u, v in [(5, 7), (7, 9), (9, 6), (6, 8), (8, 5)]: g.add_edge(u, v) petersen_edges: list[tuple[int, int]] = [ (0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 5), (1, 6), (2, 7), (3, 8), (4, 9), (5, 7), (7, 9), (9, 6), (6, 8), (8, 5), ] next_v: int = 10 for u, v in petersen_edges: g.add_edge(u, next_v) g.add_edge(next_v, next_v + 1) g.add_edge(next_v + 1, v) next_v += 2 round1_edges: list[tuple[Hashable, Hashable]] = [ (e.u, e.v) for e in g.edges ] for u, v in round1_edges: g.add_edge(u, next_v) g.add_edge(next_v, next_v + 1) g.add_edge(next_v + 1, v) next_v += 2 return g class TestSPQRK33(unittest.TestCase): """Tests for the SPQR-tree of K_{3,3}. K_{3,3} is 3-connected, so it decomposes into a single TRICONNECTED component -> single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for K_{3,3}.""" self.g: MultiGraph = _make_k33() """The K_{3,3} graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for K_{3,3}.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that K_{3,3} produces exactly 1 R-node.""" self.assertEqual( len(self.all_nodes), 1, f"K33 should have 1 node, got {len(self.all_nodes)}", ) self.assertEqual( self.root.type, NodeType.R, f"K33 root should be R-node, got {self.root.type}", ) def test_nine_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 9 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual( len(real), 9, f"K33 skeleton should have 9 real edges, got {len(real)}", ) class TestSPQRW4(unittest.TestCase): """Tests for the SPQR-tree of wheel W4. W4 (hub=0, rim=1-4) is 3-connected, so it decomposes into a single TRICONNECTED component -> single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for wheel W4.""" self.g: MultiGraph = _make_w4() """The W4 wheel graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for W4.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that W4 produces exactly 1 R-node.""" self.assertEqual( len(self.all_nodes), 1, f"W4 should have 1 node, got {len(self.all_nodes)}", ) self.assertEqual( self.root.type, NodeType.R, f"W4 root should be R-node, got {self.root.type}", ) def test_eight_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 8 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual( len(real), 8, f"W4 skeleton should have 8 real edges, got {len(real)}", ) class TestSPQRK3Doubled(unittest.TestCase): """Tests for the SPQR-tree of K3 with each edge doubled. Each pair of doubled edges forms a BOND (-> P-node). The triangle K3 forms a POLYGON (-> S-node). Expected: 4 nodes (3 P + 1 S). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for doubled K3.""" self.g: MultiGraph = _make_k3_doubled() """The doubled K3 graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for doubled K3.""" _check_spqr_invariants(self, self.g, self.root) def test_four_nodes_total(self) -> None: """Test that doubled K3 produces exactly 4 nodes. Expected: 4 total (3 P + 1 S). """ self.assertEqual( len(self.all_nodes), 4, f"Doubled K3 should have 4 nodes, " f"got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_one_s_node_three_p_nodes(self) -> None: """Test that doubled K3 has 1 S-node and 3 P-nodes.""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(s), 1, f"Expected 1 S-node, got {len(s)}", ) self.assertEqual( len(p), 3, f"Expected 3 P-nodes, got {len(p)}", ) class TestSPQRFourParallel(unittest.TestCase): """Tests for the SPQR-tree of 4 parallel edges. Four parallel edges form a single BOND with 4 real edges -> single P-node. """ def setUp(self) -> None: """Build the SPQR-tree for 4 parallel edges.""" self.g: MultiGraph = _make_four_parallel() """The 4-parallel-edges graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for 4 parallel edges.""" _check_spqr_invariants(self, self.g, self.root) def test_single_p_node(self) -> None: """Test that 4 parallel edges produce exactly 1 P-node.""" self.assertEqual( len(self.all_nodes), 1, f"4-parallel should have 1 node, " f"got {len(self.all_nodes)}", ) self.assertEqual( self.root.type, NodeType.P, f"4-parallel root should be P-node, " f"got {self.root.type}", ) def test_four_real_edges_in_skeleton(self) -> None: """Test that the P-node skeleton has 4 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual( len(real), 4, f"4-parallel skeleton should have 4 real edges, " f"got {len(real)}", ) class TestSPQRFiveParallel(unittest.TestCase): """Tests for the SPQR-tree of 5 parallel edges. Five parallel edges form a single BOND with 5 real edges -> single P-node. """ def setUp(self) -> None: """Build the SPQR-tree for 5 parallel edges.""" self.g: MultiGraph = _make_five_parallel() """The 5-parallel-edges graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for 5 parallel edges.""" _check_spqr_invariants(self, self.g, self.root) def test_single_p_node(self) -> None: """Test that 5 parallel edges produce exactly 1 P-node.""" self.assertEqual( len(self.all_nodes), 1, f"5-parallel should have 1 node, " f"got {len(self.all_nodes)}", ) self.assertEqual( self.root.type, NodeType.P, f"5-parallel root should be P-node, " f"got {self.root.type}", ) def test_five_real_edges_in_skeleton(self) -> None: """Test that the P-node skeleton has 5 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual( len(real), 5, f"5-parallel skeleton should have 5 real edges, " f"got {len(real)}", ) class TestSPQRThreeLongPathsDoubled(unittest.TestCase): """Tests for the SPQR-tree of three doubled length-3 paths. Three length-3 paths (0-a-b-1) with all edges doubled. Each pair of doubled edges forms a P-node, and each path length-3 forms an S-node. Expected: 13 nodes (3 S + 10 P). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for doubled three-length-3 paths.""" self.g: MultiGraph = \ _make_three_long_paths_doubled() """The doubled three-long-paths graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for doubled long paths.""" _check_spqr_invariants(self, self.g, self.root) def test_thirteen_nodes_total(self) -> None: """Test that doubled three-paths has 13 total nodes. Expected: 13 total (3 S + 10 P). """ self.assertEqual( len(self.all_nodes), 13, f"Doubled three-paths should have 13 nodes, " f"got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_three_s_nodes_ten_p_nodes(self) -> None: """Test that the tree has 3 S-nodes and 10 P-nodes.""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(s), 3, f"Expected 3 S-nodes, got {len(s)}", ) self.assertEqual( len(p), 10, f"Expected 10 P-nodes, got {len(p)}", ) class TestSPQRSageDocstringGraph(unittest.TestCase): """Tests for the SPQR-tree of the 13V/23E graph (graph6 'LlCG{O@?GBoMw?'). This biconnected graph has multiple separation pairs and yields 12 SPQR nodes (2 R + 5 S + 5 P). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the 13-vertex docstring graph.""" self.g: MultiGraph = \ _make_graph6_sage_docstring() """The 13V/23E docstring graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for the 13V/23E graph.""" _check_spqr_invariants(self, self.g, self.root) def test_twelve_nodes_total(self) -> None: """Test that the 13V/23E graph has 12 SPQR nodes total. Expected: 12 total (2 R + 5 S + 5 P). Matches SageMath's ``spqr_tree`` output. """ self.assertEqual( len(self.all_nodes), 12, f"13V/23E graph should have 12 nodes, " f"got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_two_r_five_s_five_p(self) -> None: """Test node type counts: 2 R-nodes, 5 S-nodes, 5 P-nodes.""" r: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.R ] s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual(len(r), 2, f"Expected 2 R-nodes, got {len(r)}") self.assertEqual(len(s), 5, f"Expected 5 S-nodes, got {len(s)}") self.assertEqual(len(p), 5, f"Expected 5 P-nodes, got {len(p)}") class TestSPQRPetersenAugmentedTwice(unittest.TestCase): """Tests for the SPQR-tree of the doubly-augmented Petersen graph. Round 1: for each of the 15 Petersen edges (u,v), add a parallel path u-w1-w2-v alongside. Round 2: for each of the 60 round-1 edges, add another parallel path alongside. Result: 160 vertices, 240 edges, 136 SPQR nodes. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the doubly-augmented Petersen.""" self.g: MultiGraph = \ _make_petersen_augmented_twice() """The doubly-augmented Petersen graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for doubly-aug. Petersen.""" _check_spqr_invariants(self, self.g, self.root) def test_136_nodes_total(self) -> None: """Test that the doubly-augmented Petersen has 136 nodes. Expected: 136 total (60 P + 75 S + 1 R). """ self.assertEqual( len(self.all_nodes), 136, f"Doubly-augmented Petersen should have 136 nodes, " f"got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_one_r_node(self) -> None: """Test that there is exactly 1 R-node (Petersen skeleton).""" r: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.R ] self.assertEqual( len(r), 1, f"Expected 1 R-node, got {len(r)}", ) def test_sixty_p_nodes(self) -> None: """Test that there are exactly 60 P-nodes.""" p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual( len(p), 60, f"Expected 60 P-nodes, got {len(p)}", ) def test_seventy_five_s_nodes(self) -> None: """Test that there are exactly 75 S-nodes.""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] self.assertEqual( len(s), 75, f"Expected 75 S-nodes, got {len(s)}", ) class TestSPQRDiamondExact(unittest.TestCase): """Exact SPQR-tree node tests for the diamond graph. The diamond has separation pair {2,3}. Expected: 3 nodes total: 2 S-nodes (the two triangular halves as polygons) and 1 P-node (the direct edge (2,3) forming a bond with 1 real + 2 virtual edges = 3 edges -> P-node). """ def setUp(self) -> None: """Build the SPQR-tree for the diamond graph.""" self.root: SPQRNode = \ build_spqr_tree(_make_diamond()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_exactly_three_nodes(self) -> None: """Test that the diamond produces exactly 3 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 3, f"Diamond should have 3 SPQR nodes, " f"got {len(self.all_nodes)}", ) def test_two_s_nodes_one_p_node(self) -> None: """Test that diamond has 2 S-nodes and 1 P-node.""" p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] self.assertEqual(len(p), 1, "Expected 1 P-node") self.assertEqual(len(s), 2, "Expected 2 S-nodes") def _make_ladder() -> MultiGraph: """Build the 3-rung ladder graph (2x4 grid). Vertices 0-7. Top row: 0-1-2-3, bottom row: 4-5-6-7, rungs: (0,4), (1,5), (2,6), (3,7). Separation pairs {1,5} and {2,6} yield 5 SPQR nodes: 3 S-nodes + 2 P-nodes. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing the 3-rung ladder graph. """ g: MultiGraph = MultiGraph() g.add_edge(0, 1) g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(4, 5) g.add_edge(5, 6) g.add_edge(6, 7) g.add_edge(0, 4) g.add_edge(1, 5) g.add_edge(2, 6) g.add_edge(3, 7) return g class TestSPQRLadder(unittest.TestCase): """Tests for the SPQR-tree of the 3-rung ladder graph. The ladder (2x4 grid) has separation pairs {1,5} and {2,6}. Expected: 5 nodes (3 S-nodes + 2 P-nodes). Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the ladder graph.""" self.g: MultiGraph = _make_ladder() """The ladder graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for the ladder graph.""" _check_spqr_invariants(self, self.g, self.root) def test_five_nodes_total(self) -> None: """Test that the ladder graph produces exactly 5 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 5, f"Ladder should have 5 nodes, " f"got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_three_s_nodes_two_p_nodes(self) -> None: """Test that the ladder has 3 S-nodes and 2 P-nodes.""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual(len(s), 3, "Expected 3 S-nodes") self.assertEqual(len(p), 2, "Expected 2 P-nodes") def test_no_r_or_q_nodes(self) -> None: """Test that the ladder has no R-nodes or Q-nodes.""" r: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.R ] q: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.Q ] self.assertEqual(len(r), 0, "Expected no R-nodes") self.assertEqual(len(q), 0, "Expected no Q-nodes") def _make_c7() -> MultiGraph: """Build the 7-cycle C7 (vertices 0-6). :return: A MultiGraph representing C7. """ g: MultiGraph = MultiGraph() for i in range(7): g.add_edge(i, (i + 1) % 7) return g class TestSPQRC7(unittest.TestCase): """Tests for the SPQR-tree of the 7-cycle C7. C7 is a simple cycle with 7 edges. It yields a single S-node (POLYGON). """ def setUp(self) -> None: """Build the SPQR-tree for C7.""" self.g: MultiGraph = _make_c7() """The C7 cycle graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for C7.""" _check_spqr_invariants(self, self.g, self.root) def test_single_s_node(self) -> None: """Test that C7 produces exactly 1 S-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.S) def test_seven_real_edges_in_skeleton(self) -> None: """Test that the S-node skeleton has 7 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 7) def _make_c8() -> MultiGraph: """Build the 8-cycle C8 (vertices 0-7). :return: A MultiGraph representing C8. """ g: MultiGraph = MultiGraph() for i in range(8): g.add_edge(i, (i + 1) % 8) return g class TestSPQRC8(unittest.TestCase): """Tests for the SPQR-tree of the 8-cycle C8. C8 is a simple cycle with 8 edges. It yields a single S-node (POLYGON). """ def setUp(self) -> None: """Build the SPQR-tree for C8.""" self.g: MultiGraph = _make_c8() """The C8 cycle graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for C8.""" _check_spqr_invariants(self, self.g, self.root) def test_single_s_node(self) -> None: """Test that C8 produces exactly 1 S-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.S) def test_eight_real_edges_in_skeleton(self) -> None: """Test that the S-node skeleton has 8 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 8) def _make_k23() -> MultiGraph: """Build the complete bipartite graph K_{2,3}. Vertices 0,1 (part A) and 2,3,4 (part B). Edges: all 6 pairs between parts. K_{2,3} has vertex connectivity 2 (separation pair {0,1}). Each internal vertex x in {2,3,4} creates a 2-edge path 0-x-1, yielding 4 SPQR nodes: 3 S-nodes + 1 P-node. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing K_{2,3}. """ g: MultiGraph = MultiGraph() for a in [0, 1]: for b in [2, 3, 4]: g.add_edge(a, b) return g class TestSPQRK23(unittest.TestCase): """Tests for the SPQR-tree of K_{2,3}. K_{2,3} has 5 vertices and 6 edges. It has vertex connectivity 2 (separation pair {0,1}), yielding 4 SPQR nodes: 3 S-nodes + 1 P-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for K_{2,3}.""" self.g: MultiGraph = _make_k23() """The K_{2,3} graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for K_{2,3}.""" _check_spqr_invariants(self, self.g, self.root) def test_four_nodes_total(self) -> None: """Test that K_{2,3} produces exactly 4 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 4, f"Expected 4 nodes, got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_three_s_one_p(self) -> None: """Test that K_{2,3} has 3 S-nodes and 1 P-node.""" s: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.S ] p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] self.assertEqual(len(s), 3, "Expected 3 S-nodes") self.assertEqual(len(p), 1, "Expected 1 P-node") def _make_w5() -> MultiGraph: """Build the wheel graph W5 (hub + 5-cycle, 6 vertices). Hub vertex 0 connected to rim vertices 1-5. W5 is 3-connected, yielding a single R-node with 10 edges. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing W5. """ g: MultiGraph = MultiGraph() for i in range(1, 6): g.add_edge(i, i % 5 + 1) for i in range(1, 6): g.add_edge(0, i) return g class TestSPQRW5(unittest.TestCase): """Tests for the SPQR-tree of the wheel graph W5. W5 has 6 vertices and 10 edges. It is triconnected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for W5.""" self.g: MultiGraph = _make_w5() """The W5 wheel graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for W5.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that W5 produces exactly 1 R-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.R) def test_ten_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 10 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 10) def _make_w6() -> MultiGraph: """Build the wheel graph W6 (hub + 6-cycle, 7 vertices). Hub vertex 0 connected to rim vertices 1-6. W6 is 3-connected, yielding a single R-node with 12 edges. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing W6. """ g: MultiGraph = MultiGraph() for i in range(1, 7): g.add_edge(i, i % 6 + 1) for i in range(1, 7): g.add_edge(0, i) return g class TestSPQRW6(unittest.TestCase): """Tests for the SPQR-tree of the wheel graph W6. W6 has 7 vertices and 12 edges. It is triconnected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for W6.""" self.g: MultiGraph = _make_w6() """The W6 wheel graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for W6.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that W6 produces exactly 1 R-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.R) def test_twelve_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 12 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 12) def _make_q3_cube() -> MultiGraph: """Build the Q3 cube graph (3-dimensional hypercube). 8 vertices (0-7), 12 edges. The cube graph is 3-connected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing the Q3 cube. """ g: MultiGraph = MultiGraph() # Bottom face: 0-1-2-3 g.add_edge(0, 1) g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(3, 0) # Top face: 4-5-6-7 g.add_edge(4, 5) g.add_edge(5, 6) g.add_edge(6, 7) g.add_edge(7, 4) # Vertical edges g.add_edge(0, 4) g.add_edge(1, 5) g.add_edge(2, 6) g.add_edge(3, 7) return g class TestSPQRQ3Cube(unittest.TestCase): """Tests for the SPQR-tree of the Q3 cube graph. The cube (Q3) has 8 vertices and 12 edges. It is 3-connected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the Q3 cube.""" self.g: MultiGraph = _make_q3_cube() """The Q3 cube graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for Q3 cube.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that Q3 cube produces exactly 1 R-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.R) def test_twelve_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 12 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 12) def _make_octahedron() -> MultiGraph: """Build the octahedron graph (6 vertices, 12 edges). The octahedron is the complete tripartite graph K_{2,2,2}. It is 4-connected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing the octahedron. """ g: MultiGraph = MultiGraph() # All pairs except (0,5), (1,3), (2,4) pairs: list[tuple[int, int]] = [ (0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 4), (1, 5), (2, 3), (2, 5), (3, 4), (3, 5), (4, 5), ] for u, v in pairs: g.add_edge(u, v) return g class TestSPQROctahedron(unittest.TestCase): """Tests for the SPQR-tree of the octahedron graph. The octahedron has 6 vertices and 12 edges. It is 4-connected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the octahedron.""" self.g: MultiGraph = _make_octahedron() """The octahedron graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for octahedron.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that octahedron produces exactly 1 R-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.R) def test_twelve_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 12 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 12) def _make_k4_one_doubled() -> MultiGraph: """Build K4 with one edge doubled. K4 has 6 edges. One edge (1-2) is doubled, giving 7 edges total. The doubled edge creates separation pair {1,2}, yielding 2 SPQR nodes: 1 P-node (BOND with the doubled edge) and 1 R-node (K4 skeleton). Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing K4 with one doubled edge. """ g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(1, 3) g.add_edge(1, 4) g.add_edge(2, 3) g.add_edge(2, 4) g.add_edge(3, 4) g.add_edge(1, 2) # duplicate return g class TestSPQRK4OneDoubled(unittest.TestCase): """Tests for the SPQR-tree of K4 + 1 doubled edge. K4 with one edge doubled has 7 edges. Expected: 2 SPQR nodes: 1 P-node and 1 R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for K4 + 1 doubled edge.""" self.g: MultiGraph = _make_k4_one_doubled() """The K4 with one doubled edge under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for K4+doubled.""" _check_spqr_invariants(self, self.g, self.root) def test_two_nodes_total(self) -> None: """Test that K4+doubled produces exactly 2 SPQR nodes.""" self.assertEqual( len(self.all_nodes), 2, f"Expected 2 nodes, got {len(self.all_nodes)}: " f"{_count_nodes_by_type(self.root)}", ) def test_one_p_one_r(self) -> None: """Test that K4+doubled has 1 P-node and 1 R-node.""" p: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.P ] r: list[SPQRNode] = [ n for n in self.all_nodes if n.type == NodeType.R ] self.assertEqual(len(p), 1, "Expected 1 P-node") self.assertEqual(len(r), 1, "Expected 1 R-node") def _make_mobius_kantor() -> MultiGraph: """Build the Mobius-Kantor graph GP(8,3) (16 verts, 24 edges). The Mobius-Kantor graph is the generalized Petersen graph GP(8,3). It is 3-regular and 3-connected, yielding a single R-node with 24 real edges. Inspired by the SageMath ``spqr_tree`` test suite. :return: A MultiGraph representing the Mobius-Kantor graph. """ g: MultiGraph = MultiGraph() # Outer 8-cycle: 0-1-2-3-4-5-6-7-0 for i in range(8): g.add_edge(i, (i + 1) % 8) # Spokes: i -> 8+i for i in range(8): g.add_edge(i, 8 + i) # Inner star (step 3): 8+i -> 8+(i+3)%8 for i in range(8): g.add_edge(8 + i, 8 + (i + 3) % 8) return g class TestSPQRMobiusKantor(unittest.TestCase): """Tests for the SPQR-tree of the Mobius-Kantor graph. The Mobius-Kantor graph has 16 vertices and 24 edges. It is 3-connected, yielding a single R-node. Inspired by the SageMath ``spqr_tree`` test suite. """ def setUp(self) -> None: """Build the SPQR-tree for the Mobius-Kantor graph.""" self.g: MultiGraph = _make_mobius_kantor() """The Mobius-Kantor graph under test.""" self.root: SPQRNode = build_spqr_tree(self.g) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_all_invariants(self) -> None: """Test all SPQR-tree invariants for Mobius-Kantor.""" _check_spqr_invariants(self, self.g, self.root) def test_single_r_node(self) -> None: """Test that Mobius-Kantor produces exactly 1 R-node.""" self.assertEqual(len(self.all_nodes), 1) self.assertEqual(self.root.type, NodeType.R) def test_twenty_four_real_edges_in_skeleton(self) -> None: """Test that the R-node skeleton has 24 real edges.""" real: list[Edge] = [ e for e in self.root.skeleton.edges if not e.virtual ] self.assertEqual(len(real), 24) def _make_large_ladder(n: int) -> MultiGraph: """Build a ladder graph with n rungs (2n vertices, 3n-2 edges). The ladder graph is biconnected and produces a chain of triconnected components, exercising the full SPQR algorithm. :param n: The number of rungs (must be >= 2). :return: A MultiGraph representing the ladder. """ g: MultiGraph = MultiGraph() for i in range(n): g.add_edge(2 * i, 2 * i + 1) for i in range(n - 1): g.add_edge(2 * i, 2 * (i + 1)) g.add_edge(2 * i + 1, 2 * (i + 1) + 1) return g class LinearTimeComplexityTest(unittest.TestCase): """Test that SPQR-tree construction runs in linear time. Builds ladder graphs of increasing sizes, measures the time for build_spqr_tree, and checks that the growth ratio is consistent with O(n) rather than O(n^2). """ def test_linear_time(self) -> None: """Test that doubling graph size roughly doubles run time. Builds ladder graphs with 2000, 4000, 8000, and 16000 rungs. For each size, measures the median time over 3 runs. Asserts that every ratio t(2n)/t(n) is below 3.0, which rejects O(n^2) behavior (expected ratio ~4) while tolerating noise in a linear algorithm (expected ratio ~2). """ sizes: list[int] = [2000, 4000, 8000, 16000] times: list[float] = [] for n in sizes: g: MultiGraph = _make_large_ladder(n) trial_times: list[float] = [] for _ in range(3): t0: float = time.perf_counter() build_spqr_tree(g.copy()) t1: float = time.perf_counter() trial_times.append(t1 - t0) trial_times.sort() times.append(trial_times[1]) # median for i in range(1, len(sizes)): ratio: float = times[i] / times[i - 1] self.assertLess( ratio, 3.0, f"Time ratio for n={sizes[i]} vs " f"n={sizes[i - 1]} is {ratio:.2f}, " f"suggesting super-linear growth " f"(times: {times})" ) def _make_wikimedia_spqr() -> MultiGraph: """Build the Wikimedia Commons SPQR-tree example graph. 16 vertices (a-p), 26 edges. From File:SPQR_tree_2.svg. :return: A MultiGraph representing the Wikimedia SPQR example. """ g: MultiGraph = MultiGraph() edges: list[tuple[str, str]] = [ ('a', 'b'), ('a', 'c'), ('a', 'g'), ('b', 'd'), ('b', 'h'), ('c', 'd'), ('c', 'e'), ('d', 'f'), ('e', 'f'), ('e', 'g'), ('f', 'h'), ('h', 'i'), ('h', 'j'), ('i', 'j'), ('i', 'n'), ('j', 'k'), ('k', 'm'), ('k', 'n'), ('m', 'n'), ('l', 'm'), ('l', 'o'), ('l', 'p'), ('m', 'o'), ('m', 'p'), ('o', 'p'), ('g', 'l'), ] for u, v in edges: g.add_edge(u, v) return g def _make_rpst_fig1a() -> MultiGraph: """Build the RPST paper Figure 1(a) graph. 15 vertices, 19 edges. From Polyvyanyy, Vanhatalo & Voelzer (2011), Figure 1(a). :return: A MultiGraph representing RPST Fig 1(a). """ g: MultiGraph = MultiGraph() edges: list[tuple[str, str]] = [ ('s', 'u'), ('a1', 'u'), ('a4', 'u'), ('a1', 'v'), ('a4', 'w'), ('a3', 'v'), ('a3', 'w'), ('a2', 'v'), ('a5', 'w'), ('a2', 'x'), ('a5', 'x'), ('x', 'y'), ('a6', 'y'), ('y', 'z'), ('a7', 'y'), ('a6', 'z'), ('a7', 'z'), ('t', 'z'), ('s', 't'), ] for u, v in edges: g.add_edge(u, v) return g class TestSPQRWikimediaSpqr(unittest.TestCase): """Tests for the SPQR-tree of the Wikimedia SPQR example.""" def setUp(self) -> None: """Build the SPQR-tree for the Wikimedia example.""" self.root: SPQRNode = \ build_spqr_tree(_make_wikimedia_spqr()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_node_count(self) -> None: """Test that the tree has exactly 5 nodes.""" self.assertEqual(len(self.all_nodes), 5) def test_r_node_count(self) -> None: """Test that there are exactly 3 R-nodes.""" n: int = sum( 1 for nd in self.all_nodes if nd.type == NodeType.R ) self.assertEqual(n, 3) def test_s_node_count(self) -> None: """Test that there is exactly 1 S-node.""" n: int = sum( 1 for nd in self.all_nodes if nd.type == NodeType.S ) self.assertEqual(n, 1) def test_p_node_count(self) -> None: """Test that there is exactly 1 P-node.""" n: int = sum( 1 for nd in self.all_nodes if nd.type == NodeType.P ) self.assertEqual(n, 1) def test_s_node_vertices(self) -> None: """Test that the S-node has vertices {g, h, l, m}.""" s_nodes: list[SPQRNode] = [ nd for nd in self.all_nodes if nd.type == NodeType.S ] self.assertEqual(len(s_nodes), 1) self.assertEqual( set(s_nodes[0].skeleton.vertices), {'g', 'h', 'l', 'm'}, ) def test_p_node_vertices(self) -> None: """Test that the P-node has vertices {l, m}.""" p_nodes: list[SPQRNode] = [ nd for nd in self.all_nodes if nd.type == NodeType.P ] self.assertEqual(len(p_nodes), 1) self.assertEqual( set(p_nodes[0].skeleton.vertices), {'l', 'm'}, ) def test_r_node_vertex_sets(self) -> None: """Test exact vertex sets of R-nodes.""" r_verts: list[frozenset[Hashable]] = [ frozenset(nd.skeleton.vertices) for nd in self.all_nodes if nd.type == NodeType.R ] expected: set[frozenset[Hashable]] = { frozenset({'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}), frozenset({'h', 'i', 'j', 'k', 'm', 'n'}), frozenset({'l', 'm', 'o', 'p'}), } self.assertEqual(set(r_verts), expected) def test_no_adjacent_s_nodes(self) -> None: """Test that no S-node is adjacent to another S-node.""" _assert_no_ss_pp(self, self.root, NodeType.S) def test_no_adjacent_p_nodes(self) -> None: """Test that no P-node is adjacent to another P-node.""" _assert_no_ss_pp(self, self.root, NodeType.P) class TestSPQRRpstFig1a(unittest.TestCase): """Tests for the SPQR-tree of the RPST Fig 1(a) graph.""" def setUp(self) -> None: """Build the SPQR-tree for RPST Fig 1(a).""" self.root: SPQRNode = \ build_spqr_tree(_make_rpst_fig1a()) """The root node of the SPQR tree.""" self.all_nodes: list[SPQRNode] = \ _collect_all_nodes(self.root) """All SPQR tree nodes.""" def test_node_count(self) -> None: """Test that the tree has exactly 10 nodes.""" self.assertEqual(len(self.all_nodes), 10) def test_r_node_count(self) -> None: """Test that there is exactly 1 R-node.""" n: int = sum( 1 for nd in self.all_nodes if nd.type == NodeType.R ) self.assertEqual(n, 1) def test_s_node_count(self) -> None: """Test that there are exactly 8 S-nodes.""" n: int = sum( 1 for nd in self.all_nodes if nd.type == NodeType.S ) self.assertEqual(n, 8) def test_p_node_count(self) -> None: """Test that there is exactly 1 P-node.""" n: int = sum( 1 for nd in self.all_nodes if nd.type == NodeType.P ) self.assertEqual(n, 1) def test_no_adjacent_s_nodes(self) -> None: """Test that no S-node is adjacent to another S-node.""" _assert_no_ss_pp(self, self.root, NodeType.S) def test_no_adjacent_p_nodes(self) -> None: """Test that no P-node is adjacent to another P-node.""" _assert_no_ss_pp(self, self.root, NodeType.P) class TestBuildSpqrTreeBiconnectivity(unittest.TestCase): """Tests that build_spqr_tree rejects non-biconnected graphs.""" def test_cut_vertex_raises(self) -> None: """Test that a graph with a cut vertex raises ValueError.""" g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(2, 3) g.add_edge(1, 3) g.add_edge(3, 4) g.add_edge(4, 5) g.add_edge(3, 5) with self.assertRaises(ValueError) as ctx: build_spqr_tree(g) self.assertIn("cut vertex", str(ctx.exception)) def test_disconnected_raises(self) -> None: """Test that a disconnected graph raises ValueError.""" g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(3, 4) with self.assertRaises(ValueError) as ctx: build_spqr_tree(g) self.assertIn("not connected", str(ctx.exception)) def test_path_raises(self) -> None: """Test that a path graph raises ValueError.""" g: MultiGraph = MultiGraph() g.add_edge(1, 2) g.add_edge(2, 3) with self.assertRaises(ValueError) as ctx: build_spqr_tree(g) self.assertIn("cut vertex", str(ctx.exception)) # Script used by TestSPQRTreeDeterminism to run SPQR-tree # construction in a subprocess with a specific PYTHONHASHSEED. _SUBPROCESS_SCRIPT: str = """ import json, sys from collections import deque sys.path.insert(0, "src") from spqrtree import MultiGraph, NodeType, SPQRNode, build_spqr_tree g = MultiGraph() edges = [ ('a', 'b'), ('a', 'c'), ('a', 'g'), ('b', 'd'), ('b', 'h'), ('c', 'd'), ('c', 'e'), ('d', 'f'), ('e', 'f'), ('e', 'g'), ('f', 'h'), ('h', 'i'), ('h', 'j'), ('i', 'j'), ('i', 'n'), ('j', 'k'), ('k', 'm'), ('k', 'n'), ('m', 'n'), ('l', 'm'), ('l', 'o'), ('l', 'p'), ('m', 'o'), ('m', 'p'), ('o', 'p'), ('g', 'l'), ] for u, v in edges: g.add_edge(u, v) root = build_spqr_tree(g) nodes = [] queue = deque([root]) while queue: nd = queue.popleft() verts = sorted(nd.skeleton.vertices) nodes.append({"type": nd.type.value, "vertices": verts}) for ch in nd.children: queue.append(ch) nodes.sort(key=lambda x: (x["type"], x["vertices"])) print(json.dumps(nodes)) """ class TestSPQRTreeDeterminism(unittest.TestCase): """Test that SPQR-tree construction is deterministic. Runs SPQR-tree construction of the Wikimedia SPQR example graph in subprocesses with different PYTHONHASHSEED values and verifies that all runs produce identical results. """ def test_deterministic_across_hash_seeds(self) -> None: """Test consistent results with 20 different hash seeds.""" results: list[str] = [] env: dict[str, str] = os.environ.copy() cwd: str = os.path.join( os.path.dirname(__file__), os.pardir ) for seed in range(20): env["PYTHONHASHSEED"] = str(seed) proc: subprocess.CompletedProcess[str] = \ subprocess.run( [sys.executable, "-c", _SUBPROCESS_SCRIPT], capture_output=True, text=True, env=env, cwd=cwd, ) self.assertEqual( proc.returncode, 0, f"seed={seed} failed:\n{proc.stderr}" ) results.append(proc.stdout.strip()) # All runs must produce the same result. for i, r in enumerate(results): self.assertEqual( r, results[0], f"seed={i} differs from seed=0:\n" f" seed=0: {results[0]}\n" f" seed={i}: {r}" )