From 035d73b513290638fbe86bca81ecd16369c59d34 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:03:23 +0000 Subject: [PATCH 1/7] packed/cascade: pin that the two stroke packers agree at 2048 bytes packed::PackedDatabase (fixed 128/384/1536) and cascade::PackedDatabase (width-derived split) lay out the same strokes for 2048-byte rows; a test pins it. Doc cross-links name the third, unrelated cam_pq type. Not merged: the two types have different query paths and the first carries an index. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- src/hpc/cascade.rs | 7 ++++++- src/hpc/packed.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/hpc/cascade.rs b/src/hpc/cascade.rs index de2e34fc..67a29f37 100644 --- a/src/hpc/cascade.rs +++ b/src/hpc/cascade.rs @@ -598,7 +598,12 @@ pub fn adaptive_resolution(query_entropy: f32, corpus_cv: f32) -> Band { } } -/// Packed database for stroke-aligned cascade search. +/// Packed database for stroke-aligned cascade search, for rows of any width. +/// +/// For 2048-byte rows the split is the fixed one in +/// [`crate::hpc::packed::PackedDatabase`] (128 / 384 / 1536); that type adds +/// an index and its own query path. `crate::hpc::cam_pq::PackedDatabase` is +/// unrelated (6-byte CAM codes). pub struct PackedDatabase { pub stroke1: Vec, pub stroke2: Vec, diff --git a/src/hpc/packed.rs b/src/hpc/packed.rs index 03fbb6e5..3f91e625 100644 --- a/src/hpc/packed.rs +++ b/src/hpc/packed.rs @@ -75,6 +75,13 @@ pub const FINGERPRINT_BYTES: usize = 2048; /// stroke2: [cand[0]_128..512 | cand[1]_128..512 | ...] /// stroke3: [cand[0]_512..2048 | cand[1]_512..2048 | ...] /// ``` +/// +/// Two other types carry this name and are not this one: +/// [`crate::hpc::cascade::PackedDatabase`] packs rows of any width, and for +/// 2048-byte rows its stroke split is exactly this one (128 / 384 / 1536, +/// pinned by `stroke_split_equals_the_cascade_split_at_2048`), with its own +/// query path and no index. `crate::hpc::cam_pq::PackedDatabase` holds 6-byte +/// CAM codes and shares only the name. pub struct PackedDatabase { /// Stroke 1 data: N × STROKE1_BYTES contiguous bytes. stroke1: Vec, @@ -277,6 +284,25 @@ impl PackedDatabase { #[cfg(test)] mod tests { + /// The fixed 2048-byte split here and cascade's width-derived split must + /// lay out the same bytes, or the two packers disagree on what a stroke + /// is. + #[test] + fn stroke_split_equals_the_cascade_split_at_2048() { + let n = 5; + let db: Vec = (0..n * FINGERPRINT_BYTES) + .map(|i| (i * 31 % 251) as u8) + .collect(); + let fixed = PackedDatabase::pack(&db, FINGERPRINT_BYTES); + let cas = crate::hpc::cascade::PackedDatabase::pack(&db, FINGERPRINT_BYTES); + assert_eq!((cas.s1_bytes, cas.s2_bytes, cas.s3_bytes), (STROKE1_BYTES, STROKE2_BYTES, STROKE3_BYTES)); + for i in 0..n { + assert_eq!(fixed.get_stroke1(i), &cas.stroke1[i * STROKE1_BYTES..(i + 1) * STROKE1_BYTES]); + assert_eq!(fixed.get_stroke2(i), &cas.stroke2[i * STROKE2_BYTES..(i + 1) * STROKE2_BYTES]); + assert_eq!(fixed.get_stroke3(i), &cas.stroke3[i * STROKE3_BYTES..(i + 1) * STROKE3_BYTES]); + } + } + use super::*; #[test] From a386c9e0f56b9d530e9304cc15caa83c0bd86946 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:07:25 +0000 Subject: [PATCH 2/7] simd: mask_row_window (2-D window, refuse or zero-fill) + bit_sliced_weighted_count R-MHB-1 prerequisites. mask_row_window reads up to 64 bits of one row of a row-major packed grid at any column; a window that leaves its row is refused (None) by default, or zero-filled on request, and never reads a neighbouring row. bit_sliced_weighted_count is the bit-sliced weighted popcount (arm F's row kernel): sum_b 2^b (popcnt(w & pos_b) - popcnt(w & neg_b)), exact for integer weights, no weight lane. Reference-tested (20k random windows, 2k weighted rows) plus an edge test that a flat bit offset would fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- src/simd.rs | 3 + src/simd_masking_ops.rs | 186 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) diff --git a/src/simd.rs b/src/simd.rs index 3c618191..e5a06f79 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -835,6 +835,7 @@ pub use crate::simd_masking_ops::{ // 2026-09-16: the ordered u64 family (N3/G2). The width where the packing // is NOT free — `U64x8` is 8 lanes, so eight groups share a word and each // lands at its own byte. + bit_sliced_weighted_count, blend_i32, eq_i32_to_mask, eq_i32_to_mask_under, @@ -874,6 +875,7 @@ pub use crate::simd_masking_ops::{ mask_not_assign, mask_or, mask_or_assign, + mask_row_window, mask_scatter_or_u32, mask_set_range, mask_shift_morton, @@ -935,6 +937,7 @@ pub use crate::simd_masking_ops::{ KeyRunCarry, MortonDir, PowerSums, + WindowEdge, BOUNDED_TILE_ROWS, SYM_EMPTY_I64, }; diff --git a/src/simd_masking_ops.rs b/src/simd_masking_ops.rs index fc3dff9c..04307412 100644 --- a/src/simd_masking_ops.rs +++ b/src/simd_masking_ops.rs @@ -9901,3 +9901,189 @@ mod bounded_register_tests { assert_eq!(out[0], PowerSums::default()); } } + +// ──────────────────────────────────────────────────────────────────────── +// 2-D windows over a row-major packed grid + bit-sliced weighted counts +// (R-MHB-1: a centre-surround sum as popcounts, no weight lane) +// ──────────────────────────────────────────────────────────────────────── + +/// What a row window does with columns outside its row. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WindowEdge { + /// A window that leaves the row (or the grid) is refused: `None`. + Refuse, + /// Columns outside the row read as 0. Never reads a neighbouring row. + ZeroFill, +} + +/// `len` bits (1..=64) of row `row` of a row-major packed grid, starting at +/// column `col`; bit `j` of the result is column `col + j`. +/// +/// The grid has `words_per_row` words per row, so a row is +/// `64 · words_per_row` columns and row `r` starts at word +/// `r · words_per_row`. A window never crosses into another row: with +/// [`WindowEdge::Refuse`] it returns `None` when `col < 0`, +/// `col + len > 64 · words_per_row` or the row is past the grid; with +/// [`WindowEdge::ZeroFill`] those columns read as 0 (a row past the grid +/// reads all-zero). `len` outside 1..=64 is always `None`. +/// +/// # Example +/// +/// ``` +/// use ndarray::simd::{mask_row_window, WindowEdge}; +/// // Two rows of one word each; row 1 has columns 0..8 set. +/// let grid = [0u64, 0xFF]; +/// assert_eq!(mask_row_window(&grid, 1, 1, 4, 8, WindowEdge::Refuse), Some(0x0F)); +/// assert_eq!(mask_row_window(&grid, 1, 1, -2, 8, WindowEdge::Refuse), None); +/// assert_eq!(mask_row_window(&grid, 1, 1, -2, 8, WindowEdge::ZeroFill), Some(0xFC)); +/// ``` +pub fn mask_row_window( + words: &[u64], words_per_row: usize, row: usize, col: i64, len: u32, edge: WindowEdge, +) -> Option { + if len == 0 || len > 64 || words_per_row == 0 { + return None; + } + let width = (words_per_row * 64) as i64; + let rows = words.len() / words_per_row; + let end = col + i64::from(len); + let inside = row < rows && col >= 0 && end <= width; + if !inside && edge == WindowEdge::Refuse { + return None; + } + if row >= rows { + return Some(0); + } + let base = row * words_per_row; + // Read bit `c` of the row, or 0 outside it. + let word_at = |w: i64| -> u64 { + if w < 0 || w >= words_per_row as i64 { + 0 + } else { + words[base + w as usize] + } + }; + let w0 = col.div_euclid(64); + let sh = col.rem_euclid(64) as u32; + let lo = word_at(w0) >> sh; + let hi = if sh == 0 { 0 } else { word_at(w0 + 1) << (64 - sh) }; + let v = lo | hi; + Some(if len == 64 { v } else { v & ((1u64 << len) - 1) }) +} + +/// `Σ_b 2^b · (popcount(window ∧ pos[b]) − popcount(window ∧ neg[b]))`. +/// +/// The weighted count of the set bits of `window`, where a cell's integer +/// weight is spread over bit-planes: bit `b` of a positive weight's magnitude +/// is set in `pos[b]`, of a negative weight's in `neg[b]`. Exact for integer +/// weights; no per-cell weight lane is read. +/// +/// # Panics +/// +/// Panics unless `pos.len() == neg.len()` and there are at most 32 planes +/// (so every term fits an `i64`). +/// +/// # Example +/// +/// ``` +/// use ndarray::simd::bit_sliced_weighted_count; +/// // Cell 0 weighs +5 (planes 0 and 2), cell 1 weighs -2 (plane 1). +/// let pos = [0b01, 0, 0b01]; +/// let neg = [0, 0b10, 0]; +/// assert_eq!(bit_sliced_weighted_count(0b11, &pos, &neg), 5 - 2); +/// assert_eq!(bit_sliced_weighted_count(0b10, &pos, &neg), -2); +/// ``` +pub fn bit_sliced_weighted_count(window: u64, pos: &[u64], neg: &[u64]) -> i64 { + assert_eq!(pos.len(), neg.len(), "bit_sliced_weighted_count: plane count mismatch"); + assert!(pos.len() <= 32, "bit_sliced_weighted_count: at most 32 planes"); + let mut s = 0i64; + for (b, (&p, &n)) in pos.iter().zip(neg).enumerate() { + s += i64::from((window & p).count_ones()) << b; + s -= i64::from((window & n).count_ones()) << b; + } + s +} + +#[cfg(test)] +mod window_tests { + use super::*; + + struct Lcg(u64); + impl Lcg { + fn next(&mut self) -> u64 { + self.0 = self + .0 + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + self.0 + } + } + + /// Reference: one bit at a time, by grid coordinates. + fn reference(words: &[u64], wpr: usize, row: usize, col: i64, len: u32) -> u64 { + let width = (wpr * 64) as i64; + let rows = words.len() / wpr; + let mut v = 0u64; + for j in 0..i64::from(len) { + let c = col + j; + if row < rows && c >= 0 && c < width { + let i = row * wpr * 64 + c as usize; + v |= (words[i / 64] >> (i % 64) & 1) << j; + } + } + v + } + + #[test] + fn row_window_matches_the_bitwise_reference() { + let mut r = Lcg(3); + let wpr = 3; + let grid: Vec = (0..wpr * 5).map(|_| r.next()).collect(); + for _ in 0..20_000 { + let row = (r.next() % 6) as usize; + let len = 1 + (r.next() % 64) as u32; + let col = (r.next() % 260) as i64 - 40; + let want = reference(&grid, wpr, row, col, len); + assert_eq!(mask_row_window(&grid, wpr, row, col, len, WindowEdge::ZeroFill), Some(want)); + let inside = row < 5 && col >= 0 && col + i64::from(len) <= (wpr * 64) as i64; + let refused = mask_row_window(&grid, wpr, row, col, len, WindowEdge::Refuse); + assert_eq!(refused, inside.then_some(want), "row {row} col {col} len {len}"); + } + } + + /// A window at a row's edge must not read the neighbouring row, which is + /// what a flat bit offset into row-major words would do. + #[test] + fn a_window_never_reads_the_neighbouring_row() { + // Row 0 empty, rows 1 and 2 full. + let grid = [0u64, u64::MAX, u64::MAX]; + assert_eq!(mask_row_window(&grid, 1, 0, 60, 8, WindowEdge::ZeroFill), Some(0)); + assert_eq!(mask_row_window(&grid, 1, 2, -4, 8, WindowEdge::ZeroFill), Some(0xF0)); + assert_eq!(mask_row_window(&grid, 1, 0, 60, 8, WindowEdge::Refuse), None); + assert_eq!(mask_row_window(&grid, 1, 1, 0, 64, WindowEdge::Refuse), Some(u64::MAX)); + assert_eq!(mask_row_window(&grid, 1, 3, 0, 8, WindowEdge::ZeroFill), Some(0)); + assert_eq!(mask_row_window(&grid, 1, 0, 0, 0, WindowEdge::ZeroFill), None); + } + + #[test] + fn bit_sliced_count_equals_the_weighted_sum() { + let mut r = Lcg(11); + for _ in 0..2_000 { + let weights: Vec = (0..64).map(|_| (r.next() % 8193) as i32 - 4096).collect(); + let (mut pos, mut neg) = ([0u64; 13], [0u64; 13]); + for (j, &w) in weights.iter().enumerate() { + let (planes, m) = if w >= 0 { (&mut pos, w) } else { (&mut neg, -w) }; + for (b, p) in planes.iter_mut().enumerate() { + if m >> b & 1 == 1 { + *p |= 1 << j; + } + } + } + let window = r.next(); + let want: i64 = (0..64) + .filter(|j| window >> j & 1 == 1) + .map(|j| i64::from(weights[j])) + .sum(); + assert_eq!(bit_sliced_weighted_count(window, &pos, &neg), want); + } + } +} From 24ad716ee56cde1ada3c7786e8c081f3148f1f29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:09:25 +0000 Subject: [PATCH 3/7] blackboard: R-MHB-1 prerequisites; PackedDatabase is three types, not one Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .claude/blackboard.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.claude/blackboard.md b/.claude/blackboard.md index 32841821..467f9abf 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -1,3 +1,10 @@ +## 2026-10-10 — R-MHB-1 prerequisites in `ndarray::simd`; the three `PackedDatabase`s + +- **`mask_row_window(words, words_per_row, row, col, len, edge) -> Option`**: up to 64 bits of one row of a row-major packed grid at any column. `WindowEdge::Refuse` (None outside the row) is the default the R-MHB-1 plan asked for; `ZeroFill` is the opt-in. A window never reads a neighbouring row; a flat bit offset into row-major words would, and `a_window_never_reads_the_neighbouring_row` is red under exactly that disable. +- **`bit_sliced_weighted_count(window, pos, neg) -> i64`**: Σ_b 2^b (popcnt(w ∧ pos_b) − popcnt(w ∧ neg_b)), the mexhat probe's arm-F row kernel. Exact for integer weights; at most 32 planes. Dropping the negative planes turns `bit_sliced_count_equals_the_weighted_sum` red. +- Both are plain u64 ops (`count_ones`, shifts), identical on every realization; no backend code. +- **`PackedDatabase` ×3 is not a duplicate.** `packed` (fixed 128/384/1536 + an index) and `cascade` (width-derived split) lay out the same strokes at 2048 bytes, now pinned by `stroke_split_equals_the_cascade_split_at_2048` (red if cascade's split point moves); they keep separate query paths. `cam_pq::PackedDatabase` holds 6-byte CAM codes and shares only the name. Doc cross-links added; not merged. + ## 2026-10-10 — x86-64-v2 realization (SSE4.2 without AVX) via the scalar backend - **What:** an eighth realization, selected at compile time by `all(target_arch = "x86_64", target_feature = "sse4.2", not(target_feature = "avx"))`. You get it with `target-cpu=native` on a v2-only CPU (Nehalem/Westmere, Silvermont-class Atom, VMs that hide AVX) or with `.cargo/config-v2.toml`. `simd.rs` routes this arm to `simd_scalar.rs`: the AVX2/AVX-512 types wrap 256/512-bit registers and cannot be the v2 realization, so plain arrays that LLVM compiles to SSE are used instead. Baseline x86-64 builds (no SSE4.2) are unchanged: they keep the AVX2 arm and the `cpu_guard` AVX2 floor, which now exempts `sse4.2 && !avx2` (the AVX and v2 arms together). From df7abe0b510c54fd154a0c1ad1b7b759ba403349 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:14:52 +0000 Subject: [PATCH 4/7] mask_row_window: no overflow at extreme columns or widths col + len overflowed for col near i64::MAX (a panic with overflow checks, a wrapped end without them, which let Refuse accept an out-of-row window). The end is now checked and the width saturates; an unrepresentable end is simply not inside the row, so both edge policies still apply. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- src/simd_masking_ops.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/simd_masking_ops.rs b/src/simd_masking_ops.rs index 04307412..1e4fd163 100644 --- a/src/simd_masking_ops.rs +++ b/src/simd_masking_ops.rs @@ -9943,10 +9943,18 @@ pub fn mask_row_window( if len == 0 || len > 64 || words_per_row == 0 { return None; } - let width = (words_per_row * 64) as i64; + // A slice holds at most 2^60 words, so the true width can exceed + // `i64::MAX` only for a grid that cannot exist; saturating keeps the + // inside test exact for every representable column. + let wpr = i64::try_from(words_per_row).unwrap_or(i64::MAX); + let width = wpr.saturating_mul(64); let rows = words.len() / words_per_row; - let end = col + i64::from(len); - let inside = row < rows && col >= 0 && end <= width; + // An end past `i64::MAX` is past the row, so it is simply not inside. + let inside = row < rows + && col >= 0 + && col + .checked_add(i64::from(len)) + .is_some_and(|end| end <= width); if !inside && edge == WindowEdge::Refuse { return None; } @@ -9956,7 +9964,7 @@ pub fn mask_row_window( let base = row * words_per_row; // Read bit `c` of the row, or 0 outside it. let word_at = |w: i64| -> u64 { - if w < 0 || w >= words_per_row as i64 { + if w < 0 || w >= wpr { 0 } else { words[base + w as usize] @@ -10064,6 +10072,21 @@ mod window_tests { assert_eq!(mask_row_window(&grid, 1, 0, 0, 0, WindowEdge::ZeroFill), None); } + /// Extreme columns: `col + len` must not overflow, and both edge + /// policies must still apply. + #[test] + fn extreme_columns_do_not_overflow() { + let grid = [u64::MAX, u64::MAX]; + for col in [i64::MAX, i64::MAX - 3, i64::MIN, i64::MIN + 1] { + for len in [1, 8, 64] { + assert_eq!(mask_row_window(&grid, 1, 0, col, len, WindowEdge::Refuse), None); + assert_eq!(mask_row_window(&grid, 1, 0, col, len, WindowEdge::ZeroFill), Some(0)); + } + } + // A huge declared width is not a reason to refuse an in-row window. + assert_eq!(mask_row_window(&grid, 2, 0, 64, 8, WindowEdge::Refuse), Some(0xFF)); + } + #[test] fn bit_sliced_count_equals_the_weighted_sum() { let mut r = Lcg(11); From 547b8aab35f2b457f6ab2bd613811bb96d5b4330 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:29:50 +0000 Subject: [PATCH 5/7] Revert "mask_row_window: no overflow at extreme columns or widths" This reverts commit df7abe0b510c54fd154a0c1ad1b7b759ba403349. --- src/simd_masking_ops.rs | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/src/simd_masking_ops.rs b/src/simd_masking_ops.rs index 1e4fd163..04307412 100644 --- a/src/simd_masking_ops.rs +++ b/src/simd_masking_ops.rs @@ -9943,18 +9943,10 @@ pub fn mask_row_window( if len == 0 || len > 64 || words_per_row == 0 { return None; } - // A slice holds at most 2^60 words, so the true width can exceed - // `i64::MAX` only for a grid that cannot exist; saturating keeps the - // inside test exact for every representable column. - let wpr = i64::try_from(words_per_row).unwrap_or(i64::MAX); - let width = wpr.saturating_mul(64); + let width = (words_per_row * 64) as i64; let rows = words.len() / words_per_row; - // An end past `i64::MAX` is past the row, so it is simply not inside. - let inside = row < rows - && col >= 0 - && col - .checked_add(i64::from(len)) - .is_some_and(|end| end <= width); + let end = col + i64::from(len); + let inside = row < rows && col >= 0 && end <= width; if !inside && edge == WindowEdge::Refuse { return None; } @@ -9964,7 +9956,7 @@ pub fn mask_row_window( let base = row * words_per_row; // Read bit `c` of the row, or 0 outside it. let word_at = |w: i64| -> u64 { - if w < 0 || w >= wpr { + if w < 0 || w >= words_per_row as i64 { 0 } else { words[base + w as usize] @@ -10072,21 +10064,6 @@ mod window_tests { assert_eq!(mask_row_window(&grid, 1, 0, 0, 0, WindowEdge::ZeroFill), None); } - /// Extreme columns: `col + len` must not overflow, and both edge - /// policies must still apply. - #[test] - fn extreme_columns_do_not_overflow() { - let grid = [u64::MAX, u64::MAX]; - for col in [i64::MAX, i64::MAX - 3, i64::MIN, i64::MIN + 1] { - for len in [1, 8, 64] { - assert_eq!(mask_row_window(&grid, 1, 0, col, len, WindowEdge::Refuse), None); - assert_eq!(mask_row_window(&grid, 1, 0, col, len, WindowEdge::ZeroFill), Some(0)); - } - } - // A huge declared width is not a reason to refuse an in-row window. - assert_eq!(mask_row_window(&grid, 2, 0, 64, 8, WindowEdge::Refuse), Some(0xFF)); - } - #[test] fn bit_sliced_count_equals_the_weighted_sum() { let mut r = Lcg(11); From 4a6dd93ef924a0c2ac29574cd291c469cc99b462 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:29:50 +0000 Subject: [PATCH 6/7] Revert "simd: mask_row_window (2-D window, refuse or zero-fill) + bit_sliced_weighted_count" This reverts commit a386c9e0f56b9d530e9304cc15caa83c0bd86946. --- src/simd.rs | 3 - src/simd_masking_ops.rs | 186 ---------------------------------------- 2 files changed, 189 deletions(-) diff --git a/src/simd.rs b/src/simd.rs index e5a06f79..3c618191 100644 --- a/src/simd.rs +++ b/src/simd.rs @@ -835,7 +835,6 @@ pub use crate::simd_masking_ops::{ // 2026-09-16: the ordered u64 family (N3/G2). The width where the packing // is NOT free — `U64x8` is 8 lanes, so eight groups share a word and each // lands at its own byte. - bit_sliced_weighted_count, blend_i32, eq_i32_to_mask, eq_i32_to_mask_under, @@ -875,7 +874,6 @@ pub use crate::simd_masking_ops::{ mask_not_assign, mask_or, mask_or_assign, - mask_row_window, mask_scatter_or_u32, mask_set_range, mask_shift_morton, @@ -937,7 +935,6 @@ pub use crate::simd_masking_ops::{ KeyRunCarry, MortonDir, PowerSums, - WindowEdge, BOUNDED_TILE_ROWS, SYM_EMPTY_I64, }; diff --git a/src/simd_masking_ops.rs b/src/simd_masking_ops.rs index 04307412..fc3dff9c 100644 --- a/src/simd_masking_ops.rs +++ b/src/simd_masking_ops.rs @@ -9901,189 +9901,3 @@ mod bounded_register_tests { assert_eq!(out[0], PowerSums::default()); } } - -// ──────────────────────────────────────────────────────────────────────── -// 2-D windows over a row-major packed grid + bit-sliced weighted counts -// (R-MHB-1: a centre-surround sum as popcounts, no weight lane) -// ──────────────────────────────────────────────────────────────────────── - -/// What a row window does with columns outside its row. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum WindowEdge { - /// A window that leaves the row (or the grid) is refused: `None`. - Refuse, - /// Columns outside the row read as 0. Never reads a neighbouring row. - ZeroFill, -} - -/// `len` bits (1..=64) of row `row` of a row-major packed grid, starting at -/// column `col`; bit `j` of the result is column `col + j`. -/// -/// The grid has `words_per_row` words per row, so a row is -/// `64 · words_per_row` columns and row `r` starts at word -/// `r · words_per_row`. A window never crosses into another row: with -/// [`WindowEdge::Refuse`] it returns `None` when `col < 0`, -/// `col + len > 64 · words_per_row` or the row is past the grid; with -/// [`WindowEdge::ZeroFill`] those columns read as 0 (a row past the grid -/// reads all-zero). `len` outside 1..=64 is always `None`. -/// -/// # Example -/// -/// ``` -/// use ndarray::simd::{mask_row_window, WindowEdge}; -/// // Two rows of one word each; row 1 has columns 0..8 set. -/// let grid = [0u64, 0xFF]; -/// assert_eq!(mask_row_window(&grid, 1, 1, 4, 8, WindowEdge::Refuse), Some(0x0F)); -/// assert_eq!(mask_row_window(&grid, 1, 1, -2, 8, WindowEdge::Refuse), None); -/// assert_eq!(mask_row_window(&grid, 1, 1, -2, 8, WindowEdge::ZeroFill), Some(0xFC)); -/// ``` -pub fn mask_row_window( - words: &[u64], words_per_row: usize, row: usize, col: i64, len: u32, edge: WindowEdge, -) -> Option { - if len == 0 || len > 64 || words_per_row == 0 { - return None; - } - let width = (words_per_row * 64) as i64; - let rows = words.len() / words_per_row; - let end = col + i64::from(len); - let inside = row < rows && col >= 0 && end <= width; - if !inside && edge == WindowEdge::Refuse { - return None; - } - if row >= rows { - return Some(0); - } - let base = row * words_per_row; - // Read bit `c` of the row, or 0 outside it. - let word_at = |w: i64| -> u64 { - if w < 0 || w >= words_per_row as i64 { - 0 - } else { - words[base + w as usize] - } - }; - let w0 = col.div_euclid(64); - let sh = col.rem_euclid(64) as u32; - let lo = word_at(w0) >> sh; - let hi = if sh == 0 { 0 } else { word_at(w0 + 1) << (64 - sh) }; - let v = lo | hi; - Some(if len == 64 { v } else { v & ((1u64 << len) - 1) }) -} - -/// `Σ_b 2^b · (popcount(window ∧ pos[b]) − popcount(window ∧ neg[b]))`. -/// -/// The weighted count of the set bits of `window`, where a cell's integer -/// weight is spread over bit-planes: bit `b` of a positive weight's magnitude -/// is set in `pos[b]`, of a negative weight's in `neg[b]`. Exact for integer -/// weights; no per-cell weight lane is read. -/// -/// # Panics -/// -/// Panics unless `pos.len() == neg.len()` and there are at most 32 planes -/// (so every term fits an `i64`). -/// -/// # Example -/// -/// ``` -/// use ndarray::simd::bit_sliced_weighted_count; -/// // Cell 0 weighs +5 (planes 0 and 2), cell 1 weighs -2 (plane 1). -/// let pos = [0b01, 0, 0b01]; -/// let neg = [0, 0b10, 0]; -/// assert_eq!(bit_sliced_weighted_count(0b11, &pos, &neg), 5 - 2); -/// assert_eq!(bit_sliced_weighted_count(0b10, &pos, &neg), -2); -/// ``` -pub fn bit_sliced_weighted_count(window: u64, pos: &[u64], neg: &[u64]) -> i64 { - assert_eq!(pos.len(), neg.len(), "bit_sliced_weighted_count: plane count mismatch"); - assert!(pos.len() <= 32, "bit_sliced_weighted_count: at most 32 planes"); - let mut s = 0i64; - for (b, (&p, &n)) in pos.iter().zip(neg).enumerate() { - s += i64::from((window & p).count_ones()) << b; - s -= i64::from((window & n).count_ones()) << b; - } - s -} - -#[cfg(test)] -mod window_tests { - use super::*; - - struct Lcg(u64); - impl Lcg { - fn next(&mut self) -> u64 { - self.0 = self - .0 - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - self.0 - } - } - - /// Reference: one bit at a time, by grid coordinates. - fn reference(words: &[u64], wpr: usize, row: usize, col: i64, len: u32) -> u64 { - let width = (wpr * 64) as i64; - let rows = words.len() / wpr; - let mut v = 0u64; - for j in 0..i64::from(len) { - let c = col + j; - if row < rows && c >= 0 && c < width { - let i = row * wpr * 64 + c as usize; - v |= (words[i / 64] >> (i % 64) & 1) << j; - } - } - v - } - - #[test] - fn row_window_matches_the_bitwise_reference() { - let mut r = Lcg(3); - let wpr = 3; - let grid: Vec = (0..wpr * 5).map(|_| r.next()).collect(); - for _ in 0..20_000 { - let row = (r.next() % 6) as usize; - let len = 1 + (r.next() % 64) as u32; - let col = (r.next() % 260) as i64 - 40; - let want = reference(&grid, wpr, row, col, len); - assert_eq!(mask_row_window(&grid, wpr, row, col, len, WindowEdge::ZeroFill), Some(want)); - let inside = row < 5 && col >= 0 && col + i64::from(len) <= (wpr * 64) as i64; - let refused = mask_row_window(&grid, wpr, row, col, len, WindowEdge::Refuse); - assert_eq!(refused, inside.then_some(want), "row {row} col {col} len {len}"); - } - } - - /// A window at a row's edge must not read the neighbouring row, which is - /// what a flat bit offset into row-major words would do. - #[test] - fn a_window_never_reads_the_neighbouring_row() { - // Row 0 empty, rows 1 and 2 full. - let grid = [0u64, u64::MAX, u64::MAX]; - assert_eq!(mask_row_window(&grid, 1, 0, 60, 8, WindowEdge::ZeroFill), Some(0)); - assert_eq!(mask_row_window(&grid, 1, 2, -4, 8, WindowEdge::ZeroFill), Some(0xF0)); - assert_eq!(mask_row_window(&grid, 1, 0, 60, 8, WindowEdge::Refuse), None); - assert_eq!(mask_row_window(&grid, 1, 1, 0, 64, WindowEdge::Refuse), Some(u64::MAX)); - assert_eq!(mask_row_window(&grid, 1, 3, 0, 8, WindowEdge::ZeroFill), Some(0)); - assert_eq!(mask_row_window(&grid, 1, 0, 0, 0, WindowEdge::ZeroFill), None); - } - - #[test] - fn bit_sliced_count_equals_the_weighted_sum() { - let mut r = Lcg(11); - for _ in 0..2_000 { - let weights: Vec = (0..64).map(|_| (r.next() % 8193) as i32 - 4096).collect(); - let (mut pos, mut neg) = ([0u64; 13], [0u64; 13]); - for (j, &w) in weights.iter().enumerate() { - let (planes, m) = if w >= 0 { (&mut pos, w) } else { (&mut neg, -w) }; - for (b, p) in planes.iter_mut().enumerate() { - if m >> b & 1 == 1 { - *p |= 1 << j; - } - } - } - let window = r.next(); - let want: i64 = (0..64) - .filter(|j| window >> j & 1 == 1) - .map(|j| i64::from(weights[j])) - .sum(); - assert_eq!(bit_sliced_weighted_count(window, &pos, &neg), want); - } - } -} From 5cc83b406668d196565740d1ced9d5798e8fc7bb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 16:29:50 +0000 Subject: [PATCH 7/7] blackboard: withdraw the signed window helpers; keep the stroke-split pin Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EFw2WdKr1oxvaKCJC2ua2R --- .claude/blackboard.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.claude/blackboard.md b/.claude/blackboard.md index 467f9abf..e798ac4c 100644 --- a/.claude/blackboard.md +++ b/.claude/blackboard.md @@ -1,9 +1,7 @@ -## 2026-10-10 — R-MHB-1 prerequisites in `ndarray::simd`; the three `PackedDatabase`s +## 2026-10-10 — the three `PackedDatabase`s (stroke split pinned) -- **`mask_row_window(words, words_per_row, row, col, len, edge) -> Option`**: up to 64 bits of one row of a row-major packed grid at any column. `WindowEdge::Refuse` (None outside the row) is the default the R-MHB-1 plan asked for; `ZeroFill` is the opt-in. A window never reads a neighbouring row; a flat bit offset into row-major words would, and `a_window_never_reads_the_neighbouring_row` is red under exactly that disable. -- **`bit_sliced_weighted_count(window, pos, neg) -> i64`**: Σ_b 2^b (popcnt(w ∧ pos_b) − popcnt(w ∧ neg_b)), the mexhat probe's arm-F row kernel. Exact for integer weights; at most 32 planes. Dropping the negative planes turns `bit_sliced_count_equals_the_weighted_sum` red. -- Both are plain u64 ops (`count_ones`, shifts), identical on every realization; no backend code. - **`PackedDatabase` ×3 is not a duplicate.** `packed` (fixed 128/384/1536 + an index) and `cascade` (width-derived split) lay out the same strokes at 2048 bytes, now pinned by `stroke_split_equals_the_cascade_split_at_2048` (red if cascade's split point moves); they keep separate query paths. `cam_pq::PackedDatabase` holds 6-byte CAM codes and shares only the name. Doc cross-links added; not merged. +- **Withdrawn the same day:** `mask_row_window` (signed `i64` column) and `bit_sliced_weighted_count` (signed sum over positive/negative planes). Signed masks are not part of this facade. A 256×256 tile is the palette256 × palette256 address (`u8:u8`), answered by 256×256 distance/compose tables, not by geometric windows. ## 2026-10-10 — x86-64-v2 realization (SSE4.2 without AVX) via the scalar backend