Files
spqrtree/tests/test_triconnected.py
依瑪貓 23282de176 Initial commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:33:44 +08:00

2788 lines
91 KiB
Python

# 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 triconnected components algorithm (_triconnected.py).
Tests cover: triangle K3, 4-cycle C4, complete graph K4, two parallel
edges, three parallel edges, real-edge count invariant, and virtual
edge appearance count.
"""
import unittest
from collections.abc import Hashable
from spqrtree._graph import Edge, MultiGraph
from spqrtree._triconnected import (
ComponentType,
TriconnectedComponent,
find_triconnected_components,
)
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 _count_real_edges(
components: list[TriconnectedComponent],
) -> int:
"""Count total real (non-virtual) edges across all components.
:param components: A list of triconnected components.
:return: Total count of real edges summed over all components.
"""
return sum(
1
for comp in components
for e in comp.edges
if not e.virtual
)
def _virtual_edge_component_count(
components: list[TriconnectedComponent],
) -> dict[int, int]:
"""Count how many components each virtual edge appears in.
:param components: A list of triconnected components.
:return: A dict mapping virtual edge ID to component count.
"""
counts: dict[int, int] = {}
for comp in components:
for e in comp.edges:
if e.virtual:
counts[e.id] = counts.get(e.id, 0) + 1
return counts
class TestTriconnectedK3(unittest.TestCase):
"""Tests for triconnected decomposition of the triangle K3."""
def setUp(self) -> None:
"""Build the components for K3."""
self.g: MultiGraph = _make_k3()
"""The K3 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_returns_list(self) -> None:
"""Test that find_triconnected_components returns a list."""
self.assertIsInstance(self.comps, list)
def test_single_component(self) -> None:
"""Test that K3 produces exactly 1 triconnected component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_polygon(self) -> None:
"""Test that K3 yields a POLYGON component."""
self.assertEqual(self.comps[0].type, ComponentType.POLYGON)
def test_polygon_has_three_real_edges(self) -> None:
"""Test that the POLYGON from K3 contains 3 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 3)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedC4(unittest.TestCase):
"""Tests for triconnected decomposition of the 4-cycle C4."""
def setUp(self) -> None:
"""Build the components for C4."""
self.g: MultiGraph = _make_c4()
"""The C4 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_component(self) -> None:
"""Test that C4 produces exactly 1 triconnected component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_polygon(self) -> None:
"""Test that C4 yields a POLYGON component."""
self.assertEqual(self.comps[0].type, ComponentType.POLYGON)
def test_polygon_has_four_real_edges(self) -> None:
"""Test that the POLYGON from C4 contains 4 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 4)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedK4(unittest.TestCase):
"""Tests for triconnected decomposition of the complete graph K4."""
def setUp(self) -> None:
"""Build the components for K4."""
self.g: MultiGraph = _make_k4()
"""The K4 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_component(self) -> None:
"""Test that K4 produces exactly 1 triconnected component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that K4 yields a TRICONNECTED component."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED
)
def test_triconnected_has_six_real_edges(self) -> None:
"""Test that the TRICONNECTED from K4 has 6 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 6)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedTwoParallel(unittest.TestCase):
"""Tests for triconnected decomposition of two parallel edges."""
def setUp(self) -> None:
"""Build the components for 2 parallel edges between 1 and 2."""
self.g: MultiGraph = _make_two_parallel()
"""The two-parallel-edges graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_component(self) -> None:
"""Test that 2 parallel edges produce exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_bond(self) -> None:
"""Test that 2 parallel edges yield a BOND component."""
self.assertEqual(self.comps[0].type, ComponentType.BOND)
def test_bond_has_two_real_edges(self) -> None:
"""Test that the BOND from 2 parallel edges has 2 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 2)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedThreeParallel(unittest.TestCase):
"""Tests for triconnected decomposition of three parallel edges."""
def setUp(self) -> None:
"""Build the components for 3 parallel edges between 1 and 2."""
self.g: MultiGraph = _make_three_parallel()
"""The three-parallel-edges graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_component(self) -> None:
"""Test that 3 parallel edges produce exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_bond(self) -> None:
"""Test that 3 parallel edges yield a BOND component."""
self.assertEqual(self.comps[0].type, ComponentType.BOND)
def test_bond_has_three_real_edges(self) -> None:
"""Test that the BOND has 3 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 3)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedInvariants(unittest.TestCase):
"""Tests for global invariants across all graphs."""
def _check_real_edge_count(self, g: MultiGraph) -> None:
"""Check that real edge count is preserved across decomposition.
:param g: The input graph.
:return: None
"""
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
self.assertEqual(_count_real_edges(comps), g.num_edges())
def _check_virtual_edges_in_two_comps(
self, comps: list[TriconnectedComponent]
) -> None:
"""Check that each virtual edge appears in exactly 2 components.
:param comps: The list of triconnected components.
:return: None
"""
counts: dict[int, int] = \
_virtual_edge_component_count(comps)
for eid, cnt in counts.items():
self.assertEqual(
cnt,
2,
f"Virtual edge {eid} appears in {cnt} components "
f"(expected 2)",
)
def test_k3_real_edge_count(self) -> None:
"""Test real edge count invariant for K3."""
self._check_real_edge_count(_make_k3())
def test_c4_real_edge_count(self) -> None:
"""Test real edge count invariant for C4."""
self._check_real_edge_count(_make_c4())
def test_k4_real_edge_count(self) -> None:
"""Test real edge count invariant for K4."""
self._check_real_edge_count(_make_k4())
def test_two_parallel_real_edge_count(self) -> None:
"""Test real edge count invariant for 2 parallel edges."""
self._check_real_edge_count(_make_two_parallel())
def test_three_parallel_real_edge_count(self) -> None:
"""Test real edge count invariant for 3 parallel edges."""
self._check_real_edge_count(_make_three_parallel())
def test_k3_virtual_edges_in_two_comps(self) -> None:
"""Test virtual edge invariant for K3."""
comps: list[TriconnectedComponent] = \
find_triconnected_components(_make_k3())
self._check_virtual_edges_in_two_comps(comps)
def test_c4_virtual_edges_in_two_comps(self) -> None:
"""Test virtual edge invariant for C4."""
comps: list[TriconnectedComponent] = \
find_triconnected_components(_make_c4())
self._check_virtual_edges_in_two_comps(comps)
def test_k4_virtual_edges_in_two_comps(self) -> None:
"""Test virtual edge invariant for K4."""
comps: list[TriconnectedComponent] = \
find_triconnected_components(_make_k4())
self._check_virtual_edges_in_two_comps(comps)
def test_two_parallel_virtual_edges_in_two_comps(self) -> None:
"""Test virtual edge invariant for 2 parallel edges."""
comps: list[TriconnectedComponent] = \
find_triconnected_components(_make_two_parallel())
self._check_virtual_edges_in_two_comps(comps)
def test_three_parallel_virtual_edges_in_two_comps(self) -> None:
"""Test virtual edge invariant for 3 parallel edges."""
comps: list[TriconnectedComponent] = \
find_triconnected_components(_make_three_parallel())
self._check_virtual_edges_in_two_comps(comps)
def test_component_types_are_valid(self) -> None:
"""Test that all component types are valid ComponentType values."""
for g in [
_make_k3(), _make_c4(), _make_k4(),
_make_two_parallel(), _make_three_parallel(),
]:
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
for comp in comps:
self.assertIsInstance(comp.type, ComponentType)
self.assertIn(
comp.type,
[
ComponentType.BOND,
ComponentType.POLYGON,
ComponentType.TRICONNECTED,
],
)
def test_each_component_has_edges(self) -> None:
"""Test that every component has at least 2 edges."""
for g in [
_make_k3(), _make_c4(), _make_k4(),
_make_two_parallel(), _make_three_parallel(),
]:
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
for comp in comps:
self.assertGreaterEqual(
len(comp.edges),
2,
f"Component {comp.type} has fewer than 2 edges",
)
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.
The pair {2,3} is a separation pair.
: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.
The pair {1,2} is a separation pair.
: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.
Vertices 1-6; 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 TestTriconnectedDiamond(unittest.TestCase):
"""Tests for triconnected decomposition of the diamond graph."""
def setUp(self) -> None:
"""Build the components for the diamond graph."""
self.g: MultiGraph = _make_diamond()
"""The diamond graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_at_least_two_components(self) -> None:
"""Test that the diamond produces at least 2 components."""
self.assertGreaterEqual(
len(self.comps),
2,
"Diamond has separation pair {2,3}, expect >=2 components",
)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count (5)."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
def test_virtual_edges_in_exactly_two_comps(self) -> None:
"""Test that each virtual edge appears in exactly 2 components."""
counts: dict[int, int] = \
_virtual_edge_component_count(self.comps)
for eid, cnt in counts.items():
self.assertEqual(
cnt,
2,
f"Virtual edge {eid} appears in {cnt} components "
f"(expected 2)",
)
def test_no_ss_adjacency(self) -> None:
"""Test that no two S-type components share a virtual edge."""
_assert_no_same_type_adjacency(
self, self.comps, ComponentType.POLYGON
)
def test_no_pp_adjacency(self) -> None:
"""Test that no two P-type components share a virtual edge."""
_assert_no_same_type_adjacency(
self, self.comps, ComponentType.BOND
)
def test_each_component_has_at_least_two_edges(self) -> None:
"""Test that every component has at least 2 edges."""
for comp in self.comps:
self.assertGreaterEqual(
len(comp.edges),
2,
f"Component {comp.type} has fewer than 2 edges",
)
class TestTriconnectedTheta(unittest.TestCase):
"""Tests for triconnected decomposition of the theta graph."""
def setUp(self) -> None:
"""Build the components for the theta graph."""
self.g: MultiGraph = _make_theta()
"""The theta graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count (6)."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
def test_virtual_edges_in_exactly_two_comps(self) -> None:
"""Test that each virtual edge appears in exactly 2 components."""
counts: dict[int, int] = \
_virtual_edge_component_count(self.comps)
for eid, cnt in counts.items():
self.assertEqual(
cnt,
2,
f"Virtual edge {eid} appears in {cnt} components "
f"(expected 2)",
)
def test_no_ss_adjacency(self) -> None:
"""Test that no two S-type components share a virtual edge."""
_assert_no_same_type_adjacency(
self, self.comps, ComponentType.POLYGON
)
def test_no_pp_adjacency(self) -> None:
"""Test that no two P-type components share a virtual edge."""
_assert_no_same_type_adjacency(
self, self.comps, ComponentType.BOND
)
def test_each_component_has_at_least_two_edges(self) -> None:
"""Test that every component has at least 2 edges."""
for comp in self.comps:
self.assertGreaterEqual(
len(comp.edges),
2,
f"Component {comp.type} has fewer than 2 edges",
)
class TestTriconnectedPrism(unittest.TestCase):
"""Tests for triconnected decomposition of the triangular prism."""
def setUp(self) -> None:
"""Build the components for the triangular prism."""
self.g: MultiGraph = _make_prism()
"""The triangular prism graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_triconnected_component(self) -> None:
"""Test that the prism (3-connected) yields 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that the single component is TRICONNECTED."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED
)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count (9)."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
def test_nine_real_edges(self) -> None:
"""Test that the TRICONNECTED component contains all 9 edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 9)
def _assert_no_same_type_adjacency(
tc: unittest.TestCase,
comps: list[TriconnectedComponent],
ctype: ComponentType,
) -> None:
"""Assert that no two components of *ctype* share a virtual edge.
Checks the SPQR-tree invariant that adjacent components in the
decomposition have different types (no S-S or P-P pairs after
merging).
:param tc: The TestCase instance for assertions.
:param comps: The list of triconnected components.
:param ctype: The component type to check (BOND or POLYGON).
:return: None
"""
# Map: virtual edge ID -> list of component indices containing it.
ve_to_comps: dict[int, list[int]] = {}
for i, comp in enumerate(comps):
for e in comp.edges:
if e.virtual:
ve_to_comps.setdefault(e.id, []).append(i)
for eid, idxs in ve_to_comps.items():
if len(idxs) == 2:
i, j = idxs
both_same: bool = (
comps[i].type == ctype
and comps[j].type == ctype
)
tc.assertFalse(
both_same,
f"Virtual edge {eid} shared by two {ctype.name} "
f"components (S-S or P-P adjacency not allowed)",
)
class TestTriconnectedNoSSPP(unittest.TestCase):
"""Tests that no S-S or P-P adjacency occurs for any graph."""
def _check_no_ss(self, g: MultiGraph) -> None:
"""Check no S-S adjacency for graph g.
:param g: The input multigraph.
:return: None
"""
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
_assert_no_same_type_adjacency(
self, comps, ComponentType.POLYGON
)
def _check_no_pp(self, g: MultiGraph) -> None:
"""Check no P-P adjacency for graph g.
:param g: The input multigraph.
:return: None
"""
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
_assert_no_same_type_adjacency(
self, comps, ComponentType.BOND
)
def test_k3_no_ss(self) -> None:
"""Test no S-S adjacency for K3."""
self._check_no_ss(_make_k3())
def test_c4_no_ss(self) -> None:
"""Test no S-S adjacency for C4."""
self._check_no_ss(_make_c4())
def test_k4_no_ss(self) -> None:
"""Test no S-S adjacency for K4."""
self._check_no_ss(_make_k4())
def test_diamond_no_ss(self) -> None:
"""Test no S-S adjacency for the diamond graph."""
self._check_no_ss(_make_diamond())
def test_theta_no_ss(self) -> None:
"""Test no S-S adjacency for the theta graph."""
self._check_no_ss(_make_theta())
def test_prism_no_ss(self) -> None:
"""Test no S-S adjacency for the triangular prism."""
self._check_no_ss(_make_prism())
def test_k3_no_pp(self) -> None:
"""Test no P-P adjacency for K3."""
self._check_no_pp(_make_k3())
def test_c4_no_pp(self) -> None:
"""Test no P-P adjacency for C4."""
self._check_no_pp(_make_c4())
def test_k4_no_pp(self) -> None:
"""Test no P-P adjacency for K4."""
self._check_no_pp(_make_k4())
def test_diamond_no_pp(self) -> None:
"""Test no P-P adjacency for the diamond graph."""
self._check_no_pp(_make_diamond())
def test_theta_no_pp(self) -> None:
"""Test no P-P adjacency for the theta graph."""
self._check_no_pp(_make_theta())
def test_prism_no_pp(self) -> None:
"""Test no P-P adjacency for the triangular prism."""
self._check_no_pp(_make_prism())
def _check_all_invariants(
tc: unittest.TestCase,
g: MultiGraph,
comps: list[TriconnectedComponent],
) -> None:
"""Check all decomposition invariants for a given graph.
Verifies: real edge count preserved, virtual edges in exactly 2
components, no S-S or P-P adjacency, each component has >= 2 edges,
and reconstruction (real edges) matches the original graph.
:param tc: The TestCase instance for assertions.
:param g: The original input graph.
:param comps: The triconnected components to check.
:return: None
"""
# 1. Real edge count.
tc.assertEqual(
_count_real_edges(comps),
g.num_edges(),
"Real edge count mismatch",
)
# 2. Virtual edges in exactly 2 components.
counts: dict[int, int] = _virtual_edge_component_count(comps)
for eid, cnt in counts.items():
tc.assertEqual(
cnt, 2,
f"Virtual edge {eid} in {cnt} components (expected 2)",
)
# 3. No S-S adjacency.
_assert_no_same_type_adjacency(tc, comps, ComponentType.POLYGON)
# 4. No P-P adjacency.
_assert_no_same_type_adjacency(tc, comps, ComponentType.BOND)
# 5. Each component has at least 2 edges.
for comp in comps:
tc.assertGreaterEqual(
len(comp.edges), 2,
f"Component {comp.type} has fewer than 2 edges",
)
# 6. Reconstruction: real edges across all components == original.
orig_edge_ids: set[int] = {e.id for e in g.edges}
decomp_real_ids: set[int] = set()
for comp in comps:
for e in comp.edges:
if not e.virtual:
decomp_real_ids.add(e.id)
tc.assertEqual(
decomp_real_ids,
orig_edge_ids,
"Reconstructed real-edge set does not match original graph",
)
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] as the running example.
: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. Expected: 2 BOND + 1 POLYGON.
:return: A MultiGraph with embedded parallel edges.
"""
g: MultiGraph = MultiGraph()
# Cycle backbone: 1-2, 3-4, 4-5
g.add_edge(1, 2)
# Double edge 2-3
g.add_edge(2, 3)
g.add_edge(2, 3)
# Continue cycle: 3-4, 4-5
g.add_edge(3, 4)
g.add_edge(4, 5)
# Double edge 1-5
g.add_edge(1, 5)
g.add_edge(1, 5)
return g
class TestTriconnectedWikipediaExample(unittest.TestCase):
"""Tests for triconnected decomposition of the Wikipedia example."""
def setUp(self) -> None:
"""Build the components for the Wikipedia SPQR-tree example."""
self.g: MultiGraph = _make_wikipedia_example()
"""The Wikipedia example graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for the Wikipedia example."""
_check_all_invariants(self, self.g, self.comps)
def test_at_least_two_components(self) -> None:
"""Test that the Wikipedia example produces multiple components."""
self.assertGreaterEqual(
len(self.comps),
2,
"Wikipedia example has separation pairs, expect >=2 comps",
)
class TestTriconnectedHTExample(unittest.TestCase):
"""Tests for triconnected decomposition of the HT1973 example."""
def setUp(self) -> None:
"""Build the components for the Hopcroft-Tarjan 1973 example."""
self.g: MultiGraph = _make_ht_example()
"""The Hopcroft-Tarjan 1973 example graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for the HT1973 example."""
_check_all_invariants(self, self.g, self.comps)
def test_at_least_two_components(self) -> None:
"""Test that the HT1973 example produces multiple components."""
self.assertGreaterEqual(
len(self.comps),
2,
"HT1973 example has separation pairs, expect >=2 comps",
)
class TestTriconnectedGMExample(unittest.TestCase):
"""Tests for triconnected decomposition of the GM2001 example."""
def setUp(self) -> None:
"""Build the components for the Gutwenger-Mutzel 2001 example."""
self.g: MultiGraph = _make_gm_example()
"""The Gutwenger-Mutzel 2001 example graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for the GM2001 example."""
_check_all_invariants(self, self.g, self.comps)
def test_at_least_two_components(self) -> None:
"""Test that the GM2001 example produces multiple components."""
self.assertGreaterEqual(
len(self.comps),
2,
"GM2001 example has separation pairs, expect >=2 comps",
)
class TestTriconnectedMultiEdgeComplex(unittest.TestCase):
"""Tests for decomposition of a complex multi-edge graph.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the complex multi-edge graph."""
self.g: MultiGraph = _make_multiedge_complex()
"""The multi-edge complex graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for the multi-edge graph."""
_check_all_invariants(self, self.g, self.comps)
def test_has_bond_components(self) -> None:
"""Test that multi-edges produce BOND components."""
bond_types: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertGreaterEqual(
len(bond_types), 1,
"Multi-edge graph should have at least one BOND "
"component",
)
def test_has_polygon_component(self) -> None:
"""Test that the backbone cycle produces a POLYGON component."""
poly_types: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
self.assertGreaterEqual(
len(poly_types), 1,
"Multi-edge graph should have at least one POLYGON",
)
def test_exact_component_structure(self) -> None:
"""Test exact component counts: 2 BONDs and 1 POLYGON.
The graph (1,2),(1,5)x2,(2,3)x2,(3,4),(4,5) has two
parallel pairs -> 2 BOND, and a backbone cycle -> 1 POLYGON.
"""
bond_count: int = sum(
1 for c in self.comps
if c.type == ComponentType.BOND
)
poly_count: int = sum(
1 for c in self.comps
if c.type == ComponentType.POLYGON
)
self.assertEqual(
bond_count, 2,
f"Expected 2 BOND components, got {bond_count}",
)
self.assertEqual(
poly_count, 1,
f"Expected 1 POLYGON component, got {poly_count}",
)
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 (0,3) creates a separation pair {0,3} splitting the
graph into two 4-cycles and a degenerate bond.
: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 triconnected.
: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 (triconnected).
Outer 5-cycle: 0-1-2-3-4-0.
Spokes: 0-5, 1-6, 2-7, 3-8, 4-9.
Inner pentagram: 5-7, 7-9, 9-6, 6-8, 8-5.
: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_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
the 6 edges among {0,1,a,b}. The edge (0,1) appears 3 times.
: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 components (1 BOND + 3 POLYGON).
: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
class TestTriconnectedC5(unittest.TestCase):
"""Tests for triconnected decomposition of the 5-cycle C5."""
def setUp(self) -> None:
"""Build the components for C5."""
self.g: MultiGraph = _make_c5()
"""The C5 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_polygon_component(self) -> None:
"""Test that C5 yields exactly 1 POLYGON component."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(self.comps[0].type, ComponentType.POLYGON)
def test_five_real_edges(self) -> None:
"""Test that the POLYGON from C5 contains 5 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 5)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedC6(unittest.TestCase):
"""Tests for triconnected decomposition of the 6-cycle C6.
Expected: 1 POLYGON component (the entire cycle).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for C6."""
self.g: MultiGraph = _make_c6()
"""The C6 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_polygon_component(self) -> None:
"""Test that C6 yields exactly 1 POLYGON component."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(self.comps[0].type, ComponentType.POLYGON)
def test_six_real_edges(self) -> None:
"""Test that the POLYGON from C6 contains 6 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 6)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedC6Chord(unittest.TestCase):
"""Tests for triconnected decomposition of C6 plus chord (0,3).
The chord (0,3) creates separation pair {0,3} yielding 3
components: 2 POLYGON and 1 BOND.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for C6 with chord."""
self.g: MultiGraph = _make_c6_with_chord()
"""The C6-with-chord graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for C6 plus chord."""
_check_all_invariants(self, self.g, self.comps)
def test_three_components(self) -> None:
"""Test that C6 plus chord produces exactly 3 components.
The chord (0,3) creates separation pair {0,3} yielding
2 POLYGON components and 1 BOND.
"""
self.assertEqual(
len(self.comps),
3,
f"C6+chord should have 3 components, "
f"got {len(self.comps)}",
)
def test_two_polygon_components(self) -> None:
"""Test that C6 plus chord has 2 POLYGON components."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
self.assertEqual(
len(poly), 2,
f"Expected 2 POLYGON components, got {len(poly)}",
)
def test_one_bond_component(self) -> None:
"""Test that C6 plus chord has 1 BOND component."""
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(
len(bond), 1,
f"Expected 1 BOND component, got {len(bond)}",
)
class TestTriconnectedK5(unittest.TestCase):
"""Tests for triconnected decomposition of the complete graph K5.
K5 is 4-connected, so the entire graph is one TRICONNECTED
component.
"""
def setUp(self) -> None:
"""Build the components for K5."""
self.g: MultiGraph = _make_k5()
"""The K5 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_triconnected_component(self) -> None:
"""Test that K5 yields exactly 1 TRICONNECTED component."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED
)
def test_ten_real_edges(self) -> None:
"""Test that the TRICONNECTED component has 10 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 10)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedPetersen(unittest.TestCase):
"""Tests for triconnected decomposition of the Petersen graph.
The Petersen graph is 3-connected, so it yields a single
TRICONNECTED component with all 15 edges.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the Petersen graph."""
self.g: MultiGraph = _make_petersen()
"""The Petersen graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_triconnected_component(self) -> None:
"""Test that the Petersen graph yields 1 TRICONNECTED."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED
)
def test_fifteen_real_edges(self) -> None:
"""Test that the TRICONNECTED component has 15 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 15)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedThreeK4Cliques(unittest.TestCase):
"""Tests for decomposition of 3 K4 cliques sharing poles {0,1}.
Expected: 4 components: 1 BOND (3-way parallel at {0,1}) and
3 TRICONNECTED components (one per K4 clique).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the three-K4-cliques graph."""
self.g: MultiGraph = _make_three_k4_cliques()
"""The three-K4-cliques graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for three K4 cliques."""
_check_all_invariants(self, self.g, self.comps)
def test_four_components_total(self) -> None:
"""Test that the graph produces exactly 4 components.
Expected: 1 BOND + 3 TRICONNECTED = 4 total.
"""
self.assertEqual(
len(self.comps),
4,
f"Expected 4 components, got {len(self.comps)}",
)
def test_three_triconnected_components(self) -> None:
"""Test that there are exactly 3 TRICONNECTED components."""
tc: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.TRICONNECTED
]
self.assertEqual(
len(tc), 3,
f"Expected 3 TRICONNECTED components, "
f"got {len(tc)}",
)
def test_one_bond_component(self) -> None:
"""Test that there is exactly 1 BOND component."""
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(
len(bond), 1,
f"Expected 1 BOND component, got {len(bond)}",
)
class TestTriconnectedThreeLongPaths(unittest.TestCase):
"""Tests for decomposition of 3 length-3 paths between 0 and 1.
Expected: 4 components: 1 BOND (3-way parallel at {0,1}) and
3 POLYGON components (one per length-3 path).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the three-long-paths graph."""
self.g: MultiGraph = _make_three_long_paths()
"""The three-long-paths graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for three long paths."""
_check_all_invariants(self, self.g, self.comps)
def test_four_components_total(self) -> None:
"""Test that the graph produces exactly 4 components.
Expected: 1 BOND + 3 POLYGON = 4 total.
"""
self.assertEqual(
len(self.comps),
4,
f"Expected 4 components, got {len(self.comps)}",
)
def test_three_polygon_components(self) -> None:
"""Test that there are exactly 3 POLYGON components."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
self.assertEqual(
len(poly), 3,
f"Expected 3 POLYGON components, "
f"got {len(poly)}",
)
def test_one_bond_component(self) -> None:
"""Test that there is exactly 1 BOND component."""
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(
len(bond), 1,
f"Expected 1 BOND component, got {len(bond)}",
)
def _make_k33() -> MultiGraph:
"""Build the complete bipartite graph K_{3,3} (9 edges).
Vertices 0,1,2 on one side and 3,4,5 on the other. K_{3,3}
is 3-connected (triconnected) and non-planar.
: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 the wheel graph W4: hub vertex 0, rim vertices 1-4.
Edges: hub spokes (0,1),(0,2),(0,3),(0,4) and rim cycle
(1,2),(2,3),(3,4),(4,1). W4 is 3-connected.
: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 total).
Each of the 3 triangle edges appears twice. Expected: 4
components (1 POLYGON + 3 BOND).
:return: A MultiGraph representing K3 with doubled edges.
"""
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 a graph with 4 parallel edges between vertices 1 and 2.
:return: A MultiGraph with four parallel edges.
"""
g: MultiGraph = MultiGraph()
for _ in range(4):
g.add_edge(1, 2)
return g
def _make_five_parallel() -> MultiGraph:
"""Build a graph with 5 parallel edges between vertices 1 and 2.
:return: A MultiGraph with five 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 between 0 and 1 with each edge doubled.
All 9 edges of _make_three_long_paths() appear twice. Expected:
13 components (3 POLYGON + 10 BOND).
:return: A MultiGraph with doubled 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 separation pairs yielding 12 split
components (5 BOND + 2 TRICONNECTED + 5 POLYGON).
: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 path
u-w1-w2-v alongside. Round 2: for each of the 60 edges from
round 1, add path alongside. Result: 160 vertices, 240 edges.
Expected: 136 components (60 BOND + 75 POLYGON + 1 TRICONNECTED).
: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
# Round 2: augment the 60 edges from round 1.
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 TestTriconnectedK33(unittest.TestCase):
"""Tests for triconnected decomposition of K_{3,3}.
K_{3,3} is 3-connected, so it yields a single TRICONNECTED
component.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for K_{3,3}."""
self.g: MultiGraph = _make_k33()
"""The K_{3,3} graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for K_{3,3}."""
_check_all_invariants(self, self.g, self.comps)
def test_single_triconnected_component(self) -> None:
"""Test that K_{3,3} yields exactly 1 TRICONNECTED."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED
)
def test_nine_real_edges(self) -> None:
"""Test that the TRICONNECTED component has 9 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 9)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedW4(unittest.TestCase):
"""Tests for triconnected decomposition of the wheel graph W4.
W4 (hub + 4-rim) is 3-connected, yielding 1 TRICONNECTED.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for W4."""
self.g: MultiGraph = _make_w4()
"""The W4 wheel graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for W4."""
_check_all_invariants(self, self.g, self.comps)
def test_single_triconnected_component(self) -> None:
"""Test that W4 yields exactly 1 TRICONNECTED component."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED
)
def test_eight_real_edges(self) -> None:
"""Test that the TRICONNECTED component has 8 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 8)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedK3Doubled(unittest.TestCase):
"""Tests for triconnected decomposition of K3 with doubled edges.
Each K3 edge appears twice. Expected: 1 POLYGON (backbone) and
3 BOND components (one per parallel pair).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for K3 with doubled edges."""
self.g: MultiGraph = _make_k3_doubled()
"""The K3-doubled graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for K3 doubled."""
_check_all_invariants(self, self.g, self.comps)
def test_four_components(self) -> None:
"""Test exactly 4 components: 1 POLYGON + 3 BOND."""
self.assertEqual(
len(self.comps), 4,
f"Expected 4 components, got {len(self.comps)}",
)
def test_one_polygon_three_bonds(self) -> None:
"""Test 1 POLYGON and 3 BOND components."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(len(poly), 1, "Expected 1 POLYGON")
self.assertEqual(len(bond), 3, "Expected 3 BOND")
class TestTriconnectedFourParallel(unittest.TestCase):
"""Tests for triconnected decomposition of 4 parallel edges."""
def setUp(self) -> None:
"""Build the components for 4 parallel edges."""
self.g: MultiGraph = _make_four_parallel()
"""The four-parallel-edges graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_bond_component(self) -> None:
"""Test that 4 parallel edges yield exactly 1 BOND."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(
self.comps[0].type, ComponentType.BOND
)
def test_four_real_edges(self) -> None:
"""Test that the BOND has 4 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 4)
def test_total_real_edges(self) -> None:
"""Test that total real edge count equals input edge count."""
self.assertEqual(
_count_real_edges(self.comps), self.g.num_edges()
)
class TestTriconnectedFiveParallel(unittest.TestCase):
"""Tests for triconnected decomposition of 5 parallel edges."""
def setUp(self) -> None:
"""Build the components for 5 parallel edges."""
self.g: MultiGraph = _make_five_parallel()
"""The five-parallel-edges graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_single_bond_component(self) -> None:
"""Test that 5 parallel edges yield exactly 1 BOND."""
self.assertEqual(len(self.comps), 1)
self.assertEqual(
self.comps[0].type, ComponentType.BOND
)
def test_five_real_edges(self) -> None:
"""Test that the BOND has 5 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 5)
class TestTriconnectedThreeLongPathsDoubled(unittest.TestCase):
"""Tests for 3 length-3 paths with all edges doubled.
Each of the 9 edges appears twice, yielding 13 components
(3 POLYGON + 10 BOND).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the doubled 3-paths graph."""
self.g: MultiGraph = \
_make_three_long_paths_doubled()
"""The doubled three-long-paths graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for doubled 3-paths."""
_check_all_invariants(self, self.g, self.comps)
def test_at_least_four_components(self) -> None:
"""Test that doubled 3-paths produces many components."""
self.assertGreaterEqual(
len(self.comps), 4,
"Doubled 3-paths should have many components",
)
def test_has_polygon_and_bond(self) -> None:
"""Test that both POLYGON and BOND components exist."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertGreaterEqual(
len(poly), 1, "Need >= 1 POLYGON"
)
self.assertGreaterEqual(
len(bond), 1, "Need >= 1 BOND"
)
class TestTriconnectedSageDocstringGraph(unittest.TestCase):
"""Tests for the 13-vertex, 23-edge graph (graph6 'LlCG{O@?GBoMw?').
This biconnected graph has multiple separation pairs and yields
12 split components (5 BOND + 2 TRICONNECTED + 5 POLYGON).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the 13-vertex docstring graph."""
self.g: MultiGraph = _make_graph6_sage_docstring()
"""The SageMath docstring graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for the 13V/23E graph."""
_check_all_invariants(self, self.g, self.comps)
def test_at_least_two_components(self) -> None:
"""Test that the 13V/23E graph produces multiple components."""
self.assertGreaterEqual(
len(self.comps),
2,
"Graph has separation pairs and must have > 1 component",
)
class TestTriconnectedPetersenAugmentedTwice(unittest.TestCase):
"""Tests for the doubly-augmented Petersen graph.
Round 1: for each of the 15 Petersen edges (u,v), a parallel path
u-w1-w2-v is added alongside. Round 2: for each of the 60 edges
from round 1, another parallel path is added.
Result: 160 vertices, 240 edges, 136 components.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the doubly-augmented Petersen."""
self.g: MultiGraph = \
_make_petersen_augmented_twice()
"""The doubly-augmented Petersen graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for doubly-aug. Petersen."""
_check_all_invariants(self, self.g, self.comps)
def test_136_total_components(self) -> None:
"""Test that the doubly-augmented Petersen yields 136 comps.
Expected: 60 BOND + 75 POLYGON + 1 TRICONNECTED = 136 total.
"""
self.assertEqual(
len(self.comps),
136,
f"Doubly-augmented Petersen should have 136 components, "
f"got {len(self.comps)}",
)
def test_one_triconnected(self) -> None:
"""Test that there is exactly 1 TRICONNECTED component."""
tc: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.TRICONNECTED
]
self.assertEqual(
len(tc), 1,
f"Expected 1 TRICONNECTED, got {len(tc)}",
)
def test_sixty_bonds(self) -> None:
"""Test that there are exactly 60 BOND components."""
bonds: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(
len(bonds), 60,
f"Expected 60 BOND, got {len(bonds)}",
)
def test_seventy_five_polygons(self) -> None:
"""Test that there are exactly 75 POLYGON components."""
polys: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
self.assertEqual(
len(polys), 75,
f"Expected 75 POLYGON, got {len(polys)}",
)
class TestTriconnectedDiamondExact(unittest.TestCase):
"""Exact component-type tests for the diamond graph.
The diamond has separation pair {2,3}. Expected: exactly 3
components: 2 POLYGON (the two triangular halves) and 1 BOND
(the direct edge (2,3) with 2 virtual edges = P-node).
"""
def setUp(self) -> None:
"""Build the components for the diamond graph."""
self.g: MultiGraph = _make_diamond()
"""The diamond graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_exactly_three_components(self) -> None:
"""Test that the diamond produces exactly 3 components."""
self.assertEqual(
len(self.comps),
3,
f"Diamond should have 3 components, "
f"got {len(self.comps)}",
)
def test_two_polygons_one_bond(self) -> None:
"""Test that diamond has 2 POLYGON and 1 BOND components."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(len(poly), 2, "Expected 2 POLYGON")
self.assertEqual(len(bond), 1, "Expected 1 BOND")
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 components: 3 POLYGON + 2 BOND.
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 TestTriconnectedLadder(unittest.TestCase):
"""Tests for triconnected decomposition of the 3-rung ladder graph.
The ladder (2x4 grid) has separation pairs {1,5} and {2,6}.
Expected: 5 components (3 POLYGON + 2 BOND).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the ladder graph."""
self.g: MultiGraph = _make_ladder()
"""The 3-rung ladder graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for the ladder graph."""
_check_all_invariants(self, self.g, self.comps)
def test_five_components(self) -> None:
"""Test that the ladder graph yields exactly 5 components."""
self.assertEqual(
len(self.comps),
5,
f"Expected 5 components, got {len(self.comps)}",
)
def test_three_polygons_two_bonds(self) -> None:
"""Test that the ladder has 3 POLYGON and 2 BOND components."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(len(poly), 3, "Expected 3 POLYGON")
self.assertEqual(len(bond), 2, "Expected 2 BOND")
def test_real_edge_count(self) -> None:
"""Test that total real edge count equals 10."""
self.assertEqual(
_count_real_edges(self.comps),
self.g.num_edges(),
)
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 TestTriconnectedC7(unittest.TestCase):
"""Tests for triconnected decomposition of the 7-cycle C7.
C7 is biconnected but not triconnected. It yields a single
POLYGON component with 7 real edges.
"""
def setUp(self) -> None:
"""Build the components for C7."""
self.g: MultiGraph = _make_c7()
"""The C7 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for C7."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that C7 produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_polygon(self) -> None:
"""Test that C7 yields a POLYGON component."""
self.assertEqual(self.comps[0].type, ComponentType.POLYGON)
def test_seven_real_edges(self) -> None:
"""Test that the POLYGON has 7 real edges."""
real: list[Edge] = [
e for e in self.comps[0].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 TestTriconnectedC8(unittest.TestCase):
"""Tests for triconnected decomposition of the 8-cycle C8.
C8 is biconnected but not triconnected. It yields a single
POLYGON component with 8 real edges.
"""
def setUp(self) -> None:
"""Build the components for C8."""
self.g: MultiGraph = _make_c8()
"""The C8 graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for C8."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that C8 produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_polygon(self) -> None:
"""Test that C8 yields a POLYGON component."""
self.assertEqual(self.comps[0].type, ComponentType.POLYGON)
def test_eight_real_edges(self) -> None:
"""Test that the POLYGON has 8 real edges."""
real: list[Edge] = [
e for e in self.comps[0].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: removing {0,1} disconnects the graph.
Each internal vertex x in {2,3,4} creates a path 0-x-1,
yielding 4 components: 3 POLYGON + 1 BOND.
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 TestTriconnectedK23(unittest.TestCase):
"""Tests for triconnected decomposition of K_{2,3}.
K_{2,3} has 5 vertices and 6 edges. It has vertex
connectivity 2 (separation pair {0,1}), yielding
4 components: 3 POLYGON + 1 BOND.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for K_{2,3}."""
self.g: MultiGraph = _make_k23()
"""The K_{2,3} graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for K_{2,3}."""
_check_all_invariants(self, self.g, self.comps)
def test_four_components(self) -> None:
"""Test that K_{2,3} produces exactly 4 components."""
self.assertEqual(
len(self.comps), 4,
f"Expected 4 components, got {len(self.comps)}",
)
def test_three_polygons_one_bond(self) -> None:
"""Test that K_{2,3} has 3 POLYGON and 1 BOND."""
poly: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(len(poly), 3, "Expected 3 POLYGON")
self.assertEqual(len(bond), 1, "Expected 1 BOND")
def test_real_edge_count(self) -> None:
"""Test that total real edge count equals 6."""
self.assertEqual(
_count_real_edges(self.comps),
self.g.num_edges(),
)
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 1 TRICONNECTED 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 TestTriconnectedW5(unittest.TestCase):
"""Tests for triconnected decomposition of the wheel W5.
W5 has 6 vertices and 10 edges. It is triconnected,
so it yields a single TRICONNECTED component.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for W5."""
self.g: MultiGraph = _make_w5()
"""The W5 wheel graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for W5."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that W5 produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that W5 yields a TRICONNECTED component."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED,
)
def test_ten_real_edges(self) -> None:
"""Test that the TRICONNECTED has 10 real edges."""
real: list[Edge] = [
e for e in self.comps[0].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 1 TRICONNECTED 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 TestTriconnectedW6(unittest.TestCase):
"""Tests for triconnected decomposition of the wheel W6.
W6 has 7 vertices and 12 edges. It is triconnected,
so it yields a single TRICONNECTED component.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for W6."""
self.g: MultiGraph = _make_w6()
"""The W6 wheel graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for W6."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that W6 produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that W6 yields a TRICONNECTED component."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED,
)
def test_twelve_real_edges(self) -> None:
"""Test that the TRICONNECTED has 12 real edges."""
real: list[Edge] = [
e for e in self.comps[0].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 1 TRICONNECTED component with 12 edges.
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 TestTriconnectedQ3Cube(unittest.TestCase):
"""Tests for triconnected decomposition of the Q3 cube graph.
The cube (Q3) has 8 vertices and 12 edges. It is
3-connected, so it yields a single TRICONNECTED component.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the Q3 cube."""
self.g: MultiGraph = _make_q3_cube()
"""The Q3 cube graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for Q3 cube."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that Q3 cube produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that Q3 cube yields a TRICONNECTED component."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED,
)
def test_twelve_real_edges(self) -> None:
"""Test that the TRICONNECTED has 12 real edges."""
real: list[Edge] = [
e for e in self.comps[0].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 1 TRICONNECTED component.
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 TestTriconnectedOctahedron(unittest.TestCase):
"""Tests for triconnected decomposition of the octahedron.
The octahedron has 6 vertices and 12 edges. It is 4-connected,
so it yields a single TRICONNECTED component.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the octahedron."""
self.g: MultiGraph = _make_octahedron()
"""The octahedron graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for octahedron."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that octahedron produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that octahedron yields a TRICONNECTED component."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED,
)
def test_twelve_real_edges(self) -> None:
"""Test that the TRICONNECTED has 12 real edges."""
real: list[Edge] = [
e for e in self.comps[0].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 (say 1-2) is doubled, giving 7
edges total. The doubled edge creates a separation pair
{1,2}, yielding 2 components: 1 BOND (the doubled edge) and
1 TRICONNECTED (the K4 skeleton with a virtual edge).
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 TestTriconnectedK4OneDoubled(unittest.TestCase):
"""Tests for triconnected decomposition of K4 + 1 doubled edge.
K4 with one edge doubled has 7 edges. The doubled edge
creates separation pair {1,2}, yielding 2 components:
1 BOND and 1 TRICONNECTED.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for K4 + 1 doubled edge."""
self.g: MultiGraph = _make_k4_one_doubled()
"""The K4-one-doubled graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for K4+doubled."""
_check_all_invariants(self, self.g, self.comps)
def test_two_components(self) -> None:
"""Test that K4+doubled produces exactly 2 components."""
self.assertEqual(
len(self.comps), 2,
f"Expected 2 components, got {len(self.comps)}",
)
def test_one_bond_one_triconnected(self) -> None:
"""Test that K4+doubled has 1 BOND and 1 TRICONNECTED."""
bond: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
tri: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.TRICONNECTED
]
self.assertEqual(len(bond), 1, "Expected 1 BOND")
self.assertEqual(
len(tri), 1, "Expected 1 TRICONNECTED",
)
def test_real_edge_count(self) -> None:
"""Test that total real edge count equals 7."""
self.assertEqual(
_count_real_edges(self.comps),
self.g.num_edges(),
)
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 1
TRICONNECTED component 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 TestTriconnectedMobiusKantor(unittest.TestCase):
"""Tests for triconnected decomposition of Mobius-Kantor graph.
The Mobius-Kantor graph has 16 vertices and 24 edges. It is
3-connected, so it yields a single TRICONNECTED component.
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the Mobius-Kantor graph."""
self.g: MultiGraph = _make_mobius_kantor()
"""The Mobius-Kantor graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for Mobius-Kantor."""
_check_all_invariants(self, self.g, self.comps)
def test_single_component(self) -> None:
"""Test that Mobius-Kantor produces exactly 1 component."""
self.assertEqual(len(self.comps), 1)
def test_component_is_triconnected(self) -> None:
"""Test that Mobius-Kantor yields TRICONNECTED."""
self.assertEqual(
self.comps[0].type, ComponentType.TRICONNECTED,
)
def test_twenty_four_real_edges(self) -> None:
"""Test that the TRICONNECTED has 24 real edges."""
real: list[Edge] = [
e for e in self.comps[0].edges if not e.virtual
]
self.assertEqual(len(real), 24)
def _make_petersen_augmented() -> MultiGraph:
"""Build the Petersen graph with each edge augmented by a path.
For each of the 15 Petersen edges (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 components (15 BOND + 15 POLYGON + 1 TRICONNECTED).
Inspired by the SageMath ``spqr_tree`` test suite.
:return: The 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:
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
class TestTriconnectedPetersenAugmented(unittest.TestCase):
"""Tests for triconnected decomposition of augmented Petersen.
Each Petersen edge (u,v) gets a parallel path u-w1-w2-v.
Expected: 31 components (15 BOND + 15 POLYGON + 1 TRICONN).
Inspired by the SageMath ``spqr_tree`` test suite.
"""
def setUp(self) -> None:
"""Build the components for the augmented Petersen."""
self.g: MultiGraph = _make_petersen_augmented()
"""The augmented Petersen graph under test."""
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
"""The triconnected split components."""
def test_all_invariants(self) -> None:
"""Test all decomposition invariants for aug. Petersen."""
_check_all_invariants(self, self.g, self.comps)
def test_thirty_one_components(self) -> None:
"""Test that augmented Petersen has 31 components."""
self.assertEqual(
len(self.comps), 31,
f"Expected 31 components, got {len(self.comps)}",
)
def test_one_triconnected(self) -> None:
"""Test that there is exactly 1 TRICONNECTED component."""
tri: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.TRICONNECTED
]
self.assertEqual(
len(tri), 1,
f"Expected 1 TRICONNECTED, got {len(tri)}",
)
def test_fifteen_bonds(self) -> None:
"""Test that there are exactly 15 BOND components."""
bonds: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.BOND
]
self.assertEqual(
len(bonds), 15,
f"Expected 15 BOND, got {len(bonds)}",
)
def test_fifteen_polygons(self) -> None:
"""Test that there are exactly 15 POLYGON components."""
polys: list[TriconnectedComponent] = [
c for c in self.comps
if c.type == ComponentType.POLYGON
]
self.assertEqual(
len(polys), 15,
f"Expected 15 POLYGON, got {len(polys)}",
)
def test_real_edge_count(self) -> None:
"""Test that total real edge count equals 60."""
self.assertEqual(
_count_real_edges(self.comps),
self.g.num_edges(),
)
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 TestTriconnectedWikimediaSpqr(unittest.TestCase):
"""Tests for triconnected decomposition of the Wikimedia
SPQR example."""
def setUp(self) -> None:
"""Set up graph and triconnected components."""
self.g: MultiGraph = _make_wikimedia_spqr()
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
def test_component_count(self) -> None:
"""Test that there are exactly 5 components."""
self.assertEqual(len(self.comps), 5)
def test_triconnected_count(self) -> None:
"""Test that there are exactly 3 TRICONNECTED."""
n: int = sum(
1 for c in self.comps
if c.type == ComponentType.TRICONNECTED
)
self.assertEqual(n, 3)
def test_polygon_count(self) -> None:
"""Test that there is exactly 1 POLYGON."""
n: int = sum(
1 for c in self.comps
if c.type == ComponentType.POLYGON
)
self.assertEqual(n, 1)
def test_bond_count(self) -> None:
"""Test that there is exactly 1 BOND."""
n: int = sum(
1 for c in self.comps
if c.type == ComponentType.BOND
)
self.assertEqual(n, 1)
def test_all_invariants(self) -> None:
"""Test all decomposition invariants."""
_check_all_invariants(self, self.g, self.comps)
class TestTriconnectedRpstFig1a(unittest.TestCase):
"""Tests for triconnected decomposition of RPST Fig 1(a)."""
def setUp(self) -> None:
"""Set up graph and triconnected components."""
self.g: MultiGraph = _make_rpst_fig1a()
self.comps: list[TriconnectedComponent] = \
find_triconnected_components(self.g)
def test_component_count(self) -> None:
"""Test that there are exactly 10 components."""
self.assertEqual(len(self.comps), 10)
def test_triconnected_count(self) -> None:
"""Test that there is exactly 1 TRICONNECTED."""
n: int = sum(
1 for c in self.comps
if c.type == ComponentType.TRICONNECTED
)
self.assertEqual(n, 1)
def test_polygon_count(self) -> None:
"""Test that there are exactly 8 POLYGON."""
n: int = sum(
1 for c in self.comps
if c.type == ComponentType.POLYGON
)
self.assertEqual(n, 8)
def test_bond_count(self) -> None:
"""Test that there is exactly 1 BOND."""
n: int = sum(
1 for c in self.comps
if c.type == ComponentType.BOND
)
self.assertEqual(n, 1)
def test_all_invariants(self) -> None:
"""Test all decomposition invariants."""
_check_all_invariants(self, self.g, self.comps)