converse.py

Checks: TOWER: The unitary spectrum theorem; rule.

Cited on: TOWER.

To run it: save converse.py; standard library only.

"""converse.py -- the doubling rung's converse: past k = 3, must a rung
of the tower with no totient collision bring a new prime into lambda?

QUESTION. At the rung k, p = p_k, n = p - 1 and Q the odd primes below
p, a totient collision is a pair of disjoint subsets A, B of Q with
prod_A (q - 1) = n * prod_B (q - 1) (a prime in both cancels). A prime
of n dividing no q - 1, q in Q (a new prime of lambda) forbids one.
Does a rung past k = 3 with no new prime always have one?

DESIGN.
(1) THE CAPACITY (proved). A collision forces, at every prime l,
v_l(n) <= cap_l(p), the sum over q in Q of v_l(q - 1): v_l(prod_A) =
v_l(n) + v_l(prod_B) >= v_l(n). A new prime is cap 0. So the converse
fails by a CAPACITY BREAK at an old l (v_l(n) > cap_l > 0), or with
capacity held at every l and no collision.
(2) AT l = 2 the break is p = 5 alone: with e = v_2(n), Bertrand puts
an odd prime in (2^j, 2^(j+1)] for j = 1..e-1, all below p, so cap_2
>= e - 1, and 5 adds one more at e >= 3; at e = 2 and p >= 13, 3 and 5
give 3; at e = 1, 3 gives 1.
(3) AT ODD l a break with l old needs v_l(n) >= 2: p is the m-th prime
= 1 mod l with v_l(p - 1) above the sum over the earlier ones, m >= 2,
each at least 1, so v_l(p - 1) >= m, p > 2l^m, and fewer than m primes
= 1 mod l lie below 2l^m + 1. At m = 2, p = 1 mod l^2 is the second
prime = 1 mod l, with at most one prime among the l - 1 candidates
2lj + 1 below 2l^2. A TRANSPLANT (independent candidates, prime with chance
about 1/ln l): about (l/ln l) e^(-l/ln l) / l, so an m = 2 break, if
any, comes at a small l.
(4) THE WITNESS SEARCH. Below 100 the exact listing: U, the set of
prod (q - 1) over subsets of Q, against n U. Past it, a depth-first
search on the residual exponent vector r (r = v(n) at the start, a
prime put in A subtracts v(q - 1), in B adds it): take the largest
prime l with r_l != 0, put an unused q = 1 mod l on the side that
lowers |r_l|, the candidates ordered by the largest prime of (q - 1)
other than l, four tried, deepening to 12 moves. TABLE, the eleven
odd primes 3..41 whose q - 1 is 7-smooth, is kept out of the search;
once r lives on {2, 3, 5, 7}, a table of all 3^11 signed choices
among them, the fewest primes kept per vector, closes it. Every
witness is verified exactly: A, B disjoint, inside Q, no repeat, the
products equal.

PREDICTIONS (frozen before the code).
C1 (proved) cap_2 >= v_2(n) at every odd p but 3 (2 new) and 5 (the
   break); printed to 10^6.
C2 (pattern, 85%) no capacity break at an odd old l, p < 10^6.
C3 (pattern, 85%) at every rung 7 <= p_k < X1 with capacity held at
   every l, the search finds a witness.
C4 (a guess, 60%) |A| + |B| <= 6 at every witnessed rung.

THE KILL, frozen. The script prints CAPACITY BREAKS at odd l with l
and every prime of n old, p < 10^6: past 5, "CONVERSE FALSE AT p =
..." (proved, the capacity being necessary). Over 7 <= p < X1 with
capacity held, UNWITNESSED [...]: nonempty, "UNDECIDED AT ...". Both
empty: "THE CONVERSE HOLDS AT EVERY RUNG p_k < X1".

CONTROLS, read first. K1 17's witness found and verified. K2 the exact
listing below 100: its collision rungs are exactly the rungs with
capacity held, the search (run with the exact listing off) witnesses
every one at 43 <= p < 100 (the table's primes below p), and its
non-collision rungs are 3, 5, 7, 11, 23, 29, 47, 53, 59, 83, the
doubling rung's list to k = 24. K3 the verifier rejects planted
witnesses: A = {17} at 17, a repeated prime, a product off by 2. K4
the capacity flags 5 at l = 2 and no other p < 10^6 there.

FINDINGS (from the printed run; the controls passed first). K1 prints
17's witness A = {3, 5, 13}, B = {7}; K2 (narrowed after the freeze
from k <= 20 to 43..97, the table's primes lying below p) the listing's
non-collision
rungs below 100 are 3, 5, 7, 11, 23, 29, 47, 53, 59, 83, the rest
exactly the capacity-held rungs, and the search witnesses all 8
collision rungs in 43..97; K3 and K4 hold, the capacity at 2 breaking
at 5 alone below 10^6 (C1). Below 10^6, 13143 of the 78497 odd p bring
a new prime, the first 3, 7, 11, 23, 29, 47, 53, 59, 83, 103, 107,
149; CAPACITY BREAKS at an odd l: none, with every prime old or with a
new one beside (C2 holds). Every one of the 65353 rungs 7 <= p_k <
10^6 with capacity held is witnessed, 14 by the listing, verified
exactly: UNWITNESSED none (C3 holds). THE PRINT: THE CONVERSE HOLDS AT
EVERY RUNG p_k < 1000000. The search's |A| + |B|, not minimal (a
depth-first find, the table minimal over its own primes), runs 2 to 14
(C4 FAILS): 4875,
17711, 16421, 10367, 6616, 3866, 2392, 1029, 959, 795, 293, 27 and 2
rungs at 2..14, the largest at p = 958931. At X1 = 10^3, 10^4, 10^5
the same verdict, 116, 946 and 7726 rungs witnessed. A witness read
by hand: 9311, n = 2 * 5 * 7^2 * 19, A = {1217, 113, 11, 19, 29, 31},
B = {3, 5, 13, 17, 37, 41}: 2^15 3^3 5^2 7^2 19 over 2^14 3^3 5.
So the converse holds to 10^6, and with it the sharper statement:
there, a rung has a collision exactly when the capacity holds at every
l, the capacity failing only at a new prime and at 5.
RUN RECORD. Rehearsal --x1 1000: 6/6, 0.8 s, peak 119.8 MB; --x1
10^4 0.8 s, 120.7 MB; --x1 10^5 1.3 s, 127.7 MB; the full run (X1 =
10^6) 6/6, 52.0 s, peak 188.4 MB under a memory guard.

    python converse.py [--x1 X1]     (X1 default 10^6)
"""

