Skip to content

Fix #1102: [Rule] MaxCut/One to Maximum2Satisfiability - #1227

Open
isPANN wants to merge 2 commits into
mainfrom
issue-1102-maxcut-max2sat
Open

isPANN wants to merge 2 commits into
mainfrom
issue-1102-maxcut-max2sat

Conversation

@isPANN

@isPANN isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1102

Summary

Adds the direct reduction MaxCut/SimpleGraph/One → Maximum2Satisfiability (Gramm, Hirsch, Niedermeier, Rossmanith, Discrete Applied Mathematics 130(2), 2003, Section 5).

  • Each vertex v becomes the variable x_v (literal v + 1); its truth value is the side of the cut.
  • Each edge (u, v), in edge order, becomes the clauses [u+1, v+1] and [-(u+1), -(v+1)].
  • A cut edge satisfies both clauses and an uncut edge exactly one, so satisfied(x) = m + cut(x) for every assignment. Maximum cuts are exactly the optimal assignments, and extraction returns the assignment unchanged.
  • Overhead (exact): num_vars = num_vertices, num_clauses = 2 * num_edges.

Changes

  • src/rules/maxcut_maximum2satisfiability.rs: the rule, built through Maximum2Satisfiability::try_new (errors kept via target_construction), with checked arithmetic for the literal v + 1 and the clause count 2m. Extraction validates once and returns the assignment. Canonical example: the 4-cycle 01, 12, 23, 30 plus the diagonal 02, witness [0, 1, 0, 1] (cut 4, 9 satisfied clauses).
  • src/unit_tests/rules/maxcut_maximum2satisfiability.rs: 14 tests covering:
    • the closed loop, target structure and parameter predictions, and the issue example's values;
    • the pointwise identity on every simple graph with up to 4 vertices, and preserved edge orientation;
    • empty, edgeless, isolated-vertex and disconnected graphs;
    • threshold NO cases on a single edge and a triangle;
    • wrong-length extraction, and both arithmetic boundaries.
  • src/rules/mod.rs: module and example registration.
  • docs/paper/reductions.typ, docs/paper/references.bib: a reduction-rule entry with the construction, both directions, extraction, and YES and NO examples. Every number in the examples is computed from the loaded fixture, including both optima by enumerating all assignments. Adds the gramm2003 citation.

Notes

  • The issue says SimpleGraph admits self-loops and parallel edges, but on current main SimpleGraph::try_new and its deserializer reject both. The construction is still correct for multigraphs (each loop contributes exactly one satisfied clause; each parallel occurrence contributes its own clause pair), and the verification below covers that case mathematically. The rule never receives such graphs, and a test confirms the source rejects them.
  • No aggregate value map is registered. AggregateReductionResult::extract_value is infallible, and the map target − m would have to panic or clamp on an arbitrary input. Witness extraction covers the optimization workflow; pred solve through the existing Maximum2Satisfiability → ILP route recovers the optimal cut.

Verification

  • make check and make paper pass locally; the paper shows no completeness warnings.
  • The new module has 100% line coverage.
  • pred reduce on 1,110 simple graphs produces exactly the targets of two independently written Python encoders (0 mismatches).
  • A verification certificate with full counts, the proof and both scripts is posted as a comment.

🤖 Generated with Claude Code

Each vertex becomes a Boolean variable and each edge (u, v) the clauses
(x_u OR x_v) and (NOT x_u OR NOT x_v), so every assignment satisfies
exactly m + cut clauses (Gramm, Hirsch, Niedermeier, Rossmanith 2003).
Extraction returns the assignment unchanged.

Includes unit tests, the canonical example (4-cycle plus a diagonal) and
the paper entry with the gramm2003 citation.

Fixes #1102

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

isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate

Verdict: VERIFIED. The construction was checked by two independently written scripts (a constructor and an adversary that saw only the proof), cross-compared on shared instances, and the Rust rule was compared target-for-target against both.

Type resolution

Source: MaxCut<SimpleGraph, One>: Value = Max<W::Sum>, W = One, <One as WeightElement>::Sum = i64 -> Max<i64>
Target: Maximum2Satisfiability: Value = Max<i64>
Max<i64> -> Max<i64>: witness-compatible

Redundancy: pred path MaxCut/SimpleGraph/One Maximum2Satisfiability reported no path before this rule, so the edge is novel (PASS).

Source checked: Gramm, Hirsch, Niedermeier, Rossmanith, ECCC TR00-037, Section 5 (p. 10) and Corollary 2 (p. 12): each edge of weight w becomes (w, {i, j}) and (w, {¬i, ¬j}), and a cut of weight k corresponds to satisfiable clause weight M + k. Unit weights give this rule.

Constructor: 2,280,976 checks, 0 failures

Section Checks
1 symbolic overhead (sympy) 19
2 exhaustive forward/backward (pointwise identity, every threshold) 2,183,304
3 extraction (every assignment, every optimal witness, wrong lengths) 81,028
4 size vs formula 2,448
5 structure (literal range, clause length, complementary pairs, special cases) 14,149
6 YES example 12
7 NO example 16

Instances: every simple graph with n ≤ 5 and every configuration; every simple graph with n = 6 (32,768 graphs × 64 configurations, via edge bitmasks); every edge multiset with loops/parallel edges for n ≤ 3 (mathematical extension only: SimpleGraph rejects these). Hypothesis: 2 strategies (random simple graphs n ≤ 10; multigraphs with all-zero/all-one/alternating/random assignments), derandomize=True, database=None, 1500 examples each; script seed 1102. Mutation sanity: a flipped literal sign and dropped loop clauses were both detected.

Adversary: 897,287 checks, 0 failures

Check group Count
Exact variable and clause overhead 69,834
Clause widths and literal ranges 34,917
Ordered per-edge clause pairs 257,876
Pointwise affine identity 208,856
Extraction with correct cut value 208,856
Optimum shift 2,149
Optimal target witnesses extract to optimal cuts 22,090
Threshold equivalence (incl. negative and oversized bounds) 22,861
Wrong-length extraction rejection 69,832
Existential feasibility checkers 16

Instances: all 1,100 simple graphs for n = 0..5 with every configuration; edge multisets with loops/multiplicity for n = 1..3, m = 1..4; all 32,768 six-vertex simple graphs on all-zero, all-one and alternating configurations. Hypothesis 6.155.7: 2 strategies (arbitrary ordered edge lists on up to 8 vertices and 30 edges; two disconnected blocks with a forced repeated self-loop), 400 examples each, seeds 846703 and 846704, database=None, deadline=None. Five deliberately wrong encoders/extractors were all detected.

The adversary found one wording error in an earlier draft of the proof (it claimed sat = cut = 0 whenever a graph has isolated vertices; counterexample n = 3, E = [(0,1)], x = [0,1,0] gives cut 1, sat 2). The proof was corrected to state that flipping an isolated vertex changes neither value and that only edgeless graphs have all values zero; the constructor gained a check for this, and the adversary re-verified.

Cross-comparison

Comparison Count Mismatches
Constructor vs adversary targets (exact, clause order and multiplicity) 1,349 0
Source/target values and extracted configurations 35,731 0
Threshold equivalence, k = -1..m+1 10,214 0
pred reduce (this PR) vs both scripts, legal SimpleGraph instances 1,110 0

Targets are compared for exact equality; no isomorphism or reordering is used. The remaining 239 shared instances contain loops or parallel edges and are compared between the scripts only.

Typst proof
#set page(width: 18cm, height: auto, margin: 1.5cm)
#set text(size: 10.5pt)
#set heading(numbering: "1.")

