Skip to content

Fix #1204: [Rule] MaximumCommonEdgeSubgraph to MaximumClique - #1237

Open
isPANN wants to merge 1 commit into
mainfrom
issue-1204-maximumcommonedgesubgraph-maximumclique
Open

isPANN wants to merge 1 commit into
mainfrom
issue-1204-maximumcommonedgesubgraph-maximumclique

Conversation

@isPANN

@isPANN isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1204

Adds the reduction MaximumCommonEdgeSubgraph → MaximumClique<SimpleGraph, One> (association-graph construction): one vertex per pair of source/target arcs with equal label and loop status, edges between pairs that define a consistent partial injection. Maximum cliques decode to maximum common edge subgraphs with equal value.

  • Rule: src/rules/maximumcommonedgesubgraph_maximumclique.rs (upper-bound overhead, identity value map, validated extraction), registered in src/rules/mod.rs with a canonical example.
  • Tests: src/unit_tests/rules/maximumcommonedgesubgraph_maximumclique.rs (closed loop, no-solution, empty source, labelled random samples up to 5×5, extraction rejection paths).
  • Paper: entry in docs/paper/reductions.typ plus bibliography key.
  • One-line trait disambiguation in maximumcommonedgesubgraph_ilp.rs, needed once the source has a second ReduceTo impl.

A verification certificate and a review report follow as comments.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate

Rule: MaximumCommonEdgeSubgraph → MaximumClique/SimpleGraph/One (issue #1204).
Verdict: verified. Two independently written scripts and the Typst proof agree; no counterexample, no cross-comparison disagreement.

Type resolution

  • Source: MaximumCommonEdgeSubgraph::Value = Max<i64> (impl Problem, type Value = Max<i64>).
  • Target: MaximumClique<SimpleGraph, One>::Value = Max<W::Sum>, <One as WeightElement>::Sum = i64 (src/types.rs) → Max<i64>.
  • Max<i64> → Max<i64>: witness-compatible; full-domain compatibility: PASSED (optimization rule, identity value map).
  • Redundancy: pred path MaximumCommonEdgeSubgraph MaximumClique/SimpleGraph/One reported no path before this rule; the only other outgoing edge of the source is to ILP. Not dominated.

Construction

Candidates are pairs of source/target arcs with equal label and equal loop status (N ≤ m1·m2); two candidates are adjacent iff they define a consistent partial injection on endpoints (E ≤ N(N−1)/2). A maximum clique decodes to a maximum common edge subgraph with the same value; any clique decodes to a feasible mapping.

Script A (exhaustive + sampled) — 2,006,378 checks, 0 failures

Sections: symbolic overhead 83; exhaustive forward+backward/optimum 1,325,821; extraction from every clique 524,345; measured size vs formula 35,264; structural well-formedness 120,834; YES example 14; NO example 12; mutation self-test 5.

  • 8,816 instances (5,554 with non-empty target), 47,874 cliques, 1,574,281 configurations, 20,911 cliques with strict value gap.
  • Families: all 1-label graph pairs with n1,n2 ≤ 2; every 29th pair of 2-vertex 2-label graphs; random n1,n2 ∈ 1..5 with 3 labels and ≤ 6 arcs (300 per size pair up to 4×4, 40 for the 5-vertex rows, 20 for 5×5); dense 1-label (3,3), (3,4), (4,3), (4,4), 60 each.
  • Seeds: random.Random(2026110301), random.Random(7); Hypothesis max_examples=400, derandomize=True, deadline=None.

Script B (independent) — 11,408,661 checks, 0 failures

  • Exhaustive: 1-label n1,n2 ≤ 2 and 3×3 (≤ 3 arcs) and 3×≤2, ≤2×3; 2-label 2×2 (≤ 3 arcs); random n1,n2 ∈ 1..5 (300 instances per size pair, 25 pairs); label-direction, loop-heavy and planted-solution Hypothesis families (300 each); special cases (empty arc sets, B=1, antiparallel arcs, disconnected, isolated vertex, loops, n1>n2).
  • Seed: SEED = 20261103 (and SEED + 1).
  • Mutation test: 17 deliberately broken constructions, all caught by both scripts.

Cross-comparison

  • Script-to-script: 55,776/55,776 identical targets; 3,370 identical targets with 19,316 cross-extractions, 0 disagreements.
  • Real pred (release build of this PR): pred reduce matches both scripts on all 670 shared instances (0 disagreements); pred solve value and extraction agree on the sampled instances; pred evaluate agrees on 2,680/2,680 configurations.

The scripts and the Typst proof follow in the next comments.

@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate (continued): Typst proof and script A

Typst proof
#set page(margin: 2cm)
#set text(size: 10.5pt)
#set heading(numbering: "1.")

= MaximumCommonEdgeSubgraph $->$ MaximumClique/SimpleGraph/One

*Provenance.* The association-graph technique is classical. Nerem, Crawford-Kahrl, Cummins and Gedeon (arXiv:1910.14638, Section 6, Theorem 6.3) relate a restricted directed MCES (weakly connected, simple, oriented, node-labelled graphs) to cliques through an extended line digraph. That theorem does not cover loops, edge labels, disconnected graphs or partial maps, so it is not applied here. The construction and the proof below are derived here for the library's domain; the sibling rules `MaximumCommonEdgeSubgraph` $->$ ILP (`src/rules/maximumcommonedgesubgraph_ilp.rs`) and `MaximumIndependentSet` $<->$ `MaximumClique` were read for conventions only.

== Type resolution and redundancy

- Source: `MaximumCommonEdgeSubgraph::Value = Max<i64>` (`src/models/graph/maximum_common_edge_subgraph.rs`, `impl Problem`, `type Value = Max<i64>`). Solution: `Vec<usize>` of length $n_1$, entries in $0..n_2$, sentinel $n_2$ for unmatched; matched entries pairwise distinct; a configuration of the right length with entries in $0..n_2$ that repeats a matched entry evaluates to `Max(None)`, while a wrong length or an out-of-range entry is an evaluation error.
- Target: `MaximumClique<SimpleGraph, One>::Value = Max<W::Sum>`, and `<One as WeightElement>::Sum = i64` (`src/types.rs`), so `Max<i64>`. Solution: `Vec<bool>`, feasible iff the selected set is pairwise adjacent; value is the number of selected vertices.
- `Max<i64>` $->$ `Max<i64>`: witness-compatible; this is an optimization rule.
- Redundancy: `pred path MaximumCommonEdgeSubgraph MaximumClique/SimpleGraph/One` reports no path. The only outgoing edge of the source is to `ILP/bool/i64/general`, and ILP has no route to MaximumClique. The edge is not dominated.

== Theorem

Let $G_i = (V_i, E_i)$, $V_i = {0, dots, n_i - 1}$, $E_i subset.eq V_i times Sigma times V_i$ be finite arc sets, $m_i = |E_i|$. For a partial injection $f: U subset.eq V_1 -> V_2$ let $s(f)$ be the number of arcs $(u, lambda, v) in E_1$ with $u, v in U$ and $(f(u), lambda, f(v)) in E_2$. Let $H$ be the graph of the construction below. Then $max_f s(f) = omega(H)$, every maximum clique of $H$ extracts to an optimal $f$, and every clique $C$ extracts to an $f$ with $s(f) >= |C|$.

== Construction

+ Fix the stored arc orders $E_1 = (e_1, dots, e_(m_1))$ and $E_2 = (e'_1, dots, e'_(m_2))$.
+ A _pair_ is $(i, j)$ with $e_i = (u, lambda, v)$ and $e'_j = (a, mu, b)$. It is _retained_ iff $lambda = mu$ and $(u = v) <=> (a = b)$.
+ The vertex set of $H$ is the retained pairs, ordered by $i$ and then by $j$. The pair $p = (i, j)$ carries the _assignment set_ $A_p = {(u, a), (v, b)}$. For a loop ($u = v$ and $a = b$) $A_p$ has one element; otherwise it has two elements with distinct first coordinates and distinct second coordinates, because $u != v$ and $a != b$. In both cases $A_p$ is a partial injection from source vertices to target vertices.
+ Distinct retained pairs $p, q$ are joined by an edge iff for every $(s, t) in A_p$ and $(s', t') in A_q$: $(s = s') <=> (t = t')$.
+ All weights are $1$ (the `One` weight type). The target is the simple graph $H$ with $N$ vertices, built with `MaximumClique::new`-equivalent validation of length $N$.

== Overhead

#table(
  columns: (auto, auto, auto),
  [Target parameter], [Formula (upper bound)], [Derivation],
  [`num_vertices`], [$N <= m_1 m_2$ (`num_arcs_1 * num_arcs_2`)], [at most one vertex per pair $(i, j)$; label and loop filters only remove pairs, so equality fails in general],
  [`num_edges`], [$<= (m_1 m_2)(m_1 m_2 - 1) / 2$], [at most one edge per unordered vertex pair, $N(N-1)/2 <= B(B-1)/2$ for $B = m_1 m_2 >= N$, because $B(B-1)/2 - N(N-1)/2 = (B-N)(B+N-1)/2 >= 0$],
)

The bounds are not exact for all inputs: an empty $E_1$, an empty $E_2$, or disjoint label sets give $N = 0$, while $m_1 m_2 > 0$ is possible. For $B in {0, 1}$ the edge bound is $0$.

== Correctness

