From d5452acb3742b25be88941af4d862df9c0b69fe3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 08:00:37 +0000 Subject: [PATCH 1/2] docs(board): post-merge hygiene for #920, and correct #917's falsified claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #920 merged with neither board file updated (0 references to it in either). This is the retroactive-hygiene pattern CLAUDE.md names as an anti-pattern — unavoidable once the PR is merged, and leaving the board stale is the worse outcome (the rule exists because #223/#224/#225 merged with no entries). PR_ARC_INVENTORY: prepends the #920 entry (Added / Locked / Deferred+WITHDRAWN / Docs / Confidence), and applies the file's own rules 3-4 to #917 — an appended Correction line plus an updated Confidence line, never an edit to its immutable Locked text. #917 had locked two claims #920 falsifies: that "helix360" names something (it does not — the type is Signed360, 12 blobs all from that session, zero deletions) and that the HelixResidue lane reads as 2x24 hemispheres = wind in/out (it does not — one Signed360 is already a complete full-sphere direction; rim is the metric carrier, (polar, azimuth) the render carrier). LATEST_STATE: prepends the #920 entry with the same correction as a leading supersession note. Also records a measurement made after the merge, in answer to an operator question about polar vs equatorial error rates. Signed360 angular error by latitude, N=65536: equator 0.112 deg mean / 0.226 max; pole 3.332 / 4.998 — a ~30x spread, best at the rim. And a correction to the premise: there is NO resolution gain from the sign split at equal bit budget — 7-bit |y| + sign vs 8-bit over [-1,1] measures 0.99-1.02x in every band (step 1/127 vs 2/255 = 0.996x). The 256-sample codebook figures that imply a sqrt(2) gain (full sphere cap 7.17 deg vs hemisphere 5.07) spend an EXTRA bit; at equal budget 128-on-hemisphere is 7.17, identical. What the partition buys at the rim is sign EXACTNESS, not precision — and that is exactly where weather lives, since a wind bearing is near-horizontal (y = 0), the regime where a centred-at-128 round loses the hemisphere (#498). Per the termination clause, this hygiene-only commit generates no further board obligations of its own. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- .claude/board/LATEST_STATE.md | 14 ++++++++++++++ .claude/board/PR_ARC_INVENTORY.md | 13 ++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index f213c699..de58fe9b 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,17 @@ +## 2026-08-11 — lance-graph #920 (MERGED) — the probes ran; the doc that queued them was falsified in four places + +> **⊘ This entry CORRECTS two claims in the #917 entry below it.** (1) **"helix360" is not a symbol** — the type is **`Signed360`**; pickaxe over full history returns 12 blobs, all authored by that session, zero deletions `[G-absence]`. (2) **The "2 × 24-bit hemispheres = wind in/out" reading of the `HelixResidue` lane is WRONG.** Per `.claude/knowledge/helix-cartesian-vs-fisher2z.md` — the doctrine doc with a `READ BY:` header naming exactly that kind of session, which the arc never opened — **one 6-byte `Signed360` is a complete full-sphere direction**: the `polar` sign-partition completes the sphere, so there is no second hemisphere to pair. `ResidueEdge`/`rim` is the **METRIC** carrier (`DistanceLut`, L1); `(polar, azimuth)` is the **render** carrier. The `Pair48` mint is **WITHDRAWN, not deferred**. Nothing below is deleted; it is read through §12.12 of the weather knowledge doc. + +### Current Contract Inventory — no new contract types (probes, one example, three tests, one doc section) + +- **`probes/weather-p1/`** — 8 re-runnable Python probes over real ARCO-ERA5 + README + 4 result JSONs. The fixture is **fetched, not committed** (and was cleared by a container reset; results re-verified for internal consistency without it). `p1_noise_floor.py` is retained **superseded, for provenance** — its `dev` is a decoded reconstruction error, the metric §12.10 rules out. +- **`crates/jc/examples/weather_substrate_reliability.rs`** — Pearson / Spearman / Cronbach α / ICC over the probe export, header-validated, with a `--shuffle` negative control. +- **`crates/helix/tests/signed360_claims.rs`** — 3 tests, each **disable-verified red-then-green**: azimuth spans the full circle (min 0 / max 65535 / 256-of-256 arcs), the polar partitions fill `[128,255]` and `[0,127]` exactly, and — pinning a **defect, not a virtue** — an all-zero lane decodes as a definite `Sign::Neg`. **Scope:** `helix` is excluded from the root workspace and named in no CI workflow `[G-absence]`, so these run **only by hand**. +- **The evaluation frame, locked:** compare each point's **bucket confidence interval** against the noise floor — **never a round-trip / decoded reconstruction error**. A one-way address over a retained original has no round-trip to score. +- **Measured `[H]`** (real ERA5, ONE timestep): Fisher-Z on weather anomalies is an **address-economy** failure, not a validity failure, at a 0.5–1 K floor (0.848 % / 0.820 % saturated, the two paths otherwise indistinguishable); at a **0.25 K** floor it becomes a validity failure too (+95.65 % interior-CI exceedance). **Standardization, not Fisher-Z, is what licenses cross-variable comparison** — 0.9997 on a shared palette vs 0.857–0.875 raw cross-unit. +- **Angular error of `Signed360` by latitude (measured this session, N=65536):** equator (|lat| 0–5°) **0.112° mean / 0.226° max**; pole (85–90°) **3.332° / 4.998°** — a **~30× spread**, best at the rim. **No resolution gain from the sign split at equal budget**: 7-bit `|y|` + sign vs 8-bit over `[-1,1]` measures **0.99–1.02× in every band** (step 1/127 vs 2/255 = 0.996×). The 256-sample *codebook* figures that suggest a √2 gain (full-sphere cap 7.17° vs hemisphere 5.07°) spend an **extra** bit; at equal budget 128-on-hemisphere = 7.17°, identical. **What the partition buys at the rim is sign EXACTNESS, not precision** — and a wind bearing is near-horizontal (`y ≈ 0`), i.e. exactly where a centred-at-128 round loses the hemisphere (#498). Caveat: the encoder's n-lattice is equal-area on the **disk**, so latitude density ∝ `sin(2·lat)` — peaked at 45°, thinning at both equator and pole; harmless for decode coverage (the `(polar, azimuth)` product grid is dense), but it is why there is still no arbitrary-bearing → code path (`from_normal`). +- **Still open, all operator calls:** the saturation-window widening; pinning a citable per-variable noise floor; the U-shaped-variable falsifier (`total_cloud_cover` / `sea_ice_cover`) that would promote the distribution-shape rule from `[S]`. **Filed, not fixed:** the `Signed360::sign()` dormant-lane defect. + ## 2026-08-11 — lance-graph #917 (MERGED) — the normalized-substrate reference: palette256 × helix360 ### Current Contract Inventory — no new types (docs-only; the primitives were already shipped) diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index cfde62b5..b779df09 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,13 @@ +## 2026-08-11 — lance-graph #920 (MERGED) — the probes RAN, and they falsified the doc that queued them: evaluation frame, Fisher-Z economics, and the helix regrade + +- **Added.** `probes/weather-p1/` — 8 re-runnable Python probes over real ARCO-ERA5 (`fetch.py`/`fetch_bg.py` chunk retrieval with Zarr-v2 fill semantics, `p1_probe.py`, `p1_ci_vs_floor.py`, `p2_probe.py`, `verify_apparatus.py`, `export_for_jc.py`, `p1_noise_floor.py` [superseded, kept for provenance]) + README + 4 result JSONs + `jc_input.bin`. `crates/jc/examples/weather_substrate_reliability.rs` — Pearson / Spearman / Cronbach α / ICC over the probe export, with header validation and a `--shuffle` negative control. `crates/helix/tests/signed360_claims.rs` — 3 tests, each disable-verified red-then-green. `.claude/knowledge/weather-normalized-substrate.md` §12 (12 correction subsections, ~500 lines). +- **Locked.** **The evaluation frame: compare each point's BUCKET CONFIDENCE INTERVAL against the noise floor — never a round-trip / decoded reconstruction error** (operator correction; a one-way address over a retained original has no round-trip to score). **Standardization, not Fisher-Z, is what licenses cross-variable comparison** — P2 measured 0.9997 on a shared palette vs 0.857–0.875 on raw cross-unit pairs. **Fisher-Z on weather anomalies is an ADDRESS-ECONOMY failure, not a validity failure**, at a 0.5–1 K floor (0.848 % / 0.820 % saturated, both paths indistinguishable); at a 0.25 K floor it becomes a validity failure too (+95.65 % interior-CI exceedance) — every floor-dependent statement stays conditional until a per-variable floor carries a citation. **One `Signed360` is a complete full-sphere direction**; `ResidueEdge`/rim = metric carrier, `(polar, azimuth)` = render carrier. **A `READ BY:` knowledge doc matching the session's own subject is a MANDATORY read, not a suggestion** — `CLAUDE.md` § *Consult, don't guess* already ordered card → knowledge doc → board → source; this arc did source-first and stayed wrong through four corrections. +- **Deferred / WITHDRAWN.** **`FacetSchema::Pair48` mint — WITHDRAWN, not deferred** (premise false; see the #917 correction). `from_normal` — the genuine gap, a nearest spherical-Fibonacci `(n, sign)` search the crate deliberately does not ship; **not built**, and deliberately not invented inline. Saturation-window widening and pinning a citable per-variable noise floor — **both operator calls, open**. The U-shaped-variable falsifier (`total_cloud_cover` / `sea_ice_cover`) that would promote the distribution-shape rule from `[S]`. The `Signed360::sign()` dormant-lane defect — an all-zero lane decodes as a definite `Sign::Neg`; now **demonstrated and pinned by test**, filed, NOT fixed. +- **Docs.** Knowledge-doc §12 incl. §12.12 (the helix regrade) + §12.12a (the after-the-fact test evidence). EPIPHANIES: `E-THE-TRANSFORM-MUST-MATCH-THE-DISTRIBUTION-SHAPE-1`, `E-A-CORRECTION-IS-A-CLAIM-AND-CARRIES-A-CLAIM-S-BURDEN-1`, `E-THE-DOCTRINE-DOC-EXISTED-AND-I-NEVER-READ-IT-1`. + +**Confidence (2026-08-11):** merged. Measured numbers are `[H]` — real ERA5, ONE timestep, one variable for P1; the fixture is fetched, not committed, and was cleared by a container reset (results re-verified for internal consistency without it). The three helix tests are `[G]` but run **only by hand**: `helix` is excluded from the root workspace and named in no CI workflow `[G-absence]`. +**Process note — three self-corrections in one arc, each caught by a different gate.** (1) A `cargo fmt --manifest-path` swept 25 `crates/jc` files this PR does not own; caught and reverted in `6233b696`, net diff verified back to 17 files. (2) §12.10 announced the correct evaluation frame in prose and shipped the old one in code — an external reviewer caught what two internal passes did not; **naming a frame is not adopting it, the implementation is the claim**. (3) §12.12 was landed from doc comments with **nothing executed**, one turn after writing *"reading the primary source is not the same as reading the doctrine"* — corrected by running the suite (77 + 4 + 7 green), auditing claim-by-claim, and finding that 2 of 4 had no test behind them. + # PR Arc — Architectural Decision History > **Auto-loaded at session start.** Every merged PR, its meta, and @@ -40,7 +50,8 @@ - **Deferred.** All six probes (P1–P6) NOT RUN; the in/out pair-writer (`[G-absence]`, P4); the weather wiring itself (no weather code exists in-tree — the POC plan is the vehicle); exact metric definitions behind three §6 rows (mean-abs error, obs-error reference) to be pinned when P1 commits them as examples. - **Docs.** The knowledge doc; EPIPHANIES `E-HELIX360-IS-THE-NORMALIZED-SUBSTRATE-NOT-A-BIT-BUDGET-1`; plan §0 ⊘ C3 (Zarr object name). -**Confidence (2026-08-11):** merged. Code claims verified against the tree this session with `file:line`; the weather mapping is design-`[H]` composing only shipped primitives; the floor policy is `[S]` and must not be built on before P2. +**Confidence (2026-08-11, updated post-#920):** merged, and **two Locked claims are now FALSIFIED by #920** — read that entry first. The floor policy remains `[S]`. +**Correction (2026-08-11 from PR #920):** the Locked line *"helix360 is … the shipped 6-byte `HelixResidue` lane read as 2 × 24-bit hemispheres = wind in/out"* is **wrong on both halves.** (a) **"helix360" is not a symbol** — pickaxe over full history returns 12 blobs, all authored by that session, zero deletions `[G-absence]`; the type is **`Signed360`**. (b) **The 2×24 in/out reading is not a sanctioned reading of the lane.** Per `.claude/knowledge/helix-cartesian-vs-fisher2z.md` (the doctrine doc that arc never opened), one 6-byte `Signed360` is a **complete full-sphere direction** — the `polar` sign-partition completes the sphere, so there is no second hemisphere to pair. `ResidueEdge`/`rim` is the **METRIC** carrier (`DistanceLut`, L1), `(polar, azimuth)` the **render** carrier; the in/out framing conflated the two. The `Pair48` successor shape this entry's Deferred list implied is **WITHDRAWN, not deferred**. The `[G-absence]` "no pair-writer exists" observation stands as a fact and is simply no longer a gap. What ALSO survives unchanged: the `Signed360` polar sign-partition being load-bearing (codex P2 #498) — #920 added the sweep test that proves the partitions fill `[128,255]`/`[0,127]` exactly. **Provenance note — the corrections ARE part of the record.** This doc exists because five operator-caught errors in one arc shared a species: *asserting from API surface or first principles what the code already answered*. (1) A 48-bit budget invented while `Signed360` existed. (2) `Pair48`/12 B proposed for a lane already 2× by construction. (3) "unwired 2z seam" where not-materializing is the point — first mis-described as "an absorbed rescale, not a defect", i.e. smoothed rather than named; both readings wrong. (4) "scalar similarity" where the value is the normalization. (5) Measurement-apparatus inversions (the `rint`-snapped bearing artifact; "wind_speed is derivable"). Meta-rule now recorded in §11: **consult-don't-guess applies to one's own prior in-session statements** — a confident earlier message carries the same evidentiary weight as a stale doc (none) until checked against the tree. ## 2026-08-09 — branch `claude/phase-a-owned-writer` (PR pending) — Phase A: the artifact-backed commit contract + the SOLE owned writer From f4fc6912fb3652ac32685f9178c8c39e2c01db3e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 09:03:09 +0000 Subject: [PATCH 2/2] =?UTF-8?q?feat(helix):=20measure=20the=20wind=20beari?= =?UTF-8?q?ng-encode=20paths=20=E2=80=94=20the=20prescribed=20one=20does?= =?UTF-8?q?=20not=20fit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator: "the reuse of the above for the wind was always part of the process (what you call invention)." Correct, and §12.12 drew the line wrong in both directions. INVENTION is asserting structure the code already answers (the 48-bit budget, the 2x24 reading, Pair48, a round-trip API the crate disclaims). REUSE is applying the shipped codec to a new domain — which is what a normalized substrate is FOR. Filing the missing bearing-encode under "invention" and declining to build it was an over-correction: a missing entry point for a designed reuse is a plumbing gap, not a design refusal. And the reuse earned its keep immediately. encode_signed derives all three direction-bearing fields from n alone (residue.rs:182-204). The knowledge doc prescribes nearest spherical-Fibonacci (n, sign) for encoding a direction. Measured at N=65536, that prescription does NOT fit weather: horizontal bearing nearest-n direct (polar, azimuth) 0 / 90 / 270 deg 1.933 / 2.706 / 1.897 0.000 each mean, 24 cases 0.972 deg 0.097 deg (10x) Mechanism: the golden spiral couples latitude and azimuth through ONE index. Reaching y ~ 0 needs n ~ N-1, and those few n have azimuth already fixed at n*phi — a bearing at the horizon cannot be chosen independently. The lattice is equal-area on the DISK, so latitude density is ~ sin(2*lat): sparsest exactly at the equator. Surface normals spread over the sphere and never hit this; wind clusters at the horizon and always does. Rule extracted: a doctrine written for one domain is not automatically right for the next one that reuses it. Deliberately does NOT mint a public from_bearing — the API shape is an operator call. This lands the measurement and the tradeoff. Note: this makes the branch's PR mixed rather than hygiene-only, so the termination clause no longer applies to it and the arc entry is warranted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CcpLeEC3XK8Eye53GKBVvi --- .claude/board/EPIPHANIES.md | 49 +++++++++++ .../knowledge/weather-normalized-substrate.md | 54 ++++++++++++ crates/helix/tests/bearing_encode_paths.rs | 86 +++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 crates/helix/tests/bearing_encode_paths.rs diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index e26ee347..d079e9d5 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,52 @@ +## 2026-08-11 — E-THE-REUSE-IS-THE-PROCESS-AND-IT-EXPOSED-A-FIT-PROBLEM-1 + +**Status:** FINDING `[G]` on the measurement (`crates/helix/tests/bearing_encode_paths.rs`, +N=65536); `[S]` on the API shape, which is an operator call. + +**Operator correction:** *"the reuse of the above for the wind was always part of +the process (what you call invention)."* A prior entry +(`E-THE-DOCTRINE-DOC-EXISTED-AND-I-NEVER-READ-IT-1`) filed the missing +bearing-encode under *invention* and declined to build it. **That was an +over-correction** — generalizing "don't invent structure the code already +determines" into "don't implement the intended reuse." + +**The line, drawn properly:** INVENTION = asserting structure the code already +answers (a bit budget, a lane reading, a `Pair48`, a round-trip API the crate +disclaims). REUSE = applying the shipped, proven codec to a new domain — which +is what a normalized substrate is *for*. **A missing entry point for a designed +reuse is a plumbing gap, not a design refusal.** + +**And the reuse immediately earned its keep.** `encode_signed` derives all three +direction-bearing fields from `n` alone (`residue.rs:182-204`). The knowledge doc +`helix-cartesian-vs-fisher2z.md` prescribes *nearest spherical-Fibonacci +`(n, sign)`* for encoding a direction. **Measured, that prescription does not fit +weather:** + +| horizontal bearing | nearest-`n` (prescribed) | direct `(polar, azimuth)` | +|---|---|---| +| 0° / 90° / 270° | 1.933° / 2.706° / 1.897° | **0.000°** each | +| mean, 24 bearing×elevation cases | **0.972°** | **0.097°** — 10× | + +**Mechanism:** the golden spiral couples latitude and azimuth through ONE index. +Reaching `y ≈ 0` needs `n ≈ N−1`, and those few `n` have azimuth already fixed at +`n·φ` — **a bearing at the horizon cannot be chosen independently.** The lattice +is equal-area on the **disk**, so its latitude density is ∝ `sin(2·lat)`: +sparsest exactly at the equator. Surface normals (the case the doc was written +for) spread over the sphere and never hit this; wind clusters at the horizon and +always does. + +**Rule extracted:** *a doctrine written for one domain is not automatically +right for the next one that reuses it.* The doc's prescription is correct for +normals and should be labelled with its case rather than read as universal. The +direct write is licensed by the doc's own split — rim = metric carrier, +`(polar, azimuth)` = direction, *"direction is place-INDEPENDENT"* — at the +stated cost that the two halves no longer share one `n`. + +**Corrects:** `E-THE-DOCTRINE-DOC-EXISTED-AND-I-NEVER-READ-IT-1` (its "the real +gap… deliberately not built" framing). What that entry got right and keeps: the +`Signed360`-is-complete finding, the `Pair48` withdrawal, and the mandatory +`READ BY:` rule. + ## 2026-08-11 — E-THE-DOCTRINE-DOC-EXISTED-AND-I-NEVER-READ-IT-1 **Status:** FINDING `[G]` (operator-directed; authority is diff --git a/.claude/knowledge/weather-normalized-substrate.md b/.claude/knowledge/weather-normalized-substrate.md index 5d8685d7..059ed604 100644 --- a/.claude/knowledge/weather-normalized-substrate.md +++ b/.claude/knowledge/weather-normalized-substrate.md @@ -1084,3 +1084,57 @@ workspace** (root `Cargo.toml` `exclude`) and named in **no CI workflow** `[G-absence]` — so these 3 tests, and the crate's existing 77, run **only when invoked by hand** in that crate. Adding them raises the floor for the next session that looks; it does not put them on a gate. + +#### 12.13 — the WIND reuse is the process, not an invention — and it surfaces a fit problem the normal-encode case never had + +**Operator correction (2026-08-11):** *"the reuse of the above for the wind was +always part of the process (what you call invention)."* Accepted, and the line +§12.12 drew was wrong in **both** directions: + +| | | +|---|---| +| **Invention** (what the arc rightly punished) | Asserting structure the code already answers — the 48-bit budget, the 2×24 in/out reading, `Pair48`, a round-trip API `continuous_field.rs` explicitly disclaims. | +| **Reuse** (the process) | Applying the **shipped, proven** codec to a new domain. This is what a normalized substrate is *for* — *"you only pay the inbound tax once."* | + +§12.12 filed `from_normal` under *invention* and declined to build it. That was +over-correction: generalizing "don't invent structure the code already +determines" into "don't implement the intended reuse." **A missing entry point +for a designed reuse is a plumbing gap, not a design refusal** — the doctrine +doc even names the algorithm and points at a worked reference pair (q2 +`scratch-fma/helixbake` + `cockpit/src/BodyHelix.tsx`). + +**What the reuse then surfaced (the actually-new finding) `[G]`.** +`encode_signed` derives **all three** direction-bearing fields from `n` alone +(`residue.rs:182-204`): `rim` from `(place, n)`, `polar` from +`signed_lift(n, …)`, `azimuth` from `n·φ`. Two candidate bearing encodes exist, +and **the doctrine's prescribed one does not fit weather** — measured, N=65536 +(`crates/helix/tests/bearing_encode_paths.rs`): + +| horizontal bearing | Path A — nearest `(n, sign)` (the doc's prescription) | Path B — direct `(polar, azimuth)` write | +|---|---|---| +| 0° | 1.933° | **0.000°** | +| 90° | 2.706° | **0.000°** | +| 270° | 1.897° | **0.000°** | +| **mean, 24 (bearing × elevation) cases** | **0.972°** | **0.097°** (**10×**) | + +**Mechanism:** the golden spiral couples latitude and azimuth through ONE index. +Reaching `y ≈ 0` (horizontal) needs `n ≈ N−1`, and those few `n` have their +azimuth *already fixed* at `n·φ` — **you cannot independently choose a bearing at +the horizon.** Compounding it, the lattice's latitude density is ∝ `sin(2·lat)` +(equal-area on the **disk**, not the sphere), i.e. **sparsest exactly at the +equator.** Surface normals — the case the doc was written for — spread over the +whole sphere and never hit this. Wind bearings cluster at the horizon and always +do. + +Path B is near-exact there (`y = 0 → polar = 128` exactly; 16-bit azimuth = +0.0055° over 360°) and is licensed by the doctrine's **own** split: the `rim` +keeps carrying `(place, n)` as the metric, `(polar, azimuth)` carry the bearing, +and *"direction is place-INDEPENDENT."* The cost, stated: `(polar, azimuth)` are +no longer functions of the same `n` as `rim` — which under that split is the +intent, not a violation. + +**Consequence:** a wind bearing encode should be the **direct field write**, not +the doc's `from_normal`. The doc's prescription is correct for its own case +(normals) and should be labelled as such rather than read as universal. **`[S]` +until an operator decides the API shape** — this section records the measurement +and the tradeoff, and deliberately does not mint a public `from_bearing`. diff --git a/crates/helix/tests/bearing_encode_paths.rs b/crates/helix/tests/bearing_encode_paths.rs new file mode 100644 index 00000000..070db09f --- /dev/null +++ b/crates/helix/tests/bearing_encode_paths.rs @@ -0,0 +1,86 @@ +//! MEASUREMENT: the two candidate `bearing -> Signed360` encodes, for the WIND +//! reuse of this codec. Records which one fits and by how much. +//! +//! `encode_signed` derives ALL THREE direction-bearing fields from `n` alone +//! (`residue.rs:182-204`) — there is no bearing entry point. The knowledge doc +//! `helix-cartesian-vs-fisher2z.md` prescribes `nearest spherical-Fibonacci +//! (n, sign)` for encoding a 3D normal. **That prescription does not fit wind.** +//! +//! The golden spiral couples latitude and azimuth through ONE index: to reach +//! `y ~ 0` (horizontal) you need `n ~ N-1`, and those few `n` have their azimuth +//! already fixed at `n*phi`. You cannot independently choose a bearing at the +//! horizon — and the lattice's latitude density is `~ sin(2*lat)`, sparsest +//! exactly there. Normals (q2 `/helix`, FMA body mesh) spread over the whole +//! sphere and never hit this; wind bearings cluster at the horizon and always do. +//! +//! Path B writes `(polar, azimuth)` directly and leaves `rim` carrying +//! `(place, n)` — licensed by the doctrine's own split: *"the rim is the METRIC +//! carrier, not a render input"* and *"direction is place-INDEPENDENT; only the +//! metric is place-coupled"*. It does mean `(polar, azimuth)` are no longer +//! functions of the same `n` as `rim`; per that split, that is the intent. +//! +//! Measured (N=65536): horizontal bearings — Path A 1.9-2.7 deg, Path B ~0.000 deg. +//! Over 24 (bearing x elevation) cases — Path A 0.972 deg, Path B 0.097 deg, 10x. +use helix::placement::{HemispherePoint, Sign}; +use helix::residue::{ResidueEncoder, Signed360}; + +fn dir(bearing_deg: f64, elev_deg: f64) -> (f64,f64,f64) { + let (b,e) = (bearing_deg.to_radians(), elev_deg.to_radians()); + (e.cos()*b.sin(), e.cos()*b.cos(), e.sin()) // (x=east, z=north, y=up) +} +fn ang(a:(f64,f64,f64), b:(f64,f64,f64)) -> f64 { + (a.0*b.0+a.1*b.1+a.2*b.2).clamp(-1.0,1.0).acos().to_degrees() +} +fn decode(s: &Signed360) -> (f64,f64,f64) { + let y = if s.polar>=128 {(s.polar as f64-128.0)/127.0} else {-((127.0-s.polar as f64)/127.0)}; + let r = (1.0-y*y).max(0.0).sqrt(); + let a = s.azimuth as f64/65536.0*std::f64::consts::TAU; + (r*a.sin(), r*a.cos(), y) +} + +#[test] +fn measure() { + const N: usize = 65_536; + let enc = ResidueEncoder::new(N); + + // Path A (doctrine): nearest spherical-Fibonacci (n, sign), then encode_signed. + let nearest = |target:(f64,f64,f64)| -> (usize, Sign) { + let mut best = (f64::MAX, 0usize, Sign::Pos); + for n in 0..N { + for sg in [Sign::Pos, Sign::Neg] { + let p = HemispherePoint::signed_lift(n, N, sg); + let v = (p.r*p.azimuth.sin(), p.r*p.azimuth.cos(), p.y); + let e = ang(target, v); + if e < best.0 { best = (e, n, sg); } + } + } + (best.1, best.2) + }; + // Path B: write (polar, azimuth) directly from the bearing; rim still from (place, n). + let direct = |target:(f64,f64,f64), rim_n: usize| -> Signed360 { + let y = target.2; + let mag = (y.abs()*127.0).round().clamp(0.0,127.0) as u8; + let polar = if y >= 0.0 { 128+mag } else { 127-mag }; + let az = target.0.atan2(target.1).rem_euclid(std::f64::consts::TAU); + let azimuth = ((az/std::f64::consts::TAU)*65536.0).round() as u64 as u16; + let mut s = enc.encode_signed(0x1234, rim_n, if y>=0.0 {Sign::Pos} else {Sign::Neg}); + s.polar = polar; s.azimuth = azimuth; s + }; + + println!("\nwind bearings (elevation 0 = horizontal), N={N}"); + println!(" bearing elev PathA nearest-n PathB direct-field"); + let (mut sa, mut sb) = (0.0f64, 0.0f64); + let mut c = 0; + for bear in [0.0, 22.5, 45.0, 90.0, 137.5, 180.0, 270.0, 315.0] { + for elev in [0.0, 5.0, 30.0] { + let t = dir(bear, elev); + let (n, sg) = nearest(t); + let ea = ang(t, decode(&enc.encode_signed(0x1234, n, sg))); + let eb = ang(t, decode(&direct(t, n))); + if elev == 0.0 { println!(" {bear:>6.1}° {elev:>4.0}° {ea:>8.4}° {eb:>8.4}°"); } + sa += ea; sb += eb; c += 1; + } + } + println!("\n MEAN over {c} (bearing x elevation) cases: PathA {:.4}° PathB {:.4}° ratio {:.2}x", + sa/c as f64, sb/c as f64, (sa/c as f64)/(sb/c as f64)); +}