= MaxCut/SimpleGraph/One $arrow.r$ Maximum2Satisfiability

*Source.* Gramm, Hirsch, Niedermeier, Rossmanith, "Worst-case upper bounds for MAX-2-SAT with an
application to MAX-CUT", _Discrete Applied Mathematics_ 130(2):139--155, 2003,
DOI 10.1016/S0166-218X(02)00402-X; preprint ECCC TR00-037, Section 5 "Application to MAX-CUT"
(p. 10) and Corollary 2 (p. 12). Section 5 translates a graph $G=(V,E)$ into the 2-CNF formula with the
vertices as variables and, for each edge $(i,j)$ of weight $w$, the two clauses $(w,{i,j})$ and
$(w,{overline(i),overline(j)})$, and states that $G$ has a cut of weight $k$ iff the formula has
simultaneously satisfiable clauses of weight $M+k$, $M$ the total edge weight; every optimal
assignment translates into a maximum cut. The construction below is that one with all weights 1 (the
paper credits the reduction to Poljak and Tuza). Shen and Zhang, "Improving Exact Algorithms for
MAX-2-SAT" (DOI 10.1007/s10472-005-7036-z), Theorem 5.1, is reported to state the unit-weight case.

*Type resolution.* Source `MaxCut<SimpleGraph, One>`: `Value = Max<W::Sum>` with `W = One` and
`<One as WeightElement>::Sum = i64` (`src/types.rs`), so `Max<i64>`. Target `Maximum2Satisfiability`:
`Value = Max<i64>` (`src/models/formula/maximum_2_satisfiability.rs`). `Max<i64> -> Max<i64>`:
witness-compatible. *Redundancy:* `pred path MaxCut/SimpleGraph/One Maximum2Satisfiability` reports no
path, so the edge is novel (PASS).

== Definitions

A source instance is a graph $G=(V,E)$ with $V={0,dots,n-1}$ and an edge list
$E=(e_0,dots,e_(m-1))$, $e_j=(u_j,v_j)$ with $u_j,v_j in V$. A configuration is
$x in {0,1}^n$ and its value is $"cut"(x) = |{j : x_(u_j) != x_(v_j)}|$, counted over list positions.
The library's `SimpleGraph` rejects self-loops ($u_j=v_j$) and repeated pairs; the statements below are
proved for arbitrary lists, so they hold on that domain and on multigraphs alike.

A target instance is a number of variables $N$ and a list of clauses, each a list of two nonzero
integer literals $ell$ with $|ell| <= N$. Literal $ell > 0$ is true under $y in {0,1}^N$ iff
$y_(ell-1)=1$; literal $ell<0$ is true iff $y_(-ell-1)=0$. A clause is satisfied iff some literal in it
is true, and the value of $y$ is $"sat"(y)$, the number of satisfied clauses. Both problems maximize.

#strong[Theorem.] _For every source instance $G$ with $m$ edges, the instance $phi(G)$ built below
satisfies $"sat"(x) = m + "cut"(x)$ for every $x in {0,1}^n$. Hence for every integer $k$, $G$ has a cut
of value $>= k$ iff $phi(G)$ has an assignment satisfying $>= m+k$ clauses, the optima satisfy
$"OPT"(phi(G)) = m + "OPT"(G)$, and the identity map sends optimal assignments to maximum cuts._

== Construction

+ Set $N = n$: variable $x_v$ (library index $v$, literal $v+1$) stands for vertex $v$; $x_v = 1$
  places $v$ on side 1 of the cut, $x_v=0$ on side 0.
+ For each position $j = 0, dots, m-1$ of the edge list, with $e_j=(u_j,v_j)$, append in this order the
  clauses $P_j = (x_(u_j) or x_(v_j))$, encoded $[u_j+1, v_j+1]$, and
  $Q_j = (not x_(u_j) or not x_(v_j))$, encoded $[-(u_j+1), -(v_j+1)]$.
+ No other clause is created. A self-loop $u_j=v_j$ yields the repeated-literal clauses
  $[u_j+1,u_j+1]$ and $[-(u_j+1),-(u_j+1)]$; a repeated pair yields a repeated clause pair, kept with its
  multiplicity because the clauses form a list.

Every literal has magnitude $u_j+1 <= n = N$ and is nonzero, and every clause has two literals, so
$phi(G)$ is accepted by `Maximum2Satisfiability::try_new`.

== Overhead

#table(
  columns: 2,
  [*Target parameter*], [*Formula*],
  [`num_vars`], [`num_vertices`],
  [`num_clauses`], [`2 * num_edges`],
)
Step 1 creates one variable per vertex; step 2 creates exactly two clauses per edge-list position with
no branch, no omitted clause and no merging of duplicates, so both formulas are exact.

== Correctness

*Lemma (one edge).* For each $j$ and each $x$, the number of satisfied clauses among $P_j, Q_j$ equals
$1 + [x_(u_j) != x_(v_j)]$.

_Proof._ Write $a = x_(u_j)$, $b = x_(v_j)$. If $a != b$, one of $a,b$ is 1, so $P_j$ holds, and one of
them is 0, so $Q_j$ holds: two clauses, $= 1+1$. If $a = b = 1$, $P_j$ holds and both literals of
$Q_j$ are false: one clause. If $a=b=0$, $P_j$ fails and $Q_j$ holds: one clause. In both equal cases
the count is $1 = 1+0$. A self-loop always has $a=b$ and falls in the equal cases. $square$

*Identity.* Each clause of $phi(G)$ belongs to exactly one pair $(P_j,Q_j)$, so summing the lemma over
$j$ gives $"sat"(x) = sum_j (1 + [x_(u_j) != x_(v_j)]) = m + "cut"(x)$ for every $x in {0,1}^n$.

*($arrow.r.double$) Source YES gives target YES.* Let $x$ be a configuration with $"cut"(x) >= k$. Take
the target assignment $y = x$ (same length $n = N$). By the identity, $"sat"(y) = m + "cut"(x) >= m+k$,
so $phi(G)$ has an assignment satisfying at least $m+k$ clauses.

*($arrow.l.double$) Target YES gives source YES.* Let $y in {0,1}^N$ satisfy at least $m+k$ clauses of
$phi(G)$. Define the configuration $x$ by $x_v = y_v$ for $v in V$, which is well defined because
$N = n$. By the identity applied to $x$, $"cut"(x) = "sat"(y) - m >= (m+k) - m = k$, so $G$ has a cut of
value at least $k$.

*Optima.* The map $x |-> x$ is a bijection between source configurations and target assignments, and
by the identity it adds the constant $m$ to the value. A strictly increasing shift preserves the
argmax: $"OPT"(phi(G)) = m + "OPT"(G)$, and $y$ is optimal for $phi(G)$ iff $y$, read as a
configuration, is a maximum cut.

== Solution extraction

Given a target assignment $y$ of length $N = n$, return the configuration $x$ with $x_v = y_v$,
position for position (`true` $|->$ side 1). Its value is $"cut"(x) = "sat"(y) - m$; if $y$ is optimal,
$x$ is a maximum cut by the paragraph above. An assignment of any other length is not a target
solution and is rejected, as both `evaluate` functions reject wrong lengths.

== Special cases

- *Empty graph* ($n=m=0$): $N=0$, no clauses; the empty assignment has value $0 = 0+0$ and extracts to
  the empty configuration.
