Skip to content

Implement Whitney 2-isomorphism for graphs and graphic matroids - #42630

Open
cxzhong wants to merge 3 commits into
sagemath:developfrom
cxzhong:codex/whitney-2isomorphism
Open

Implement Whitney 2-isomorphism for graphs and graphic matroids#42630
cxzhong wants to merge 3 commits into
sagemath:developfrom
cxzhong:codex/whitney-2isomorphism

Conversation

@cxzhong

@cxzhong cxzhong commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Closes #42595.

This also includes the groundset-size regression fix from #42574.

Summary

  • add Graph.is_2isomorphic using block decomposition, SPQR trees, and colored canonical forms;
  • make certificate=True return a versioned, replayable Whitney-operation witness rather than only an edge bijection;
  • add Graph.verify_2isomorphism_certificate to validate and replay vertex cleavings, Whitney twists, vertex identifications, and isolated-vertex adjustments;
  • use the graph implementation from GraphicMatroid._is_isomorphic, while preserving the matroid API's groundset-mapping certificate;
  • retain a private edge-mapping-only path for GraphicMatroid, so it does not build an operation sequence that it would immediately discard;
  • preserve loops, parallel elements, repeated edge triples, disconnected blocks, and isolated vertices;
  • make unhashable graph edge labels fall back through the existing Matroid(G) / GraphicMatroid(G) groundset rules;
  • move the backend cg() inline definitions from .pxd files into their owning .pyx modules, preventing unused local copies in unrelated generated extensions.

Motivation and root cause

GraphicMatroid._is_isomorphic previously had a graph fast path only when the other matroid was 3-connected. General graphic matroids fell back to regular-matroid isomorphism, and the fast path could also lose size information after simplifying loops and parallel edges. That is the regression addressed by #42574.

Whitney's 2-isomorphism theorem gives the right graph-level equivalence: two graphs represent isomorphic cycle matroids exactly when their edge occurrences can be related by vertex cleavings/identifications, Whitney twists, and a final graph isomorphism. The implementation uses Sage's existing block and SPQR decomposition machinery and does not enumerate subsets or possible twist sequences.

Certificate format

On success, G.is_2isomorphic(H, certificate=True) returns (True, witness), where witness contains:

  • version (currently 1);
  • edge_mapping, a bijection between positions in list(G.edge_iterator()) and list(H.edge_iterator());
  • operations, containing explicit vertex_cleaving, whitney_twist, vertex_identification, delete_isolated_vertex, and add_isolated_vertex steps;
  • vertex_mapping, the final normalized graph isomorphism.

The public verifier uses private edge-occurrence IDs, checks each operation's preconditions, requires the final vertex map to be a bijection, and compares every final edge incidence with the target. Malformed or tampered witnesses return False instead of leaking parsing exceptions.

Performance

Local timings below use Sage 10.10.beta8 / CPython 3.12. Each function is warmed once; the new paths use the median of 5 runs and the previous paths use the median of 3 runs, with gc.collect() before each sample. Absolute timings are machine-dependent; the relative comparisons use the same process and inputs.

For the SPQR rows, "Previous path" is the generic RegularMatroid fallback. For the wheel rows, it is the previous 3-connected GraphicMatroid fast path, including its connectivity check.

Case Edges Previous path New boolean New certificate Boolean speedup
SPQR chain (multi_k4(4)) 21 6.33 ms 4.02 ms 3.85 ms 1.6x
SPQR chain (multi_k4(8)) 41 38.43 ms 6.42 ms 6.74 ms 6.0x
SPQR chain (multi_k4(16)) 81 382.96 ms 11.46 ms 11.50 ms 33.4x
Wheel graph 62 90.71 ms 2.02 ms - 44.9x
Wheel graph 126 600.57 ms 3.29 ms - 182.6x
1,024 three-edge windmill blocks 3,072 - 281.76 ms 319.65 ms -

For the 21-edge SPQR case, the previous regular-matroid certificate path took 459.56 ms versus 3.85 ms for the new mapping-only GraphicMatroid certificate path (about 119x faster). On the 3,072-edge graph, constructing the full replayable Graph witness adds about 13% over the boolean result.

Complete benchmark source

Save this as benchmark_two_isomorphism.py and run ./sage -python benchmark_two_isomorphism.py.

