Skip to content

Fix #1200: [Rule] Unit ComparativeContainment to DecisionQUBO - #1240

Open
isPANN wants to merge 1 commit into
mainfrom
issue-1200-comparative-containment-qubo
Open

isPANN wants to merge 1 commit into
mainfrom
issue-1200-comparative-containment-qubo

Conversation

@isPANN

@isPANN isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds the reduction ComparativeContainment<One> → DecisionQUBO<i64> (issue #1200): a gate-based encoding of the "unit" comparative-containment decision problem as a QUBO energy bound, with solution extraction back to the subset.

  • New rule src/rules/comparativecontainment_qubo.rs, registered in src/rules/mod.rs, with a canonical example.
  • Unit tests in src/unit_tests/rules/comparativecontainment_qubo.rs: the issue's YES example, a NO instance, exhaustive small-instance round trips, special cases named in the proof, and both sides of the i64 coefficient limits.
  • Paper entry in docs/paper/reductions.typ (definition, proof, example, pred commands) and a url on the existing borosgruber2014quadratization bibliography entry.

Verification

The math was verified independently by two implementations before the Rust rule was written (see the certificate comment), then the Rust rule was checked against them through the real pred CLI on 606 shared instances, with matching targets and extraction behaviour.

Local gates: make check and make paper pass; coverage of the new module is 100% of executable lines.

Notes for reviewers

  • The 100 reference instances for this source problem are all YES instances, so the NO direction rests on the unit tests and the verification scripts, not on those instances.
  • The registered num_quadratic_terms bound, 6n(r+s) + r + s, is the issue's formula. It is a valid but conservative upper bound (about twice the provable 3n(r+s)).

Fixes #1200

🤖 Generated with Claude Code

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

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate

Verdict: VERIFIED — rule ComparativeContainment<One> → DecisionQUBO<i64> (issue #1200).

Type chain. ComparativeContainment<One> (decision, Or) → DecisionQUBO<i64> (decision, Or), identity aggregate mapping; solution extraction returns the first n target bits after validating the full target witness.

Method

Two implementations written independently from the same Typst proof: a constructor script and an adversary script (the latter written before reading the former). Their targets were then compared with each other and with the real Rust pred CLI.

Constructor script — 5,827,919 checks, 0 failures

Section Checks Content
1 39 symbolic construction, overhead and arithmetic boundaries
2 2,899,393 exhaustive forward/backward and decision equivalence
3 142,505 every feasible target extraction and invalid-witness rejection
4 27,020 measured registered parameters and stored support
5 2,758,949 target structure, expanded energy, numeric safety
6 7 YES example reproduced
7 6 NO example reproduced
  • Instances: 6,755 distinct. Exhaustive over ordered (R, S) with r, s ≤ 2 for n ≤ 2; exhaustive m ≤ 2 for n = 3..5; 300 distinct samples per (n, m) for n = 3..5, m = 3, 4 with target size N ≤ 12 (seeds 2026110303..2026110305). Every target vector is enumerated.
  • Hypothesis: 2 strategies, max_examples=300 each, seeds 2026110301 / 2026110302, database=None, deadline=None.

Adversary script — 35,972,269 checks, 0 failures

Section Checks
1 symbolic 123
2 backward 25,617,212
2 equivalence 25,416
2 forward 1,244,472
3 extract 8,512,208
4/5 size + structure 505,087
5 special cases 67,717
6 YES 14
7 NO 20
  • Instances: 24,514 exhaustive, covering all ordered (R, S) with total size m per n: m ≤ 5 for n ≤ 1, m ≤ 4 for n = 2, m ≤ 3 for n = 3, 4, m ≤ 2 for n = 5; every target vector enumerated.
  • Hypothesis: 3 strategies × 300 examples, seeds 2026110411 / 2026110412 / 2026110413, database=None, deadline=None; full target enumeration when N ≤ 16.
  • Mutation testing: the adversary script detects all 49 seeded faults (+1 targeted); the constructor script detects all but one coefficient-saturation mutant, a gap closed by a Rust unit test at the exact coefficient limit.

Cross-comparison

  • Constructor vs adversary: identical targets on 606 shared instances and 24,514 extended instances, 0 mismatches.
  • Real Rust pred vs both scripts: 606 instances, identical pred reduce targets; pred extract accepts 519 feasible witnesses, rejects 495 infeasible vectors and 606 wrong-length vectors; 36,997 comparisons in total, 0 mismatches.
  • The 100 reference instances for this source problem are all YES instances; the NO direction is covered by the scripts and the Rust unit tests.

The scripts and the proof follow, collapsed. Code runs with Python 3, sympy and hypothesis; no library code is imported.

Constructor script
#!/usr/bin/env python3
"""Construct and audit ComparativeContainment/One -> DecisionQUBO/i64.

Run with python3 (sympy and hypothesis installed). No library code is imported.
All tested target witnesses are enumerated, not just optimal witnesses.
See proof.typ for derivations, sampling scope, and the seven-section claim map.
"""
from collections import Counter, defaultdict
from dataclasses import dataclass
from itertools import product
import random
import sys
import traceback

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

I64_MAX = (1 << 63) - 1
INDEX_MAX = (1 << 64) - 1
SECTIONS = {
    1: "symbolic construction, overhead and arithmetic boundaries",
    2: "exhaustive forward/backward and decision equivalence",
    3: "every feasible target extraction and invalid-witness rejection",
    4: "measured registered parameters and stored support",
    5: "target structure, expanded energy and numeric safety",
    6: "YES example reproduced",
    7: "NO example reproduced",
}
COUNTS = Counter()
FAILURES = 0
CASES = Counter()
SEEN = set()
SAMPLE_REJECTIONS = Counter()
WORK = Counter()


def check(section, condition, detail):
    global FAILURES
    COUNTS[section] += 1
    if not condition:
        FAILURES += 1
        raise AssertionError(f"section {section}: {detail}")


@dataclass(frozen=True)
class Instance:
    n: int
    r_sets: tuple
    s_sets: tuple


def source_difference(instance, bits):
    if len(bits) != instance.n or any(v not in (0, 1) for v in bits):
        raise ValueError("invalid source witness")
    selected = {i for i, v in enumerate(bits) if v}
    # Independent membership/count oracle; no chain or QUBO operations.
    return (sum(selected.issubset(a) for a in instance.s_sets)
            - sum(selected.issubset(a) for a in instance.r_sets))


class NumericError(OverflowError):
    """An unrepresentable target is an error, never an infeasibility answer."""


@dataclass(frozen=True)
class Budget:
    num_vars: int
    penalty: int
    absolute_sum: int


def numeric_certificate(n, r, s, g, limit=I64_MAX, index_limit=INDEX_MAX):
    if min(n, r, s, g) < 0 or max(n, r, s, g) > index_limit:
        raise NumericError("input index/count overflow")
    if r + s > index_limit or n + g > index_limit:
        raise NumericError("derived size overflow")
    if max(r, s, g) > limit or r + 1 > limit:
        raise NumericError("count or penalty conversion overflow")
    # M*g first is significant when g=0 and 8*M would overflow.
    budget = 8 * ((r + 1) * g) + r + s
    if budget > limit:
        raise NumericError("coefficient/evaluation certificate overflow")
    return Budget(n + g, r + 1, budget)


# Polynomials map a sorted tuple of distinct binary coordinates to a coefficient.
# The empty tuple is the constant. This multiplication enforces y_i^2 = y_i.
def add_scaled(dst, src, scale=1):
    for key, value in src.items():
        dst[key] += scale * value


def multiply(left, right):
    result = defaultdict(int)
    for i, a in left.items():
        for j, b in right.items():
            result[tuple(sorted(set(i + j)))] += a * b
    return dict(result)


def literal(index, complemented=False):
    return {(): 1, (index,): -1} if complemented else {(index,): 1}


def gate_polynomial(a, b, z):
    result = defaultdict(int)
    add_scaled(result, multiply(a, b))
    add_scaled(result, multiply(a, literal(z)), -2)
    add_scaled(result, multiply(b, literal(z)), -2)
    add_scaled(result, literal(z), 3)
    return {key: value for key, value in result.items() if value}


@dataclass(frozen=True)
class Target:
    source_n: int
    num_vars: int
    bound: int
    entries: tuple
    gates: tuple  # ((input index, complemented), (input index, complemented), z)
    indicators: tuple  # (family sign, constant/affine/output polynomial)
    budget: Budget

    def as_dict(self):
        return {"num_vars": self.num_vars, "entries": [list(e) for e in self.entries],
                "bound": self.bound}


def reduce(instance, limit=I64_MAX, index_limit=INDEX_MAX):
    n, r_sets, s_sets = instance.n, instance.r_sets, instance.s_sets
    if n < 0 or any(type(v) is not int or not 0 <= v < n
                    for a in r_sets + s_sets for v in a):
        raise ValueError("element outside universe")
    g = sum(max(0, n - len(set(a)) - 1) for a in r_sets + s_sets)
    budget = numeric_certificate(n, len(r_sets), len(s_sets), g, limit, index_limit)
    polynomial = defaultdict(int)
    gates, indicators = [], []
    next_var = n
    for family, sign in ((r_sets, -1), (s_sets, 1)):
        for entry in family:
            complement = sorted(set(range(n)).difference(entry))
            if not complement:
                indicator = {(): 1}
            elif len(complement) == 1:
                indicator = literal(complement[0], True)
            else:
                a = (complement[0], True)
                for element in complement[1:]:
                    b = (element, True)
                    z = next_var
                    next_var += 1
                    gates.append((a, b, z))
                    add_scaled(polynomial,
                               gate_polynomial(literal(*a), literal(*b), z),
                               budget.penalty)
                    a = (z, False)
                indicator = literal(next_var - 1)
            indicators.append((sign, indicator))
            add_scaled(polynomial, indicator, sign)
    constant = polynomial.pop((), 0)
    entries = tuple(sorted((key[0], key[-1], value)
                           for key, value in polynomial.items() if value))
    # Same sparse contract as QUBO::from_entries: unique upper triangle, no zero.
    return Target(n, next_var, -constant, entries, tuple(gates),
                  tuple(indicators), budget)


def target_value(target, bits):
    if len(bits) != target.num_vars or any(v not in (0, 1) for v in bits):
        raise ValueError("invalid target witness")
    total = 0
    for i, j, coefficient in target.entries:
        if bits[i] and bits[j]:
            total += coefficient
            if not -I64_MAX - 1 <= total <= I64_MAX:
                raise NumericError("target evaluation overflow")
    return total


def extract_solution(target, bits):
    if target_value(target, bits) > target.bound:
        raise ValueError("target witness misses decision bound")
    return tuple(bits[:target.source_n])


def extend(instance, bits):
    # Derive the extension from source membership alone, not constructed gates.
    values = list(bits)
    for entry in instance.r_sets + instance.s_sets:
        outside = [v for v in range(instance.n) if v not in entry]
        if len(outside) >= 2:
            prefix = 1 - bits[outside[0]]
            for v in outside[1:]:
                prefix *= 1 - bits[v]
                values.append(prefix)
    return tuple(values)


def rejected(call):
    try:
        call()
    except (ValueError, NumericError):
        return True
    return False


def bits_of(mask, n):
    return tuple((mask >> i) & 1 for i in range(n))


def measured_size(instance, target):
    n, r, s = instance.n, len(instance.r_sets), len(instance.s_sets)
    g = sum(max(0, n - len(set(a)) - 1)
            for a in instance.r_sets + instance.s_sets)
    off_diagonal = sum(i != j for i, j, _ in target.entries)
    check(4, target.num_vars == n + g, (instance, "exact variables", target))
    check(4, target.num_vars <= n + n * (r + s), "registered variable bound")
    check(4, off_diagonal <= 3 * g <= 6 * n * (r + s) + r + s,
          "registered off-diagonal bound")
    check(4, len(target.entries) <= 6 * g + r + s, "all stored entries bound")
    check(5, len(target.gates) == g and all(
        z == n + ordinal and a[0] < z and b[0] < z and a[0] != b[0]
        and (not a[1] or a[0] < n) and b[1] and b[0] < n
        for ordinal, (a, b, z) in enumerate(target.gates)), "fresh acyclic gates")
    check(5, list(target.entries) == sorted(target.entries)
          and len({(i, j) for i, j, _ in target.entries}) == len(target.entries)
          and all(0 <= i <= j < target.num_vars and c != 0
                  for i, j, c in target.entries), "sparse target contract")
    check(5, sum(abs(c) for _, _, c in target.entries) <= target.budget.absolute_sum
          and abs(target.bound) <= target.budget.absolute_sum <= I64_MAX,
          "coefficient and constant envelopes")


def verify_instance(instance):
    if instance in SEEN:
        return
    SEEN.add(instance)
    CASES[(instance.n, len(instance.r_sets) + len(instance.s_sets))] += 1
    target = reduce(instance)
    measured_size(instance, target)
    n, N = instance.n, target.num_vars
    WORK["source_vectors"] += 1 << n
    WORK["target_vectors"] += 1 << N
    WORK["max_target_variables"] = max(WORK["max_target_variables"], N)
    differences = [source_difference(instance, bits_of(mask, n))
                   for mask in range(1 << n)]
    for xmask, difference in enumerate(differences):
        extension = extend(instance, bits_of(xmask, n))
        check(2, target_value(target, extension) - target.bound == difference,
              (instance, "forward energy", xmask))

    terms = [((1 << i) | (1 << j), c) for i, j, c in target.entries]
    gates = [(1 << a[0], a[1], 1 << b[0], b[1], 1 << z)
             for a, b, z in target.gates]
    indicators = [(sign, [(sum(1 << i for i in key), c)
                          for key, c in polynomial.items()])
                  for sign, polynomial in target.indicators]
    found = False
    first_infeasible = None
    for mask in range(1 << N):
        energy = sum(c for support, c in terms if mask & support == support)
        penalty = 0
        correct = True
        for amask, aneg, bmask, bneg, zmask in gates:
            a = int(bool(mask & amask)) ^ aneg
            b = int(bool(mask & bmask)) ^ bneg
            z = int(bool(mask & zmask))
            penalty += a * b - 2 * a * z - 2 * b * z + 3 * z
            correct = correct and z == a * b
        objective = sum(sign * sum(c for support, c in polynomial
                                   if mask & support == support)
                        for sign, polynomial in indicators)
        h = energy - target.bound
        check(5, h == target.budget.penalty * penalty + objective,
              (instance, "expanded versus unexpanded energy", mask))
        feasible = energy <= target.bound
        difference = differences[mask & ((1 << n) - 1)]
        check(2, feasible == (correct and difference <= 0)
              and (correct or h >= 1), (instance, "backward/faulty gate", mask))
        if feasible:
            found = True
            WORK["feasible_extractions"] += 1
            bits = bits_of(mask, N)
            extracted = extract_solution(target, bits)
            check(3, extracted == bits[:n]
                  and source_difference(instance, extracted) <= 0,
                  (instance, "feasible extraction", mask))
        elif first_infeasible is None:
            first_infeasible = bits_of(mask, N)
    check(2, found == any(d <= 0 for d in differences),
          (instance, "existential decision equivalence"))
    # Invalid lengths include zero-variable targets and unused universe bits.
    check(3, rejected(lambda: extract_solution(target, (0,) * (N + 1))),
          "long target witness accepted")
    if N:
        check(3, rejected(lambda: extract_solution(target, (0,) * (N - 1)))
              and rejected(lambda: extract_solution(target, (2,) + (0,) * (N - 1))),
              "short/nonbinary target witness accepted")
    if first_infeasible is not None:
        check(3, rejected(lambda: extract_solution(target, first_infeasible)),
              "infeasible target witness accepted")


def symbolic_checks():
    u, v, z = sp.symbols("u v z")
    first = gate_polynomial(literal(0, True), literal(1, True), 2)
    later = gate_polynomial(literal(0), literal(1, True), 2)
    symbols = [u, v, z]
    for actual, a, b, support, norm, constant in (
            (first, 1-u, 1-v, 6, 8, 1),
            (later, u, 1-v, 5, 7, 0)):
        expression = sum(c * sp.prod(symbols[i] for i in key)
                         for key, c in actual.items())
        check(1, sp.expand(expression - (a*b - 2*a*z - 2*b*z + 3*z)) == 0,
              "constructed gate expansion against SymPy")
        check(1, (len([k for k in actual if k]),
                  sum(abs(c) for k, c in actual.items() if k), actual.get((), 0))
              == (support, norm, constant), "gate support/norm/constant")
        for assignment in product((0, 1), repeat=3):
            value = int(expression.subs(dict(zip(symbols, assignment))))
            aval = int(a.subs(dict(zip(symbols, assignment))))
            bval = int(b.subs(dict(zip(symbols, assignment))))
            check(1, value >= 0 and (value == 0) == (assignment[2] == aval*bval),
                  "constructed gate truth table")
    # Symbolic upper-bound slack, derived from measured gate templates above.
    n, r, s, g, slack = sp.symbols("n r s g slack", nonnegative=True, integer=True)
    variable_bound = n + n * (r + s)
    quadratic_bound = 6 * n * (r + s) + r + s
    stored_bound = 6 * g + r + s
    check(1, sp.expand((variable_bound - (n + g)).subs(g, n*(r+s)-slack)) == slack,
          "variable-bound slack")
    gate_edges = max(sum(len(key) == 2 for key in poly) for poly in (first, later))
    gate_entries = max(sum(bool(key) for key in poly) for poly in (first, later))
    check(1, sp.expand((quadratic_bound - gate_edges*g).subs(g, n*(r+s)-slack)
                      - (3*n*(r+s) + 3*slack + r+s)) == 0,
          "off-diagonal-bound slack")
    check(1, sp.expand(stored_bound - (gate_entries*g + r+s)) == 0,
          "stored support from six-entry gate template and indicators")
    gate_norm = max(sum(abs(c) for k, c in poly.items() if k) for poly in (first, later))
    gate_constant = max(abs(poly.get((), 0)) for poly in (first, later))
    check(1, sp.expand((gate_norm*(r+1)*g+r+s)
                      - (gate_constant*(r+1)*g+r+s) - 7*(r+1)*g) == 0,
          "constant-envelope slack")
    for k in range(6):
        instance = Instance(5, (tuple(range(k, 5)),), ())
        target = reduce(instance)
        check(1, target.num_vars - instance.n == max(0, k - 1),
              "piecewise chain-length formula including k=0,1")

    # These boundary certificates describe symbolic target sizes, not allocated families.
    for rcount, scount, gates in ((0, 0, I64_MAX//8), (1, 0, (I64_MAX-1)//16),
                                  (2, 3, (I64_MAX-5)//24)):
        budget = numeric_certificate(0, rcount, scount, gates)
        check(1, budget.absolute_sum == 8*budget.penalty*gates + rcount+scount
              and rejected(lambda: numeric_certificate(0, rcount, scount, gates+1)),
              "i64 certificate: largest admitted gate count and next")
    edge = numeric_certificate(0, I64_MAX-1, 1, 0)
    check(1, edge.penalty == I64_MAX and edge.absolute_sum == I64_MAX
          and rejected(lambda: numeric_certificate(0, I64_MAX-1, 2, 0))
          and rejected(lambda: numeric_certificate(0, I64_MAX, 0, 0)),
          "zero-gate path and count/penalty boundaries")
    edge = numeric_certificate(INDEX_MAX-1, 0, 0, 1)
    check(1, edge.num_vars == INDEX_MAX
          and rejected(lambda: numeric_certificate(INDEX_MAX, 0, 0, 1))
          and rejected(lambda: numeric_certificate(0, INDEX_MAX, 1, 0)),
          "derived variable and family size boundaries")
    for instance in (Instance(3, ((),), ()), Instance(3, (), ((),)),
                     Instance(3, (), (tuple(range(3)),))):
        target = reduce(instance)
        limit = target.budget.absolute_sum
        admitted = reduce(instance, limit=limit)
        check(1, admitted.entries == target.entries and admitted.bound == target.bound
              and rejected(lambda: reduce(instance, limit=limit-1)),
              "real constructed target at and beyond its certificate limit")
    target = reduce(Instance(3, ((),), ()))
    admitted = reduce(Instance(3, ((),), ()), index_limit=target.num_vars)
    check(1, admitted.num_vars == target.num_vars
          and rejected(lambda: reduce(Instance(3, ((),), ()),
                                      index_limit=target.num_vars-1)),
          "real constructed target at and beyond variable limit")


def finite_instances():
    for n in range(3):
        subsets = [tuple(i for i in range(n) if mask & (1 << i))
                   for mask in range(1 << n)]
        families = [family for length in range(3)
                    for family in product(subsets, repeat=length)]
        for r_sets in families:
            for s_sets in families:
                yield Instance(n, r_sets, s_sets)
    for n in range(3, 6):
        subsets = [tuple(i for i in range(n) if mask & (1 << i))
                   for mask in range(1 << n)]
        for m in range(3):
            for r in range(m+1):
                for entries in product(subsets, repeat=m):
                    yield Instance(n, entries[:r], entries[r:])
        rng = random.Random(2026110300 + n)
        for m in (3, 4):
            samples = set()
            while len(samples) < 300:
                r = rng.randrange(m+1)
                entries = tuple(rng.choice(subsets) for _ in range(m))
                instance = Instance(n, entries[:r], entries[r:])
                g = sum(max(0, n-len(a)-1) for a in entries)
                if n + g > 12:
                    SAMPLE_REJECTIONS[(n, m)] += 1
                    continue
                if instance not in samples:
                    samples.add(instance)
                    yield instance


@st.composite
def raw_membership_instances(draw):
    n = draw(st.integers(0, 5))
    m = draw(st.integers(0, 2))
    r = draw(st.integers(0, m))
    entry = st.lists(st.integers(0, n-1), max_size=n+2).map(tuple) if n else st.just(())
    entries = tuple(draw(st.lists(entry, min_size=m, max_size=m)))
    return Instance(n, entries[:r], entries[r:])


@st.composite
def repeated_boundary_instances(draw):
    n = draw(st.integers(0, 5))
    r, s = draw(st.integers(0, 2)), draw(st.integers(0, 4))
    entry = tuple(draw(st.sets(st.integers(0, n-1)))) if n else ()
    # Long chains/repeated entries against constant S terms, including r < s.
    return Instance(n, (entry,) * r, (tuple(range(n)),) * s)


@seed(2026110301)
@settings(max_examples=300, database=None, deadline=None)
@given(raw_membership_instances())
def raw_membership_property(instance):
    WORK["raw_hypothesis_examples"] += 1
    verify_instance(instance)
    # Raw-list order and duplicates must not change the actual constructed target.
    normalized = Instance(instance.n, tuple(tuple(sorted(set(a))) for a in instance.r_sets),
                          tuple(tuple(sorted(set(a))) for a in instance.s_sets))
    raw, clean = reduce(instance), reduce(normalized)
    check(5, raw.as_dict() == clean.as_dict(), "membership normalization")


@seed(2026110302)
@settings(max_examples=300, database=None, deadline=None)
@given(repeated_boundary_instances())
def repeated_boundary_property(instance):
    WORK["boundary_hypothesis_examples"] += 1
    verify_instance(instance)
    target = reduce(instance)
    # Explicit adversarial auxiliary vector, evaluated against the target threshold.
    values = list(extend(instance, (0,) * instance.n))
    if target.gates:
        values[target.gates[0][2]] ^= 1
        check(5, target_value(target, values) - target.bound >= 1,
              "one faulty gate cannot buy feasibility")
    else:
        check(5, (target_value(target, values) <= target.bound)
              == (source_difference(instance, (0,) * instance.n) <= 0),
              "constant and affine branches")


EXPECTED_ENTRIES = {
    (0, 0): -8, (2, 2): -8, (3, 3): -8,
    (0, 2): 4, (0, 3): 4, (2, 3): 4,
    (4, 4): -5, (5, 5): -5, (6, 6): -5,
    (2, 4): 8, (3, 4): 8, (0, 5): 8,
    (3, 5): 8, (0, 6): 8, (2, 6): 8,
}


def example_checks():
    r_sets = ((0, 1), (1, 2), (1, 3))
    full = tuple(range(4))
    x = (0, 1, 0, 0)
    witness = x + (1, 1, 1)
    for section, s, bound, budget in ((6, 2, -14, 101), (7, 4, -16, 103)):
        instance = Instance(4, r_sets, (full,) * s)
        target = reduce(instance)
        check(section, {(i, j): c for i, j, c in target.entries} == EXPECTED_ENTRIES,
              "every hand-derived canonical coefficient")
        check(section, (target.num_vars, target.bound, len(target.gates),
                        len(target.entries), sum(i != j for i, j, _ in target.entries),
                        target.budget.penalty, target.budget.absolute_sum)
              == (7, bound, 3, 15, 9, 4, budget), "example parameters and constants")
        check(section, sum(abs(c) for _, _, c in target.entries) == 99,
              "example coefficient absolute sum")
        check(section, target_value(target, witness) == -15
              and target_value(target, witness) - bound == s-3,
              "canonical energy and threshold offset")
        if section == 6:
            check(6, source_difference(instance, x) == -1
                  and extract_solution(target, witness) == x, "nonempty YES witness")
            faulty = x + (0, 1, 1)
            check(6, target_value(target, faulty) == -10
                  and target_value(target, faulty)-bound == 4
                  and rejected(lambda: extract_solution(target, faulty)), "flipped gate")
            candidate = (1, 1, 0, 0)
            consistent = candidate + (1, 0, 0)
            check(6, source_difference(instance, candidate) == 1
                  and extend(instance, candidate) == consistent
                  and target_value(target, consistent) == -13
                  and rejected(lambda: extract_solution(target, consistent)),
                  "infeasible source candidate")
        else:
            check(7, all(source_difference(instance, bits_of(mask, 4)) >= 1
                         for mask in range(16))
                  and min(target_value(target, bits_of(mask, 7))
                          for mask in range(128)) == -15 > target.bound,
                  "all source and target vectors infeasible")
        verify_instance(instance)


def special_cases():
    # No new constructor branches are needed for these admitted inputs.
    for n in range(6):
        full = tuple(range(n))
        for r_sets, s_sets in (((), ()), ((), (full,)), (((),), ()),
                               ((full,), ()), ((full,), (full,)),
                               (((), ()), (full,)), ((full,), ((),)),
                               ((full, full), (full, full, full))):
            verify_instance(Instance(n, r_sets, s_sets))
    plain = Instance(3, ((0, 2),), ((1,),))
    repeated = Instance(3, ((2, 0, 2, 0),), ((1, 1),))
    a, b = reduce(plain), reduce(repeated)
    check(5, a.as_dict() == b.as_dict(), "explicit duplicated/unsorted elements")
    verify_instance(repeated)
    constant_no = reduce(Instance(3, (), ((0, 1, 2),)))
    check(7, constant_no.num_vars == 3 and constant_no.entries == ()
          and constant_no.bound == -1
          and all(target_value(constant_no, bits_of(mask, 3)) > constant_no.bound
                  for mask in range(8)), "empty R with full S is NO")
    cancellation = reduce(Instance(3, ((0, 1),), ((0, 1),)))
    check(5, cancellation.entries == () and cancellation.num_vars == 3
          and all(target_value(cancellation, bits_of(mask, 3)) == 0
                  for mask in range(8)), "omit cancelled terms, preserve variables")
    # Invalid source inputs are tested through construction, never silently normalized.
    check(5, rejected(lambda: reduce(Instance(0, ((0,),), ())))
          and rejected(lambda: reduce(Instance(3, (), ((-1,),)))),
          "out-of-universe elements rejected")


def main():
    global FAILURES
    try:
        symbolic_checks()
        example_checks()
        special_cases()
        for instance in finite_instances():
            verify_instance(instance)
        raw_membership_property()
        repeated_boundary_property()
        for section in SECTIONS:
            if not COUNTS[section]:
                raise AssertionError(f"empty audit section {section}")
        if sum(COUNTS.values()) < 10000:
            raise AssertionError("fewer than 10000 checks")
    except Exception:
        if not FAILURES:
            FAILURES += 1
        traceback.print_exc()
    print("CLAIMS: gate/size/guard derivations=1; equivalence/faulty gates=2; "
          "extraction=3; sizes=4; sparse form/expansion/numeric safety=5; YES=6; NO=7")
    print("SCOPE: exhaustive ordered r,s<=2 for n<=2; exhaustive m<=2 for n=3..5; "
          "300 distinct samples per (n,m), n=3..5,m=3,4, N<=12; every target vector")
    print(f"INSTANCES: {sum(CASES.values())}; by (n,m): {dict(sorted(CASES.items()))}")
    print(f"WORK: {dict(WORK)}")
    print(f"SAMPLING: seeds 2026110303..2026110305; N>12 rejections {dict(SAMPLE_REJECTIONS)}")
    print("HYPOTHESIS: 2 strategies, max_examples=300 each, seeds 2026110301/2026110302; "
          "database=None, deadline=None; previously checked instances reused")
    for section, name in SECTIONS.items():
        print(f"SECTION {section}: {COUNTS[section]} checks, {name}")
    print(f"TOTAL: {sum(COUNTS.values())} checks, {FAILURES} failures")
    return bool(FAILURES)


if __name__ == "__main__":
    sys.exit(main())
Typst proof
#set document(title: "Unit comparative containment to decision QUBO")
#set page(margin: 22mm)
#set text(size: 10pt)

= Theorem

For every admitted instance of `ComparativeContainment/One`, the construction
below gives, over mathematical integers, an equivalent decision QUBO. On inputs
passing the explicit machine guards it gives `DecisionQUBO/i64`, preserves YES
and NO, and extracts a source witness from every target witness. Machine guard
failure is a typed reduction error, never a NO answer. Completed `Or` values
map identically; this statement does not assert an optimization-value map.

== Type resolution and admitted domain

The inspected library revision is `e7bacb37`. File paths in this document are
relative to its root.

- `src/models/set/comparative_containment.rs`: the generic `Problem` impl has
  `Solution = Vec<bool>` and `Value = crate::types::Or` directly. Substitution
  of `W = One` leaves `Value = Or`. `src/types.rs` gives
  `<One as WeightElement>::Sum = i64` for the internal containment sums.
- `src/models/algebraic/qubo.rs`: `QUBO<W>::Value = Min<W::Sum>`;
  `src/types.rs` gives `<i64 as WeightElement>::Sum = i64`, hence `Min<i64>`.
  `src/models/decision.rs` sets `Decision<P>::Value = Or` and inherits
  `P::Solution`. Its bound type is `OptimizationValue::Inner`, which is `i64`
  for `Min<i64>`; `meets_bound` in `src/types.rs` means value at most bound.
  Thus the target is `Or` with `Vec<bool>` witnesses and an `i64` bound.
- Full-domain value compatibility: *PASS*, `Or` to `Or`. No type remains
  unresolved. `docs/src/design.md`, “Result mappings”, requires the identity
  aggregate map for this decision-to-decision rule.

Let the universe size be the nonnegative integer $n$, and write
$U = {0, dots, n - 1}$, interpreted as empty when $n=0$. Let $R$ and $S$ be
indexed families with respective lengths $r$ and $s$, and put $m=r+s$.
Every element listed inside a family entry must belong to $U$. Repeated
elements have membership semantics; repeated family entries count separately.
There are no nonemptiness, disjointness, or distinctness assumptions. All
weights are `One`. A witness has exactly $n$ Boolean coordinates and selects
the subset $Y$ of coordinates equal to one. The source accepts precisely when
the count of entries of $R$ containing $Y$ is at least the corresponding count
for $S$. Its evaluated unit sums must fit `i64`; the guards below ensure this.
The constructors reject out-of-universe elements and incorrect weight counts;
source evaluation rejects wrong witness lengths.

The target stores sorted unique nonzero triples `(i,j,q)` with
`0 <= i <= j < N`; diagonal entries represent linear terms. Its polynomial
is the sum of `q*y[i]*y[j]`, with each entry used once. There is no implicit
symmetric doubling and no stored constant. Evaluation checks witness length
and uses checked `i64` addition in entry order. The registered target
parameters are exactly `num_vars` and `num_quadratic_terms`.
*The latter counts only off-diagonal nonzero entries in current main*, unlike
the proposed issue's description of that metric.

== Search and provenance

Before deriving this instance-specific construction, the following sources
and siblings were inspected:

- Endre Boros and Aritanan Gruber, _On Quadratization of Pseudo-Boolean
  Functions_ (2014), #link("https://arxiv.org/pdf/1404.6538")[arXiv:1404.6538],
  section 2.1, page 3. Equation (4) states the zero-versus-positive gate
  condition; the polynomial immediately following it is Rosenberg's
  `ab - 2az - 2bz + 3z`. This supplies the gate, not a containment rule.
- The #link("https://github.com/CodingThrust/problem-reductions/issues/401")[ComparativeContainment antecedent]
  suggests containment indicators and comparing their weighted counts in an
  integer formulation. Credit that indicator idea to this antecedent and
  the specific containment-to-QUBO adaptation to Thiago Costa's proposal.
  Its “R empty is always YES” sentence is not used: an S entry equal to U
  is a counterexample, included below.
- `src/rules/ksatisfiability_qubo.rs` has the same Rosenberg gate in a private
  clause-specific helper `add_3sat_clause_penalty`; it also uses complemented
  literals, moves a constant into a `Decision<QUBO<i64>>` bound, and validates
  a witness before extracting its prefix. No reusable standalone product-gate
  helper was found in the QUBO siblings. The present chain, penalty sufficiency,
  bounds and edge-case arguments are *derived here*, independently of the
  proposal's reported checks. No new quadratization theorem is claimed.

Before construction, a fresh checkout build
`cargo build -p problemreductions-cli --bin pred` succeeded. Then
`pred path ComparativeContainment/One DecisionQUBO/i64 --limit all` reported
“No reduction path” (exit 1, the CLI's absence status). `pred show` resolved
both exact variants and their parameter names. Redundancy result: *PASS*,
no existing witness path at this revision, hence no dominating path to compare.

== Construction

Define the binary universe coordinates $x_0, dots, x_(n-1)$ first. For each
family entry $A$, let $k_A=n-|A|$, and let $g$ be the sum, with entry
multiplicity, of $max(0,k_A-1)$. Put $N=n+g$ and $M=r+1$.

1. Visit entries of $R$, then entries of $S$, each in input order. For entry
   $A$, enumerate its distinct complement elements in increasing order as
   $a_1, dots, a_(k_A)$. Define its intended containment indicator
   $t_A = product_(j=1)^(k_A) (1-x_(a_j))$.
2. If $k_A=0$, use the constant 1. If $k_A=1$, use the affine expression
   $1-x_(a_1)$. Otherwise allocate $k_A-1$ fresh bits consecutively after the
   first $n$ bits. The first gate multiplies the first two complemented
   coordinates; each later gate multiplies the preceding fresh bit by the
   next complemented coordinate. Use the last fresh bit as $t_A$.
   Fresh bits are not shared across entries, even for repeated entries.
3. For every gate with binary inputs $a,b$ and output $z$, add
   $P(a,b,z)=a b-2a z-2b z+3z$. Inputs here are affine complemented universe
   bits or preceding chain bits. Form
   $H=M sum_"gates" P + sum_(A in S)t_A - sum_(A in R)t_A$.
4. Expand, replace squared binary coordinates by themselves, collect like
   terms into the upper triangle and omit zero coefficients. Write $H=C+Q$,
   where $C$ is constant and $Q$ has no constant. Construct the sparse QUBO
   with $N$ variables through its fallible constructor and use decision bound
   $b=-C$. Thus `Q <= b` is exactly `H <= 0`.
5. Enforce the numeric guards below with checked arithmetic, and propagate
   target-construction failures as typed reduction errors. Canonical target
   identity for later comparisons is the pair `(N, sorted entries)` together
   with $b$, using precisely the allocation order above; no permutation or
   energy rescaling is needed.

== Overhead and complexity

#table(
  columns: (2.1fr, 2.2fr, 2.7fr),
  [Target parameter or auxiliary quantity], [Instance formula], [Registered bound in source parameters],
  [`num_vars`], [$N=n+g$ exactly], [`universe_size + universe_size*(num_r_sets+num_s_sets)` (upper bound)],
  [`num_quadratic_terms` (off-diagonal)], [At most $3g$], [`6*universe_size*(num_r_sets+num_s_sets)+num_r_sets+num_s_sets` (upper bound)],
  [All stored entries, including diagonal], [At most $6g+r+s$], [Not a registered target parameter],
  [Gates], [$g=sum_A max(0,k_A-1)$ exactly], [At most $n(r+s)$],
  [Coefficient absolute-sum certificate], [$B=8(r+1)g+r+s$], [Not a registered target parameter],
)

For a complement length $k=0,1$, there are zero gates; for $k>=2$ there are exactly $k-1$.
In every case `max(0,k-1) <= n`, including $n=0$, giving $g<=n m$.
For distinct coordinates $u,v,z$, a first gate expands to
`1-u-v+uv-z+2uz+2vz`: six nonconstant entries, three off-diagonal,
nonconstant coefficient absolute sum 8, and constant 1.
A later gate with previous bit $w$, next universe bit $u$ and new bit $z$
expands to `w-wu-2wz+z+2uz`: five entries, three off-diagonal,
absolute sum 7, constant 0. Every ungated indicator contributes at most one
nonconstant entry and constant absolute value at most one; a gated final
indicator contributes one diagonal entry. Summing these supports gives the
stored-entry bound. Collection and cancellation cannot increase support or
the absolute-sum bound. No indicator adds an off-diagonal entry, giving $3g$;
the displayed registered bound is conservative since $3g<=6n m+m$.
Both registered transforms are upper bounds, never exact: full-universe
entries give $g=0$, and cancellation changes the support.

Complement enumeration takes $O(n m)$ membership records after building
membership sets (also read every raw input element, including duplicates).
Sparse collection costs $O((g+m) log(2+g+m))$ map operations; full input reading
also costs its raw listing length. The $n$ original coordinates are preserved
even if unused. The output has polynomial size and integer bit length.

== Numeric contract

Let $L=2^63-1$ and let $J$ be the platform's maximum index (`usize::MAX`), with
canonical parameter results also representable in `u64`. Before allocation,
check the sum $m=r+s$, complement gate total $g$, and $N=n+g$ for index overflow.
Convert $r,s,g$ to `i64` using checked conversions, and check $M=r+1<=L$.
Use checked multiplication and addition to require $B=8M g+r+s<=L$.
Reject every failed operation with a typed reduction arithmetic error.
These are sufficient guards, not a maximal representability characterization.
In particular $r=L,g=0,s=0$ fails the chosen $M$ guard even though $B=L$.
No multiplication order may reject solely because it computes `8*M` when
$g=0$: multiply $M*g$ first. Registered bounds are mathematical parameter
expressions, not requested allocation sizes.

The sum of absolute *nonconstant* coefficients of every partial construction
is at most $8M g+r+s=B$. The absolute constant is separately at most
$M g+r+s<=B$. These are separate bounds; their sum need not be at most $B$.
Every collected coefficient, partial coefficient sum, QUBO evaluation prefix
on every assignment (including infeasible assignments), constant accumulation,
and the threshold $-C$ therefore lies in $[-L,L]$. Source unit count sums are
at most $r,s<=L$. Every arithmetic step and sign-changing conversion remains
checked, including the final negation. This prevents overflow in both
construction and target evaluation, not just on satisfying assignments.

== Correctness

*Gate lemma.* The rows below list `P(a,b,0), P(a,b,1)`:
`(0,0): (0,3); (0,1): (0,1); (1,0): (0,1); (1,1): (1,0)`.
Thus $P>=0$ and it is zero exactly when $z=a b$. A faulty gate contributes
at least 1 before multiplication by $M$.

*Forward (⇒).* Start with a source witness and put its selected universe
bits first. Fill every chain with the successive true products. Every gate
penalty is zero. Membership in a set means that no selected element lies in
its complement, so each final indicator equals the truth of $Y subset.eq A$.
Consequently $H$ is the S containment count minus the R containment count,
which is at most zero for the source witness. Because $Q=H-C$, this extension
satisfies $Q<=-C=b$.

*Backward (⇐).* Start with any target witness, including an arbitrary setting
of its auxiliary bits. Every indicator expression in the constructed target
is binary: it is 1, a complemented coordinate, or a fresh bit. Their signed
sum is therefore at least $-r$, irrespective of correctness of any chain.
If any gate were faulty, nonnegativity of all penalties would give
$H>=M-r=1$, contradicting the target threshold $H<=0$. Hence all gates are
correct. Induction in allocation order establishes that each chain bit is
its intended prefix product. The ungated expressions already equal their
containment indicators. Thus $H$ is the actual S count minus actual R count,
and the first $n$ target coordinates satisfy the source. This uses neither
a presumed optimum nor a repair of a faulty witness.

== Solution extraction

Check target length and evaluate its decision predicate exactly once. Reject
wrong length, non-Boolean coordinates, or `Q > b` as extraction errors; return
the first $n$ coordinates only after successful validation. The backward
argument proves that every returned prefix is a source witness. With an
empty universe this returns the empty Boolean vector. Mapping a completed
`Or(false)` does not attempt extraction; both completed `Or` values map by
identity. The empty conjunction of gates is permitted.

== Special cases

- Empty set $A$: its complement is all of $U$, so its indicator is 1 only
  for empty $Y$. For $n=0$, this entry is also the full universe and uses 1
  with no gates. For $n=1$ it uses the single complemented bit.
- Full universe $A=U$: use constant 1, no gates; move its signed contribution
  into $C$. If every entry is full universe then $Q=0$, $C=s-r$, and the
  decision is exactly $r>=s$, independent of the selected subset.
- Empty families: $R=S=()$ gives $g=C=0$, $Q=0$, bound 0 and all subsets
  accepted. If just $S$ is empty, every subset is a source witness. If just
  $R$ is empty, a witness must avoid containment by every S entry; one full
  universe S entry makes this impossible. For example $n=3,R=(),S=(U)$
  gives three target variables, no entries, and bound −1: a NO instance.
- Empty universe: every admitted entry is empty and full, the only witness
  is the empty vector, and both sides accept exactly when $r>=s$.
- Repeated elements inside a set do not alter its complement. Unsorted raw
  listings do not change membership. Repeated entries keep distinct chain
  bits and distinct unit contributions; no deduplication of entries occurs.
- A faulty gate is excluded even if all R indicators are set to 1 and all
  S indicators to 0: the preceding bound is still $M-r=1$.
- Zero coefficients after cancellation are omitted, with the original $N$
  retained. A gate-free target can have no stored coefficients and a
  negative bound; it is then infeasible, rather than malformed.

== YES example

Take $n=4$, `R=({0,1},{1,2},{1,3})` and two copies of $U$ in $S$.
Thus $r=3,s=2,M=4,g=3,N=7$, stored entries 15, off-diagonal entries 9,
and $B=8*4*3+3+2=101$. Allocate
`z0=(1-x2)(1-x3)`, `z1=(1-x0)(1-x3)`, `z2=(1-x0)(1-x2)`.
The constant is $C=3*4+2=14$, so the bound is −14. The complete Q is
```
-8(x0+x2+x3) +4(x0*x2+x0*x3+x2*x3) -5(z0+z1+z2)
+8(x2*z0+x3*z0+x0*z1+x3*z1+x0*z2+x2*z2).
```
Its nonconstant absolute coefficient sum is $24+12+15+48=99<=101$.
For `x=(0,1,0,0)`, the selected set is `{1}`, the R/S counts are 3 and 2,
and `z=(1,1,1)`. All penalties vanish: $H=-1$, $Q=-15<=-14$.
Extraction returns `(0,1,0,0)`. Flip `z0` to zero: its gate penalty is 1,
the signed indicator sum becomes 0, giving $H=4$ and $Q=-10$ (rejected).
For `x=(1,1,0,0)`, the counts are 1 and 2, `z=(1,0,0)`, $H=1$,
and $Q=-13$ (rejected). All 128 target vectors are checked, as are all 16
source vectors.

== NO example

Keep $n=4$ and the same $R$, but use four copies of $U$ in $S$.
Now $r=3,s=4,M=4,g=3,N=7$, $C=16$, bound −16, $B=103$.
The same Q has 15 stored entries and 9 off-diagonal entries. Every source
subset is contained by all four S entries and at most three R entries, so
its count difference is at least 1. A consistent target has $H>=1$;
an inconsistent target has $H>=M-r=1$. Hence both sides have no witness.
For `x=(0,1,0,0), z=(1,1,1)`, $H=1$ and $Q=-15>-16$.
The minimum Q over all 128 target vectors is −15. All 16 source vectors
fail the original inequality.

== Executable claim coverage

`constructor.py` has seven nonempty audit sections. Section 1 compares the
constructed gate polynomials and size expressions against SymPy derivations,
including numeric guards on both sides of boundaries. Section 2 tests
independently evaluated source subsets against constructed Q energies,
forward extension, every target vector, faulty gates, and YES/NO equivalence.
Section 3 extracts every feasible target witness and rejects malformed and
infeasible ones. Section 4 measures both registered parameters and total
stored entries. Section 5 checks sparse form, gate order, exact expanded
energy, numeric envelopes and normalization. Sections 6 and 7 reproduce the
two examples, including every coefficient, threshold, counts and energies.

The finite domain is explicit: all ordered families with each length at most
two on universes of size 0–2; all allocations of at most two total entries
on universes 3–5; and 300 distinct samples for each (n,m) with n=3–5 and
m=3,4, conditioned on at most 12 target bits so every target vector can be
enumerated. Two Hypothesis strategies each request 300 examples, seeded
2026110301 and 2026110302, with no database or deadline. They include raw
listing duplicates and repeated entries, empty and full sets, and long
chains. Gate tables cover all eight binary cases. Boundary arithmetic uses
symbolic-size certificates without allocating astronomical families, plus
actual small targets with reduced numeric limits. Infinite-domain correctness,
size inequalities and polynomial running time follow from the proof above;
finite tests do not prove those universal statements. This step is constructor
verification only; independent adversary and cross-comparison remain pending.

@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Verification certificate (continued): adversary script

adv_core.py
"""Independent adversary implementation of ComparativeContainment/One -> DecisionQUBO/i64.

Written from proof.typ alone. The target polynomial is built with generic
multilinear polynomial arithmetic (products of affine factors, x^2 = x), not with
hand-expanded gate formulas, so the expansion claims of the proof can be tested
against it.

Instance: (n, R, S) with R, S tuples of raw listings (tuples of ints, possibly
unsorted, possibly with repeated elements).

Target: dict with N, entries (sorted list of (i, j, q), i <= j, q != 0), bound.
"""

L_I64 = 2**63 - 1


class ReductionError(Exception):
    pass


class ExtractionError(Exception):
    pass


# ---------- multilinear polynomials: dict frozenset(vars) -> int ----------

def p_const(c):
    return {frozenset(): c} if c else {}


def p_var(i):
    return {frozenset([i]): 1}


def p_neg(i):  # 1 - x_i
    return {frozenset(): 1, frozenset([i]): -1}


def p_add(p, q, scale=1):
    r = dict(p)
    for k, v in q.items():
        r[k] = r.get(k, 0) + scale * v
        if r[k] == 0:
            del r[k]
    return r


def p_mul(p, q):
    r = {}
    for a, ca in p.items():
        for b, cb in q.items():
            k = a | b
            r[k] = r.get(k, 0) + ca * cb
    return {k: v for k, v in r.items() if v}


def p_abs_nonconst(p):
    return sum(abs(v) for k, v in p.items() if k)


def lit_poly(lit):
    kind, i = lit
    if kind == "neg":
        return p_neg(i)
    if kind == "var":
        return p_var(i)
    if kind == "const":
        return p_const(1)
    raise AssertionError(lit)


def gate_poly(a, b, z, mutant=None):
    """Rosenberg P(a,b,z) = ab - 2az - 2bz + 3z with affine inputs."""
    A, B, Z = lit_poly(a), lit_poly(b), p_var(z)
    P = p_mul(A, B)
    if mutant != "drop_az":
        P = p_add(P, p_mul(A, Z), -2)
    P = p_add(P, p_mul(B, Z), -2)
    P = p_add(P, Z, {"gate_coef": 2, "gate_coef_4": 4}.get(mutant, 3))
    return P


# ---------- construction ----------

def validate_source(n, R, S, mutant=None):
    if not (isinstance(n, int) and n >= 0):
        raise ReductionError("bad universe size")
    for A in list(R) + list(S):
        for e in A:
            if not (isinstance(e, int) and 0 <= e and (e < n or mutant == "no_universe_check")):
                raise ReductionError("element outside universe")


def complement(n, A, mutant=None):
    s = set(A)
    comp = [v for v in range(n) if v not in s]
    if mutant == "reverse_complement":
        comp.reverse()
    if mutant == "raw_len":  # bug: length taken from the raw listing
        comp = comp[: max(0, n - len(A))]
    return comp


def gate_total(n, R, S):
    return sum(max(0, len(complement(n, A)) - 1) for A in list(R) + list(S))


J_USIZE = 2**64 - 1


def numeric_guard_counts(r, s, g, L=L_I64, mutant=None, n=0, J=J_USIZE):
    """Guards of the proof's numeric contract on counts. Returns B or raises."""
    jlim = J + 1 if mutant == "index_off_by_one" else J
    if r + s > J or g > J or n + g > jlim:
        raise ReductionError("index")
    for v in (r, s, g):
        if v > L:
            raise ReductionError("conversion")
    M = r + 1
    if M > L and mutant != "no_m_guard":
        raise ReductionError("M")
    if mutant == "eight_m_first":
        if 8 * M > L:
            raise ReductionError("8M")
    t = M * g  # multiply M*g first
    if t > L:
        raise ReductionError("Mg")
    t = 8 * t
    if t > L:
        raise ReductionError("8Mg")
    t = t + r + s
    limit = L + 1 if mutant == "guard_off_by_one" else L
    if t > limit or (mutant == "guard_rejects_equal" and t == limit):
        raise ReductionError("B")
    return t


def numeric_guard(n, R, S, L=L_I64, mutant=None, J=J_USIZE):
    return numeric_guard_counts(len(R), len(S), gate_total(n, R, S), L, mutant, n=n, J=J)


def reduce(n, R, S, L=L_I64, mutant=None, trace=None, J=J_USIZE):
    """Build the target. `trace` (a list) receives the running polynomial's
    (abs nonconstant sum, abs constant) after every added piece."""
    validate_source(n, R, S, mutant)
    R = [tuple(A) for A in R]
    S = [tuple(A) for A in S]
    B = numeric_guard(n, R, S, L, mutant, J)
    r, s = len(R), len(S)
    M = r + 1
    if mutant == "weak_penalty":
        M = r
    if mutant == "strong_penalty":  # still a correct reduction, but outside the B envelope
        M = 10 * (r + 1)
    entries_order = [("R", A) for A in R] + [("S", A) for A in S]
    if mutant == "S_before_R":
        entries_order = [("S", A) for A in S] + [("R", A) for A in R]
    if mutant == "dedupe_entries":
        seen, eo = set(), []
        for fam, A in entries_order:
            key = (fam, frozenset(A))
            if key not in seen:
                seen.add(key)
                eo.append((fam, A))
        entries_order = eo
    nxt = n
    gates = []        # (a_lit, b_lit, z)
    indicators = []   # (family, lit)
    for fam, A in entries_order:
        comp = complement(n, A, mutant)
        k = len(comp)
        if k == 0:
            ind = ("const", None)
        elif k == 1:
            ind = ("const", None) if mutant == "k1_as_const" else ("neg", comp[0])
        else:
            lit_of = (lambda u: ("var", u)) if mutant == "chain_plain_input" else (lambda u: ("neg", u))
            prev = lit_of(comp[0])
            seq = comp[1:] + (comp[-1:] if mutant == "chain_extra_gate" else [])
            for u in seq:
                z = nxt
                nxt += 1
                gates.append((prev, lit_of(u), z))
                prev = ("var", z)
            ind = prev
        indicators.append((fam, ind))
    N = nxt + (1 if mutant == "extra_vars" else 0)
    H = {}
    for (a, b, z) in gates:
        P = gate_poly(a, b, z, mutant)
        H = p_add(H, P, M)
        if trace is not None:
            trace.append((p_abs_nonconst(H), abs(H.get(frozenset(), 0))))
    for fam, lit in indicators:
        sign = 1 if fam == "S" else -1
        if mutant == "sign_flip":
            sign = -sign
        H = p_add(H, lit_poly(lit), sign)
        if trace is not None:
            trace.append((p_abs_nonconst(H), abs(H.get(frozenset(), 0))))
    if mutant == "dense_noise":
        for j in range(N):
            for i in range(j):
                H = p_add(H, {frozenset([i, j]): 1})
    C = H.get(frozenset(), 0)
    entries = []
    for key, q in H.items():
        if not key:
            continue
        idx = sorted(key)
        assert len(idx) in (1, 2), "degree > 2"
        i, j = (idx[0], idx[0]) if len(idx) == 1 else (idx[0], idx[1])
        if mutant == "offdiag_double" and i != j:
            q = 2 * q
        if mutant == "saturate":  # saturating arithmetic bug
            q = max(-(L // 8), min(L // 8, q))
        entries.append((i, j, q))
    if mutant == "zero_entries":
        diag = {i for i, j, _ in entries if i == j}
        entries += [(i, i, 0) for i in range(N) if i not in diag]
    entries.sort()
    if mutant == "unsorted_entries":
        entries.reverse()
    for _, _, q in entries:
        if abs(q) > L:
            raise ReductionError("coefficient")
    if abs(C) > L:
        raise ReductionError("constant")
    bound = -C
    if mutant == "bound_off_by_one":
        bound += 1
    if mutant == "bound_too_tight":
        bound -= 1
    return {
        "n": n, "N": N, "entries": entries, "bound": bound, "C": C, "M": M,
        "r": r, "s": s, "g": len(gates), "B": B, "gates": gates,
        "indicators": indicators, "H": H,
    }


# ---------- evaluation ----------

ORACLE_MUTANT = None  # set by the harness; "oracle_superset" breaks the source oracle


def source_counts(n, R, S, x):
    Y = {v for v in range(n) if x[v]}
    cR = sum(1 for A in R if Y <= set(A))
    if ORACLE_MUTANT == "oracle_superset":
        cS = sum(1 for A in S if Y >= set(A))
    else:
        cS = sum(1 for A in S if Y <= set(A))
    return cR, cS


def source_eval(n, R, S, x):
    if len(x) != n:
        raise ValueError("length")
    cR, cS = source_counts(n, R, S, x)
    return cR >= cS


def target_value(T, y):
    """QUBO energy: each stored entry used once."""
    if len(y) != T["N"]:
        raise ValueError("length")
    v = 0
    for i, j, q in T["entries"]:
        if y[i] and y[j]:
            v += q
    return v


def target_eval(T, y):
    return target_value(T, y) <= T["bound"]


def extract(T, y, mutant=None):
    if mutant != "extract_no_validate":
        if not isinstance(y, (list, tuple)) or len(y) != T["N"]:
            raise ExtractionError("wrong length")
        if any(not isinstance(b, bool) for b in y):
            raise ExtractionError("non-Boolean")
        if not target_eval(T, y):
            raise ExtractionError("Q > b")
    x = list(y[: T["n"]])
    return x[::-1] if mutant == "extract_permuted" else x


def lit_val_mask(lit, mask):
    kind, i = lit
    if kind == "const":
        return 1
    bit = (mask >> i) & 1
    return 1 - bit if kind == "neg" else bit


def direct_H(T, mask):
    """H straight from the definition; returns (H, number of faulty gates)."""
    M = T["M"]
    H = 0
    faults = 0
    for a, b, z in T["gates"]:
        av, bv, zv = lit_val_mask(a, mask), lit_val_mask(b, mask), (mask >> z) & 1
        P = av * bv - 2 * av * zv - 2 * bv * zv + 3 * zv
        H += M * P
        if zv != av * bv:
            faults += 1
    for fam, lit in T["indicators"]:
        H += lit_val_mask(lit, mask) * (1 if fam == "S" else -1)
    return H, faults


def forward_mask(T, xmask):
    """Fill every chain with true products."""
    mask = xmask
    for a, b, z in T["gates"]:
        if lit_val_mask(a, mask) * lit_val_mask(b, mask):
            mask |= 1 << z
        else:
            mask &= ~(1 << z)
    return mask


def mask_to_vec(mask, N):
    return [bool((mask >> i) & 1) for i in range(N)]
adversary.py
#!/usr/bin/env python3
"""Adversary harness for ComparativeContainment/One -> DecisionQUBO/i64.

Independent of constructor.py (never imported). Usage:
    python3 adversary.py            # full run
    python3 adversary.py --quick    # reduced run
Environment (used by mutants.py only):
    ADV_MUTANT=<name>   run against a deliberately broken construction (adv_core.reduce)
    ADV_CLAIM=<name>    falsify one claimed constant/formula of the proof (CLAIMS below)
    ADV_SITES_OUT=path  write executed / failing check call sites as JSON
Ends with: TOTAL: <checks> checks, <failures> failures
"""
import itertools
import os
import sys
from collections import Counter
from multiprocessing import Pool

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import adv_core as core  # noqa: E402

MUTANT = os.environ.get("ADV_MUTANT") or None
CLAIM_MUTANT = os.environ.get("ADV_CLAIM") or None
if MUTANT and MUTANT.startswith("oracle_"):
    core.ORACLE_MUTANT = MUTANT
    MUTANT = None
MAX_FAIL_LOG = 8
MAXN = 16  # full target enumeration up to this many target bits

# Every constant or formula the proof claims, in one place, so a claim mutant can falsify it.
CLAIMS = {
    "offdiag_per_gate": 3,                                   # off-diagonal <= 3g
    "stored_per_gate": 6,                                    # stored <= 6g + r + s
    "reg_num_vars": lambda n, m: n + n * m,                  # registered num_vars bound
    "reg_quad": lambda n, m: 6 * n * m + m,                  # registered num_quadratic_terms bound
    "B": lambda M, g, r, s: 8 * M * g + r + s,               # coefficient certificate
    "C_bound": lambda M, g, r, s: M * g + r + s,             # |constant| bound
    "first_norm": 8, "later_norm": 7,                        # gate abs sums
    "first_hand": {(): 1, ("u",): -1, ("v",): -1, ("u", "v"): 1, ("z",): -1, ("u", "z"): 2, ("v", "z"): 2},
    "later_hand": {("w",): 1, ("w", "u"): -1, ("w", "z"): -2, ("z",): 1, ("u", "z"): 2},
    "gate_table": {(0, 0): (0, 3), (0, 1): (0, 1), (1, 0): (0, 1), (1, 1): (1, 0)},
    "gates_per_entry": lambda n: n,                          # max(0, k-1) <= n
    "margin": 1,                                             # M - r = 1
    "yes": dict(r=3, s=2, M=4, g=3, N=7, stored=15, offd=9, B=101, C=14, bound=-14, abs=99,
                Hw=-1, Qw=-15, Hf=4, Qf=-10, H01=1, Q01=-13),
    "no": dict(r=3, s=4, M=4, g=3, N=7, C=16, bound=-16, B=103, minQ=-15),
}
CLAIM_MUTANTS = {
    "offdiag_per_gate": 2, "stored_per_gate": 4,
    "reg_num_vars": lambda n, m: n + m, "reg_quad": lambda n, m: m,
    "B": lambda M, g, r, s: 7 * M * g + r + s, "C_bound": lambda M, g, r, s: g + r + s,
    "first_norm": 7, "later_norm": 6,
    "first_hand": {(): 1, ("u",): -1, ("v",): -1, ("u", "v"): 1, ("z",): -1, ("u", "z"): 1, ("v", "z"): 2},
    "later_hand": {("w",): 1, ("w", "u"): -1, ("w", "z"): -2, ("z",): 1, ("u", "z"): 1},
    "gate_table": {(0, 0): (0, 3), (0, 1): (0, 1), (1, 0): (0, 1), (1, 1): (0, 1)},
    "gates_per_entry": lambda n: n - 1,
    "margin": 2,
    "yes": dict(CLAIMS["yes"], C=13, Qw=-14),
    "no": dict(CLAIMS["no"], minQ=-16),
}
if CLAIM_MUTANT:
    CLAIMS[CLAIM_MUTANT] = CLAIM_MUTANTS[CLAIM_MUTANT]


class Audit:
    def __init__(self):
        self.checks = Counter()
        self.fails = Counter()
        self.log = []
        self.sites = set()
        self.fail_sites = set()

    def ok(self, section, cond, msg=""):
        self.checks[section] += 1
        line = sys._getframe(1).f_lineno
        self.sites.add(line)
        if not cond:
            self.fail_sites.add(line)
            self.fails[section] += 1
            if len(self.log) < MAX_FAIL_LOG:
                self.log.append(f"[{section}] {msg}")
        return cond

    def merge(self, other):
        self.checks.update(other.checks)
        self.fails.update(other.fails)
        self.sites |= other.sites
        self.fail_sites |= other.fail_sites
        for m in other.log:
            if len(self.log) < MAX_FAIL_LOG:
                self.log.append(m)


def reduce_(n, R, S, **kw):
    return core.reduce(n, R, S, mutant=MUTANT, **kw)


def extract_(T, y):
    return core.extract(T, y, mutant=MUTANT)


def raises(fn, exc):
    try:
        fn()
    except exc:
        return True
    return False


def energy_mask(entries, mask):
    v = 0
    for i, j, q in entries:
        if (mask >> i) & 1 and (mask >> j) & 1:
            v += q
    return v


def named_poly(hand, names):
    return {frozenset(names[x] for x in key): c for key, c in hand.items()}


# ---------------- per-instance attack ----------------

def check_structure(a, n, R, S, T, tag):
    sec = "4-size/5-structure"
    r, s, m = len(R), len(S), len(R) + len(S)
    g_formula = sum(max(0, n - len(set(A)) - 1) for A in list(R) + list(S))
    N, E = T["N"], T["entries"]
    a.ok(sec, N == n + g_formula, f"{tag} N={N} != n+g={n + g_formula}")
    a.ok(sec, len(T["gates"]) == g_formula, f"{tag} gate count")
    offd = sum(1 for i, j, _ in E if i != j)
    a.ok(sec, offd <= CLAIMS["offdiag_per_gate"] * g_formula, f"{tag} offdiag {offd} > 3g")
    a.ok(sec, len(E) <= CLAIMS["stored_per_gate"] * g_formula + m, f"{tag} stored {len(E)} > 6g+m")
    a.ok(sec, N <= CLAIMS["reg_num_vars"](n, m), f"{tag} registered num_vars bound")
    a.ok(sec, offd <= CLAIMS["reg_quad"](n, m), f"{tag} registered num_quadratic_terms bound")
    a.ok(sec, all(0 <= i <= j < N and q != 0 for i, j, q in E), f"{tag} malformed entry")
    a.ok(sec, [(i, j) for i, j, _ in E] == sorted(set((i, j) for i, j, _ in E)), f"{tag} unsorted/dup")
    M = r + 1
    B = CLAIMS["B"](M, g_formula, r, s)
    a.ok(sec, sum(abs(q) for _, _, q in E) <= B, f"{tag} abs sum > B")
    a.ok(sec, abs(T["C"]) <= CLAIMS["C_bound"](M, g_formula, r, s), f"{tag} |C| > Mg+r+s")
    a.ok(sec, T["bound"] == -T["C"], f"{tag} bound != -C")
    # gate allocation: fresh bits consecutive from n, chain structure in R-then-S order
    exp_z, exp_gates = n, []
    for A in list(R) + list(S):
        comp = [v for v in range(n) if v not in set(A)]
        if len(comp) >= 2:
            prev = ("neg", comp[0])
            for u in comp[1:]:
                exp_gates.append((prev, ("neg", u), exp_z))
                prev = ("var", exp_z)
                exp_z += 1
    a.ok(sec, T["gates"] == exp_gates, f"{tag} gate allocation order")
    # gate expansion claims of the proof (hand formula vs the generic product used by reduce)
    for ga, gb, z in T["gates"]:
        P = core.gate_poly(ga, gb, z, MUTANT)
        if ga[0] == "neg":  # first gate: 1-u-v+uv-z+2uz+2vz
            hand = named_poly(CLAIMS["first_hand"], {"u": ga[1], "v": gb[1], "z": z})
            a.ok(sec, P == hand and core.p_abs_nonconst(P) == CLAIMS["first_norm"], f"{tag} first gate expansion")
        else:  # later gate: w-wu-2wz+z+2uz
            hand = named_poly(CLAIMS["later_hand"], {"w": ga[1], "u": gb[1], "z": z})
            a.ok(sec, P == hand and core.p_abs_nonconst(P) == CLAIMS["later_norm"], f"{tag} later gate expansion")
    # partial-construction envelope
    trace = []
    reduce_(n, R, S, trace=trace)
    a.ok(sec, all(ns <= B and c <= B for ns, c in trace), f"{tag} partial envelope")
    # numeric guard tightness on this instance: L = max(B, M) passes, one less rejects
    Bn = max(8 * M * g_formula + r + s, M)
    try:
        T2 = reduce_(n, R, S, L=Bn)
        a.ok(sec, T2["entries"] == E and T2["bound"] == T["bound"], f"{tag} L=B changes target")
        a.ok(sec, all(abs(q) <= Bn for _, _, q in E) and abs(T["bound"]) <= Bn, f"{tag} L=B range")
    except core.ReductionError as e:
        a.ok(sec, False, f"{tag} L=B rejected: {e}")
    a.ok(sec, raises(lambda: reduce_(n, R, S, L=Bn - 1), core.ReductionError), f"{tag} L=B-1 accepted")
    # index guard: J = N passes, J = N-1 rejects
    a.ok(sec, not raises(lambda: reduce_(n, R, S, J=n + g_formula), core.ReductionError)
         and raises(lambda: reduce_(n, R, S, J=n + g_formula - 1), core.ReductionError)
         if n + g_formula >= max(1, m) else True, f"{tag} index guard boundary")


def attack_instance(n, R, S, tag="", full_vectors=True):
    a = Audit()
    T = reduce_(n, R, S)
    N, E, b, C = T["N"], T["entries"], T["bound"], T["C"]
    r = len(R)
    M = r + 1
    check_structure(a, n, R, S, T, tag)
    src = {}
    for xm in range(1 << n):
        x = [bool((xm >> v) & 1) for v in range(n)]
        cR, cS = core.source_counts(n, R, S, x)
        src[xm] = (cR >= cS, cR, cS)
    src_yes = any(v[0] for v in src.values())
    # forward from every source vector
    for xm, (ok, cR, cS) in src.items():
        ym = core.forward_mask(T, xm)
        H, faults = core.direct_H(T, ym)
        a.ok("2-forward", faults == 0 and H == cS - cR, f"{tag} forward H {H} vs {cS - cR}")
        e = energy_mask(E, ym)
        a.ok("2-forward", e == H - C, f"{tag} Q != H-C on forward")
        a.ok("2-forward", (e <= b) == ok, f"{tag} forward feasibility x={xm:b}")
    if full_vectors:
        tgt_yes = False
        for ym in range(1 << N):
            e = energy_mask(E, ym)
            H, faults = core.direct_H(T, ym)
            a.ok("2-backward", e == H - C, f"{tag} Q != H-C y={ym:b}")
            a.ok("2-backward", H >= M * faults - r, f"{tag} H < M*faults - r")
            if faults:
                a.ok("2-backward", H >= CLAIMS["margin"], f"{tag} faulty gate with H<1 y={ym:b}")
            else:
                ok, cR, cS = src[ym & ((1 << n) - 1)]
                a.ok("2-backward", H == cS - cR, f"{tag} consistent H wrong")
            y = core.mask_to_vec(ym, N)
            if e <= b:
                tgt_yes = True
                a.ok("2-backward", faults == 0, f"{tag} feasible with faulty gate")
                try:
                    x = extract_(T, y)
                    a.ok("3-extract", x == y[:n] and core.source_eval(n, R, S, x),
                         f"{tag} extraction wrong y={ym:b}")
                except core.ExtractionError as ex:
                    a.ok("3-extract", False, f"{tag} feasible rejected: {ex}")
            else:
                a.ok("3-extract", raises(lambda: extract_(T, y), core.ExtractionError),
                     f"{tag} infeasible accepted y={ym:b}")
        a.ok("2-equivalence", src_yes == tgt_yes, f"{tag} YES/NO mismatch src={src_yes} tgt={tgt_yes}")
    else:
        tgt_yes = any(energy_mask(E, core.forward_mask(T, xm)) <= b for xm in src)
        a.ok("2-equivalence", src_yes == tgt_yes, f"{tag} forward existence")
    # malformed extraction inputs: too long, too short, non-Boolean
    bads = [[False] * (N + 1)] + ([[False] * (N - 1), [0] * N] if N else [])
    for bad in bads:
        a.ok("3-extract", raises(lambda: extract_(T, bad), core.ExtractionError),
             f"{tag} malformed accepted len={len(bad)}")
    return a


def _work(args):
    n, R, S, tag = args
    try:
        return attack_instance(n, R, S, tag)
    except Exception as e:  # a crash under a broken construction is a detected failure
        a = Audit()
        a.ok("crash", False, f"{tag} {e!r}")
        return a


def guarded(a, fn):
    try:
        return fn(a)
    except Exception as e:
        a.ok("crash", False, f"{fn.__name__} {e!r}")


# ---------------- families ----------------

def all_subsets(n):
    return [tuple(v for v in range(n) if (mask >> v) & 1) for mask in range(1 << n)]


EXHAUSTIVE_PLAN = {0: 5, 1: 5, 2: 4, 3: 3, 4: 3, 5: 2}  # n -> max total family size m


def exhaustive_instances():
    for n, mmax in EXHAUSTIVE_PLAN.items():
        subs = all_subsets(n)
        for m in range(mmax + 1):
            for fam in itertools.product(subs, repeat=m):
                for r in range(m + 1):
                    yield (n, fam[:r], fam[r:], f"ex n={n} R={fam[:r]} S={fam[r:]}")


# ---------------- sections ----------------

def section_symbolic(a):
    import sympy as sp
    sec = "1-symbolic"
    # Gate table / lemma evaluated on the gate polynomial that reduce() actually uses
    P = core.gate_poly(("var", 0), ("var", 1), 2, MUTANT)

    def Pv(x, y, z):
        return sum(c for key, c in P.items() if all((x, y, z)[i] for i in key))
    for (x, y), (p0, p1) in CLAIMS["gate_table"].items():
        a.ok(sec, Pv(x, y, 0) == p0 and Pv(x, y, 1) == p1, "gate table")
        for z in (0, 1):
            val = Pv(x, y, z)
            a.ok(sec, val >= 0 and ((val == 0) == (z == x * y)), "gate lemma")
            if z != x * y:
                a.ok(sec, val >= CLAIMS["margin"], "faulty gate >= 1")
    # first/later hand formulas vs SymPy expansion of the gate reduce() uses
    u, v, w, zs = sp.symbols("u v w z")
    names = {0: u, 1: v, 2: zs, 3: w}

    def to_sym(poly):
        return sum(c * sp.prod([names[i] for i in key]) for key, c in poly.items())

    def hand_sym(hand):
        sym = {"u": u, "v": v, "w": w, "z": zs}
        return sum(c * sp.prod([sym[x] for x in key]) for key, c in hand.items())
    first = to_sym(core.gate_poly(("neg", 0), ("neg", 1), 2, MUTANT))
    a.ok(sec, sp.expand(first - hand_sym(CLAIMS["first_hand"])) == 0, "first gate formula")
    later = to_sym(core.gate_poly(("var", 3), ("neg", 0), 2, MUTANT))
    a.ok(sec, sp.expand(later - hand_sym(CLAIMS["later_hand"])) == 0, "later gate formula")
    # overhead inequalities: claimed per-entry gate bound, then polynomial slack in n, r, s
    for nn in range(0, 12):
        for kk in range(0, nn + 1):
            a.ok(sec, max(0, kk - 1) <= CLAIMS["gates_per_entry"](nn), "max(0,k-1)<=n")
    n, r, s = sp.symbols("n r s", nonnegative=True, integer=True)
    m = r + s
    gmax = n * m
    for label, diff in (("3g <= reg_quad", CLAIMS["reg_quad"](n, m) - CLAIMS["offdiag_per_gate"] * gmax),
                        ("N <= reg_num_vars", CLAIMS["reg_num_vars"](n, m) - (n + gmax)),
                        ("|C| <= B", CLAIMS["B"](r + 1, gmax, r, s) - CLAIMS["C_bound"](r + 1, gmax, r, s))):
        poly = sp.Poly(sp.expand(diff), n, r, s)
        a.ok(sec, all(c >= 0 for c in poly.coeffs()), label)
    # numeric guard boundaries with the real i64 / usize limits (symbolic sizes, nothing allocated)
    L, J = core.L_I64, core.J_USIZE

    def guard(r_, s_, g_, n_=0):
        return not raises(lambda: core.numeric_guard_counts(r_, s_, g_, L, mutant=MUTANT, n=n_, J=J),
                          core.ReductionError)
    a.ok(sec, guard(0, L, 0) and not guard(0, L + 1, 0), "s boundary")
    a.ok(sec, not guard(L, 0, 0), "proof: r=L,g=0,s=0 fails M guard")
    a.ok(sec, guard(L - 1, 1, 0) and not guard(L - 1, 2, 0), "r=L-1 zero gates, B=L exactly")
    for r_ in (0, 1, 2, 7):
        for g_ in (1, 2, 3, L // (8 * (r_ + 1)) - 1):
            s_ = L - 8 * (r_ + 1) * g_ - r_
            a.ok(sec, s_ >= 0 and guard(r_, s_, g_) and not guard(r_, s_ + 1, g_), f"B=L boundary r={r_} g={g_}")
        gm = (L - r_) // (8 * (r_ + 1))
        a.ok(sec, guard(r_, 0, gm) and not guard(r_, 0, gm + 1), f"largest g r={r_}")
    a.ok(sec, guard(0, 0, 1, J - 1) and not guard(0, 0, 1, J), "index N = n+g boundary")


def section_examples(a):
    U = (0, 1, 2, 3)
    R = ((0, 1), (1, 2), (1, 3))
    Y, NO = CLAIMS["yes"], CLAIMS["no"]
    sec = "6-YES"
    T = reduce_(4, R, (U, U))
    a.ok(sec, (T["r"], T["s"], T["M"], T["g"], T["N"]) == (Y["r"], Y["s"], Y["M"], Y["g"], Y["N"]), "params")
    a.ok(sec, len(T["entries"]) == Y["stored"], "15 entries")
    a.ok(sec, sum(1 for i, j, _ in T["entries"] if i != j) == Y["offd"], "9 offdiag")
    a.ok(sec, (T["B"], T["C"], T["bound"]) == (Y["B"], Y["C"], Y["bound"]), "B,C,bound")
    expQ = {(0, 0): -8, (2, 2): -8, (3, 3): -8, (0, 2): 4, (0, 3): 4, (2, 3): 4,
            (4, 4): -5, (5, 5): -5, (6, 6): -5,
            (2, 4): 8, (3, 4): 8, (0, 5): 8, (3, 5): 8, (0, 6): 8, (2, 6): 8}
    a.ok(sec, {(i, j): q for i, j, q in T["entries"]} == expQ, "full Q")
    a.ok(sec, sum(abs(q) for _, _, q in T["entries"]) == Y["abs"], "abs sum 99")
    a.ok(sec, T["gates"] == [(("neg", 2), ("neg", 3), 4), (("neg", 0), ("neg", 3), 5), (("neg", 0), ("neg", 2), 6)],
         "z0,z1,z2 allocation")

    def vec(x, z):
        return [bool(t) for t in list(x) + list(z)]

    def msk(y):
        return sum(1 << i for i, t in enumerate(y) if t)
    y = vec((0, 1, 0, 0), (1, 1, 1))
    H, f = core.direct_H(T, msk(y))
    a.ok(sec, (H, f, core.target_value(T, y)) == (Y["Hw"], 0, Y["Qw"]) and core.target_eval(T, y), "witness")
    a.ok(sec, core.source_counts(4, R, (U, U), [False, True, False, False]) == (3, 2), "counts 3,2")
    a.ok(sec, not raises(lambda: extract_(T, y), core.ExtractionError)
         and extract_(T, y) == [False, True, False, False], "extract")
    y2 = vec((0, 1, 0, 0), (0, 1, 1))
    H2, f2 = core.direct_H(T, msk(y2))
    a.ok(sec, (H2, f2, core.target_value(T, y2)) == (Y["Hf"], 1, Y["Qf"]) and not core.target_eval(T, y2), "flip z0")
    y3 = vec((1, 1, 0, 0), (1, 0, 0))
    H3, f3 = core.direct_H(T, msk(y3))
    a.ok(sec, (H3, f3, core.target_value(T, y3)) == (Y["H01"], 0, Y["Q01"]) and not core.target_eval(T, y3), "Y={0,1}")
    a.ok(sec, core.source_counts(4, R, (U, U), [True, True, False, False]) == (1, 2), "counts 1,2")
    a.ok(sec, core.forward_mask(T, 0b0011) == msk(y3) and core.forward_mask(T, 0b0010) == msk(y), "consistent extensions")
    a.merge(attack_instance(4, R, (U, U), "YES example"))
    sec = "7-NO"
    T = reduce_(4, R, (U,) * 4)
    a.ok(sec, (T["r"], T["s"], T["M"], T["g"], T["N"], T["C"], T["bound"], T["B"]) ==
         (NO["r"], NO["s"], NO["M"], NO["g"], NO["N"], NO["C"], NO["bound"], NO["B"]), "params")
    a.ok(sec, {(i, j): q for i, j, q in T["entries"]} == expQ, "same Q")
    a.ok(sec, core.target_value(T, y) == -15 and not core.target_eval(T, y) and core.direct_H(T, msk(y))[0] == 1,
         "vector")
    vals = [energy_mask(T["entries"], ym) for ym in range(1 << T["N"])]
    a.ok(sec, min(vals) == NO["minQ"] and min(vals) > T["bound"], f"min Q {min(vals)}")
    for xm in range(16):
        x = [bool((xm >> v) & 1) for v in range(4)]
        cR, cS = core.source_counts(4, R, (U,) * 4, x)
        a.ok(sec, cS == 4 and cR <= 3 and not core.source_eval(4, R, (U,) * 4, x), "no source witness")
    a.merge(attack_instance(4, R, (U,) * 4, "NO example"))


def section_special(a):
    sec = "5-special"
    for n in range(0, 5):
        U = tuple(range(n))
        for r in range(0, 4):
            for s in range(0, 4):
                T = reduce_(n, (U,) * r, (U,) * s)
                a.ok(sec, T["entries"] == [] and T["C"] == s - r and T["N"] == n
                     and (0 <= T["bound"]) == (r >= s), f"full n={n} r={r} s={s}")
    for n in range(0, 5):
        T = reduce_(n, (), ())
        a.ok(sec, T["entries"] == [] and T["bound"] == 0 and T["g"] == 0 and T["N"] == n, "R=S=()")
    T = reduce_(3, (), ((0, 1, 2),))
    a.ok(sec, T["N"] == 3 and T["entries"] == [] and T["bound"] == -1, "R=(),S=(U)")
    a.ok(sec, not any(core.source_eval(3, (), ((0, 1, 2),), [bool((m >> v) & 1) for v in range(3)])
                      for m in range(8)), "R=(),S=(U) source NO")
    T = reduce_(1, ((),), ())
    a.ok(sec, T["g"] == 0 and T["N"] == 1 and T["indicators"] == [("R", ("neg", 0))], "n=1 empty set")
    T = reduce_(0, ((), ()), ((),))
    a.ok(sec, T["N"] == 0 and T["entries"] == [] and T["bound"] == 1 and extract_(T, []) == [], "n=0 YES")
    T = reduce_(0, ((),), ((), ()))
    a.ok(sec, T["bound"] == -1 and raises(lambda: extract_(T, []), core.ExtractionError), "n=0 NO")
    T1 = reduce_(4, ((0, 0, 2, 2),), ((3, 1, 1),))
    T2 = reduce_(4, ((0, 2),), ((1, 3),))
    a.ok(sec, (T1["entries"], T1["bound"], T1["N"]) == (T2["entries"], T2["bound"], T2["N"]), "raw duplicates")
    T = reduce_(4, ((0,), (0,)), ())
    a.ok(sec, T["g"] == 4 and len({g[2] for g in T["gates"]}) == 4, "repeated entries distinct bits")
    T = reduce_(3, ((0, 1),), ((0, 1),))
    a.ok(sec, T["entries"] == [] and T["N"] == 3 and T["C"] == 0, "cancellation keeps N, omits zeros")
    for bad in [(3, ((3,),), ()), (2, (), ((0, 5),)), (0, ((0,),), ()), (2, ((-1,),), ())]:
        a.ok(sec, raises(lambda: reduce_(*bad), core.ReductionError), f"accepted {bad}")
    for n in range(2, 5):
        T = reduce_(n, ((),) * 2, ((),) * 2)
        for ym in range(1 << T["N"]):
            H, f = core.direct_H(T, ym)
            if f:
                a.ok(sec, H >= (T["r"] + 1) - T["r"], "faulty bound M-r")


def section_hypothesis(a):
    from hypothesis import given, settings, seed, strategies as st, HealthCheck

    common = dict(max_examples=300, database=None, deadline=None,
                  suppress_health_check=list(HealthCheck))
    stats = Counter()

    def run(n, R, S, label):
        tag = f"hyp {label} n={n} R={R} S={S}"
        try:
            T = reduce_(n, R, S)
            full = T["N"] <= MAXN
            stats[label] += 1
            stats[label + (" full" if full else " forward-only")] += 1
            a.merge(attack_instance(n, R, S, tag, full_vectors=full))
        except Exception as e:
            a.ok("crash", False, f"{tag} {e!r}")

    # Strategy 1: long chains (small sets), raw duplicates, unsorted listings, repeated entries
    @st.composite
    def chains(draw):
        n = draw(st.integers(3, 6))
        small = st.lists(st.integers(0, n - 1), min_size=0, max_size=3)
        R = draw(st.lists(small, min_size=0, max_size=3))
        S = draw(st.lists(small, min_size=0, max_size=3))
        if draw(st.booleans()) and R:
            R = R + [R[0]]
        return n, tuple(tuple(x) for x in R), tuple(tuple(x) for x in S)

    @seed(2026110411)
    @settings(**common)
    @given(chains())
    def t1(inst):
        run(*inst, "chains")
    t1()

    # Strategy 2: count boundaries r = s, s +- 1; full / empty / near-full / singleton mixtures
    @st.composite
    def boundary(draw):
        n = draw(st.integers(0, 5))
        U = list(range(n))

        def mk():
            k = draw(st.sampled_from(["full", "empty", "near", "single", "any"]))
            if k == "full" or n == 0:
                return tuple(U)
            if k == "empty":
                return ()
            if k == "near":
                d = draw(st.integers(0, n - 1))
                return tuple(v for v in U if v != d)
            if k == "single":
                return (draw(st.integers(0, n - 1)),)
            return tuple(draw(st.lists(st.integers(0, n - 1), max_size=n + 2)))
        r = draw(st.integers(0, 4))
        s = max(0, r + draw(st.sampled_from([-1, 0, 1])))
        return n, tuple(mk() for _ in range(r)), tuple(mk() for _ in range(s))

    @seed(2026110412)
    @settings(**common)
    @given(boundary())
    def t2(inst):
        run(*inst, "boundary")
    t2()

    # Strategy 3: one empty family
    @st.composite
    def onesided(draw):
        n = draw(st.integers(0, 5))
        entry = st.lists(st.integers(0, n - 1), max_size=n + 1).map(tuple) if n else st.just(())
        F = tuple(draw(st.lists(entry, max_size=3)))
        return (n, F, ()) if draw(st.booleans()) else (n, (), F)

    @seed(2026110413)
    @settings(**common)
    @given(onesided())
    def t3(inst):
        run(*inst, "onesided")
    t3()
    return stats


def main():
    quick = "--quick" in sys.argv
    a = Audit()
    if MUTANT or CLAIM_MUTANT or core.ORACLE_MUTANT:
        print(f"MUTANT: {MUTANT} CLAIM: {CLAIM_MUTANT} ORACLE: {core.ORACLE_MUTANT}")
    guarded(a, section_symbolic)
    guarded(a, section_examples)
    guarded(a, section_special)
    insts = list(exhaustive_instances())
    if quick:
        insts = [i for i in insts if i[0] <= 3 and len(i[1]) + len(i[2]) <= 2]
    with Pool(int(os.environ.get("ADV_PROCS", "10"))) as pool:
        for res in pool.imap_unordered(_work, insts, chunksize=64):
            a.merge(res)
    print(f"SCOPE: exhaustive all ordered (R,S) with total size m per n: {EXHAUSTIVE_PLAN}"
          f"{' (quick: n<=3, m<=2)' if quick else ''}; every target vector enumerated")
    print(f"exhaustive instances: {len(insts)}")
    if not quick:
        stats = guarded(a, section_hypothesis) or {}
        print("HYPOTHESIS: 3 strategies x 300 examples, seeds 2026110411/12/13, database=None, deadline=None;"
              f" full target enumeration when N <= {MAXN}")
        print("hypothesis examples:", dict(stats))
    for sec in sorted(a.checks):
        print(f"  {sec:22s} {a.checks[sec]:10d} checks {a.fails[sec]:6d} failures")
    for m in a.log:
        print("FAIL", m)
    tot, fl = sum(a.checks.values()), sum(a.fails.values())
    out = os.environ.get("ADV_SITES_OUT")
    if out:
        import json
        json.dump({"sites": sorted(a.sites), "fail_sites": sorted(a.fail_sites)}, open(out, "w"))
    print(f"TOTAL: {tot} checks, {fl} failures")
    return 1 if fl else 0


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

@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Agentic Review Report

Subject: rule ComparativeContainment/One → DecisionQUBO/i64 (src/rules/comparativecontainment_qubo.rs).
Verdict: no open findings. Ready to merge from the review's side.

Structural and semantic

  • Files. The change touches src/rules/comparativecontainment_qubo.rs and src/unit_tests/rules/comparativecontainment_qubo.rs (both new), plus src/rules/mod.rs, docs/paper/reductions.typ and docs/paper/references.bib.
    • mod.rs adds one module line and one example-spec line.
    • references.bib adds url to the existing borosgruber2014quadratization entry.
    • No generated exports, unrelated edits, deleted tests or core-trait changes. No shared test loses lines.
  • Registration. #[reduction(transform = upper_bound { num_vars, num_quadratic_terms })] covers both registered DecisionQUBO parameters exactly once. The impl names ComparativeContainment<One> literally. #[aggregate_reduction(identity)] gives the identity Or mapping that docs/src/design.md requires for a decision-to-decision rule.
  • Transform. num_vars ≤ n + n(r+s): the actual count is n + g with g = Σ max(0, n − |A| − 1) ≤ n(r+s). num_quadratic_terms ≤ 6n(r+s) + r + s: actual off-diagonal terms are at most 3g. Both are upper bounds, not exact, which is correct: full-universe entries and cancellation make them strict. pred path … instance.json on the canonical instance measures 7 variables and 9 off-diagonal terms, against bounds of 24 and 125. parameter_formula_validation.rs checks both formulas automatically through the canonical example.
  • Hand trace of reduce_to() on the issue's instance: n = 4, R = ({0,1},{1,2},{1,3}), S = (U,U).
    • Each R entry has a two-element complement, so there is one gate each (z₄, z₅, z₆), and M = 4.
    • Each gate contributes M·(1 − x_u − x_v + x_u x_v − z + 2x_u z + 2x_v z), and the R indicator adds −z. That gives diagonals −8 on x₀, x₂, x₃ (each in two complements), +4 on the pairs, +8 on the x–z pairs, and −4 − 1 = −5 on each z.
    • Constant 3·4 + 2 = 14, so the bound is −14.
    • This matches the test fixture's 15 entries. The witness (0,1,0,0,1,1,1) gives Q = −15 ≤ −14, and extraction returns (0,1,0,0).
  • Extraction calls validate_target_witness once, then returns the first n bits. Wrong lengths, the flipped-gate vector, the infeasible candidate, and every infeasible vector in the exhaustive tests are rejected.
  • Errors and numeric contract.
    • Gate count, m = r + s and N = n + g are checked for usize overflow.
    • r, r + s and g are converted with try_from.
    • M = r + 1 and the certificate B = 8·M·g + r + s ≤ i64::MAX use checked arithmetic, multiplying M·g first so a gate-free target with large r is admitted.
    • Every coefficient and constant accumulates through QuboBuilder with checked i128 → i64 narrowing.
    • Target construction errors keep ReductionError::Construction.
  • Paper entry. It has a marker comment and load-example with explicit variants. Construction, both directions, extraction, sizes and arithmetic are present and sound, and agree with the Rust. The YES and NO examples are computed from the loaded fixture, with #asserts on every fact an argument relies on, including the NO-example premise that every S entry is the full universe. The Rosenberg gate is cited to Boros–Gruber (arXiv 1404.6538, now with URL), and the indicator idea is credited to the antecedent issue.
  • Issue compliance.
    • The construction, penalty M = r + 1, constant offset into the bound, extraction, registered formulas and numeric certificate match the issue.
    • The issue's YES instance is the canonical example and is tested, with feasibility confirmed by brute force. The issue's NO instance (four copies of U) is tested, with brute-force infeasibility on both sides.
    • The issue's description of num_quadratic_terms as diagonal-inclusive does not match the library. The library counts off-diagonal entries only, and the registered bound remains a valid upper bound for that metric.

Quality

  • 10 unit tests. The validation plan's items are covered at the stated sizes, and every test finishes in about 1 s or less in a debug build.
    • Closed loop (test_comparativecontainment_to_qubo_closed_loop): the round-trip helper with brute force on both sides for YES, and brute-force infeasibility of source and target for NO.
    • The canonical polynomial coefficient by coefficient, with the rejected faulty-gate and infeasible-candidate vectors and malformed lengths.
    • Exhaustive checks over every pair of at-most-two-entry families for n ≤ 2, and every two-entry family for n = 3, 4, 5: chains up to four gates, every target vector, independent source sums from the model.
    • Empty sets, full universes, empty families, empty R with full S (NO), the empty universe, duplicate and unsorted elements, repeated entries, and full cancellation.
    • Numeric boundaries on both sides: the largest admitted gate count and the next one, B = i64::MAX exactly at g = 0, usize index limits, and the zero-gate penalty case.
    • A target built at its exact coefficient limit, rejected one below it, on an instance whose coefficients exceed limit/8.
  • Coverage. The new module's lines are fully covered by the full suite. With only its own tests, the uncovered regions are the ? error continuations that the certificate makes unreachable, plus the example-spec function, which the example-db tests cover.
  • Reuse. The rule uses the shared QuboBuilder. No existing standalone product-gate helper exists; the 3-SAT rule's gate helper is private and clause-specific.

Feature test

From a scratch directory with the branch's pred:

  • pred list --rules ComparativeContainment shows ComparativeContainment/One → DecisionQUBO/i64.
  • pred create --example ComparativeContainment/One --to DecisionQUBO/i64 writes the issue's instance.
  • pred path ComparativeContainment/One DecisionQUBO/i64 gives one direct route with the two upper-bound formulas.
  • pred reduce then pred solve bundle.json (both brute force and the default ILP solver) returns Or(true) with a valid source witness.
  • pred evaluate of the paper's witness (0,1,0,0) returns Or(true).
  • For the NO instance (four copies of U), pred solve reports infeasible through both the witness and the --aggregate bundles.
  • pred extract --value true/false maps by identity.
  • pred extract rejects infeasible vectors and wrong lengths.
  • Every command shown in the paper entry was run as written and works.
  • Independently written reference implementations of the construction produce targets identical to pred reduce (same variables, entries and bound) on 606 instances. These include exhaustive small families, special cases, 200 seeded random instances with duplicate and unsorted listings, and 100 external instances. All 100 external instances round-trip correctly; all of them have a solution.

Findings

None (Critical: 0, Important: 0, Minor: 0).

@codecov

codecov Bot commented Nov 4, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1240    +/-   ##
========================================
  Coverage   97.19%   97.20%            
========================================
  Files        1146     1148     +2     
  Lines      151558   151925   +367     
========================================
+ Hits       147308   147675   +367     
  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.

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] Unit ComparativeContainment to DecisionQUBO

1 participant