*Lemma 1 (adjacency is union-compatibility).* For distinct retained $p, q$: $p tilde q$ iff $A_p union A_q$ is a function from source vertices to target vertices and is injective. Indeed, a violation of functionality is a pair $(s, t), (s, t')$ with $t != t'$, a violation of injectivity is $(s, t), (s', t)$ with $s != s'$; the adjacency condition forbids exactly these.

*(⇒) Source value $<=$ clique number.* Let $f$ be feasible with $U$ its matched set and $s(f) = k$. Let $e_(i_1), dots, e_(i_k)$ be the preserved arcs, $e_(i_r) = (u_r, lambda_r, v_r)$. By preservation, $e'_(j_r) = (f(u_r), lambda_r, f(v_r)) in E_2$ for exactly one index $j_r$ (arcs form a set). The pair $(i_r, j_r)$ is retained: the labels agree, and $u_r = v_r <=> f(u_r) = f(v_r)$ because $f$ is a well-defined injection on $U$. Its assignment set is contained in the graph of $f$. Any two of these $k$ pairs are distinct (their first coordinates differ) and their assignment sets lie in the graph of the single injection $f$, so their union is a function and injective, hence adjacent by Lemma 1. They form a clique of size $k$. Therefore $omega(H) >= s(f)$ for each feasible $f$, so $omega(H) >= max_f s(f)$. The empty map is feasible with value $0$ (also when $n_1 > n_2$), so the maximum is attained and is a non-negative integer.

*(⇐) Clique number $<=$ source value.* Let $C$ be a clique of size $c$ and $g = union_(p in C) A_p$. By Lemma 1 applied to every pair in $C$ (and by the fact that each $A_p$ is itself a partial injection), $g$ is a function and injective, so $g$ is a partial injection $V_1 supset.eq U -> V_2$. Each $p = (i, j) in C$ has $A_p subset.eq g$, so $g(u) = a$ and $g(v) = b$ for $e_i = (u, lambda, v)$ and $e'_j = (a, lambda, b) in E_2$; hence $e_i$ is preserved by $g$. Distinct $p, q in C$ have distinct first coordinates: if $p = (i, j)$ and $q = (i, j')$ with $j != j'$, then $e'_j = (a, lambda, b)$ and $e'_(j') = (a', lambda, b')$; both contain the source pair $(u, ., .)$ so adjacency gives $a = a'$ (from $u = u$) and $b = b'$ (from $v = v$), hence $e'_j = e'_(j')$ and $j = j'$, a contradiction. So $c$ distinct source arcs are preserved: $s(g) >= c$. Therefore $max_f s(f) >= omega(H)$.

Both directions give $max_f s(f) = omega(H)$. An arc counted by $s(g)$ need not come from $C$: the preserved set of $g$ can strictly contain the selected arcs. Example: in the labelled directed four-cycle $0 -> 1 -> 2 -> 3 -> 0$ (all label $0$) matched to itself, the three pairs of the arcs $(0,1), (1,2), (2,3)$ with themselves form a 3-clique whose union is the identity on all four vertices, which preserves $4$ arcs. Hence $s(g) > |C|$ is possible and only the inequality $s(g) >= |C|$ holds for arbitrary cliques.

== Solution extraction

Given a clique $C$ (a `Vec<bool>` selecting vertices of $H$): verify once that the selection is a clique of $H$ (otherwise reject). Set $f(x) = n_2$ (unmatched) for all $x in V_1$, then for every $p in C$ and $(s, t) in A_p$ set $f(s) = t$. By the (⇐) argument all writes agree and the written values are pairwise distinct, so $f$ is feasible, has length $n_1$, entries in $0..n_2$, and $s(f) >= |C|$. If $C$ is a maximum clique, $|C| = omega(H) = max s$, hence $s(f) = max s$ (as $s(f) <= max s$): optimal cliques extract to optimal maps. Non-optimal cliques may extract to a value strictly above $|C|$ (the four-cycle above), so the value map of the reduction is the identity only on completed optimum values; arbitrary witness scores do not correspond.

== Special cases

- *Loops.* A source loop pairs only with target loops (a loop/non-loop pair is dropped by the loop filter): otherwise one candidate would force $f(u) = a$ and $f(u) = b$ with $a != b$, or two source vertices to share an image.
- *Empty arc sets, or disjoint label sets.* $N = 0$. The only clique is empty, with value $0$; extraction returns the all-unmatched map $[n_2, dots, n_2]$, the source optimum is $0$.
- *$n_1 > n_2$.* The map is partial, so a feasible $f$ exists; cliques have size $<= min(m_1, m_2)$ (distinct source arcs and, by injectivity, distinct target arcs).
- *Antiparallel arcs and several labels on one ordered pair.* They are distinct arcs, each gives its own pair, and the label filter keeps differing labels apart. Reversed arcs are not identified.
- *Disconnected graphs.* No connectivity is used.
- *Isolated source vertices* never appear in any $A_p$ and stay unmatched.
- *Overflow.* $N <= m_1 m_2$ and edge bound $B(B-1)/2$ need checked arithmetic on usize in an implementation.

== YES example (`MaximumCommonEdgeSubgraph` with 4 source and 5 target vertices)

$E_1 = [(0,0,1), (1,1,2), (2,2,3), (3,3,0)]$, $n_1 = 4$; $E_2 = [(0,0,1), (1,1,3), (3,2,4), (4,3,0), (2,0,3)]$, $n_2 = 5$. Triples are (source vertex, label, destination vertex).

Retained pairs (label equal, no loops anywhere): $e_1$ (label 0) with $e'_1$ and $e'_5$; $e_2$ (label 1) with $e'_2$; $e_3$ (label 2) with $e'_3$; $e_4$ (label 3) with $e'_4$. Hence $N = 5$:

#table(
  columns: (auto, auto, auto),
  [vertex], [pair $(i, j)$], [$A_p$],
  [0], [(1,1)], [$0 -> 0, 1 -> 1$],
  [1], [(1,5)], [$0 -> 2, 1 -> 3$],
  [2], [(2,2)], [$1 -> 1, 2 -> 3$],
  [3], [(3,3)], [$2 -> 3, 3 -> 4$],
  [4], [(4,4)], [$3 -> 4, 0 -> 0$],
)
(pairs 1-indexed, vertices 0-indexed as in the library). Vertex 1 conflicts with 0 (source $0$ goes to $0$ and $2$), with 2 (source $1$ goes to $3$ and $1$), with 3 (the images $3$ and $3$ of source $1$ and $2$ coincide) and with 4 (source $0$ goes to $2$ and $0$); it is isolated. Vertices $0, 2, 3, 4$ agree pairwise: $(0,2)$ share source $1$ with image $1$ in both; $(0,3)$ share no source and no image; $(0,4)$ share source $0$ with image $0$ in both; $(2,3)$ share source $2$ with image $3$ in both; $(2,4)$ share no source and no image; $(3,4)$ share source $3$ with image $4$ in both. The edge list is $[(0,2), (0,3), (0,4), (2,3), (2,4), (3,4)]$: a 4-clique plus an isolated vertex, $N = 5$ vertices and $6$ edges ($<= 20 times 19 / 2$ bound with $B = 20$).

Clique ${0, 2, 3, 4}$ extracts to $f = [0, 1, 3, 4]$, preserving all four source arcs: $(0,0,1) -> (0,0,1)$, $(1,1,2) -> (1,1,3)$, $(2,2,3) -> (3,2,4)$, $(3,3,0) -> (4,3,0)$; value $4 = omega$. Source optimum is $4$, equal (it cannot exceed $m_1 = 4$). The sub-optimal cliques ${0, 2}$ and ${1}$ extract to $[0, 1, 3, 5]$ (score $2$) and $[2, 3, 5, 5]$ (score $1$), where $5$ is the sentinel.

== NO example

The question "is there a map with value $>= 5$?" on the instance above: the answer is no. A $5$-clique needs all $5$ vertices pairwise adjacent, but vertex $1$ is isolated and $H$ has $6 < 10$ edges, so $omega(H) = 4$. On the source side the value never exceeds $m_1 = 4$ and the optimum is $4$. Both sides are infeasible at threshold $5$.

A second NO example with no common arc: $G_1$ has $3$ vertices and arcs $[(0,0,1), (1,0,2), (2,0,0)]$ (label $0$), $G_2$ has $3$ vertices and arcs $[(0,1,1), (1,1,2), (2,1,0)]$ (label $1$). No pair is retained: $N = 0$, the only clique is empty (value $0$), extraction returns $[3, 3, 3]$, and of the $4^3 = 64$ configurations, $30$ map two source vertices to one image and are infeasible, and the other $34$ partial injections all have value $0$, so a threshold of $1$ is infeasible on both sides. A third: loops $[(0,0,0), (1,0,1), (2,0,2)]$ against the 3-cycle $[(0,0,1), (1,0,2), (2,0,0)]$ gives $N = 0$ by the loop filter and optimum $0$.
Script A
#!/usr/bin/env python3
"""Verification script A: MaximumCommonEdgeSubgraph -> MaximumClique<SimpleGraph, One>.

Source model (src/models/graph/maximum_common_edge_subgraph.rs):
  instance (n1, A1, n2, A2); A_i a set of arcs (u, label, v) over vertices 0..n_i-1.
  config: list of length n1, entries in 0..n2, sentinel n2 = unmatched; matched entries
  pairwise distinct; value Max<i64> = number of source arcs (u,l,v) with both ends matched
  and (f(u), l, f(v)) in A2; infeasible config -> Max(None).
Target model (MaximumClique<SimpleGraph, One>): config is a bool vector over vertices,
  feasible iff the selected set is pairwise adjacent, value Max<i64> = number selected.

Sections (each counted):
  1 symbolic overhead      4 measured size vs formula     7 NO example
  2 exhaustive fwd + bwd   5 structural well-formedness
  3 extraction             6 YES example
Section 8 (extra) mutates the reduction and requires the checks to catch every mutant.
"""
import itertools
import random
import sys

import sympy as sp
from hypothesis import HealthCheck, given, settings, strategies as st

COUNTS = {}
FAILS = []


def check(section, cond, msg=""):
    COUNTS[section] = COUNTS.get(section, 0) + 1
    if not cond:
        FAILS.append((section, msg))
        if len(FAILS) <= 20:
            print("FAIL", section, msg)


# ----------------------------------------------------------------- the reduction
def reduce(n1, A1, n2, A2, *, loop_filter=True, label_filter=True, inj=True, agree=True):
    """Return (cands, edges): cands[i] = (i1, i2, assignments tuple); edges sorted pairs."""
    cands = []
    for i1, (u, l, v) in enumerate(A1):
        for i2, (a, l2, b) in enumerate(A2):
            if label_filter and l != l2:
                continue
            if loop_filter and ((u == v) != (a == b)):
                continue
            cands.append((i1, i2, ((u, a), (v, b))))
    edges = []
    for p in range(len(cands)):
        for q in range(p + 1, len(cands)):
            ok = True
            for (s, t) in cands[p][2]:
                for (s2, t2) in cands[q][2]:
                    if agree and inj:
                        if (s == s2) != (t == t2):
                            ok = False
                    elif agree and not inj:
                        if s == s2 and t != t2:
                            ok = False
                    elif inj and not agree:
                        if t == t2 and s != s2:
                            ok = False
            if ok:
                edges.append((p, q))
    return cands, edges


