2 Commits

Author SHA1 Message Date
imacat dc307acac7 Initial commit.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:52:03 +08:00
imacat 14ad81ad00 Add .gitignore 2026-03-01 23:11:22 +08:00
11 changed files with 18 additions and 209 deletions
+3 -4
View File
@@ -1,7 +1,6 @@
# The SPQR-Tree algorithm implementation. # The SQPR-Tree algorithm implementation.
# Copyright 2026 imacat. All rights reserved. # Copyright 2026 DSP, inc. All rights reserved.
# Authors: # Author: imacat@mail.imacat.idv.tw (imacat), 2026/3/1
# imacat@mail.imacat.idv.tw (imacat), 2026/3/1
*.pyc *.pyc
__pycache__ __pycache__
+1 -1
View File
@@ -17,6 +17,6 @@
# limitations under the License. # limitations under the License.
recursive-include docs * recursive-include docs *
prune docs/build recursive-exclude docs/build *
recursive-include tests * recursive-include tests *
recursive-exclude tests *.pyc recursive-exclude tests *.pyc
-19
View File
@@ -1,22 +1,3 @@
Change Log Change Log
========== ==========
Version 0.1.1
-------------
Released 2026/3/11
- ``find_triconnected_components()`` now raises ``ValueError`` when
the input graph is not biconnected (disconnected or has a cut
vertex). Previously, non-biconnected input was silently accepted
and produced incorrect results.
- Use public API imports in documentation examples.
Version 0.1.0
-------------
Released 2026/3/5
Initial release.
+3 -2
View File
@@ -133,11 +133,12 @@ Using a MultiGraph
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
For more control, build a For more control, build a
:class:`~spqrtree.MultiGraph` directly: :class:`~spqrtree._graph.MultiGraph` directly:
.. code-block:: python .. code-block:: python
from spqrtree import MultiGraph, SPQRTree from spqrtree._graph import MultiGraph
from spqrtree import SPQRTree
g = MultiGraph() g = MultiGraph()
g.add_edge(0, 1) g.add_edge(0, 1)
+1 -8
View File
@@ -3,7 +3,7 @@
# imacat@mail.imacat.idv.tw (imacat), 2026/3/2 # imacat@mail.imacat.idv.tw (imacat), 2026/3/2
# AI assistance: Claude Code (Anthropic) # AI assistance: Claude Code (Anthropic)
# Copyright (c) 2026 imacat. # Copyright (c) 2022-2026 imacat.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -53,13 +53,6 @@ classifiers = [
"Typing :: Typed", "Typing :: Typed",
] ]
[project.urls]
Homepage = "https://github.com/imacat/spqrtree"
Repository = "https://github.com/imacat/spqrtree"
Documentation = "https://spqrtree.readthedocs.io"
"Change Log" = "https://spqrtree.readthedocs.io/en/latest/changelog.html"
"Bug Tracker" = "https://github.com/imacat/spqrtree/issues"
[build-system] [build-system]
requires = ["setuptools>=61"] requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
+4 -4
View File
@@ -25,11 +25,11 @@ Di Battista & Tamassia (1996).
Public API:: Public API::
from spqrtree import SPQRTree, SPQRNode, NodeType
from spqrtree import Edge, MultiGraph from spqrtree import Edge, MultiGraph
from spqrtree import NodeType, SPQRNode, build_spqr_tree from spqrtree import build_spqr_tree
from spqrtree import ( from spqrtree import (
ComponentType, ComponentType, TriconnectedComponent,
TriconnectedComponent,
find_triconnected_components, find_triconnected_components,
) )
""" """
@@ -43,7 +43,7 @@ from spqrtree._triconnected import (
find_triconnected_components, find_triconnected_components,
) )
VERSION: str = "0.1.1" VERSION: str = "0.0.0"
"""The package version.""" """The package version."""
__all__: list[str] = [ __all__: list[str] = [
"SPQRTree", "SPQRNode", "NodeType", "SPQRTree", "SPQRNode", "NodeType",
-39
View File
@@ -92,41 +92,6 @@ class TriconnectedComponent:
"""All edges in this component (real and virtual).""" """All edges in this component (real and virtual)."""
def _check_biconnected(graph: MultiGraph) -> None:
"""Check that a graph is biconnected (connected with no cut vertex).
Uses a single DFS pass to verify connectivity and detect cut
vertices via Tarjan's algorithm. A non-root vertex v is a cut
vertex if it has a child w such that lowpt1[w] >= dfs_num[v].
The DFS root is a cut vertex if it has two or more children.
:param graph: The multigraph to check.
:raises ValueError: If the graph is not connected or has a cut
vertex.
"""
start: Hashable = next(iter(graph.vertices))
pt: PalmTree = build_palm_tree(graph, start)
# Check connectivity: DFS must visit all vertices.
if len(pt.dfs_num) < graph.num_vertices():
raise ValueError("graph is not connected")
# Check for cut vertices.
for v in graph.vertices:
if pt.parent[v] is None:
# Root: cut vertex if it has 2+ children.
if len(pt.children[v]) >= 2:
raise ValueError("graph has a cut vertex")
else:
# Non-root: cut vertex if any child w has
# lowpt1[w] >= dfs_num[v].
v_num: int = pt.dfs_num[v]
for w in pt.children[v]:
if pt.lowpt1[w] >= v_num:
raise ValueError(
"graph has a cut vertex")
def find_triconnected_components( def find_triconnected_components(
graph: MultiGraph, graph: MultiGraph,
) -> list[TriconnectedComponent]: ) -> list[TriconnectedComponent]:
@@ -143,14 +108,10 @@ def find_triconnected_components(
:param graph: A biconnected multigraph. :param graph: A biconnected multigraph.
:return: A list of TriconnectedComponent objects. :return: A list of TriconnectedComponent objects.
:raises ValueError: If the graph is not connected or has a
cut vertex.
""" """
if graph.num_vertices() == 0 or graph.num_edges() == 0: if graph.num_vertices() == 0 or graph.num_edges() == 0:
return [] return []
_check_biconnected(graph)
# Work on a copy to avoid modifying the caller's graph. # Work on a copy to avoid modifying the caller's graph.
g: MultiGraph = graph.copy() g: MultiGraph = graph.copy()
+1 -1
View File
@@ -20,7 +20,7 @@
import unittest import unittest
from collections.abc import Hashable from collections.abc import Hashable
from spqrtree import Edge, MultiGraph from spqrtree._graph import Edge, MultiGraph
class TestEdge(unittest.TestCase): class TestEdge(unittest.TestCase):
+1 -1
View File
@@ -24,7 +24,7 @@ DFS from vertex 1. Edge insertion order is specified in each test.
import unittest import unittest
from collections.abc import Hashable from collections.abc import Hashable
from spqrtree import Edge, MultiGraph from spqrtree._graph import Edge, MultiGraph
from spqrtree._palm_tree import PalmTree, build_palm_tree, phi_key from spqrtree._palm_tree import PalmTree, build_palm_tree, phi_key
+2 -42
View File
@@ -26,13 +26,8 @@ import unittest
from collections import deque from collections import deque
from collections.abc import Hashable from collections.abc import Hashable
from spqrtree import ( from spqrtree._graph import Edge, MultiGraph
Edge, from spqrtree._spqr import NodeType, SPQRNode, build_spqr_tree
MultiGraph,
NodeType,
SPQRNode,
build_spqr_tree,
)
def _make_k3() -> MultiGraph: def _make_k3() -> MultiGraph:
@@ -2856,38 +2851,3 @@ class TestSPQRRpstFig1a(unittest.TestCase):
def test_no_adjacent_p_nodes(self) -> None: def test_no_adjacent_p_nodes(self) -> None:
"""Test that no P-node is adjacent to another P-node.""" """Test that no P-node is adjacent to another P-node."""
_assert_no_ss_pp(self, self.root, NodeType.P) _assert_no_ss_pp(self, self.root, NodeType.P)
class TestBuildSpqrTreeBiconnectivity(unittest.TestCase):
"""Tests that build_spqr_tree rejects non-biconnected graphs."""
def test_cut_vertex_raises(self) -> None:
"""Test that a graph with a cut vertex raises ValueError."""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(2, 3)
g.add_edge(1, 3)
g.add_edge(3, 4)
g.add_edge(4, 5)
g.add_edge(3, 5)
with self.assertRaises(ValueError) as ctx:
build_spqr_tree(g)
self.assertIn("cut vertex", str(ctx.exception))
def test_disconnected_raises(self) -> None:
"""Test that a disconnected graph raises ValueError."""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(3, 4)
with self.assertRaises(ValueError) as ctx:
build_spqr_tree(g)
self.assertIn("not connected", str(ctx.exception))
def test_path_raises(self) -> None:
"""Test that a path graph raises ValueError."""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(2, 3)
with self.assertRaises(ValueError) as ctx:
build_spqr_tree(g)
self.assertIn("cut vertex", str(ctx.exception))
+2 -88
View File
@@ -25,10 +25,9 @@ edge appearance count.
import unittest import unittest
from collections.abc import Hashable from collections.abc import Hashable
from spqrtree import ( from spqrtree._graph import Edge, MultiGraph
from spqrtree._triconnected import (
ComponentType, ComponentType,
Edge,
MultiGraph,
TriconnectedComponent, TriconnectedComponent,
find_triconnected_components, find_triconnected_components,
) )
@@ -2786,88 +2785,3 @@ class TestTriconnectedRpstFig1a(unittest.TestCase):
def test_all_invariants(self) -> None: def test_all_invariants(self) -> None:
"""Test all decomposition invariants.""" """Test all decomposition invariants."""
_check_all_invariants(self, self.g, self.comps) _check_all_invariants(self, self.g, self.comps)
def _make_cut_vertex_graph() -> MultiGraph:
"""Build a graph with a cut vertex.
Graph: 1-2-3 triangle connected to 3-4-5 triangle via
shared vertex 3 (the cut vertex).
:return: A MultiGraph with a cut vertex at vertex 3.
"""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(2, 3)
g.add_edge(1, 3)
g.add_edge(3, 4)
g.add_edge(4, 5)
g.add_edge(3, 5)
return g
def _make_disconnected_graph() -> MultiGraph:
"""Build a disconnected graph with two components.
Component 1: edge 1-2.
Component 2: edge 3-4.
:return: A disconnected MultiGraph.
"""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(3, 4)
return g
def _make_path_graph() -> MultiGraph:
"""Build a path graph 1-2-3 (not biconnected).
Vertex 2 is a cut vertex (removing it disconnects 1 and 3).
:return: A MultiGraph representing a path.
"""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(2, 3)
return g
class TestBiconnectivityCheck(unittest.TestCase):
"""Tests that non-biconnected graphs raise ValueError."""
def test_cut_vertex_raises(self) -> None:
"""Test that a graph with a cut vertex raises ValueError."""
g: MultiGraph = _make_cut_vertex_graph()
with self.assertRaises(ValueError) as ctx:
find_triconnected_components(g)
self.assertIn("cut vertex", str(ctx.exception))
def test_disconnected_raises(self) -> None:
"""Test that a disconnected graph raises ValueError."""
g: MultiGraph = _make_disconnected_graph()
with self.assertRaises(ValueError) as ctx:
find_triconnected_components(g)
self.assertIn("not connected", str(ctx.exception))
def test_path_raises(self) -> None:
"""Test that a path graph (has cut vertex) raises ValueError."""
g: MultiGraph = _make_path_graph()
with self.assertRaises(ValueError) as ctx:
find_triconnected_components(g)
self.assertIn("cut vertex", str(ctx.exception))
def test_single_vertex_no_edges(self) -> None:
"""Test that a single vertex with no edges returns empty."""
g: MultiGraph = MultiGraph()
g.add_vertex(1)
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
self.assertEqual(comps, [])
def test_biconnected_graph_ok(self) -> None:
"""Test that a biconnected graph does not raise."""
g: MultiGraph = _make_k3()
comps: list[TriconnectedComponent] = \
find_triconnected_components(g)
self.assertEqual(len(comps), 1)