diff --git a/notes/issue-665-joint-rank-wrap-20260908.md b/notes/issue-665-joint-rank-wrap-20260908.md new file mode 100644 index 00000000..d34d01eb --- /dev/null +++ b/notes/issue-665-joint-rank-wrap-20260908.md @@ -0,0 +1,129 @@ +# #665 joint dictionary — (k, r_b, r_w) x wrapping-type x 5-names, axis L=3,4 + +**Ticket:** LightChainr/Matching-One#665 (parent #650). **Date:** 2026-09-08. +**Mode:** new census at axis L=3 (`2^9`) and L=4 (`2^16`) only, seconds-scale. Axis +L=5 (`2^25`) is **NEED_HUAWEI** and was not run on the Mac. Integers only. +No `docs/STATUS.md` edit; no ticket closed or merged. + +## What was computed + +The two exact views of the same site configurations had never been tabulated +jointly. This census streams every configuration on the axis torus and records + +```text +(k, r_black, r_white, wrap_black, wrap_white, label5_black, label5_white) +``` + +with + +- `(r_b, r_w)` — ambient H_1 winding ranks of the black NN (primal) graph and + the white NN+NNN (matching) graph, as in PR #653's repaired lab + (`scripts/probe_invariant_shape/exact_rank_census.py`); +- coarse wrap labels `neither / dir0 / dir1 / both` via + `torus_homology.wrapping_channels` (PR #653's 4×4 classifier, where `both` + lumps rank-2 cross with rank-1 spirals); +- #646 5-names `none / x / y / both-same / both-two` via + `scripts/probe635_sector_decomposition.py` (`both-same` = a *single* cluster + wrapping both axes, rank-1 spiral or rank-2 cross alike; `both-two` = the + two-distinct-clusters case). + +Script: `scripts/issue665_joint_rank_wrap_census.py`. Exact integer joints: +`results/issue665-joint-rank-wrap/axis-L3.json`, `axis-L4.json`. +Test: `tests/test_issue665_joint_rank_wrap.py` (axis L=2,3; seconds). + +## Tripwires — both pass at L=3 and L=4 + +1. **Bernstein.** Collapsing the coarse 4×4 with + `D = 1{primal either} − 1{matching either}` reproduces the committed + Bernstein integers bit-for-bit: + - L=3: `[-1, -9, -36, -78, -90, -36, 36, 36, 9, 1]`; + - L=4: the committed rung (matching the `collapsed_D` field in the JSON). +2. **`r_b + r_w = 2`** on every configuration (PR #653), zero violations at + both sizes. Rank-pair totals reproduce PR #653 exactly: + - L=3: `(0,2)=259, (1,1)=162, (2,0)=91`; + - L=4: `(0,2)=36559, (1,1)=19932, (2,0)=9045`. + +## The question: is `both-same` rank-2, or does `both-same × both-same` contain spirals? + +**Answer: `both-same` is NOT the same as rank 2, and `both-same × both-same` +is pure spiral — rank-1 × rank-1, never rank-2.** The exact integers: + +- The 5-name `both-same` splits by rank (black side; white side is the + transpose, as expected from the L=3,4 cell support): + + | L | `both-same`, rank 1 (spiral) | `both-same`, rank 2 (cross) | + |---|---|---| + | 3 | 6 | 91 | + | 4 | 1120 | 9045 | + + So `both-same = spirals ⊎ crosses`: the label is a *cluster-level* statement + (one cluster, both axes) and a rank-1 spiral cluster satisfies it. The coarse + `both` label also contains both, so neither label alone is the rank-2 class; + the rank-2 (cross) class is exactly `both-same` with component rank 2 — the + `cross` bit of `torus_homology.ComponentHomology`, which neither committed + table carries as a column. + +- The cell `both-same × both-same` (the "spiral pairing" cell of #660/#662) + is **entirely rank-1 × rank-1**: + + | L | `both-same × both-same` by `(r_b, r_w)` | + |---|---| + | 3 | `(1,1): 6` (at k=6 only) | + | 4 | `(1,1): 1120` (k=8: 120, k=9: 416, k=10: 448, k=11: 128, k=12: 8) | + + The coarse `both × both` cell coincides with it at both sizes — there is no + rank-2 × rank-2 configuration at L=3,4 (a rank-2 black graph forces + `r_w = 0`, so rank-2 on both sides is impossible given `r_b + r_w = 2`). + This is the structural reason `both × both` carries `D = 0`: both sides are + rank-1 spirals there, `either` is 1 on both sides, and no cross is involved. + +- **Rank-2 lives exclusively in the D-carrying exclusive-cross cells.** Every + configuration with `wrap_black = neither, wrap_white = both` has + `(r_b, r_w) = (0, 2)` (white cross-wraps: rank 2); every configuration with + `wrap_black = both, wrap_white = neither` has `(r_b, r_w) = (2, 0)`. L=3 + counts: 259 and 91; L=4: 36559 and 9045. No D-cell ever mixes ranks. + +- Corollary for the #635/#660 identity: the both-same amplitude rows of PR + #646's decomposition are **not** rank-pure. `A_black^{both-same}` contains + the rank-2 cross mass (D = +1 side) *and* rank-1 spiral mass (D = 0); the + spiral 6 at axis L=3 k=6 rides inside `both × both` with D = 0 and cancels + out of `M` — consistent with the Mertens–Ziff pairing (spiraling counts + match), but the sector label alone does not separate them. Any future + sector-level transfer-matrix reading (#636, not funded) must split + `both-same` by rank or it is not measuring one homology class. + +- `both-two` has zero mass at every k at L=3,4 (consistent with #651's + A-continues through axis L=5), and `dir0 × dir0 = dir1 × dir1` at every k + (L=3: 78 each; L=4: 9406 each), reproducing PR #653's published support. + +## Cell support (all configurations, both L) + +```text +neither × both (0,2) D = −1 259 (L=3) / 36559 (L=4) +both × neither (2,0) D = +1 91 (L=3) / 9045 (L=4) +dir0 × dir0 (1,1) D = 0 78 (L=3) / 9406 (L=4) +dir1 × dir1 (1,1) D = 0 78 (L=3) / 9406 (L=4) +both × both (1,1) D = 0 6 (L=3) / 1120 (L=4) ← spirals +``` + +No other cell of the coarse 4×4 is populated at axis L=3 or L=4; within each +populated coarse cell the 5-name pair is constant (`none×both-same`, +`both-same×none`, `x×x`, `y×y`, `both-same×both-same` respectively). The five +Mertens–Ziff cells are therefore exactly rank-diagonal at these sizes: +D-carrying cells are the rank-exhausting `(0,2)`/`(2,0)` mismatches, and the +D-neutral cells are all rank-1 × rank-1. + +## What L=3,4 cannot settle + +Whether rank-2 × rank-2 configurations first appear somewhere (they cannot at +any L with `r_b + r_w = 2` on site — that identity forces at most one rank-2 +side), whether a sixth coarse cell appears, and whether `both-two` stays +empty at larger L. Axis L=5 (`2^25`) is NEED_HUAWEI; the #657 run already +covers the 5-name and coarse views there, but the joint rank-splitting of +this ticket has not been run at L=5. + +**Tripwire standing rule (unchanged):** any claimed decomposition must +reproduce `[-1, -9, -36, -78, -90, -36, 36, 36, 9, 1]` at axis L=3 +bit-for-bit, integers / Fraction only, or it is unused. + +Related: #635, #640, #646, #650, #651, #653, #657, #660, #662. diff --git a/results/issue665-joint-rank-wrap/axis-L3.json b/results/issue665-joint-rank-wrap/axis-L3.json new file mode 100644 index 00000000..551eedcb --- /dev/null +++ b/results/issue665-joint-rank-wrap/axis-L3.json @@ -0,0 +1,300 @@ +{ + "schema": "matching-one.issue665.joint-rank-wrap.v1", + "geometry": "axis", + "L": 3, + "N": 9, + "configs": 512, + "tripwire_bernstein": "pass", + "tripwire_rb_plus_rw_eq_2": "pass", + "committed_bernstein": [ + -1, + -9, + -36, + -78, + -90, + -36, + 36, + 36, + 9, + 1 + ], + "collapsed_D": [ + -1, + -9, + -36, + -78, + -90, + -36, + 36, + 36, + 9, + 1 + ], + "rank_pair_totals": { + "0,2": 259, + "1,1": 162, + "2,0": 91 + }, + "joint": [ + { + "k": 0, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 1 + }, + { + "k": 1, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 9 + }, + { + "k": 2, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 36 + }, + { + "k": 3, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 78 + }, + { + "k": 3, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 3 + }, + { + "k": 3, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 3 + }, + { + "k": 4, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 90 + }, + { + "k": 4, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 18 + }, + { + "k": 4, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 18 + }, + { + "k": 5, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 45 + }, + { + "k": 5, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 36 + }, + { + "k": 5, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 36 + }, + { + "k": 5, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 9 + }, + { + "k": 6, + "r_black": 1, + "r_white": 1, + "wrap_black": "both", + "wrap_white": "both", + "label5_black": "both-same", + "label5_white": "both-same", + "count": 6 + }, + { + "k": 6, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 21 + }, + { + "k": 6, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 21 + }, + { + "k": 6, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 36 + }, + { + "k": 7, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 36 + }, + { + "k": 8, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 9 + }, + { + "k": 9, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 1 + } + ], + "five_name_by_rank_black": { + "none": [ + 259, + 0, + 0 + ], + "x": [ + 0, + 78, + 0 + ], + "y": [ + 0, + 78, + 0 + ], + "both-same": [ + 0, + 6, + 91 + ], + "both-two": [ + 0, + 0, + 0 + ] + }, + "five_name_by_rank_white": { + "none": [ + 91, + 0, + 0 + ], + "x": [ + 0, + 78, + 0 + ], + "y": [ + 0, + 78, + 0 + ], + "both-same": [ + 0, + 6, + 259 + ], + "both-two": [ + 0, + 0, + 0 + ] + }, + "both-same_x_both-same_by_rank_pair": { + "1,1": 6 + }, + "coarse-both_x_coarse-both_by_rank_pair": { + "1,1": 6 + } +} \ No newline at end of file diff --git a/results/issue665-joint-rank-wrap/axis-L4.json b/results/issue665-joint-rank-wrap/axis-L4.json new file mode 100644 index 00000000..6f8ffef9 --- /dev/null +++ b/results/issue665-joint-rank-wrap/axis-L4.json @@ -0,0 +1,564 @@ +{ + "schema": "matching-one.issue665.joint-rank-wrap.v1", + "geometry": "axis", + "L": 4, + "N": 16, + "configs": 65536, + "tripwire_bernstein": "pass", + "tripwire_rb_plus_rw_eq_2": "pass", + "committed_bernstein": [ + -1, + -16, + -120, + -560, + -1812, + -4272, + -7448, + -9424, + -7874, + -2896, + 1720, + 2832, + 1660, + 560, + 120, + 16, + 1 + ], + "collapsed_D": [ + -1, + -16, + -120, + -560, + -1812, + -4272, + -7448, + -9424, + -7874, + -2896, + 1720, + 2832, + 1660, + 560, + 120, + 16, + 1 + ], + "rank_pair_totals": { + "0,2": 36559, + "1,1": 19932, + "2,0": 9045 + }, + "joint": [ + { + "k": 0, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 1 + }, + { + "k": 1, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 16 + }, + { + "k": 2, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 120 + }, + { + "k": 3, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 560 + }, + { + "k": 4, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 1812 + }, + { + "k": 4, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 4 + }, + { + "k": 4, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 4 + }, + { + "k": 5, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 4272 + }, + { + "k": 5, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 48 + }, + { + "k": 5, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 48 + }, + { + "k": 6, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 7448 + }, + { + "k": 6, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 280 + }, + { + "k": 6, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 280 + }, + { + "k": 7, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 9440 + }, + { + "k": 7, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 992 + }, + { + "k": 7, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 992 + }, + { + "k": 7, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 16 + }, + { + "k": 8, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 8082 + }, + { + "k": 8, + "r_black": 1, + "r_white": 1, + "wrap_black": "both", + "wrap_white": "both", + "label5_black": "both-same", + "label5_white": "both-same", + "count": 120 + }, + { + "k": 8, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 2230 + }, + { + "k": 8, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 2230 + }, + { + "k": 8, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 208 + }, + { + "k": 9, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 3984 + }, + { + "k": 9, + "r_black": 1, + "r_white": 1, + "wrap_black": "both", + "wrap_white": "both", + "label5_black": "both-same", + "label5_white": "both-same", + "count": 416 + }, + { + "k": 9, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 2976 + }, + { + "k": 9, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 2976 + }, + { + "k": 9, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 1088 + }, + { + "k": 10, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 792 + }, + { + "k": 10, + "r_black": 1, + "r_white": 1, + "wrap_black": "both", + "wrap_white": "both", + "label5_black": "both-same", + "label5_white": "both-same", + "count": 448 + }, + { + "k": 10, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 2128 + }, + { + "k": 10, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 2128 + }, + { + "k": 10, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 2512 + }, + { + "k": 11, + "r_black": 0, + "r_white": 2, + "wrap_black": "neither", + "wrap_white": "both", + "label5_black": "none", + "label5_white": "both-same", + "count": 32 + }, + { + "k": 11, + "r_black": 1, + "r_white": 1, + "wrap_black": "both", + "wrap_white": "both", + "label5_black": "both-same", + "label5_white": "both-same", + "count": 128 + }, + { + "k": 11, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 672 + }, + { + "k": 11, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 672 + }, + { + "k": 11, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 2864 + }, + { + "k": 12, + "r_black": 1, + "r_white": 1, + "wrap_black": "both", + "wrap_white": "both", + "label5_black": "both-same", + "label5_white": "both-same", + "count": 8 + }, + { + "k": 12, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir0", + "wrap_white": "dir0", + "label5_black": "x", + "label5_white": "x", + "count": 76 + }, + { + "k": 12, + "r_black": 1, + "r_white": 1, + "wrap_black": "dir1", + "wrap_white": "dir1", + "label5_black": "y", + "label5_white": "y", + "count": 76 + }, + { + "k": 12, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 1660 + }, + { + "k": 13, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 560 + }, + { + "k": 14, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 120 + }, + { + "k": 15, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 16 + }, + { + "k": 16, + "r_black": 2, + "r_white": 0, + "wrap_black": "both", + "wrap_white": "neither", + "label5_black": "both-same", + "label5_white": "none", + "count": 1 + } + ], + "five_name_by_rank_black": { + "none": [ + 36559, + 0, + 0 + ], + "x": [ + 0, + 9406, + 0 + ], + "y": [ + 0, + 9406, + 0 + ], + "both-same": [ + 0, + 1120, + 9045 + ], + "both-two": [ + 0, + 0, + 0 + ] + }, + "five_name_by_rank_white": { + "none": [ + 9045, + 0, + 0 + ], + "x": [ + 0, + 9406, + 0 + ], + "y": [ + 0, + 9406, + 0 + ], + "both-same": [ + 0, + 1120, + 36559 + ], + "both-two": [ + 0, + 0, + 0 + ] + }, + "both-same_x_both-same_by_rank_pair": { + "1,1": 1120 + }, + "coarse-both_x_coarse-both_by_rank_pair": { + "1,1": 1120 + } +} \ No newline at end of file diff --git a/scripts/issue665_joint_rank_wrap_census.py b/scripts/issue665_joint_rank_wrap_census.py new file mode 100644 index 00000000..f685c859 --- /dev/null +++ b/scripts/issue665_joint_rank_wrap_census.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""#665 joint dictionary: (k, r_b, r_w, wrap 4x4, 5-name labels) at axis L=3,4. + +Crosses two exact views of the same site configurations that had never been +tabulated jointly: + +* the rank pair ``(r_b, r_w)`` of the repaired bond/site lab (PR #653, + ``scripts/probe_invariant_shape/exact_rank_census.py``): rank of the winding + image in H_1(T^2; Q) of the black NN (primal) graph and of the white + NN+NNN (matching) graph, with ``r_b + r_w = 2`` configuration-wise; +* the wrapping-type 4x4 (PR #653, ``scripts/wrapping_type_census.py``) and the + #646 5-names ``none / x / y / both-same / both-two`` + (``scripts/probe635_sector_decomposition.py``). + +For each configuration the record is + + (k, r_black, r_white, wrap_black, wrap_white, label5_black, label5_white) + +with wrap labels in {neither, dir0, dir1, both} (coarse) and 5-names in +{none, x, y, both-same, both-two}. All counts are exact integers. + +Tripwires (both must pass or the run aborts): + +1. collapsing the coarse 4x4 with ``D = 1{primal either} - 1{matching either}`` + reproduces the committed Bernstein integers at each L + (axis L=3: [-1,-9,-36,-78,-90,-36,36,36,9,1]); +2. ``r_b + r_w = 2`` on every site configuration (PR #653). + +The question this census answers (#665 / parent #650): is the 5-name +``both-same`` exactly the coarse rank-2 (``cross``) class, or does the cell +``both-same x both-same`` also contain rank-1 spiral configurations? The +coarse ``both`` label lumps rank-2 cross with rank-1 spirals +(``torus_homology.ComponentHomology.cross`` distinguishes them); the 5-name +``both-same`` means a *single* cluster wrapping both axes, which a rank-1 +spiral cluster also satisfies. The joint table resolves this at L=3,4 +integers-only; L=5 (2^25) is NEED_HUAWEI and is deliberately not run here. +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +from exact_matching_polynomial import bernstein_counts # noqa: E402 +from matched_torus_reference import ( # noqa: E402 + axis_geometry, + popcount, +) +from probe635_sector_decomposition import ( # noqa: E402 (PR #646) + sector_label, + wrap_homology, +) +from torus_homology import ( # noqa: E402 + component_homologies, + geometry_periods, + wrapping_channels, +) + +WRAP_LABELS = ("neither", "dir0", "dir1", "both") +FIVE_NAMES = ("none", "x", "y", "both-same", "both-two") + + +def wrap_label(channels) -> str: + if channels.both: + return "both" + if channels.direction_0: + return "dir0" + if channels.direction_1: + return "dir1" + return "neither" + + +def either(label: str) -> bool: + return label != "neither" + + +def rank_of(components) -> int: + """Ambient H_1 winding rank of a configuration: max component basis rank.""" + return max((c.rank for c in components), default=0) + + +def census(L: int) -> dict: + geometry = axis_geometry(L) + n = geometry.n + periods = geometry_periods(geometry) + edges_p = geometry.primal_edges + edges_m = geometry.matching_edges + + # joint[k][wrap_b][wrap_w][lab5_b][lab5_w][r_b][r_w] -> count + # Materialised sparsely: dict keyed by full tuple. + joint: dict[tuple, int] = {} + # 5-name x rank cross for each side: five_by_rank[side][lab][r] -> count + five_by_rank = { + side: {lab: [0] * 3 for lab in FIVE_NAMES} for side in ("black", "white") + } + # both-same x both-same cell, split by (r_b, r_w): the question cell + bs_bs_by_rank: dict[tuple[int, int], int] = {} + both_both_by_rank: dict[tuple[int, int], int] = {} + + collapsed = [0] * (n + 1) + dual_fail = 0 + rank_pair_totals: dict[tuple[int, int], int] = {} + + for mask in range(1 << n): + k = popcount(mask) + black = [bool((mask >> i) & 1) for i in range(n)] + white = [not v for v in black] + + comps_b = component_homologies(black, edges_p, periods) + comps_w = component_homologies(white, edges_m, periods) + rb = rank_of(comps_b) + rw = rank_of(comps_w) + if rb + rw != 2: + dual_fail += 1 + + ch_b = wrapping_channels(comps_b) + ch_w = wrapping_channels(comps_w) + pb = wrap_label(ch_b) + pw = wrap_label(ch_w) + + hb = wrap_homology(black, edges_p, n) + hw = wrap_homology(white, edges_m, n) + lb = sector_label(*hb) + lw = sector_label(*hw) + + joint[(k, rb, rw, pb, pw, lb, lw)] = ( + joint.get((k, rb, rw, pb, pw, lb, lw), 0) + 1 + ) + five_by_rank["black"][lb][rb] += 1 + five_by_rank["white"][lw][rw] += 1 + if lb == "both-same" and lw == "both-same": + bs_bs_by_rank[(rb, rw)] = bs_bs_by_rank.get((rb, rw), 0) + 1 + if pb == "both" and pw == "both": + both_both_by_rank[(rb, rw)] = both_both_by_rank.get((rb, rw), 0) + 1 + + collapsed[k] += (int(either(pb)) - int(either(pw))) + rank_pair_totals[(rb, rw)] = rank_pair_totals.get((rb, rw), 0) + 1 + + committed = bernstein_counts(geometry) + if dual_fail: + raise SystemExit(f"tripwire failed: r_b+r_w=2 violated on {dual_fail} configs") + if collapsed != committed: + raise SystemExit( + f"tripwire failed: collapsed={collapsed} committed={committed}" + ) + + return { + "schema": "matching-one.issue665.joint-rank-wrap.v1", + "geometry": "axis", + "L": L, + "N": n, + "configs": 1 << n, + "tripwire_bernstein": "pass", + "tripwire_rb_plus_rw_eq_2": "pass", + "committed_bernstein": committed, + "collapsed_D": collapsed, + "rank_pair_totals": { + f"{rb},{rw}": cnt for (rb, rw), cnt in sorted(rank_pair_totals.items()) + }, + "joint": [ + { + "k": k, + "r_black": rb, + "r_white": rw, + "wrap_black": pb, + "wrap_white": pw, + "label5_black": lb, + "label5_white": lw, + "count": cnt, + } + for (k, rb, rw, pb, pw, lb, lw), cnt in sorted(joint.items()) + ], + "five_name_by_rank_black": five_by_rank["black"], + "five_name_by_rank_white": five_by_rank["white"], + "both-same_x_both-same_by_rank_pair": { + f"{rb},{rw}": cnt for (rb, rw), cnt in sorted(bs_bs_by_rank.items()) + }, + "coarse-both_x_coarse-both_by_rank_pair": { + f"{rb},{rw}": cnt for (rb, rw), cnt in sorted(both_both_by_rank.items()) + }, + } + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--L", type=int, choices=(3, 4), required=True, + help="axis L=3 (2^9) or L=4 (2^16); L=5 is NEED_HUAWEI") + ap.add_argument("--out", default=None) + args = ap.parse_args() + + result = census(args.L) + text = json.dumps(result, indent=1) + dest = Path(args.out) if args.out else ( + ROOT / "results" / "issue665-joint-rank-wrap" / f"axis-L{args.L}.json" + ) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(text) + print(f"wrote {dest}") + print("ALL_CHECKS_PASS") + + +if __name__ == "__main__": + main() diff --git a/scripts/probe635_sector_decomposition.py b/scripts/probe635_sector_decomposition.py new file mode 100644 index 00000000..7fe0c19f --- /dev/null +++ b/scripts/probe635_sector_decomposition.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""#635: exact finite-L decomposition of M(p) by wrapping homology sector. + +Question (issue #635): is M(p) = sum_C D(C) p^k (1-p)^(N-k) exactly a +difference of two "topological sector" amplitudes at FINITE L, where +"topological sector" means: wrap type labeled by the homology class of the +wrapping cluster in Z^2 universal cover (square-site primal, NN edges, black) +vs. its matching lattice (NN+NNN, white)? + +Sectors enumerated here: + + primal (black, NN edges): + sector nn-x : wraps only along x + sector nn-y : wraps only along y + sector nn-both : nontrivial wrap in both directions (same or two clusters) + sector nn-ee : both, via two distinct clusters (the "e;e" of Jacobsen) + + matching (white, NN+NNN): + same four with prefix m-. + +D(C) restricted to a sector is the signed difference of the two indicators, +attributed to the config by its pair of labels. Exact arithmetic throughout +(fractions.Fraction); the acceptance gate is bit-exact reproduction of the +axis L=3 Bernstein integers [-1,-9,-36,-78,-90,-36,36,36,9,1]. + +Outputs results/probe635-sector-map/latest.json with per-sector Bernstein +coefficient tables for axis L=2,3,4 and diamond L=2, plus the exact check +that summing sectors reproduces the reference counts. +""" + +from __future__ import annotations + +import argparse +import json +import math +from fractions import Fraction +from pathlib import Path + +from matched_torus_reference import ( + Geometry, + axis_geometry, + cluster_stats, + diamond_geometry, + popcount, +) + + +def wrap_homology(active: list[bool], edges, n: int) -> tuple[bool, bool, bool, bool]: + """Return (wx, wy, both_same_cluster, both_two_clusters) for active sites. + + wx/wy: some single cluster has nontrivial displacement along x / y. + both_same_cluster: one cluster wraps in both directions. + both_two_clusters: wx and wy hold, but achieved by two distinct clusters + (the sector labelled 'e;e' in Jacobsen's terminology). + """ + size = [1] * n + parent = list(range(n)) + dx_ = [0] * n + dy_ = [0] * n + wrap_x = [False] * n + wrap_y = [False] * n + + def find(x): + if parent[x] == x: + return x, 0, 0 + p = parent[x] + r, px, py = find(p) + dx_[x] += px + dy_[x] += py + parent[x] = r + return r, dx_[x], dy_[x] + + for e in edges: + if not (active[e.i] and active[e.j]): + continue + ri, ix, iy = find(e.i) + rj, jx, jy = find(e.j) + rdx = ix + e.dx - jx + rdy = iy + e.dy - jy + if ri == rj: + if rdx != 0: + wrap_x[ri] = True + if rdy != 0: + wrap_y[ri] = True + else: + if size[rj] < size[ri]: + ri, rj = rj, ri + rdx, rdy = -rdx, -rdy + parent[rj] = ri + size[ri] += size[rj] + dx_[rj] = rdx + dy_[rj] = rdy + wrap_x[ri] = wrap_x[ri] or wrap_x[rj] + wrap_y[ri] = wrap_y[ri] or wrap_y[rj] + + any_x = any_y = same = False + for i in range(n): + if active[i]: + r, _, _ = find(i) + any_x = any_x or wrap_x[r] + any_y = any_y or wrap_y[r] + if any_x and any_y: + seen = set() + for i in range(n): + if active[i]: + r, _, _ = find(i) + if r in seen: + continue + seen.add(r) + if wrap_x[r] and wrap_y[r]: + same = True + break + two = any_x and any_y and not same + return any_x, any_y, same, two + + +def sector_label(wx: bool, wy: bool, same: bool, two: bool) -> str: + if not wx and not wy: + return "none" + if wx and not wy: + return "x" + if wy and not wx: + return "y" + if same: + return "both-same" + return "both-two" + + +def sector_counts(geometry: Geometry) -> dict[str, list[int]]: + n = geometry.n + if n > 24: + raise ValueError(f"N={n} too large for exact enumeration") + tables: dict[str, list[int]] = {} + for prefix in ("p", "m"): + for lab in ("none", "x", "y", "both-same", "both-two"): + tables[f"{prefix}-{lab}"] = [0] * (n + 1) + # diagnostics: occurrences of each wrap label regardless of D + diag: dict[str, list[int]] = {f"{p}-{l}": [0] * (n + 1) for p in ("p", "m") for l in ("none", "x", "y", "both-same", "both-two")} + + for mask in range(1 << n): + k = popcount(mask) + black = [bool((mask >> i) & 1) for i in range(n)] + white = [not v for v in black] + + b = wrap_homology(black, geometry.primal_edges, n) + w = wrap_homology(white, geometry.matching_edges, n) + db = int(any(b[:2])) + dw = int(any(w[:2])) + lb = sector_label(*b) + lw = sector_label(*w) + diag[f"p-{lb}"][k] += 1 + diag[f"m-{lw}"][k] += 1 + d = db - dw + if d == 0: + continue + # attribute the signed indicator difference to the observed pair + if db: + tables[f"p-{lb}"][k] += 1 + if dw: + tables[f"m-{lw}"][k] -= 1 + + return tables, diag + + +def verify(tables: dict[str, list[int]], reference: list[int]) -> bool: + n = len(reference) - 1 + total = [0] * (n + 1) + for key, row in tables.items(): + for k in range(n + 1): + total[k] += row[k] + return total == reference + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--geometry", default="axis", choices=["axis", "diamond"]) + ap.add_argument("--L", type=int, default=3) + ap.add_argument("--out", default=None) + args = ap.parse_args() + + if args.geometry == "axis": + geometry = axis_geometry(args.L) + else: + geometry = diamond_geometry(args.L) + n = geometry.n + if n > 22: + raise SystemExit(f"N={n}: exact 2^N enumeration too slow here; stop") + + reference = [0] * (n + 1) + for mask in range(1 << n): + k = popcount(mask) + black = [bool((mask >> i) & 1) for i in range(n)] + white = [not v for v in black] + _, bw = cluster_stats(black, geometry.primal_edges) + _, ww = cluster_stats(white, geometry.matching_edges) + reference[k] += int(bw) - int(ww) + + tables, diag = sector_counts(geometry) + ok = verify(tables, reference) + if not ok: + total = [0] * (n + 1) + for key, row in tables.items(): + for k in range(n + 1): + total[k] += row[k] + raise SystemExit(f"sector decomposition failed to reproduce reference: {total}") + + print(f"geometry: {geometry.name} L={args.L} N={n}") + print("reference bernstein:", reference) + for key in sorted(tables): + if any(tables[key]): + print(f" {key:14s}", tables[key]) + print("wrap-label diagnostics (occurrences incl. D=0):") + for key in sorted(diag): + if any(diag[key]): + print(f" {key:14s}", diag[key]) + + payload = { + "ticket": 635, + "geometry": geometry.name, + "L": args.L, + "N": n, + "reference_bernstein": reference, + "sector_tables": {k: v for k, v in tables.items() if any(v)}, + "wrap_label_diagnostics": {k: v for k, v in diag.items() if any(v)}, + "sector_decomposition_reproduces_reference": ok, + } + out = Path(args.out) if args.out else Path("results/probe635-sector-map/latest.json") + out.parent.mkdir(parents=True, exist_ok=True) + payload_path = out if out.suffix == ".json" else out.with_suffix(".json") + if payload_path.exists(): + prior = json.loads(payload_path.read_text()) + prior.setdefault("runs", []).append(payload) + payload_path.write_text(json.dumps(prior, indent=1)) + else: + payload_path.write_text(json.dumps({"runs": [payload]}, indent=1)) + print(f"wrote {payload_path}") + print("ALL_CHECKS_PASS" if ok else "CHECKS_FAIL") + + +if __name__ == "__main__": + main() diff --git a/tests/test_issue665_joint_rank_wrap.py b/tests/test_issue665_joint_rank_wrap.py new file mode 100644 index 00000000..c1652b76 --- /dev/null +++ b/tests/test_issue665_joint_rank_wrap.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""#665 joint rank x wrap-label census tests (axis L=2,3 — seconds).""" +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +from issue665_joint_rank_wrap_census import census # noqa: E402 + + +def _tuples(result: dict) -> dict: + return { + (r["k"], r["r_black"], r["r_white"], r["wrap_black"], r["wrap_white"], + r["label5_black"], r["label5_white"]): r["count"] + for r in result["joint"] + } + + +def test_axis_l2_tripwires_and_support() -> None: + result = census(2) + assert result["tripwire_bernstein"] == "pass" + assert result["tripwire_rb_plus_rw_eq_2"] == "pass" + # rank-pair totals must be exactly the three site classes + assert result["rank_pair_totals"] == {"0,2": 7, "1,1": 4, "2,0": 5} + + +def test_axis_l3_both_same_is_not_rank2() -> None: + result = census(3) + assert result["tripwire_bernstein"] == "pass" + assert result["tripwire_rb_plus_rw_eq_2"] == "pass" + # PR #653 published rank-pair totals, reproduced + assert result["rank_pair_totals"] == {"0,2": 259, "1,1": 162, "2,0": 91} + # both-same splits: rank-1 spirals (6) + rank-2 crosses (91) on the black side + assert result["five_name_by_rank_black"]["both-same"] == [0, 6, 91] + # the question cell both-same x both-same is PURE rank-1 x rank-1 (spirals) + assert result["both-same_x_both-same_by_rank_pair"] == {"1,1": 6} + # coarse both x both coincides with it here (no rank-2 x rank-2 at L=3) + assert result["coarse-both_x_coarse-both_by_rank_pair"] == {"1,1": 6} + # both-two has zero mass at every k + joint = _tuples(result) + assert not any(k[5] == "both-two" or k[6] == "both-two" for k in joint) + # D-carrying cells are exactly neither x both (rank 0,2) and both x neither + # (rank 2,0) — rank-2 sits in the exclusive-cross mismatch, never in bs x bs + d_cells = {k: v for k, v in joint.items() + if (k[3] == "neither") != (k[4] == "neither")} + assert all(k[1:3] == (0, 2) for k in d_cells if k[3] == "neither") + assert all(k[1:3] == (2, 0) for k in d_cells if k[4] == "neither") + + +if __name__ == "__main__": + test_axis_l2_tripwires_and_support() + test_axis_l3_both_same_is_not_rank2() + print("ALL_CHECKS_PASS")