def extract_checked(n1, n2, cands, edges, sel):
    """Extraction on a target witness (bool vector): None unless it has length N and is a clique."""
    N = len(cands)
    if len(sel) != N or not is_clique_cfg(N, edges, sel):
        return None
    return extract(n1, n2, cands, [i for i in range(N) if sel[i]])


def extract(n1, n2, cands, clique):
    """Source config from a clique (list of candidate indices)."""
    f = [n2] * n1
    for p in clique:
        for (s, t) in cands[p][2]:
            f[s] = t
    return f


# ----------------------------------------------------------------- model semantics
def valid_cfg(n1, n2, f):
    if len(f) != n1 or any(x < 0 or x > n2 for x in f):
        return False
    m = [x for x in f if x != n2]
    return len(m) == len(set(m))


def score(n1, A1, n2, A2, f):
    """None if infeasible, else preserved-arc count."""
    if not valid_cfg(n1, n2, f):
        return None
    S2 = set(A2)
    c = 0
    for (u, l, v) in A1:
        if f[u] != n2 and f[v] != n2 and (f[u], l, f[v]) in S2:
            c += 1
    return c


def all_configs(n1, n2):
    return itertools.product(range(n2 + 1), repeat=n1)


def all_cliques(N, edges):
    """Every clique (including empty) as a tuple of vertices."""
    adj = [set() for _ in range(N)]
    for p, q in edges:
        adj[p].add(q)
        adj[q].add(p)
    out = []

    def rec(cur, cand):
        out.append(tuple(cur))
        for x in sorted(cand):
            rec(cur + [x], {y for y in cand if y > x and y in adj[x]})

    rec([], set(range(N)))
    return out


def is_clique_cfg(N, edges, sel):
    E = set(edges)
    s = [i for i in range(N) if sel[i]]
    return all((p, q) in E for p, q in itertools.combinations(s, 2))


# ----------------------------------------------------------------- instance generators
def all_graphs(n, labels):
    poss = [(u, l, v) for u in range(n) for l in range(labels) for v in range(n)]
    for mask in range(1 << len(poss)):
        yield tuple(poss[i] for i in range(len(poss)) if mask >> i & 1)


def rand_graph(rng, n, labels, max_arcs):
    poss = [(u, l, v) for u in range(n) for l in range(labels) for v in range(n)]
    k = rng.randint(0, min(max_arcs, len(poss)))
    return tuple(rng.sample(poss, k))


# ----------------------------------------------------------------- per-instance battery
STATS = {"strict_gap": 0, "instances": 0, "cliques": 0, "configs": 0, "nonempty_target": 0}


def full_check(n1, A1, n2, A2, tag=""):
    """Sections 2-5 on one instance, exhaustive over configs and over all cliques."""
    STATS["instances"] += 1
    m1, m2 = len(A1), len(A2)
    cands, edges = reduce(n1, A1, n2, A2)
    N = len(cands)
    if N:
        STATS["nonempty_target"] += 1
    cliques = all_cliques(N, edges)
    STATS["cliques"] += len(cliques)

    # source optimum by enumeration of every config
    opt = None
    feas = {}
    for f in all_configs(n1, n2):
        STATS["configs"] += 1
        s = score(n1, A1, n2, A2, f)
        if s is not None:
            feas[f] = s
            if opt is None or s > opt:
                opt = s
    check("2", opt is not None and 0 <= opt <= min(m1, m2), f"optimum in range {tag}")
    check("2", (tuple([n2] * n1) in feas) and feas[tuple([n2] * n1)] == 0, f"all-unmatched {tag}")

    # forward: every feasible f gives a clique of size = score(f)
    for f, s in feas.items():
        sel = [i for i, (i1, i2, asg) in enumerate(cands) if all(f[x] == y for x, y in asg)]
        check("2", all(f[x] != n2 for i in sel for x, _ in cands[i][2]), f"fwd matched {tag}")
        check("2", is_clique_cfg(N, edges, [i in sel for i in range(N)]), f"fwd clique {tag} f={f}")
        check("2", len(sel) == s, f"fwd size==score {tag} f={f}: {len(sel)} vs {s}")

    # backward: every clique -> feasible config whose score >= clique size
    max_clique = max(len(c) for c in cliques)
    for c in cliques:
        f = extract(n1, n2, cands, c)
        check("3", len(f) == n1 and all(0 <= x <= n2 for x in f), f"config shape {tag}")
        sc = score(n1, A1, n2, A2, f)
        check("3", sc is not None, f"extraction feasible {tag} c={c}")
        if sc is None:
            continue
        check("3", sc >= len(c), f"score >= size {tag} c={c}")
        check("3", len(c) <= min(m1, m2), f"clique <= min arcs {tag}")
        # distinct clique vertices use distinct source arcs and distinct target arcs
        check("3", len({cands[p][0] for p in c}) == len(c) and len({cands[p][1] for p in c}) == len(c),
              f"distinct arcs {tag}")
        if len(c) == max_clique:
            check("3", sc == opt, f"optimal clique extracts optimal {tag} c={c} sc={sc} opt={opt}")
        if sc > len(c):
            STATS["strict_gap"] += 1
        # extraction via the model's own evaluator semantic: Max value of config

    # extraction validates its input: every bool vector is accepted iff it is a clique
    if N <= 10:
        cset = set(cliques)
        for bits in itertools.product([False, True], repeat=N):
            sel_t = tuple(i for i in range(N) if bits[i])
            r = extract_checked(n1, n2, cands, edges, list(bits))
            check("3", (r is not None) == (sel_t in cset), f"accept iff clique {tag} {sel_t}")
            if r is not None:
                check("3", r == extract(n1, n2, cands, list(sel_t)) and valid_cfg(n1, n2, r), f"valid {tag}")
    for bad in ([True] * (N + 1), [False] * (N + 1), [True] * max(N - 1, 0) if N else [False]):
        if len(bad) != N:
            check("3", extract_checked(n1, n2, cands, edges, bad) is None, f"wrong length rejected {tag}")

    # optimum equivalence
    check("2", max_clique == opt, f"optimum equal {tag}: clique {max_clique} vs source {opt}")

    # Section 4: size
    exact = sum(1 for (u, l, v) in A1 for (a, l2, b) in A2 if l == l2 and (u == v) == (a == b))
    check("4", N == exact, f"vertex count exact {tag}")
    check("4", N <= m1 * m2, f"N bound {tag}")
    B = m1 * m2
    check("4", len(edges) <= B * (B - 1) // 2, f"E bound {tag}")
    check("4", len(edges) <= N * (N - 1) // 2, f"E bound N {tag}")

    # Section 5: structure
    check("5", all(0 <= p < q < N for p, q in edges) and len(set(edges)) == len(edges), f"simple graph {tag}")
    check("5", edges == sorted(edges), f"edge order {tag}")
    check("5", [(c[0], c[1]) for c in cands] == sorted((c[0], c[1]) for c in cands), f"cand order {tag}")
    for (i1, i2, asg) in cands:
        (u, l, v), (a, l2, b) = A1[i1], A2[i2]
        check("5", l == l2 and (u == v) == (a == b), f"candidate valid {tag}")
        check("5", (len({s for s, _ in asg}) == 1) == (len({t for _, t in asg}) == 1), f"cand partial inj {tag}")
    # adjacency recomputed by the semantic definition: union of assignments is a partial injection
    E = set(edges)
    for p in range(N):
        for q in range(p + 1, N):
            fwd, bwd = {}, {}
            ok = True
            for (s, t) in cands[p][2] + cands[q][2]:
                if fwd.setdefault(s, t) != t or bwd.setdefault(t, s) != s:
                    ok = False
            check("5", ((p, q) in E) == ok, f"adjacency == union partial injection {tag} {p},{q}")
            if cands[p][0] == cands[q][0] or cands[p][1] == cands[q][1]:
                check("5", (p, q) not in E, f"same arc non-adjacent {tag}")
    return opt, max_clique, cands, edges


# ----------------------------------------------------------------- section 1
def section1():
    m1, m2, N, B = sp.symbols("m1 m2 N B", nonnegative=True, integer=True)
    Bexpr = m1 * m2
    edge_bound = Bexpr * (Bexpr - 1) / 2
    # edge bound equals C(m1 m2, 2) and is monotone nondecreasing; zero at B in {0,1}
    check("1", sp.simplify(edge_bound - sp.binomial(Bexpr, 2).expand(func=True)) == 0, "C(B,2)")
    check("1", sp.simplify(edge_bound.subs(m1, 0)) == 0 and sp.simplify(edge_bound.subs(m2, 0)) == 0, "zero at m=0")
    check("1", sp.simplify(edge_bound.subs({m1: 1, m2: 1})) == 0, "zero at B=1")
    check("1", sp.expand(edge_bound.subs(m1, 2).subs(m2, 3) - 15) == 0, "B=6 -> 15")
    # monotonicity of B(B-1)/2 in B for integers B >= 0: difference is B
    diff = sp.simplify(((B + 1) * B / 2) - (B * (B - 1) / 2))
    check("1", sp.simplify(diff - B) == 0, "increment is B")
    # N <= B and E(N) <= N(N-1)/2 <= B(B-1)/2 for N <= B
    gap = sp.expand(B * (B - 1) / 2 - N * (N - 1) / 2)
    factored = sp.factor(gap)
    check("1", sp.simplify(gap - (B - N) * (B + N - 1) / 2) == 0, "gap factor " + str(factored))
    # the (B-N)(B+N-1)/2 factor is >= 0 when 0 <= N <= B, B >= 1
    for Bv in range(1, 12):
        for Nv in range(0, Bv + 1):
            check("1", gap.subs({B: Bv, N: Nv}) >= 0, f"gap nonneg {Bv},{Nv}")


# ----------------------------------------------------------------- examples
E1 = [(0, 0, 1), (1, 1, 2), (2, 2, 3), (3, 3, 0)]
E2 = [(0, 0, 1), (1, 1, 3), (3, 2, 4), (4, 3, 0), (2, 0, 3)]


def section6():
    n1, n2 = 4, 5
    cands, edges = reduce(n1, E1, n2, E2)
    check("6", [(c[0], c[1]) for c in cands] == [(0, 0), (0, 4), (1, 1), (2, 2), (3, 3)], "candidate pairs")
    check("6", [c[2] for c in cands] == [((0, 0), (1, 1)), ((0, 2), (1, 3)), ((1, 1), (2, 3)),
                                          ((2, 3), (3, 4)), ((3, 4), (0, 0))], "assignments")
    check("6", edges == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)], f"edge list {edges}")
    check("6", len(cands) == 5 and len(edges) == 6, "5 vertices 6 edges")
    check("6", all(1 not in e for e in edges), "candidate 1 isolated")
    f = extract(n1, n2, cands, [0, 2, 3, 4])
    check("6", f == [0, 1, 3, 4], f"extracted {f}")
    check("6", score(n1, E1, n2, E2, f) == 4, "score 4")
    f2 = extract(n1, n2, cands, [0, 2])
    check("6", f2 == [0, 1, 3, 5] and score(n1, E1, n2, E2, f2) == 2, f"clique {{0,2}} -> {f2}")
    f3 = extract(n1, n2, cands, [1])
    check("6", f3 == [2, 3, 5, 5] and score(n1, E1, n2, E2, f3) == 1, f"clique {{1}} -> {f3}")
    cl = all_cliques(5, edges)
    check("6", max(len(c) for c in cl) == 4 and [c for c in cl if len(c) == 4] == [(0, 2, 3, 4)], "unique max clique")
    opt = max(s for f in all_configs(n1, n2) if (s := score(n1, E1, n2, E2, f)) is not None)
    check("6", opt == 4, "source optimum 4")
    # exact set of optimal source maps: those with score 4
    best = [f for f in all_configs(n1, n2) if score(n1, E1, n2, E2, f) == 4]
    check("6", best == [(0, 1, 3, 4)], f"optimal maps {best}")
    full_check(n1, E1, n2, E2, "yes-example")
    # non-optimal clique decoding more than its size: 3 consecutive cycle arcs fix all four images
    A = [(0, 0, 1), (1, 0, 2), (2, 0, 3), (3, 0, 0)]
    cands2, edges2 = reduce(4, A, 4, A)
    sel = [i for i, c in enumerate(cands2) if (c[0], c[1]) in {(0, 0), (1, 1), (2, 2)}]
    check("6", len(sel) == 3 and all((a, b) in set(edges2) for a, b in itertools.combinations(sel, 2)), "3-clique")
    f4 = extract(4, 4, cands2, sel)
    check("6", f4 == [0, 1, 2, 3] and score(4, A, 4, A, f4) == 4 > 3, f"gap example {f4}")


