schaefer.py
Checks: LATENCY: Trees of units property; rule; theorem; observation.
Cited on: LATENCY.
To run it: save schaefer.py, with chains.py, dags.py, drops.py, graphs.py, manystream.py, median.py, minclose.py, pairwise.py, poles.py, raises.py, redundant.py, sublattice.py beside it; needs numpy.
"""
schaefer.py -- is a (z, h) triple ever a hard relation?
QUESTION. Below naive, is pricing a graph of units at one cell
NP-hard? A union of graphs sharing units has, at every T, F (and RF,
the raise-free part) exactly the points whose projection on each graph
lies in that graph's own (D1 below), so an instance is a CSP: gadget
graphs, their private units projected out, the shared units the
variables. A variable that can sit in any slot of any gadget has one
cone everywhere: the VARIABLE KIND is (z, h), h one shared head (a
head never drops: its floor is the least delay alive on fresh inputs).
On drop indicators (below floor or not) the CSP over a set Γ of
relations is NP-complete iff Γ lies inside none of Schaefer's six
tractable classes (0-valid, 1-valid, Horn, dual-Horn, bijunctive,
affine; Schaefer 1978, known, its text not opened), read on the whole
set. Γ is what one (cell, T) realizes over every graph there, on (z, h)
singles, pairs and triples.
BY HAND (frozen before code). D1: a shared unit is its inputs, so it
brings its cone; every path ends at a unit of some graph G_i and every
unit on it is an ancestor of that unit, so the path lies in G_i; each
unit's arrival and leftover read only its cone. So gluing by shared
units is the conjunction of the copies, at every T. A NAND of three is
Horn and 0-valid, an OR of three dual-Horn and 1-valid: each alone is
tractable, together monotone 3-SAT. All relations 0-valid means all
variables at floor extends everywhere. TRANSPLANTs: (a) RF only;
pricing reads F, where a raise can rescue a drop, so F's sets are read
beside; (b) a drop by two is folded into "dropped"; (c) the
(4, 2, 2) NAND's third literal is its sink, not a (z, h) unit.
PREDICTIONS. Every (z, h) relation in RF at 7 units is 0-valid
(moderately: private units absorb), so no Γ escapes and the kill
fires; F's sets escape neither (weakly).
KILL (frozen). "KILL (cell, T) whose (z, h) relations escape
Schaefer's six classes: N ((cell, T) read M, RF sets with three (z, h)
units K)" over X6 at the 441 cells and X7 at the four cells; fires if
N == 0 (the census offers no hard gadget set at this scale),
UNREADABLE if K < 10. Beside: the same over F; per (cell, T)
the classes Γ sits in; the triple relations not 0-valid.
CONTROLS. C0 by D1: two copies of the first (4, 2, 2) NAND graph
glued on h and 1, F and RF at T = 6, 7, 8 equal to the join of the
copies' own sets; a plant, copy B mapped onto the wrong units (its
2' onto 2), reads a join off the composite's. (Frozen with glue.join
as the reader and a deleted point as the plant; the deletion is caught
by construction once the join matches, and glue.join joins a set's own
projections, not the copies' own sets: replaced by glued, its plant
on the map, on review.) C1 the
classifier: NAND3 {0-valid, Horn}; OR3 {1-valid, dual-Horn}; 1-in-3
none; x + y + z + w = 1 mod 2 {affine}; the set {NAND3, OR3} none; a
planted 1-in-3 with (0, 0, 0) added read back as 0-valid. (Frozen with
x + y + z = 1 as the affine-only case; by hand before code it holds
(1, 1, 1), so it is 1-valid too: replaced by the four-variable parity.)
C2 the RF of pairwise.py's first fire projected on (1, 2, 6) reads
NAND3 exactly. (Frozen so; the first run read {001, 011, 101, 110},
and by hand the frozen answer forgot the paths: at T = 7 the paths
h, 1, 3, 4, 5, 6 and h, 2, 3, 4, 5, 6 floor at 8, and with h, 3, 4, 5
never dropping they force d1 or d6 and d2 or d6, beside not all three.
So the relation is d6 = NAND(d1, d2), a gate; C2 now asks that, and
that h, 3, 4, 5 sit at their floors in every point.)
FINDINGS (copied from the runs below). C0 F and RF equal the join at
T = 6, 7, 8 (T = 7: F 20, RF 8; T = 8: F 568, RF 25; T = 6 empty), the
plant read; C1 NAND3 [0-valid, Horn], OR3 [1-valid, dual-Horn], 1-in-3
none, parity 4 [affine], the pair none, the plant [0-valid, Horn,
bijunctive]; C2 {001, 011, 101, 110}, h, 3, 4, 5 at floor, classes
none: the gate d6 = NAND(d1, d2) escapes all six alone.
RF: (cell, T) read 807; Γ in {0-valid, Horn, dual-Horn, bijunctive,
affine} 721, {1-valid, Horn, dual-Horn, bijunctive, affine} 85,
{1-valid, Horn, dual-Horn, bijunctive} 1; triple relations not
0-valid 108 (summed over (cell, T), not distinct). F: read 808; 718,
84, all six 4, {1-valid, dual-Horn, bijunctive} 1, {1-valid, Horn,
dual-Horn, bijunctive} 1; not 0-valid 108.
KILL (cell, T) whose (z, h) relations escape Schaefer's six classes: 0
((cell, T) read 807, RF sets with three (z, h) units 1424). FIRES, as
predicted; F escapes neither, as weakly predicted. Every RF Γ lies in
Horn, dual-Horn and bijunctive at once, and in 0-valid or 1-valid;
every F Γ in 0-valid or 1-valid (one, {1-valid, dual-Horn,
bijunctive}, not Horn): so over X6 at the 441 cells and X7 at the
four, relations of arity at most 3, each gadget read below its own
naive, a (z, h) variable layer carries no hard gadget set. Unread:
groups of four or more (z, h) units on one head as one relation, and
gadgets read at or above their own naive. The prediction's reason
was half wrong: 86 of 807 are not 0-valid (the (z, h) units forced
down), yet then all-dropped extends on indicators (a drop by one in
one graph and by two in another unread). What C2 found instead is a
gate on non-interchangeable units, the sink's drop a NAND of the two (z, h)
units' drops: hardness, if it is there, is a question of whether such
gates compose.
RUN RECORD. Controls 5/5, 0.1 s, peak commit 19.4 MB. Rehearsed at
one cell per population, shard 0, 13.4 s. Four shards, 199.3 to
207.9 s, peak commit 31.0 MB. Rerun from code/: controls 5/5,
0.2 s, 18.4 MB.
"""
import itertools
import json
import os
import sys
import time
from collections import Counter
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import dags # noqa: E402
import graphs # noqa: E402
import median # noqa: E402
import sublattice as sl # noqa: E402
from drops import cut # noqa: E402
from minclose import cell_named # noqa: E402
from pairwise import jobs, seven # noqa: E402
SIX = ("0-valid", "1-valid", "Horn", "dual-Horn", "bijunctive", "affine")
NAND1 = ((None, None), (None, 0), (None, 0), (1, 2), (1, 3), (1, 4),
(2, 5))
CHECKS = []
def check(name, ok, detail=""):
CHECKS.append(bool(ok))
tail = f" ({detail})" if detail else ""
print(f" {'PASS' if ok else 'FAIL'} {name}{tail}")
def classes_of(R):
"""Schaefer's classes a non-empty Boolean relation lies in."""
R = set(R)
k = len(next(iter(R)))
op2 = lambda f: all(tuple(f(x, y) for x, y in zip(a, b)) in R
for a in R for b in R)
op3 = lambda f: all(tuple(f(x, y, z) for x, y, z in zip(a, b, c)) in R
for a in R for b in R for c in R)
out = set()
if (0,) * k in R:
out.add("0-valid")
if (1,) * k in R:
out.add("1-valid")
if op2(lambda x, y: x & y):
out.add("Horn")
if op2(lambda x, y: x | y):
out.add("dual-Horn")
if op3(lambda x, y, z: (x & y) | (y & z) | (x & z)):
out.add("bijunctive")
if op3(lambda x, y, z: x ^ y ^ z):
out.add("affine")
return out
def set_classes(G):
out = set(SIX)
for R in G:
out &= classes_of(R)
return out
def zh(d):
"""{h: the (z, h) units reading head h}."""
heads = [j for j, u in enumerate(d.units) if tuple(u) == (None, None)]
return {h: [j for j, u in enumerate(d.units)
if tuple(u) in ((None, h), (h, None))] for h in heads}
def relations(P, floors, groups):
"""Every drop relation of P on (z, h) singles, pairs and triples."""
out = set()
for V in groups.values():
for r in (1, 2, 3):
for U in itertools.combinations(V, r):
out.add(frozenset(tuple(int(p[u] < floors[u]) for u in U)
for p in P))
return out
def sets_at(d, T, rows=None):
nv = d.naive()
rows = rows if rows is not None else sl.alive_all(d, max(T, nv))
F = median.sets(d, nv, rows, T)["FD"]
return F, cut(F, d.Ls)[0] if F else set()
def glued(PA, A, B, n):
"""The join of two copies' sets, each copy's own set PA, copy A on
composite units A and copy B on B (same unit order)."""
out = set()
for a in PA:
for b in PA:
p = [None] * n
ok = True
for U, v in ((A, a), (B, b)):
for u, x in zip(U, v):
ok &= p[u] is None or p[u] == x
p[u] = x
if ok:
out.add(tuple(p))
return out
def controls():
print("C0..C2 the instrument")
cell = cell_named("(4, 2, 2)")
comp = list(NAND1) + [(None, 0), (1, 7), (1, 8), (1, 9), (7, 10)]
A = list(range(7))
B = [0, 1, 7, 8, 9, 10, 11]
dA, dG = dags.Dag(cell, NAND1), dags.Dag(cell, comp)
ok, detail = True, []
for T in (6, 7, 8):
FA, RA = sets_at(dA, T)
FG, RG = sets_at(dG, T)
for arm, PA, PG in (("F", FA, FG), ("RF", RA, RG)):
J = glued(PA, A, B, dG.n)
ok &= J == PG
detail.append(f"T={T} {arm} {len(PG)}/{len(J)}")
FA, _ = sets_at(dA, 7)
FG, _ = sets_at(dG, 7)
wrong = [0, 1, 2, 8, 9, 10, 11]
ok_plant = glued(FA, A, wrong, dG.n) != FG
check("C0 glued on h and 1: F, RF = the copies' join, T = 6, 7, 8",
ok, ", ".join(detail))
check("C0 plant: copy B mapped onto 2 reads a join off the composite's",
ok_plant)
NAND3 = {p for p in itertools.product((0, 1), repeat=3) if sum(p) < 3}
OR3 = {p for p in itertools.product((0, 1), repeat=3) if sum(p) > 0}
ONE3 = {(1, 0, 0), (0, 1, 0), (0, 0, 1)}
PAR4 = {p for p in itertools.product((0, 1), repeat=4) if sum(p) % 2}
hand = ((NAND3, {"0-valid", "Horn"}), (OR3, {"1-valid", "dual-Horn"}),
(ONE3, set()), (PAR4, {"affine"}))
good = all(classes_of(R) == want for R, want in hand)
good &= set_classes([NAND3, OR3]) == set()
planted = classes_of(ONE3 | {(0, 0, 0)})
check("C1 the classifier on NAND3, OR3, 1-in-3, parity 4, the pair",
good, " ".join(f"{sorted(classes_of(R))}" for R, _ in hand))
check("C1 plant: 1-in-3 with (0, 0, 0) reads 0-valid",
"0-valid" in planted, f"{sorted(planted)}")
_, RF = sets_at(dA, 7)
R = {tuple(int(p[u] < dA.Ls[u]) for u in (1, 2, 6)) for p in RF}
still = all(p[u] == dA.Ls[u] for p in RF for u in (0, 3, 4, 5))
gate = {(a, b, 1 - (a & b)) for a in (0, 1) for b in (0, 1)}
check("C2 the first fire's RF on (1, 2, 6): h, 3, 4, 5 never drop, "
"so d6 = NAND(d1, d2)", still and R == gate,
f"{sorted(R)}, h 3 4 5 at floor {still}, "
f"classes {sorted(classes_of(R))}")
def sweep_one(cell, units, gam, out):
try:
d = dags.Dag(cell, units)
except AssertionError:
out["skipped"] += 1
return
groups = {h: V for h, V in zh(d).items() if V}
if not groups:
return
three = any(len(V) >= 3 for V in groups.values())
nv = d.naive()
rows = sl.alive_all(d, nv)
least = min(r[0] for r in rows)
name = graphs.name(cell)
for T in range(least, nv):
F = median.sets(d, nv, rows, T)["FD"]
if not F:
continue
RF = cut(F, d.Ls)[0]
key = f"{name}|{T}"
g = gam.setdefault(key, {"RF": set(), "F": set()})
g["F"] |= relations(F, d.Ls, groups)
if RF:
g["RF"] |= relations(RF, d.Ls, groups)
out["K"] += three
def run(shard, ncells):
gam, out = {}, Counter()
t0 = time.time()
n = 0
for tag, cell, cls in jobs(shard, ncells, seven()):
if tag not in ("X6", "X7"):
continue
n += 1
for units in cls:
sweep_one(cell, units, gam, out)
return gam, out, time.time() - t0, n
def dump(path, gam, out, secs, n):
enc = {k: {a: [sorted(R) for R in v[a]] for a in v}
for k, v in gam.items()}
with open(path, "w", encoding="utf-8", newline="\n") as fh:
json.dump({"gam": enc, "out": dict(out), "secs": secs,
"cells": n}, fh)
def merge(paths):
gam, out = {}, Counter()
for p in paths:
with open(p, encoding="utf-8") as fh:
rec = json.load(fh)
for k, v in rec["gam"].items():
g = gam.setdefault(k, {"RF": set(), "F": set()})
for a in v:
g[a] |= {frozenset(tuple(t) for t in R) for R in v[a]}
out.update(rec["out"])
print(f" {os.path.basename(p)}: {rec['cells']} (pop, cell) jobs, "
f"{rec['secs']:.1f} s")
return gam, out
def report(gam, out):
for arm in ("RF", "F"):
keys = [k for k in gam if gam[k][arm]]
by = Counter()
esc, not0 = [], 0
for k in sorted(keys):
G = gam[k][arm]
sc = set_classes(G)
by[tuple(c for c in SIX if c in sc)] += 1
not0 += sum(1 for R in G if len(next(iter(R))) == 3
and (0, 0, 0) not in R)
if not sc:
esc.append(k)
print(f"\n {arm}: (cell, T) read {len(keys)}; by classes G sits "
f"in {dict(by)}; triple relations not 0-valid {not0}")
for k in esc[:5]:
print(f" escapes: {k}, {len(gam[k][arm])} relations")
tail = " UNREADABLE" if out["K"] < 10 else ""
label = "" if arm == "RF" else " (F, beside)"
print(f"KILL (cell, T) whose (z, h) relations escape Schaefer's six "
f"classes{label}: {len(esc)} ((cell, T) read {len(keys)}, RF "
f"sets with three (z, h) units {out['K']}){tail}")
print(f" skipped {out['skipped']}")
def main(argv):
t0 = time.time()
if argv and argv[0] == "--merge":
report(*merge(argv[1:]))
return 0
if argv and argv[0] == "--shard":
shard, ncells, path = int(argv[1]), int(argv[2]), argv[3]
gam, out, secs, n = run(None if shard < 0 else shard, ncells)
dump(path, gam, out, secs, n)
print(f"shard {shard}: {secs:.1f} s")
return 0
controls()
if not all(CHECKS):
print("\nCONTROL FAILED: nothing below is read.")
return 1
print(f"\n{sum(CHECKS)}/{len(CHECKS)} checks, {time.time() - t0:.1f} s")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))