From 363a507fc63b4bb8f066da3176bb31c66946fd36 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 26 Sep 2026 09:56:48 +0000 Subject: [PATCH] Revert "Merge pull request #1293 from AdaWorldAPI/claude/llvm-codegen-polyfill-gni3cw" This reverts commit a1c9cae9243929035245d7bd4486452141ea86a1, reversing changes made to 6f9ab3ade665585a5d921a69837472d4f968a378. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm --- .claude/board/SUPERSESSION-INDEX.md | 4 +- ...26-09-25-dispatch-materialization-trace.md | 39 -- .claude/board/entries/README.md | 3 +- crates/cognitive-shader-driver/Cargo.toml | 3 - crates/cognitive-shader-driver/src/driver.rs | 575 +++--------------- .../tests/dispatch_trace.rs | 140 ----- .../tests/p64_target_identity_probe.rs | 31 +- .../src/translator.rs | 35 +- crates/p64-bridge/src/lib.rs | 118 +--- 9 files changed, 97 insertions(+), 851 deletions(-) delete mode 100644 .claude/board/entries/2026-09-25-dispatch-materialization-trace.md delete mode 100644 crates/cognitive-shader-driver/tests/dispatch_trace.rs diff --git a/.claude/board/SUPERSESSION-INDEX.md b/.claude/board/SUPERSESSION-INDEX.md index 8aa87d1f9..e4199db6b 100644 --- a/.claude/board/SUPERSESSION-INDEX.md +++ b/.claude/board/SUPERSESSION-INDEX.md @@ -14,7 +14,7 @@ ## What this table says **`BindSpace` is the shape of the problem.** Marked RETIRE, and simultaneously the -most-referenced symbol here: **69 crate files, 48 plans, 42 of them blind.** +most-referenced symbol here: **68 crate files, 48 plans, 42 of them blind.** That is a programme, not a cleanup. **`GateState` is the sharpest case: 1 plan names it and *every one* is blind.** @@ -60,7 +60,7 @@ coverage instead. | `GateState` | REPURPOSE | — | 14 | 1 | 1 | | `MergeMode` | REPURPOSE | — | 8 | 14 | 13 | | `ResonanceDto` | REPURPOSE | `PerturbationDto` | 2 | 12 | 7 | -| `BindSpace` | RETIRE | — | 69 | 48 | 42 | +| `BindSpace` | RETIRE | — | 68 | 48 | 42 | | `CollapseGateEmission` | RETIRE | — | 5 | 14 | 12 | | `ThinkingStyle` | RETIRE-toward-contract | — | 52 | 28 | 24 | diff --git a/.claude/board/entries/2026-09-25-dispatch-materialization-trace.md b/.claude/board/entries/2026-09-25-dispatch-materialization-trace.md deleted file mode 100644 index 227c5158e..000000000 --- a/.claude/board/entries/2026-09-25-dispatch-materialization-trace.md +++ /dev/null @@ -1,39 +0,0 @@ -# 2026-09-25 — One shader dispatch materialized O(rows²) to return at most 8 hits - -**Status:** MEASURED · DONE (two collections removed, output digest unchanged; duplicate candidate slots replaced by one SPOFC candidate per row, #1293) · OPEN (the prefilter row list; writing SPOFC into CE64) - -## The measurement (`crates/cognitive-shader-driver/tests/dispatch_trace.rs`) -A counting global allocator measures one `ShaderDriver::dispatch`, after a warm-up, at 16..256 rows. The driver's module doc claims "no allocations beyond top-k + edges". - -| rows | before | after | -|---|---|---| -| 16 | 23 KB, 44 allocs | 1.2 KB, 5 allocs | -| 256 | 5.3 MB, 530 allocs | 2.2 KB, 5 allocs | - -Before, bytes grew with the square of the population and allocations grew linearly. The answer is at most 8 hits either way (`hit_count` is taken after the cut to 8). - -## What was materialized and discarded -- **Content pre-pass.** It pushed two hits per resonant pair into a `Vec` (O(rows²)), then stable-sorted the Vec and cut it to 8. `TopHits` now keeps the best 8 as they arrive. A new hit goes after every kept hit whose resonance is not lower, so ties keep arrival order exactly as the stable sort did. -- **`p64-bridge` `cascade`.** It returned every candidate (up to 256 per row) in a fresh `Vec`, sorted, of which the driver read 4. `cascade_nearest::` keeps the K nearest, allocation-free. Both methods share one private candidate walk. - -## Evidence it is the same computation -- **Digest.** A hash over every field of the crystal (60 configurations × 3 dispatches: rows 16..300, radii 50..MAX, `Auto` + `Ordinal` 0/5/11) is identical before and after (`355d5fccbc424763`). -- **Equivalence tests.** `TopHits` and `cascade_nearest` each have a test against the collect-sort-truncate they replace, on tie-heavy inputs. -- **Disable runs.** Restoring either collection breaks the pinned constant allocation count (37 → 69, 12 → 14). - -## Open -- **The row list.** `passed_rows` (4 bytes per surviving row) is the prefilter's population materialized as a list of row ids. Replacing it with a mask is the natural next step, but it changes `BackingStore::prefilter`'s shape. -- **The rest of the trace.** Stage [4] rotates each hit's row by its `cycle_index` before the XOR, so each hit contributes in its own basis. That is the relative-coordinate step of the end-to-end chain, and it has not yet been traced as such. - -## The `cycle_index` trace found a cancellation (#1293) -Stage [4] builds `cycle_fp = XOR over hits of R^{cycle_index} · row`. Stage [3] offered every supporting relationship (each content pair, each P64 cascade hit) as its own slot, and a row's `cycle_index` is its position, so one row supported N ways gave N identical terms. XOR cancels pairs: on empty predicate planes, 36 of 60 dispatches had a repeated row in top-k, and in some the strongest row vanished from `cycle_fp` entirely. - -**Fix.** One candidate per surviving row. Its relationships aggregate into a SPOFC record (driver-private; the only code precedent is arm-discovery's `CandidateTriple`/`TruthU8`): predicate union, best-resonance partner as object (`Row` or P64 `Palette` target), support count `m`, and `TruthU8 { f = best resonance, c = m·255/(m+k) }` via `evidence_confidence_u8`, extracted from `arm_to_truth_u8` so both share one convention. - -**Effect, 60 dispatches each, old vs new.** Non-empty planes: `cycle_fp` changed in 12, edges and top-k in 60. Empty planes: 48, 48, 48; repeated rows 36 → 0. Disabling only the predicate union drops the non-empty edge change to 12, so 48 of those edge changes are the union's `CausalMask` bits. The digest above is therefore intentionally not preserved by this change. - -**Allocation.** One more allocation (the candidate table, 6 total), still constant in rows; bytes grow 28 per surviving row (4 row id + 24 SPOFC). `dispatch_trace` pins both. - -**Falsifiers, each disable-verified:** re-offering each relationship → the braid test fails; not counting support → both SPOFC evidence tests fail; dropping or overwriting the union → the union test fails (it had no falsifier before; all 111 lib tests stayed green without it). - -**Not done here.** The SPOFC record ends at a `debug_assert`: the emitted CE64 still packs `s = row%256, p = 0, o = (row/4)%256`, `f = c = resonance`. CE64's 24-bit S/P/O could hold the target (palette256³); the driver does not write it. That, and carrying the evidence across cycles without recounting it, is the follow-up. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 137538155..39a0b0e44 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,7 +25,7 @@ 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. -175 entries, 2026-08-06 .. 2026-09-25. +174 entries, 2026-08-06 .. 2026-09-25. | date | entry id | finding | file | |---|---|---|---| @@ -39,7 +39,6 @@ exactly one of them, never both. | 2026-09-25 | `lance12-lancedb039-sweep` | | [2026-09-25-lance12-lancedb039-sweep.md](2026-09-25-lance12-lancedb039-sweep.md) | | 2026-09-25 | `keep-fold` | | [2026-09-25-keep-fold.md](2026-09-25-keep-fold.md) | | 2026-09-25 | `hhtl-ordered-access` | | [2026-09-25-hhtl-ordered-access.md](2026-09-25-hhtl-ordered-access.md) | -| 2026-09-25 | `dispatch-materialization-trace` | | [2026-09-25-dispatch-materialization-trace.md](2026-09-25-dispatch-materialization-trace.md) | | 2026-09-25 | `argon2-in-register-compress-per-tier` | | [2026-09-25-argon2-in-register-compress-per-tier.md](2026-09-25-argon2-in-register-compress-per-tier.md) | | 2026-09-25 | `aperture-prefix-lowers-to-range` | | [2026-09-25-aperture-prefix-lowers-to-range.md](2026-09-25-aperture-prefix-lowers-to-range.md) | | 2026-09-25 | `aperture-masks-and-kernel-gap` | | [2026-09-25-aperture-masks-and-kernel-gap.md](2026-09-25-aperture-masks-and-kernel-gap.md) | diff --git a/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index a6d17c7b6..fb8de61f9 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -41,9 +41,6 @@ lance-graph-ontology = { path = "../lance-graph-ontology", default-features = fa p64-bridge = { path = "../p64-bridge" } bgz17 = { path = "../bgz17" } causal-edge = { path = "../causal-edge" } -# SPOFC truth (`TruthU8`, the evidence-confidence convention). Zero-dependency -# with default features. -lance-graph-arm-discovery = { path = "../lance-graph-arm-discovery" } ndarray = { path = "../../../ndarray", default-features = false, features = ["std", "hpc-extras"] } # Optional: real thinking-engine wiring behind a feature gate, matching diff --git a/crates/cognitive-shader-driver/src/driver.rs b/crates/cognitive-shader-driver/src/driver.rs index be69406b0..1590b2578 100644 --- a/crates/cognitive-shader-driver/src/driver.rs +++ b/crates/cognitive-shader-driver/src/driver.rs @@ -30,7 +30,6 @@ use causal_edge::edge::{CausalEdge64, InferenceType}; use causal_edge::pearl::CausalMask; use causal_edge::plasticity::PlasticityState; use causal_edge::tables::{unpack_c, unpack_f, NarsTables}; -use lance_graph_arm_discovery::translator::{evidence_confidence_u8, TruthU8, NARS_PERSONALITY_K}; use lance_graph_contract::cognitive_shader::{ AlphaComposite, CognitiveShaderDriver, EmitMode, MaterializeProvenance, MetaSummary, NullSink, RungElevator, RungLevel, ShaderBus, ShaderCrystal, ShaderDispatch, ShaderHit, ShaderResonance, @@ -223,29 +222,51 @@ impl ShaderDriver { } } - /// Stage [3]: every supporting relationship for every prefilter row — the - /// content pre-pass partners and the cascade targets — aggregated per row - /// into one SPOFC candidate (`CandidateTable`). - fn collect_candidates( - &self, - backing: &BackingStore<'_>, - req: &ShaderDispatch, - passed_rows: &[u32], - style_ord: u8, - effective_layer_mask: u8, - ) -> CandidateTable { + /// Run one dispatch, feeding a sink. This is the single hot path. + fn run(&self, req: &ShaderDispatch, sink: &mut S) -> ShaderCrystal { + // W3 read-shim: select the substrate (singleton BindSpace by default; + // the designated MailboxSoA under `mailbox-thoughtspace`). The body + // below is written ONCE against `backing` — no `#[cfg]` branches here. + let backing = self.backing(); + + // ── Rung ascent loop (D-TRI-6) ────────────────────────────────────── + // The persistent per-driver RungElevator's CURRENT level — advanced by + // the PREVIOUS cycle's gate via `on_gate` at the end of this fn — selects + // THIS cycle's cascade plane breadth. A dispatch whose requested base + // rung differs from the elevator's tracked base resets the elevator + // first, so streaks never leak across unrelated dispatch bases. At base + // the mask is unchanged (no regression); above base it widens by union + // (see `rung_widened_layer_mask` + its HAZARD(a) note). + let effective_layer_mask = { + let mut elevator = self + .rung_elevator + .write() + .expect("rung_elevator RwLock poisoned"); + if elevator.base != req.rung { + *elevator = RungElevator::new(req.rung); + } + rung_widened_layer_mask(elevator.base, elevator.level, req.layer_mask) + }; + + // [1] Cheap meta prefilter (u32 column sweep). + let passed_rows = backing.prefilter(req.rows, &req.meta_prefilter); + + // [2] Resolve style — Auto reads the qualia of the FIRST surviving row. + // D-CSV-5b: qualia is QualiaI4_16D; the shim converts to f32 at the read. + let qualia_f32_arr: [f32; 17] = if let Some(&row) = passed_rows.first() { + backing.qualia_17d(row as usize) + } else { + [0.0f32; 17] + }; + let style_ord = auto_style::resolve(req.style, &qualia_f32_arr[..]); + // [3] Shader cascade — bgz17 O(1) per probed block. // Snapshot the planes under the read lock so the cascade sees a // consistent topology even if `update_planes` fires mid-dispatch. let planes_snapshot: [[u64; 64]; 8] = **self.planes.read().expect("planes RwLock poisoned"); let shader = CognitiveShader::new(planes_snapshot, &self.semiring); let max_dist = (self.semiring.k as f32) * (self.semiring.k as f32); - // Every supporting relationship found for a row — a pre-pass partner - // or a cascade target — is recorded against that row's position, so a - // row with many partners is ONE candidate carrying all of its support - // (SPOFC), not many candidates. Duplicate candidates would share one - // rotation basis and cancel each other in the XOR braid below. - let mut candidates = CandidateTable::new(passed_rows.len()); + let mut hits = Vec::::with_capacity(passed_rows.len().min(64)); // TD-INT-10: optional NARS truth-table lookups per hit. let nars_tables = self.nars_tables.as_deref(); @@ -272,21 +293,22 @@ impl ShaderDriver { ndarray::hpc::bitwise::hamming_distance_raw(fp_i_bytes, fp_j_bytes) as u32; let resonance = 1.0 - (hamming as f32 / FP_BITS); if resonance >= min_resonance { - let distance = hamming.min(u16::MAX as u32) as u16; - candidates.support( - i, + hits.push(ShaderHit { + row: row_i, + distance: hamming.min(u16::MAX as u32) as u16, + predicates: CONTENT_MATCH_PREDICATE, + _pad: 0, resonance, - distance, - CONTENT_MATCH_PREDICATE, - SupportPartner::Row(row_j), - ); - candidates.support( - j_off, + cycle_index: i as u32, + }); + hits.push(ShaderHit { + row: row_j, + distance: hamming.min(u16::MAX as u32) as u16, + predicates: CONTENT_MATCH_PREDICATE, + _pad: 0, resonance, - distance, - CONTENT_MATCH_PREDICATE, - SupportPartner::Row(row_i), - ); + cycle_index: j_off as u32, + }); } } } @@ -300,11 +322,8 @@ impl ShaderDriver { // Rows with edge=0 default to palette 0 (identity probe). let edge = backing.edge(row as usize); let query = edge.s_idx(); - // The 4 nearest, kept as they arrive: the full candidate list - // (up to 256 per row) was sorted and then cut to 4. - let (nearest, n_nearest) = - shader.cascade_nearest::<4>(query, req.radius, effective_layer_mask); - for hit in &nearest[..n_nearest] { + let raw = shader.cascade(query, req.radius, effective_layer_mask); + for hit in raw.into_iter().take(4) { let resonance = 1.0 / (1.0 + (hit.distance as f32 / max_dist)); // TD-INT-10: NARS truth lookup against precomputed tables. @@ -321,78 +340,24 @@ impl ShaderDriver { let _revised_truth = (unpack_f(packed), unpack_c(packed)); } - candidates.support( - cycle_idx, + hits.push(ShaderHit { + row, + distance: hit.distance, + predicates: hit.predicates, + _pad: 0, resonance, - hit.distance, - hit.predicates, - SupportPartner::Palette(hit.target), - ); + cycle_index: cycle_idx as u32, + }); } } - candidates - } - - /// Run one dispatch, feeding a sink. This is the single hot path. - fn run(&self, req: &ShaderDispatch, sink: &mut S) -> ShaderCrystal { - // W3 read-shim: select the substrate (singleton BindSpace by default; - // the designated MailboxSoA under `mailbox-thoughtspace`). The body - // below is written ONCE against `backing` — no `#[cfg]` branches here. - let backing = self.backing(); - - // ── Rung ascent loop (D-TRI-6) ────────────────────────────────────── - // The persistent per-driver RungElevator's CURRENT level — advanced by - // the PREVIOUS cycle's gate via `on_gate` at the end of this fn — selects - // THIS cycle's cascade plane breadth. A dispatch whose requested base - // rung differs from the elevator's tracked base resets the elevator - // first, so streaks never leak across unrelated dispatch bases. At base - // the mask is unchanged (no regression); above base it widens by union - // (see `rung_widened_layer_mask` + its HAZARD(a) note). - let effective_layer_mask = { - let mut elevator = self - .rung_elevator - .write() - .expect("rung_elevator RwLock poisoned"); - if elevator.base != req.rung { - *elevator = RungElevator::new(req.rung); - } - rung_widened_layer_mask(elevator.base, elevator.level, req.layer_mask) - }; - - // [1] Cheap meta prefilter (u32 column sweep). - let passed_rows = backing.prefilter(req.rows, &req.meta_prefilter); - - // [2] Resolve style — Auto reads the qualia of the FIRST surviving row. - // D-CSV-5b: qualia is QualiaI4_16D; the shim converts to f32 at the read. - let qualia_f32_arr: [f32; 17] = if let Some(&row) = passed_rows.first() { - backing.qualia_17d(row as usize) - } else { - [0.0f32; 17] - }; - let style_ord = auto_style::resolve(req.style, &qualia_f32_arr[..]); - - // [3] Collect every supporting relationship per candidate row. - let candidates = - self.collect_candidates(&backing, req, &passed_rows, style_ord, effective_layer_mask); - - // Each candidate row once, ranked by its best partner resonance; its - // position in the prefilter list stays its rotation basis. Ties keep - // row-position order. - let mut top = TopHits::new(); - for (pos, cand) in candidates.iter() { - top.offer(cand.hit(passed_rows[pos], pos)); - } - let hits: &[ShaderHit] = top.as_slice(); - // A selected candidate always carries its evidence: at least one - // supporting relationship, hence a non-zero evidence confidence. The - // support is retained here (SPOFC); writing it into the emitted - // CausalEdge64 is the follow-up, so this change leaves edges as-is. - debug_assert!(hits.iter().all(|h| { - candidates - .spofc(h.cycle_index as usize, h.row) - .is_some_and(|s| s.truth.confidence > 0) - })); + // Sort by resonance descending, keep top-8. + hits.sort_by(|a, b| { + b.resonance + .partial_cmp(&a.resonance) + .unwrap_or(std::cmp::Ordering::Equal) + }); + hits.truncate(8); // [4] Build the cycle_fingerprint with positional Markov braiding. // Each row is rotated by its cycle_index before XOR — preserves @@ -400,7 +365,7 @@ impl ShaderDriver { // Per I-SUBSTRATE-MARKOV: this activates the Markov ±5 property // even in binary space; full f32 VSA bundle is the next step. let mut cycle_fp = [0u64; WORDS_PER_FP]; - for h in hits { + for h in &hits { let row_words = backing.content_row(h.row as usize); let pos = (h.cycle_index as usize) % WORDS_PER_FP; for (i, w) in row_words.iter().enumerate() { @@ -409,7 +374,7 @@ impl ShaderDriver { } // [5] Entropy + std-dev of top-k resonances. - let (entropy, std_dev) = entropy_std(hits); + let (entropy, std_dev) = entropy_std(&hits); // [6] FreeEnergy gate (principled F from resonance + KL surrogate). let top_resonance = hits.first().map(|h| h.resonance).unwrap_or(0.0); @@ -572,7 +537,7 @@ impl ShaderDriver { .alpha_saturation_override .unwrap_or(ALPHA_SATURATION_THRESHOLD); Some(alpha_front_to_back_composite( - hits, + &hits, |row| { hit_qualia_f32 .iter() @@ -954,218 +919,6 @@ impl Default for CognitiveShaderBuilder { // Helpers // ═══════════════════════════════════════════════════════════════════════════ -/// The other end of one supporting relationship: a content pre-pass partner -/// (another row) or a cascade target (a palette archetype). Two different -/// id spaces, so they stay two variants rather than one number. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum SupportPartner { - Row(u32), - Palette(u8), -} - -/// One candidate row's support, in the SPOFC shape used by -/// `lance-graph-arm-discovery` (`{s, p, o, f, c}` with a [`TruthU8`]): -/// subject = the row, predicates = the union of its relationships' kinds, -/// object = its best supporting relationship, frequency = that -/// relationship's resonance, confidence = the -/// NARS evidence confidence of how many supporting relationships were found -/// (`m / (m + k)`, `evidence_confidence_u8`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct Spofc { - pub subject: u32, - pub predicates: u8, - pub object: SupportPartner, - pub truth: TruthU8, - /// `m`: supporting relationships found for the subject. - pub support: u16, -} - -#[derive(Clone, Copy)] -struct Candidate { - resonance: f32, - distance: u16, - predicates: u8, - partner: SupportPartner, - support: u16, -} - -impl Candidate { - fn hit(&self, row: u32, pos: usize) -> ShaderHit { - ShaderHit { - row, - distance: self.distance, - predicates: self.predicates, - _pad: 0, - resonance: self.resonance, - cycle_index: pos as u32, - } - } -} - -/// Per prefilter position: the best supporting relationship found for that -/// row and how many were found. One record per surviving row, not per pair. -pub(crate) struct CandidateTable { - slots: Vec>, -} - -impl CandidateTable { - pub(crate) fn new(rows: usize) -> Self { - Self { - slots: vec![None; rows], - } - } - - /// Record one supporting relationship for the row at `pos`. The best - /// relationship (highest resonance; on a tie the first) sets the ranking, - /// distance and object; the predicate bits are the union over all of them. - fn support( - &mut self, - pos: usize, - resonance: f32, - distance: u16, - predicates: u8, - partner: SupportPartner, - ) { - let slot = &mut self.slots[pos]; - match slot { - Some(c) => { - c.support = c.support.saturating_add(1); - // Every relationship's kind stays on the candidate, whichever - // one ranks it. - c.predicates |= predicates; - if resonance > c.resonance { - c.resonance = resonance; - c.distance = distance; - c.partner = partner; - } - } - None => { - *slot = Some(Candidate { - resonance, - distance, - predicates, - partner, - support: 1, - }); - } - } - } - - fn iter(&self) -> impl Iterator { - self.slots - .iter() - .enumerate() - .filter_map(|(pos, c)| c.as_ref().map(|c| (pos, c))) - } - - /// The SPOFC record of the row at `pos`, if it has any support. - pub(crate) fn spofc(&self, pos: usize, row: u32) -> Option { - self.slots.get(pos).copied().flatten().map(|c| Spofc { - subject: row, - predicates: c.predicates, - object: c.partner, - truth: TruthU8 { - frequency: (c.resonance.clamp(0.0, 1.0) * 255.0) as u8, - confidence: evidence_confidence_u8(u32::from(c.support), NARS_PERSONALITY_K), - }, - support: c.support, - }) - } -} - -/// The best 8 hits by resonance, kept as they arrive. -/// -/// Equivalent to collecting every hit, stable-sorting by resonance descending -/// and truncating to 8: a new hit goes after every kept hit whose resonance is -/// not lower, so equal resonances keep arrival order, and a hit that would land -/// ninth is dropped. Holds exactly for finite resonances. -struct TopHits { - buf: [ShaderHit; 8], - len: usize, -} - -impl TopHits { - fn new() -> Self { - Self { - buf: [ShaderHit::default(); 8], - len: 0, - } - } - - #[inline] - fn offer(&mut self, hit: ShaderHit) { - let pos = self.buf[..self.len] - .iter() - .position(|kept| kept.resonance < hit.resonance) - .unwrap_or(self.len); - if pos >= self.buf.len() { - return; - } - let last = self.len.min(self.buf.len() - 1); - self.buf.copy_within(pos..last, pos + 1); - self.buf[pos] = hit; - self.len = (self.len + 1).min(self.buf.len()); - } - - fn as_slice(&self) -> &[ShaderHit] { - &self.buf[..self.len] - } -} - -#[cfg(test)] -mod top_hits_tests { - use super::*; - - /// The replaced implementation: collect everything, stable-sort by - /// resonance descending, cut to 8. - fn collect_sort_truncate(stream: &[ShaderHit]) -> Vec { - let mut v = stream.to_vec(); - v.sort_by(|a, b| { - b.resonance - .partial_cmp(&a.resonance) - .unwrap_or(std::cmp::Ordering::Equal) - }); - v.truncate(8); - v - } - - fn key(h: &ShaderHit) -> (u32, u32, u32) { - (h.row, h.resonance.to_bits(), h.cycle_index) - } - - #[test] - fn top_hits_matches_collect_sort_truncate_including_ties() { - let mut state = 0x9E37_79B9_7F4A_7C15u64; - let mut next = || { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - state - }; - for len in [0usize, 1, 7, 8, 9, 20, 300] { - for _ in 0..200 { - // Few distinct resonance values, so ties are common and the - // arrival-order rule is actually exercised. - let stream: Vec = (0..len) - .map(|i| ShaderHit { - row: i as u32, - resonance: (next() % 6) as f32 / 5.0, - cycle_index: (next() % 1000) as u32, - ..Default::default() - }) - .collect(); - let mut top = TopHits::new(); - for h in &stream { - top.offer(*h); - } - let want: Vec<_> = collect_sort_truncate(&stream).iter().map(key).collect(); - let got: Vec<_> = top.as_slice().iter().map(key).collect(); - assert_eq!(got, want, "len {len}"); - } - } - } -} - fn entropy_std(hits: &[ShaderHit]) -> (f32, f32) { if hits.is_empty() { return (0.0, 0.0); @@ -1329,190 +1082,6 @@ mod tests { ColumnWindow, MetaFilter, ShaderDispatch, StyleSelector, }; - /// `n` rows, each with one distinct content bit, so every pair resonates - /// equally in the content pre-pass; empty predicate planes, so the cascade - /// returns nothing and the pre-pass alone fills the top 8. - fn empty_plane_driver(n: u32) -> ShaderDriver { - let mut bs = BindSpace::zeros(n as usize); - let indices: Vec = (0..n as u16).collect(); - crate::engine_bridge::ingest_codebook_indices(&mut bs, &indices, 1, 1000, 0); - CognitiveShaderBuilder::new() - .bindspace(Arc::new(bs)) - .semiring(Arc::new(demo_semiring())) - .planes([[0u64; 64]; 8]) - .build() - } - - fn all_rows(n: u32) -> ShaderDispatch { - ShaderDispatch { - rows: ColumnWindow::new(0, n), - meta_prefilter: MetaFilter::ALL, - layer_mask: 0xFF, - radius: u16::MAX, - style: StyleSelector::Auto, - max_cycles: u16::MAX / 4, - ..Default::default() - } - } - - /// Regression: with the cascade empty, the pre-pass emitted row i once per - /// resonating partner. Row 0 took four of the eight slots with one basis, - /// and its four rotated contributions XOR-cancelled out of cycle_fp. - #[test] - fn repeated_support_is_one_candidate_and_survives_the_braid() { - let n = 16u32; - let driver = empty_plane_driver(n); - let req = all_rows(n); - let crystal = driver.dispatch(&req); - let k = crystal.bus.resonance.hit_count as usize; - let top = &crystal.bus.resonance.top_k[..k]; - assert_eq!(k, 8, "sixteen rows with support must fill all eight slots"); - - // Distinct candidate rows: none takes two slots. - let mut rows: Vec = top.iter().map(|h| h.row).collect(); - rows.sort_unstable(); - rows.dedup(); - assert_eq!(rows.len(), k, "a row occupied more than one slot: {top:?}"); - - // The basis is the row's prefilter position (here, its row id). - assert!(top.iter().all(|h| h.cycle_index == h.row)); - - // The braid is exactly one rotated contribution per selected row, and - // the strongest candidate is in it. - let bs = driver.bindspace(); - let mut expected = [0u64; WORDS_PER_FP]; - for h in top { - let words = bs.fingerprints.content_row(h.row as usize); - let pos = h.cycle_index as usize % WORDS_PER_FP; - for (i, w) in words.iter().enumerate() { - expected[(i + pos) % WORDS_PER_FP] ^= *w; - } - } - assert_eq!(crystal.bus.cycle_fingerprint, expected); - let strongest = top[0]; - let mut alone = [0u64; WORDS_PER_FP]; - for (i, w) in bs - .fingerprints - .content_row(strongest.row as usize) - .iter() - .enumerate() - { - alone[(i + strongest.cycle_index as usize) % WORDS_PER_FP] ^= *w; - } - assert!( - alone - .iter() - .zip(crystal.bus.cycle_fingerprint.iter()) - .all(|(a, f)| a & f == *a), - "the strongest row's contribution is missing from cycle_fp" - ); - assert_eq!( - crystal - .bus - .cycle_fingerprint - .iter() - .map(|w| w.count_ones()) - .sum::(), - k as u32, - "one bit per selected row: nothing cancelled" - ); - } - - /// F-ARW-TARGET-1's fixture: one source row whose cascade returns four - /// distinct targets at equal distance. They were four candidate slots and - /// four identical CE64s; now they are one candidate carrying all four. - /// A row supported by several relationship kinds keeps all of them: the - /// predicate bits are the union, while ranking and object follow the best - /// resonance. Taking only the best relationship's bits would silently drop - /// the weaker kinds from the emitted `CausalMask`. - #[test] - fn spofc_predicates_are_the_union_of_supporting_relationships() { - let mut t = CandidateTable::new(1); - t.support(0, 0.5, 10, 0b001, SupportPartner::Palette(1)); - t.support(0, 0.9, 5, 0b010, SupportPartner::Palette(2)); - t.support(0, 0.3, 20, 0b100, SupportPartner::Row(7)); - let s = t.spofc(0, 0).expect("row 0 was supported"); - assert_eq!(s.predicates, 0b111, "every relationship kind kept"); - assert_eq!(s.support, 3); - // The best (0.9) relationship ranks it and names the object; the - // union does not come from simply keeping the last or first bits. - assert!(matches!(s.object, SupportPartner::Palette(2))); - assert_eq!(s.truth.frequency, (0.9f32 * 255.0) as u8); - } - - #[test] - fn p64_targets_survive_as_spofc_support() { - let q = lance_graph_contract::qualia::QualiaI4_16D::ZERO; - let bs = BindSpaceBuilder::new(1) - .push( - &[0u64; WORDS_PER_FP], - MetaWord::new(1, 1, 200, 200, 5), - 0, - q, - 0, - 0, - ) - .build(); - let semiring = PaletteSemiring::build(&Palette { - entries: (0..4).map(|_| Base17 { dims: [0i16; 17] }).collect(), - }); - let mut planes = [[0u64; 64]; 8]; - planes[0][0] = 1; - let driver = CognitiveShaderBuilder::new() - .bindspace(Arc::new(bs)) - .semiring(Arc::new(semiring)) - .planes(planes) - .build(); - let req = ShaderDispatch { - rows: ColumnWindow::new(0, 1), - meta_prefilter: MetaFilter::ALL, - layer_mask: 0b0000_0001, - radius: u16::MAX, - style: StyleSelector::Ordinal(1), - ..ShaderDispatch::default() - }; - let backing = driver.backing(); - let passed = backing.prefilter(req.rows, &req.meta_prefilter); - assert_eq!(passed.len(), 1); - let table = driver.collect_candidates(&backing, &req, &passed, 1, req.layer_mask); - let s = table - .spofc(0, passed[0]) - .expect("the row has cascade support"); - assert_eq!(s.support, 4, "all four target relations retained"); - assert!(matches!(s.object, SupportPartner::Palette(t) if t < 4)); - assert_eq!(table.iter().count(), 1, "one candidate, not four"); - } - - /// The repeats are evidence, not noise: each row keeps a count of its - /// supporting relationships and the best one, in SPOFC form. - #[test] - fn repeated_support_is_retained_as_spofc_evidence() { - let n = 16u32; - let driver = empty_plane_driver(n); - let req = all_rows(n); - let backing = driver.backing(); - let passed = backing.prefilter(req.rows, &req.meta_prefilter); - let style = auto_style::resolve(req.style, &backing.qualia_17d(passed[0] as usize)[..]); - let table = driver.collect_candidates(&backing, &req, &passed, style, req.layer_mask); - for (pos, &row) in passed.iter().enumerate() { - let s = table.spofc(pos, row).expect("every row has partners"); - // Every other row resonates with this one: n - 1 relationships. - assert_eq!(s.support, (n - 1) as u16, "row {row}"); - assert_eq!(s.subject, row); - assert_eq!( - s.truth.confidence, - evidence_confidence_u8(n - 1, NARS_PERSONALITY_K) - ); - // The best partner is another row, never the subject itself. - assert!(matches!(s.object, SupportPartner::Row(p) if p != row)); - } - // More evidence, more confidence: fifteen partners beat one. - assert!( - evidence_confidence_u8(n - 1, NARS_PERSONALITY_K) - > evidence_confidence_u8(1, NARS_PERSONALITY_K) - ); - } - fn demo_bindspace() -> BindSpace { use lance_graph_contract::qualia::QualiaI4_16D; let q = QualiaI4_16D::ZERO; diff --git a/crates/cognitive-shader-driver/tests/dispatch_trace.rs b/crates/cognitive-shader-driver/tests/dispatch_trace.rs deleted file mode 100644 index 976e9ba08..000000000 --- a/crates/cognitive-shader-driver/tests/dispatch_trace.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! What does one `ShaderDriver::dispatch` materialize, and does it grow with -//! the population it looks at? -//! -//! The driver's module doc claims "no allocations beyond top-k + edges". A -//! counting global allocator measures one dispatch (after a warm-up) at -//! several population sizes. Everything the cycle materializes shows up here; -//! nothing is inferred from reading the code. -//! -//! Before the fix: 23 KB / 44 allocations at 16 rows, 5.3 MB / 530 at 256, -//! for an answer of at most 8 hits. Now: a constant allocation count, and bytes -//! grow only per surviving row (its prefilter entry and its SPOFC candidate -//! record), never per pair. - -use std::alloc::{GlobalAlloc, Layout, System}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; - -use bgz17::base17::Base17; -use bgz17::palette::Palette; -use bgz17::palette_semiring::PaletteSemiring; - -use cognitive_shader_driver::bindspace::BindSpace; -use cognitive_shader_driver::driver::CognitiveShaderBuilder; -use cognitive_shader_driver::engine_bridge::ingest_codebook_indices; -use cognitive_shader_driver::{ - CognitiveShaderDriver, ColumnWindow, MetaFilter, ShaderDispatch, StyleSelector, -}; - -struct Counting; - -static BYTES: AtomicUsize = AtomicUsize::new(0); -static COUNT: AtomicUsize = AtomicUsize::new(0); - -// SAFETY: a pure pass-through to `System`; the counters are the only addition. -unsafe impl GlobalAlloc for Counting { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - BYTES.fetch_add(layout.size(), Ordering::Relaxed); - COUNT.fetch_add(1, Ordering::Relaxed); - // SAFETY: same layout, same contract as the caller's. - unsafe { System.alloc(layout) } - } - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - // SAFETY: `ptr` came from `alloc` above with this `layout`. - unsafe { System.dealloc(ptr, layout) } - } - unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - BYTES.fetch_add(new_size, Ordering::Relaxed); - COUNT.fetch_add(1, Ordering::Relaxed); - // SAFETY: forwarded unchanged. - unsafe { System.realloc(ptr, layout, new_size) } - } -} - -#[global_allocator] -static A: Counting = Counting; - -fn palette_256() -> PaletteSemiring { - let entries: Vec = (0..256) - .map(|i| { - let mut dims = [0i16; 17]; - dims[0] = (i * 100 % 3400) as i16; - dims[1] = ((i * 37) % 200) as i16; - Base17 { dims } - }) - .collect(); - PaletteSemiring::build(&Palette { entries }) -} - -fn planes_chain() -> [[u64; 64]; 8] { - let mut planes = [[0u64; 64]; 8]; - for (i, row) in planes[0].iter_mut().enumerate().take(63) { - *row |= 1u64 << (i + 1); - } - for (i, row) in planes[2].iter_mut().enumerate() { - *row |= 1u64 << i; - } - planes -} - -/// Bytes and allocation count of one dispatch over `n` rows. -fn measure(n: u32) -> (usize, usize, u16) { - let mut bs = BindSpace::zeros(n as usize); - let indices: Vec = (0..n as u16).collect(); - ingest_codebook_indices(&mut bs, &indices, 1, 1000, 0); - let driver = CognitiveShaderBuilder::new() - .bindspace(Arc::new(bs)) - .semiring(Arc::new(palette_256())) - .planes(planes_chain()) - .build(); - let req = ShaderDispatch { - rows: ColumnWindow::new(0, n), - meta_prefilter: MetaFilter::ALL, - layer_mask: 0xFF, - radius: u16::MAX, - style: StyleSelector::Auto, - max_cycles: u16::MAX / 4, - ..Default::default() - }; - let _warm = driver.dispatch(&req); - let (b0, c0) = (BYTES.load(Ordering::Relaxed), COUNT.load(Ordering::Relaxed)); - let crystal = driver.dispatch(&req); - let (b1, c1) = (BYTES.load(Ordering::Relaxed), COUNT.load(Ordering::Relaxed)); - (b1 - b0, c1 - c0, crystal.bus.resonance.hit_count) -} - -#[test] -fn trace_dispatch_materialization() { - println!( - "{:>6} {:>12} {:>12} {:>10}", - "rows", "bytes", "allocs", "hit_count" - ); - let sizes = [16u32, 32, 64, 128, 256]; - let rows: Vec<(u32, usize, usize, u16)> = sizes - .iter() - .map(|&n| { - let (bytes, allocs, hits) = measure(n); - println!("{n:>6} {bytes:>12} {allocs:>12} {hits:>10}"); - (n, bytes, allocs, hits) - }) - .collect(); - let (n0, b0, a0, _) = rows[0]; - for &(n, bytes, allocs, hits) in &rows { - assert!(hits > 0, "fixture must produce hits at {n} rows"); - // The cycle keeps at most 8 hits and 4 cascade results per row, so the - // number of allocations must not depend on how many rows it looks at. - assert_eq!( - allocs, a0, - "{n} rows: {allocs} allocations vs {a0} at {n0} rows" - ); - // Per surviving row: the prefilter's row list (4 bytes) and one SPOFC - // candidate record carrying that row's aggregated support (24 bytes). - // Nothing grows with the number of PAIRS. - let per_row_limit = 28 * (n - n0) as usize; - assert!( - bytes - b0 <= per_row_limit, - "{n} rows: {bytes} bytes, {} more than at {n0} rows (limit {per_row_limit})", - bytes - b0 - ); - } -} diff --git a/crates/cognitive-shader-driver/tests/p64_target_identity_probe.rs b/crates/cognitive-shader-driver/tests/p64_target_identity_probe.rs index 1e265a617..118647b85 100644 --- a/crates/cognitive-shader-driver/tests/p64_target_identity_probe.rs +++ b/crates/cognitive-shader-driver/tests/p64_target_identity_probe.rs @@ -12,18 +12,6 @@ //! source BindSpace row but not `CascadeHit.target`. CE64 emission then derives //! S/P/O from that source row. If all emitted CE64 words are identical, target //! identity is proven lost at this seam. -//! -//! Re-pinned 2026-09-25 (#1293). The four targets used to become four -//! candidate slots and four byte-identical CE64s — repeated emissions of one -//! relation. Stage [3] now aggregates every supporting relationship per source -//! row (SPOFC): the four targets are ONE candidate with support 4, whose best -//! target is kept as its object (`driver::tests`, -//! `p64_targets_survive_as_spofc_support`). What stands is narrower than -//! "CE64 cannot carry the target": its 24-bit S/P/O can hold three palette256 -//! indices, so a target has a place to go. The driver just never writes it -//! there — it packs S/O from the source row id and P = 0. So exactly one edge -//! now represents the four relations, with the target left in the SPOFC record. -//! Writing the aggregated support and target into CE64 is the follow-up. use std::collections::BTreeSet; use std::sync::Arc; @@ -107,16 +95,19 @@ fn distinct_p64_targets_collapse_to_one_emitted_ce64_identity() { let crystal = driver.dispatch(&req); let n = crystal.bus.emitted_edge_count as usize; - // One source row with four relations is one candidate, so one CE64 — not - // four identical copies of it. - assert_eq!( - n, - 1, - "the four targets of one source row must be one candidate: {:?}", - &crystal.bus.emitted_edges[..n] + assert!( + n > 1, + "probe is vacuous: the live driver emitted fewer than two CE64s (n={n})" + ); + + let emitted = &crystal.bus.emitted_edges[..n]; + let first = emitted[0]; + assert!( + emitted.iter().all(|&edge| edge == first), + "F-ARW-TARGET-1 falsified: distinct P64 targets remain distinguishable in emitted CE64s: {emitted:?}" ); - let decoded = CausalEdge64(crystal.bus.emitted_edges[0]); + let decoded = CausalEdge64(first); assert_eq!(decoded.s_idx(), 0, "source row 0 is projected to S=0"); assert_eq!(decoded.p_idx(), 0, "current emission writes P=0"); assert_eq!(decoded.o_idx(), 0, "source row 0 is projected to O=0"); diff --git a/crates/lance-graph-arm-discovery/src/translator.rs b/crates/lance-graph-arm-discovery/src/translator.rs index 01e90e18c..32775fe9e 100644 --- a/crates/lance-graph-arm-discovery/src/translator.rs +++ b/crates/lance-graph-arm-discovery/src/translator.rs @@ -76,38 +76,15 @@ pub fn arm_to_truth_u8(rule: &CandidateRule, k: u32) -> TruthU8 { } else { ((rule.cooccur as u64 * 255) / rule.antecedent_count as u64).min(255) as u8 }; - let confidence = evidence_confidence_u8(rule.cooccur, k); + // confidence = m / (m + k), m = cooccur (integer evidential mass); k>0 ⇒ denom>0 + let m = rule.cooccur as u64; + let confidence = ((m * 255) / (m + k as u64)) as u8; TruthU8 { frequency, confidence, } } -/// NARS confidence of `m` units of evidence, `c = m / (m + k)`, quantised so -/// that `255` = 1.0. The confidence half of [`arm_to_truth_u8`], for callers -/// whose evidence is a count of supporting relationships rather than a rule. -/// -/// ``` -/// use lance_graph_arm_discovery::translator::{evidence_confidence_u8, NARS_PERSONALITY_K}; -/// assert_eq!(evidence_confidence_u8(0, NARS_PERSONALITY_K), 0); -/// assert_eq!(evidence_confidence_u8(1, NARS_PERSONALITY_K), 127); -/// assert_eq!(evidence_confidence_u8(8, NARS_PERSONALITY_K), 226); -/// ``` -/// -/// # Panics -/// -/// If `k == 0`: any positive evidence would then be dogmatic (confidence 1.0). -#[must_use] -pub fn evidence_confidence_u8(m: u32, k: u32) -> u8 { - assert!( - k > 0, - "NARS personality constant k must be > 0; k=0 makes any evidence dogmatic (confidence=1.0)" - ); - // m / (m + k) on integers; k > 0 so the denominator is positive. - let m = m as u64; - ((m * 255) / (m + k as u64)) as u8 -} - /// An `f32` NARS truth — **edge convenience only** (see module docs). Derived /// from [`TruthU8`]; never read inside the discovery path. #[derive(Debug, Clone, Copy, PartialEq)] @@ -280,11 +257,7 @@ mod tests { #[test] fn triple_projection() { - let t = CandidateTriple::from_rule( - &rule(90, 100, 400), - &DebugProjector::default(), - NARS_PERSONALITY_K, - ); + let t = CandidateTriple::from_rule(&rule(90, 100, 400), &DebugProjector::default(), NARS_PERSONALITY_K); assert_eq!(t.s, "arm:feat0=cat1"); assert_eq!(t.p, "implies"); assert_eq!(t.o, "arm:feat1=cat0"); diff --git a/crates/p64-bridge/src/lib.rs b/crates/p64-bridge/src/lib.rs index 40d0d6940..cbd75979c 100644 --- a/crates/p64-bridge/src/lib.rs +++ b/crates/p64-bridge/src/lib.rs @@ -350,7 +350,7 @@ pub mod cognitive_shader { } /// Result of a cascade query. - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone)] pub struct CascadeHit { /// Target archetype index (0..255). pub target: u8, @@ -381,59 +381,9 @@ pub mod cognitive_shader { /// /// Returns hits sorted by distance ascending. pub fn cascade(&self, query: u8, radius: u16, layer_mask: u8) -> Vec { - let mut hits = Vec::new(); - self.for_each_candidate(query, radius, layer_mask, |h| hits.push(h)); - hits.sort_by_key(|h| h.distance); - hits - } - - /// The `K` nearest hits of [`Self::cascade`], without allocating. - /// - /// Returns the same hits, in the same order, as - /// `cascade(..).into_iter().take(K)`: nearest first, and equal - /// distances keep candidate order (the order a stable sort keeps). - /// The second value is how many of the `K` slots are filled. - pub fn cascade_nearest( - &self, - query: u8, - radius: u16, - layer_mask: u8, - ) -> ([CascadeHit; K], usize) { - let empty = CascadeHit { - target: 0, - distance: 0, - predicates: 0, - }; - let mut buf = [empty; K]; - let mut len = 0usize; - self.for_each_candidate(query, radius, layer_mask, |h| { - let pos = buf[..len] - .iter() - .position(|kept| kept.distance > h.distance) - .unwrap_or(len); - if pos >= K { - return; - } - let last = len.min(K - 1); - buf.copy_within(pos..last, pos + 1); - buf[pos] = h; - len = (len + 1).min(K); - }); - (buf, len) - } - - /// Visit every candidate within `radius`, in candidate order (block - /// column ascending, then the 4 archetypes of each block). - fn for_each_candidate( - &self, - query: u8, - radius: u16, - layer_mask: u8, - mut visit: impl FnMut(CascadeHit), - ) { let block_row = query as usize / 4; if block_row >= 64 { - return; + return Vec::new(); } // Collect which block-columns are active across selected layers @@ -455,6 +405,7 @@ pub mod cognitive_shader { } // Expand active block-columns to archetype indices, lookup distance + let mut hits = Vec::new(); let mut bits = active_cols; while bits != 0 { let block_col = bits.trailing_zeros() as usize; @@ -468,7 +419,7 @@ pub mod cognitive_shader { } let dist = self.semiring.distance(query, target); if dist <= radius { - visit(CascadeHit { + hits.push(CascadeHit { target, distance: dist, predicates: per_col_predicates[block_col], @@ -476,6 +427,9 @@ pub mod cognitive_shader { } } } + + hits.sort_by_key(|h| h.distance); + hits } /// Transitive deduction: A→B→C via compose. @@ -697,64 +651,6 @@ mod tests { assert_eq!(ctr, contra::INVERT); } - #[test] - fn cascade_nearest_equals_cascade_take_k() { - use super::cognitive_shader::CognitiveShader; - use bgz17::base17::Base17; - use bgz17::palette::Palette; - use bgz17::palette_semiring::PaletteSemiring; - - // Coarse palette values so equal distances are common: the tie rule - // (candidate order, as the stable sort keeps it) must be exercised. - let entries: Vec = (0..256) - .map(|i| { - let mut dims = [0i16; 17]; - dims[0] = ((i % 7) * 100) as i16; - dims[1] = ((i % 3) * 50) as i16; - Base17 { dims } - }) - .collect(); - let semiring = PaletteSemiring::build(&Palette { entries }); - let mut state = 0x2545_F491_4F6C_DD1Du64; - let mut planes = [[0u64; 64]; 8]; - for plane in planes.iter_mut() { - for row in plane.iter_mut() { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - *row = state; - } - } - let shader = CognitiveShader::new(planes, &semiring); - - fn key(h: &super::cognitive_shader::CascadeHit) -> (u8, u16, u8) { - (h.target, h.distance, h.predicates) - } - let mut compared = 0usize; - for query in 0..=255u8 { - for radius in [0u16, 40, 400, u16::MAX] { - for mask in [0x01u8, 0x0F, 0xFF] { - let all = shader.cascade(query, radius, mask); - let (b4, n4) = shader.cascade_nearest::<4>(query, radius, mask); - let want: Vec<_> = all.iter().take(4).map(key).collect(); - let got: Vec<_> = b4[..n4].iter().map(key).collect(); - assert_eq!(got, want, "query {query} radius {radius} mask {mask:#x}"); - let (b1, n1) = shader.cascade_nearest::<1>(query, radius, mask); - assert_eq!( - b1[..n1].iter().map(key).collect::>(), - all.iter().take(1).map(key).collect::>() - ); - compared += usize::from(all.len() > 4); - } - } - } - // The case that matters: more candidates than kept slots. - assert!( - compared > 100, - "fixture too sparse: only {compared} overflowing queries" - ); - } - #[test] fn cognitive_shader_cascade() { use super::cognitive_shader::CognitiveShader;