/machines/1RB0RA_1RC1RF_1LD0LC_1RA1LE_1RA0LC_---0RD/check_proof.py · raw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 | #!/usr/bin/env python3 """Check every rule and claim in proof.md for 1RB0RA_1RC1RF_1LD0LC_1RA1LE_1RA0LC_---0RD (stdlib only). usage: python3 -B check_proof.py [--long] [--seed S] [--quick] default: about 6 minutes of pure Python (--quick: about 2.5 minutes). --long : also recompile and rerun the C trackers (scripts/proof/rle2.c to 1.3e10 steps, about 60 s; scripts/funded/cfgdump.c to 3e8 steps) and compare with the stored files. --quick: fewer random instances (smoke test). Checks (names as in proof.md): HALT the only undefined transition is F0, and F is entered only by B1. FULLPY independent Python run from blank to 1.6e7 steps: visit runs at b18 and b618 equal the stored C file. IFACE stored run to 1.3e10 (data/proof/rle_b18_b618_1.3e10.txt): b18 visits are only AC/DC, b618 only AE/BE; b18 runs alternate A/D (one D-run per epoch); the epoch table N_m, c_m, j_m. INIT the rest point S0 (step 300,032,133): M clean, T canonical T(P,PP;e=5521). ZM zone M (cells 18..617): per-visit rules MD/MA (and the top carry -> B visits), exact on random words. GMAP the value map g: every v ends at 0 or 5; visit bound. TA zone T: A visit = throttle increment (exact), including K = -1. TB zone T: the first B visits after an A-run follow the throttle frontier rule (exact, throttle part). TBURST5 [flagged F2] all 40 canonical throttles with K <= 2: B^5 then A^2600 -> canonical, K' in {K, K-1} (INFO line: K = 3, where 3 of 81 throttles end with a dead slot). TBURSTJ [flagged F3] random canonical throttles, B^j (j up to 3000) then A^2600: no halt, all answers E (INFO line: how many end canonical; the rest end with a dead slot or big-region leftovers). TNEG zone T is NOT safe for arbitrary visit words: T(R,R) on B A^7 B A^8 B answers C. (Expected PASS = the counterexample reproduces; it is why the burst shape in IFACE matters.) """ import os, sys, random, json, subprocess, math, itertools, time HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, 'scripts', 'proof')) from zones import TR, Halt, visit, load_cfg, win, set_cells, to_b18_rest ARGS = sys.argv[1:] LONG = '--long' in ARGS; QUICK = '--quick' in ARGS SEED = int(ARGS[ARGS.index('--seed') + 1]) if '--seed' in ARGS else 1 rng = random.Random(SEED) fails = [] def check(name, ok, info=''): print(('PASS ' if ok else 'FAIL ') + name + ((' ' + info) if info else '')); sys.stdout.flush() if not ok: fails.append(name) T0 = time.time() # ---------------- HALT ---------------- undef = [k for k, v in TR.items() if v is None] into_F = [k for k, v in TR.items() if v is not None and v[2] == 'F'] check('HALT', undef == [('F', 0)] and into_F == [('B', 1)] and TR[('B', 1)] == (1, 1, 'F') and TR[('F', 1)] == (0, 1, 'D'), f'undefined {undef}, into F {into_F}: halts iff B reads 1 with a 0 right of it ("B> 1 0")') # ---------------- FULLPY + IFACE ---------------- RLE = os.path.join(HERE, 'data', 'proof', 'rle_b18_b618_1.3e10.txt') def parse_rle(lines): runs = {18: [], 618: []} for l in lines: p = l.split() if p and p[0] in ('b18', 'b618'): runs[int(p[0][1:])].append((p[1], int(p[2]), int(p[3]))) return runs stored = parse_rle(open(RLE).read().split('\n')) def py_rle(nsteps): W = {}; D = {}; N = {} S = 'ABCDEF' for (q, s), v in TR.items(): if v is not None: W[(S.index(q), s)] = v[0]; D[(S.index(q), s)] = v[1]; N[(S.index(q), s)] = S.index(v[2]) size = 1 << 16; off = 1 << 12; t = bytearray(size); pos = 0; q = 0 wt = [[W.get((a, b), 0) for b in (0, 1)] for a in range(6)]; dt = [[D.get((a, b), 0) for b in (0, 1)] for a in range(6)] nt = [[N.get((a, b), -1) for b in (0, 1)] for a in range(6)] runs = {18: [], 618: []}; inv = {18: -1, 618: -1}; last = {18: None, 618: None} step = 0 while step < nsteps: i = pos + off; s = t[i] if q == 5 and s == 0: return None, step t[i] = wt[q][s]; old = pos; pos += dt[q][s]; q = nt[q][s]; step += 1 if old == 17 or old == 18 or old == 617 or old == 618: for b in (18, 618): if old == b - 1 and pos == b: inv[b] = q elif old == b and pos == b - 1: ty = S[inv[b]] + S[q]; r = runs[b] if r and r[-1][0] == ty: r[-1][1] += 1 else: r.append([ty, 1, step]) return runs, step NPY = 6_000_000 if QUICK else 16_000_000 pr, _ = py_rle(NPY) ok = pr is not None for b in (18, 618): got = [tuple(x) for x in pr[b][:-1]] # last run may be cut by the horizon ref = [x for x in stored[b] if x[2] < NPY][:len(got)] ok = ok and got == ref and len(got) > 0 check('FULLPY', ok, f'Python run to {NPY:,} steps: {len(pr[18]) - 1} closed b18 runs and {len(pr[618]) - 1} b618 runs equal the stored C runs') if LONG: src = os.path.join(HERE, 'scripts', 'proof', 'rle2.c'); exe = os.path.join('/tmp', 'r8_rle2_check') subprocess.run(['gcc', '-O2', '-o', exe, src], check=True) out = subprocess.run(['nice', '-n', '10', exe, '1.3e10'], capture_output=True, text=True).stdout check('FULLC', out.strip() == open(RLE).read().strip(), 'C rerun to 1.3e10 equals the stored run file') ty18 = sorted({x[0] for x in stored[18]}); ty618 = sorted({x[0] for x in stored[618]}) alt = all(stored[18][i][0] != stored[18][i + 1][0] for i in range(len(stored[18]) - 1)) check('IFACE-types', ty18 == ['AC', 'DC'] and ty618 == ['AE', 'BE'], f'b18 {ty18} ({sum(x[1] for x in stored[18]):,} visits), b618 {ty618} ({sum(x[1] for x in stored[618]):,} visits), 0 halts to 1.3e10') check('IFACE-alternation', alt and stored[18][0][0] == 'AC', 'b18 visit word = A^N1 D^c1 A^N2 D^c2 ... (one D-run per epoch)') # epoch table: D-run k (from 0) is the pause of epoch m = k + 3: D-run 117 at 7,565,813 is m = 10 D = [x for x in stored[18] if x[0] == 'DC']; A = [x for x in stored[18] if x[0] == 'AC'] tab = [] for k, d in enumerate(D): m = k + 3 nA = next((a[1] for a in A if a[2] > d[2]), None) nxt = D[k + 1][2] if k + 1 < len(D) else 10 ** 13 j = sum(b[1] for b in stored[618] if b[0] == 'BE' and d[2] < b[2] < nxt) tab.append((m, d[2], d[1], nA, j)) print(' epoch table: m, first D step, c_m, N_m (A-run after), B visits at b618 in that ending') for r in tab: print(' ', r) gl = [r for r in tab if r[4]] check('IFACE-epochs', [r[0] for r in gl] == [5, 14] and all(r[4] == 5 for r in gl) and all((r[3] or 10 ** 9) >= 3000 for r in tab if r[0] >= 9), 'B-bursts at b618: exactly 5 visits, at m = 5 (start-up) and m = 14 only; every A-run for m >= 9 has N_m >= 3,000') # ---------------- INIT ---------------- INB = {'P': '1001', 'Q': '1011', 'R': '1000'}; D4 = {'1001': 'P', '1011': 'Q', '1000': 'R', '0011': 's'} MSB = {'.': '0110', '1': '1110', '2': '0010', '0': '0000'}; MDEC = {v: k for k, v in MSB.items()} def parseT(T): """canonical T(s3, digits; e): cell 618 = 1, groups from 619 in P/Q/R, last one the marker Q, then (0011)^e, then 0s. Returns (throttle string incl. s3, without marker; e) or None.""" if T.get(618, 0) != 1: return None hi = max(T); g = []; p = 619 while True: b = win(T, p, p + 3) if b == '0011' or b == '0000': break if b not in ('1001', '1011', '1000'): return None g.append(D4[b]); p += 4 if len(g) > 40: return None if not g or g[-1] != 'Q': return None e = 0 while win(T, p, p + 3) == '0011': e += 1; p += 4 if any(T.get(c, 0) for c in range(p, hi + 1)): return None return ''.join(g[:-1]), e def buildT(thr, e): T = {}; set_cells(T, 618, '1' + ''.join(INB[c] for c in thr) + '1011' + '0011' * e); return T def mword(M): return [MDEC.get(win(M, 2 + 4 * k, 5 + 4 * k), '?') for k in range(4, 154)] cfg = os.path.join(HERE, 'data', 'funded', 'cfg_3e8.txt') tape, pos, st, step = load_cfg(cfg) if LONG: exe = '/tmp/r8_cfgdump_check' subprocess.run(['gcc', '-O2', '-o', exe, os.path.join(HERE, 'scripts', 'funded', 'cfgdump.c')], check=True) out = subprocess.run([exe, '3e8'], capture_output=True, text=True).stdout check('INIT-cfg', out.strip() == open(cfg).read().strip(), 'cfgdump rerun from blank to 3e8 equals data/funded/cfg_3e8.txt') pos, st, n = to_b18_rest(tape, pos, st) S0 = step + n Mz = {c: 1 for c in tape if 18 <= c <= 617}; Tz = {c: 1 for c in tape if c >= 618}; Lz = {c: 1 for c in tape if c <= 17} pt = parseT(Tz) check('INIT', S0 == 300_032_133 and set(mword(Mz)) == {'.'} and win(Mz, 618 - 4, 617) == '0110' and pt == ('PPP', 5521), f'rest point S0 at step {S0:,} (head 17, state {st}): M = 150 blocks ".", T = {pt}') LEFT0 = win(Lz, -100, 17) # ---------------- ZM ---------------- VAL = {'.': 0, '2': 1, '1': 2}; DIG = {0: '.', 1: '2', 2: '1'} def m_add(w, i, c): w = list(w); out = 0 while c: if i >= len(w): return w, c # carry c out of the top block -> c B visits at b618 v = VAL[w[i]] + c; w[i] = DIG[v % 3]; c = v // 3; i += 1 return w, 0 def m_pred(w, x): """predicted (new word, emitted b618 visits) for one visit x at b18; w = blocks 4..153.""" if x == 'D': w2, c = m_add(w, 1, 2); return w2, 'B' * c f = next((i for i, c in enumerate(w) if c != '.'), None) if f is None: return list(w), 'A' w = list(w); c0 = w[f] if c0 == '0': w[f] = '.'; return w, '' if c0 == '2': w[f] = '0'; return w, '' w[f] = '.'; w2, c = m_add(w, f + 1, 4); return w2, 'B' * c def zm_instance(w, prog): M = dict(Mz) for k, c in enumerate(w): set_cells(M, 2 + 4 * (4 + k), MSB[c]) pred = mword(M); nv = okv = 0; bad = None; emitted = '' for x in prog: try: o, rv, _ = visit(M, 18, x, hi=617, answer='E', cap=10 ** 6) except Halt: return nv, okv, ('halt', nv), emitted pw, pe = m_pred(pred, x); pred = pw; nv += 1; emitted += ''.join(rv) if o == 'C' and ''.join(rv) == pe and mword(M) == pred: okv += 1 elif bad is None: bad = (nv, x, o, ''.join(rv), pe) if x == 'A' and emitted.endswith('AA'): break # ending done: the word is clean and visits pass return nv, okv, bad, emitted NZ = 40 if QUICK else 160 tot = okt = 0; bads = []; gl = cl = mo = 0 for it in range(NZ): kind = it % 4 if kind < 2: # pause end: clean word, D-run, A-run through the ending w = ['.'] * 150; c = rng.randrange(1, 400 if kind == 0 else 60) prog = 'D' * c + 'A' * 330 elif kind == 2: # random short word (incl. a junk block at the frontier), then A-run L = rng.randrange(1, 25); w = [rng.choice('.12') for _ in range(L)] + ['.'] * (150 - L) f = next((i for i, c in enumerate(w) if c != '.'), None) if f is not None and rng.random() < 0.3: w[f] = '0' prog = 'A' * 340 else: # MO: nearly full word, D-run with carries out of the top, then A-run w = ['.'] + [rng.choice('11112') for _ in range(149)] prog = 'D' * rng.randrange(1, 30) + 'A' * 330 nv, okv, bad, em = zm_instance(w, prog) tot += nv; okt += okv if bad and len(bads) < 5: bads.append((it, bad)) gl += em.count('B') >= 5 and kind < 3; mo += kind == 3 and 'B' in em check('ZM', okt == tot and not bads, f'{NZ} instances, {okt:,}/{tot:,} visits exact (word and b618 output), all answers C; ' f'{gl} instances with a glider exit (5 B), {mo} MO instances with top carries' + (f' bad {bads}' if bads else '')) # ---------------- GMAP ---------------- def g_out(v): n = 0 while True: if v == 0: return 0, n while v % 3 == 0: v //= 3 if v == 5: return 5, n if v % 3 == 1: v = (v - 1) // 3; n += 2 else: v = (v + 10) // 3; n += 1 res = [g_out(v) for v in range(1, 3 ** 10)] ends = {r[0] for r in res}; ng = sum(r[0] == 5 for r in res) def nd(v): k = 0 while v: v //= 3; k += 1 return k bound = all(n <= 2 * nd(v) + 3 for v, (_, n) in zip(range(1, 3 ** 10), res)) check('GMAP', ends == {0, 5} and ng == 3 ** 8 and bound, f'v in 1..3^10-1: ends in {sorted(ends)}, {ng} = 3^8 gliders, visits <= 2*(ternary digits of v) + 3') # ---------------- TA ---------------- def t_inc(thr, e): if thr == '': return thr, e + 2 s3 = thr[0]; c = 0 if s3 == 'P' else 1 s3n = {'P': 'Q', 'Q': 'R', 'R': 'P'}[s3]; d = list(thr[1:]); V = {'P': 0, 'R': 1, 'Q': 2}; IV = 'PRQ' for j in range(len(d)): if not c: break v = V[d[j]] + c; d[j] = IV[v % 3]; c = v // 3 return s3n + ''.join(d), e + c NT = 30 if QUICK else 120 tot = okt = 0; bad = None for it in range(NT): K = rng.randrange(-1, 5); thr = '' if K < 0 else ''.join(rng.choice('PQR') for _ in range(K + 1)); e = rng.randrange(6, 30) T = buildT(thr, e); pthr, pe = thr, e for v in range(40): o, rv, _ = visit(T, 618, 'A', cap=10 ** 6); pthr, pe = t_inc(pthr, pe); tot += 1 if o == 'E' and parseT(T) == (pthr, pe): okt += 1 elif bad is None: bad = (thr, e, v, o, parseT(T), (pthr, pe)) check('TA', okt == tot, f'{NT} canonical throttles (K = -1..4), {okt:,}/{tot:,} A visits: answer E and T = predicted increment' + (f' bad {bad}' if bad else '')) # ---------------- TB ---------------- JUNK = {'P': '0001', 'R': '0000', 'Q': '0011'} def t_bvisits(thr, n): """throttle part (s3, digits, marker) after each of n B visits (frontier rule; see proof.md TB).""" t = list(thr) + ['Q']; pend = None; saved = None; out = [] V = {'P': 0, 'R': 1, 'Q': 2}; IV = 'PRQ' for _ in range(n): if pend is not None: t[pend] = 'P'; t[pend + 1] = saved; pend = None else: f = next((i for i in range(len(t) - 1) if t[i] != 'P'), None) if f is not None: if t[f] == 'Q': t[f] = 'P'; c = 4 for j in range(f + 1, len(t) - 1): v = V[t[j]] + c; t[j] = IV[v % 3]; c = v // 3 else: saved = t[f + 1]; t[f + 1] = JUNK[t[f + 1]]; pend = f out.append(list(t)) return out, pend def tgroups(T, n): return [D4.get(win(T, 619 + 4 * j, 622 + 4 * j), win(T, 619 + 4 * j, 622 + 4 * j)) for j in range(n)] tot = okt = 0; bad = None NB = 60 if QUICK else 240 for it in range(NB): K = rng.randrange(0, 5); thr = ''.join(rng.choice('PQR') for _ in range(K + 1)); e = 20 T = buildT(thr, e); pred, _ = t_bvisits(thr, 5) for v in range(5): o, rv, _ = visit(T, 618, 'B', cap=10 ** 6); tot += 1 got = [{'s': '0011'}.get(x, x) for x in tgroups(T, K + 2)] exp = [x if x in 'PQR' else x for x in pred[v]] exp = ['0011' if x == '0011' else x for x in exp] if o == 'E' and got == exp: okt += 1 elif bad is None: bad = (thr, v, o, got, exp) check('TB', okt == tot, f'{NB} canonical throttles (K = 0..4), {okt:,}/{tot:,} B visits: answer E, throttle part as predicted' + (f' bad {bad}' if bad else '')) # ---------------- TBURST5 / TBURSTJ (flagged lemmas F2 / F3) ---------------- NA = 2600 def burst(thr, e, j, na=NA): T = buildT(thr, e); outs = set() try: for _ in range(j): outs.add(visit(T, 618, 'B', cap=10 ** 7)[0]) for _ in range(na): outs.add(visit(T, 618, 'A', cap=10 ** 7)[0]) except Halt: return 'halt', None return outs, parseT(T) states = [''] + [s + ''.join(d) for K in range(0, 3) for s in 'PQR' for d in itertools.product('PQR', repeat=K)] # K <= 2 is the reachable range: T(PPP) has K = 2 at S0 and no rule raises K. K = 3 is reported separately below. if QUICK: states = states[:20] okc = 0; drops = 0; stays = 0; pred_ok = 0; bad = [] for thr in states: outs, pt = burst(thr, 25, 5) K = len(thr) - 1 if outs == {'E'} and pt is not None and len(pt[0]) - 1 in (K, K - 1) and (K >= 0 or len(pt[0]) - 1 == -1) and pt[1] >= 25: okc += 1; d = len(pt[0]) - 1 == K - 1; drops += d; stays += not d if thr: _, pend = t_bvisits(thr, 5); pred_ok += (pend is not None) == d else: pred_ok += 1 elif len(bad) < 4: bad.append((thr, outs, pt)) check('TBURST5', okc == len(states) and pred_ok == len(states), f'[flagged F2] {okc}/{len(states)} throttles (K = -1..2, e = 25): B^5 A^{NA} -> canonical, e\' >= e, K\' = K in {stays}, K-1 in {drops}; ' f'K-drop = "R step half done after the 5th B" in {pred_ok}/{len(states)}' + (f' bad {bad}' if bad else '')) k3 = [s + ''.join(d) for s in 'PQR' for d in itertools.product('PQR', repeat=3)] if QUICK: k3 = k3[:9] k3c = [thr for thr in k3 if (lambda o, p: o == {'E'} and p is not None and len(p[0]) - 1 in (2, 3))(*burst(thr, 25, 5))] print(f'INFO TBURST5-K3 {len(k3c)}/{len(k3)} K = 3 throttles canonical after B^5 A^{NA}; the others end with a dead slot ' f'(0011/0000 between the digits and the marker): {sorted(set(k3) - set(k3c))}. K = 3 is not reachable from S0.') NJ = 10 if QUICK else 40 okc = 0; bad = []; js = []; kd = 0; safe = 0 for it in range(NJ): K = rng.randrange(-1, 3); thr = '' if K < 0 else ''.join(rng.choice('PQR') for _ in range(K + 1)) j = int(math.exp(rng.uniform(0, math.log(3000)))); js.append(j); e = rng.randrange(30, 60) outs, pt = burst(thr, e, j) safe += outs == {'E'} if outs == {'E'} and pt is not None and len(pt[0]) - 1 in ({K, K - 1} if K >= 0 else {-1}) and pt[1] >= e: okc += 1; kd += len(pt[0]) - 1 < K elif len(bad) < 6: bad.append((thr, e, j)) check('TBURSTJ', safe == NJ, f'[flagged F3] {safe}/{NJ} random throttles (K = -1..2, e = 30..59), B^j with j = {min(js)}..{max(js)} ' f'then A^{NA}: no halt, every visit answered E') print(f'INFO TBURSTJ-canonical only {okc}/{NJ} end canonical (K\' in {{K, K-1}}, {kd} drops); the others end with a dead slot ' f'or leftover big-region blocks, so F3 needs a wider invariant family: {bad}') # ---------------- TNEG ---------------- T = buildT('RR', 30); outs = [] for x in 'B' + 'A' * 7 + 'B' + 'A' * 8 + 'B': outs.append(visit(T, 618, x, cap=10 ** 6)[0]) check('TNEG', outs[-1] == 'C' and set(outs[:-1]) == {'E'}, 'T(R,R; e=30) on B A^7 B A^8 B answers C on the last visit: T is safe only for the burst shape the run produces') print(f'\nSUMMARY: {"ALL PASS" if not fails else f"{len(fails)} FAIL: {fails}"} ({time.time() - T0:.0f} s, seed {SEED})') |