Skip to content

Fix #1103: ThreeDimensionalMatching to ExactCoverBy3Sets - #1226

Open
isPANN wants to merge 2 commits into
mainfrom
issue-1103-3dm-to-x3c
Open

isPANN wants to merge 2 commits into
mainfrom
issue-1103-3dm-to-x3c

Conversation

@isPANN

@isPANN isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1103

Summary

  • Adds the direct tagged-disjoint-union reduction ThreeDimensionalMatching -> ExactCoverBy3Sets (src/rules/threedimensionalmatching_exactcoverby3sets.rs). Triple (w, x, y) over W = X = Y = {0, ..., q-1} becomes the subset {w, q + x, 2q + y} of {0, ..., 3q-1}; the selection vector is shared, so extraction is the identity after validating the target witness.
  • Exact transform: universe_size = 3 * universe_size, num_subsets = num_triples. Computing 3q is checked and overflow returns a typed ReductionError; target construction goes through ExactCoverBy3Sets::try_new (now pub(crate)) so construction failures stay ReductionError::Construction.
  • Canonical example: the issue's instance (q = 3, five triples, the diagonal is the unique exact cover).
  • Paper entry in docs/paper/reductions.typ with construction, both directions of correctness, extraction, the worked YES example with a uniqueness argument, and a NO example (an unused X-coordinate).
  • Unit tests in src/unit_tests/rules/threedimensionalmatching_exactcoverby3sets.rs: closed loop on the issue example; every one of the 256 families of triples for q = 2, compared with the source on every selection vector (175 feasible, derived independently); empty q = 0; duplicate triples; an unused coordinate; equal numeric coordinates across domains; extraction rejecting non-covers and wrong lengths; aggregate identity; and the 3q overflow boundary.

Verification

  • make check and make paper pass locally.
  • The verification certificate (constructor, independent adversary, and cross-comparison against pred reduce) follows as a comment.

Map each triple (w, x, y) to the subset {w, q + x, 2q + y} of the tagged
disjoint union {0, ..., 3q-1}; the selection vector is shared, so
extraction is the identity after validating the target witness. Exact
transform: universe_size = 3 * universe_size, num_subsets = num_triples.
Includes the canonical example, unit tests and the paper entry.
@isPANN

isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate

Rule: ThreeDimensionalMatching (variant {}) -> ExactCoverBy3Sets (variant {}), issue #1103.

Verdict: VERIFIED. The constructor and the adversary both pass with 0 failures. They agree on every shared instance. Both also agree with the Rust rule's output on 102 instances.

All scripts are included below. They were run on 2026-11-02 with Python 3.12, sympy and hypothesis==6.170.14, and each exited 0. cross_compare.py takes the path of a pred binary built from this branch.

1. Type resolution

TYPE RESOLUTION:
  Source: ThreeDimensionalMatching, no generics -> type Value = crate::types::Or
          (src/models/set/three_dimensional_matching.rs:133)
  Target: ExactCoverBy3Sets, no generics -> type Value = crate::types::Or
          (src/models/set/exact_cover_by_3_sets.rs:201)
  Or -> Or: witness-compatible. Full-domain compatibility: PASSED

