diff --git a/research/p112-next-moving-simple-root/README.md b/research/p112-next-moving-simple-root/README.md new file mode 100644 index 0000000000..8522ba0540 --- /dev/null +++ b/research/p112-next-moving-simple-root/README.md @@ -0,0 +1,243 @@ +# The next moving P112 simple root keeps the transported weight-16 equality + +**Exhaustive finite census of one later reflected frame code. No new κ.** +The selected conditional exponent stays `781451/250000000`. Continue the +pinned simple-root order past the first moving pair, reflect by the next +simple root that moves a punctured line, rebuild that 112-line complement, +and transport the seven supports recorded for the unreflected frame. One +half-split of the rebuilt incidence kernel returns exactly those seven +transported words. + +## Result + +The order is the one fixed by #7017: simple-root index in +`research/p112-a3-six-flat/geometry.py`, then punctured-line index in the +120-line model of `research/p112-rank6-frame-coverage/engine.py`. All 64 +pairs were evaluated. Sixteen pairs move a line, and they lie on simple +roots 0, 3, 5 and 7, four lines each. Simple roots 1, 2, 4 and 6 fix every +punctured line, so they do not rebuild a frame. + +Simple root 0 is the reflection already compared in #7017. Its three later +moving punctured lines use that same reflection and the same rebuilt list. +The next simple root that moves a punctured line is index 3, + +```text +α = (0, -2, 2, 0, 0, 0, 0, 0), +``` + +and the first punctured line it moves is index 0, + +```text +β = (2, 2, 0, 0, 0, 0, 0, 0). +``` + +The reflection coefficient is `β·α / 4 = -1`, so + +```text +s_α(β) = β + α = (2, 0, 2, 0, 0, 0, 0, 0). +``` + +That vector is line 2 of the pinned list. The same reflection applied to +the whole puncture fixes four lines and moves four: + +| Old line | Old vector | New line | Image vector | +|---:|---|---:|---| +| 0 | `(2, 2, 0, 0, 0, 0, 0, 0)` | 2 | `(2, 0, 2, 0, 0, 0, 0, 0)` | +| 1 | `(2, -2, 0, 0, 0, 0, 0, 0)` | 3 | `(2, 0, -2, 0, 0, 0, 0, 0)` | +| 26 | `(0, 0, 2, 2, 0, 0, 0, 0)` | 16 | `(0, 2, 0, 2, 0, 0, 0, 0)` | +| 27 | `(0, 0, 2, -2, 0, 0, 0, 0)` | 17 | `(0, 2, 0, -2, 0, 0, 0, 0)` | +| 44 | `(0, 0, 0, 0, 2, 2, 0, 0)` | 44 | same line | +| 45 | `(0, 0, 0, 0, 2, -2, 0, 0)` | 45 | same line | +| 54 | `(0, 0, 0, 0, 0, 0, 2, 2)` | 54 | same line | +| 55 | `(0, 0, 0, 0, 0, 0, 2, -2)` | 55 | same line | + +The image is a different orthogonal 8-frame. The rebuilt 112-line list is +the complement of that frame, in increasing position in the same 120-line +order. Its SHA256, of `json.dumps` of the 112 coordinate lists, is +`36e3ad43dbfd53d71b20cfac522fbf9c0c1c065054994b9c81767a228d7e6f3c`. +It is not the #7017 list +`a2ac7049d9eef1cff8af1eec35b084e6d261efad5311625e5356cceafe260da7`. + +The seven recorded supports, in the order published with their hash, are + +```text +64691900160 +1034819666160 +280375465144335 +4772259772306660006937399132160 +15509841550538713979819718082560 +1216944648758260568639796318044160 +3955070108453721685622467917250560 +``` + +Their SHA256, of `json.dumps` of that sorted list, is +`ff8861dab18040c03d403a860773218df7c120d03770d701afb61b78c941e3d3`. +This package uses those integers as the set to transport. It does not rerun +the unreflected half-split or the #7017 half-split. + +Transport sends each old live line to its reflected line and records the +new position. The resulting permutation of the 112 bit positions is a +bijection and fixes 62 positions. The transported supports are + +```text +68451290880 +1031047741680 +280375477678095 +4814039611113066403813140725760 +15468061711732307582943976488960 +1216944811960755906164784113909760 +3955069945251226348097480121384960 +``` + +Their SHA256, of `json.dumps` of that sorted list, is +`0ab33f7ea89182ea1cbc5ca06d5f3bdcaeb72a08aef3eaceb33e6a43bbb96636`. +They are seven disjoint weight-16 words and cover every rebuilt line. +They are not the untransported integers, and they are not the #7017 +transported integers +`9ce7e5edc72ee3104d62cb068146b73840e9a3c42c4132403dece70c8146cccb`. + +On that line list there are 1260 orthogonal 8-frames, and every line lies +in 90 of them. The F2 incidence matrix has rank 76. A kernel basis with an +identity on the free coordinates + +```text +(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 18, 20, 22, 24, + 26, 28, 32, 34, 36, 40, 42, 48, 49, 50, 51, 52, 56, 58, 64, 80) +``` + +is orthogonal to every frame row. These free coordinates differ from the +#7017 basis. The seven transported supports meet every frame evenly. Each +of the seven untransported integers fails a frame parity. + +`scan.c` lists the span of the first 18 generators and the span of the +other 18 by doubling. The basis identity makes the 36 generators +independent, so each of the `2^36` kernel words is the XOR of exactly one +pair. The scan's seen-count and the histogram both equal `68719476736`. +The words of weight 1 through 16 are exactly the seven transported +supports, and the least positive weight is 16. The weight distribution is +the same tuple recorded for the unreflected frame: + +```text +0:1 16:7 20:336 22:3584 24:3752 26:10752 28:6864 30:50176 +32:872613 34:2243584 36:5633600 38:21482496 40:123827424 +42:275960832 44:878407488 46:1531507712 48:3797117779 +50:4774038528 52:8574631520 54:8427087872 56:11893702896 +58:8427087872 60:8574631520 62:4774038528 64:3797117779 +66:1531507712 68:878407488 70:275960832 72:123827424 +74:21482496 76:5633600 78:2243584 80:872613 82:50176 +84:6864 86:10752 88:3752 90:3584 92:336 96:7 112:1 +``` + +That agreement is a comparison of this split's histogram with the recorded +unreflected enumerator. It is a separate run, on this simple-root-3 basis. +The seven words themselves differ from both earlier support lists. + +A copy of the low-weight set with one support removed compares unequal. +Clearing the top bit of a transported support produces a weight-15 word +that fails a frame parity, and the weight predicate rejects a set that +contains it. The seven untransported integers are not the low-weight set. + +The same coordinate vector α is the simple root used by the D6 +reflected-anchor census in discussion comment 19515229. That census +transports the #6959 anchors on a 30-line rank-10 code. It does not rebuild +this 112-line list and it does not compare these seven supports. + +## Scope + +Task card `replay-runtime`, priority revision 8, base +`17469fff78c22941280f04d39dc5a1faee09c840`. Scope status is **refined**. + +The card asks for a new measured speedup that keeps coverage. The +revision-8 verification row instead says that faster output is not the same +coverage until checked. Open PR #7017 recorded the first moving pair and +asked whether the next simple root, in the same order, induces the same set +equality on its own rebuilt list. This package is that comparison for +simple root 3 and punctured line 0. Width 112, weight bound 16, one split +after 18 generators. + +The selected record's κ is `781451/250000000`. The priority note on this +base still quotes the older displayed value `0.00310045` from PR #5599. +This package uses the selected record and leaves it unchanged. + +Ordered overlap check on 2026-11-10, after fetching upstream `main` at +`17469fff78c22941280f04d39dc5a1faee09c840`. `scripts/find_overlap.py +--refresh` read 339 open pull requests, including drafts (fetched +`2026-11-10T10:25:29Z`, pagination complete) and 7013 discussion records +(fetched `2026-11-10T10:26:30Z`, pagination complete), plus 10178 local +notes at that head, with a complete local inventory. The matches were open +PR #7017, which poses this question and answers only the first pair, and +discussion comment 19514152, which records that first pair. A cache search +for “simple root 3” also hits discussion comment 19515229, the D6 +reflected-anchor census named above. No open pull request and no fork +`cursor/*` branch had named this next P112 simple root. That search is not +a proof that no comment uses other wording. + +This is an exhaustive finite census of two finite domains. The movement +predicate is all 64 pairs of the 8 simple roots with the 8 punctured lines; +the recorded 16 moving pairs are the completeness statement for which root +is next. The code census is all `2^36` kernel words of this one reflected +frame, through one half-split. The basis identity and the seen-count are +the completeness argument for that kernel. It is not a census of simple +roots 5 or 7, not a proof that every simple reflection preserves the +equality, not a proof that the iterator is correct for every generator +matrix, and not an unrestricted nonexistence result. A failed search was +not used. The unreflected equality of #7007, the first reflected equality +of #7017, the D6 equality of #6959, and the E6 equality of #6948 are not +claimed as this result. + +## Reproduce + +Python 3.12, `gcc`, and the standard library, from the repository root. +One half-split of this kernel measured about 67 seconds on 4 cores. + +```sh +python3 -B scripts/research_package.py --package research/p112-next-moving-simple-root --check-only +python3 -I -B research/p112-next-moving-simple-root/verify.py +python3 -B scripts/research_package.py --package research/p112-next-moving-simple-root +``` + +The direct verifier prints + +```text +PASS_P112_NEXT_MOVING_SIMPLE_ROOT width=112 frames=1260 dimension=36 minimum=16 supports=7 codewords=68719476736 split=18 simple=3 punctured=0 moved_lines=4 fixed_roots=4 later_roots=5,7 same_frame=false first_pair=false unreflected_census=false engine=false no_new_kappa=true +``` + +Outside this run: the unreflected half-split, the #7017 half-split, a +second split of this kernel, `low_weight_words`, +`research/e7-intersection-counters/engine.py`, the E6 and D6 censuses, the +frames of simple roots 5 and 7, Lean, Docker, and +`scripts/research_selected_baseline.py` as a scientific replay. The +selected record is provenance only. + +## Open next questions + +1. Does simple root 5, the next root after this one in the same pinned + order that moves a punctured line, induce the same set equality on its + own rebuilt list? The 64-pair scan records that root, and simple root 7 + after it, but neither half-split was run. The transported integers above + belong to simple root 3. First rebuild the simple-root-5 complement and + compare one half-split with that root's transported supports. +2. Does `low_weight_words` on this simple-root-3 36-generator basis return + the same seven transported supports? This is the bit-lane iterator on + the basis pinned here, compared with + `0ab33f7ea89182ea1cbc5ca06d5f3bdcaeb72a08aef3eaceb33e6a43bbb96636`. + It is not the #7017 reflected basis and not the hash + `9ce7e5edc72ee3104d62cb068146b73840e9a3c42c4132403dece70c8146cccb`. + First run that iterator at one batch size on the free coordinates + pinned above. + +## Credit + +The 120-line coordinate order and the unreflected puncture are the model in +`research/p112-rank6-frame-coverage`, from the PR1226/PR1323 E8 listing +used by Cinna Davis. The simple system is the one fixed in +`research/p112-a3-six-flat/geometry.py`, from Nick Petalas's PR3161/PR3162 +lineage. The seven supports, their hash, and the unreflected weight +distribution are the recorded result of open PR #7007. The first moving +pair, the question answered here, and the doubling half-split are from +open PR #7017. This scanner reads a different basis. The D6 use of the +same simple-root vector is discussion comment 19515229 and is a different +code. + +Prepared by unpaidmoon with substantial Cursor Grok assistance. Apache-2.0. +No independent human review and no formal-kernel acceptance are claimed. diff --git a/research/p112-next-moving-simple-root/SOURCES.json b/research/p112-next-moving-simple-root/SOURCES.json new file mode 100644 index 0000000000..2913610575 --- /dev/null +++ b/research/p112-next-moving-simple-root/SOURCES.json @@ -0,0 +1,30 @@ +{ + "schema_version": 1, + "base_commit": "17469fff78c22941280f04d39dc5a1faee09c840", + "files": [ + { + "path": "certificates/selected-result.json", + "usage": "provenance", + "git_blob": "d59b5035e7b4cbaa9d688a1d32dd0d6eb79c466a", + "sha256": "9b8d0eea95b3bf0dfcb7938a732a6439b87a2685dec2a5ab9e525b3b34debbfa" + }, + { + "path": "docs/research/agent-task-catalog.json", + "usage": "provenance", + "git_blob": "3404fbdd5e7529a0674eef26114b5071e4586cec", + "sha256": "ead6cdc9d0af6d58e8a49744d8ec8c7c15e13d2ba93c276c434bcf9b51b93a81" + }, + { + "path": "research/p112-a3-six-flat/geometry.py", + "usage": "input", + "git_blob": "222ea556faf2b275f73815c4e7d0d5f1e3294f2f", + "sha256": "b8ac379e41f5ad83bdb6c7f7c55129fc3d9c07801ea61591478f25d72ff0b021" + }, + { + "path": "research/p112-rank6-frame-coverage/engine.py", + "usage": "input", + "git_blob": "6afabd3777975310683e4cdc8a4e4b95f0a330ac", + "sha256": "b59101564855a89de04f39fd28b0a853aa3c46ce0ce99951d93d7dd018543536" + } + ] +} diff --git a/research/p112-next-moving-simple-root/TASK.json b/research/p112-next-moving-simple-root/TASK.json new file mode 100644 index 0000000000..901476e168 --- /dev/null +++ b/research/p112-next-moving-simple-root/TASK.json @@ -0,0 +1,67 @@ +{ + "schema_version": 1, + "task_id": "replay-runtime", + "priority_revision": 8, + "base_commit": "17469fff78c22941280f04d39dc5a1faee09c840", + "original_card": { + "id": "replay-runtime", + "question": "Can one measured replay bottleneck be sped up without dropping coverage?", + "starting_points": [ + "scripts/run_isolated_tests.py", + "scripts/verify_replay_coverage.py", + "certificates/selected-result.json" + ], + "methods": [ + "Profile one isolated replay and optimize the measured bottleneck.", + "Design source-bound reuse with explicit dependency invalidation and negative controls." + ], + "acceptance_gate": "Report before/after wall time on the same fixture, retained checks and invalidation behavior.", + "first_step": { + "command": [ + "python3", + "-B", + "scripts/research_selected_baseline.py" + ], + "expected": "PASS_RESEARCH_BASELINE_BINDINGS_ONLY", + "inspect": "Resolve the current selected review, separate complex/ordinary BIT/admitted BIT/outer budgets, and compare the card against live handoffs before starting. Historical ceilings require exact source/model equivalence.", + "scope": "Source-binding orientation only; not a scientific replay or all-size proof. Materialize selected inputs if this checkout is sparse." + }, + "first_experiment": "Measure one unchanged fixture, preserve check coverage, and test one optimization with invalidation controls.", + "bottleneck_link": "Faster trustworthy replays increase review throughput and let mechanism candidates be checked sooner.", + "allowed_variations": [ + "Profile one stage or design one source-bound cache boundary." + ], + "scope_change_triggers": [ + "Skipping coverage or comparing different fixtures to claim speedup." + ], + "overlap_terms": [ + "replay runtime", + "cache invalidation", + "isolated tests" + ] + }, + "scope_status": "refined", + "actual_scope": "Does the next simple root, in the #7017 pinned order, that moves a punctured line induce the same weight-16 set equality on its own rebuilt 112-line list?", + "scope_change_reason": "The replay-runtime card asks for a measured speedup. The revision-8 verification row says a published faster replay is not equivalent coverage until checked. Open PR #7017 recorded the first moving pair and asked whether the next simple root preserves the transported weight-16 equality. This package answers that question for the next root only. It does not time an engine or rerun the first reflection.", + "bottleneck_link": "Frame-code coverage checks remain usable only when the low-weight set still matches after the root list changes. This comparison binds the seven recorded supports to the next reflected P112 frame. It does not change a supplier bill or the selected kappa.", + "budget_minutes": 60, + "stop_rule": "Stop after the next simple root that moves a punctured line, one rebuilt 112-line list, and one half-split compared with the transported supports. Do not half-split a later simple root, rerun the first reflection, or call the bit-lane or intersection-counter engines.", + "outcome": "completed", + "evidence": "The 8 by 8 scan moves 16 pairs, on simple roots 0, 3, 5 and 7. Roots 1, 2, 4 and 6 fix every punctured line. The next simple root after the #7017 root is alpha=(0,-2,2,0,0,0,0,0), and its first moved punctured line is (2,2,0,0,0,0,0,0), sent to (2,0,2,0,0,0,0,0). The image frame moves four lines and is a different orthogonal 8-frame. The rebuilt 112-line complement has SHA256 36e3ad43dbfd53d71b20cfac522fbf9c0c1c065054994b9c81767a228d7e6f3c. Transporting the seven recorded supports gives SHA256 0ab33f7ea89182ea1cbc5ca06d5f3bdcaeb72a08aef3eaceb33e6a43bbb96636. The incidence matrix has 1260 rows, rank 76 and kernel dimension 36, with free coordinates distinct from the #7017 basis. One half-split after 18 generators visits all 68719476736 kernel words and returns exactly those seven weight-16 supports. The weight distribution equals the distribution recorded for the unreflected frame. The seven untransported integers fail frame parity. Selected kappa stays 781451/250000000.", + "author": "unpaidmoon", + "ai_assistance": "Substantial Cursor Grok assistance.", + "open_questions": [ + { + "question": "Does simple root 5, the next root after this one in the same pinned order that moves a punctured line, induce the same set equality on its own rebuilt list?", + "why_it_matters": "This census is simple root 3 only. The 64-pair scan records simple roots 5 and 7 as later moving roots, and a different root produces a different frame, so these transported integers do not answer them.", + "first_step": "Rebuild the simple-root-5 112-line complement and compare one half-split with that root's transported supports.", + "blocked_by": "This package stops once simple root 3 has been compared." + }, + { + "question": "Does low_weight_words on this simple-root-3 36-generator basis return the same seven transported supports?", + "why_it_matters": "The half-split is one enumeration of this kernel. The bit-lane iterator is a different procedure. This basis and hash 0ab33f7ea89182ea1cbc5ca06d5f3bdcaeb72a08aef3eaceb33e6a43bbb96636 are not the #7017 basis or hash 9ce7e5edc72ee3104d62cb068146b73840e9a3c42c4132403dece70c8146cccb.", + "first_step": "Run low_weight_words at one batch size on the free coordinates pinned here and compare it with support hash 0ab33f7ea89182ea1cbc5ca06d5f3bdcaeb72a08aef3eaceb33e6a43bbb96636.", + "blocked_by": "This package does not call low_weight_words." + } + ] +} diff --git a/research/p112-next-moving-simple-root/VERIFY.json b/research/p112-next-moving-simple-root/VERIFY.json new file mode 100644 index 0000000000..03b81c2656 --- /dev/null +++ b/research/p112-next-moving-simple-root/VERIFY.json @@ -0,0 +1,22 @@ +{ + "schema_version": 1, + "status": "ready", + "python": "3.12", + "requirements": [], + "inputs": [ + "research/p112-a3-six-flat/geometry.py", + "research/p112-rank6-frame-coverage/engine.py" + ], + "commands": [ + { + "argv": [ + "{python}", + "-I", + "-B", + "{package}/verify.py" + ], + "timeout_seconds": 600, + "checks": "Authenticate the live E8 line list and the pinned simple system. Continue the simple-root then punctured-line scan past the first moving pair, select the next simple root that moves a punctured line, rebuild its 112-line complement, and transport the seven recorded supports. Compare one half-split after 18 generators, visiting all 2^36 kernel words, with those transported supports. Reject a dropped support, a weight-15 injection, the untransported integers, and the first reflection's transported integers." + } + ] +} diff --git a/research/p112-next-moving-simple-root/scan.c b/research/p112-next-moving-simple-root/scan.c new file mode 100644 index 0000000000..68b81c07c0 --- /dev/null +++ b/research/p112-next-moving-simple-root/scan.c @@ -0,0 +1,198 @@ +/* Exhaustive half-split of one 112-bit, dimension-36 binary basis. + * + * Each generator is a 112-bit integer, written as 28 lowercase hex digits. + * A split s lists the span of the first s generators and the span of the + * rest by doubling. Every codeword is the XOR of exactly one pair. The + * integer popcount is the Hamming weight. This file does not implement the + * bit-lane iterator. + * + * The doubling method is the one used for the unreflected P112 frame in + * PR #7007 and for the first reflected frame in PR #7017. This scanner + * reads only the basis of the next moving simple root. + * + * Apache-2.0. Prepared by unpaidmoon with substantial Cursor Grok assistance. + */ +#include +#include +#include +#include +#include +#include + +enum { DIM = 36, WIDTH = 112, MAX_LOW = 64 }; + +typedef struct { uint64_t lo, hi; } Word; + +static Word basis[DIM]; +static Word *low_words; +static Word *high_words; +static size_t n_low; +static size_t n_high; +static pthread_mutex_t low_mu = PTHREAD_MUTEX_INITIALIZER; +static Word low_found[MAX_LOW]; +static int n_low_found; +static int overflow; + +typedef struct { + size_t begin, end; + uint64_t hist[WIDTH + 1]; + uint64_t seen; +} Job; + +static Word wxor(Word a, Word b) { + Word c; + c.lo = a.lo ^ b.lo; + c.hi = a.hi ^ b.hi; + return c; +} + +static int weight(uint64_t lo, uint64_t hi) { + return __builtin_popcountll(lo) + __builtin_popcountll(hi); +} + +static int hex_value(char ch) { + if (ch >= '0' && ch <= '9') return ch - '0'; + if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10; + return -1; +} + +static int load_basis(const char *path) { + FILE *fp = fopen(path, "r"); + char hex[80]; + if (!fp) return 0; + for (int i = 0; i < DIM; i++) { + uint64_t lo = 0, hi = 0; + if (fscanf(fp, "%79s", hex) != 1 || strlen(hex) != 28) { + fclose(fp); + return 0; + } + for (int k = 0; k < 28; k++) { + int v = hex_value(hex[k]); + if (v < 0) { + fclose(fp); + return 0; + } + hi = (hi << 4) | (lo >> 60); + lo = (lo << 4) | (unsigned)v; + } + if (hi >> 48) { + fclose(fp); + return 0; + } + basis[i].lo = lo; + basis[i].hi = hi; + } + if (fscanf(fp, "%79s", hex) == 1) { + fclose(fp); + return 0; + } + fclose(fp); + return 1; +} + +static void span(Word *out, int start, int count, size_t expected) { + size_t len = 1; + out[0].lo = 0; + out[0].hi = 0; + for (int i = 0; i < count; i++) { + Word g = basis[start + i]; + for (size_t j = 0; j < len; j++) out[len + j] = wxor(out[j], g); + len <<= 1; + } + if (len != expected) { + fprintf(stderr, "span length mismatch\n"); + exit(1); + } +} + +static void *worker(void *arg) { + Job *job = arg; + memset(job->hist, 0, sizeof job->hist); + job->seen = 0; + for (size_t i = job->begin; i < job->end; i++) { + uint64_t hlo = high_words[i].lo; + uint64_t hhi = high_words[i].hi; + for (size_t j = 0; j < n_low; j++) { + uint64_t lo = hlo ^ low_words[j].lo; + uint64_t hi = hhi ^ low_words[j].hi; + int wt = weight(lo, hi); + job->hist[wt]++; + job->seen++; + if (wt > 0 && wt <= 16) { + pthread_mutex_lock(&low_mu); + if (n_low_found >= MAX_LOW) overflow = 1; + else { + low_found[n_low_found].lo = lo; + low_found[n_low_found].hi = hi; + n_low_found++; + } + pthread_mutex_unlock(&low_mu); + } + } + } + return NULL; +} + +int main(int argc, char **argv) { + int split, threads; + long cpus; + Job *jobs; + pthread_t *ids; + uint64_t hist[WIDTH + 1]; + uint64_t seen = 0; + if (argc != 3) { + fprintf(stderr, "usage: scan BASIS SPLIT\n"); + return 2; + } + split = atoi(argv[2]); + if (split < 1 || split >= DIM || !load_basis(argv[1])) { + fprintf(stderr, "bad basis or split\n"); + return 2; + } + n_low = (size_t)1 << split; + n_high = (size_t)1 << (DIM - split); + low_words = calloc(n_low, sizeof(Word)); + high_words = calloc(n_high, sizeof(Word)); + if (!low_words || !high_words) return 1; + span(low_words, 0, split, n_low); + span(high_words, split, DIM - split, n_high); + cpus = sysconf(_SC_NPROCESSORS_ONLN); + if (cpus < 1) cpus = 1; + if (cpus > 4) cpus = 4; + threads = (int)cpus; + jobs = calloc((size_t)threads, sizeof(Job)); + ids = calloc((size_t)threads, sizeof(pthread_t)); + if (!jobs || !ids) return 1; + for (int t = 0; t < threads; t++) { + size_t chunk = n_high / (size_t)threads; + jobs[t].begin = (size_t)t * chunk; + jobs[t].end = (t + 1 == threads) ? n_high : ((size_t)t + 1) * chunk; + if (pthread_create(&ids[t], NULL, worker, &jobs[t]) != 0) return 1; + } + memset(hist, 0, sizeof hist); + for (int t = 0; t < threads; t++) { + pthread_join(ids[t], NULL); + seen += jobs[t].seen; + for (int w = 0; w <= WIDTH; w++) hist[w] += jobs[t].hist[w]; + } + if (overflow) { + fprintf(stderr, "more than %d words of weight 1..16\n", MAX_LOW); + return 1; + } + printf("SPLIT %d\n", split); + printf("SEEN %llu\n", (unsigned long long)seen); + printf("THREADS %d\n", threads); + for (int w = 0; w <= WIDTH; w++) { + if (hist[w]) printf("HIST %d %llu\n", w, (unsigned long long)hist[w]); + } + for (int i = 0; i < n_low_found; i++) { + printf("LOW %012llx%016llx\n", + (unsigned long long)low_found[i].hi, + (unsigned long long)low_found[i].lo); + } + free(jobs); + free(ids); + free(low_words); + free(high_words); + return 0; +} diff --git a/research/p112-next-moving-simple-root/verify.py b/research/p112-next-moving-simple-root/verify.py new file mode 100644 index 0000000000..4bb818c7ca --- /dev/null +++ b/research/p112-next-moving-simple-root/verify.py @@ -0,0 +1,590 @@ +#!/usr/bin/env python3 +"""The next moving simple root of the reflected P112 frame. + +Continue the pinned simple-root, then punctured-line scan past the first +moving pair. The next simple root that moves a punctured line rebuilds its +own 112-line complement. One half-split of that kernel is compared with the +transported images of the seven supports recorded for the unreflected frame. +The scanner lists every kernel word once. It does not call the bit-lane +iterator, and it does not rerun the first reflection's half-split. +""" + +import json +import subprocess +import sys +import tempfile +from fractions import Fraction +from hashlib import sha256 +from itertools import combinations, product +from pathlib import Path +from runpy import run_path + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[1] +WIDTH = 112 +FRAME_SIZE = 8 +FRAME_COUNT = 1260 +RANK = 76 +DIMENSION = 36 +DEGREE = 90 +WEIGHT_BOUND = 16 +SUPPORT_COUNT = 7 +CODEWORDS = 1 << DIMENSION +SPLIT_AT = 18 +SIMPLE_INDEX = 3 +PUNCTURE_INDEX = 0 +MOVED_LINES = 4 +FIXED_BITS = 62 +FIXED_ROOTS = (1, 2, 4, 6) +LATER_ROOTS = (5, 7) +LINE_SHA256 = "36e3ad43dbfd53d71b20cfac522fbf9c0c1c065054994b9c81767a228d7e6f3c" +RECORDED_SUPPORT_SHA256 = "ff8861dab18040c03d403a860773218df7c120d03770d701afb61b78c941e3d3" +TRANSPORTED_SUPPORT_SHA256 = "0ab33f7ea89182ea1cbc5ca06d5f3bdcaeb72a08aef3eaceb33e6a43bbb96636" +FIRST_LINE_SHA256 = "a2ac7049d9eef1cff8af1eec35b084e6d261efad5311625e5356cceafe260da7" +FIRST_TRANSPORTED_SHA256 = "9ce7e5edc72ee3104d62cb068146b73840e9a3c42c4132403dece70c8146cccb" +NAMED_ROOT = (2, 2, 0, 0, 0, 0, 0, 0) +IMAGE_ROOT = (2, 0, 2, 0, 0, 0, 0, 0) +IMAGE_PUNCTURE = (2, 3, 16, 17, 44, 45, 54, 55) +FREE_COORDINATES = ( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 18, 20, 22, 24, 26, + 28, 32, 34, 36, 40, 42, 48, 49, 50, 51, 52, 56, 58, 64, 80, +) +FIRST_FREE_COORDINATES = ( + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 17, 19, 21, 25, 26, 27, + 28, 30, 34, 36, 38, 42, 43, 52, 53, 54, 55, 56, 60, 68, 84, +) +BASIS_WEIGHTS = ( + 38, 34, 38, 30, 42, 34, 26, 30, 42, 30, 50, 30, 38, 32, 32, 30, 32, 32, 42, + 34, 22, 32, 30, 22, 24, 36, 20, 36, 28, 36, 28, 32, 32, 32, 32, 32, +) +RECORDED_SUPPORTS = ( + 64691900160, + 1034819666160, + 280375465144335, + 4772259772306660006937399132160, + 15509841550538713979819718082560, + 1216944648758260568639796318044160, + 3955070108453721685622467917250560, +) +TRANSPORTED_SUPPORTS = ( + 68451290880, + 1031047741680, + 280375477678095, + 4814039611113066403813140725760, + 15468061711732307582943976488960, + 1216944811960755906164784113909760, + 3955069945251226348097480121384960, +) +# (simple index, punctured-line index, old line, image line), in the pinned +# order. The first four rows are the simple-root-0 reflection already compared +# by #7017. This package selects the first later simple root. +MOVING_PAIRS = ( + (0, 1, 1, 88), + (0, 2, 26, 95), + (0, 4, 44, 113), + (0, 7, 55, 118), + (3, 0, 0, 2), + (3, 1, 1, 3), + (3, 2, 26, 16), + (3, 3, 27, 17), + (5, 2, 26, 28), + (5, 3, 27, 29), + (5, 4, 44, 38), + (5, 5, 45, 39), + (7, 4, 44, 46), + (7, 5, 45, 47), + (7, 6, 54, 52), + (7, 7, 55, 53), +) +CARTAN = ( + (2, 0, -1, 0, 0, 0, 0, 0), + (0, 2, 0, -1, 0, 0, 0, 0), + (-1, 0, 2, -1, 0, 0, 0, 0), + (0, -1, -1, 2, -1, 0, 0, 0), + (0, 0, 0, -1, 2, -1, 0, 0), + (0, 0, 0, 0, -1, 2, -1, 0), + (0, 0, 0, 0, 0, -1, 2, -1), + (0, 0, 0, 0, 0, 0, -1, 2), +) +NEIGHBORS = ( + (2,), + (3,), + (0, 3), + (1, 2, 4), + (3, 5), + (4, 6), + (5, 7), + (6,), +) +HISTOGRAM = { + 0: 1, + 16: 7, + 20: 336, + 22: 3584, + 24: 3752, + 26: 10752, + 28: 6864, + 30: 50176, + 32: 872613, + 34: 2243584, + 36: 5633600, + 38: 21482496, + 40: 123827424, + 42: 275960832, + 44: 878407488, + 46: 1531507712, + 48: 3797117779, + 50: 4774038528, + 52: 8574631520, + 54: 8427087872, + 56: 11893702896, + 58: 8427087872, + 60: 8574631520, + 62: 4774038528, + 64: 3797117779, + 66: 1531507712, + 68: 878407488, + 70: 275960832, + 72: 123827424, + 74: 21482496, + 76: 5633600, + 78: 2243584, + 80: 872613, + 82: 50176, + 84: 6864, + 86: 10752, + 88: 3752, + 90: 3584, + 92: 336, + 96: 7, + 112: 1, +} + + +def need(condition, message): + if not condition: + raise ValueError(message) + + +def dot(left, right): + return sum(a * b for a, b in zip(left, right)) + + +def line_key(vector): + return max(tuple(vector), tuple(-value for value in vector)) + + +def reflect(vector, simple): + pairing = dot(vector, simple) + need(pairing % 4 == 0, "reflection coefficient is not integral") + return tuple(value - pairing // 4 * coefficient + for value, coefficient in zip(vector, simple)) + + +def support_digest(words): + payload = json.dumps(sorted(words)).encode() + return sha256(payload).hexdigest() + + +def authenticate(): + sources = json.loads((HERE / "SOURCES.json").read_text(encoding="utf-8")) + checked = 0 + for row in sources["files"]: + if row["usage"] != "input": + continue + path = ROOT / row["path"] + digest = sha256(path.read_bytes()).hexdigest() + need(digest == row["sha256"], "input hash mismatch: " + row["path"]) + checked += 1 + need(checked == 2, "two live inputs") + need(support_digest(RECORDED_SUPPORTS) == RECORDED_SUPPORT_SHA256, + "recorded support hash") + need(support_digest(TRANSPORTED_SUPPORTS) == TRANSPORTED_SUPPORT_SHA256, + "pinned transported hash") + need(TRANSPORTED_SUPPORT_SHA256 != FIRST_TRANSPORTED_SHA256, + "this transport repeats the first reflection") + need(LINE_SHA256 != FIRST_LINE_SHA256, "line-list hash repeats the first reflection") + return checked + + +def independent_lines(): + roots = [] + for i, j in combinations(range(8), 2): + for si in (2, -2): + for sj in (2, -2): + root = [0] * 8 + root[i], root[j] = si, sj + roots.append(tuple(root)) + for signs in product((1, -1), repeat=8): + if signs.count(-1) % 2 == 0: + roots.append(signs) + need(len(roots) == len(set(roots)) == 240, "E8 root count") + lines = [] + seen = set() + for root in roots: + key = line_key(root) + if key not in seen: + seen.add(key) + lines.append(root) + need(len(lines) == 120, "E8 line count") + puncture = [] + for pair in range(4): + for sign in (1, -1): + root = [0] * 8 + root[2 * pair] = 2 + root[2 * pair + 1] = 2 * sign + puncture.append(lines.index(tuple(root))) + return tuple(lines), tuple(puncture), tuple(roots) + + +def pinned_model(): + model = run_path( + str(ROOT / "research/p112-rank6-frame-coverage/engine.py"), + run_name="p112_next_simple_model", + )["model"]() + lines = tuple(tuple(row) for row in model["lines"]) + puncture = tuple(model["punctured_frame"]) + live = tuple(model["live"]) + own_lines, own_puncture, signed = independent_lines() + need(lines == own_lines, "pinned lines differ from the E8 builder") + need(puncture == own_puncture, "pinned puncture differs") + need(live == tuple(i for i in range(120) if i not in set(puncture)), "live order") + need(len(live) == WIDTH, "unreflected width") + need(all(dot(row, row) == 8 for row in lines), "line norms") + need(all(dot(lines[i], lines[j]) == 0 for i, j in combinations(puncture, 2)), + "unreflected frame is not orthogonal") + return lines, puncture, live, signed + + +def load_simple(): + simple = run_path( + str(ROOT / "research/p112-a3-six-flat/geometry.py"), + run_name="p112_next_simple_geometry", + )["SIMPLE"] + need(len(simple) == 8, "simple count") + need(simple[SIMPLE_INDEX] == (0, -2, 2, 0, 0, 0, 0, 0), "simple root 3 changed") + return tuple(tuple(row) for row in simple) + + +def integer_combination(root, simples): + rows = [] + for coordinate in range(8): + rows.append([Fraction(simple[coordinate]) for simple in simples] + + [Fraction(root[coordinate])]) + for column in range(8): + pivot = next((row for row in range(column, 8) if rows[row][column]), None) + need(pivot is not None, "singular simple system") + rows[column], rows[pivot] = rows[pivot], rows[column] + scale = rows[column][column] + rows[column] = [value / scale for value in rows[column]] + for other in range(8): + if other == column or not rows[other][column]: + continue + factor = rows[other][column] + rows[other] = [value - factor * base + for value, base in zip(rows[other], rows[column])] + coeffs = [] + for row in rows: + value = row[-1] + need(value.denominator == 1, "nonintegral simple coordinates") + coeffs.append(int(value)) + built = [0] * 8 + for coeff, simple in zip(coeffs, simples): + for coordinate in range(8): + built[coordinate] += coeff * simple[coordinate] + need(tuple(built) == tuple(root), "simple coordinates do not rebuild the root") + return tuple(coeffs) + + +def check_simple_system(lines, signed, simple_roots): + cartan = tuple(tuple(dot(left, right) // 4 for right in simple_roots) + for left in simple_roots) + need(cartan == CARTAN, "Cartan matrix") + neighbors = tuple(tuple(j for j in range(8) if j != i and cartan[i][j] == -1) + for i in range(8)) + need(neighbors == NEIGHBORS, "E8 diagram") + need(all(dot(root, root) == 8 for root in simple_roots), "simple norms") + index_of = {line_key(row): index for index, row in enumerate(lines)} + for simple in simple_roots: + images = {index_of[line_key(reflect(row, simple))] for row in lines} + need(images == set(range(120)), "a simple reflection misses a line") + for row in lines: + need(reflect(reflect(row, simple), simple) == tuple(row), + "a simple reflection is not an involution on the stored vector") + for simple in simple_roots: + coeffs = integer_combination(simple, simple_roots) + place = next(index for index, value in enumerate(coeffs) if value) + need(coeffs == tuple(int(index == place) for index in range(8)), + "simple roots are not the coordinate basis") + for root in signed: + coeffs = integer_combination(root, simple_roots) + need(all(value >= 0 for value in coeffs) or all(value <= 0 for value in coeffs), + "a root is neither positive nor negative on this system") + return index_of + + +def moving_pairs(lines, puncture, index_of, simple_roots): + found = [] + for simple_index, simple in enumerate(simple_roots): + for puncture_index, line in enumerate(puncture): + image = index_of[line_key(reflect(lines[line], simple))] + if image != line: + found.append((simple_index, puncture_index, line, image)) + need(tuple(found) == MOVING_PAIRS, "moving-pair census changed") + need(len(simple_roots) * len(puncture) == 64, "the movement scan is not the full 8 by 8 grid") + fixed = tuple(index for index in range(8) + if all(row[0] != index for row in found)) + need(fixed == FIXED_ROOTS, "fixed simple roots changed") + return tuple(found) + + +def select_reflection(lines, puncture, index_of, simple_roots, pairs): + need(pairs[0][:2] == (0, 1), "the first moving pair is no longer simple root 0") + same_root = tuple(row for row in pairs if row[0] == pairs[0][0]) + need(len(same_root) == 4, "simple root 0 no longer moves four punctured lines") + later = tuple(row for row in pairs if row[0] > pairs[0][0]) + need(later, "no later simple root moves a punctured line") + chosen = later[0] + need(chosen[:2] == (SIMPLE_INDEX, PUNCTURE_INDEX), "next moving simple root changed") + need(chosen[2:] == (0, 2), "the first line moved by simple root 3 changed") + later_roots = tuple(dict.fromkeys(row[0] for row in later if row[0] != chosen[0])) + need(later_roots == LATER_ROOTS, "roots after simple root 3 changed") + simple = simple_roots[chosen[0]] + line = puncture[chosen[1]] + need(lines[line] == NAMED_ROOT and reflect(NAMED_ROOT, simple) == IMAGE_ROOT, + "named reflection arithmetic") + image = index_of[line_key(IMAGE_ROOT)] + need(image == chosen[3] and lines[image] == IMAGE_ROOT, "image line representative") + images = tuple(index_of[line_key(reflect(lines[old], simple))] for old in puncture) + need(images == IMAGE_PUNCTURE, "image frame") + need(len(set(images)) == FRAME_SIZE, "image frame collapsed") + need(all(dot(lines[i], lines[j]) == 0 for i, j in combinations(images, 2)), + "image frame is not orthogonal") + moved = tuple(pair for pair in zip(puncture, images) if pair[0] != pair[1]) + need(len(moved) == MOVED_LINES, "moved punctured-line count") + need(set(images) != set(puncture), "reflection fixed the frame") + return simple, images + + +def rebuilt_list(lines, puncture, images, index_of, simple): + new_live = tuple(index for index in range(120) if index not in set(images)) + need(len(new_live) == WIDTH, "rebuilt width") + roots = [list(lines[index]) for index in new_live] + digest = sha256(json.dumps(roots).encode()).hexdigest() + need(digest == LINE_SHA256, "rebuilt root-list hash") + old_live = tuple(index for index in range(120) if index not in set(puncture)) + position = {line: bit for bit, line in enumerate(new_live)} + permutation = [] + for line in old_live: + image = index_of[line_key(reflect(lines[line], simple))] + need(image in position, "a live line reflected into the new puncture") + permutation.append(position[image]) + need(sorted(permutation) == list(range(WIDTH)), "transport misses a new line") + fixed = sum(bit == image for bit, image in enumerate(permutation)) + need(fixed == FIXED_BITS, "transport moved a different number of positions") + return new_live, tuple(permutation) + + +def transport_mask(mask, permutation): + image = 0 + while mask: + bit = (mask & -mask).bit_length() - 1 + image |= 1 << permutation[bit] + mask &= mask - 1 + return image + + +def transported_supports(permutation): + images = tuple(transport_mask(mask, permutation) for mask in RECORDED_SUPPORTS) + need(images == TRANSPORTED_SUPPORTS, "transported integers changed") + covered = 0 + for mask in images: + need(mask.bit_count() == WEIGHT_BOUND, "transported weight") + need(covered & mask == 0, "transported supports meet") + covered |= mask + need(covered == (1 << WIDTH) - 1, "transported supports miss a line") + need(set(images) != set(RECORDED_SUPPORTS), "transport left the integers fixed") + return images + + +def orthogonal_frames(lines, live): + adjacency = [] + for index in live: + mask = 0 + for bit, other in enumerate(live): + if index != other and dot(lines[index], lines[other]) == 0: + mask |= 1 << bit + adjacency.append(mask) + frames = [] + stack = [((), (1 << WIDTH) - 1)] + steps = 0 + while stack: + steps += 1 + need(steps <= 5000000, "frame enumeration exceeded its state cap") + chosen, candidates = stack.pop() + if len(chosen) == FRAME_SIZE: + frames.append(chosen) + continue + need_more = FRAME_SIZE - len(chosen) + while candidates: + if candidates.bit_count() < need_more: + break + bit = candidates & -candidates + candidates ^= bit + index = bit.bit_length() - 1 + stack.append((chosen + (index,), candidates & adjacency[index])) + frames = tuple(sorted(set(frames))) + need(len(frames) == FRAME_COUNT, "reflected 8-frame count") + for frame in frames: + need(all(dot(lines[live[i]], lines[live[j]]) == 0 + for i, j in combinations(frame, 2)), "frame not orthogonal") + return frames + + +def kernel(rows): + pivots = {} + for row in rows: + reduced = row + while reduced: + pivot = reduced.bit_length() - 1 + if pivot in pivots: + reduced ^= pivots[pivot] + else: + pivots[pivot] = reduced + break + order = sorted(pivots) + for pivot in order: + row = pivots[pivot] + for other in order: + if other != pivot and (pivots[other] >> pivot) & 1: + pivots[other] ^= row + free = tuple(coordinate for coordinate in range(WIDTH) if coordinate not in pivots) + need(len(pivots) == RANK and free == FREE_COORDINATES, "reflected frame-code rank") + need(free != FIRST_FREE_COORDINATES, "free coordinates repeat the first reflection") + basis = [] + for coordinate in free: + vector = 1 << coordinate + for pivot in order: + if (pivots[pivot] & vector).bit_count() % 2: + vector |= 1 << pivot + need(all((vector & row).bit_count() % 2 == 0 for row in rows), + "a frame parity survives a basis vector") + basis.append(vector) + weights = tuple(vector.bit_count() for vector in basis) + need(weights == BASIS_WEIGHTS, "basis weights changed") + for index, vector in enumerate(basis): + pattern = tuple((vector >> place) & 1 for place in free) + identity = tuple(int(place == index) for place in range(DIMENSION)) + need(pattern == identity, "free coordinates are not a basis identity") + return tuple(basis) + + +def in_code(mask, rows): + return all((mask & row).bit_count() % 2 == 0 for row in rows) + + +def half_split(basis): + with tempfile.TemporaryDirectory(prefix="p112-next-simple-") as directory: + work = Path(directory) + binary = work / "scan" + subprocess.run( + ["gcc", "-O3", "-std=c11", "-pthread", "-Wall", "-Werror", + "-o", str(binary), str(HERE / "scan.c")], + check=True, cwd=work, timeout=60, + ) + path = work / "basis.txt" + path.write_text("".join("%028x\n" % vector for vector in basis), encoding="ascii") + completed = subprocess.run( + [str(binary), str(path), str(SPLIT_AT)], + check=True, capture_output=True, text=True, timeout=480, + ) + need(completed.stderr == "", "scan wrote to stderr") + histogram = {} + found = [] + seen = None + reported = None + for line in completed.stdout.splitlines(): + kind, *rest = line.split() + if kind == "SPLIT": + reported = int(rest[0]) + elif kind == "SEEN": + seen = int(rest[0]) + elif kind == "THREADS": + need(1 <= int(rest[0]) <= 4, "thread cap") + elif kind == "HIST": + histogram[int(rest[0])] = int(rest[1]) + elif kind == "LOW": + need(len(rest[0]) == 28, "low-word width") + found.append(int(rest[0], 16)) + else: + raise ValueError("unexpected scan line") + need(reported == SPLIT_AT, "scan split") + need(seen == CODEWORDS, "half-split missed a codeword") + need(sum(histogram.values()) == CODEWORDS, "histogram sum") + for weight, count in histogram.items(): + need(histogram[WIDTH - weight] == count, "complement asymmetry") + need(histogram == HISTOGRAM, "weight distribution changed") + need(min(weight for weight in histogram if weight) == WEIGHT_BOUND, "minimum left 16") + supports = set(found) + need(len(found) == len(supports) == SUPPORT_COUNT, "support count") + need(all(word.bit_count() == WEIGHT_BOUND for word in supports), "a lighter positive word") + return supports + + +def positive_words_have_weight(words, weight): + return all(word.bit_count() == weight for word in words if word) + + +def main(): + authenticate() + lines, puncture, live, signed = pinned_model() + need(live == tuple(index for index in range(120) if index not in set(puncture)), + "live list") + simple_roots = load_simple() + index_of = check_simple_system(lines, signed, simple_roots) + pairs = moving_pairs(lines, puncture, index_of, simple_roots) + simple, images = select_reflection(lines, puncture, index_of, simple_roots, pairs) + new_live, permutation = rebuilt_list(lines, puncture, images, index_of, simple) + supports = transported_supports(permutation) + frames = orthogonal_frames(lines, new_live) + degree = [0] * WIDTH + for frame in frames: + for index in frame: + degree[index] += 1 + need(set(degree) == {DEGREE}, "frame degree") + rows = tuple(sum(1 << index for index in frame) for frame in frames) + basis = kernel(rows) + need(all(in_code(mask, rows) for mask in supports), "a transported support leaves a frame") + need(in_code((1 << WIDTH) - 1, rows), "all-ones leaves a frame") + need(all(not in_code(mask, rows) for mask in RECORDED_SUPPORTS), + "an untransported support is a codeword of this frame") + scanned = half_split(basis) + need(scanned == set(supports), "half-split disagrees with the transported supports") + dropped = set(supports) + dropped.remove(supports[0]) + need(dropped != scanned, "dropping one support still compared equal") + lighter = supports[0] ^ (1 << (supports[0].bit_length() - 1)) + need(lighter.bit_count() == WEIGHT_BOUND - 1, "clearing the top bit did not reduce weight") + need(lighter not in scanned and not in_code(lighter, rows), + "a weight-fifteen word was already in the code") + forged = set(scanned) + forged.add(lighter) + need(not positive_words_have_weight(forged, WEIGHT_BOUND), + "weight-fifteen injection was accepted") + need(set(RECORDED_SUPPORTS) != scanned, + "the untransported integers equal this low-weight set") + print( + "PASS_P112_NEXT_MOVING_SIMPLE_ROOT" + " width=%d frames=%d dimension=%d minimum=%d supports=%d" + " codewords=%d split=%d simple=%d punctured=%d moved_lines=%d" + " fixed_roots=%d later_roots=%s same_frame=false first_pair=false" + " unreflected_census=false engine=false no_new_kappa=true" + % (WIDTH, FRAME_COUNT, DIMENSION, WEIGHT_BOUND, SUPPORT_COUNT, + CODEWORDS, SPLIT_AT, SIMPLE_INDEX, PUNCTURE_INDEX, MOVED_LINES, + len(FIXED_ROOTS), ",".join(str(index) for index in LATER_ROOTS)) + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())