def section7():
    # NO: threshold 5 on the YES instance's data: no clique of size 5, no source score 5.
    n1, n2 = 4, 5
    cands, edges = reduce(n1, E1, n2, E2)
    cl = all_cliques(len(cands), edges)
    check("7", all(len(c) < 5 for c in cl), "no 5-clique")
    check("7", len(cands) == 5 and len(edges) < 10, "K5 impossible (6 < 10 edges)")
    opt_no = max(s for f in all_configs(n1, n2) if (s := score(n1, E1, n2, E2, f)) is not None)
    check("7", all((score(n1, E1, n2, E2, f) or 0) < 5 for f in all_configs(n1, n2)), "no score 5")
    check("7", max(len(c) for c in cl) == 4 and opt_no == 4, "optimum is 4 on both sides, below 5")
    # NO with incompatible labels: three source arcs, disjoint labels -> empty target, only empty clique
    A1 = [(0, 0, 1), (1, 0, 2), (2, 0, 0)]
    A2 = [(0, 1, 1), (1, 1, 2), (2, 1, 0)]
    c, e = reduce(3, A1, 3, A2)
    check("7", c == [] and e == [], "empty target")
    check("7", all_cliques(len(c), e) == [()], "only the empty clique of the constructed target")
    feasible = [f for f in all_configs(3, 3) if score(3, A1, 3, A2, f) is not None]
    check("7", len(feasible) == 34 and all(score(3, A1, 3, A2, f) == 0 for f in feasible), "34 feasible maps, all value 0")
    check("7", sum(1 for f in all_configs(3, 3) if score(3, A1, 3, A2, f) is None) == 30, "30 non-injective maps infeasible")
    check("7", extract(3, 3, c, []) == [3, 3, 3], "all-unmatched extraction")
    # NO with loop/nonloop: source loops cannot go to non-loops
    A1 = [(0, 0, 0), (1, 0, 1), (2, 0, 2)]
    A2 = [(0, 0, 1), (1, 0, 2), (2, 0, 0)]
    c, e = reduce(3, A1, 3, A2)
    check("7", c == [], "loop vs non-loop rejected")
    check("7", max(score(3, A1, 3, A2, f) for f in all_configs(3, 3) if score(3, A1, 3, A2, f) is not None) == 0, "no loop preserved")
    full_check(3, A1, 3, A2, "no-loop")
    # NO with injectivity: two source vertices can't share one image
    A1 = [(0, 0, 1), (2, 0, 3), (1, 0, 0), (3, 0, 2)]  # two disjoint 2-cycles
    A2 = [(0, 0, 1), (1, 0, 0), (2, 0, 2)]  # one 2-cycle and one loop
    opt, mc, _, _ = full_check(4, A1, 3, A2, "inj-no")
    check("7", opt == 2 and mc == 2, f"only one 2-cycle fits: {opt},{mc}")


# ----------------------------------------------------------------- special cases
def special_cases():
    # loops
    full_check(2, ((0, 0, 0), (1, 0, 1)), 2, ((0, 0, 0),), "two loops into one loop")
    full_check(1, ((0, 0, 0),), 3, ((0, 0, 1), (1, 0, 1)), "loop vs nonloop")
    # n1 > n2
    full_check(4, ((0, 0, 1), (1, 0, 2), (2, 0, 3)), 2, ((0, 0, 1), (1, 0, 0)), "n1>n2")
    full_check(3, (), 1, (), "no vertices arcs")
    # empty arc sets
    full_check(3, (), 3, ((0, 0, 1),), "empty A1")
    full_check(3, ((0, 0, 1),), 3, (), "empty A2")
    full_check(0, (), 0, (), "n=0")
    # antiparallel and different labels on same ordered pair
    full_check(2, ((0, 0, 1), (1, 0, 0)), 2, ((0, 0, 1), (1, 0, 0)), "antiparallel")
    full_check(2, ((0, 0, 1), (0, 1, 1)), 2, ((0, 0, 1), (0, 1, 1)), "two labels same pair")
    full_check(2, ((0, 0, 1), (0, 1, 1)), 2, ((0, 0, 1), (1, 1, 0)), "labels on reversed arcs")
    # reversed arcs not identified
    o, m, _, _ = full_check(2, ((0, 0, 1),), 2, ((1, 0, 0),), "reverse")
    check("5", o == 1 and m == 1, "reversed arc is matched by swapping vertices only")
    # disconnected sources
    full_check(4, ((0, 0, 1), (2, 0, 3)), 3, ((0, 0, 1), (1, 0, 2)), "disconnected")
    # strict gap exists (non-maximum witnesses score more than size) -> already in section 6
    check("5", STATS["strict_gap"] > 0, "at least one strict gap observed")


# ----------------------------------------------------------------- bulk exhaustive + sampled
def bulk():
    # All unlabelled looped digraph pairs with n <= 2 (1 label).
    graphs = {n: list(all_graphs(n, 1)) for n in range(3)}
    for n1 in range(3):
        for n2 in range(3):
            for A1 in graphs[n1]:
                for A2 in graphs[n2]:
                    full_check(n1, A1, n2, A2, f"unl {n1}{n2}")
    # All pairs n=2 with 2 labels is 256^2 = 65536 instances: take every 7th for time.
    g2 = list(all_graphs(2, 2))
    for i, A1 in enumerate(g2):
        for j, A2 in enumerate(g2):
            if (i * 256 + j) % 29 == 0:
                full_check(2, A1, 2, A2, "lab2")
    # Sampled: n1,n2 in 1..5, 3 labels, up to 6 arcs; >= 300 per (n1,n2) up to (4,4),
    # fewer for the n=5 rows because of the exhaustive config enumeration (6^5 = 7776).
    rng = random.Random(2026110301)
    for n1 in range(1, 6):
        for n2 in range(1, 6):
            reps = 300 if max(n1, n2) <= 4 else 40
            if n1 == 5 and n2 == 5:
                reps = 20
            for _ in range(reps):
                A1 = rand_graph(rng, n1, 3, 6)
                A2 = rand_graph(rng, n2, 3, 6)
                full_check(n1, A1, n2, A2, f"smp {n1}{n2}")
    # dense one-label instances, where cliques are large
    for n1, n2 in [(3, 3), (3, 4), (4, 3), (4, 4)]:
        for _ in range(60):
            A1 = rand_graph(rng, n1, 1, 6)
            A2 = rand_graph(rng, n2, 1, 8)
            full_check(n1, A1, n2, A2, f"dense {n1}{n2}")


# ----------------------------------------------------------------- hypothesis
@st.composite
def instance_small_labels(draw):
    n1 = draw(st.integers(0, 4))
    n2 = draw(st.integers(0, 4))

    def g(n):
        if n == 0:
            return ()
        arc = st.tuples(st.integers(0, n - 1), st.integers(0, 2), st.integers(0, n - 1))
        return tuple(draw(st.lists(arc, max_size=6, unique=True)))
    return n1, g(n1), n2, g(n2)


