Skip to content

Fix #1201: [Rule] OneInThreeSatisfiability to SubsetProduct - #1236

Open
isPANN wants to merge 1 commit into
mainfrom
issue-1201-oneinthreesat-subsetproduct
Open

isPANN wants to merge 1 commit into
mainfrom
issue-1201-oneinthreesat-subsetproduct

Conversation

@isPANN

@isPANN isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1201

Adds the reduction OneInThreeSatisfiability → SubsetProduct: one private prime per variable, one prime per clause, two items per variable (false/true) whose clause-prime exponents count the literal occurrences, and the target is the product of the first n + m primes. num_elements = 2 * num_vars; the identity aggregate mapping applies (Or → Or).

  • rule, unit tests (issue examples, exhaustive small instances, special cases, >64-bit target), canonical example and paper entry (YES and NO example)
  • assigned_primes / product_biguint and SubsetProduct::try_new made pub(crate) for reuse, bodies unchanged
  • oneinthreesatisfiability_ilp.rs: qualified target_construction call, needed once the source has a second ReduceTo impl

🤖 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. Two independently written scripts and a Typst proof agree; no counterexample found.

Type resolution. Source OneInThreeSatisfiability: Value = Or, solution Vec<bool> of length n. Target SubsetProduct: Value = Or, solution Vec<bool> of length 2n (arbitrary-precision items and target). Or -> Or with identical solution type: witness-compatible, identity aggregate mapping. pred path OneInThreeSatisfiability SubsetProduct --limit all found no existing path, so the rule is not dominated.

Construction script (seed 2026110301, Hypothesis derandomized): 3,827,291 checks, 0 failures.

Section Checks
symbolic 530
forward + backward 261,335 (+2,444 Hypothesis)
extraction 995,339
sizes 1,746,332
structure 821,283
YES / NO example 8 / 20

Instances: n = 1..5 variables. Exhaustive: every single clause (n <= 5), every ordered clause pair (n <= 3), clause-shape cases (n = 4). Sampled: 300 per (n, m) with m = 2..5, planted-satisfiable instances, plus large instances beyond 64 bits. 59,972 instances (40,039 YES, 19,933 NO). Every 2^(2n) selection is enumerated.

Adversary script (written from the proof alone, seed 2026110304, Hypothesis 3 strategies x 300 examples, derandomize, no database, no deadline): 4,358,569 checks, 0 failures over 109,182 instances (52,115 YES, 57,067 NO).

  • Exhaustive (98,382): empty instance; m = 0 for n <= 5; every ordered single clause for n <= 5; every ordered clause pair for n <= 3; every multiset of 3 clauses for n = 2.
  • Random (10,800): 300 per (n, m), n, m in 1..6.
  • Large planted: (n, m) = (40,60), (120,200), (300,500), products above 64 bits.

Cross-comparison: 169,154 shared instances; identical targets (items in index order and B), identical witness sets (all 167,354 instances with n <= 5) and identical extractions (176,555 witnesses); 0 disagreements. The Rust rule was additionally compared against both scripts on 170 instances, 0 mismatches.

Mutation testing: 26 mutants of construction, checker and extraction; the adversary kills 26/26, the construction script 24/26 (the two survivors are a 64-bit-overflow mutant, which needs the large instances, and unvalidated extraction, covered by the Rust unit tests).

Typst proof
#set page(margin: 2cm)
#set text(size: 10.5pt)
#let thm(body) = block(inset: 8pt, stroke: 0.5pt, width: 100%)[#body]

= OneInThreeSatisfiability $arrow.r$ SubsetProduct

*Provenance.* Derived here. The nearest library rule, ExactCoverBy3Sets $arrow.r$ SubsetProduct, uses the same device (one prime per constraint, unique factorization) but has one item per set; this rule adds a private prime per variable that forces one of two items per variable, and signed occurrence counts as prime exponents. No cited source states this exact construction.

*Type resolution.* Source `OneInThreeSatisfiability`: `Solution = Vec<bool>` of length $n$, `Value = Or` (`src/models/formula/one_in_three_satisfiability.rs`); `evaluate` rejects a wrong length and otherwise returns whether every clause has exactly one true literal occurrence. Target `SubsetProduct`: `Solution = Vec<bool>` of length equal to the number of items, `Value = Or` (`src/models/misc/subset_product.rs`); sizes and target are arbitrary-precision `BigUint`, all sizes and the target positive. `Or` $arrow.r$ `Or` with the identical solution type: witness-compatible, the identity aggregate mapping applies.

*Redundancy.* `pred path OneInThreeSatisfiability SubsetProduct --limit all` reports no path, so the rule is novel (PASS). Existing outgoing edges of the source: only to ILP.

#thm[*Theorem.* Let $F$ have $n$ variables and $m$ clauses, each clause a triple of nonzero signed literals $ell in {plus.minus 1, dots, plus.minus n}$ (repeats and both signs of a variable allowed). The SubsetProduct instance below is feasible iff $F$ has an assignment making exactly one literal occurrence true in every clause, and a target witness maps to such an assignment.]

== Construction

+ Let $pi_1 < pi_2 < dots$ be the primes starting at $2$. Put $p_i = pi_i$ for $i = 1..n$ (variable primes) and $q_j = pi_(n+j)$ for $j = 1..m$ (clause primes). All $n+m$ primes are distinct.
+ For variable $i$ and clause $j$ let $nu^-_(j i)$ be the number of positions of clause $j$ holding the literal $-i$, and $nu^+_(j i)$ the number holding $+i$ (multiplicity counted).
+ For each variable $i$ create two items, in this index order (zero-based positions $2(i-1)$ and $2(i-1)+1$):
  $ a_(i,0) = p_i dot product_j q_j^(nu^-_(j i)) quad ("false item"), quad a_(i,1) = p_i dot product_j q_j^(nu^+_(j i)) quad ("true item"). $
  Items with equal value remain distinct indices.
+ The target is $B = (product_i p_i)(product_j q_j)$, the product of the first $n+m$ primes ($B=1$ if $n=m=0$).

== Overhead

#table(columns: 3, [Target quantity], [Formula], [Status],
  [`num_elements`], [$2n$], [exact, every input including $n=0$],
  [prime factors in all items, with multiplicity], [$2n + 3m$], [exact (not a registered parameter)],
  [prime factors of $B$], [$n + m$], [exact],
  [encoding size], [$O((n+m) log(n+m+2))$ bits], [bound])

The first count: each item holds one $p_i$ ($2n$ in total), and every clause has three literal positions, each adding one factor $q_j$ to exactly one item ($3m$ in total). The first $k = n+m$ primes are computed by trial division in time polynomial in $k$. Each has $O(log(k+2))$ bits and there are $2n+3m+n+m$ prime factors to write, giving the bit bound; individual values are not polynomially bounded numerically.

== Correctness

Let $S$ be a set of item indices, $x_i^S$ the number of selected items among ${a_(i,0), a_(i,1)}$. The product $P(S)$ of selected items has $p_i$-adic valuation $x_i^S$, because $p_i$ divides only those two items (the only primes in any item are $p_i$ of its own variable and clause primes, all distinct).

*($arrow.r.double$) Satisfiable $arrow.r$ feasible.* Let assignment $alpha$ make exactly one occurrence true in every clause. Select $a_(i,1)$ if $alpha_i$ is true and $a_(i,0)$ otherwise. Each $p_i$ then appears exactly once. For clause $j$, the exponent of $q_j$ in $P(S)$ is the sum over $i$ of $nu^(alpha_i)_(j i)$ with sign chosen by $alpha_i$, which counts the occurrences in clause $j$ that are true under $alpha$ (an occurrence of $+i$ is true iff $alpha_i$ is true, and of $-i$ iff $alpha_i$ is false). That count is one, so $q_j$ appears exactly once. By unique factorization $P(S) = B$.