import sys
import time
from array import array
from itertools import product

N6 = 10 ** 6
TABLE = (3, 5, 7, 11, 13, 17, 19, 29, 31, 37, 41)
SMALL = (2, 3, 5, 7)
CHECKS = []


def check(name, ok, detail=""):
    CHECKS.append((name, bool(ok)))
    print(f"  [{'PASS' if ok else 'FAIL'}] {name}"
          + (f" -- {detail}" if detail else ""))


def spf_to(n):
    s = array("i", [0]) * (n + 1)
    for i in range(2, int(n ** 0.5) + 1):
        if s[i] == 0:
            for j in range(i * i, n + 1, i):
                if s[j] == 0:
                    s[j] = i
    return s


def factor(m, spf):
    out = {}
    while m > 1:
        r = spf[m] or m
        while m % r == 0:
            m //= r
            out[r] = out.get(r, 0) + 1
    return out


def capacity(primes, spf):
    """{p: (new primes of n, breaks [(l, v_l(n), cap_l)])} over the odd
    primes p, cap read before p joins Q."""
    cap, out = {}, {}
    for p in primes:
        if p == 2:
            continue
        f = factor(p - 1, spf)
        new = [l for l in f if cap.get(l, 0) == 0]
        brk = [(l, v, cap[l]) for l, v in f.items()
               if 0 < cap.get(l, 0) < v]
        out[p] = (new, brk)
        for l, v in f.items():
            cap[l] = cap.get(l, 0) + v
    return out


