From c7cdb5c36ed93dc250f9f9425a6af62f689f35a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 20:51:56 +0000 Subject: [PATCH 1/4] probe(recipe): sector permutation on ndarray::simd::PhaseLut; all-order-statistics timing; shipped checked_affine cross-check Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .../examples/algebraic_recipe_probe.rs | 132 +++++++++++++++++- 1 file changed, 128 insertions(+), 4 deletions(-) diff --git a/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs b/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs index c46a655bb..9941246ca 100644 --- a/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs +++ b/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs @@ -24,7 +24,7 @@ //! ``` use ndarray::hpc::rolling_floor::rank_per_10000; -use ndarray::simd::{masked_group_cross_power_sums_i32, CrossPowerSums}; +use ndarray::simd::{masked_group_cross_power_sums_i32, phase_lut_4096, CrossPowerSums, PhaseLut}; use std::collections::HashMap; use std::f64::consts::TAU; use std::hash::{BuildHasher, Hasher}; @@ -515,6 +515,18 @@ impl SectorPerm { let j = self.perm[usize::from(f >= 0.5)][rank]; (TAU * ((f + j as f64) / m)).cos() } + + /// [`value`](Self::value) with the cosine from `ndarray::simd::PhaseLut`: + /// the angle `(f + j)/m` turns becomes a `u32` phase and one + /// interpolated table lookup. + #[inline] + fn value_lut(&self, rank: usize, phi_t: f64, lut: &PhaseLut) -> f64 { + let m = self.m as f64; + let f = (phi_t * m).rem_euclid(1.0); + let j = self.perm[usize::from(f >= 0.5)][rank]; + let p = ((f + j as f64) / m * TURN) as u64 as u32; + f64::from(lut.lerp(p).0) + } } // ───────────────────────── oracle ───────────────────────── @@ -1050,6 +1062,12 @@ impl IAff { } } +fn fold_cross(xs: &[i32], ys: &[i32], mask: &[u64], keys: &[u32]) -> CrossPowerSums { + let mut out = [CrossPowerSums::default(); 1]; + masked_group_cross_power_sums_i32(mask, keys, xs, ys, &mut out); + out[0] +} + fn fold_ndarray(xs: &[i32], ys: &[i32], mask: &[u64], keys: &[u32]) -> S2 { let mut out = [CrossPowerSums::default(); 1]; masked_group_cross_power_sums_i32(mask, keys, xs, ys, &mut out); @@ -1156,6 +1174,12 @@ fn population() { assert_eq!(a_res, a2); assert_eq!(a_res, b_res); assert_eq!(a_res, c_res); + // The shipped method (ndarray #344) gives the same summary. + let shipped: S2 = fold_cross(&xs, &ys, &mask, &keys) + .checked_affine(comp.m, [comp.t.0, comp.t.1]) + .expect("in range") + .into(); + assert_eq!(shipped, c_res, "CrossPowerSums::checked_affine"); println!( " {n:>9} {ns_a:>10.2} {ns_a2:>10.2} {ns_b:>10.2} {ns_c:>10.2} {ns_w:>10.2} all four equal, bitwise (i128)" ); @@ -1391,9 +1415,21 @@ fn quantiles() { let arm_c = |p: &Profile, it: &Inst| { center(it) + it.r * p.value(it.rank as usize, it.phase as f64 / TURN) }; + // E-LUT: arm E with every cosine from the shipped phase table. + let lut = phase_lut_4096(); + let center_l = |it: &Inst| { + if wankel { + it.c * f64::from(lut.lerp(it.phase.wrapping_mul(3)).0) + } else { + it.c + } + }; + let arm_el = |it: &Inst| { + center_l(it) + it.r * sp.value_lut(it.rank as usize, it.phase as f64 / TURN, lut) + }; // Error over a dense phase sweep, every rank, including every tie. let mut buf = [0f64; 256]; - let (mut eb, mut ec, mut eco, mut ee) = (0f64, 0f64, 0f64, 0f64); + let (mut eb, mut ec, mut eco, mut ee, mut eel) = (0f64, 0f64, 0f64, 0f64, 0f64); let sweep = 200_003u32; for i in 0..sweep { let phase = (u64::from(i) * (1u64 << 32) / u64::from(sweep)) as u32; @@ -1420,14 +1456,22 @@ fn quantiles() { ec = ec.max((arm_c(&prof, &it) - o).abs()); eco = eco.max((arm_c(&prof_odd, &it) - o).abs()); ee = ee.max((arm_e(&it) - o).abs()); + eel = eel.max((arm_el(&it) - o).abs()); } } assert!(ee < 1e-9, "sector permutation is exact up to rounding"); + // The table's own per-component bound, scaled by everything it + // multiplies: the radius, and the Wankel centre's e = 7. + let bound_el = (100.0 + if wankel { 7.0 } else { 0.0 }) * lut.lerp_error_bound() + 1e-6; + assert!( + eel <= bound_el, + "E-LUT error {eel} above its stated bound {bound_el}" + ); if wankel { assert!(eb < 1e-9); } println!( - " m = {m:<3} max |error| on R = 100 ({sweep} phases × all {m} ranks): B {} C(64 even) {ec:.1e} C(63 odd) {eco:.1e} E {ee:.1e} C table {} B, E table {} B", + " m = {m:<3} max |error| on R = 100 ({sweep} phases × all {m} ranks): B {} C(64 even) {ec:.1e} C(63 odd) {eco:.1e} E {ee:.1e} E-LUT {eel:.1e} (bound {bound_el:.1e}) C table {} B, E table {} B", if wankel { format!("{eb:.1e}") } else { "—".into() }, prof.bytes(), 2 * m as usize * 4 @@ -1453,6 +1497,7 @@ fn quantiles() { }; let ns_c = median_ns(reps, n, || inst.iter().map(|it| arm_c(&prof, it)).sum()); let ns_e = median_ns(reps, n, || inst.iter().map(arm_e).sum()); + let ns_el = median_ns(reps, n, || inst.iter().map(arm_el).sum()); // D: terminals that need no profile at all. let ns_d = median_ns(reps, n, || { inst.iter() @@ -1460,12 +1505,90 @@ fn quantiles() { .sum() }); println!( - " {n:>9} instances ns/instance: A gen+select {ns_a:>7.1} B analytic {ns_b:>6} C LUT {ns_c:>6.1} E sector {ns_e:>6.1} (D Σ|z−c|², no profile: {ns_d:.2})" + " {n:>9} instances ns/instance: A gen+select {ns_a:>7.1} B analytic {ns_b:>6} C LUT {ns_c:>6.1} E sector {ns_e:>6.1} E-LUT {ns_el:>5.1} (D Σ|z−c|², no profile: {ns_d:.2})" ); } } } +/// Every order statistic of each instance: generate + sort, against the +/// sector permutation (m cosines, no sort) with `f64` or table cosines, and +/// against `m` interpolated profile rows. +fn all_quantiles() { + println!("\n== 4b. all m order statistics per instance (65,536 instances; m = 256: 16,384) =="); + let lut = phase_lut_4096(); + for m in [3u32, 16, 256] { + let n = if m == 256 { 16_384 } else { 65_536 }; + let inst = instances(n, m, 0x2B + u64::from(m)); + let sp = SectorPerm::new(m); + let prof = Profile::new(m, 64); + let mu = m as usize; + let reps = 5; + let ns_a = median_ns(reps, n, || { + let mut buf = [0f64; 256]; + inst.iter() + .map(|it| { + let th = rad(it.phase); + let b = &mut buf[..mu]; + for (k, v) in b.iter_mut().enumerate() { + *v = it.r * (th + TAU * k as f64 / m as f64).cos(); + } + b.sort_unstable_by(f64::total_cmp); + b.iter().sum::() + b[mu - 1] + }) + .sum() + }); + let ns_e = median_ns(reps, n, || { + inst.iter() + .map(|it| { + let phi = it.phase as f64 / TURN; + (0..mu).map(|r| it.r * sp.value(r, phi)).sum::() + }) + .sum() + }); + let ns_el = median_ns(reps, n, || { + inst.iter() + .map(|it| { + let phi = it.phase as f64 / TURN; + (0..mu) + .map(|r| it.r * sp.value_lut(r, phi, lut)) + .sum::() + }) + .sum() + }); + let ns_c = median_ns(reps, n, || { + inst.iter() + .map(|it| { + let phi = it.phase as f64 / TURN; + (0..mu).map(|r| it.r * prof.value(r, phi)).sum::() + }) + .sum() + }); + // Agreement of the three fast arms with the sorted oracle, all ranks. + let mut worst = (0f64, 0f64, 0f64); + let mut buf = [0f64; 256]; + for it in inst.iter().take(2048) { + let th = rad(it.phase); + let b = &mut buf[..mu]; + for (k, v) in b.iter_mut().enumerate() { + *v = it.r * (th + TAU * k as f64 / m as f64).cos(); + } + b.sort_unstable_by(f64::total_cmp); + let phi = it.phase as f64 / TURN; + for (r, o) in b.iter().enumerate() { + worst.0 = worst.0.max((it.r * sp.value(r, phi) - o).abs()); + worst.1 = worst.1.max((it.r * sp.value_lut(r, phi, lut) - o).abs()); + worst.2 = worst.2.max((it.r * prof.value(r, phi) - o).abs()); + } + } + assert!(worst.0 < 1e-9); + println!( + " m = {m:<3} ns/instance: A gen+sort {ns_a:>8.1} E sector {ns_e:>7.1} E-LUT {ns_el:>6.1} C profile {ns_c:>6.1} max |error| E {:.1e} E-LUT {:.1e} C {:.1e}", + worst.0, worst.1, worst.2 + ); + } +} + // ───────────────────────── section 5: lookup and rewrite cost ───────────────────────── fn lookup_cost() { @@ -1615,6 +1738,7 @@ fn main() { guards(); population(); quantiles(); + all_quantiles(); lookup_cost(); quantile_guard(); println!("\nall equalities and falsifiers hold"); From 34ee05117f337347a6081ce3ad85b7f6d5c869a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 20:53:26 +0000 Subject: [PATCH 2/4] probe(recipe): isolate the cross-power-sum fold cost by ingredient Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .../examples/algebraic_recipe_probe.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs b/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs index 9941246ca..156a5675c 100644 --- a/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs +++ b/crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs @@ -1294,6 +1294,95 @@ fn population() { ); } +/// Where the shipped grouped fold's time goes, against plain scalar folds +/// that add one ingredient at a time: the `i128` accumulation alone, plus a +/// mask-bit walk, plus a group key, then the shipped +/// `masked_group_cross_power_sums_i32` itself. +fn fold_cost() { + println!("\n== 3b. cross-power-sum fold: where the time goes (ns/row, 1M rows, one group) =="); + let n = 1_000_000usize; + let mut rng = Rng(0xF01D); + let xs: Vec = (0..n).map(|_| rng.below(201) as i32 - 100).collect(); + let ys: Vec = (0..n).map(|_| rng.below(201) as i32 - 100).collect(); + let keys = vec![0u32; n]; + let mask = full_mask(n); + let acc = |s: &mut S2, x: i32, y: i32| { + let (x, y) = (i128::from(x), i128::from(y)); + s.n += 1; + s.x += x; + s.y += y; + s.xx += x * x; + s.yy += y * y; + s.xy += x * y; + }; + let reps = 9; + let mut r = [S2::default(); 5]; + let ns_plain = median_ns(reps, n, || { + let mut s = S2::default(); + for i in 0..n { + acc(&mut s, xs[i], ys[i]); + } + r[0] = s; + s.n as f64 + }); + // i64 accumulators where they suffice (|x|·n < 2^63), i128 only for squares. + let ns_narrow = median_ns(reps, n, || { + let (mut sx, mut sy, mut sxx, mut syy, mut sxy) = (0i64, 0i64, 0i64, 0i64, 0i64); + for i in 0..n { + let (x, y) = (i64::from(xs[i]), i64::from(ys[i])); + sx += x; + sy += y; + sxx += x * x; + syy += y * y; + sxy += x * y; + } + r[1] = S2 { + n: n as i128, + x: sx.into(), + y: sy.into(), + xx: sxx.into(), + yy: syy.into(), + xy: sxy.into(), + }; + r[1].n as f64 + }); + let ns_mask = median_ns(reps, n, || { + let mut s = S2::default(); + for (w, &word) in mask.iter().enumerate() { + let mut m = word; + while m != 0 { + let i = w * 64 + m.trailing_zeros() as usize; + m &= m - 1; + acc(&mut s, xs[i], ys[i]); + } + } + r[2] = s; + s.n as f64 + }); + let ns_key = median_ns(reps, n, || { + let mut out = [S2::default(); 1]; + for i in 0..n { + let k = keys[i] as usize; + if k < out.len() { + acc(&mut out[k], xs[i], ys[i]); + } + } + r[3] = out[0]; + out[0].n as f64 + }); + let ns_shipped = median_ns(reps, n, || { + r[4] = fold_ndarray(&xs, &ys, &mask, &keys); + r[4].n as f64 + }); + assert!( + r.iter().all(|x| *x == r[0]), + "every fold must give the same sums" + ); + println!( + " plain i128 {ns_plain:.2} i64 accumulators {ns_narrow:.2} + mask-bit walk {ns_mask:.2} + group key {ns_key:.2} shipped grouped fold {ns_shipped:.2} (all equal)" + ); +} + // ───────────────────────── section 4: projected order statistics ───────────────────────── #[derive(Clone, Copy)] @@ -1737,6 +1826,7 @@ fn main() { wankel_table(); guards(); population(); + fold_cost(); quantiles(); all_quantiles(); lookup_cost(); From 623afdfc40236e2d158728915c5df0da234a7f49 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 20:54:06 +0000 Subject: [PATCH 3/4] board: D-ART-2 (PhaseLut-backed order statistics, all-ranks timing, fold cost by ingredient) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .claude/board/STATUS_BOARD.md | 1 + ...26-10-08-recipe-probe-phaselut-foldcost.md | 70 +++++++++++++++++++ .claude/board/entries/README.md | 3 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .claude/board/entries/2026-10-08-recipe-probe-phaselut-foldcost.md diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index afea71083..828b62e6e 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -5,6 +5,7 @@ Board entry: `entries/2026-10-08-algebraic-recipe-probe.md`. A linear canonicali | D-id | scope | status | gate / falsifier | |---|---|---|---| | **D-ART-1** | Recipe table: constants, centre phasor, isotropic invariant, origin norm, sector quantile, vertex phasor, summary affine, materialise; PowerSums affine chains; order statistics by generate/analytic/profile LUT/sector permutation; lookup cost | Shipped (probe; `CrossPowerSums` affine transform proposed for ndarray) | 36,000 cases equal the materialising oracle; summary path bitwise equal in `i128`; dropping the isotropy guard, the half-turn fold, the rank reversal, the half-sector split, the population refusal or the general `‖L‖_F²` each fails | +| **D-ART-2** | Sector permutation on `ndarray::simd::PhaseLut`; all-order-statistics timing; power-sum fold cost by ingredient; cross-check of the shipped `checked_affine` | Shipped (probe; split-accumulator fold proposed for ndarray) | E-LUT within `(r+e)·lerp_error_bound()` on every rank, and `nearest` instead of `lerp` fails it; the five folds give equal sums; the shipped `checked_affine` equals the probe's transform. Entry: `entries/2026-10-08-recipe-probe-phaselut-foldcost.md` | ## D-PHT — Phasors without transcendental calls (2026-10-08) diff --git a/.claude/board/entries/2026-10-08-recipe-probe-phaselut-foldcost.md b/.claude/board/entries/2026-10-08-recipe-probe-phaselut-foldcost.md new file mode 100644 index 000000000..08e3ff3f4 --- /dev/null +++ b/.claude/board/entries/2026-10-08-recipe-probe-phaselut-foldcost.md @@ -0,0 +1,70 @@ +# 2026-10-08 — Recipe probe on the shipped PhaseLut; all order statistics; where the power-sum fold spends its time (D-ART-2) + +**Status:** MEASURED. Probe only; no production code changes. Follows D-ART-1. +- **Probe:** `crates/lance-graph-mask-risc/examples/algebraic_recipe_probe.rs`, sections 3b, 4 (E-LUT arm) and 4b. +- **Uses:** `ndarray::simd::PhaseLut` (ndarray #343) and `CrossPowerSums::checked_affine` (ndarray #344). +- **Host:** Intel Xeon @ 2.8 GHz, 4 vCPU, `avx2` build, release, `debug = 0`. Medians of 5–9 runs. The VM is noisy; ranges below are across three runs. + +## 1. Sector permutation on the shipped phase table + +Arm **E-LUT** is arm E (two fixed orderings plus one cosine) with every cosine taken from `phase_lut_4096()`. + +| m | A generate + select | C profile LUT, 64 buckets | E sector, `f64` cos | **E-LUT** | E-LUT error on R = 100 | its stated bound | +|---|---|---|---|---|---|---| +| 3 (Wankel) | 106–108 ns | 31–34 | 49–56 | **15.6–18.9** | 3.7e-5 | 8.3e-5 | +| 16 | 400–718 | 10.6–20.9 | 42–61 | **15.8–29.0** | 3.5e-5 | 7.8e-5 | +| 256 | 4,918–5,415 | 10.7–14.9 | 41–46 | **14.6–16.0** | 3.5e-5 | 7.8e-5 | + +Ranges are over 65,536 and 1M instances. Errors are max |error| over 200,003 phases × every rank. + +- The error stays within `(r + e) · lerp_error_bound()`, asserted on every rank. Disable run: `nearest` instead of `lerp` gives 0.077 against the 8.3e-5 bound, and the assertion fires. +- E-LUT needs a 32 KB shared table plus a 24 B–2 KB permutation, and its cost does not depend on m. For the Wankel triangle it beats both the profile LUT (2×) and the analytic arm B (2.7×). + +## 2. All m order statistics per instance + +65,536 instances; m = 256 uses 16,384. + +| m | A generate + sort | E sector | E-LUT | C profile row | +|---|---|---|---|---| +| 3 | 92 ns | 80 | 35 | **18** | +| 16 | 432 | 352 | 133 | **42** | +| 256 | 9,101 | 4,671 | 1,956 | **493** | + +Max error: E 3e-13, E-LUT 5e-5, C 2e-2 / 7e-4 / 1e-5 for m = 3 / 16 / 256. + +When every rank is wanted, the profile row is 2–4× faster than E-LUT: the row is contiguous and needs no permutation lookup. When one rank is wanted, E-LUT is the choice: it is exact up to the table's bound and needs no per-m table. + +## 3. Where the cross-power-sum fold spends its time + +1M rows, one group, all rows selected. ns per row, three runs: + +| fold | ns/row | +|---|---| +| plain scalar, `i128` accumulators | 4.4–7.5 | +| same, with `i64` accumulators | **0.5–0.9** | +| `i128` + mask-bit walk | 3.9–6.7 | +| `i128` + group key | 5.3–9.6 | +| shipped `masked_group_cross_power_sums_i32` | 5.3–8.1 | + +All five produce identical sums. + +- **The accumulator width is the cost.** `i64` accumulators are about 8× faster than `i128`, presumably because they vectorise. The mask walk and the group key are within noise. +- **D-ART-1's "fused 3.7 vs shipped 4.8 ns/row" was noise between two `i128` folds.** It is not a defect in the shipped fold's addressing. +- **`i64` is not exact for the full `i32` range.** One `x²` reaches 2^62, so two rows can overflow it. A faster exact fold therefore needs either known-bounded inputs, or a split such as `x = hi·2^16 + lo` with `u64` partial sums combined into `u128` at the end. ndarray's `masked_group_bounded_power_sums_u8` is the bounded case of this idea for `u8`. + +## 4. Shipped `checked_affine` + +The probe's own `i128` summary transform now agrees bitwise with `CrossPowerSums::checked_affine` (ndarray #344) on every row count. + +## Recommendations + +| item | verdict | +|---|---| +| Single order statistic of an m-fold family: sector permutation + `PhaseLut` | **ADOPT** as the method (exact to the table's bound, flat in m) | +| All order statistics, m ≥ 16: interpolated profile row, bucket edges on the tie points | **PROBE** | +| Wide-input power-sum fold with split `u64` partial sums | **PROBE** in ndarray, with a parity test against the `i128` fold; measured lever ≈ 8× | + +## OPEN + +- One host class. +- The split-accumulator fold is not built. The 8× is measured on `i64` accumulators, which are not exact for full-range `i32`. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 5390f9dd0..b8b1247ad 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -277 entries, 2026-08-06 .. 2026-10-08. +278 entries, 2026-08-06 .. 2026-10-08. | date | entry id | finding | file | |---|---|---|---| | 2026-10-08 | `register128-signed-readings` | | [2026-10-08-register128-signed-readings.md](2026-10-08-register128-signed-readings.md) | +| 2026-10-08 | `D-ART-2` | | [2026-10-08-recipe-probe-phaselut-foldcost.md](2026-10-08-recipe-probe-phaselut-foldcost.md) | | 2026-10-08 | `rbac-nested-scope-path` | | [2026-10-08-rbac-nested-scope-path.md](2026-10-08-rbac-nested-scope-path.md) | | 2026-10-08 | `rbac-membership-scope` | | [2026-10-08-rbac-membership-scope.md](2026-10-08-rbac-membership-scope.md) | | 2026-10-08 | `rbac-hotplug-socket` | | [2026-10-08-rbac-hotplug-socket.md](2026-10-08-rbac-hotplug-socket.md) | From 5028237c38658a48b1e80596426a11f25f022acf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 21:24:00 +0000 Subject: [PATCH 4/4] Board: record R-MHB-1's blocker on D-MHB-1 The D path needs a 2-D window operand that mask-risc's IR lacks. Shipping F alone would bring back the 2.5x worst case the per-query choice removes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .claude/board/STATUS_BOARD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 828b62e6e..e8c13b9e9 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -21,7 +21,7 @@ Board entry: `entries/2026-10-08-mexhat-bucket-cascade-probe.md`. Popcount stack | D-id | scope | status | gate / falsifier | |---|---|---|---| -| **D-MHB-1** | Mexican-hat response: direct geometry (D), ring buckets (E), bit-sliced weight planes (F), per-row choice (G), mask-risc lane (B), exact-bound early exit (H) | Shipped (probe; R-MHB-1 proposed) | D = F = G = B = full scan asserted on every centre; H equals the full decision on every query; one zero crossing, one minimum, symmetry; dropping the unvisited-row bound decides 52 / 512 wrongly | +| **D-MHB-1** | Mexican-hat response: direct geometry (D), ring buckets (E), bit-sliced weight planes (F), per-row choice (G), mask-risc lane (B), exact-bound early exit (H) | Shipped (probe; R-MHB-1 OPEN: blocked on a 2-D window operand mask-risc's IR lacks, and F alone would bring back the 2.5× worst case Q removes) | D = F = G = B = full scan asserted on every centre; H equals the full decision on every query; one zero crossing, one minimum, symmetry; dropping the unvisited-row bound decides 52 / 512 wrongly | | **D-MHB-2** | Per-query D/F choice from the exact window count (Q); early exit over D rows (HD) and after a per-query choice (HQ) | Shipped (probe; Q proposed with R-MHB-1) | Q = D for cuts 0, 100, ∞ on every centre; HD/HQ decide as H after the same rows; a D row dropping one cell fails Q. Entry: `entries/2026-10-08-mexhat-df-choice.md` | ## D-RPF — Mask × fold over projections of resident bytes (2026-10-08)