*($arrow.l.double$) Feasible $arrow.r$ satisfiable.* Let $P(S) = B$. The exponent of $p_i$ in $B$ is $1$, so $x_i^S = 1$: exactly one of the two items of every variable is selected. Define $alpha_i$ true iff $a_(i,1)$ is selected. No other items exist, so $S$ consists of these choices. The exponent of $q_j$ in $P(S)$ is, as computed above, the number of true occurrences in clause $j$ under $alpha$; it equals the exponent $1$ in $B$. So every clause has exactly one true occurrence. Repeated literals contribute their full multiplicity (a clause $(x,x,x)$ has count $3$ or $0$, never $1$), opposite signs of one variable contribute to the false and the true item respectively (exactly one of them is true), and unused variables contribute only their private prime.

== Solution extraction

Given a target witness $s in {0,1}^(2n)$ with product $B$ (validated first), return source bit $i$ equal to $s_(2i+1)$ (zero-based), i.e. whether the true item of variable $i$ is selected. By the ($arrow.l.double$) paragraph this is a satisfying assignment, and $s$ is recovered as $s_(2i)=not alpha_i$, $s_(2i+1)=alpha_i$. No integer is factored. The number of target witnesses equals the number of satisfying assignments.

== Special cases

- *Repeated literal in a clause*, e.g. $(x_1,x_1,x_1)$: exponent $3$ on the true item of $x_1$, $0$ on the false item; never exactly one, so correctly unsatisfiable. $(x_1,x_1,x_2)$ gives exponent $2$ for $x_1$ true.
- *Both signs in one clause*, e.g. $(x_1, not x_1, x_2)$: both items of $x_1$ carry $q_1^1$, so they are equal in value ($10 = 10$ for $n=2$) but remain two indices; the $x_1$ occurrences contribute exactly one true literal either way.
- *Unused variables*: the two items both equal $p_i$ and stay distinct indices; both choices give valid witnesses, matching the two free assignments.
- *No clauses* ($m=0$, $n >= 1$): items $(p_1,p_1,dots)$, $B = product p_i$; feasible, with $2^n$ witnesses.
- *Empty instance* ($n=m=0$): no items, $B=1$, the empty selection has product $1$, feasible; the source empty assignment satisfies the vacuous formula.
- A clause with $n=0$ cannot exist (literals need a variable), so $m>0$ implies $n>=1$.

== Example (YES)

Clauses $(x_1,x_2,x_3)$, $(not x_1, not x_2, x_3)$, $(x_1, not x_2, not x_3)$; $n=m=3$. Variable primes $(2,3,5)$, clause primes $(7,11,13)$.

#table(columns: 3, [Variable], [False item], [True item],
  [$x_1$], [$2 dot 11 = 22$], [$2 dot 7 dot 13 = 182$],
  [$x_2$], [$3 dot 11 dot 13 = 429$], [$3 dot 7 = 21$],
  [$x_3$], [$5 dot 13 = 65$], [$5 dot 7 dot 11 = 385$])

$B = 2 dot 3 dot 5 dot 7 dot 11 dot 13 = 30030$. Item list $(22,182,429,21,65,385)$. The selection $(1,0,0,1,1,0)$ gives $22 dot 21 dot 65 = 30030 = B$. Extraction reads positions $1,3,5$: $(0,1,0)$. Check: clause 1 has only $x_2$ true, clause 2 has only $not x_1$ true, clause 3 has only $not x_3$ true. This is the only satisfying assignment and the only target witness (exhaustive check over all $2^6$ selections).

== Example (NO)

Clauses $(x_1,x_2,x_3)$, $(not x_1,not x_2,not x_3)$; $n=3$, $m=2$. Primes $(2,3,5)$, $(7,11)$. Items $(22,14,33,21,55,35)$ with $a_(1,0)=2 dot 11$, $a_(1,1)=2 dot 7$, $a_(2,0)=3 dot 11$, $a_(2,1)=3 dot 7$, $a_(3,0)=5 dot 11$, $a_(3,1)=5 dot 7$; $B = 2310$. Source: with $t$ variables true, clause 1 has $t$ true literals and clause 2 has $3-t$; $t=1$ and $3-t=1$ cannot hold together. Target: a product equal to $B$ must pick one item per variable; then the exponents of $7$ and $11$ are $t$ and $3-t$, which are not both $1$. All $2^6$ selections give a product different from $2310$, matching the infeasible source.
Construction script (Python)
#!/usr/bin/env python3
"""Constructor verification: OneInThreeSatisfiability -> SubsetProduct.

Source: n variables, m clauses of exactly three nonzero signed literals (repeats and
both signs allowed); solution = Vec<bool> of length n; Or value = every clause has
exactly one true literal occurrence.
Target: SubsetProduct, positive integer items (indexed) + positive target B;
solution = Vec<bool> of length 2n; Or value = product of selected items == B.

Construction: p_i = i-th prime (i<n), q_j = (n+j)-th prime. Items 2i (false) and 2i+1
(true): p_i * prod_j q_j^{nu-/nu+}. B = prod p * prod q. Extraction: source bit i =
target bit 2i+1.
"""
import itertools
import random
import sys
from collections import Counter

import sympy
from sympy import Symbol, simplify, Sum, factorint
from hypothesis import given, settings, strategies as st, HealthCheck

SEED = 2026110301
random.seed(SEED)

counts = Counter()
fails = Counter()
failure_log = []


def check(section, cond, msg=""):
    counts[section] += 1
    if not cond:
        fails[section] += 1
        if len(failure_log) < 20:
            failure_log.append(f"[{section}] {msg}")


# ---------------------------------------------------------------- semantics
def first_primes(k):
    out, c = [], 2
    while len(out) < k:
        if all(c % p for p in out if p * p <= c):
            out.append(c)
        c += 1
    return out


def lit_true(lit, a):
    v = a[abs(lit) - 1]
    return v if lit > 0 else not v


def src_eval(n, clauses, a):
    """Source Or value (exact semantics of is_one_in_three_satisfying)."""
    assert len(a) == n
    return all(sum(lit_true(l, a) for l in c) == 1 for c in clauses)


def valid_source(n, clauses):
    return all(len(c) == 3 and all(l != 0 and abs(l) <= n for l in c) for c in clauses)


def reduce(n, clauses):
    m = len(clauses)
    pr = first_primes(n + m)
    p, q = pr[:n], pr[n:]
    items = []
    for i in range(n):
        for sign in (-1, +1):
            v = p[i]
            for j, c in enumerate(clauses):
                nu = sum(1 for l in c if l == sign * (i + 1))
                v *= q[j] ** nu
            items.append(v)
    B = 1
    for x in pr:
        B *= x
    return items, B


def tgt_eval(items, B, sel):
    assert len(sel) == len(items)
    prod = 1
    for x, s in zip(items, sel):
        if s:
            prod *= x
    return prod == B


def all_subset_products(items):
    k = len(items)
    prod = [1] * (1 << k)
    for mask in range(1, 1 << k):
        low = mask & -mask
        prod[mask] = prod[mask ^ low] * items[low.bit_length() - 1]
    return prod


def bits(mask, k):
    return [bool((mask >> t) & 1) for t in range(k)]