import gc
import statistics
import time

from sage.all import Graph, graphs
from sage.matroids.graphic_matroid import GraphicMatroid


def median(function, repetitions):
    function()
    samples = []
    for _ in range(repetitions):
        gc.collect()
        start = time.perf_counter()
        result = function()
        samples.append(time.perf_counter() - start)
        assert result is True or result[0] is True
    return statistics.median(samples)


def multi_k4(k):
    G = Graph(2)
    for _ in range(k):
        a = G.add_vertex()
        b = G.add_vertex()
        G.add_clique([0, 1, a, b])
    return G


def reverse_labels(G):
    return G.relabel(dict(zip(list(G), reversed(list(G)))), inplace=False)


for k in (4, 8, 16):
    G = multi_k4(k)
    H = reverse_labels(G)
    M = GraphicMatroid(G, groundset=range(G.size()))
    N = GraphicMatroid(H, groundset=range(G.size(), 2 * G.size()))
    row = {
        'edges': G.size(),
        'new_bool': median(lambda: M.is_isomorphic(N), 5),
        'new_cert': median(lambda: M.is_isomorphic(N, certificate=True), 5),
        'old_regular_bool': median(
            lambda: M.regular_matroid()._is_isomorphic(N.regular_matroid()), 3),
    }
    if k == 4:
        row['old_regular_cert'] = median(
            lambda: M.regular_matroid()._is_isomorphic(
                N.regular_matroid(), certificate=True), 3)
    print('spqr', k, row, flush=True)


def old_three_connected_path(M, N):
    if not N.is_3connected():
        return False
    G = M.graph()
    H = N.graph()
    G.allow_loops(False)
    G.allow_multiple_edges(False)
    H.allow_loops(False)
    H.allow_multiple_edges(False)
    return G.is_isomorphic(H)


for n in (32, 64):
    G = graphs.WheelGraph(n)
    H = reverse_labels(G)
    M = GraphicMatroid(G, groundset=range(G.size()))
    N = GraphicMatroid(H, groundset=range(G.size(), 2 * G.size()))
    print('wheel', n, {
        'edges': G.size(),
        'new_bool': median(lambda: M.is_isomorphic(N), 5),
        'old_graphic': median(lambda: old_three_connected_path(M, N), 3),
    }, flush=True)


G = graphs.WindmillGraph(3, 1024)
H = reverse_labels(G)
print('many_blocks', {
    'edges': G.size(),
    'new_bool': median(lambda: G.is_2isomorphic(H), 3),
    'new_cert': median(lambda: G.is_2isomorphic(H, certificate=True), 3),
}, flush=True)

Validation

  • 1,139 doctests passed across the six directly affected implementation modules;
  • exhaustive comparison of all 209 unlabeled simple graphs through order 6 (1,384 invariant-compatible pairs);
  • exhaustive comparison of 435 three-vertex looped multigraphs with multiplicity at most 2 and at most 6 edges (5,298 invariant-compatible pairs);
  • every checked positive certificate was replayed and independently checked as a matroid isomorphism;
  • 401 additional oracle/random integration cases and 60 constructed block-cleaving cases passed;
  • dense, sparse, and static-sparse input backends, repeated/unhashable labels, loops, parallel classes, bridges, forests, disconnected sums, isolated vertices, self-certificates, and malformed witnesses are covered;
  • ruff (normal and preview configuration), relint, git diff --check, Cython compilation, and linking passed;
  • after the final build, ninja -C builddir -n reports no work to do;
  • the generated graphic_matroid C source no longer contains local DenseGraphBackend_cg, SparseGraphBackend_cg, or StaticSparseBackend_cg copies.

Known limitation

The generated sequence is deterministic and replayable, but is not promised to use the minimum number of Whitney operations. Because every twist explicitly serializes one edge side, deeply nested SPQR decompositions can produce a witness with quadratic total serialized size. The boolean path and the GraphicMatroid edge-mapping-only certificate path do not pay this output cost.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Documentation preview for this PR (built with commit 51dbc8b; changes) is ready! 🎉
This preview will update shortly after each push to this PR.

@cxzhong
cxzhong marked this pull request as ready for review August 7, 2026 03:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement is_2isomorphic (Whitney 2-isomorphism) for graphs

1 participant