3 Commits

Author SHA1 Message Date
imacat ee28a225d1 Advance to version 0.1.0 2026-03-05 09:02:10 +08:00
imacat c3763f1768 Initial commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:02:05 +08:00
imacat 14ad81ad00 Add .gitignore 2026-03-01 23:11:22 +08:00
11 changed files with 18 additions and 202 deletions
+3 -4
View File
@@ -1,7 +1,6 @@
# The SPQR-Tree algorithm implementation.
# Copyright 2026 imacat. All rights reserved.
# Authors:
# imacat@mail.imacat.idv.tw (imacat), 2026/3/1
# The SQPR-Tree algorithm implementation.
# Copyright 2026 DSP, inc. All rights reserved.
# Author: imacat@mail.imacat.idv.tw (imacat), 2026/3/1
*.pyc
__pycache__
+1 -1
View File
@@ -17,6 +17,6 @@
# limitations under the License.
recursive-include docs *
prune docs/build
recursive-exclude docs/build *
recursive-include tests *
recursive-exclude tests *.pyc
-12
View File
@@ -2,18 +2,6 @@ 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
-------------
+3 -2
View File
@@ -133,11 +133,12 @@ Using a MultiGraph
~~~~~~~~~~~~~~~~~~
For more control, build a
:class:`~spqrtree.MultiGraph` directly:
:class:`~spqrtree._graph.MultiGraph` directly:
.. code-block:: python
from spqrtree import MultiGraph, SPQRTree
from spqrtree._graph import MultiGraph
from spqrtree import SPQRTree
g = MultiGraph()
g.add_edge(0, 1)
+1 -8
View File
@@ -3,7 +3,7 @@
# imacat@mail.imacat.idv.tw (imacat), 2026/3/2
# AI assistance: Claude Code (Anthropic)
# Copyright (c) 2026 imacat.
# Copyright (c) 2022-2026 imacat.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -53,13 +53,6 @@ classifiers = [
"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]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
+4 -4
View File
@@ -25,9 +25,9 @@ Di Battista & Tamassia (1996).
Public API::
from spqrtree import Edge, MultiGraph
from spqrtree import NodeType, SPQRNode, build_spqr_tree
from spqrtree import (
from spqrtree._graph import Edge, MultiGraph
from spqrtree._spqr import NodeType, SPQRNode, build_spqr_tree
from spqrtree._triconnected import (
ComponentType,
TriconnectedComponent,
find_triconnected_components,
@@ -43,7 +43,7 @@ from spqrtree._triconnected import (
find_triconnected_components,
)
VERSION: str = "0.1.1"
VERSION: str = "0.1.0"
"""The package version."""
__all__: list[str] = [
"SPQRTree", "SPQRNode", "NodeType",
-39
View File
@@ -92,41 +92,6 @@ class TriconnectedComponent:
"""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(
graph: MultiGraph,
) -> list[TriconnectedComponent]:
@@ -143,14 +108,10 @@ def find_triconnected_components(
:param graph: A biconnected multigraph.
: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:
return []
_check_biconnected(graph)
# Work on a copy to avoid modifying the caller's graph.
g: MultiGraph = graph.copy()
+1 -1
View File
@@ -20,7 +20,7 @@
import unittest
from collections.abc import Hashable
from spqrtree import Edge, MultiGraph
from spqrtree._graph import Edge, MultiGraph
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
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
+2 -42
View File
@@ -26,13 +26,8 @@ import unittest
from collections import deque
from collections.abc import Hashable
from spqrtree import (
Edge,
MultiGraph,
NodeType,
SPQRNode,
build_spqr_tree,
)
from spqrtree._graph import Edge, MultiGraph
from spqrtree._spqr import NodeType, SPQRNode, build_spqr_tree
def _make_k3() -> MultiGraph:
@@ -2856,38 +2851,3 @@ class TestSPQRRpstFig1a(unittest.TestCase):
def test_no_adjacent_p_nodes(self) -> None:
"""Test that no P-node is adjacent to another P-node."""
_assert_no_ss_pp(self, self.root, NodeType.P)
class TestBuildSpqrTreeBiconnectivity(unittest.TestCase):
"""Tests that build_spqr_tree rejects non-biconnected graphs."""
def test_cut_vertex_raises(self) -> None:
"""Test that a graph with a cut vertex raises ValueError."""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(2, 3)
g.add_edge(1, 3)
g.add_edge(3, 4)
g.add_edge(4, 5)
g.add_edge(3, 5)
with self.assertRaises(ValueError) as ctx:
build_spqr_tree(g)
self.assertIn("cut vertex", str(ctx.exception))
def test_disconnected_raises(self) -> None:
"""Test that a disconnected graph raises ValueError."""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(3, 4)
with self.assertRaises(ValueError) as ctx:
build_spqr_tree(g)
self.assertIn("not connected", str(ctx.exception))
def test_path_raises(self) -> None:
"""Test that a path graph raises ValueError."""
g: MultiGraph = MultiGraph()
g.add_edge(1, 2)
g.add_edge(2, 3)
with self.assertRaises(ValueError) as ctx:
build_spqr_tree(g)
self.assertIn("cut vertex", str(ctx.exception))
+2 -88
View File
@@ -25,10 +25,9 @@ edge appearance count.
import unittest
from collections.abc import Hashable
from spqrtree import (
from spqrtree._graph import Edge, MultiGraph
from spqrtree._triconnected import (
ComponentType,
Edge,
MultiGraph,
TriconnectedComponent,
find_triconnected_components,
)
@@ -2786,88 +2785,3 @@ class TestTriconnectedRpstFig1a(unittest.TestCase):
def test_all_invariants(self) -> None:
"""Test all decomposition invariants."""
_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)