- *Isolated vertices*: a vertex $w$ in no edge gets a variable $x_w$ occurring in no clause, and $w$
  is an endpoint of no edge, so changing $x_w$ alone changes neither $"sat"$ nor $"cut"$. Both values
  need not be zero: with $n=3$, $E=((0,1))$ and $x=(0,1,0)$, $"cut"=1$ and $"sat"=2=1+1$. Each value of
  $x_w$ extracts to a valid position.
- *Edgeless graphs* ($m=0$, $n >= 1$): $phi(G)$ has $n$ variables and no clause, so every assignment
  has $"sat"=0="cut"=0+0$.
- *Disconnected graphs*: clauses of distinct components share no variable, and both values are sums
  over edge positions, so the identity holds component by component and adds up; no connectivity is
  used anywhere in the proof.
- *Self-loops*: contribute exactly one satisfied clause and never cross the cut (lemma, equal case).
- *Parallel edges*: each occurrence is its own position $j$ with its own pair; it contributes
  $1+[x_u != x_v]$ once per occurrence, matching the source, which also counts per position.

`SimpleGraph::try_new` (and deserialization, which calls it) rejects the last two cases, so the
library rule never meets them; the construction stays correct if the source domain admits them.

== YES example

Source: $n=4$, $E = (0,1),(1,2),(2,3),(3,0),(0,2)$ (the 4-cycle plus the diagonal $0 2$), $m=5$.

Target: $N=4$ and the ten clauses, in order,
$[1,2],[-1,-2],[2,3],[-2,-3],[3,4],[-3,-4],[4,1],[-4,-1],[1,3],[-1,-3]$, i.e.
$(x_0 or x_1)$, $(not x_0 or not x_1)$, $(x_1 or x_2)$, $(not x_1 or not x_2)$, $(x_2 or x_3)$,
$(not x_2 or not x_3)$, $(x_3 or x_0)$, $(not x_3 or not x_0)$, $(x_0 or x_2)$, $(not x_0 or not x_2)$.

Witness $x = y = (0,1,0,1)$. The cycle edges $01, 12, 23, 30$ cross, the diagonal $02$ does not:
$"cut"(x) = 4$. Per edge the satisfied counts are $2,2,2,2,1$, so $"sat"(y) = 9 = 5+4$.
All on one side, $x=(0,0,0,0)$: $"cut" = 0$ and $"sat"=5 = m$ (every $Q_j$ holds, every $P_j$ fails).

Over all 16 configurations the cut values are $0$ (twice: one side empty), $2$ (four times: vertex 1 or
3 alone), $3$ (eight times: vertex 0 or 2 alone, or the splits ${0,1}|{2,3}$, ${0,3}|{1,2}$) and $4$
(twice: ${0,2}|{1,3}$). Target values are these plus 5: $5,7,8,9$ with the same multiplicities.
So $"OPT"(G)=4$, $"OPT"(phi(G))=9$, the optimal assignments are exactly $(0,1,0,1)$ and $(1,0,1,0)$,
and extraction from either returns a cut of value 4.

== NO example

*Single edge (threshold check).* $n=2$, $E=((0,1))$, $m=1$; clauses $[1,2],[-1,-2]$. A single edge
cuts at most one edge, so no cut has value $>= 2$. The target values are $1,2,2,1$ for
$y = 00,01,10,11$, so its optimum is 2 and the corresponding threshold $m+2=3$ is unattainable. Both
sides are NO, as the theorem requires.

*Triangle (three vertices).* $n=3$, $E=(0,1),(1,2),(0,2)$, $m=3$; clauses
$[1,2],[-1,-2],[2,3],[-2,-3],[1,3],[-1,-3]$. Threshold $k=3$: a cut of value 3 would need all three
edges to cross, i.e. $x_0 != x_1 != x_2 != x_0$ over two values, impossible on an odd cycle; the cut
values are $0$ (twice) and $2$ (six times). For the target threshold $m+k=6$, all six clauses would
need to hold, i.e. both clauses of every pair, which by the lemma requires every edge to cross;
by the same parity argument some edge has equal endpoints and its pair contributes 1. The target values
are $3$ (twice) and $5$ (six times), maximum $5 < 6$. Both sides are NO.

== Evidence

The constructor script checks every claim: Section 1 (symbolic overhead and per-edge polynomial
identity), 2 (pointwise identity and every threshold, exhaustive over all simple graphs with $n <= 6$
and all configurations, all multigraphs with loops/parallel edges for $n <= 3$, two Hypothesis
strategies), 3 (extraction from every assignment and every optimal witness, wrong lengths rejected),
4 (sizes), 5 (literal range, clause length, complementary pairs, special cases), 6 and 7 (both examples,
number for number). Checked arithmetic for $2m$ and $v+1$ on machine integers is left to the Rust unit
tests, since Python integers are unbounded.
Constructor script
#!/usr/bin/env python3
"""Constructor verification: MaxCut/SimpleGraph/One -> Maximum2Satisfiability.

Construction (Gramm, Hirsch, Niedermeier, Rossmanith 2003, Section 5):
one Boolean variable x_v per vertex v; for every edge occurrence {u, v}, in edge
order, the two clauses [u+1, v+1] and [-(u+1), -(v+1)] (signed, 1-indexed
literals).  Extraction copies the assignment position for position.

The scripts mirror the library semantics:
  * MaxCut<SimpleGraph, One>::evaluate: length must equal num_vertices, value is
    the number of edges (u, v) with config[u] != config[v]  (Max<i64>);
  * Maximum2Satisfiability::evaluate: length must equal num_vars, value is the
    number of clauses with some literal true, literal l > 0 true iff
    assignment[l-1], l < 0 true iff not assignment[-l-1]  (Max<i64>);
  * Maximum2Satisfiability::try_new: num_vars <= i64::MAX, every literal is
    nonzero, not i64::MIN, |l| <= num_vars, and every clause has length 2.
  * SimpleGraph::try_new rejects self-loops and parallel edges; the
    construction is additionally checked on multigraphs (loops, parallel
    edges) because the proof claims it for them.

Sections (how-to-verify): 1 symbolic, 2 exhaustive forward/backward,
3 extraction, 4 size vs formula, 5 structure, 6 YES example, 7 NO example.
Ends with the line: TOTAL: <checks> checks, <failures> failures
"""

import itertools
import os
import random
import shutil
import sys

# ---------------------------------------------------------------- environment
os.environ.setdefault("HYPOTHESIS_STORAGE_DIRECTORY",
                      os.path.join(__import__("tempfile").gettempdir(), "maxcut_m2s_hypothesis"))
try:
    import sympy  # noqa: F401
    import hypothesis  # noqa: F401
except ImportError:  # re-run in an ephemeral environment that has both
    if os.environ.get("MAXCUT_M2S_REEXEC") != "1" and shutil.which("uv"):
        os.environ["MAXCUT_M2S_REEXEC"] = "1"
        os.execvp("uv", ["uv", "run", "--quiet", "--no-project",
                         "--with", "sympy==1.14.0", "--with", "hypothesis",
                         "python", "-I", os.path.abspath(__file__)] + sys.argv[1:])
    raise

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

SEED = 1102
random.seed(SEED)

COUNTS = {}
FAILURES = []
SECTION = None


def section(name):
    global SECTION
    SECTION = name
    COUNTS.setdefault(name, 0)
    print(f"--- {name}")


def check(cond, msg):
    COUNTS[SECTION] += 1
    if not cond:
        FAILURES.append(f"[{SECTION}] {msg}")
        if len(FAILURES) <= 20:
            print("FAIL:", SECTION, msg)


# ---------------------------------------------------------------- models
class EvalError(Exception):
    pass