def verify(p, A, B, primes_set):
    if len(set(A)) != len(A) or len(set(B)) != len(B) or set(A) & set(B):
        return False
    if any(q == 2 or q >= p or q not in primes_set for q in A + B):
        return False
    a = b = 1
    for q in A:
        a *= q - 1
    for q in B:
        b *= q - 1
    return a == (p - 1) * b


def exact(p, Q):
    """A witness (A, B) from the listing of U over Q, or None."""
    U = {1: ()}
    for q in Q:
        for v, S in list(U.items()):
            w = v * (q - 1)
            if w not in U:
                U[w] = S + (q,)
    n = p - 1
    for v, S in U.items():
        if v * n in U:
            A, B = set(U[v * n]), set(S)
            c = A & B
            return sorted(A - c), sorted(B - c)
    return None


def vec(f):
    return tuple(f.get(r, 0) for r in SMALL)


def build_table(spf):
    vs = [vec(factor(q - 1, spf)) for q in TABLE]
    best = {}
    for signs in product((0, 1, -1), repeat=len(TABLE)):
        v = [0, 0, 0, 0]
        for s, x in zip(signs, vs):
            if s:
                for i in range(4):
                    v[i] += s * x[i]
        v = tuple(v)
        size = sum(1 for s in signs if s)
        if v not in best or size < best[v][0]:
            best[v] = (size, signs)
    return best


class Search:
    def __init__(self, spf, primes, table):
        self.spf, self.table = spf, table
        self.fac = {q: factor(q - 1, spf) for q in primes if q > 2}
        self.by = {}                      # l -> [q with l | q - 1]
        for q, f in self.fac.items():
            if q in TABLE:
                continue
            for l in f:
                self.by.setdefault(l, []).append(q)
        self.key = {}
        for q, f in self.fac.items():
            for l in f:
                others = [r for r in f if r != l]
                self.key[(q, l)] = (max(others) if others else 1, q)

    def close(self, r):
        """Table primes closing r on {2, 3, 5, 7}, or None."""
        if any(l not in SMALL for l in r):
            return None
        hit = self.table.get(vec(r))
        if hit is None:
            return None
        A = [q for q, s in zip(TABLE, hit[1]) if s == 1]
        B = [q for q, s in zip(TABLE, hit[1]) if s == -1]
        return A, B

    def run(self, p, depth=12, width=4):
        r0 = factor(p - 1, self.spf)
        for d in range(1, depth + 1):
            got = self.dfs(p, dict(r0), [], [], set(), d, width)
            if got:
                return got
        return None

    def dfs(self, p, r, A, B, used, d, width):
        c = self.close(r)
        if c is not None:
            return A + c[0], B + c[1]
        if d == 0:
            return None
        big = [l for l in r if l not in SMALL]
        l = max(big) if big else max(r)
        side = 1 if r[l] > 0 else -1     # 1: into A, -1: into B
        cand = [q for q in self.by.get(l, ()) if q < p and q not in used]
        cand.sort(key=lambda q: self.key[(q, l)])
        for q in cand[:width]:
            r2 = dict(r)
            for x, v in self.fac[q].items():
                r2[x] = r2.get(x, 0) - side * v
                if r2[x] == 0:
                    del r2[x]
            used.add(q)
            got = self.dfs(p, r2, A + [q] * (side == 1), B + [q] * (side == -1),
                           used, d - 1, width)
            used.discard(q)
            if got:
                return got
        return None