@st.composite
def instance_structured(draw):
    # source is a relabelled copy/perturbation of a subgraph of the target: forces large cliques
    n2 = draw(st.integers(1, 5))
    arc = st.tuples(st.integers(0, n2 - 1), st.integers(0, 1), st.integers(0, n2 - 1))
    A2 = tuple(draw(st.lists(arc, max_size=7, unique=True)))
    perm = draw(st.permutations(list(range(n2))))
    keep = [a for a in A2 if draw(st.booleans())]
    A1 = tuple(dict.fromkeys((perm[u], l, perm[v]) for u, l, v in keep))
    extra = draw(st.lists(st.tuples(st.integers(0, n2 - 1), st.integers(0, 1), st.integers(0, n2 - 1)),
                          max_size=2, unique=True))
    A1 = tuple(dict.fromkeys(A1 + tuple(extra)))
    return n2, A1, n2, A2


def hyp():
    cfg = settings(max_examples=400, deadline=None, derandomize=True, suppress_health_check=list(HealthCheck))

    @cfg
    @given(instance_small_labels())
    def t1(inst):
        n1, A1, n2, A2 = inst
        full_check(n1, A1, n2, A2, "hyp-small")

    @cfg
    @given(instance_structured())
    def t2(inst):
        n1, A1, n2, A2 = inst
        o, m, _, _ = full_check(n1, A1, n2, A2, "hyp-struct")
        # planted: the source is a subgraph of the target up to permutation only before extra arcs
        check("2", o <= min(len(A1), len(A2)), "optimum <= min arcs")

    t1()
    t2()


# ----------------------------------------------------------------- mutants
def run_mutant(**kw):
    """True if the checks (optimum equality, size, extraction feasibility) detect the mutant."""
    rng = random.Random(7)
    tests = [(2, ((0, 0, 0), (1, 0, 1)), 2, ((0, 0, 0),)),
             (1, ((0, 0, 0),), 3, ((0, 0, 1),)),
             (3, ((0, 0, 1), (1, 0, 2)), 3, ((0, 0, 1), (1, 0, 2))),
             (4, ((0, 0, 1), (2, 0, 3)), 3, ((0, 0, 1), (1, 0, 2))),
             (2, ((0, 0, 1),), 2, ((0, 1, 1),))]
    for _ in range(300):
        n1, n2 = rng.randint(1, 4), rng.randint(1, 4)
        tests.append((n1, rand_graph(rng, n1, 2, 5), n2, rand_graph(rng, n2, 2, 5)))
    for n1, A1, n2, A2 in tests:
        cands, edges = reduce(n1, A1, n2, A2, **kw)
        cl = all_cliques(len(cands), edges)
        opt = max(s for f in all_configs(n1, n2) if (s := score(n1, A1, n2, A2, f)) is not None)
        if max(len(c) for c in cl) != opt:
            return True
        for c in cl:
            if score(n1, A1, n2, A2, extract(n1, n2, cands, c)) is None:
                return True
    return False


def section8():
    check("8", not run_mutant(), "correct reduction must not be flagged")
    check("8", run_mutant(loop_filter=False), "mutant: loop filter dropped is caught")
    check("8", run_mutant(label_filter=False), "mutant: label filter dropped is caught")
    check("8", run_mutant(inj=False), "mutant: injectivity condition dropped is caught")
    check("8", run_mutant(agree=False), "mutant: agreement condition dropped is caught")


def main():
    section1()
    section6()
    section7()
    special_cases()
    bulk()
    hyp()
    section8()
    names = {"1": "symbolic overhead", "2": "exhaustive fwd+bwd / optimum", "3": "extraction from every clique",
             "4": "measured size vs formula", "5": "structural well-formedness", "6": "YES example",
             "7": "NO example", "8": "mutation self-test"}
    print("== audit ==")
    for k in sorted(COUNTS):
        print(f"section {k} ({names[k]}): {COUNTS[k]} checks")
    print(f"instances {STATS['instances']} (nonempty target {STATS['nonempty_target']}), "
          f"cliques {STATS['cliques']}, configs {STATS['configs']}, strict-gap cliques {STATS['strict_gap']}")
    total = sum(COUNTS.values())
    for k in "12345678":
        if COUNTS.get(k, 0) == 0:
            FAILS.append((k, "empty section"))
    print(f"TOTAL: {total} checks, {len(FAILS)} failures")
    return 1 if FAILS else 0


if __name__ == "__main__":
    sys.exit(main())

@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate (continued): script B

Script B