def maxcut_eval(n, edges, config):
    """MaxCut<SimpleGraph, One>::evaluate."""
    if len(config) != n:
        raise EvalError("cut assignment length does not match the graph vertices")
    return sum(1 for (u, v) in edges if config[u] != config[v])


def literal_true(lit, assignment):
    var = abs(lit) - 1
    value = assignment[var] if var < len(assignment) else False
    return value if lit > 0 else not value


def m2s_valid(num_vars, clauses):
    """Maximum2Satisfiability::try_new acceptance."""
    if num_vars > 2**63 - 1:
        return False
    for c in clauses:
        for lit in c:
            if lit == 0 or lit == -(2**63) or abs(lit) > num_vars:
                return False
    return all(len(c) == 2 for c in clauses)


def m2s_eval(num_vars, clauses, assignment):
    """Maximum2Satisfiability::evaluate."""
    if len(assignment) != num_vars:
        raise EvalError("assignment length does not match the formula variables")
    return sum(1 for c in clauses if any(literal_true(l, assignment) for l in c))


# ---------------------------------------------------------------- reduction
def reduce(n, edges):
    clauses = []
    for (u, v) in edges:
        clauses.append([u + 1, v + 1])
        clauses.append([-(u + 1), -(v + 1)])
    return n, clauses


def extract(n, target_config):
    if len(target_config) != n:
        raise EvalError("target assignment length does not match the source vertices")
    return list(target_config)


# ---------------------------------------------------------------- instances
def all_pairs(n):
    return list(itertools.combinations(range(n), 2))


def simple_graphs(n):
    pairs = all_pairs(n)
    for mask in range(1 << len(pairs)):
        yield [pairs[i] for i in range(len(pairs)) if mask >> i & 1]


def configs(n):
    return [list(bits) for bits in itertools.product([False, True], repeat=n)]


def multigraphs(n, max_edges):
    """All edge multisets (loops and parallel edges allowed) of size <= max_edges,
    every ordering-insensitive multiset once, plus a reversed-endpoint variant."""
    slots = [(u, v) for u in range(n) for v in range(u, n)]
    for k in range(max_edges + 1):
        for combo in itertools.combinations_with_replacement(slots, k):
            yield list(combo)
            yield [(v, u) for (u, v) in combo]


def brute_opt(values):
    return max(values) if values else None


# ================================================================ section 1
section("1 symbolic overhead")
n_s, m_s = sp.symbols("num_vertices num_edges", integer=True, nonnegative=True)
i = sp.symbols("i", integer=True)
num_vars_formula = n_s
num_clauses_formula = 2 * m_s
# per-edge clause count summed over the edge list = 2 m
check(sp.simplify(sp.summation(2, (i, 1, m_s)) - num_clauses_formula) == 0,
      "sum_{e in E} 2 == 2*num_edges")
# one variable per vertex
check(sp.simplify(sp.summation(1, (i, 1, n_s)) - num_vars_formula) == 0,
      "sum_{v in V} 1 == num_vertices")
# per-edge contribution: [a or b] + [not a or not b] == 1 + [a xor b] on {0,1}
a, b = sp.symbols("a b")
pos = a + b - a * b          # indicator of (a or b) on 0/1 values
neg = 1 - a * b              # indicator of (not a or not b)
xor = a + b - 2 * a * b      # indicator of a != b
check(sp.expand(pos + neg - (1 + xor)) == 0, "polynomial identity pos+neg = 1+xor")
for av, bv in itertools.product([0, 1], repeat=2):
    check(pos.subs({a: av, b: bv}) == int(av or bv), f"pos indicator at {av},{bv}")
    check(neg.subs({a: av, b: bv}) == int((not av) or (not bv)), f"neg indicator at {av},{bv}")
    check(xor.subs({a: av, b: bv}) == int(av != bv), f"xor indicator at {av},{bv}")
# boolean-logic form: with a==b exactly one clause holds, with a!=b both hold
A, B = sp.symbols("A B")
check(sp.satisfiable(~((A | B) | (~A | ~B))) is False, "never both clauses false")
check(sp.satisfiable((A | B) & (~A | ~B) & sp.Equivalent(A, B)) is False,
      "a == b never satisfies both")
check(sp.satisfiable(sp.Xor(A, B) & ~((A | B) & (~A | ~B))) is False,
      "a != b always satisfies both")
# affine map is strictly increasing => maximizers coincide; threshold map k -> m+k
k_s = sp.symbols("k", integer=True)
check(sp.diff(m_s + k_s, k_s) == 1, "d(m+k)/dk = 1 > 0")

# ================================================================ section 2
section("2 exhaustive forward/backward")
EXH = {}  # (n, tuple(edges)) -> data, reused by section 3/4/5
for n in range(0, 6):
    for edges in simple_graphs(n):
        m = len(edges)
        tn, cl = reduce(n, edges)
        cs = configs(n)
        src = [maxcut_eval(n, edges, c) for c in cs]
        tgt = [m2s_eval(tn, cl, c) for c in cs]
        for c, s, t in zip(cs, src, tgt):
            check(t == m + s, f"identity n={n} E={edges} x={c}: sat={t}, m+cut={m + s}")
        opt_s, opt_t = brute_opt(src), brute_opt(tgt)
        check(opt_t == m + opt_s, f"optimum n={n} E={edges}")
        # decision form, every threshold k in [0, m+1]: cut>=k feasible <=> sat>=m+k feasible
        for k in range(0, m + 2):
            check((opt_s >= k) == (opt_t >= m + k), f"threshold k={k} n={n} E={edges}")
        EXH[(n, tuple(edges))] = (tn, cl, cs, src, tgt)

# n = 6, every simple graph (32768) and every configuration (64), via edge bitmasks.
# Per configuration and per possible edge slot, the indicator of each clause is
# computed by the clause semantics above; a graph is a mask over the 15 slots.
n6 = 6
pairs6 = all_pairs(n6)
cs6 = configs(n6)
cutmask, posmask, negmask = [], [], []
for c in cs6:
    cm = pm = nm = 0
    for j, (u, v) in enumerate(pairs6):
        _, cl = reduce(n6, [(u, v)])
        if maxcut_eval(n6, [(u, v)], c):
            cm |= 1 << j
        if literal_true(cl[0][0], c) or literal_true(cl[0][1], c):
            pm |= 1 << j
        if literal_true(cl[1][0], c) or literal_true(cl[1][1], c):
            nm |= 1 << j
    cutmask.append(cm)
    posmask.append(pm)
    negmask.append(nm)
pc = int.bit_count
n6_pairs = 0
for g in range(1 << len(pairs6)):
    m = pc(g)
    best_s = best_t = -1
    ok = True
    for ci in range(64):
        s = pc(g & cutmask[ci])
        t = pc(g & posmask[ci]) + pc(g & negmask[ci])
        ok &= (t == m + s)
        best_s = max(best_s, s)
        best_t = max(best_t, t)
    n6_pairs += 64
    COUNTS[SECTION] += 63  # 64 pointwise checks, the last one recorded below
    check(ok, f"n=6 identity graph mask {g}")
    check(best_t == m + best_s, f"n=6 optimum graph mask {g}")
print(f"    n=6: {1 << len(pairs6)} graphs x 64 configurations = {n6_pairs} pairs")