2. Proof

  • Paper: docs/paper/reductions.typ at reduction-rule("ThreeDimensionalMatching", "ExactCoverBy3Sets" (line 19852). It has the construction, the (⇒) and (⇐) arguments, solution extraction, a YES example (the fixture, plus a uniqueness argument) and a NO example (an unused X-coordinate).
  • Adversary input: the adversary was given only proof.md, a plain-text statement of the same proof.

3. Constructor: constructor.py

Per-section checks, 0 failures, 204,513 in total:

Section Checks What it covers
1 symbolic 128 overhead 3q and t with sympy, q, t in 0..7
2 forward/backward 96,808 source value == target value on every selection vector; wrong-length vectors are errors on both sides
3 extraction 93,056 an exact cover extracts to the same vector, which solves the source; every non-cover is rejected
4 size 3,788 measured target universe and subset count equal 3q and t
5 structure 10,727 the target is a valid X3C instance; subset l is {w, q+x, 2q+y}
6 YES 3 the issue example: subsets match exactly, and (1,1,1,0,0) is the only solution
7 NO 3 the paper's NO example: element 5 is uncovered, and neither side has a solution

Instance families:

  • Exhaustive:
    • q = 0 with no triples.
    • q = 1 and q = 2: every multiset of 0 to 5 triples, each with all 2^t selection vectors.
  • Random: 600 instances with q ∈ {3, 4} and up to 8 triples, half of them with a planted perfect matching. Seed random.Random(1103).

4. Adversary: adversary.py

The adversary was written independently from proof.md alone. It has its own reduction, extraction and checkers, and imports nothing from the constructor. It has a second, independent target checker (each element covered exactly once).

Per-section checks, 0 failures, 761,096 in total:

Section Checks
1 exhaustive (q ≤ 2, up to 6 triples, multisets; ordered lists for small sizes) 438,266
2 hypothesis random (q 1..4, 0..9 triples, all vectors) 255,324
2 hypothesis planted (q 1..5, planted matching + noise, bit flips) 14,046
2 hypothesis adversarial (all-zero, all-one, alternating) 7,800
3 target validity and overhead 43,286
4 malformed vectors and invalid targets 2,170
5 YES/NO examples 204

Hypothesis settings: hypothesis 6.170.14 with max_examples=600, derandomize=True (a fixed seed), database=None and deadline=None. There are 4 test functions built on 2 independent generators.

Mutation check: the adversary swapped in a wrong reduction, {a, q+b, 2q+b}. Its exhaustive section reported 17,604 failures, so the harness can fail.

5. Cross-comparison: cross_compare.py

Total 168,570 checks, 0 disagreements.

Comparison Checks
A: constructor.reduce vs adversary.reduce, identical targets on shared instances 1,794 instances
A: constructor vs adversary source checkers, every selection vector 83,286
A: constructor vs adversary target checkers, every selection vector 83,286
B: Rust pred reduce (direct route from pred path) == constructor target 102
B: Rust pred reduce == adversary target 102

Shared instances in A:

  • Exhaustive: q ≤ 2, multisets of up to 5 triples.
  • Random: 500 instances with q ∈ 3..5 and up to 9 triples. Seed random.Random(20261101).

Instances in B: 100 random instances (q ∈ 1..6, up to 10 random triples, every second one with a planted perfect matching; seed random.Random(1103)), plus the YES and NO examples.

6. Where each claim of the proof is checked

Claim Checked by
The target is a valid X3C instance: universe divisible by 3, three distinct in-range elements, even for equal coordinates constructor §5; adversary §3; Rust test tags_equal_coordinates
tau(d, v) = dq + v is a bijection, so subsets meet iff triples share a coordinate implied by (⇒)/(⇐) agreement on every vector: constructor §2, adversary §1–2
(⇒) a perfect matching gives an exact cover constructor §2, §3; adversary §1–2 (planted)
(⇐) an exact cover gives a perfect matching constructor §2, §3; adversary §1–2
Wrong-length vectors are rejected on both sides constructor §2 (bad vectors); adversary §4; Rust test extraction_rejects_non_covers
Duplicate triples keep their positions constructor/adversary exhaustive multisets; Rust test duplicate_triples
Extraction is the identity on exact covers and rejects the rest constructor §3; adversary §1; Rust tests
Overhead: universe 3q and t subsets, exact constructor §1, §4; adversary §3; Rust assert_parameter_predictions; parameter_formula_validation.rs
YES example constructor §6; adversary §5; Rust test closed_loop
NO example constructor §7; adversary §5; Rust test unused_coordinate_is_infeasible
The Rust implementation equals the verified construction cross-comparison B
Overflow of 3q not testable in Python; Rust test universe_overflow

Scripts

proof.md
# Theorem: ThreeDimensionalMatching -> ExactCoverBy3Sets

Source (3DM): universe size q >= 0, list of triples m_0..m_{t-1}, m_l = (a_l, b_l, c_l), each coordinate in {0..q-1}; duplicates allowed.
A selection vector x in {0,1}^t is a solution iff len(x) = t (else malformed/error), the number of selected triples equals q,
and no two selected triples share a value in the first coordinate, nor in the second, nor in the third.

Target (X3C): universe size n (must be divisible by 3), list of subsets S_0..S_{k-1}, each a 3-element set of distinct elements in {0..n-1}
(stored sorted); duplicates allowed. Selection x in {0,1}^k is a solution iff len(x) = k (else error), exactly n/3 subsets are selected,
the selected subsets are pairwise disjoint (no element repeated), and their union has n elements.

Construction: n = 3q; S_l = {a_l, q + b_l, 2q + c_l}, same order as the triples. k = t.
Extraction: identity (return the target selection vector, after checking it is an exact cover; reject otherwise).
Overhead: target universe_size = 3 * source universe_size; target num_subsets = source num_triples.

Correctness: tau(d, v) = d*q + v is a bijection {0,1,2} x {0..q-1} -> {0..3q-1}. Two selected subsets share an element iff the two
triples share a value in the same coordinate. With exactly q selected and pairwise disjoint 3-sets, the union has 3q elements = all of U.
So x solves the target iff x solves the source, for every x.

YES example: q = 3, triples (0,0,0),(1,1,1),(2,2,2),(0,1,2),(1,2,0) -> subsets {0,3,6},{1,4,7},{2,5,8},{0,4,8},{1,5,6};
x = (1,1,1,0,0) solves both; it is the unique solution.
NO example: q = 3, triples (0,0,0),(1,1,1),(2,1,2),(2,0,2),(1,0,1): second coordinate 2 never occurs, so no perfect matching;
target element 5 (= q + 2) is in no subset, so no exact cover.
constructor.py
"""Constructor verification for ThreeDimensionalMatching -> ExactCoverBy3Sets (how-to-verify section 3)."""
import itertools
import random
import sympy

counts = {k: 0 for k in ["1 symbolic", "2 forward/backward", "3 extraction", "4 size", "5 structure", "6 YES", "7 NO"]}


def check(section, cond, msg=""):
    assert cond, f"{section}: {msg}"
    counts[section] += 1


# Models, following src/models/set/*.rs evaluate()
def tdm_eval(q, triples, x):
    if len(x) != len(triples):
        return None  # EvaluationError
    if sum(x) != q:
        return False
    used = [set(), set(), set()]
    for sel, t in zip(x, triples):
        if sel:
            for d in range(3):
                if t[d] in used[d]:
                    return False
                used[d].add(t[d])
    return True


def x3c_valid(n, subsets):
    return n % 3 == 0 and all(len(set(s)) == 3 and all(0 <= e < n for e in s) for s in subsets)


def x3c_eval(n, subsets, x):
    if len(x) != len(subsets):
        return None
    if sum(x) != n // 3:
        return False
    covered = set()
    for sel, s in zip(x, subsets):
        if sel:
            for e in s:
                if e in covered:
                    return False
                covered.add(e)
    return len(covered) == n


def reduce(q, triples):
    return 3 * q, [sorted([w, q + x, 2 * q + y]) for (w, x, y) in triples]


def extract(n, subsets, x):
    if x3c_eval(n, subsets, x) is not True:
        return None  # ExtractionError
    return list(x)


def verify_instance(qq, triples):
    n, subs = reduce(qq, triples)
    check("4 size", n == 3 * qq)
    check("4 size", len(subs) == len(triples))
    check("5 structure", x3c_valid(n, subs), (qq, triples))
    for (w, x, y), s in zip(triples, subs):
        check("5 structure", s == [w, qq + x, 2 * qq + y])
    for x in itertools.product([0, 1], repeat=len(triples)):
        src, tgt = tdm_eval(qq, triples, x), x3c_eval(n, subs, x)
        check("2 forward/backward", src == tgt, (qq, triples, x))
        ext = extract(n, subs, x)
        if tgt:
            check("3 extraction", ext == list(x) and tdm_eval(qq, triples, ext))
        else:
            check("3 extraction", ext is None)
    for bad in [[0] * (len(triples) + 1), [1] * max(len(triples) - 1, 0)] if triples else [[0]]:
        check("2 forward/backward", tdm_eval(qq, triples, bad) is None and x3c_eval(n, subs, bad) is None)



def main():
    # 1. symbolic overhead
    q, t = sympy.symbols("q t", nonnegative=True, integer=True)
    for qq in range(0, 8):
        for tt in range(0, 8):
            n, subs = reduce(qq, [(0, 0, 0)] * tt if qq else [])
            expect_t = tt if qq else 0
            check("1 symbolic", sympy.Eq(3 * q, n).subs(q, qq) == True)
            check("1 symbolic", sympy.Eq(t, len(subs)).subs(t, expect_t) == True)


    # exhaustive: q <= 2, every multiset family of up to 5 triples (as ordered lists of combinations with replacement)
    verify_instance(0, [])
    for qq in [1, 2]:
        alltr = list(itertools.product(range(qq), repeat=3))
        for m in range(0, 6):
            for fam in itertools.combinations_with_replacement(alltr, m):
                verify_instance(qq, list(fam))
    # q = 3: random samples
    rng = random.Random(1103)
    for _ in range(600):
        qq = rng.choice([3, 4])
        m = rng.randint(0, 8)
        tr = [tuple(rng.randrange(qq) for _ in range(3)) for _ in range(m)]
        if rng.random() < 0.5:  # plant a perfect matching
            px, py = list(range(qq)), list(range(qq))
            rng.shuffle(px), rng.shuffle(py)
            tr = [(i, px[i], py[i]) for i in range(qq)] + tr[: 8 - qq]
            rng.shuffle(tr)
        verify_instance(qq, tr)

    # 6. YES example
    yes = [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (1, 2, 0)]
    n, subs = reduce(3, yes)
    check("6 YES", subs == [[0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [1, 5, 6]])
    sols = [x for x in itertools.product([0, 1], repeat=5) if x3c_eval(n, subs, x)]
    check("6 YES", sols == [(1, 1, 1, 0, 0)])
    check("6 YES", tdm_eval(3, yes, (1, 1, 1, 0, 0)) and x3c_eval(n, subs, (0, 0, 0, 1, 1)) is False)
    # 7. NO example
    no = [(0, 0, 0), (1, 1, 1), (2, 1, 2), (2, 0, 2), (1, 0, 1)]
    n, subs = reduce(3, no)
    check("7 NO", all(5 not in s for s in subs))
    check("7 NO", not any(tdm_eval(3, no, x) for x in itertools.product([0, 1], repeat=5)))
    check("7 NO", not any(x3c_eval(n, subs, x) for x in itertools.product([0, 1], repeat=5)))

    for k, v in counts.items():
        print(f"{k}: {v}")
    print("total:", sum(counts.values()), "failures: 0")


if __name__ == "__main__":
    main()
adversary.py
"""Independent adversarial check of 3DM -> X3C, written from proof.md only."""
import itertools
import sys
from collections import Counter

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

COUNTS = Counter()
FAILS = []


def check(section, cond, msg):
    COUNTS[section] += 1
    if not cond:
        FAILS.append((section, msg))


# ---------------- source (3DM) ----------------
def source_valid(q, triples):
    if not isinstance(q, int) or q < 0:
        return False
    for tr in triples:
        if len(tr) != 3 or any(not (0 <= v < q) for v in tr):
            return False
    return True


def source_feasible(q, triples, x):
    if len(x) != len(triples):
        raise ValueError("malformed source vector")
    if any(v not in (0, 1) for v in x):
        raise ValueError("non-binary")
    sel = [triples[i] for i, v in enumerate(x) if v == 1]
    if len(sel) != q:
        return False
    for d in range(3):
        vals = [tr[d] for tr in sel]
        if len(set(vals)) != len(vals):
            return False
    return True


# ---------------- target (X3C) ----------------
def target_valid(n, subsets):
    if not isinstance(n, int) or n < 0 or n % 3 != 0:
        return False
    for s in subsets:
        if len(s) != 3 or len(set(s)) != 3:
            return False
        if any(not (0 <= e < n) for e in s):
            return False
        if list(s) != sorted(s):
            return False
    return True


def target_feasible(n, subsets, x):
    if not target_valid(n, subsets):
        raise ValueError("invalid target instance")
    if len(x) != len(subsets):
        raise ValueError("malformed target vector")
    if any(v not in (0, 1) for v in x):
        raise ValueError("non-binary")
    sel = [subsets[i] for i, v in enumerate(x) if v == 1]
    if len(sel) != n // 3:
        return False
    elems = [e for s in sel for e in s]
    if len(set(elems)) != len(elems):
        return False
    return len(set(elems)) == n


def target_feasible_alt(n, subsets, x):
    """Second formulation: each element of U covered exactly once."""
    c = Counter(e for i, s in enumerate(subsets) if x[i] == 1 for e in s)
    return all(c[e] == 1 for e in range(n)) and set(c) <= set(range(n))


# ---------------- reduction ----------------
def reduce(q, triples):
    n = 3 * q
    subsets = [tuple(sorted((a, q + b, 2 * q + c))) for (a, b, c) in triples]
    return n, subsets


def extract_solution(n, subsets, x):
    if not target_feasible(n, subsets, x):
        raise ValueError("not an exact cover")
    return list(x)


def try_extract(n, subsets, x):
    try:
        return extract_solution(n, subsets, x)
    except ValueError:
        return None


# ---------------- core per-instance check ----------------
def check_instance(section, q, triples, vectors):
    assert source_valid(q, triples)
    n, subsets = reduce(q, triples)
    # section 3: validity + overhead
    check("3-validity-overhead", target_valid(n, subsets), f"invalid target {q} {triples}")
    check("3-validity-overhead", n == 3 * q, "universe overhead")
    check("3-validity-overhead", len(subsets) == len(triples), "subset overhead")
    for (a, b, c), s in zip(triples, subsets):
        check("3-validity-overhead", set(s) == {a, q + b, 2 * q + c}, "subset content/order")
    any_src = any_tgt = False
    for x in vectors:
        x = list(x)
        sf = source_feasible(q, triples, x)
        tf = target_feasible(n, subsets, x)
        check(section, sf == tf, f"feas mismatch q={q} T={triples} x={x} src={sf} tgt={tf}")
        check(section, tf == target_feasible_alt(n, subsets, x), f"tgt alt mismatch {q} {triples} {x}")
        ex = try_extract(n, subsets, x)
        check(section, (ex == x) == sf and (ex is None) == (not sf),
              f"extract mismatch q={q} T={triples} x={x} ex={ex}")
        any_src |= sf
        any_tgt |= tf
    return any_src, any_tgt


def all_vectors(t):
    return itertools.product((0, 1), repeat=t)


# ---------------- section 1: exhaustive ----------------
def section1():
    sec = "1-exhaustive"
    # q = 0, no triples
    check_instance(sec, 0, [], [[]])
    check(sec, source_feasible(0, [], []) and target_feasible(0, [], []), "q=0 empty should be feasible")
    for q in (1, 2):
        cells = list(itertools.product(range(q), repeat=3))
        seen = set()
        # all multisets (with duplicates) of size 0..6
        for t in range(0, 7):
            for fam in itertools.combinations_with_replacement(cells, t):
                fam = list(fam)
                seen.add(tuple(fam))
                s_any, t_any = check_instance(sec, q, fam, all_vectors(t))
                # brute-force decision equivalence
                check(sec, s_any == t_any, f"decision mismatch {q} {fam}")
        # all ordered lists (with duplicates) of size 0..3 for q=2, 0..6 for q=1
        maxlen = 6 if q == 1 else 3
        for t in range(0, maxlen + 1):
            for fam in itertools.product(cells, repeat=t):
                fam = list(fam)
                if tuple(fam) in seen:
                    continue
                s_any, t_any = check_instance(sec, q, fam, all_vectors(t))
                check(sec, s_any == t_any, f"decision mismatch {q} {fam}")


# ---------------- section 2: hypothesis ----------------
HSET = dict(max_examples=600, derandomize=True, database=None, deadline=None,
            suppress_health_check=list(HealthCheck))


@st.composite
def random_instance(draw):
    q = draw(st.integers(1, 4))
    t = draw(st.integers(0, 9))
    cell = st.tuples(st.integers(0, q - 1), st.integers(0, q - 1), st.integers(0, q - 1))
    triples = draw(st.lists(cell, min_size=t, max_size=t))
    return q, triples


@st.composite
def planted_instance(draw):
    q = draw(st.integers(1, 5))
    pb = draw(st.permutations(list(range(q))))
    pc = draw(st.permutations(list(range(q))))
    planted = [(i, pb[i], pc[i]) for i in range(q)]
    cell = st.tuples(st.integers(0, q - 1), st.integers(0, q - 1), st.integers(0, q - 1))
    noise = draw(st.lists(cell, min_size=0, max_size=max(0, 10 - q)))
    allt = planted + noise
    order = draw(st.permutations(list(range(len(allt)))))
    triples = [allt[i] for i in order]
    planted_x = [1 if order[j] < q else 0 for j in range(len(allt))]
    return q, triples, planted_x


def adversarial_vectors(t):
    return [[0] * t, [1] * t, [i % 2 for i in range(t)], [(i + 1) % 2 for i in range(t)]]


@settings(**HSET)
@given(random_instance())
def h_random(inst):
    q, triples = inst
    t = len(triples)
    vecs = list(all_vectors(t)) if t <= 9 else adversarial_vectors(t)
    s_any, t_any = check_instance("2-hypothesis-random", q, triples, vecs)
    check("2-hypothesis-random", s_any == t_any, f"decision mismatch {q} {triples}")


@settings(**HSET)
@given(planted_instance(), st.lists(st.booleans(), min_size=0, max_size=15))
def h_planted(inst, flips):
    q, triples, px = inst
    sec = "2-hypothesis-planted"
    n, subsets = reduce(q, triples)
    check(sec, source_feasible(q, triples, px), f"planted not src feasible {q} {triples} {px}")
    check(sec, target_feasible(n, subsets, px), f"planted not tgt feasible {q} {triples} {px}")
    check(sec, try_extract(n, subsets, px) == px, "planted extract")
    # perturbations of the planted vector
    vecs = [px]
    for k, f in enumerate(flips):
        if f and len(px) > 0:
            y = list(px)
            y[k % len(px)] ^= 1
            vecs.append(y)
    vecs += adversarial_vectors(len(px))
    check_instance(sec, q, triples, vecs)


@settings(**HSET)
@given(random_instance())
def h_adversarial(inst):
    q, triples = inst
    sec = "2-hypothesis-adversarial"
    check_instance(sec, q, triples, adversarial_vectors(len(triples)))
    # all-zero is feasible only if q == 0 (never here since q>=1)
    n, subsets = reduce(q, triples)
    check(sec, not target_feasible(n, subsets, [0] * len(triples)), "all-zero feasible with q>=1")


# ---------------- section 4: malformed ----------------
def expect_raise(fn, *a):
    try:
        fn(*a)
    except ValueError:
        return True
    return False


@settings(**HSET)
@given(random_instance(), st.integers(-3, 3).filter(lambda d: d != 0), st.integers(0, 1))
def h_malformed(inst, delta, fill):
    q, triples = inst
    sec = "4-malformed"
    t = len(triples)
    L = t + delta
    if L < 0:
        L = t + 1
    x = [fill] * L
    n, subsets = reduce(q, triples)
    check(sec, expect_raise(source_feasible, q, triples, x), f"src accepted len {L} vs {t}")
    check(sec, expect_raise(target_feasible, n, subsets, x), f"tgt accepted len {L} vs {t}")
    check(sec, expect_raise(extract_solution, n, subsets, x), f"extract accepted len {L} vs {t}")


def section4_fixed():
    sec = "4-malformed"
    for q in (0, 1, 2, 3):
        cells = list(itertools.product(range(q), repeat=3))[:4]
        for t in range(0, len(cells) + 1):
            triples = cells[:t]
            n, subsets = reduce(q, triples)
            for L in range(0, t + 4):
                if L == t:
                    continue
                for x in ([0] * L, [1] * L):
                    check(sec, expect_raise(source_feasible, q, triples, x), "src len")
                    check(sec, expect_raise(target_feasible, n, subsets, x), "tgt len")
                    check(sec, expect_raise(extract_solution, n, subsets, x), "extract len")
    # also: target validator rejects bad target instances
    for n, subs in [(4, []), (3, [(0, 0, 1)]), (3, [(0, 1, 3)]), (6, [(0, 1)]), (3, [(-1, 0, 1)])]:
        check(sec, not target_valid(n, subs), f"bad target accepted {n} {subs}")
        check(sec, expect_raise(target_feasible, n, subs, [0] * len(subs)), "invalid target checked")


# ---------------- section 5: examples ----------------
def section5():
    sec = "5-examples"
    q = 3
    yes = [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (1, 2, 0)]
    n, subsets = reduce(q, yes)
    expected = [{0, 3, 6}, {1, 4, 7}, {2, 5, 8}, {0, 4, 8}, {1, 5, 6}]
    check(sec, n == 9, "YES n")
    check(sec, [set(s) for s in subsets] == expected, f"YES subsets {subsets}")
    x = [1, 1, 1, 0, 0]
    check(sec, source_feasible(q, yes, x), "YES src")
    check(sec, target_feasible(n, subsets, x), "YES tgt")
    check(sec, extract_solution(n, subsets, x) == x, "YES extract")
    src_sols = [list(v) for v in all_vectors(5) if source_feasible(q, yes, list(v))]
    tgt_sols = [list(v) for v in all_vectors(5) if target_feasible(n, subsets, list(v))]
    check(sec, src_sols == [x], f"YES src unique {src_sols}")
    check(sec, tgt_sols == [x], f"YES tgt unique {tgt_sols}")

    no = [(0, 0, 0), (1, 1, 1), (2, 1, 2), (2, 0, 2), (1, 0, 1)]
    n, subsets = reduce(q, no)
    check(sec, 2 not in {b for _, b, _ in no}, "NO: second coord 2 absent")
    check(sec, all(5 not in s for s in subsets), "NO: element 5 uncovered")
    check(sec, not any(source_feasible(q, no, list(v)) for v in all_vectors(5)), "NO src has sol")
    check(sec, not any(target_feasible(n, subsets, list(v)) for v in all_vectors(5)), "NO tgt has sol")
    check(sec, all(try_extract(n, subsets, list(v)) is None for v in all_vectors(5)), "NO extract")
    check_instance(sec, q, yes, all_vectors(5))
    check_instance(sec, q, no, all_vectors(5))


def main():
    section1()
    h_random()
    h_planted()
    h_adversarial()
    h_malformed()
    section4_fixed()
    section5()
    total = sum(COUNTS.values())
    for k in sorted(COUNTS):
        print(f"{k}: {COUNTS[k]} checks")
    print(f"TOTAL: {total} checks, {len(FAILS)} failures")
    print(f"hypothesis settings: {({k: v for k, v in HSET.items() if k != 'suppress_health_check'})}")
    for f in FAILS[:20]:
        print("FAIL", f)
    ok = not FAILS and total >= 10000
    print("RESULT:", "PASS" if ok else "FAIL")
    sys.exit(0 if ok else 1)


if __name__ == "__main__":
    main()
cross_compare.py
"""Cross-comparison for ThreeDimensionalMatching -> ExactCoverBy3Sets (how-to-verify section 4).

A. constructor.reduce vs adversary.reduce on shared instances: identical targets, and the
   constructor's and adversary's source/target checkers agree on every selection vector.
B. The Rust rule (`pred reduce` over the direct route) vs both Python reductions on 100
   random ThreeDimensionalMatching instances plus the YES and NO examples: identical targets.

Usage: python3 cross_compare.py <pred binary>
"""
import itertools
import json
import random
import subprocess
import sys
import tempfile
from collections import Counter
from pathlib import Path

import adversary as adv
import constructor as con

counts = Counter()
fails = []


def check(section, cond, msg):
    counts[section] += 1
    if not cond:
        fails.append((section, msg))


def same_target(q, triples):
    cn, cs = con.reduce(q, triples)
    an, as_ = adv.reduce(q, triples)
    return (cn, [list(s) for s in cs]), (an, [list(s) for s in as_])


def compare_instance(q, triples):
    c, a = same_target(q, triples)
    check("A targets identical", c == a, (q, triples, c, a))
    n, subs = c
    for x in itertools.product([0, 1], repeat=len(triples)):
        check("A source checkers agree", con.tdm_eval(q, triples, x) == adv.source_feasible(q, triples, x), (q, triples, x))
        check("A target checkers agree", con.x3c_eval(n, subs, x) == adv.target_feasible(n, subs, x), (q, triples, x))


# A1: exhaustive shared instances, q <= 2, multisets of up to 5 triples.
compare_instance(0, [])
for q in [1, 2]:
    alltr = list(itertools.product(range(q), repeat=3))
    for m in range(6):
        for fam in itertools.combinations_with_replacement(alltr, m):
            compare_instance(q, list(fam))
# A2: random shared instances, q in 3..5, seed 20261101.
rng = random.Random(20261101)
for _ in range(500):
    q = rng.randint(3, 5)
    triples = [tuple(rng.randrange(q) for _ in range(3)) for _ in range(rng.randint(0, 9))]
    compare_instance(q, triples)

# B: Rust rule vs Python reductions.
pred = Path(sys.argv[1])
# B instances: 100 random ones (q in 1..6, 0..10 triples, half with a planted perfect matching,
# seed 1103), then the paper's YES and NO examples.
rng = random.Random(1103)
instances = []
for i in range(100):
    q = rng.randint(1, 6)
    triples = [tuple(rng.randrange(q) for _ in range(3)) for _ in range(rng.randint(0, 10))]
    if i % 2:
        xs, ys = rng.sample(range(q), q), rng.sample(range(q), q)
        triples += [(w, xs[w], ys[w]) for w in range(q)]
        rng.shuffle(triples)
    instances.append((q, triples))
instances += [(3, [(0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2), (1, 2, 0)]), (3, [(0, 0, 0), (1, 1, 1), (2, 1, 2), (2, 0, 2), (1, 0, 1)])]
sources = [{"data": {"triples": [list(t) for t in triples], "universe_size": q}, "type": "ThreeDimensionalMatching", "variant": {}} for q, triples in instances]
with tempfile.TemporaryDirectory() as tmp:
    paths = Path(tmp) / "paths.json"
    subprocess.run([pred, "path", "ThreeDimensionalMatching", "ExactCoverBy3Sets", "-o", paths], check=True, capture_output=True)
    route = Path(tmp) / "route.json"
    route.write_text(json.dumps(json.loads(paths.read_text())["paths"][0]))
    for i, src in enumerate(sources):
        inp, out = Path(tmp) / f"s{i}.json", Path(tmp) / f"b{i}.json"
        inp.write_text(json.dumps(src))
        subprocess.run([pred, "reduce", inp, "--via", route, "-o", out], check=True, capture_output=True)
        tgt = json.loads(out.read_text())["target"]["data"]
        q, triples = src["data"]["universe_size"], [tuple(t) for t in src["data"]["triples"]]
        c, a = same_target(q, triples)
        rust = (tgt["universe_size"], tgt["subsets"])
        check("B Rust == constructor", rust == c, (i, rust, c))
        check("B Rust == adversary", rust == a, (i, rust, a))

for k in sorted(counts):
    print(f"{k}: {counts[k]}")
print(f"instances in B: {len(sources)}")
print(f"total: {sum(counts.values())} checks, {len(fails)} disagreements")
for f in fails[:10]:
    print("DISAGREE", f)
sys.exit(1 if fails else 0)

@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.50249% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.19%. Comparing base (e7bacb3) to head (e88f9ac).

Files with missing lines Patch % Lines
...ules/threedimensionalmatching_exactcoverby3sets.rs 99.37% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1226    +/-   ##
========================================
  Coverage   97.19%   97.19%            
========================================
  Files        1146     1148     +2     
  Lines      151558   151758   +200     
========================================
+ Hits       147308   147507   +199     
- Misses       4250     4251     +1     

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

@isPANN

isPANN commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator Author

Agentic Review Report

Reviewed head f16ad3c5 against origin/main (e7bacb37). Rule review for ThreeDimensionalMatching -> ExactCoverBy3Sets (#1103). All five changed files were read in full.

Structural and semantic

Check Result
Whitelist / no generated exports (reduction_graph.json, problem_schemas.json, examples.json) in diff Pass
No unrelated edits; model change limited to making ExactCoverBy3Sets::try_new pub(crate) so target construction errors stay ReductionError::Construction Pass
#[reduction(transform = exact { universe_size, num_subsets })] covers both registered target parameters exactly once; num_sets is a getter, not a canonical parameter Pass
Registered in src/rules/mod.rs; canonical_rule_example_specs() collected Pass
Single primitive registration for the endpoint pair (no other ReduceTo<ExactCoverBy3Sets> for ThreeDimensionalMatching) Pass
extract_solution validates once via validate_target_witness, then returns the identity map; wrong-length and non-cover inputs yield ExtractionError Pass
#[aggregate_reduction(identity)] for Or -> Or Pass
Numeric contract: 3q uses checked_mul with a typed IntegerOverflow; q + x and 2q + y are below 3q so cannot overflow Pass
Parameter formulas exercised by every_executable_parameter_formula_matches_a_constructed_target (via the canonical example) and assert_parameter_predictions in the rule tests Pass
Paper reduction-rule entry with example loaded via load-example, rule-spec + cli-config in pred-commands Pass

Hand trace. Issue example, q = 3, triples (0,0,0),(1,1,1),(2,2,2),(0,1,2),(1,2,0). reduce_to sets 3q = 9 and maps each (w,x,y) to [w, 3+x, 6+y]: [0,3,6], [1,4,7], [2,5,8], [0,4,8], [1,5,6]. These are already sorted, so try_new leaves them unchanged and every subset has three distinct in-range elements. Sizes are universe_size = 9 = 3*3 and num_subsets = 5 = num_triples, matching the transform. [1,1,1,0,0] is an exact cover and decodes to the diagonal matching. [0,0,0,1,1] selects two disjoint subsets, but both models reject it because exactly q = 3 selections are required.

Semantics. ThreeDimensionalMatching::evaluate accepts exactly q selected triples with no repeated value in any coordinate. ExactCoverBy3Sets::evaluate accepts exactly q pairwise-disjoint selected subsets whose union is all 3q elements. Because tau(d, v) = d*q + v is a bijection from tagged coordinates onto {0..3q-1}, two selected triples share a coordinate exactly when their images intersect. With |J| = q, disjointness forces the union to have size 3q. So the two problems have exactly the same set of witnesses, and identity extraction is correct. Edge cases also behave: q = 0 maps to an empty X3C instance, which is YES on both sides. Duplicate triples become duplicate subsets in the same positions. Numerically equal coordinates in different domains map to distinct elements.

Paper proof. Sound. The construction argues validity of the target instance (distinct elements, 3q divisible by 3), both directions use the bijection tau correctly, and the extraction is stated. The YES uniqueness argument is derived from the construction (unique elements forcing S_0, S_1, S_2), not from fixture length. The NO example is correct: X-value 2 is unused, so tagged element 5 is uncovered. karp1972 and garey1979 exist in references.bib.

Issue compliance. Construction, identity extraction, the Or -> Or framing, both overhead formulas, the example, and every validation case the issue requests are implemented and tested: exhaustive q = 2, duplicate triples, q = 0, unused coordinates and equal numeric coordinates. The issue's num_sets row is a getter alias rather than a registered parameter, so leaving it out of the transform is correct.

Quality

  • The closed loop uses assert_satisfaction_round_trip_from_satisfaction_target on the 5-triple issue instance. It also checks with brute force that the diagonal is the only target witness.
  • assert_identical_feasibility compares source and target evaluate on every selection vector and checks extraction on each one. This is an independent semantic check, not a recomputation of the formula.
  • The exhaustive q = 2 test covers all 256 triple families. It compares the full witness sets and derives the feasible count (256 - 3^4 = 175) by an independent argument.
  • Boundary and rejection cases are covered: empty instance, duplicates, an infeasible unused coordinate, numerically equal coordinates, too few, too many or overlapping selections, wrong length in both directions, aggregate identity, and the usize::MAX / 3 overflow boundary.
  • No helper is duplicated, and problemreductions-cli/ is unchanged, so no HCI review is needed.
  • CI is green. Codecov reports one uncovered line, inside a test-file assertion message.

Feature test

Run with target/debug/pred from this head in a scratch directory:

  • pred list --rules ThreeDimensionalMatching lists ExactCoverBy3Sets. pred show ThreeDimensionalMatching shows → ExactCoverBy3Sets (num_subsets = num_triples, universe_size = 3 * universe_size).
  • pred create --example ThreeDimensionalMatching --to ExactCoverBy3Sets writes the issue instance. pred inspect lists ExactCoverBy3Sets under reduces_to. pred solve with both the ILP and brute-force solvers returns [1,1,1,0,0] and Or(true).
  • pred path ThreeDimensionalMatching ExactCoverBy3Sets inst.json measures num_subsets = 5 and universe_size = 9.
  • pred reduce inst.json --via route.json produces subsets [0,3,6],[1,4,7],[2,5,8],[0,4,8],[1,5,6] over 9 elements. pred solve bundle.json with ILP and with brute force extracts [1,1,1,0,0] and reports Or(true).
  • pred evaluate inst.json --config '[true,true,true,false,false]' gives Or(true), and '[false,false,false,true,true]' gives Or(false).
  • I built the paper's NO instance with pred create ThreeDimensionalMatching --universe-size 3 --triples "0,0,0;1,1,1;2,1,2;2,0,2;1,0,1". Reducing it gives the subsets stated in the paper, and solving the bundle reports infeasible with both solvers.
  • The empty instance (--universe-size 0 --triples "") reduces and solves to [] with Or(true).

Findings

Critical: none.

Important: none.

Minor

  1. docs/paper/reductions.typ:19873 and docs/paper/reductions.typ:19875: the worked example hard-codes facts that belong to the fixture.
    • Line 19873 says "the identical source configuration selects the diagonal triples".
    • Line 19875 names the forcing subsets S_2, S_0, S_1 and the coordinates x = 0, y = 1, and says "the cross subsets S_3 and S_4 are disjoint".
    • All of these statements are correct for the current fixture. However, how-to-write-manual says not to hard-code values the fixture holds, and these would silently go stale if the canonical spec changed.
    • Suggested fix: compute the selected source triples from triples and selected, as Step 1 already does for the triple list, and write "the selected triples #selected.map(i => triples.at(i))…". For the uniqueness paragraph, either compute the forcing subset index for each unique element in Typst, or reword it as an argument that does not name fixed positions (for example, "each of the elements … lies in exactly one subset").

…fixture

The selected triples and the element that forces each selected subset are
now computed from the loaded example, and the build fails if the
uniqueness argument no longer applies.

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] ThreeDimensionalMatching to ExactCoverBy3Sets

1 participant