```python
#!/usr/bin/env python3
"""Verification script B (written independently): MaximumCommonEdgeSubgraph -> MaximumClique/SimpleGraph/One.

Written from the Typst proof and the Rust source model only (script A
was not read before this file was written).

An instance is (n1, arcs1, n2, arcs2) with arcs as (src, label, dst) tuples.
The reduction is pluggable (reduce_fn / extract_fn) so that
a mutation test can run this harness against deliberately broken constructions.

Every check goes through Audit.check; the script ends with
TOTAL: <checks> checks, <failures> failures.
"""
from future import annotations

import itertools
import random
import sys
from collections import Counter, defaultdict

SEED = 20261103

---------------------------------------------------------------- audit

class Audit:
def init(self, stop_on_fail: bool = False):
self.counts = Counter()
self.fails = Counter()
self.examples = {}
self.stop_on_fail = stop_on_fail
self.instances = Counter()

def check(self, section: str, cond: bool, detail=None) -> bool:
    self.counts[section] += 1
    if not cond:
        self.fails[section] += 1
        self.examples.setdefault(section, detail)
        if self.stop_on_fail:
            raise AssertionError((section, detail))
    return cond

def total(self):
    return sum(self.counts.values()), sum(self.fails.values())

def report(self, out=sys.stdout):
    for sec in sorted(self.counts):
        print(f"  {sec:<40} {self.counts[sec]:>9} checks  {self.fails[sec]:>5} failures", file=out)
        if self.fails[sec]:
            print(f"    first failure: {self.examples[sec]!r}", file=out)
    print("  instances: " + ", ".join(f"{k}={v}" for k, v in sorted(self.instances.items())), file=out)

---------------------------------------------------------------- source model (restated from the Rust model)

def src_feasible(inst, cfg) -> bool:
n1, _, n2, _ = inst
if len(cfg) != n1 or any(not (0 <= x <= n2) for x in cfg):
return False
matched = [x for x in cfg if x != n2]
return len(matched) == len(set(matched))

def src_score(inst, cfg):
"""Preserved arc count, or None for an infeasible configuration."""
n1, a1, n2, a2 = inst
if not src_feasible(inst, cfg):
return None
s2 = set(a2)
return sum(1 for (u, l, v) in a1 if cfg[u] != n2 and cfg[v] != n2 and (cfg[u], l, cfg[v]) in s2)

def all_configs(inst):
n1, _, n2, _ = inst
return itertools.product(range(n2 + 1), repeat=n1)

def all_partial_injections(inst):
"""Feasible configurations only (generated directly, not by filtering)."""
n1, _, n2, _ = inst
for k in range(0, min(n1, n2) + 1):
for dom in itertools.combinations(range(n1), k):
for img in itertools.permutations(range(n2), k):
cfg = [n2] * n1
for s, t in zip(dom, img):
cfg[s] = t
yield tuple(cfg)

---------------------------------------------------------------- construction (from the proof)

def reduce_proof(inst):
"""Return (pairs, assignment sets, N, edge set). Vertices ordered by (i, j)."""
n1, a1, n2, a2 = inst
pairs = []
assigns = []
for i, (u, lam, v) in enumerate(a1):
for j, (a, mu, b) in enumerate(a2):
if lam != mu:
continue
if (u == v) != (a == b):
continue
pairs.append((i, j))
assigns.append(frozenset({(u, a), (v, b)}))
edges = set()
for p in range(len(pairs)):
for q in range(p + 1, len(pairs)):
ok = all((s == s2) == (t == t2) for (s, t) in assigns[p] for (s2, t2) in assigns[q])
if ok:
edges.add((p, q))
return pairs, assigns, len(pairs), edges

def extract_proof(inst, red, sel):
"""sel: list of bools of length N. Raises ValueError on a non-clique or bad length."""
n1, _, n2, _ = inst
pairs, assigns, N, edges = red
if len(sel) != N:
raise ValueError("wrong length")
chosen = [k for k, b in enumerate(sel) if b]
for x, y in itertools.combinations(chosen, 2):
if (x, y) not in edges:
raise ValueError("not a clique")
f = [n2] * n1
for k in chosen:
for s, t in assigns[k]:
f[s] = t
return tuple(f)

---------------------------------------------------------------- target helpers

def adjacency(N, edges):
adj = [set() for _ in range(N)]
for p, q in edges:
adj[p].add(q)
adj[q].add(p)
return adj

def all_cliques(N, edges, limit=200000):
"""Every clique (including the empty one), as sorted tuples. None if more than limit."""
adj = adjacency(N, edges)
out = []

def rec(cur, cand):
    out.append(tuple(cur))
    if len(out) > limit:
        raise OverflowError
    for k in sorted(cand):
        rec(cur + [k], {c for c in cand if c > k and c in adj[k]})

try:
    rec([], set(range(N)))
except OverflowError:
    return None
return out

def omega(N, edges):
adj = adjacency(N, edges)
best = 0

def rec(size, cand):
    nonlocal best
    if size + len(cand) <= best:
        return
    if not cand:
        best = max(best, size)
        return
    for k in sorted(cand):
        rec(size + 1, {c for c in cand if c > k and c in adj[k]})
        cand = cand - {k}
        if size + len(cand) <= best:
            return
    best = max(best, size)

rec(0, set(range(N)))
return best

---------------------------------------------------------------- the per-instance attack

def partial_injection_ok(pairs_set):
d, r = {}, {}
for s, t in pairs_set:
if d.get(s, t) != t or r.get(t, s) != s:
return False
d[s] = t
r[t] = s
return True

def attack(inst, audit: Audit, reduce_fn=reduce_proof, extract_fn=extract_proof, family="misc",
every_clique=True, every_config=True):
n1, a1, n2, a2 = inst
m1, m2 = len(a1), len(a2)
audit.instances[family] += 1
red = reduce_fn(inst)
pairs, assigns, N, edges = red
B = m1 * m2

# --- overhead: the declared upper bounds, and an independent exact count of N
audit.check("overhead/num_vertices<=m1*m2", N <= B, (inst, N))
audit.check("overhead/num_edges<=B(B-1)/2", len(edges) <= B * (B - 1) // 2, (inst, len(edges)))
lp1, nl1, lp2, nl2 = Counter(), Counter(), Counter(), Counter()
for (u, l, v) in a1:
    (lp1 if u == v else nl1)[l] += 1
for (u, l, v) in a2:
    (lp2 if u == v else nl2)[l] += 1
exactN = sum(lp1[l] * lp2[l] for l in lp1) + sum(nl1[l] * nl2[l] for l in nl1)
audit.check("overhead/N==sum_l(loops*loops+nonloops*nonloops)", N == exactN, (inst, N, exactN))

# --- structure: simple graph, vertex order, assignment sets
audit.check("structure/edges simple, in range", all(0 <= p < q < N for p, q in edges), inst)
audit.check("structure/order by (i,j)", pairs == sorted(pairs) and len(set(pairs)) == len(pairs), inst)
for k, (i, j) in enumerate(pairs):
    (u, l, v), (a, mu, b) = a1[i], a2[j]
    audit.check("structure/retained => label equal, loop status equal",
                l == mu and ((u == v) == (a == b)), (inst, k))
    audit.check("structure/A_p == {(u,a),(v,b)}", assigns[k] == frozenset({(u, a), (v, b)}), (inst, k))
    audit.check("structure/A_p partial injection, size 1 iff loop",
                partial_injection_ok(assigns[k]) and (len(assigns[k]) == 1) == (u == v), (inst, k))
# Lemma 1: adjacency iff union is a partial injection (checked on every unordered pair)
for p, q in itertools.combinations(range(N), 2):
    audit.check("lemma1/adjacent iff union partial injection",
                ((p, q) in edges) == partial_injection_ok(assigns[p] | assigns[q]), (inst, p, q))

# --- source optimum
opt = None
if every_config:
    nfeas = 0
    for cfg in all_configs(inst):
        sc = src_score(inst, cfg)
        if sc is None:
            continue
        nfeas += 1
        opt = sc if opt is None else max(opt, sc)
    audit.check("source/feasible count == partial injections",
                nfeas == sum(1 for _ in all_partial_injections(inst)), inst)
else:
    opt = max(src_score(inst, c) for c in all_partial_injections(inst))
audit.check("source/empty map feasible, value 0", src_score(inst, (n2,) * n1) == 0, inst)

# --- forward: each feasible f -> clique of its preserved arcs, size s(f)
index = {pq: k for k, pq in enumerate(pairs)}
for cfg in all_partial_injections(inst):
    sc = src_score(inst, cfg)
    cl = []
    for i, (u, l, v) in enumerate(a1):
        if cfg[u] == n2 or cfg[v] == n2:
            continue
        js = [j for j, e in enumerate(a2) if e == (cfg[u], l, cfg[v])]
        if js:
            cl.append(index.get((i, js[0])))
    ok = None not in cl and all((min(x, y), max(x, y)) in edges for x, y in itertools.combinations(cl, 2))
    audit.check("forward/preserved pairs retained and form a clique", ok, (inst, cfg))
    # independent description: the candidates whose assignment set lies in the graph of f
    consistent = {k for k in range(N) if all(cfg[s] == t for s, t in assigns[k])}
    audit.check("forward/clique == candidates consistent with f", set(cl) == consistent, (inst, cfg))
    audit.check("forward/consistent candidates count == s(f)", len(consistent) == sc, (inst, cfg))
    if ok and N:
        sel = [k in cl for k in range(N)]
        g = extract_fn(inst, red, sel)
        # extraction of the forward clique is a sub-map of f
        audit.check("forward/extract(clique(f)) restricts f",
                    all(g[x] == n2 or g[x] == cfg[x] for x in range(n1)), (inst, cfg, g))

# --- backward + extraction over every clique (or omega only when too many)
w = omega(N, edges)
audit.check("optimum/omega(H) == max s(f)", w == opt, (inst, w, opt))
audit.check("optimum/omega <= min(m1,m2)", w <= min(m1, m2), (inst, w))
cliques = all_cliques(N, edges) if every_clique else None
if cliques is None:
    cliques = []
    audit.instances["clique-enum-skipped"] += 1
above = 0
for C in cliques:
    sel = [k in C for k in range(N)]
    f = extract_fn(inst, red, sel)
    sc = src_score(inst, f)
    audit.check("extract/length n1, entries in 0..=n2", len(f) == n1 and all(0 <= x <= n2 for x in f), (inst, C))
    audit.check("extract/feasible", sc is not None, (inst, C, f))
    audit.check("extract/s(f) >= |C|", sc is not None and sc >= len(C), (inst, C, f, sc))
    audit.check("backward/distinct source and target arcs",
                len({pairs[k][0] for k in C}) == len(C) == len({pairs[k][1] for k in C}), (inst, C))
    # every write agrees: f extends each A_p
    audit.check("extract/f extends every A_p", all(f[s] == t for k in C for s, t in assigns[k]), (inst, C))
    # unmatched exactly the vertices outside every A_p
    touched = {s for k in C for s, _ in assigns[k]}
    audit.check("extract/untouched vertices unmatched", all((f[x] == n2) == (x not in touched) for x in range(n1)), (inst, C))
    if len(C) == w:
        audit.check("extract/maximum clique -> optimal map", sc == opt, (inst, C, sc, opt))
    if sc is not None and sc > len(C):
        above += 1
audit.instances["cliques-above-size"] += above

# --- extraction must reject non-cliques and wrong lengths
for bad_len in {N + 1, max(N - 1, 0)} - {N}:
    try:
        extract_fn(inst, red, [False] * bad_len)
        audit.check("extract/rejects wrong length", False, (inst, bad_len))
    except ValueError:
        audit.check("extract/rejects wrong length", True)
nonadj = [(p, q) for p, q in itertools.combinations(range(N), 2) if (p, q) not in edges][:5]
for p, q in nonadj:
    sel = [k in (p, q) for k in range(N)]
    try:
        extract_fn(inst, red, sel)
        audit.check("extract/rejects non-clique", False, (inst, p, q))
    except ValueError:
        audit.check("extract/rejects non-clique", True)
return opt, w

---------------------------------------------------------------- instance families

def graphs(n, labels, max_arcs=None):
"""Every arc set over n vertices and the given labels (loops included)."""
universe = [(u, l, v) for u in range(n) for l in labels for v in range(n)]
top = len(universe) if max_arcs is None else min(max_arcs, len(universe))
for k in range(top + 1):
for comb in itertools.combinations(universe, k):
yield list(comb)

def random_graph(rng, n, labels, m, loop_bias=0.2):
seen, out = set(), []
tries = 0
while len(out) < m and tries < 50 * (m + 1) and n > 0:
tries += 1
u = rng.randrange(n)
v = u if rng.random() < loop_bias else rng.randrange(n)
e = (u, rng.choice(labels), v)
if e not in seen:
seen.add(e)
out.append(e)
rng.shuffle(out)
return out

def exhaustive_families():
# (a) one label, every looped digraph pair with n1, n2 <= 2, plus n1 = 3 against n2 <= 2 and vice versa
small = {n: list(graphs(n, [0])) for n in range(4)}
for n1 in range(3):
for n2 in range(3):
for g1 in small[n1]:
for g2 in small[n2]:
yield "exh-1label-n<=2", (n1, g1, n2, g2)
small3 = [g for g in small[3] if len(g) <= 3]
for g1 in small3:
for g2 in small3:
yield "exh-1label-3x3-<=3arcs", (3, g1, 3, g2)
for g1 in small[3]:
for n2 in range(3):
for g2 in small[n2]:
yield "exh-1label-3x<=2", (3, g1, n2, g2)
for n1 in range(3):
for g1 in small[n1]:
for g2 in small[3]:
yield "exh-1label-<=2x3", (n1, g1, 3, g2)
# (b) two labels: every n1 <= 1 graph against every n2 <= 2 graph, both orders
two = {n: list(graphs(n, [0, 1])) for n in range(3)}
for n1 in range(2):
for g1 in two[n1]:
for n2 in range(3):
for g2 in two[n2]:
yield "exh-2label-<=1x<=2", (n1, g1, n2, g2)
yield "exh-2label-<=2x<=1", (n2, g2, n1, g1)
# (c) two labels, n = 2 on both sides, every pair with at most 3 arcs per side
g2s = [g for g in two[2] if len(g) <= 3]
for g1 in g2s:
for g2 in g2s:
yield "exh-2label-2x2-<=3arcs", (2, g1, 2, g2)

def sampled_families(rng):
for n1 in range(1, 6):
for n2 in range(1, 6):
reps = 300
for r in range(reps):
labels = list(range(rng.choice([1, 2, 3])))
m1 = rng.randint(0, min(7, n1 * n1 * len(labels)))
m2 = rng.randint(0, min(7, n2 * n2 * len(labels)))
bias = rng.choice([0.0, 0.2, 0.5, 1.0])
yield f"rand-{n1}x{n2}", (n1, random_graph(rng, n1, labels, m1, bias), n2, random_graph(rng, n2, labels, m2, bias))
# planted: G2 contains a relabelled copy of G1 plus noise, so the optimum is m1
for r in range(300):
n1 = rng.randint(1, 4)
n2 = rng.randint(n1, 5)
labels = list(range(rng.choice([1, 2])))
g1 = random_graph(rng, n1, labels, rng.randint(0, 6), 0.2)
perm = rng.sample(range(n2), n1)
g2 = list({(perm[u], l, perm[v]) for u, l, v in g1} | set(random_graph(rng, n2, labels, rng.randint(0, 3), 0.2)))
rng.shuffle(g2)
yield "planted", (n1, g1, n2, g2)

---------------------------------------------------------------- examples and prose claims

ISSUE_INST = (4, [(0, 0, 1), (1, 1, 2), (2, 2, 3), (3, 3, 0)],
5, [(0, 0, 1), (1, 1, 3), (3, 2, 4), (4, 3, 0), (2, 0, 3)])

def examples(audit: Audit, reduce_fn=reduce_proof, extract_fn=extract_proof):
for block in (_example_yes, _example_no, _example_cycle):
try:
block(audit, reduce_fn, extract_fn)
except Exception as exc: # a crash counts as a failure of that block
audit.check(f"crash/{block.name}", False, repr(exc)[:200])

def _example_yes(audit, reduce_fn, extract_fn):
sec = "example/YES"
red = reduce_fn(ISSUE_INST)
pairs, assigns, N, edges = red
audit.check(sec + " pairs", pairs == [(0, 0), (0, 4), (1, 1), (2, 2), (3, 3)], pairs)
want_A = [{(0, 0), (1, 1)}, {(0, 2), (1, 3)}, {(1, 1), (2, 3)}, {(2, 3), (3, 4)}, {(3, 4), (0, 0)}]
audit.check(sec + " assignment sets", [set(a) for a in assigns] == want_A, assigns)
audit.check(sec + " N=5", N == 5, N)
audit.check(sec + " edge list", sorted(edges) == [(0, 2), (0, 3), (0, 4), (2, 3), (2, 4), (3, 4)], sorted(edges))
audit.check(sec + " vertex 1 isolated", all(1 not in e for e in edges))
audit.check(sec + " edges within bound B(B-1)/2 = 190", len(edges) <= 190, len(edges))
for C, want, score in [((0, 2, 3, 4), (0, 1, 3, 4), 4), ((0, 2), (0, 1, 3, 5), 2), ((1,), (2, 3, 5, 5), 1)]:
f = extract_fn(ISSUE_INST, red, [k in C for k in range(N)])
audit.check(sec + f" extract {C}", f == want, f)
audit.check(sec + f" score {C}", src_score(ISSUE_INST, f) == score, src_score(ISSUE_INST, f))
f = (0, 1, 3, 4)
images = [(f[u], l, f[v]) for u, l, v in ISSUE_INST[1]]
audit.check(sec + " image arcs", images == [(0, 0, 1), (1, 1, 3), (3, 2, 4), (4, 3, 0)], images)
opt = max(src_score(ISSUE_INST, c) for c in all_partial_injections(ISSUE_INST))
audit.check(sec + " source optimum 4 = omega", opt == 4 == omega(N, edges), opt)

def _example_no(audit, reduce_fn, extract_fn):
red = reduce_fn(ISSUE_INST)
pairs, assigns, N, edges = red
sec = "example/NO"
# threshold 5 on the YES instance
audit.check(sec + " threshold 5 infeasible both sides",
omega(N, edges) < 5 and all((src_score(ISSUE_INST, c) or 0) < 5 for c in all_configs(ISSUE_INST)))
audit.check(sec + " 6 < 10 edges", len(edges) == 6)
# no common label
inst = (3, [(0, 0, 1), (1, 0, 2), (2, 0, 0)], 3, [(0, 1, 1), (1, 1, 2), (2, 1, 0)])
red = reduce_fn(inst)
audit.check(sec + " disjoint labels N=0", red[2] == 0, red[2])
audit.check(sec + " extraction [3,3,3]", extract_fn(inst, red, []) == (3, 3, 3))
cfgs = list(all_configs(inst))
audit.check(sec + " 34 feasible / 30 infeasible of 64",
sum(src_score(inst, c) is not None for c in cfgs) == 34 and sum(src_score(inst, c) is None for c in cfgs) == 30)
audit.check(sec + " every config value 0 (or infeasible)", all(src_score(inst, c) in (0, None) for c in cfgs))
audit.check(sec + " threshold 1 infeasible", max(src_score(inst, c) for c in all_partial_injections(inst)) == 0)
# the proof says "every one of the 64 configurations has value 0": configurations with a repeated
# image are infeasible (Max(None)), not value 0. Record the count of infeasible ones.
n_infeasible = sum(1 for c in cfgs if src_score(inst, c) is None)
audit.check(sec + " (prose) 64 configs: infeasible ones exist", n_infeasible == 64 - sum(1 for _ in all_partial_injections(inst)))
# loops vs 3-cycle
inst = (3, [(0, 0, 0), (1, 0, 1), (2, 0, 2)], 3, [(0, 0, 1), (1, 0, 2), (2, 0, 0)])
red = reduce_fn(inst)
audit.check(sec + " loops vs cycle N=0", red[2] == 0)
audit.check(sec + " loops vs cycle optimum 0", max(src_score(inst, c) for c in all_partial_injections(inst)) == 0)

def _example_cycle(audit, reduce_fn, extract_fn):
sec = "prose/4-cycle caveat"
cyc = [(0, 0, 1), (1, 0, 2), (2, 0, 3), (3, 0, 0)]
inst = (4, cyc, 4, cyc)
red = reduce_fn(inst)
pairs, assigns, N, edges = red
C = [pairs.index((0, 0)), pairs.index((1, 1)), pairs.index((2, 2))]
audit.check(sec + " 3-clique", all((min(x, y), max(x, y)) in edges for x, y in itertools.combinations(C, 2)))
f = extract_fn(inst, red, [k in C for k in range(N)])
audit.check(sec + " union is identity", f == (0, 1, 2, 3), f)
audit.check(sec + " preserves 4 > 3", src_score(inst, f) == 4)

def special_cases(audit: Audit, reduce_fn=reduce_proof, extract_fn=extract_proof):
sec = "special"
# loop/non-loop pair dropped
inst = (2, [(0, 0, 0)], 2, [(0, 0, 1)])
audit.check(sec + "/loop-nonloop dropped", reduce_fn(inst)[2] == 0)
inst = (2, [(0, 0, 1)], 2, [(1, 0, 1)])
audit.check(sec + "/nonloop-loop dropped", reduce_fn(inst)[2] == 0)
# loop with loop retained, single assignment
inst = (2, [(1, 0, 1)], 3, [(2, 0, 2)])
red = reduce_fn(inst)
audit.check(sec + "/loop-loop retained", red[2] == 1 and red[1][0] == frozenset({(1, 2)}))
audit.check(sec + "/loop extraction", extract_fn(inst, red, [True]) == (3, 2))
# empty arc sets
for inst in [(3, [], 2, [(0, 0, 1)]), (2, [(0, 0, 1)], 3, []), (0, [], 0, []), (3, [], 0, [])]:
red = reduce_fn(inst)
audit.check(sec + "/empty arcs N=0", red[2] == 0 and not red[3])
audit.check(sec + "/empty arcs all unmatched", extract_fn(inst, red, []) == (inst[2],) * inst[0])
# n1 > n2
inst = (4, [(0, 0, 1), (1, 0, 2), (2, 0, 3), (3, 0, 0)], 2, [(0, 0, 1), (1, 0, 0)])
opt, w = attack(inst, audit, reduce_fn, extract_fn, family="special")
# only two source vertices can be matched; 0->0, 1->1 preserves (0,0,1) alone
audit.check(sec + "/n1>n2 optimum 1", opt == 1 == w, (opt, w))
# antiparallel arcs and two labels on one ordered pair
inst = (2, [(0, 0, 1), (1, 0, 0), (0, 1, 1)], 2, [(0, 0, 1), (0, 1, 1)])
opt, w = attack(inst, audit, reduce_fn, extract_fn, family="special")
audit.check(sec + "/antiparallel not identified, labels distinct: optimum 2", opt == 2 == w, (opt, w))
# disconnected graphs, isolated vertices
inst = (5, [(0, 0, 1), (3, 0, 4)], 5, [(1, 0, 2), (4, 0, 0)])
opt, w = attack(inst, audit, reduce_fn, extract_fn, family="special")
audit.check(sec + "/disconnected: optimum 2", opt == 2 == w)
red = reduce_fn(inst)
C = [red[0].index((0, 0)), red[0].index((1, 1))]
full = extract_fn(inst, red, [k in C for k in range(red[2])])
audit.check(sec + "/isolated vertex 2 unmatched", full == (1, 2, 5, 4, 0), full)
# strictness of the overhead bounds: N < m1m2 is possible with m1m2 > 0
inst = (2, [(0, 0, 1)], 2, [(0, 1, 1)])
audit.check(sec + "/bounds not exact (N=0<1)", reduce_fn(inst)[2] == 0)
# edge bound attains equality: one label, complete matching graph with all pairs compatible?
# B in {0,1} gives bound 0
inst = (2, [(0, 0, 1)], 2, [(0, 0, 1)])
red = reduce_fn(inst)
audit.check(sec + "/B=1 edge bound 0", len(red[3]) == 0 and red[2] == 1)

---------------------------------------------------------------- hypothesis

def hypothesis_runs(audit: Audit, reduce_fn=reduce_proof, extract_fn=extract_proof, max_examples=300):
from hypothesis import given, settings, strategies as st, HealthCheck

@st.composite
def loop_heavy(draw):
    """Many loops on both sides, few labels: stresses the loop filter and singleton A_p."""
    n1 = draw(st.integers(1, 4))
    n2 = draw(st.integers(1, 4))
    L = draw(st.integers(1, 2))

    def g(n):
        loops = draw(st.lists(st.tuples(st.integers(0, n - 1), st.integers(0, L - 1)), max_size=n * L, unique=True))
        other = draw(st.lists(st.tuples(st.integers(0, n - 1), st.integers(0, L - 1), st.integers(0, n - 1)),
                              max_size=4, unique=True))
        arcs = list(dict.fromkeys([(u, l, u) for u, l in loops] + other))
        return draw(st.permutations(arcs))
    return (n1, g(n1), n2, g(n2))

@st.composite
def label_and_direction(draw):
    """Antiparallel arcs, several labels per ordered pair, n1 > n2 allowed."""
    n1 = draw(st.integers(1, 4))
    n2 = draw(st.integers(1, 4))
    L = draw(st.integers(1, 3))

    def g(n):
        base = draw(st.lists(st.tuples(st.integers(0, n - 1), st.integers(0, n - 1)), max_size=3, unique=True))
        arcs = []
        for u, v in base:
            for l in draw(st.sets(st.integers(0, L - 1), min_size=1)):
                arcs.append((u, l, v))
                if draw(st.booleans()):
                    arcs.append((v, l, u))
        arcs = list(dict.fromkeys(arcs))[:7]
        return draw(st.permutations(arcs))
    return (n1, g(n1), n2, g(n2))

@st.composite
def planted_copy(draw):
    """G2 holds an injective image of G1 plus noise: the optimum is m1."""
    n1 = draw(st.integers(1, 4))
    n2 = draw(st.integers(n1, 5))
    L = draw(st.integers(1, 2))
    g1 = draw(st.lists(st.tuples(st.integers(0, n1 - 1), st.integers(0, L - 1), st.integers(0, n1 - 1)),
                       max_size=6, unique=True))
    perm = draw(st.permutations(list(range(n2))))[:n1]
    noise = draw(st.lists(st.tuples(st.integers(0, n2 - 1), st.integers(0, L - 1), st.integers(0, n2 - 1)),
                          max_size=3, unique=True))
    g2 = list(dict.fromkeys([(perm[u], l, perm[v]) for u, l, v in g1] + noise))
    return (n1, g1, n2, draw(st.permutations(g2)))

for name, strat in [("hyp-loop-heavy", loop_heavy()), ("hyp-label-direction", label_and_direction()),
                    ("hyp-planted", planted_copy())]:
    @settings(max_examples=max_examples, deadline=None, derandomize=True, database=None,
              suppress_health_check=list(HealthCheck))
    @given(strat)
    def run(inst):
        opt, w = attack(inst, audit, reduce_fn, extract_fn, family=name)
        if name == "hyp-planted":
            audit.check("hyp/planted optimum == m1", opt == len(inst[1]), inst)
    run()

---------------------------------------------------------------- symbolic overhead

def symbolic(audit: Audit):
import sympy as sp
m1, m2, N = sp.symbols("m1 m2 N", integer=True, nonnegative=True)
B = m1 * m2
# B(B-1)/2 - N(N-1)/2 == (B-N)(B+N-1)/2 (the proof's identity)
audit.check("symbolic/edge-bound identity",
sp.simplify(B * (B - 1) / 2 - N * (N - 1) / 2 - (B - N) * (B + N - 1) / 2) == 0)
Bs = sp.symbols("B", integer=True, nonnegative=True)
bound = Bs * (Bs - 1) / 2
for b in range(0, 2):
audit.check("symbolic/B in {0,1} -> 0", bound.subs(Bs, b) == 0)
# monotone: N <= B, N >= 0 => N(N-1)/2 <= B(B-1)/2 checked on a grid (integers, includes N=0)
audit.check("symbolic/monotone on grid",
all(n * (n - 1) // 2 <= b * (b - 1) // 2 for b in range(60) for n in range(b + 1)))

---------------------------------------------------------------- out-of-domain probe (duplicates)

def duplicate_probe(audit: Audit):
"""The proof assumes set semantics. With a duplicated target arc the backward
argument breaks (two candidates for one source arc are adjacent). This is
expected and is why the rule must reject duplicate arcs at entry; we assert
that the breakage happens, so the guard is necessary."""
inst = (2, [(0, 0, 1)], 2, [(0, 0, 1), (0, 0, 1)])
red = reduce_proof(inst)
w = omega(red[2], red[3])
opt = max(src_score(inst, c) for c in all_partial_injections(inst))
audit.check("domain/duplicate target arc breaks omega==opt (guard needed)", w == 2 and opt == 1, (w, opt))

---------------------------------------------------------------- main

def run_all(reduce_fn=reduce_proof, extract_fn=extract_proof, quick=False, stop_on_fail=False, out=sys.stdout):
audit = Audit(stop_on_fail=stop_on_fail)
rng = random.Random(SEED)
symbolic(audit)
examples(audit, reduce_fn, extract_fn)
special_cases(audit, reduce_fn, extract_fn)
duplicate_probe(audit)
fams = list(exhaustive_families())
samp = list(sampled_families(rng))
if quick:
r2 = random.Random(SEED + 1)
fams = r2.sample(fams, min(1500, len(fams)))
samp = r2.sample(samp, min(800, len(samp)))
for fam, inst in fams:
attack(inst, audit, reduce_fn, extract_fn, family=fam)
for fam, inst in samp:
attack(inst, audit, reduce_fn, extract_fn, family=fam)
hypothesis_runs(audit, reduce_fn, extract_fn, max_examples=60 if quick else 300)
return audit

def main():
quick = "--quick" in sys.argv
audit = run_all(quick=quick)
print(f"script B for MaximumCommonEdgeSubgraph -> MaximumClique/SimpleGraph/One (seed {SEED})")
audit.report()
c, f = audit.total()
print(f"TOTAL: {c} checks, {f} failures")
sys.exit(1 if f else 0)

if name == "main":
main()
```