# multigraphs: loops and parallel edges (the construction is claimed for them)
MULTI = []
for n in range(1, 4):
    for edges in multigraphs(n, 4 if n <= 2 else 3):
        m = len(edges)
        tn, cl = reduce(n, edges)
        cs = configs(n)
        src = [maxcut_eval(n, edges, c) for c in cs]
        tgt = [m2s_eval(tn, cl, c) for c in cs]
        for c, s, t in zip(cs, src, tgt):
            check(t == m + s, f"multigraph identity n={n} E={edges} x={c}")
        check(max(tgt) == m + max(src), f"multigraph optimum n={n} E={edges}")
        MULTI.append((n, edges, tn, cl))

# hypothesis, strategy A: random simple graphs n <= 10, random assignment
@st.composite
def simple_graph_and_config(draw):
    n = draw(st.integers(min_value=0, max_value=10))
    pairs = all_pairs(n)
    chosen = draw(st.lists(st.sampled_from(pairs), unique=True)) if pairs else []
    edges = [(v, u) if draw(st.booleans()) else (u, v) for (u, v) in chosen]
    config = draw(st.lists(st.booleans(), min_size=n, max_size=n))
    return n, edges, config


# strategy B: multigraphs with loops/parallel edges, structured configurations
@st.composite
def multigraph_and_pattern(draw):
    n = draw(st.integers(min_value=1, max_value=8))
    edges = draw(st.lists(st.tuples(st.integers(0, n - 1), st.integers(0, n - 1)),
                          max_size=24))
    pattern = draw(st.sampled_from(["zeros", "ones", "alternating", "random"]))
    if pattern == "zeros":
        config = [False] * n
    elif pattern == "ones":
        config = [True] * n
    elif pattern == "alternating":
        config = [v % 2 == 1 for v in range(n)]
    else:
        config = draw(st.lists(st.booleans(), min_size=n, max_size=n))
    return n, edges, config


HSET = settings(max_examples=1500, derandomize=True, database=None, deadline=None,
                suppress_health_check=list(HealthCheck))


@HSET
@given(simple_graph_and_config())
def hyp_simple(data):
    n, edges, config = data
    tn, cl = reduce(n, edges)
    check(m2s_valid(tn, cl), f"hyp A valid {data}")
    s = maxcut_eval(n, edges, config)
    t = m2s_eval(tn, cl, config)
    check(t == len(edges) + s, f"hyp A identity {data}")
    check(maxcut_eval(n, edges, extract(n, config)) == t - len(edges), f"hyp A extract {data}")


@HSET
@given(multigraph_and_pattern())
def hyp_multi(data):
    n, edges, config = data
    tn, cl = reduce(n, edges)
    check(m2s_valid(tn, cl), f"hyp B valid {data}")
    s = maxcut_eval(n, edges, config)
    t = m2s_eval(tn, cl, config)
    check(t == len(edges) + s, f"hyp B identity {data}")
    check(len(cl) == 2 * len(edges), f"hyp B size {data}")


hyp_simple()
hyp_multi()

# ================================================================ section 3
section("3 extraction")
for (n, edges), (tn, cl, cs, src, tgt) in EXH.items():
    m = len(edges)
    opt_t, opt_s = max(tgt), max(src)
    for c, t in zip(cs, tgt):
        x = extract(n, c)
        check(len(x) == n and all(x[v] == c[v] for v in range(n)),
              f"position-for-position n={n} x={c}")
        check(maxcut_eval(n, edges, x) == t - m, f"extracted value n={n} E={edges} x={c}")
        if t == opt_t:  # every optimal target witness extracts to an optimal cut
            check(maxcut_eval(n, edges, x) == opt_s, f"optimal witness n={n} E={edges} x={c}")
    # wrong lengths are rejected, on both evaluations and extraction
    for bad in (n + 1, n - 1):
        if bad < 0:
            continue
        for fn in (lambda: extract(n, [False] * bad),
                   lambda: m2s_eval(tn, cl, [False] * bad),
                   lambda: maxcut_eval(n, edges, [False] * bad)):
            try:
                fn()
                check(False, f"length {bad} accepted for n={n}")
            except EvalError:
                check(True, "")
for (n, edges, tn, cl) in MULTI:
    tgt = [m2s_eval(tn, cl, c) for c in configs(n)]
    opt = max(tgt)
    for c, t in zip(configs(n), tgt):
        if t == opt:
            check(maxcut_eval(n, edges, extract(n, c)) == opt - len(edges),
                  f"multigraph optimal witness n={n} E={edges}")

# ================================================================ section 4
section("4 size vs formula")
# Derivation: the construction emits exactly one variable per vertex and a loop
# body of exactly two clauses per edge occurrence, with no branch, no omitted row
# and no normalization; so num_vars = n and num_clauses = 2m on the full domain.
for (n, edges), (tn, cl, *_rest) in EXH.items():
    check(tn == num_vars_formula.subs(n_s, n), f"num_vars n={n}")
    check(len(cl) == num_clauses_formula.subs(m_s, len(edges)), f"num_clauses E={edges}")
for (n, edges, tn, cl) in MULTI:
    check(tn == n and len(cl) == 2 * len(edges), f"multigraph size n={n} E={edges}")

# ================================================================ section 5
section("5 structure")
def structure_ok(n, edges, tn, cl):
    if not m2s_valid(tn, cl):
        return False
    if len(cl) != 2 * len(edges):
        return False
    for idx, (u, v) in enumerate(edges):
        p, q = cl[2 * idx], cl[2 * idx + 1]
        if p != [u + 1, v + 1] or q != [-(u + 1), -(v + 1)]:
            return False
        if q != [-l for l in p]:  # (MCC): complementary pair
            return False
        if not all(1 <= abs(l) <= n for l in p + q):
            return False
    return True


for (n, edges), (tn, cl, *_rest) in EXH.items():
    check(structure_ok(n, edges, tn, cl), f"structure n={n} E={edges}")
    # positive literals precisely in even clauses, negative in odd
    check(all((l > 0) == (j % 2 == 0) for j, c in enumerate(cl) for l in c), f"signs n={n}")
for (n, edges, tn, cl) in MULTI:
    check(structure_ok(n, edges, tn, cl), f"multigraph structure n={n} E={edges}")
    for idx, (u, v) in enumerate(edges):
        if u == v:  # loop: repeated literal in both clauses, still valid
            check(len(cl) == 2 * len(edges) and cl[2 * idx] == [u + 1, u + 1]
                  and cl[2 * idx + 1] == [-(u + 1), -(u + 1)],
                  f"loop clauses {edges}")
    # parallel edges: repeated clause pairs preserved with multiplicity
    for e in set(edges):
        mult = edges.count(e)
        check(cl.count([e[0] + 1, e[1] + 1]) >= mult, f"parallel multiplicity {edges}")
# special cases named in the proof
tn, cl = reduce(0, [])
check(tn == 0 and cl == [] and m2s_eval(0, [], []) == 0 and maxcut_eval(0, [], []) == 0,
      "empty graph: 0 vars, 0 clauses, value 0 = 0 + 0")
check(extract(0, []) == [], "empty graph extracts the empty configuration")
# isolated vertices in graphs with edges: flipping one changes neither value
for (n, edges), (tn, cl, cs, src, tgt) in EXH.items():
    touched = {v for e in edges for v in e}
    for w in (v for v in range(n) if v not in touched):
        for c, s, t in zip(cs, src, tgt):
            f = list(c)
            f[w] = not f[w]
            check(maxcut_eval(n, edges, f) == s and m2s_eval(tn, cl, f) == t,
                  f"isolated flip n={n} E={edges} w={w} x={c}")
