diff --git a/notes/issue-FN-vs-M-20260908.md b/notes/issue-FN-vs-M-20260908.md new file mode 100644 index 00000000..fad597bf --- /dev/null +++ b/notes/issue-FN-vs-M-20260908.md @@ -0,0 +1,123 @@ +# Issue #678 — Is `F_N = (1+M_N)/2` an identity of the rank triple, or only a coding convention? + +**Date:** 2026-09-08 +**Assign:** local (ASSIGNED_MACHINE: local). Parent #650. Follow-ups: PR #675 Fact 1, PR #676 A′, #613's `F_N = E[r_G]/2`. +**Constraint compliance:** read committed JSON only, no new census, no L=5, no Huawei, no `docs/STATUS.md`, no ticket closures, #636 not funded. Integers/rationals only — every check below is `fractions.Fraction` arithmetic; no float appears in any exact artifact or intermediate. + +**Machine-checkable reader:** `scripts/probe/fn_identity_reader.py` (added in this PR) re-runs checks A–D from the committed artifacts exactly; checks E–F additionally run when PR #668's `results/issue665-joint-rank-wrap/axis-L{3,4}.json` is present in the tree (it is not yet on main — those two checks were run this session against the PR #668 branch artifacts via `git show pr/668:...` and pass bit-for-bit). + +--- + +## 0. Verdict (one page) + +**`F_L = (1 + M_L)/2` is a theorem (an exact polynomial identity of the rank triple), not a coding convention — on the committed axis L=3,4 tables.** Concretely, coefficientwise over the power basis with `Fraction` arithmetic, both at L=3 (N=9) and L=4 (N=16): + +1. `P_0 + P_1 + P_2 = 1` (the rank layers partition the Bernstein measure); +2. `M_L = P_2 − P_0` — reproduces the committed `M_poly_L{3,4}.json` power coefficients bit-for-bit; +3. `F_L := E[r]/2 = (P_1 + 2 P_2)/2` equals `(1 + M_L)/2` as polynomials; +4. boundaries `F_L(0)=0`, `F_L(1)=1`, and `F_L(1/2) = (1+M_L(1/2))/2` exactly: `F_3(1/2) = 43/128`, `F_4(1/2) = 19011/65536`. + +The reason it *had* to be an identity: `P_0 + P_1 + P_2 = 1` and `E[r] = P_1 + 2P_2` give + +```text +F = E[r]/2 = (P_1 + 2P_2)/2 = (P_0 + P_1 + P_2 + P_2 − P_0)/2 = (1 + M)/2, +``` + +with `M = P_2 − P_0` exactly the #606/root-ledger definition. So Fact 1 (PR #675) and the #612 chart identity are the same `D`-side object at finite L: `D = 1{r=2} − 1{r=0}` has expectation `P_2 − P_0 = M` (both `both-two` and the rank-2×rank-2 cells are empty in the committed tables — PR #668 `five_name_by_rank_black.both-two = [0,0,0]`, `rank_pair_totals` has only `(0,2),(1,1),(2,0)`), and `E[(1+D)/2] = E[r]/2` because `r ∈ {0,1,2}`. No defect in Fact 1's `D` vs the Bernstein `a_k`: `a_k = #cross_k − #none_k` per layer reproduces PR #668's `collapsed_D` and the committed Bernstein integers at both L. + +**Corollary (Q3):** since `F_L` is strictly increasing (committed Sturm: `M'_L > 0` on (0,1), one root), `F_L(p*_L) = (1 + 0)/2 = 1/2` at the unique root `p*_L` of `M_L`, hence `p*_L = Q_L(1/2)`. + +**Reconciliation with W4 (Q3, kept distinct):** `M_L(1/2) ≠ 0` at both L (`−21/64`, `−13757/32768` — committed, exact), so `p*_L = Q_L(1/2) ≠ 1/2`. This is consistent with, and says nothing new about, PR #671's W4 verdict: `M(1/2)` is one pointwise value of the odd polynomial and does not pin the shape `Z`. `p*_L = Q_L(1/2)` is a *location* statement inside the committed chart `F = (1+M)/2`; W4 is a *shape* statement. They do not interact, and this note does not revive "M(1/2) governs the shape". + +--- + +## 1. Sources (read-only, committed artifacts) + +| Artifact | What is taken | +|---|---| +| `results/probe-exact-controls/joints.json` (PR #653 lineage) | `by_L.{3,4}.joint_black_count_rank` — per-`(k, r_b)` configuration counts, 512 / 65 556 configs, `dual_fail_count = 0`, `max_abs_rank_diff = 0` | +| `results/probe-exact-controls/M_poly_L{3,4}.json` (PR #653 lineage, generator `scripts/probe/exact_controls_q_and_m.py`) | committed power-basis `M_coeffs`, `M_half`, `Mp_half`, `sturm_roots_in_01 = 1` | +| `results/issue665-joint-rank-wrap/axis-L{3,4}.json` (PR #668) | per-row `(k, r_black, r_white, wrap, label5, count)` joint rows, `five_name_by_rank_black`, `collapsed_D`, `committed_bernstein`, `rank_pair_totals` | +| `results/probe-exact-controls/latest.json` | `C1_census` rank pairs `(0,2)/(1,1)/(2,0) = 259/162/91` and `36559/19932/9045`; `C3_quantiles` `Q_L(0.5)` | +| `results/homological-balance-exact-torus/latest.json` | `M_half`, `p_L^H` cross-check | + +No artifact was re-enumerated; every integer below is quoted from the above files. + +--- + +## 2. The check (exact, what the reader script does) + +From `joint_black_count_rank` (and independently from PR #668's `joint` rows), for each layer `k` let `c_{k,r}` be the number of configurations with `k` occupied sites and ambient rank `r_b = r`. Each configuration has Lebesgue weight `p^k (1−p)^{N−k}` under the product measure, so the rank masses are the polynomials + +```text +P_r(p) = Σ_k c_{k,r} · p^k (1−p)^{N−k}, r ∈ {0,1,2}, +``` + +expanded to the power basis over `Fraction`. Checks (all coefficientwise, all exact): + +- **(A)** `P_0 + P_1 + P_2 = 1` — i.e. `Σ_r c_{k,r} = C(N,k)` per layer, verified per layer before expansion; +- **(B)** `P_2 − P_0` equals the committed `M_coeffs` of `M_poly_L{L}.json` coefficient for coefficient; +- **(C)** `(P_1 + 2P_2)/2` equals `(1 + M_L)/2` coefficient for coefficient (this *is* Q1); +- **(D)** `F_L(0) = 0`, `F_L(1) = 1`, `F_L(1/2) = (1 + M_L(1/2))/2` evaluated in `Fraction` (no floats); +- **(E)** per-layer `a_k = c_{k,2} − c_{k,0}` equals PR #668's `collapsed_D` and `committed_bernstein`. + +Results: + +| L | N | (A) | (B) | (C) | (D) | F_L(1/2) | (E) | +|---|---|---|---|---|---|---|---| +| 3 | 9 | ✓ | ✓ | ✓ | ✓ | `43/128` | ✓ (`[-1,-9,-36,-78,-90,-36,36,36,9,1]`) | +| 4 | 16 | ✓ | ✓ | ✓ | ✓ | `19011/65536` | ✓ (`[-1,-16,-120,-560,-1812,-4272,-7448,-9424,-7874,-2896,1720,2832,1660,560,120,16,1]`) | + +`F_3(1/2) = 43/128` matches `notes/probe-exact-controls-new-map-20260907.md` §2.5; `F_4(1/2) = 19011/65536` matches `(1 + M_4(1/2))/2 = (1 − 13757/32768)/2`. The earlier `probe-invariant-shape-limit-20260907.md` float `0.29508…` for `F_4(1/2)` is **not** this number; the exact value committed here is `19011/65536 = 0.290084…` — that note's `0.29508` appears to be a transcription of a different quantity and should not be cited for `F_4(1/2)` (flagged, not edited — that note belongs to PR #676 lineage). + +**Why the identity is structural, not numerological (Q1, the algebra):** per configuration `r_b + r_w = 2` (I1, zero dual-fail violations in the committed tables), so `r_b ∈ {0,1,2}` and + +```text +D := 1{r_b=2} − 1{r_b=0} = r_b − 1 (pointwise, per configuration), +``` + +because both sides are functions of `r_b` alone agreeing on {0,1,2}. Taking expectations: `E[D] = M_L`, and `(1 + D)/2 = r_b/2` gives `F = E[r]/2 = (1+M)/2`. The pointwise coincidence `D = r − 1` is exactly where the *appearance* of convention ends and the theorem begins; PR #675's Fact 1 (`a_k = #cross_k − #none_k`) is its per-layer version and check (E) confirms it bit-for-bit. + +--- + +## 3. The rank triple in five-cell language (Q2) + +From PR #668's `five_name_by_rank_black` (configuration counts; per-rank triples `[rank0, rank1, rank2]`): + +| cell | L=3 | L=4 | rank | +|---|---|---|---| +| `none` (`n×b`, no wrap) | 259 | 36 559 | 0 | +| `x` (one-axis, dir0) | 78 | 9 406 | 1 | +| `y` (one-axis, dir1) | 78 | 9 406 | 1 | +| `both-same` rank-1 (spirals) | 6 | 1 120 | 1 | +| `both-same` rank-2 (exclusive crosses) | 91 | 9 045 | 2 | +| `both-two` | 0 | 0 | — | + +So, exactly: + +```text +P_0 = mass(none), (the (0,2) exclusive-cross cell) +P_1 = mass(x) + mass(y) + mass(spiral), (rank-1: both one-axis cells plus spirals) +P_2 = mass(exclusive cross), (the (2,0) cell inside both-same) +``` + +and this matches the PR #668 `rank_pair_totals` at both L: `x+y+spiral = 78+78+6 = 162 = #(1,1)` and `9406+9406+1120 = 19932 = #(1,1)`; `none = #(0,2)`; exclusive cross = `#(2,0)`. **`P_1` is confirmed to be the rank-1 mass: spirals plus the two one-axis cells** (the issue's guess is right, verified at both L). `both-two` is empty at both L; the `n×b` / `b×n` coarse pairing of #668 carries the same information since `both-same × both-same` is pure `(1,1)` (PR #676 A′), so `d0 + d1` / `b×b` refinements do not alter the rank projection. + +--- + +## 4. Corollary and reconciliations (Q3) + +- **`p*_L = Q_L(1/2)`.** `M'_L > 0` on (0,1) (committed Sturm, `sturm_roots_in_01 = 1` and `Mp_half > 0`), so `F_L` is a strictly increasing bijection [0,1]→[0,1] and `F_L^{-1}(1/2)` is the unique root of `M_L`. Cross-check: committed `C3_quantiles.Q_L(0.5)` equals `p_L^H` bit-for-bit at both L (`0.5865114551126757`, `0.5906721123310283`). +- **`Q_L(1/2) ≠ 1/2`.** `M_L(1/2) = −21/64` (L=3) and `−13757/32768` (L=4), both exact and nonzero, so the median quantile level does not sit at the self-complementing occupation point. This is the site non-self-duality already recorded in the root ledger (`M_L(1/2) ≠ 0`). +- **Not W4.** PR #671's W4 killed "M(1/2) pins shape `Z`". The corollary above is purely a location identity inside the chart `F = (1+M)/2`; it neither uses nor constrains `Z`. The two claims are kept distinct as demanded by the ticket. + +--- + +## 5. Boundaries honored + +- No new census, no re-enumeration: all integers read from committed JSON (`joints.json`, `M_poly_L{3,4}.json`, PR #668 `axis-L{3,4}.json`). +- No L=5 anywhere. No Huawei; `NEED_HUAWEI` not needed. +- No `docs/STATUS.md` edit; #613/#625/#635/#658 not closed; #636 not funded. +- Integers/rationals only: the reader script uses `fractions.Fraction` exclusively; the only floats in this note are the two labeled `Q(0.5)` cross-check strings quoted verbatim from committed JSON. + +Related: #650, #613, #625, #635, #658, #672, #678; PRs #653, #668, #671, #675, #676. diff --git a/scripts/probe/fn_identity_reader.py b/scripts/probe/fn_identity_reader.py new file mode 100644 index 00000000..9eb015cc --- /dev/null +++ b/scripts/probe/fn_identity_reader.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Issue #678 tiny reader — re-verify, exactly, that F_L = (1 + M_L)/2 is a +polynomial identity of the rank triple on the committed axis L=3,4 tables. + +Reads ONLY committed artifacts (no enumeration): + results/probe-exact-controls/joints.json (per-(k, r_b) counts) + results/probe-exact-controls/M_poly_L{3,4}.json (committed M, power basis) + results/issue665-joint-rank-wrap/axis-L{3,4}.json (PR #668 five-cell rows) + +All arithmetic is fractions.Fraction. No floats. Exits nonzero on any failure. + +Checks per L: + A P_0 + P_1 + P_2 = 1 (layer partition) + B P_2 - P_0 == committed M_coeffs (coefficientwise) + C (P_1 + 2 P_2)/2 == (1 + M)/2 (the identity, coefficientwise) + D F(0)=0, F(1)=1, F(1/2) == (1+M(1/2))/2 (Fraction evaluation) + E a_k = #cross_k - #none_k == PR #668 collapsed_D / committed_bernstein + F five-cell: P_1 == x + y + spiral mass; P_0 == none; P_2 == exclusive cross +""" +import json +from fractions import Fraction +from math import comb +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +RES = ROOT / "results" +COMMITTED_F_HALF = {3: Fraction(43, 128), 4: Fraction(19011, 65536)} + + +def counts_from_joints(L): + doc = json.loads((RES / "probe-exact-controls" / "joints.json").read_text()) + d = doc["by_L"][str(L)]["joint_black_count_rank"] + per = {} + for k, v in d.items(): + n, r = map(int, k.split(",")) + per[(n, r)] = per.get((n, r), 0) + v + return per + + +def counts_from_pr668(L): + p = RES / "issue665-joint-rank-wrap" / f"axis-L{L}.json" + if not p.exists(): + return None, None + doc = json.loads(p.read_text()) + per = {} + for row in doc["joint"]: + key = (row["k"], row["r_black"]) + per[key] = per.get(key, 0) + row["count"] + return per, doc + + +def counts_to_power(per, N): + """Power-basis coeffs of P_r(p) = sum_k c_{k,r} p^k (1-p)^{N-k}, per rank.""" + A = [[Fraction(0)] * (N + 1) for _ in range(3)] + for (k, r), c in per.items(): + for j in range(N - k + 1): + sign = -1 if (j & 1) else 1 + A[r][k + j] += c * sign * comb(N - k, j) + return A + + +def ev(a, x): + r = Fraction(0) + for c in reversed(a): + r = r * x + c + return r + + +def check(L): + N = L * L + per_main = counts_from_joints(L) + per_668, doc668 = counts_from_pr668(L) + mdoc = json.loads((RES / "probe-exact-controls" / f"M_poly_L{L}.json").read_text()) + M = [Fraction(e["c"]) for e in mdoc["M_coeffs"]] + M_half = Fraction(mdoc["M_half"]) + + results = [] + for tag, per in (("main", per_main), ("pr668", per_668)): + if per is None: + continue + # A: layer partition + layers_ok = all( + sum(per.get((k, r), 0) for r in range(3)) == comb(N, k) for k in range(N + 1) + ) + A = counts_to_power(per, N) + one = [Fraction(1) if m == 0 else Fraction(0) for m in range(N + 1)] + B = [A[2][m] - A[0][m] for m in range(N + 1)] == M + F1 = [(A[1][m] + 2 * A[2][m]) / 2 for m in range(N + 1)] + F2 = [(Fraction(1 if m == 0 else 0) + M[m]) / 2 for m in range(N + 1)] + C = F1 == F2 + D = ( + ev(F1, Fraction(0)) == 0 + and ev(F1, Fraction(1)) == 1 + and ev(F1, Fraction(1, 2)) == (1 + M_half) / 2 + ) + results.append((tag, layers_ok, B, C, D)) + + E = None + F5 = None + if doc668 is not None: + ak = [per_668.get((k, 2), 0) - per_668.get((k, 0), 0) for k in range(N + 1)] + E = ak == doc668["collapsed_D"] == doc668["committed_bernstein"] + fn = doc668["five_name_by_rank_black"] + rp = doc668["rank_pair_totals"] + F5 = ( + fn["none"][0] == rp["0,2"] + and fn["both-same"][2] == rp["2,0"] + and fn["x"][1] + fn["y"][1] + fn["both-same"][1] == rp["1,1"] + and fn["both-two"] == [0, 0, 0] + ) + + ok = all((a and b and c and d) for _, a, b, c, d in results) and E is not False and F5 is not False + return ok, results, E, F5, ev( + [(Fraction(1 if m == 0 else 0) + M[m]) / 2 for m in range(N + 1)], + Fraction(1, 2), + ) + + +def main(): + all_ok = True + for L in (3, 4): + ok, results, E, F5, fhalf = check(L) + all_ok &= ok + status = "OK" if ok else "FAIL" + print(f"L={L}: {status} F_L(1/2) = {fhalf}") + for tag, a, b, c, d in results: + print(f" [{tag}] A(layers)={a} B(M==P2-P0)={b} C(F==(1+M)/2)={c} D(boundaries)={d}") + if E is not None: + print(f" [pr668] E(a_k collapse)={E} F(five-cell P1=x+y+spiral)={F5}") + assert fhalf == COMMITTED_F_HALF[L], f"F_L(1/2) mismatch at L={L}" + print("ALL CHECKS EXACT-OK" if all_ok else "FAILURES PRESENT") + raise SystemExit(0 if all_ok else 1) + + +if __name__ == "__main__": + main()