@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Agentic Review Report

Subject: rule MaximumCommonEdgeSubgraph → MaximumClique<SimpleGraph, One> (new
src/rules/maximumcommonedgesubgraph_maximumclique.rs, its unit tests, paper entry and
bibliography entry, and a one-line disambiguation in src/rules/maximumcommonedgesubgraph_ilp.rs).

Verdict: approved. The four findings of the first pass are fixed, as listed below, and no findings
remain open.

Structural

  • Rule module and registration:
    • #[reduction(transform = upper_bound { num_vertices, num_edges })] covers both target
      parameters once.
    • The module is registered in src/rules/mod.rs, and canonical_rule_example_specs() is
      collected there.
    • The impl names MaximumCommonEdgeSubgraph literally.
  • Overhead:
    • The bounds num_arcs_1 * num_arcs_2 and B(B-1)/2 are correct upper bounds. The label and
      loop filters only remove vertices.
    • Measured on the canonical example: 5 ≤ 20 vertices and 6 ≤ 190 edges. On an empty source arc
      set: 0 vertices, 0 edges.
  • Target construction:
    • The graph goes through SimpleGraph::try_new, with errors mapped by target_construction.
    • MaximumClique::new gets exactly one weight per vertex, as in the sibling
      MaximumIndependentSet → MaximumClique rule.
  • Malformed input: raw graphs with duplicate arcs or out-of-range endpoints are rejected with a
    typed ReductionError. This guard is needed: with a repeated target arc the clique number would
    exceed the source optimum.
  • Extraction:
    • validate_target_witness runs once, then the assignments are decoded. Non-cliques and wrong
      lengths become an ExtractionError.
    • All rejection paths are tested.
  • Value map: #[aggregate_reduction(identity)] agrees with docs/src/design.md. Optimum values
    are equal on both sides and the map never needs to panic or clamp. Non-maximum cliques may decode
    to a larger source value, and a test documents this.
  • Shared files:
    • The ILP rule change only names the trait (<Self as ReduceTo<ILP<bool>>>::target_construction),
      which a second ReduceTo impl for the same source makes necessary.
    • No test was weakened, no generated exports are in the diff, and no core trait changed.
  • Semantics traced by hand on the issue instance:
    • 5 candidates, (0,0), (0,4), (1,1), (2,2), (3,3), with edges
      [(0,2),(0,3),(0,4),(2,3),(2,4),(3,4)].
    • Candidate 1 is isolated, and the clique {0,2,3,4} decodes to [0,1,3,4] with value 4.
  • Cross-check against both independent verification scripts: pred reduce builds identical
    targets on 670 shared instances, with 60 identical solve-and-extract round trips. The 100 additional
    round-trip instances are all correct; all of them have solutions.