iso_t = m2s_eval(*reduce(3, [(0, 1)]), [False, True, False])
iso_s = maxcut_eval(3, [(0, 1)], [False, True, False])
check(iso_s == 1 and iso_t == 2, "isolated vertex with an edge elsewhere: cut 1, sat 2")
for c in configs(3):  # edgeless: every value is zero
    check(m2s_eval(*reduce(3, []), c) == 0 == maxcut_eval(3, [], c), f"edgeless {c}")
# disconnected: two disjoint edges plus isolated vertex; value adds over components
dis = [(0, 1), (2, 3)]
for c in configs(5):
    t = m2s_eval(*reduce(5, dis), c)
    parts = m2s_eval(*reduce(5, [(0, 1)]), c) + m2s_eval(*reduce(5, [(2, 3)]), c)
    check(t == parts == 2 + maxcut_eval(5, dis, c), f"disconnected {c}")
for c in configs(2):  # self-loop contributes exactly one satisfied clause
    check(m2s_eval(*reduce(2, [(1, 1)]), c) == 1, f"loop baseline {c}")
for c in configs(2):  # parallel pair contributes 2*(1+[cut])
    check(m2s_eval(*reduce(2, [(0, 1), (0, 1)]), c) == 2 * (1 + (c[0] != c[1])), f"parallel {c}")

# ================================================================ section 6
section("6 YES example")
yes_n, yes_e = 4, [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]
ytn, ycl = reduce(yes_n, yes_e)
check(ytn == 4 and len(ycl) == 10, "4 variables, 10 clauses")
check(ycl == [[1, 2], [-1, -2], [2, 3], [-2, -3], [3, 4], [-3, -4],
              [4, 1], [-4, -1], [1, 3], [-1, -3]], f"clause list {ycl}")
w = [False, True, False, True]
check(maxcut_eval(yes_n, yes_e, w) == 4, "cut([0,1,0,1]) = 4")
crossing = [e for e in yes_e if w[e[0]] != w[e[1]]]
check(crossing == [(0, 1), (1, 2), (2, 3), (3, 0)], "four cycle edges cross")
per_edge = [m2s_eval(4, ycl[2 * j:2 * j + 2], w) for j in range(5)]
check(per_edge == [2, 2, 2, 2, 1], f"per-edge satisfied {per_edge}")
check(m2s_eval(ytn, ycl, w) == 9, "sat([0,1,0,1]) = 9 = 5 + 4")
yall = [(maxcut_eval(4, yes_e, c), m2s_eval(4, ycl, c)) for c in configs(4)]
check(max(s for s, _ in yall) == 4, "MaxCut optimum 4")
check(max(t for _, t in yall) == 9, "Max2SAT optimum 9")
check(m2s_eval(4, ycl, [False] * 4) == 5 and m2s_eval(4, ycl, [True] * 4) == 5,
      "all on one side: target 5 = m")
check(maxcut_eval(4, yes_e, extract(4, w)) == 4, "extraction returns the cut of value 4")
opt_witnesses = [c for c in configs(4) if m2s_eval(4, ycl, c) == 9]
check(sorted(map(tuple, opt_witnesses)) == [(False, True, False, True), (True, False, True, False)],
      f"exactly two optimal assignments {opt_witnesses}")
dist = sorted(t for _, t in yall)
check(dist == [5] * 2 + [7] * 4 + [8] * 8 + [9] * 2, f"value distribution {dist}")

# ================================================================ section 7
section("7 NO example")
# issue: single edge, threshold cut >= 2 / target >= 3
no_n, no_e = 2, [(0, 1)]
ntn, ncl = reduce(no_n, no_e)
check(ncl == [[1, 2], [-1, -2]], "single-edge clauses")
check(all(maxcut_eval(2, no_e, c) < 2 for c in configs(2)), "no cut of value >= 2")
check(all(m2s_eval(ntn, ncl, c) < 3 for c in configs(2)), "no assignment with >= 3 satisfied")
check(max(m2s_eval(ntn, ncl, c) for c in configs(2)) == 2, "target optimum 2")
# >= 3 vertices: triangle, threshold cut >= 3 / target >= 6
tri_e = [(0, 1), (1, 2), (0, 2)]
ttn, tcl = reduce(3, tri_e)
tri = [(maxcut_eval(3, tri_e, c), m2s_eval(ttn, tcl, c)) for c in configs(3)]
check(all(s < 3 for s, _ in tri), "triangle: no cut of value >= 3")
check(all(t < 6 for _, t in tri), "triangle: no assignment with >= 6 satisfied")
check(max(s for s, _ in tri) == 2 and max(t for _, t in tri) == 5, "triangle optima 2 and 5")
check(sorted(t for _, t in tri) == [3, 3, 5, 5, 5, 5, 5, 5], "triangle value distribution")
for c in configs(3):  # some edge of an odd cycle never crosses: one per-edge pair gives 1
    per = [m2s_eval(3, tcl[2 * j:2 * j + 2], c) for j in range(3)]
    check(1 in per, f"triangle: some edge pair contributes 1 at {c}")

# ================================================================ audit
print()
print("CHECK AUDIT")
for name, cnt in COUNTS.items():
    print(f"  {name}: {cnt}")
print()
print("CLAIM MAP (proof claim -> section)")
for claim, sec in [
    ("overhead num_vars = n, num_clauses = 2m", "1, 4"),
    ("per-edge lemma: 1 + [x_u != x_v] satisfied", "1, 5"),
    ("pointwise identity sat = m + cut", "2 (n<=6 exhaustive, multigraphs, hypothesis)"),
    ("(=>) cut >= k gives sat >= m + k; (<=) converse", "2 (thresholds)"),
    ("optimum m + OPT", "2"),
    ("extraction position-for-position, optimal -> optimal", "3"),
    ("wrong lengths rejected", "3"),
    ("literals in 1..n, clauses of length 2, complementary pairs", "5"),
    ("empty, edgeless, disconnected, loops, parallel edges", "2, 5"),
    ("flipping an isolated vertex changes neither value", "5"),
    ("YES example numbers", "6"),
    ("NO example numbers", "7"),
    ("checked arithmetic on 2m and v+1", "untestable here: Python integers are unbounded; Rust unit tests"),
]:
    print(f"  {claim}: {sec}")
print(f"seed={SEED}; hypothesis: derandomize=True, max_examples=1500 per strategy, 2 strategies")
total = sum(COUNTS.values())
print(f"TOTAL: {total} checks, {len(FAILURES)} failures")
sys.exit(1 if FAILURES else 0)
Adversary script: attack.py
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = ["hypothesis==6.155.7"]
# ///
"""Run with `uv run --script attack.py` (beside construction.py and instances.py). No constructor access."""
import json
import os
import tempfile
from collections import Counter
from itertools import product
from pathlib import Path

os.environ.setdefault("HYPOTHESIS_STORAGE_DIRECTORY", str(Path(tempfile.gettempdir()) / "maxcut-adversary"))
from hypothesis import given, seed, settings, strategies as st
from construction import (reduce, extract_solution, source_value, target_value,
                          source_feasible, target_feasible)

SEED = 846703
COUNTS = Counter()
FAMILIES = Counter()


def check(condition, section, detail):
    COUNTS[section] += 1
    if not condition:
        raise AssertionError((section, detail))


from instances import simple_graphs, shared_instances


