228
tests/test_graph.py
Normal file
228
tests/test_graph.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# Pure Python SPQR-Tree implementation.
|
||||
# Authors:
|
||||
# imacat@mail.imacat.idv.tw (imacat), 2026/3/1
|
||||
# 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 MultiGraph data structure (_graph.py)."""
|
||||
import unittest
|
||||
from collections.abc import Hashable
|
||||
|
||||
from spqrtree._graph import Edge, MultiGraph
|
||||
|
||||
|
||||
class TestEdge(unittest.TestCase):
|
||||
"""Tests for the Edge dataclass."""
|
||||
|
||||
def test_edge_creation(self) -> None:
|
||||
"""Test basic edge creation with required attributes."""
|
||||
e: Edge = Edge(id=0, u=1, v=2)
|
||||
self.assertEqual(e.id, 0)
|
||||
self.assertEqual(e.u, 1)
|
||||
self.assertEqual(e.v, 2)
|
||||
self.assertFalse(e.virtual)
|
||||
|
||||
def test_edge_virtual(self) -> None:
|
||||
"""Test creating a virtual edge."""
|
||||
e: Edge = Edge(id=1, u=3, v=4, virtual=True)
|
||||
self.assertTrue(e.virtual)
|
||||
|
||||
def test_edge_endpoints(self) -> None:
|
||||
"""Test that endpoints method returns both endpoints."""
|
||||
e: Edge = Edge(id=0, u=1, v=2)
|
||||
self.assertEqual(e.endpoints(), (1, 2))
|
||||
|
||||
def test_edge_other(self) -> None:
|
||||
"""Test that other() returns the opposite endpoint."""
|
||||
e: Edge = Edge(id=0, u=1, v=2)
|
||||
self.assertEqual(e.other(1), 2)
|
||||
self.assertEqual(e.other(2), 1)
|
||||
|
||||
|
||||
class TestMultiGraphVertices(unittest.TestCase):
|
||||
"""Tests for vertex operations on MultiGraph."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up a fresh MultiGraph for each test."""
|
||||
self.g: MultiGraph = MultiGraph()
|
||||
"""The graph under test."""
|
||||
|
||||
def test_add_vertex(self) -> None:
|
||||
"""Test adding a single vertex."""
|
||||
self.g.add_vertex(1)
|
||||
self.assertIn(1, self.g.vertices)
|
||||
|
||||
def test_add_multiple_vertices(self) -> None:
|
||||
"""Test adding multiple vertices."""
|
||||
for v in [1, 2, 3, 4]:
|
||||
self.g.add_vertex(v)
|
||||
self.assertEqual(set(self.g.vertices), {1, 2, 3, 4})
|
||||
|
||||
def test_add_duplicate_vertex(self) -> None:
|
||||
"""Test that adding a duplicate vertex is a no-op."""
|
||||
self.g.add_vertex(1)
|
||||
self.g.add_vertex(1)
|
||||
self.assertEqual(len(self.g.vertices), 1)
|
||||
|
||||
def test_remove_vertex(self) -> None:
|
||||
"""Test removing a vertex also removes its edges."""
|
||||
self.g.add_vertex(1)
|
||||
self.g.add_vertex(2)
|
||||
self.g.add_edge(1, 2)
|
||||
self.g.remove_vertex(1)
|
||||
self.assertNotIn(1, self.g.vertices)
|
||||
self.assertEqual(len(self.g.edges), 0)
|
||||
|
||||
def test_vertex_count(self) -> None:
|
||||
"""Test vertex count after additions."""
|
||||
for v in range(5):
|
||||
self.g.add_vertex(v)
|
||||
self.assertEqual(self.g.num_vertices(), 5)
|
||||
|
||||
|
||||
class TestMultiGraphEdges(unittest.TestCase):
|
||||
"""Tests for edge operations on MultiGraph."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up a fresh MultiGraph with two vertices."""
|
||||
self.g: MultiGraph = MultiGraph()
|
||||
"""The graph under test."""
|
||||
self.g.add_vertex(1)
|
||||
self.g.add_vertex(2)
|
||||
self.g.add_vertex(3)
|
||||
|
||||
def test_add_edge(self) -> None:
|
||||
"""Test adding an edge between two vertices."""
|
||||
e: Edge = self.g.add_edge(1, 2)
|
||||
self.assertIn(e.id, {e2.id for e2 in self.g.edges})
|
||||
self.assertEqual(e.u, 1)
|
||||
self.assertEqual(e.v, 2)
|
||||
|
||||
def test_add_edge_returns_edge(self) -> None:
|
||||
"""Test that add_edge returns an Edge object."""
|
||||
e: Edge = self.g.add_edge(1, 2)
|
||||
self.assertIsInstance(e, Edge)
|
||||
|
||||
def test_add_parallel_edges(self) -> None:
|
||||
"""Test adding parallel edges between the same pair."""
|
||||
e1: Edge = self.g.add_edge(1, 2)
|
||||
e2: Edge = self.g.add_edge(1, 2)
|
||||
self.assertNotEqual(e1.id, e2.id)
|
||||
self.assertEqual(len(self.g.edges_between(1, 2)), 2)
|
||||
|
||||
def test_remove_edge(self) -> None:
|
||||
"""Test removing a specific edge by ID."""
|
||||
e: Edge = self.g.add_edge(1, 2)
|
||||
self.g.remove_edge(e.id)
|
||||
self.assertEqual(
|
||||
len(self.g.edges_between(1, 2)), 0)
|
||||
|
||||
def test_remove_one_parallel_edge(self) -> None:
|
||||
"""Test removing one of several parallel edges."""
|
||||
e1: Edge = self.g.add_edge(1, 2)
|
||||
e2: Edge = self.g.add_edge(1, 2)
|
||||
self.g.remove_edge(e1.id)
|
||||
remaining: list[Edge] = (
|
||||
self.g.edges_between(1, 2))
|
||||
self.assertEqual(len(remaining), 1)
|
||||
self.assertEqual(remaining[0].id, e2.id)
|
||||
|
||||
def test_edge_count(self) -> None:
|
||||
"""Test total edge count."""
|
||||
self.g.add_edge(1, 2)
|
||||
self.g.add_edge(2, 3)
|
||||
self.g.add_edge(1, 2)
|
||||
self.assertEqual(self.g.num_edges(), 3)
|
||||
|
||||
def test_add_virtual_edge(self) -> None:
|
||||
"""Test adding a virtual edge."""
|
||||
e: Edge = self.g.add_edge(1, 2, virtual=True)
|
||||
self.assertTrue(e.virtual)
|
||||
|
||||
def test_edges_property(self) -> None:
|
||||
"""Test that edges property returns all edges."""
|
||||
e1: Edge = self.g.add_edge(1, 2)
|
||||
e2: Edge = self.g.add_edge(2, 3)
|
||||
ids: set[int] = {e.id for e in self.g.edges}
|
||||
self.assertIn(e1.id, ids)
|
||||
self.assertIn(e2.id, ids)
|
||||
|
||||
|
||||
class TestMultiGraphNeighbors(unittest.TestCase):
|
||||
"""Tests for neighbor/adjacency operations on MultiGraph."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Set up a triangle graph (K3)."""
|
||||
self.g: MultiGraph = MultiGraph()
|
||||
"""The graph under test."""
|
||||
for v in [1, 2, 3]:
|
||||
self.g.add_vertex(v)
|
||||
self.g.add_edge(1, 2)
|
||||
self.g.add_edge(2, 3)
|
||||
self.g.add_edge(1, 3)
|
||||
|
||||
def test_neighbors(self) -> None:
|
||||
"""Test neighbors returns all adjacent vertices."""
|
||||
nbrs: list[Hashable] = self.g.neighbors(1)
|
||||
self.assertEqual(set(nbrs), {2, 3})
|
||||
|
||||
def test_neighbors_with_parallel_edges(self) -> None:
|
||||
"""Test neighbors returns unique vertices with parallel edges."""
|
||||
self.g.add_edge(1, 2)
|
||||
nbrs: list[Hashable] = self.g.neighbors(1)
|
||||
self.assertEqual(set(nbrs), {2, 3})
|
||||
|
||||
def test_incident_edges(self) -> None:
|
||||
"""Test incident_edges returns edges incident to a vertex."""
|
||||
edges: list[Edge] = self.g.incident_edges(1)
|
||||
self.assertEqual(len(edges), 2)
|
||||
|
||||
def test_edges_between(self) -> None:
|
||||
"""Test edges_between returns edges between two vertices."""
|
||||
edges: list[Edge] = self.g.edges_between(1, 2)
|
||||
self.assertEqual(len(edges), 1)
|
||||
self.assertEqual(edges[0].u, 1)
|
||||
self.assertEqual(edges[0].v, 2)
|
||||
|
||||
def test_degree(self) -> None:
|
||||
"""Test degree counts incident edges (with multiplicity)."""
|
||||
self.assertEqual(self.g.degree(1), 2)
|
||||
self.g.add_edge(1, 2)
|
||||
self.assertEqual(self.g.degree(1), 3)
|
||||
|
||||
|
||||
class TestMultiGraphCopy(unittest.TestCase):
|
||||
"""Tests for copying operations on MultiGraph."""
|
||||
|
||||
def test_copy_is_independent(self) -> None:
|
||||
"""Test that a copy is independent from the original."""
|
||||
g: MultiGraph = MultiGraph()
|
||||
g.add_vertex(1)
|
||||
g.add_vertex(2)
|
||||
g.add_edge(1, 2)
|
||||
g2: MultiGraph = g.copy()
|
||||
g2.add_vertex(3)
|
||||
self.assertNotIn(3, g.vertices)
|
||||
|
||||
def test_copy_has_same_structure(self) -> None:
|
||||
"""Test that a copy has the same vertices and edges."""
|
||||
g: MultiGraph = MultiGraph()
|
||||
for v in [1, 2, 3]:
|
||||
g.add_vertex(v)
|
||||
g.add_edge(1, 2)
|
||||
g.add_edge(2, 3)
|
||||
g2: MultiGraph = g.copy()
|
||||
self.assertEqual(set(g2.vertices), {1, 2, 3})
|
||||
self.assertEqual(g2.num_edges(), 2)
|
||||
471
tests/test_palm_tree.py
Normal file
471
tests/test_palm_tree.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# Pure Python SPQR-Tree implementation.
|
||||
# Authors:
|
||||
# imacat@mail.imacat.idv.tw (imacat), 2026/3/1
|
||||
# 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 palm tree construction (_palm_tree.py).
|
||||
|
||||
All DFS results are based on insertion-order adjacency traversal and
|
||||
DFS from vertex 1. Edge insertion order is specified in each test.
|
||||
"""
|
||||
import unittest
|
||||
from collections.abc import Hashable
|
||||
|
||||
from spqrtree._graph import Edge, MultiGraph
|
||||
from spqrtree._palm_tree import PalmTree, build_palm_tree, phi_key
|
||||
|
||||
|
||||
def _make_k3() -> tuple[MultiGraph, list[int]]:
|
||||
"""Build the triangle graph K3 (vertices 1,2,3; edges 1-2, 2-3, 1-3).
|
||||
|
||||
:return: A tuple (graph, edge_ids) where edge_ids is
|
||||
[e0.id, e1.id, e2.id].
|
||||
"""
|
||||
g: MultiGraph = MultiGraph()
|
||||
for v in [1, 2, 3]:
|
||||
g.add_vertex(v)
|
||||
e0: Edge = g.add_edge(1, 2)
|
||||
e1: Edge = g.add_edge(2, 3)
|
||||
e2: Edge = g.add_edge(1, 3)
|
||||
return g, [e0.id, e1.id, e2.id]
|
||||
|
||||
|
||||
def _make_p3() -> tuple[MultiGraph, list[int]]:
|
||||
"""Build the path graph P3 (vertices 1,2,3; edges 1-2, 2-3).
|
||||
|
||||
:return: A tuple (graph, edge_ids) where edge_ids is [e0.id, e1.id].
|
||||
"""
|
||||
g: MultiGraph = MultiGraph()
|
||||
for v in [1, 2, 3]:
|
||||
g.add_vertex(v)
|
||||
e0: Edge = g.add_edge(1, 2)
|
||||
e1: Edge = g.add_edge(2, 3)
|
||||
return g, [e0.id, e1.id]
|
||||
|
||||
|
||||
def _make_c4() -> tuple[MultiGraph, list[int]]:
|
||||
"""Build the 4-cycle C4 (vertices 1,2,3,4; edges 1-2,2-3,3-4,4-1).
|
||||
|
||||
:return: A tuple (graph, edge_ids) in insertion order.
|
||||
"""
|
||||
g: MultiGraph = MultiGraph()
|
||||
for v in [1, 2, 3, 4]:
|
||||
g.add_vertex(v)
|
||||
e0: Edge = g.add_edge(1, 2)
|
||||
e1: Edge = g.add_edge(2, 3)
|
||||
e2: Edge = g.add_edge(3, 4)
|
||||
e3: Edge = g.add_edge(4, 1)
|
||||
return g, [e0.id, e1.id, e2.id, e3.id]
|
||||
|
||||
|
||||
class TestPalmTreeType(unittest.TestCase):
|
||||
"""Tests that build_palm_tree returns a PalmTree instance."""
|
||||
|
||||
def test_returns_palm_tree(self) -> None:
|
||||
"""Test that build_palm_tree returns a PalmTree object."""
|
||||
g: MultiGraph
|
||||
g, _ = _make_k3()
|
||||
pt: PalmTree = build_palm_tree(g, 1)
|
||||
self.assertIsInstance(pt, PalmTree)
|
||||
|
||||
|
||||
class TestPalmTreePath(unittest.TestCase):
|
||||
"""Tests for palm tree on a path graph P3 (no back edges)."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Build palm tree for P3 starting at vertex 1."""
|
||||
g: MultiGraph
|
||||
g, eids = _make_p3()
|
||||
self.eids: list[int] = eids
|
||||
"""The edge IDs of the P3 graph."""
|
||||
self.pt: PalmTree = build_palm_tree(g, 1)
|
||||
"""The palm tree for the graph."""
|
||||
|
||||
def test_dfs_num_root(self) -> None:
|
||||
"""Test that the start vertex has DFS number 1."""
|
||||
self.assertEqual(self.pt.dfs_num[1], 1)
|
||||
|
||||
def test_dfs_num_order(self) -> None:
|
||||
"""Test that DFS numbers are assigned 1, 2, 3 in traversal order."""
|
||||
nums: list[int] = sorted(self.pt.dfs_num.values())
|
||||
self.assertEqual(nums, [1, 2, 3])
|
||||
|
||||
def test_tree_edges_count(self) -> None:
|
||||
"""Test that there are n-1 = 2 tree edges."""
|
||||
self.assertEqual(len(self.pt.tree_edges), 2)
|
||||
|
||||
def test_no_fronds(self) -> None:
|
||||
"""Test that there are no fronds on a tree path."""
|
||||
self.assertEqual(len(self.pt.fronds), 0)
|
||||
|
||||
def test_all_edges_are_tree_edges(self) -> None:
|
||||
"""Test that both edges are classified as tree edges."""
|
||||
e0, e1 = self.eids
|
||||
self.assertIn(e0, self.pt.tree_edges)
|
||||
self.assertIn(e1, self.pt.tree_edges)
|
||||
|
||||
def test_parent_root(self) -> None:
|
||||
"""Test that the root has no parent (None)."""
|
||||
self.assertIsNone(self.pt.parent.get(1))
|
||||
|
||||
def test_nd_values(self) -> None:
|
||||
"""Test that ND values are correct for P3."""
|
||||
# DFS from 1: 1→2→3 (since 2 is first in adj[1])
|
||||
self.assertEqual(self.pt.nd[1], 3)
|
||||
|
||||
def test_nd_leaf(self) -> None:
|
||||
"""Test that a leaf vertex has ND = 1."""
|
||||
# Vertex 3 is a leaf in P3
|
||||
self.assertEqual(self.pt.nd[3], 1)
|
||||
|
||||
def test_lowpt1_values(self) -> None:
|
||||
"""Test lowpt1 values for P3 (all vertices reach only themselves)."""
|
||||
for v in [1, 2, 3]:
|
||||
self.assertLessEqual(
|
||||
self.pt.lowpt1[v], self.pt.dfs_num[v]
|
||||
)
|
||||
|
||||
def test_lowpt1_no_fronds(self) -> None:
|
||||
"""Test that lowpt1[v] == dfs_num[v] when no fronds exist."""
|
||||
for v in [1, 2, 3]:
|
||||
self.assertEqual(self.pt.lowpt1[v], self.pt.dfs_num[v])
|
||||
|
||||
|
||||
class TestPalmTreeTriangle(unittest.TestCase):
|
||||
"""Tests for palm tree on the triangle graph K3."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Build palm tree for K3 starting at vertex 1."""
|
||||
g: MultiGraph
|
||||
g, eids = _make_k3()
|
||||
self.eids: list[int] = eids
|
||||
"""The edge IDs of the K3 graph."""
|
||||
self.pt: PalmTree = build_palm_tree(g, 1)
|
||||
"""The palm tree for the graph."""
|
||||
|
||||
def test_dfs_num_root(self) -> None:
|
||||
"""Test that vertex 1 has DFS number 1."""
|
||||
self.assertEqual(self.pt.dfs_num[1], 1)
|
||||
|
||||
def test_dfs_num_all_assigned(self) -> None:
|
||||
"""Test that all vertices get a DFS number."""
|
||||
self.assertEqual(set(self.pt.dfs_num.keys()), {1, 2, 3})
|
||||
|
||||
def test_tree_edge_count(self) -> None:
|
||||
"""Test that there are n-1 = 2 tree edges."""
|
||||
self.assertEqual(len(self.pt.tree_edges), 2)
|
||||
|
||||
def test_frond_count(self) -> None:
|
||||
"""Test that there is exactly 1 frond."""
|
||||
self.assertEqual(len(self.pt.fronds), 1)
|
||||
|
||||
def test_frond_is_back_edge(self) -> None:
|
||||
"""Test that the frond e2 (1-3) is classified as a frond."""
|
||||
# e2 = edges[2] = the third edge added (1-3)
|
||||
e0, e1, e2 = self.eids
|
||||
self.assertIn(e2, self.pt.fronds)
|
||||
self.assertIn(e0, self.pt.tree_edges)
|
||||
self.assertIn(e1, self.pt.tree_edges)
|
||||
|
||||
def test_lowpt1_all_reach_root(self) -> None:
|
||||
"""Test that all vertices have lowpt1 = 1 via frond."""
|
||||
for v in [1, 2, 3]:
|
||||
self.assertEqual(self.pt.lowpt1[v], 1)
|
||||
|
||||
def test_nd_root(self) -> None:
|
||||
"""Test that the root has nd = 3."""
|
||||
self.assertEqual(self.pt.nd[1], 3)
|
||||
|
||||
def test_nd_leaf(self) -> None:
|
||||
"""Test that the DFS leaf (vertex 3) has nd = 1."""
|
||||
# Vertex 3 is visited last in K3 with DFS from 1
|
||||
leaf: Hashable = next(
|
||||
v for v, n in self.pt.nd.items() if n == 1
|
||||
)
|
||||
self.assertEqual(self.pt.nd[leaf], 1)
|
||||
|
||||
def test_first_child_of_root(self) -> None:
|
||||
"""Test that root vertex 1 has a first child."""
|
||||
self.assertIsNotNone(self.pt.first_child.get(1))
|
||||
|
||||
def test_first_child_of_leaf(self) -> None:
|
||||
"""Test that the DFS leaf has no first child."""
|
||||
leaf: Hashable = next(
|
||||
v for v, n in self.pt.nd.items() if n == 1
|
||||
)
|
||||
self.assertIsNone(self.pt.first_child.get(leaf))
|
||||
|
||||
def test_lowpt1_le_dfs_num(self) -> None:
|
||||
"""Test that lowpt1[v] <= dfs_num[v] for all v."""
|
||||
for v in [1, 2, 3]:
|
||||
self.assertLessEqual(self.pt.lowpt1[v], self.pt.dfs_num[v])
|
||||
|
||||
def test_lowpt1_le_lowpt2(self) -> None:
|
||||
"""Test that lowpt1[v] <= lowpt2[v] for all v."""
|
||||
for v in [1, 2, 3]:
|
||||
self.assertLessEqual(self.pt.lowpt1[v], self.pt.lowpt2[v])
|
||||
|
||||
|
||||
class TestPalmTreeC4(unittest.TestCase):
|
||||
"""Tests for palm tree on the 4-cycle C4."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Build palm tree for C4 starting at vertex 1."""
|
||||
g: MultiGraph
|
||||
g, eids = _make_c4()
|
||||
self.eids: list[int] = eids
|
||||
"""The edge IDs of the C4 graph."""
|
||||
self.pt: PalmTree = build_palm_tree(g, 1)
|
||||
"""The palm tree for the graph."""
|
||||
|
||||
def test_dfs_num_root(self) -> None:
|
||||
"""Test that vertex 1 has DFS number 1."""
|
||||
self.assertEqual(self.pt.dfs_num[1], 1)
|
||||
|
||||
def test_dfs_num_all_assigned(self) -> None:
|
||||
"""Test that all 4 vertices get DFS numbers."""
|
||||
self.assertEqual(set(self.pt.dfs_num.keys()), {1, 2, 3, 4})
|
||||
|
||||
def test_tree_edge_count(self) -> None:
|
||||
"""Test that there are n-1 = 3 tree edges."""
|
||||
self.assertEqual(len(self.pt.tree_edges), 3)
|
||||
|
||||
def test_frond_count(self) -> None:
|
||||
"""Test that there is exactly 1 frond."""
|
||||
self.assertEqual(len(self.pt.fronds), 1)
|
||||
|
||||
def test_frond_classification(self) -> None:
|
||||
"""Test that e3 (4-1) is a frond and e0,e1,e2 are tree edges."""
|
||||
e0, e1, e2, e3 = self.eids
|
||||
self.assertIn(e0, self.pt.tree_edges)
|
||||
self.assertIn(e1, self.pt.tree_edges)
|
||||
self.assertIn(e2, self.pt.tree_edges)
|
||||
self.assertIn(e3, self.pt.fronds)
|
||||
|
||||
def test_nd_root(self) -> None:
|
||||
"""Test that root vertex 1 has nd = 4."""
|
||||
self.assertEqual(self.pt.nd[1], 4)
|
||||
|
||||
def test_nd_leaf(self) -> None:
|
||||
"""Test that the DFS leaf has nd = 1."""
|
||||
leaf: Hashable = next(
|
||||
v for v, n in self.pt.nd.items() if n == 1
|
||||
)
|
||||
self.assertEqual(self.pt.nd[leaf], 1)
|
||||
|
||||
def test_lowpt1_all_reach_root(self) -> None:
|
||||
"""Test that all vertices have lowpt1 = 1 due to the frond."""
|
||||
for v in [1, 2, 3, 4]:
|
||||
self.assertEqual(self.pt.lowpt1[v], 1)
|
||||
|
||||
def test_lowpt2_intermediate(self) -> None:
|
||||
"""Test that lowpt2 values are consistent (lowpt1 <= lowpt2)."""
|
||||
for v in [1, 2, 3, 4]:
|
||||
self.assertLessEqual(self.pt.lowpt1[v], self.pt.lowpt2[v])
|
||||
|
||||
def test_parent_structure(self) -> None:
|
||||
"""Test that the parent structure is a valid tree rooted at 1."""
|
||||
# Root has no parent
|
||||
self.assertIsNone(self.pt.parent.get(1))
|
||||
# All other vertices have a parent
|
||||
for v in [2, 3, 4]:
|
||||
self.assertIsNotNone(self.pt.parent.get(v))
|
||||
|
||||
|
||||
class TestPalmTreeLowptSpecific(unittest.TestCase):
|
||||
"""Tests for specific lowpt1/lowpt2 values on P3."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Build palm tree for P3 (path 1-2-3) starting at 1."""
|
||||
g: MultiGraph
|
||||
g, _ = _make_p3()
|
||||
self.pt: PalmTree = build_palm_tree(g, 1)
|
||||
"""The palm tree for the graph."""
|
||||
|
||||
def test_exact_dfs_nums(self) -> None:
|
||||
"""Test exact DFS numbers for the path graph."""
|
||||
self.assertEqual(self.pt.dfs_num[1], 1)
|
||||
self.assertEqual(self.pt.dfs_num[2], 2)
|
||||
self.assertEqual(self.pt.dfs_num[3], 3)
|
||||
|
||||
def test_exact_lowpt1(self) -> None:
|
||||
"""Test exact lowpt1 values for P3 (no back edges)."""
|
||||
# Without fronds, lowpt1[v] = dfs_num[v]
|
||||
self.assertEqual(self.pt.lowpt1[3], 3)
|
||||
self.assertEqual(self.pt.lowpt1[2], 2)
|
||||
self.assertEqual(self.pt.lowpt1[1], 1)
|
||||
|
||||
def test_exact_nd(self) -> None:
|
||||
"""Test exact nd values for P3."""
|
||||
self.assertEqual(self.pt.nd[3], 1)
|
||||
self.assertEqual(self.pt.nd[2], 2)
|
||||
self.assertEqual(self.pt.nd[1], 3)
|
||||
|
||||
def test_exact_parent(self) -> None:
|
||||
"""Test exact parent mapping for P3."""
|
||||
self.assertIsNone(self.pt.parent.get(1))
|
||||
self.assertEqual(self.pt.parent[2], 1)
|
||||
self.assertEqual(self.pt.parent[3], 2)
|
||||
|
||||
def test_exact_first_child(self) -> None:
|
||||
"""Test exact first_child mapping for P3."""
|
||||
self.assertEqual(self.pt.first_child[1], 2)
|
||||
self.assertEqual(self.pt.first_child[2], 3)
|
||||
self.assertIsNone(self.pt.first_child.get(3))
|
||||
|
||||
|
||||
class TestPhiKeyP3(unittest.TestCase):
|
||||
"""Tests for phi_key correctness on the path graph P3.
|
||||
|
||||
P3: vertices 1,2,3; edges 1-2 (tree), 2-3 (tree).
|
||||
DFS from 1: dfs_num = {1:1, 2:2, 3:3}.
|
||||
No fronds; lowpt1[v] = dfs_num[v] for all v.
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Build palm tree for P3 and gather data for phi_key tests."""
|
||||
g: MultiGraph
|
||||
g, eids = _make_p3()
|
||||
self.g: MultiGraph = g
|
||||
"""The P3 graph under test."""
|
||||
self.eids: list[int] = eids
|
||||
"""The edge IDs of the P3 graph."""
|
||||
self.pt: PalmTree = build_palm_tree(g, 1)
|
||||
"""The palm tree for the graph."""
|
||||
|
||||
def test_tree_edge_1_2_case3_formula(self) -> None:
|
||||
"""Test phi_key for tree edge 1-2 uses case 3 formula.
|
||||
|
||||
For tree edge v=1, w=2:
|
||||
lowpt1[2]=2, lowpt2[2]=INF, dfs_num[1]=1.
|
||||
lowpt2[2] >= dfs_num[1] -> case 3 -> phi = 3*lowpt1[2]+2 = 8.
|
||||
"""
|
||||
e0: int = self.eids[0]
|
||||
key: int = phi_key(
|
||||
v=1, eid=e0, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# lowpt1[2]=2, case 3: 3*2+2 = 8
|
||||
self.assertEqual(key, 3 * 2 + 2)
|
||||
|
||||
def test_tree_edge_2_3_case3_formula(self) -> None:
|
||||
"""Test phi_key for tree edge 2-3 uses case 3 formula.
|
||||
|
||||
For tree edge v=2, w=3:
|
||||
lowpt1[3]=3, lowpt2[3]=INF, dfs_num[2]=2.
|
||||
lowpt2[3] >= dfs_num[2] -> case 3 -> phi = 3*lowpt1[3]+2 = 11.
|
||||
"""
|
||||
e1: int = self.eids[1]
|
||||
key: int = phi_key(
|
||||
v=2, eid=e1, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# lowpt1[3]=3, case 3: 3*3+2 = 11
|
||||
self.assertEqual(key, 3 * 3 + 2)
|
||||
|
||||
def test_tree_edge_case3_greater_than_case1(self) -> None:
|
||||
"""Test that case-3 phi > case-1 phi for ordering.
|
||||
|
||||
Case 1: phi = 3*lowpt1[w]
|
||||
Case 3: phi = 3*lowpt1[w]+2
|
||||
Case 3 must be strictly greater than case 1 for same lowpt1.
|
||||
"""
|
||||
e0: int = self.eids[0]
|
||||
key_v1: int = phi_key(
|
||||
v=1, eid=e0, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# Case 3 value (8) > case 1 value (6) for lowpt1[2]=2
|
||||
self.assertGreater(key_v1, 3 * 2)
|
||||
|
||||
|
||||
class TestPhiKeyK3Frond(unittest.TestCase):
|
||||
"""Tests for phi_key correctness on the triangle K3.
|
||||
|
||||
K3: vertices 1,2,3; edges 1-2 (tree), 2-3 (tree), 1-3 (frond).
|
||||
DFS from 1: dfs_num = {1:1, 2:2, 3:3}.
|
||||
Frond: 1-3 (from vertex 3 to ancestor 1).
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Build palm tree for K3 and gather data for phi_key tests."""
|
||||
g: MultiGraph
|
||||
g, eids = _make_k3()
|
||||
self.g: MultiGraph = g
|
||||
"""The K3 graph under test."""
|
||||
self.eids: list[int] = eids
|
||||
"""The edge IDs of the K3 graph."""
|
||||
self.pt: PalmTree = build_palm_tree(g, 1)
|
||||
"""The palm tree for the graph."""
|
||||
|
||||
def test_frond_phi_uses_w_dfs_num(self) -> None:
|
||||
"""Test phi_key for frond e2 (1-3) from v=3 uses dfs_num[w=1].
|
||||
|
||||
Frond from v=3 to ancestor w=1: dfs_num[w]=1.
|
||||
phi = 3 * dfs_num[w] + 1 = 3 * 1 + 1 = 4.
|
||||
"""
|
||||
e2: int = self.eids[2] # edge 1-3 (frond)
|
||||
# The frond is traversed from vertex 3 toward ancestor 1.
|
||||
# We need to identify which end is the frond source.
|
||||
# In the palm tree, frond is from v=3 (dfs_num=3) to w=1
|
||||
# (dfs_num=1).
|
||||
# phi_key called with v=3, w=1 (ancestor), eid=e2.
|
||||
key: int = phi_key(
|
||||
v=3, eid=e2, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# Correct formula: 3 * dfs_num[w=1] + 1 = 3*1+1 = 4
|
||||
self.assertEqual(key, 3 * 1 + 1)
|
||||
|
||||
def test_frond_phi_different_from_v_formula(self) -> None:
|
||||
"""Test that frond phi uses w (not v) DFS number.
|
||||
|
||||
The buggy formula uses dfs_num[v] (= 3) giving 3*3+2 = 11.
|
||||
The correct formula uses dfs_num[w] (= 1) giving 3*1+1 = 4.
|
||||
These must differ.
|
||||
"""
|
||||
e2: int = self.eids[2]
|
||||
key: int = phi_key(
|
||||
v=3, eid=e2, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# The buggy value would be 3*dfs_num[v=3]+2 = 3*3+2 = 11.
|
||||
# The correct value is 3*dfs_num[w=1]+1 = 4.
|
||||
self.assertNotEqual(key, 11)
|
||||
self.assertEqual(key, 4)
|
||||
|
||||
def test_frond_phi_less_than_tree_edge_case3(self) -> None:
|
||||
"""Test ordering: frond phi < tree-edge case-3 phi.
|
||||
|
||||
The frond phi (4) should be less than a case-3 tree-edge phi
|
||||
with the same lowpt1 (3*1+2=5), ensuring correct DFS order.
|
||||
"""
|
||||
e2: int = self.eids[2]
|
||||
frond_key: int = phi_key(
|
||||
v=3, eid=e2, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# Frond phi = 3*1+1=4; case-3 tree-edge phi at lowpt1=1 = 3*1+2=5
|
||||
self.assertLess(frond_key, 3 * 1 + 2)
|
||||
|
||||
def test_tree_edge_case1_condition(self) -> None:
|
||||
"""Test phi_key for tree edge where lowpt2[w] < dfs_num[v].
|
||||
|
||||
For K3, after sorting: tree edge v=1, w=2:
|
||||
lowpt1[2]=1, lowpt2[2]=INF, dfs_num[1]=1.
|
||||
lowpt2[2]=INF >= dfs_num[1]=1 -> case 3 -> phi = 3*1+2 = 5.
|
||||
"""
|
||||
e0: int = self.eids[0] # edge 1-2 (tree edge)
|
||||
key: int = phi_key(
|
||||
v=1, eid=e0, pt=self.pt, graph=self.g,
|
||||
)
|
||||
# lowpt1[2]=1, lowpt2[2]=INF >= dfs_num[1]=1 -> case 3:
|
||||
# phi = 3*1+2 = 5
|
||||
self.assertEqual(key, 3 * 1 + 2)
|
||||
2853
tests/test_spqrtree.py
Normal file
2853
tests/test_spqrtree.py
Normal file
File diff suppressed because it is too large
Load Diff
2787
tests/test_triconnected.py
Normal file
2787
tests/test_triconnected.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user