Quality

  • The issue's validation items are present:
    • all 361 unlabelled looped graph pairs through two vertices, with every clique checked;
    • empty graphs, loops and the loop/non-loop filter, n1 > n2, antiparallel arcs, several labels on
      one pair, arbitrary labels and disconnected graphs;
    • rejection of non-cliques and wrong lengths;
    • parameter predictions;
    • non-optimal versus optimal witness scores;
    • the aggregate value map against brute force.
  • The closed-loop test solves both sides by brute force. The issue's optimum of 4 is confirmed by
    brute force, not hard-coded.
  • Labelled samples now cover every size pair up to 5×5, three times each, over 75 rounds, with
    every clique enumerated recursively and nothing skipped.
  • All 10 tests run in 0.08 s in total. Coverage of the rule file is 99.3%.

Feature test

These commands were run from a scratch directory:

  • pred list --rules MaximumCommonEdgeSubgraph shows the new edge.
  • pred create --example MaximumCommonEdgeSubgraph --to MaximumClique/SimpleGraph/One, then
    pred path … -o paths.json: there is one direct route.
  • pred reduce --via route.json then pred solve bundle.json gives Max(4), source solution
    [0,1,3,4] and target clique [true,false,true,true,true].
  • pred path … source.json measures num_vertices = 5, num_edges = 6.
  • pred reduce --aggregate then pred extract --value 4 gives 4.
  • pred extract --config '[true,true,false,false,false]' reports target witness is infeasible
    and exits 1.
  • A source with an empty arc set solves to Max(0) with every vertex unmatched.

Findings

No open findings. Resolved since the first pass:

  1. (Important) The labelled samples covered only sizes up to 4×4, and a candidate guard could skip
    instances without saying so. Sizes now run from 1 to 5 on both sides over 75 rounds, the guard
    is gone, and cliques are enumerated recursively.
  2. (Important) The rule statement in docs/paper/reductions.typ attributed the result to Nerem et
    al. It now cites their Theorem 6.3 as related work for weakly connected, simple, oriented,
    node-labelled digraphs, and states that the construction here is proved directly for the
    general case.
  3. (Minor) The paper's example hard-coded the isolated vertex and the $K_4$. Both are now computed
    from the fixture, with asserts.
  4. (Minor) A leftover commented-out import in the unit tests has been removed.

@codecov

codecov Bot commented Nov 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.20%. Comparing base (e7bacb3) to head (8fbd656).

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1237    +/-   ##
========================================
  Coverage   97.19%   97.20%            
========================================
  Files        1146     1148     +2     
  Lines      151558   151945   +387     
========================================
+ Hits       147308   147698   +390     
+ Misses       4250     4247     -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Rule] MaximumCommonEdgeSubgraph to MaximumClique

1 participant