def audit(n, edges, target=None, extractor=extract_solution, exhaustive=True):
    if target is None:
        target = reduce(n, edges)
    FAMILIES["instances"] += 1
    FAMILIES["empty"] += n == 0
    FAMILIES["edgeless"] += not edges
    FAMILIES["loops"] += any(u == v for u, v in edges)
    FAMILIES["parallel"] += len({tuple(sorted(e)) for e in edges}) < len(edges)
    active = {v for edge in edges for v in edge}
    FAMILIES["isolated_vertices"] += len(active) < n
    reachable = {0} if n else set()
    while True:
        expanded = reachable | {v for edge in edges if reachable.intersection(edge) for v in edge}
        if expanded == reachable:
            break
        reachable = expanded
    FAMILIES["disconnected"] += n > 1 and len(reachable) < n
    context = (n, edges)
    check(target["num_vars"] == n, "overhead", context)
    check(len(target["clauses"]) == 2 * len(edges), "overhead", context)
    check(all(len(c) == 2 and all(0 < abs(lit) <= n for lit in c)
              for c in target["clauses"]), "well_formed", context)
    for j, (u, v) in enumerate(edges):
        p, q = target["clauses"][2*j:2*j+2]
        check(p == [u + 1, v + 1] and q == [-lit for lit in p], "edge_pairs", (context, j))
    configs = list(product((0, 1), repeat=n)) if exhaustive else [
        (0,) * n, (1,) * n, tuple(v % 2 for v in range(n)), tuple(1 - v % 2 for v in range(n))]
    cuts, sats = [], []
    for x in configs:
        cut = source_value(n, edges, x)
        sat = target_value(target, x)
        cuts.append(cut)
        sats.append(sat)
        check(sat == len(edges) + cut, "pointwise", (context, x, cut, sat))
        extracted = extractor(n, x)
        check(extracted == x and source_value(n, edges, extracted) == sat - len(edges),
              "extraction", (context, x, extracted))
    if exhaustive:
        check(max(sats) == len(edges) + max(cuts), "optimum", context)
        for x, value in zip(configs, sats):
            if value == max(sats):
                check(source_value(n, edges, extractor(n, x)) == max(cuts), "optimal_extraction", (context, x))
        for k in [-10**30, *range(-1, len(edges) + 2), 10**30]:
            check(any(c >= k for c in cuts) == any(s >= len(edges) + k for s in sats),
                  "thresholds", (context, k))
    for wrong in [(0,) * (n + 1)] + ([(0,) * (n - 1)] if n else []):
        try:
            extractor(n, wrong)
        except ValueError:
            check(True, "invalid_witness", context)
        else:
            check(False, "invalid_witness", (context, wrong))


@st.composite
def edge_lists(draw):
    n = draw(st.integers(0, 8))
    edges = [] if n == 0 else draw(st.lists(st.tuples(st.integers(0, n-1), st.integers(0, n-1)), max_size=30))
    return n, edges


@st.composite
def disconnected_multigraphs(draw):
    left = draw(st.integers(1, 4))
    right = draw(st.integers(1, 4))
    pair = st.one_of(st.tuples(st.integers(0, left-1), st.integers(0, left-1)),
                     st.tuples(st.integers(left, left+right-1), st.integers(left, left+right-1)))
    edges = draw(st.lists(pair, max_size=15))
    v = draw(st.integers(0, left-1))
    # Force loops and multiplicity while the two components stay disconnected.
    return left + right, edges + [(v, v), (v, v)]


@seed(SEED)
@settings(max_examples=400, database=None, deadline=None)
@given(edge_lists())
def arbitrary_property(instance):
    audit(*instance)


@seed(SEED + 1)
@settings(max_examples=400, database=None, deadline=None)
@given(disconnected_multigraphs())
def disconnected_property(instance):
    audit(*instance)


def mutation_checks():
    n, edges = 4, [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]
    mutations = []
    t = reduce(n, edges)
    t["clauses"][1][0] *= -1
    mutations.append((n, edges, t, extract_solution))
    mutations.append((1, [(0, 0)], {"num_vars": 1, "clauses": []}, extract_solution))
    mutations.append((2, [(0, 1), (0, 1)], reduce(2, [(0, 1)]), extract_solution))
    mutations.append((3, [(0, 1)], reduce(2, [(0, 1)]), extract_solution))
    mutations.append((n, edges, reduce(n, edges), lambda n, y: tuple(reversed(y))))
    for args in mutations:
        try:
            audit(*args)
        except AssertionError:
            pass
        else:
            raise AssertionError("attack failed to detect a deliberately wrong target/extractor")
    return len(mutations)


def main():
    import unittest
    from test_attack import ProofExamples
    result = unittest.TextTestRunner().run(unittest.defaultTestLoader.loadTestsFromTestCase(ProofExamples))
    if not result.wasSuccessful():
        raise SystemExit(1)
    killed = mutation_checks()
    COUNTS.clear()
    FAMILIES.clear()
    shared_count = 0
    for n, edges in shared_instances():
        audit(n, edges)
        shared_count += 1
    # Identity risk probes on every six-vertex simple graph, four extremal configurations.
    six_count = 0
    for n, edges in simple_graphs(6):
        if n == 6:
            audit(n, edges, exhaustive=False)
            six_count += 1
    arbitrary_property()
    disconnected_property()
    # Independent existential checkers, including thresholds outside the value range.
    for n, edges in [(0, []), (2, [(0, 1)]), (3, [(0, 1), (1, 2), (0, 2)])]:
        for k in range(-1, len(edges) + 3):
            check(source_feasible(n, edges, k) == target_feasible(reduce(n, edges), len(edges) + k),
                  "existential_checkers", (n, edges, k))
    summary = {"sections": dict(COUNTS), "families": dict(FAMILIES),
               "shared_instances": shared_count, "six_vertex_extreme_instances": six_count,
               "hypothesis_examples_per_strategy": 400, "seeds": [SEED, SEED + 1],
               "mutations_detected": killed, "checks": sum(COUNTS.values()), "failures": 0}
    print(json.dumps(summary, indent=2))
    Path(__file__).with_name("results.json").write_text(json.dumps(summary, indent=2) + "\n")


if __name__ == "__main__":
    main()
Adversary script: construction.py
"""Independent implementation of the definitions in proof.typ; no constructor import."""


def reduce(num_vertices, edges):
    clauses = []
    for edge in edges:
        positive = [vertex + 1 for vertex in edge]
        clauses.extend((positive, [-literal for literal in positive]))
    return {"num_vars": num_vertices, "clauses": clauses}


def extract_solution(num_vertices, assignment):
    result = tuple(assignment)
    if len(result) != num_vertices or any(bit not in (0, 1) for bit in result):
        raise ValueError("expected one Boolean per vertex")
    return result


def source_value(num_vertices, edges, assignment):
    assignment = extract_solution(num_vertices, assignment)
    side = {v for v, bit in enumerate(assignment) if bit}
    return sum((u in side) != (v in side) for u, v in edges)


def target_value(target, assignment):
    assignment = extract_solution(target["num_vars"], assignment)
    # A clause fails precisely when every variable has its literal's false value.
    return sum(not all(assignment[abs(lit) - 1] == (lit < 0) for lit in clause)
               for clause in target["clauses"])


def source_feasible(num_vertices, edges, threshold):
    from itertools import product
    return any(source_value(num_vertices, edges, x) >= threshold
               for x in product((0, 1), repeat=num_vertices))


def target_feasible(target, threshold):
    from itertools import product
    return any(target_value(target, y) >= threshold
               for y in product((0, 1), repeat=target["num_vars"]))
Adversary script: instances.py
"""Deterministic shared simple-graph and multigraph inputs."""
from itertools import combinations, combinations_with_replacement