def extract(sel):
    return [sel[2 * i + 1] for i in range(len(sel) // 2)]


# ------------------------------------------------------------ instance sets
def all_clauses(n):
    lits = [l for v in range(1, n + 1) for l in (v, -v)]
    return list(itertools.product(lits, repeat=3))


def instances():
    yield 0, []  # empty instance
    for n in range(1, 6):
        yield n, []  # unused variables only
    # exhaustive: every single clause for n <= 5
    for n in range(1, 6):
        for c in all_clauses(n):
            yield n, [c]
    # exhaustive: every ordered pair of clauses for n <= 3
    for n in range(1, 4):
        cl = all_clauses(n)
        for c1 in cl:
            for c2 in cl:
                yield n, [c1, c2]
    # sampled: >= 300 per (n, m) for 2 <= m <= 5 (n = 4, 5) and 3 <= m <= 5 (n <= 3)
    for n in range(1, 6):
        cl = all_clauses(n)
        for m in range(2, 6):
            if n <= 3 and m == 2:
                continue
            for _ in range(300):
                yield n, [random.choice(cl) for _ in range(m)]
    # satisfiable-biased samples: plant an assignment and pick one-true clauses
    for n in range(3, 6):
        for m in range(1, 6):
            for _ in range(150):
                a = [random.random() < 0.5 for _ in range(n)]
                cs = []
                while len(cs) < m:
                    c = random.choice(all_clauses(n))
                    if sum(lit_true(l, a) for l in c) == 1:
                        cs.append(c)
                yield n, cs


# ------------------------------------------------------------------ sections
def section1_symbolic():
    S = "1-symbolic"
    n, m, i, j = sympy.symbols("n m i j", integer=True, nonnegative=True)
    # num_elements: two items per variable.
    check(S, simplify(Sum(2, (i, 1, n)).doit() - 2 * n) == 0, "num_elements = 2n")
    # Prime factors (with multiplicity) across items: each item has one p_i
    # (2 per variable); clause exponents sum_i (nu-_ji + nu+_ji) = 3 per clause.
    total = Sum(2, (i, 1, n)).doit() + Sum(3, (j, 1, m)).doit()
    check(S, simplify(total - (2 * n + 3 * m)) == 0, "2n+3m")
    check(S, simplify(Sum(1, (i, 1, n)).doit() + Sum(1, (j, 1, m)).doit() - (n + m)) == 0, "n+m in B")
    # bit-size: log2 of sum of log2 of the (n+m)-th prime bound (p_k < k(ln k + ln ln k) for k>=6)
    # concrete check of per-instance identities with sympy.factorint below
    for nn, mm in [(0, 0), (1, 1), (3, 3), (5, 2), (4, 0)]:
        if nn == 0 and mm > 0:
            continue
        cl = [tuple(random.choice([-1, 1]) * random.randint(1, nn) for _ in range(3)) for _ in range(mm)]
        items, B = reduce(nn, cl)
        omega = sum(sum(factorint(x).values()) for x in items)
        check(S, omega == 2 * nn + 3 * mm, "sympy factorint Omega total")
        check(S, sum(factorint(B).values()) == nn + mm if B > 1 else nn + mm == 0, "Omega(B)")
        check(S, len(items) == 2 * nn, "len")
    # the clause-exponent identity nu- + nu+ summed over variables equals 3, for all
    # clause shapes (exhaustive, n=4)
    for c in all_clauses(4):
        tot = sum(sum(1 for l in c if abs(l) == v) for v in range(1, 5))
        check(S, tot == 3, "occurrence count 3")


def section2_equivalence(insts):
    S = "2-forward+backward"
    n_yes = n_no = 0
    for n, cl in insts:
        check(S, valid_source(n, cl), "generator produced invalid source")
        items, B = reduce(n, cl)
        k = 2 * n
        prods = all_subset_products(items)
        tgt_feasible = any(prods[mask] == B for mask in range(1 << k))
        # Cross-check against tgt_eval on the witnesses
        src_sat = [a for a in itertools.product([False, True], repeat=n) if src_eval(n, cl, list(a))]
        check(S, tgt_feasible == bool(src_sat), f"feasibility differs n={n} cl={cl}")
        n_yes += bool(src_sat)
        n_no += not src_sat
        # forward: every satisfying assignment maps to the selection of matching items
        for a in src_sat:
            mask = 0
            for ii, v in enumerate(a):
                mask |= 1 << (2 * ii + (1 if v else 0))
            check(S, prods[mask] == B, f"forward fails {n} {cl} {a}")
        # backward + counting: number of target witnesses == number of satisfying assignments
        wit = [mask for mask in range(1 << k) if prods[mask] == B]
        check(S, len(wit) == len(src_sat), f"witness count {len(wit)} vs {len(src_sat)}")
    check(S, n_yes > 1000 and n_no > 1000, f"coverage yes={n_yes} no={n_no}")
    print(f"    (YES instances {n_yes}, NO instances {n_no})")


def section3_extraction(insts):
    S = "3-extraction"
    for n, cl in insts:
        items, B = reduce(n, cl)
        k = 2 * n
        prods = all_subset_products(items)
        for mask in range(1 << k):
            if prods[mask] != B:
                continue
            sel = bits(mask, k)
            check(S, tgt_eval(items, B, sel), "witness evaluation")
            for i in range(n):
                check(S, sel[2 * i] != sel[2 * i + 1], f"exactly one of pair {i}")
            a = extract(sel)
            check(S, src_eval(n, cl, a), f"extracted not satisfying n={n} cl={cl} sel={sel}")
            # extraction is injective on witnesses and re-encoding returns the witness
            re = []
            for v in a:
                re += [not v, v]
            check(S, re == sel, "re-encode")
        # wrong-length / non-witness rejection model: a non-witness must not evaluate true
        for mask in random.sample(range(1 << k), min(8, 1 << k)):
            if prods[mask] != B:
                check(S, not tgt_eval(items, B, bits(mask, k)), "non-witness accepted")


def section4_sizes(insts):
    S = "4-sizes"
    for n, cl in insts:
        m = len(cl)
        items, B = reduce(n, cl)
        pr = first_primes(n + m)
        check(S, len(items) == 2 * n, "num_elements = 2n")
        fb = factorint(B) if B > 1 else {}
        check(S, fb == {p: 1 for p in pr}, f"B is the product of the first n+m primes n={n} m={m}")
        omega = 0
        for idx, x in enumerate(items):
            f = factorint(x)
            omega += sum(f.values())
            i, t = divmod(idx, 2)
            check(S, f.get(pr[i], 0) == 1, "variable prime exponent 1")
            for j, c in enumerate(cl):
                want = sum(1 for l in c if l == (i + 1) * (1 if t else -1))
                check(S, f.get(pr[n + j], 0) == want, "clause prime exponent = occurrence count")
            check(S, set(f) <= {pr[i]} | set(pr[n:]), "no foreign primes")
        check(S, omega == 2 * n + 3 * m, "total factor count 2n+3m")
        # bit size of the encoding: O((n+m) log(n+m+2)); explicit bound
        bitsz = sum(x.bit_length() for x in items) + B.bit_length()
        pmax = pr[-1] if pr else 2
        check(S, bitsz <= (2 * n + 3 * m + n + m + 1) * pmax.bit_length(), "bit-size bound")


def section5_structure(insts):
    S = "5-structure"
    for n, cl in insts:
        m = len(cl)
        items, B = reduce(n, cl)
        pr = first_primes(n + m)
        check(S, all(x >= 1 for x in items) and B >= 1, "positive")
        check(S, len(set(pr)) == n + m, "distinct primes")
        for i in range(n):
            holders = [idx for idx, x in enumerate(items) if x % pr[i] == 0]
            check(S, holders == [2 * i, 2 * i + 1], "p_i divides exactly its two items")
        for j in range(m):
            q = pr[n + j]
            tot = 0
            for i in range(n):
                for t in (0, 1):
                    x = items[2 * i + t]
                    e = 0
                    while x % q == 0:
                        x //= q
                        e += 1
                    tot += e
            # each literal occurrence adds one to one item: false/true items together
            # carry exactly the three occurrences of the clause
            check(S, tot == 3, "clause prime total exponent 3 across items")
        # per variable: false and true item exponents of q_j sum to the variable's occurrences
        if n > 0 and m > 0:
            for i in range(n):
                for j, c in enumerate(cl):
                    q = pr[n + j]
                    ef = ep = 0
                    x, y = items[2 * i], items[2 * i + 1]
                    while x % q == 0:
                        x //= q
                        ef += 1
                    while y % q == 0:
                        y //= q
                        ep += 1
                    check(S, ef + ep == sum(1 for l in c if abs(l) == i + 1), "occurrence split")
    # special cases, asserted explicitly (cannot be satisfied by accident)
    items, B = reduce(3, [(1, 1, 1)])  # repeated literal: x1 x1 x1
    check(S, items[0:2] == [2, 2 * 7 ** 3], "triple repeat exponent 3")
    items, B = reduce(2, [(1, -1, 2)])  # both signs in one clause
    q = first_primes(3)[2]
    check(S, items == [2 * q, 2 * q, 3 * q, 3 * q][:0] + [2 * q, 2 * q, 3, 3 * q], "both signs: equal-valued items stay indexed")
    check(S, items[0] == items[1], "equal-valued false/true items for x1")
    items, B = reduce(3, [])  # unused variables, m = 0
    check(S, items == [2, 2, 3, 3, 5, 5] and B == 30, "unused variables: equal items kept separate")
    items, B = reduce(0, [])
    check(S, items == [] and B == 1, "empty instance")
    check(S, tgt_eval(items, B, []) is True, "empty instance feasible (empty product 1)")


def section6_yes():
    S = "6-YES example"
    n, cl = 3, [(1, 2, 3), (-1, -2, 3), (1, -2, -3)]
    items, B = reduce(n, cl)
    check(S, items == [22, 182, 429, 21, 65, 385], f"items {items}")
    check(S, B == 30030, "B = 30030")
    # assignment (0,1,0): x1 false, x2 true, x3 false -> items 0,3,4  (vector (1,0,0,1,1,0))
    sel = [True, False, False, True, True, False]
    check(S, tgt_eval(items, B, sel), "22*21*65 = 30030")
    check(S, 22 * 21 * 65 == 30030, "arithmetic")
    check(S, extract(sel) == [False, True, False], "extraction (0,1,0)")
    check(S, src_eval(n, cl, [False, True, False]), "source satisfied")
    # all satisfying assignments agree with all target witnesses
    prods = all_subset_products(items)
    wit = [bits(mk, 6) for mk in range(64) if prods[mk] == B]
    sat = [list(a) for a in itertools.product([False, True], repeat=3) if src_eval(n, cl, list(a))]
    check(S, sorted(extract(w) for w in wit) == sorted(sat), "witness set matches satisfying set")
    check(S, len(sat) == 1, "unique satisfying assignment")


def section7_no():
    S = "7-NO example"
    n, cl = 3, [(1, 2, 3), (-1, -2, -3)]
    items, B = reduce(n, cl)
    check(S, items == [22, 14, 33, 21, 55, 35], f"items {items}")
    check(S, B == 2310, "B = 2310")
    check(S, not any(src_eval(n, cl, list(a)) for a in itertools.product([False, True], repeat=3)), "source infeasible")
    prods = all_subset_products(items)
    check(S, not any(prods[mk] == B for mk in range(64)), "target infeasible")
    # reason: with t true variables exponents of 7 and 11 are t and 3-t
    for mk in range(64):
        sel = bits(mk, 6)
        if all(sel[2 * i] != sel[2 * i + 1] for i in range(3)):
            t = sum(sel[2 * i + 1] for i in range(3))
            x = prods[mk]
            e7 = e11 = 0
            while x % 7 == 0:
                x //= 7
                e7 += 1
            while x % 11 == 0:
                x //= 11
                e11 += 1
            check(S, e7 == t and e11 == 3 - t, "exponents t and 3-t")
            check(S, not (e7 == 1 and e11 == 1), "cannot both be one")


def hypothesis_section():
    S = "2-hypothesis"
    lit = st.integers(min_value=1, max_value=6).flatmap(lambda v: st.sampled_from([v, -v]))

    # strategy 1: arbitrary instances, literals drawn freely (n=6 allowed; repeats common)
    s1 = st.integers(min_value=1, max_value=6).flatmap(
        lambda n: st.tuples(
            st.just(n),
            st.lists(
                st.tuples(*[st.integers(1, n).flatmap(lambda v: st.sampled_from([v, -v]))] * 3).map(tuple),
                min_size=0, max_size=5,
            ),
        )
    )

    # strategy 2: planted-satisfiable instances (one true literal per clause under a hidden assignment)
    @st.composite
    def planted(draw):
        n = draw(st.integers(1, 6))
        a = draw(st.lists(st.booleans(), min_size=n, max_size=n))
        m = draw(st.integers(0, 5))
        cl = []
        for _ in range(m):
            for _try in range(200):
                c = tuple(draw(st.integers(1, n)) * draw(st.sampled_from([1, -1])) for _ in range(3))
                if sum(lit_true(l, a) for l in c) == 1:
                    cl.append(c)
                    break
        return n, cl, a

    @settings(max_examples=300, deadline=None, database=None,
              suppress_health_check=list(HealthCheck), derandomize=True)
    @given(s1)
    def run1(inst):
        n, cl = inst
        items, B = reduce(n, cl)
        prods = all_subset_products(items)
        wit = [mk for mk in range(1 << (2 * n)) if prods[mk] == B]
        sat = [a for a in itertools.product([False, True], repeat=n) if src_eval(n, cl, list(a))]
        check(S, len(wit) == len(sat), "hyp1 witness count")
        for mk in wit:
            check(S, src_eval(n, cl, extract(bits(mk, 2 * n))), "hyp1 extraction")

    @settings(max_examples=300, deadline=None, database=None,
              suppress_health_check=list(HealthCheck), derandomize=True)
    @given(planted())
    def run2(inst):
        n, cl, a = inst
        # a satisfies every clause by construction when all were found
        if all(sum(lit_true(l, a) for l in c) == 1 for c in cl) and len(cl) > 0:
            items, B = reduce(n, cl)
            sel = []
            for v in a:
                sel += [not v, v]
            check(S, tgt_eval(items, B, sel), "hyp2 planted assignment maps to witness")
            check(S, extract(sel) == list(a), "hyp2 extraction")
            # flipping one variable's choice breaks the pair constraint or a clause
            for i in range(n):
                sel2 = list(sel)
                sel2[2 * i], sel2[2 * i + 1] = sel2[2 * i + 1], sel2[2 * i]
                ok = src_eval(n, cl, extract(sel2))
                check(S, tgt_eval(items, B, sel2) == ok, "hyp2 flip agrees with source")

    run1()
    run2()


def main():
    insts = list(instances())
    print(f"seed {SEED}; {len(insts)} instances")
    section1_symbolic()
    print("section 1 done")
    section2_equivalence(insts)
    print("section 2 done")
    section3_extraction(insts)
    print("section 3 done")
    section4_sizes(insts)
    print("section 4 done")
    section5_structure(insts)
    print("section 5 done")
    section6_yes()
    section7_no()
    hypothesis_section()
    total = 0
    for s in sorted(counts):
        print(f"  {s}: {counts[s]} checks, {fails[s]} failures")
        total += counts[s]
    for line in failure_log:
        print("FAIL", line)
    nf = sum(fails.values())
    print(f"TOTAL: {total} checks, {nf} failures")
    sys.exit(1 if nf else 0)


if __name__ == "__main__":
    main()
Adversary script (Python)
#!/usr/bin/env python3
"""Independent adversary for OneInThreeSatisfiability -> SubsetProduct (issue 1201).

Written from the proof alone (the other script not consulted). Own prime
generator (sieve), own reduce / extract, own source and target checkers.

Instance format: (n, clauses) with clauses a list of 3-tuples of nonzero ints in
[-n, n] \\ {0}. Target format: (items: list[int], B: int).

Run: python3 <this script> [--quick]
Ends with "TOTAL: <checks> checks, <failures> failures".
"""
import itertools
import math
import random
import sys
import time
from collections import Counter, defaultdict

SEED = 2026110304
CHECKS = Counter()
FAILS = Counter()
FAIL_EXAMPLES = defaultdict(list)


def check(section, cond, detail=""):
    CHECKS[section] += 1
    if not cond:
        FAILS[section] += 1
        if len(FAIL_EXAMPLES[section]) < 5:
            FAIL_EXAMPLES[section].append(detail() if callable(detail) else detail)


# ---------------------------------------------------------------- primitives

def sieve_primes(k):
    """First k primes via a growing sieve of Eratosthenes."""
    if k == 0:
        return []
    limit = 16
    while True:
        flags = bytearray([1]) * (limit + 1)
        flags[0] = flags[1] = 0
        for p in range(2, int(limit ** 0.5) + 1):
            if flags[p]:
                flags[p * p :: p] = bytearray(len(flags[p * p :: p]))
        ps = [i for i in range(limit + 1) if flags[i]]
        if len(ps) >= k:
            return ps[:k]
        limit *= 2


def is_valid_source(n, clauses):
    return n >= 0 and all(
        len(c) == 3 and all(isinstance(l, int) and l != 0 and abs(l) <= n for l in c)
        for c in clauses
    )


def source_ok(n, clauses, assign):
    """OneInThreeSatisfiability.evaluate: length must match; exactly one true occurrence per clause."""
    if len(assign) != n:
        raise ValueError("wrong assignment length")
    return all(count_true(c, assign) == 1 for c in clauses)


def count_true(clause, assign):
    """Number of true literal occurrences of a clause under an assignment."""
    t = 0
    for l in clause:
        v = assign[abs(l) - 1]
        t += (v if l > 0 else not v)
    return t


def target_ok(items, B, sel):
    """SubsetProduct.evaluate: length must match; product of selected == B."""
    if len(sel) != len(items):
        raise ValueError("wrong selection length")
    prod = 1
    for x, s in zip(items, sel):
        if s:
            prod *= x
    return prod == B


def reduce(n, clauses, primes=None):
    """Construction steps 1-4 of the proof."""
    primes = primes or globals()["sieve_primes"]
    assert is_valid_source(n, clauses)
    m = len(clauses)
    pr = primes(n + m)
    p, q = pr[:n], pr[n:]
    items = []
    for i in range(1, n + 1):
        f = p[i - 1]
        t = p[i - 1]
        for j, c in enumerate(clauses):
            nu_minus = sum(1 for l in c if l == -i)
            nu_plus = sum(1 for l in c if l == i)
            f *= q[j] ** nu_minus
            t *= q[j] ** nu_plus
        items += [f, t]
    B = math.prod(pr)
    return items, B


def extract(items, B, sel):
    """Validate the witness, then source bit i = sel[2i+1]."""
    if not target_ok(items, B, sel):
        raise ValueError("not a target witness")
    return [bool(sel[2 * i + 1]) for i in range(len(items) // 2)]


def raises(fn, *args):
    try:
        fn(*args)
    except ValueError:
        return True
    return False


def encode(assign):
    sel = []
    for a in assign:
        sel += [not a, bool(a)]
    return sel


def subset_products(items):
    """prods[mask] = product of items[k] for bit k of mask set."""
    prods = [1]
    for x in items:
        prods = prods + [v * x for v in prods]
    return prods


def mask_to_sel(mask, k):
    return [bool((mask >> b) & 1) for b in range(k)]


def all_sat(n, clauses):
    return [list(a) for a in itertools.product([False, True], repeat=n) if source_ok(n, clauses, list(a))]


def factor_over(x, primes):
    """Exponent vector of x over the given primes, and the cofactor."""
    if x <= 0:  # not a positive integer: no valid factorization
        return [0] * len(primes), x
    exps = []
    for p in primes:
        e = 0
        while x % p == 0:
            x //= p
            e += 1
        exps.append(e)
    return exps, x


# ---------------------------------------------------------------- per-instance battery

def battery(n, clauses, reduce_fn=None, extract_fn=None, tag="exh"):
    """Forward/backward equivalence, extraction, witness bijection, overhead, structure."""
    reduce_fn = reduce_fn or globals()["reduce"]
    extract_fn = extract_fn or globals()["extract"]
    m = len(clauses)
    items, B = reduce_fn(n, clauses)
    k = len(items)
    # --- overhead (section 4)
    check("4 overhead", k == 2 * n, lambda: f"num_elements {k} != 2n for {n},{clauses}")
    pr = sieve_primes(n + m)
    total_mult = 0
    exps_all = []
    for x in items:
        e, cof = factor_over(x, pr)
        exps_all.append(e)
        total_mult += sum(e)
        check("5 structure", cof == 1 and x > 0, lambda: f"item {x} not over first n+m primes {n},{clauses}")
    check("4 overhead", total_mult == 2 * n + 3 * m, lambda: f"factor count {total_mult} != 2n+3m {n},{clauses}")
    eB, cofB = factor_over(B, pr)
    check("4 overhead", cofB == 1 and eB == [1] * (n + m), lambda: f"B={B} not squarefree product of first n+m primes")
    # encoding bit-size bound O((n+m) log(n+m+2)): sum of item bits <= (2n+3m) * bits(max prime) + k
    if pr:
        bitbound = (2 * n + 3 * m) * pr[-1].bit_length() + k
        check("4 overhead", sum(x.bit_length() for x in items) <= bitbound, "item bits exceed bound")
        check("4 overhead", B.bit_length() <= (n + m) * pr[-1].bit_length(), "B bits exceed bound")
    # --- structure (section 5): exact exponents and private variable prime
    for i in range(1, n + 1):
        for side in (0, 1):
            e = exps_all[2 * (i - 1) + side]
            check("5 structure", e[i - 1] == 1 and all(e[v] == 0 for v in range(n) if v != i - 1),
                  lambda: f"item {2*(i-1)+side} var-prime exponents {e[:n]}")
            for j, c in enumerate(clauses):
                want = sum(1 for l in c if l == (i if side else -i))
                check("5 structure", e[n + j] == want,
                      lambda: f"item ({i},{side}) q_{j} exponent {e[n+j]} != {want} in {clauses}")
    # --- equivalence (section 2): enumerate every target selection
    prods = subset_products(items)
    tw = [mask for mask, v in enumerate(prods) if v == B]
    sats = all_sat(n, clauses)
    check("2 equivalence", (len(tw) > 0) == (len(sats) > 0),
          lambda: f"feasibility mismatch {n},{clauses}: src {len(sats)} tgt {len(tw)}")
    # forward: every satisfying assignment's encoding is a witness
    for a in sats:
        check("2 equivalence", target_ok(items, B, encode(a)), lambda: f"forward fails {a} {clauses}")
    # backward: every witness picks one item per variable and decodes to a satisfying assignment
    check("2 equivalence", len(tw) == len(sats),
          lambda: f"#witnesses {len(tw)} != #sat {len(sats)} {n},{clauses}")
    wset = set()
    for mask in tw:
        sel = mask_to_sel(mask, k)
        check("2 equivalence", all(sel[2 * i] + sel[2 * i + 1] == 1 for i in range(n)),
              lambda: f"witness not one-per-variable {sel}")
        # --- extraction (section 3)
        try:
            a = extract_fn(items, B, sel)
        except ValueError:
            check("3 extraction", False, f"extract rejected witness {sel}")
            continue
        check("3 extraction", len(a) == n and source_ok(n, clauses, a),
              lambda: f"extracted {a} not satisfying {clauses}")
        check("3 extraction", encode(a) == sel, lambda: f"encode(extract(s)) != s for {sel}")
        wset.add(tuple(sel))
    check("3 extraction", wset == {tuple(encode(a)) for a in sats}, "witness set != encoded satisfying set")
    # extraction rejects non-witnesses (sample a few)
    rng = random.Random(hash((n, tuple(map(tuple, clauses)))) & 0xFFFF)
    for _ in range(3):
        mask = rng.randrange(len(prods))
        if prods[mask] != B:
            check("3 extraction", raises(extract_fn, items, B, mask_to_sel(mask, k)),
                  f"extract accepted non-witness {mask}")
    check("3 extraction", raises(extract_fn, items, B, [False] * (k + 1)), "extract accepted wrong length")
    return len(sats) > 0


# ---------------------------------------------------------------- instance families

def all_clauses(n):
    lits = [l for v in range(1, n + 1) for l in (v, -v)]
    return list(itertools.product(lits, repeat=3))


def exhaustive_instances():
    yield 0, []
    for n in range(1, 6):
        yield n, []
    for n in range(1, 6):
        for c in all_clauses(n):
            yield n, [c]
    for n in range(1, 4):
        cl = all_clauses(n)
        for c1 in cl:
            for c2 in cl:
                yield n, [c1, c2]
    cl = all_clauses(2)
    for trip in itertools.combinations_with_replacement(cl, 3):
        yield 2, list(trip)


def random_clause(rng, n, pool=None):
    vs = pool or list(range(1, n + 1))
    return tuple(rng.choice(vs) * rng.choice((1, -1)) for _ in range(3))


def planted(rng, n, m):
    a = [rng.random() < 0.5 for _ in range(n)]
    cls = []
    while len(cls) < m:
        c = random_clause(rng, n)
        # generator's own truth count, so a mutated checker cannot stall instance generation
        if sum((a[abs(l) - 1] if l > 0 else not a[abs(l) - 1]) for l in c) == 1:
            cls.append(c)
    return a, cls


def random_instances(rng, per=300):
    for n in range(1, 7):
        for m in range(1, 7):
            for t in range(per):
                if t % 3 == 0:
                    _, cls = planted(rng, n, m)
                elif t % 3 == 1:
                    pool = rng.sample(range(1, n + 1), k=min(n, 2))  # heavy repetition, unused vars
                    cls = [random_clause(rng, n, pool) for _ in range(m)]
                else:
                    cls = [random_clause(rng, n) for _ in range(m)]
                yield n, cls


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

def section1_symbolic():
    import sympy as sp
    n, m = sp.symbols("n m", integer=True, nonnegative=True)
    i, j = sp.symbols("i j", integer=True, positive=True)
    # items: 2 per variable
    num_elements = sp.summation(2, (i, 1, n))
    check("1 symbolic", sp.simplify(num_elements - 2 * n) == 0, "sum_i 2 != 2n")
    # factor multiplicity: each item one p_i (2n), each clause 3 positions each in exactly one item
    total = sp.summation(2, (i, 1, n)) + sp.summation(3, (j, 1, m))
    check("1 symbolic", sp.simplify(total - (2 * n + 3 * m)) == 0, "factor count != 2n+3m")
    check("1 symbolic", sp.simplify(sp.summation(1, (i, 1, n)) + sp.summation(1, (j, 1, m)) - (n + m)) == 0,
          "B factor count")
    # per-variable item exponent balance: nu^- + nu^+ summed over variables = 3 per clause
    a, b, c = sp.symbols("a b c", integer=True, nonnegative=True)
    check("1 symbolic", sp.simplify((a + b + c) - 3).subs({a: 1, b: 1, c: 1}) == 0, "3 positions")
    # prime number theorem bound: pi_k <= k(ln k + ln ln k) for k >= 6 -> bits O(log k)
    for kk in [6, 10, 100, 1000, 5000]:
        pk = sieve_primes(kk)[-1]
        check("1 symbolic", pk <= kk * (math.log(kk) + math.log(math.log(kk))), f"Rosser bound fails k={kk}")
    # sympy prime() agrees with our sieve
    ps = sieve_primes(300)
    for idx in range(1, 301):
        check("1 symbolic", sp.prime(idx) == ps[idx - 1], f"prime({idx})")


def section_large(rng):
    """Big instances: forward with planted assignment, extraction, and single-bit flips fail."""
    for n, m in [(40, 60), (120, 200), (300, 500)]:
        for _ in range(3):
            a, cls = planted(rng, n, m)
            items, B = reduce(n, cls)
            check("4 overhead", len(items) == 2 * n)
            check("4 overhead", B.bit_length() > 64, "expect beyond fixed width")
            s = encode(a)
            check("2 equivalence", target_ok(items, B, s), "planted forward fails (large)")
            check("3 extraction", extract(items, B, s) == a, "large extraction")
            for _ in range(20):
                b = rng.randrange(2 * n)
                s2 = list(s)
                s2[b] = not s2[b]
                check("2 equivalence", not target_ok(items, B, s2), "single flip still product B")


def section6_yes():
    n = 3
    cls = [(1, 2, 3), (-1, -2, 3), (1, -2, -3)]
    items, B = reduce(n, cls)
    check("6 YES", sieve_primes(6) == [2, 3, 5, 7, 11, 13])
    check("6 YES", items == [22, 182, 429, 21, 65, 385], f"items {items}")
    check("6 YES", [2 * 11, 2 * 7 * 13, 3 * 11 * 13, 3 * 7, 5 * 13, 5 * 7 * 11] == items)
    check("6 YES", B == 30030 == 2 * 3 * 5 * 7 * 11 * 13, f"B {B}")
    sel = [True, False, False, True, True, False]
    check("6 YES", 22 * 21 * 65 == 30030 and target_ok(items, B, sel))
    check("6 YES", extract(items, B, sel) == [False, True, False])
    check("6 YES", all_sat(n, cls) == [[False, True, False]], "unique satisfying assignment")
    prods = subset_products(items)
    check("6 YES", [mk for mk, v in enumerate(prods) if v == B] == [0b011001], "unique witness")
    # prose: clause 1 only x2 true; clause 2 only not x1; clause 3 only not x3
    a = [False, True, False]
    def truths(c):
        return [(a[abs(l) - 1] if l > 0 else not a[abs(l) - 1]) for l in c]
    check("6 YES", truths(cls[0]) == [False, True, False])
    check("6 YES", truths(cls[1]) == [True, False, False])
    check("6 YES", truths(cls[2]) == [False, False, True])
    battery(n, cls)


def section7_no():
    n = 3
    cls = [(1, 2, 3), (-1, -2, -3)]
    items, B = reduce(n, cls)
    check("7 NO", items == [22, 14, 33, 21, 55, 35], f"items {items}")
    check("7 NO", items == [2 * 11, 2 * 7, 3 * 11, 3 * 7, 5 * 11, 5 * 7])
    check("7 NO", B == 2310)
    check("7 NO", all_sat(n, cls) == [], "source should be unsat")
    prods = subset_products(items)
    check("7 NO", len(prods) == 64 and all(v != 2310 for v in prods), "some selection hits 2310")
    # argument: t true -> clause1 count t, clause2 count 3-t
    for a in itertools.product([False, True], repeat=3):
        t = sum(a)
        c1, c2 = count_true(cls[0], list(a)), count_true(cls[1], list(a))
        check("7 NO", c1 == t and c2 == 3 - t and not (c1 == 1 and c2 == 1))
        # exponents of 7, 11 in the one-per-variable selection
        prod = math.prod(items[2 * i + int(a[i])] for i in range(3))
        e, _ = factor_over(prod, [7, 11])
        check("7 NO", e == [t, 3 - t], f"exponents {e} for t={t}")
    battery(n, cls)


def section_special_cases():
    S = "5 structure"
    # (x1,x1,x1): exponent 3 on true item, 0 on false item; unsat
    items, B = reduce(1, [(1, 1, 1)])
    check(S, items == [2, 2 * 3 ** 3], f"(x1,x1,x1) items {items}")
    check(S, all_sat(1, [(1, 1, 1)]) == [] and all(v != B for v in subset_products(items)))
    items, B = reduce(1, [(-1, -1, -1)])
    check(S, items == [2 * 3 ** 3, 2])
    # (x1,x1,x2): exponent 2 for x1 true
    items, B = reduce(2, [(1, 1, 2)])
    e, _ = factor_over(items[1], [5])
    check(S, e == [2], f"(x1,x1,x2) true item exponent {e}")
    # (x1,-x1,x2) with n=2: both items of x1 equal 10
    items, B = reduce(2, [(1, -1, 2)])
    check(S, items[0] == items[1] == 10, f"both-sign items {items[:2]}")
    sats = all_sat(2, [(1, -1, 2)])
    check(S, sats == [[False, False], [True, False]], f"both-sign sats {sats}")
    tw = [mk for mk, v in enumerate(subset_products(items)) if v == B]
    check(S, len(tw) == 2, f"equal-valued items must stay distinct: {len(tw)} witnesses")
    # unused variables: both items equal p_i
    items, B = reduce(4, [(1, 2, 2)])
    pr = sieve_primes(5)
    check(S, items[4] == items[5] == pr[2] and items[6] == items[7] == pr[3], f"unused items {items}")
    # no clauses: items (p1,p1,...), B = prod p, 2^n witnesses
    for n in range(0, 7):
        items, B = reduce(n, [])
        pr = sieve_primes(n)
        check(S, items == [p for p in pr for _ in (0, 1)] and B == math.prod(pr))
        check(S, sum(v == B for v in subset_products(items)) == 2 ** n, f"m=0 witness count n={n}")
    # empty instance
    items, B = reduce(0, [])
    check(S, items == [] and B == 1 and target_ok(items, B, []) and source_ok(0, [], []))
    check(S, extract(items, B, []) == [])
    # m>0 implies n>=1: no valid clause with n=0
    check(S, not is_valid_source(0, [(1, 1, 1)]) and all_clauses(0) == [])
    # invalid literals rejected (0 and out of range)
    check(S, not is_valid_source(2, [(0, 1, 2)]) and not is_valid_source(2, [(1, 2, 3)]))
    # source evaluate rejects wrong length
    check(S, raises(source_ok, 2, [], [True]), "source accepted wrong length")


def hypothesis_section(n_examples=300):
    from hypothesis import given, settings, strategies as st, HealthCheck

    cfg = settings(max_examples=n_examples, deadline=None, database=None, derandomize=True,
                   suppress_health_check=list(HealthCheck))

    # Strategy A: degenerate literal patterns -- repeats, both signs, few distinct variables.
    @st.composite
    def degenerate(draw):
        n = draw(st.integers(1, 6))
        used = draw(st.lists(st.integers(1, n), min_size=1, max_size=2, unique=True))
        m = draw(st.integers(0, 6))
        cls = []
        for _ in range(m):
            cls.append(tuple(draw(st.sampled_from(used)) * draw(st.sampled_from([1, -1])) for _ in range(3)))
        return n, cls

    # Strategy B: planted satisfiable at the 'exactly one' boundary, plus a perturbation that may
    # turn one clause into a 0- or 2-true clause (boundary between YES and NO).
    @st.composite
    def boundary(draw):
        n = draw(st.integers(1, 6))
        a = draw(st.lists(st.booleans(), min_size=n, max_size=n))
        m = draw(st.integers(1, 6))
        cls = []
        for _ in range(m):
            trues = draw(st.integers(0, 3))  # number of true occurrences the clause should get
            lits = []
            for pos in range(3):
                v = draw(st.integers(1, n))
                want_true = pos < trues
                sign = 1 if a[v - 1] == want_true else -1
                lits.append(sign * v)
            perm = draw(st.permutations(lits))
            cls.append(tuple(perm))
        return n, cls

    # Strategy C: n up to 6 with unused trailing variables and possibly duplicate clauses.
    @st.composite
    def dup_clauses(draw):
        n = draw(st.integers(1, 6))
        base = draw(st.lists(st.tuples(*[st.integers(1, n).flatmap(
            lambda v: st.sampled_from([v, -v]))] * 3), min_size=1, max_size=3))
        reps = draw(st.lists(st.sampled_from(base), min_size=0, max_size=3))
        return n, base + reps

    counts = Counter()

    for name, strat in [("A", degenerate()), ("B", boundary()), ("C", dup_clauses())]:
        @cfg
        @given(strat)
        def run(inst):
            n, cls = inst
            counts[name] += 1
            CHECKS["H hypothesis"] += 1
            battery(n, cls, tag="hyp" + name)
        run()
    return counts


def safe_battery(n, cls, **kw):
    try:
        return battery(n, cls, **kw)
    except Exception as e:
        check("2 equivalence", False, f"battery crashed on {n},{cls}: {type(e).__name__}: {e}")
        return False


def main():
    quick = "--quick" in sys.argv
    t0 = time.time()
    rng = random.Random(SEED)
    section1_symbolic()
    n_inst = yes = 0
    for n, cls in exhaustive_instances():
        n_inst += 1
        yes += safe_battery(n, cls)
    n_exh = n_inst
    for n, cls in random_instances(rng, per=60 if quick else 300):
        n_inst += 1
        yes += safe_battery(n, cls, tag="rand")
    hc = {}
    for name, fn in [("2 equivalence", lambda: section_large(rng)), ("5 structure", section_special_cases),
                     ("6 YES", section6_yes), ("7 NO", section7_no),
                     ("H hypothesis", lambda: hc.update(hypothesis_section(50 if quick else 300)))]:
        try:
            fn()
        except Exception as e:  # a crash is a failure of that section, not of the whole run
            check(name, False, f"{fn} crashed: {type(e).__name__}: {e}")
    print(f"instances: {n_inst} ({n_exh} exhaustive, {n_inst - n_exh} random), YES {yes}, NO {n_inst - yes}")
    print(f"hypothesis examples per strategy: {dict(hc)}")
    for sec in sorted(CHECKS):
        print(f"  {sec:16s} {CHECKS[sec]:>9d} checks  {FAILS[sec]:>6d} failures")
        for ex in FAIL_EXAMPLES[sec]:
            print(f"      e.g. {ex}")
    print(f"seed {SEED}, {time.time() - t0:.1f} s")
    total, fails = sum(CHECKS.values()), sum(FAILS.values())
    print(f"TOTAL: {total} checks, {fails} failures")
    return 1 if fails else 0


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

@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Agentic Review Report

Subject: rule OneInThreeSatisfiability → SubsetProduct (#1201).

The change adds two new files:

  • src/rules/oneinthreesatisfiability_subsetproduct.rs
  • src/unit_tests/rules/oneinthreesatisfiability_subsetproduct.rs

It edits five more:

  • src/rules/mod.rs
  • src/rules/oneinthreesatisfiability_ilp.rs
  • src/rules/exactcoverby3sets_subsetproduct.rs
  • src/models/misc/subset_product.rs
  • docs/paper/reductions.typ

Verdict: approve. There are no open findings. All five findings from the previous round are fixed.

Structural and semantic

  • Registration and transform: #[reduction(transform = exact { num_elements = "2 * num_vars" })] covers the target's only registered parameter. The module and the canonical example are registered in src/rules/mod.rs. No generated exports are in the diff.
  • Result mapping: both endpoints have Or values, so #[aggregate_reduction(identity)] matches docs/src/design.md.
  • Target construction: the target is now built with SubsetProduct::try_new, and its error goes through target_construction. The model change only makes try_new pub(crate); its behaviour is unchanged.
  • Prime helpers: primes and the target product reuse assigned_primes and product_biguint. Both are now pub(crate) in exactcoverby3sets_subsetproduct.rs with their bodies unchanged, and the copies were removed.
  • Extraction: it validates the target witness once, then reads the true item 2i+1 of each variable.
  • Hand trace: the issue's YES instance gives items (22, 182, 429, 21, 65, 385) and B = 30030. The NO instance gives (22, 14, 33, 21, 55, 35) and B = 2310. The empty instance gives no items and B = 1.
  • Overflow: the checked doubling is tested on both sides of the usize::MAX / 2 boundary. The unchecked num_vars + num_clauses is justified in a comment and is sound.
  • ILP rule edit: the change to oneinthreesatisfiability_ilp.rs only disambiguates target_construction, which became necessary once the source had a second ReduceTo impl.
  • Issue compliance: construction, extraction, the size formula and both examples match the issue. The issue has no comments.

Quality

  • Tests (11):
    • the issue's YES and NO examples number for number, with the YES witness shown unique by brute force;
    • every ordered single clause over 3 variables and every ordered clause pair over 2 variables, each with its full witness set compared against the source;
    • two 5-variable instances;
    • every special case: repeated literals, both signs in one clause, unused variables, no clauses and the empty instance;
    • the first twelve primes;
    • a planted 20-variable, 30-clause instance with B above 64 bits, checking the witness, its extraction and two rejected selections;
    • extraction rejection, the identity value mapping, and the overflow boundary.
  • Paper entry:
    • it rebuilds the primes, items, factorizations, target and true-item positions from the loaded fixture, with asserts;
    • it adds a NO example whose 64 selections are all asserted to miss B.
  • Coverage: the rule file has 100% line coverage and 97.94% region coverage. The uncovered regions are the error branches, which the infallible construction cannot reach.

Feature test

  • CLI: using the debug pred build:
    • pred create --example OneInThreeSatisfiability --to SubsetProduct followed by pred reduce --via route.json gives the issue's items and target;
    • pred solve with brute force returns (0, 1, 0);
    • the NO instance reduced with --aggregate solves to infeasible, and pred extract --value false returns false.
  • Build checks: fmt, clippy (warnings denied), make check and make paper all pass, and the paper's asserts hold.
  • Round trips: 100/100 correct, 24 with a solution and 76 without.

Findings

None open.

1 similar comment
@isPANN

isPANN commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator Author

Agentic Review Report

Subject: rule OneInThreeSatisfiability → SubsetProduct (#1201).

The change adds two new files:

  • src/rules/oneinthreesatisfiability_subsetproduct.rs
  • src/unit_tests/rules/oneinthreesatisfiability_subsetproduct.rs

It edits five more:

  • src/rules/mod.rs
  • src/rules/oneinthreesatisfiability_ilp.rs
  • src/rules/exactcoverby3sets_subsetproduct.rs
  • src/models/misc/subset_product.rs
  • docs/paper/reductions.typ

Verdict: approve. There are no open findings. All five findings from the previous round are fixed.

Structural and semantic

  • Registration and transform: #[reduction(transform = exact { num_elements = "2 * num_vars" })] covers the target's only registered parameter. The module and the canonical example are registered in src/rules/mod.rs. No generated exports are in the diff.
  • Result mapping: both endpoints have Or values, so #[aggregate_reduction(identity)] matches docs/src/design.md.
  • Target construction: the target is now built with SubsetProduct::try_new, and its error goes through target_construction. The model change only makes try_new pub(crate); its behaviour is unchanged.
  • Prime helpers: primes and the target product reuse assigned_primes and product_biguint. Both are now pub(crate) in exactcoverby3sets_subsetproduct.rs with their bodies unchanged, and the copies were removed.
  • Extraction: it validates the target witness once, then reads the true item 2i+1 of each variable.
  • Hand trace: the issue's YES instance gives items (22, 182, 429, 21, 65, 385) and B = 30030. The NO instance gives (22, 14, 33, 21, 55, 35) and B = 2310. The empty instance gives no items and B = 1.
  • Overflow: the checked doubling is tested on both sides of the usize::MAX / 2 boundary. The unchecked num_vars + num_clauses is justified in a comment and is sound.
  • ILP rule edit: the change to oneinthreesatisfiability_ilp.rs only disambiguates target_construction, which became necessary once the source had a second ReduceTo impl.
  • Issue compliance: construction, extraction, the size formula and both examples match the issue. The issue has no comments.

Quality

  • Tests (11):
    • the issue's YES and NO examples number for number, with the YES witness shown unique by brute force;
    • every ordered single clause over 3 variables and every ordered clause pair over 2 variables, each with its full witness set compared against the source;
    • two 5-variable instances;
    • every special case: repeated literals, both signs in one clause, unused variables, no clauses and the empty instance;
    • the first twelve primes;
    • a planted 20-variable, 30-clause instance with B above 64 bits, checking the witness, its extraction and two rejected selections;
    • extraction rejection, the identity value mapping, and the overflow boundary.
  • Paper entry:
    • it rebuilds the primes, items, factorizations, target and true-item positions from the loaded fixture, with asserts;
    • it adds a NO example whose 64 selections are all asserted to miss B.
  • Coverage: the rule file has 100% line coverage and 97.94% region coverage. The uncovered regions are the error branches, which the infallible construction cannot reach.

Feature test

  • CLI: using the debug pred build:
    • pred create --example OneInThreeSatisfiability --to SubsetProduct followed by pred reduce --via route.json gives the issue's items and target;
    • pred solve with brute force returns (0, 1, 0);
    • the NO instance reduced with --aggregate solves to infeasible, and pred extract --value false returns false.
  • Build checks: fmt, clippy (warnings denied), make check and make paper all pass, and the paper's asserts hold.
  • Round trips: 100/100 correct, 24 with a solution and 76 without.

Findings

None open.

@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 (ac461c8).

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1236    +/-   ##
========================================
  Coverage   97.19%   97.20%            
========================================
  Files        1146     1148     +2     
  Lines      151558   151806   +248     
========================================
+ Hits       147308   147557   +249     
+ Misses       4250     4249     -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.

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] OneInThreeSatisfiability to SubsetProduct

1 participant