def main():
    x1 = 10 ** 6
    if "--x1" in sys.argv:
        x1 = int(float(sys.argv[sys.argv.index("--x1") + 1]))
    t0 = time.time()
    spf = spf_to(N6)
    primes = [p for p in range(2, N6) if spf[p] == 0]
    pset = set(primes)
    print(f"sieve to {N6}: {len(primes)} primes ({time.time() - t0:.1f} s)")
    cap = capacity(primes, spf)
    table = build_table(spf)
    S = Search(spf, [q for q in primes if q < x1], table)
    print(f"table: {len(table)} vectors ({time.time() - t0:.1f} s)")

    print("CONTROLS")
    w = exact(17, [q for q in primes if 2 < q < 17])
    check("K1 17's witness found and verified",
          w is not None and verify(17, w[0], w[1], pset), f"{w}")
    low = [p for p in primes if 2 < p < 100]
    ex, bad = {}, []
    for p in low:
        ex[p] = exact(p, [q for q in primes if 2 < q < p])
        if ex[p] is not None and not verify(p, *ex[p], pset):
            bad.append(p)
    nocol = [p for p in low if ex[p] is None]
    held = [p for p in low if not cap[p][0] and not cap[p][1]]
    srch = [p for p in low if p >= 43 and ex[p] is not None]
    miss = [p for p in srch if not (S.run(p) and verify(p, *S.run(p), pset))]
    check("K2 below 100 the collision rungs are the capacity-held rungs",
          not bad and sorted(set(low) - set(nocol)) == held,
          f"no collision at {nocol}")
    check("K2 the non-collision rungs are 3, 5, 7, 11, 23, 29, 47, 53, 59, 83",
          nocol == [3, 5, 7, 11, 23, 29, 47, 53, 59, 83])
    check("K2 the search witnesses every collision rung 43 <= p < 100",
          not miss, f"{len(srch)} rungs, missed {miss}")
    check("K3 the verifier rejects A = {17} at 17, a repeat, an off-by-2",
          not verify(17, [17], [], pset)
          and not verify(17, [5, 5], [], pset)
          and not verify(17, [3, 5, 13], [5], pset)
          and verify(17, [3, 5, 13], [7], pset))
    two = [p for p, (new, brk) in cap.items() if any(l == 2 for l, _, _ in brk)]
    check("K4 the capacity breaks at l = 2 at 5 alone, p < 10^6 (C1)",
          two == [5], f"{two[:5]}")
    if not all(ok for _, ok in CHECKS):
        print("CONTROL FAILED: nothing below is read.")
        raise SystemExit(1)

    print("THE CAPACITY TO 10^6")
    newr = [p for p, (new, _) in cap.items() if new]
    brk = [(p, b) for p, (new, bs) in cap.items() for b in bs
           if b[0] != 2 and not new]
    print(f"  rungs with a new prime: {len(newr)} of {len(cap)} odd p; "
          f"first {newr[:12]}")
    print(f"  CAPACITY BREAKS at odd l, every prime old: {brk[:10]}"
          f" ({len(brk)})")
    allb = [(p, b) for p, (new, bs) in cap.items() for b in bs if b[0] != 2]
    print(f"  breaks at odd l with a new prime beside: {allb[:6]} "
          f"({len(allb)})")

    print(f"THE WITNESSES, 7 <= p < {x1}")
    t1 = time.time()
    unw, sizes, ex_used = [], {}, 0
    for p in primes:
        if p < 7 or p >= x1:
            continue
        new, bs = cap[p]
        if new or bs:
            continue
        if p < 100:
            w = ex[p]
            ex_used += 1
        else:
            w = S.run(p)
        if w is None or not verify(p, w[0], w[1], pset):
            unw.append(p)
        else:
            sizes[p] = len(w[0]) + len(w[1])
    big = max(sizes, key=lambda p: (sizes[p], p)) if sizes else None
    hist = {}
    for v in sizes.values():
        hist[v] = hist.get(v, 0) + 1
    print(f"  {len(sizes)} witnessed ({ex_used} by the listing), "
          f"{time.time() - t1:.1f} s; |A| + |B| counts {sorted(hist.items())}"
          f", largest at p = {big}")
    print(f"  UNWITNESSED: {unw[:20]} ({len(unw)})")
    print(f"    C4: {'HOLDS' if all(v <= 6 for v in sizes.values()) else 'FAILS'}")

    print("THE PRINT")
    if [b for b in brk if b[0] > 5]:
        print(f"  CONVERSE FALSE AT p = {brk[0][0]}")
    elif unw:
        print(f"  UNDECIDED AT {unw[:10]}")
    else:
        print(f"  THE CONVERSE HOLDS AT EVERY RUNG p_k < {x1}")
    print(f"{sum(ok for _, ok in CHECKS)}/{len(CHECKS)} checks passed, "
          f"{time.time() - t0:.1f} s")
    raise SystemExit(0 if all(ok for _, ok in CHECKS) else 1)


if __name__ == "__main__":
    main()