def simple_graphs(max_n=5):
    for n in range(max_n + 1):
        possible = list(combinations(range(n), 2))
        for mask in range(1 << len(possible)):
            yield n, [edge for i, edge in enumerate(possible) if mask & (1 << i)]


def shared_instances():
    yield from simple_graphs()
    # Preserve noncanonical endpoint and edge order, and explicitly include multigraphs.
    yield 4, [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)]
    yield 5, [(4, 3), (1, 0)]
    for n in range(1, 4):
        possible = list(combinations_with_replacement(range(n), 2))
        for m in range(1, 5):
            for edges in combinations_with_replacement(possible, m):
                yield n, list(edges)

@isPANN

isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Agentic Review Report

PR: #1227 — MaxCut/SimpleGraph/One → Maximum2Satisfiability (fixes #1102)
Head reviewed: a42018be
Verdict: No Critical or Important findings. Two Minor items, both optional.

(a) Structural and semantic

Check Result
Generated exports (reduction_graph.json, problem_schemas.json, examples.json) in the diff None. Only 5 files change: the rule, its tests, src/rules/mod.rs, reductions.typ and references.bib
Unrelated edits, core-trait or macro changes None
#[reduction(transform = exact { ... })] Covers both target parameters (num_vars, num_clauses) exactly once
Registered in src/rules/mod.rs Yes: the module and canonical_rule_example_specs() are both registered
extract_solution Calls validate_target_solution once, then returns the assignment unchanged. Wrong lengths produce ExtractionError, and this is tested for lengths 3, 5 and 0
Error contract Target construction goes through Maximum2Satisfiability::try_new and is mapped with target_construction. No Result<_, String>
Numeric contract Checked v + 1 → i64 and checked 2m, with tests at both overflow boundaries
Closed-loop test test_maxcut_to_maximum2satisfiability_closed_loop uses assert_optimization_round_trip_from_optimization_target
Paper reduction-rule("MaxCut", "Maximum2Satisfiability", ...) is present. All example numbers come from load-example, including both optima, which are computed by enumeration. gramm2003 is added to the bib with the issue's metadata

Hand trace on the issue example. The source is the 4-cycle plus the diagonal 02, so n = 4 and m = 5. reduce_to emits the pairs [u+1, v+1] and [-(u+1), -(v+1)] in edge order. That gives the ten clauses listed in the issue, matching test_..._structure. Counts: num_vars = 4 = n and num_clauses = 10 = 2m, which agree with the transform block.

Per edge, a crossing edge satisfies both clauses and a non-crossing edge exactly one. So sat(x) = m + cut(x) holds for every x. The witness [0,1,0,1] gives cut 4 and 9 satisfied clauses. All-false and all-true give 5. Extraction is the identity, which is correct because the variable and vertex indices coincide.

Paper proof. The proof is sound. It gives the per-edge case split, then the pointwise identity, then both threshold directions, then the bijection that shifts the objective by the constant m. It also states correctly that SimpleGraph rejects loops and parallel edges on current main, which differs from the issue text. The PR explains this and tests the rejection through both try_new and deserialization.

Issue compliance.

  • Construction, 1-indexed signed literals, direct extraction and the overhead formulas all match the issue body and its comments.
  • The issue example is the canonical example and is used in the tests. Its optima (4 and 9, and the two optimal assignments) are confirmed by brute force, not hardcoded.
  • The single-edge NO-threshold case is tested, and the PR adds a triangle case on top of it.
  • The PR leaves out an aggregate value map and gives a reason (extract_value is infallible, so target − m would have to clamp). Witness extraction covers the optimization workflow, so this is acceptable.

(b) Quality

  • The tests are strong. They check the pointwise identity on every assignment of every simple graph with n ≤ 4, which is stronger than optimum-only checks. They also cover edge orientation, empty, edgeless, isolated-vertex and disconnected graphs, NO thresholds, extraction rejection and arithmetic boundaries.
  • There is a 5-vertex instance: the disconnected-graph test runs the brute-force round trip on n = 5.
  • The tests check behavior rather than re-deriving the formula: they compare evaluate on both models instead of recomputing clause counts.
  • No duplicated helper logic. The rule is small and readable.
  • problemreductions-cli/ is unchanged, so the HCI checks do not apply.
  • cargo test --lib --features example-db maxcut_maximum2satisfiability: 14 passed.

(c) Feature test (pred built from this PR head)

  • pred list --rules MaxCut lists MaxCut/SimpleGraph/One → Maximum2Satisfiability.
  • pred show MaxCut/SimpleGraph/One shows the outgoing edge with num_clauses = 2 * num_edges, num_vars = num_vertices.
  • pred create --example MaxCut/SimpleGraph/One --to Maximum2Satisfiability -o inst.json, then pred inspect, then pred solve (default and --solver brute-force): all give Max(4) with witness [false,true,false,true].
  • pred path MaxCut/SimpleGraph/One Maximum2Satisfiability finds a single one-step path.
  • pred reduce inst.json --via route.json -o bundle.json: the bundle target has exactly the ten issue clauses, in order.
  • pred solve bundle.json gives source Max(4) with intermediate Max(9). This was checked with both the ILP route and brute force.
  • I made my own 6-vertex, 7-edge instance (0-1,1-2,2-3,3-4,4-0,0-2,1-5):
    • pred path ... g6.json measures num_vars = 6, num_clauses = 14, matching the formulas.
    • Solving the bundle gives cut Max(6) and target Max(13) = 7 + 6. This agrees with solving the source directly.
  • pred create MaxCut/SimpleGraph/One --graph 0-0,0-1 is rejected with edge (0, 0) is a self-loop, as the paper states.
  • pred evaluate inst.json --config '[false,true,false,true]' gives Max(4), matching the paper's pred-commands line.

Findings

Critical: none.

Important: none.

Minor

  1. src/unit_tests/rules/maxcut_maximum2satisfiability.rs:113: the exhaustive pointwise-identity sweep stops at n ≤ 4. The issue's validation method asks for every simple graph through five vertices. Going to n = 5 is 1,024 graphs × 32 assignments, still well under a second. Suggested fix: change 0..=4usize to 0..=5usize so the in-repo tests match the issue's validation plan.
  2. docs/paper/reductions.typ:20051: the sibling MaxCut rule entries are each preceded by a // Source → Target (#issue) marker comment, for example // MaxCut → MinimumMatrixCover (#925, ...). This entry has none. Suggested fix: add // MaxCut → Maximum2Satisfiability (#1102) above #let mc_m2s = load-example(...) so the entry is easy to find.

@codecov

codecov Bot commented Nov 2, 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 (a919268).

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1227    +/-   ##
========================================
  Coverage   97.19%   97.20%            
========================================
  Files        1146     1148     +2     
  Lines      151558   151798   +240     
========================================
+ Hits       147308   147548   +240     
  Misses       4250     4250            

☔ 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.

Address review: the pointwise sat = m + cut test now covers every simple
graph through five vertices, as in the issue's validation plan, and the
paper entry carries the source/target marker comment used by its
neighbours.

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

isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Both Minor findings are addressed in a919268:

  • Minor 1: test_maxcut_to_maximum2satisfiability_pointwise_identity_on_all_small_graphs now covers every simple graph with n ≤ 5 (every assignment of each), matching the issue's validation plan. It still runs in well under a second.
  • Minor 2: the paper entry now starts with the marker comment // MaxCut → Maximum2Satisfiability (#1102, Gramm et al. 2003), like its neighbours.

make check and make paper pass locally on the new head. No change to the rule itself.

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] MaxCut/One to Maximum2Satisfiability

1 participant