Skip to main content

summa_core/structures/vector/scann/
engine.rs

1//! Deterministic hierarchical k-means training and routing.
2//!
3//! This is the executable float routing core. The trained tree is global to an
4//! index generation; immutable segments only store terminal leaf identifiers.
5
6use std::cell::RefCell;
7use std::ops::Range;
8
9use rand::{Rng, SeedableRng};
10
11use super::quantized_dot::QuantizedDotKernel;
12use super::{
13    AhCodebook, AhQuery, DEFAULT_ANISOTROPIC_THRESHOLD, FastScanQuery, MAX_SCANN_TREE_LEVELS,
14    ScannEncoding, ScannFormatError, ScannResult, ScannRoutingLevel, ScannTrainedArtifact,
15    ScannTrainedArtifactView,
16};
17
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct RoutedLeaf {
20    pub leaf: u32,
21    pub squared_distance: f32,
22}
23
24/// Beam-search candidate: `(node, squared distance)`. Node identifiers are
25/// `u32` like every other ScaNN directory entry, so a candidate is 8 bytes
26/// instead of the 16 a `(usize, f32)` pair costs after padding.
27type RoutingCandidate = (u32, f32);
28
29#[derive(Clone, Debug, Default)]
30pub struct RoutingScratch {
31    active: Vec<RoutingCandidate>,
32    next: Vec<RoutingCandidate>,
33    /// Per-level query transform for the fixed-point centroid plane.
34    scaled_query: Vec<f32>,
35}
36
37thread_local! {
38    /// Serving-path routing scratch. Query routing happens once per query per
39    /// index generation, and the beam buffers are a few KiB, so retaining them
40    /// per thread removes three allocations per query without a scratch
41    /// threaded through every caller.
42    static ROUTING_SCRATCH: RefCell<RoutingScratch> = RefCell::new(RoutingScratch::default());
43}
44
45fn with_routing_scratch<T>(scope: impl FnOnce(&mut RoutingScratch) -> T) -> T {
46    ROUTING_SCRATCH.with(|cell| match cell.try_borrow_mut() {
47        Ok(mut scratch) => scope(&mut scratch),
48        // Re-entrant use is not expected; fall back to a fresh scratch rather
49        // than panicking inside a query.
50        Err(_) => scope(&mut RoutingScratch::default()),
51    })
52}
53
54/// Reusable storage for assigning and AH-encoding float vectors. Segment
55/// builders keep one of these for their lifetime so encoding a row does not
56/// allocate dimension-sized residuals or code buffers.
57#[derive(Clone, Debug, Default)]
58pub struct FloatEncodeScratch {
59    routing: RoutingScratch,
60    routed: Vec<RoutedLeaf>,
61    residual: Vec<f32>,
62    codes: Vec<u8>,
63    ah: super::AhEncodeScratch,
64}
65
66/// Query routing intentionally explores a wider beam than the final probe
67/// count when probes are small. Large probe requests widen this floor just
68/// enough to keep the requested terminal leaves reachable.
69const QUERY_INTERMEDIATE_ROUTING_BEAM: usize = 64;
70
71#[derive(Clone, Debug, PartialEq)]
72pub struct RoutingTraining {
73    pub tree: FloatRoutingTree,
74    /// Final training assignment in the reordered terminal-leaf namespace.
75    pub assignments: Vec<u32>,
76    /// Deterministic accounting for the bounded recursive trainer.
77    pub stats: RoutingTrainingStats,
78}
79
80/// Work accounting for hierarchical routing training. `max_split_clusters`
81/// is the scale invariant: it is bounded by local training fanout and does not
82/// grow to the terminal leaf count.
83#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
84pub struct RoutingTrainingStats {
85    pub splits: usize,
86    pub max_split_clusters: usize,
87    pub max_depth: usize,
88    pub distance_evaluations: u64,
89    /// Membership recovery replaces a terminal `points * leaves` scan.
90    pub assignment_distance_evaluations: u64,
91}
92
93#[derive(Clone, Debug, PartialEq)]
94pub struct FloatRoutingTree {
95    dimension: usize,
96    levels: Vec<Vec<f32>>,
97    child_offsets: Vec<Vec<u32>>,
98}
99
100/// Executable float ScaNN model shared by all immutable segments in one index
101/// generation.
102#[derive(Clone, Debug, PartialEq)]
103pub struct FloatScannModel {
104    pub routing: FloatRoutingTree,
105    pub codebook: AhCodebook,
106    anisotropic_threshold: f32,
107}
108
109#[derive(Clone, Debug, PartialEq)]
110struct QuantizedFloatRoutingLevel {
111    centroid_count: usize,
112    centroid_codes: Range<usize>,
113    minimums: Vec<f32>,
114    steps: Vec<f32>,
115    /// `sum step_i^2 * code_i^2` per centroid: the query-independent term of
116    /// the expanded squared distance (see `quantized_dot`). One f32 per
117    /// centroid, computed once when the artifact is opened.
118    code_norms: Vec<f32>,
119    child_offsets: Vec<u32>,
120}
121
122/// Query-side constants for scoring one fixed-point routing level.
123struct ScaledLevelQuery {
124    /// `sum (q_i - min_i)^2`.
125    constant: f32,
126}
127
128/// Small executable metadata for a persisted float ScaNN model. Quantized
129/// centroid planes remain in the caller-owned artifact bytes; only per-level
130/// scale vectors, child directories, and the AH codebook are resident.
131#[derive(Clone, Debug, PartialEq)]
132pub struct QuantizedFloatScannModel {
133    dimension: usize,
134    num_leaves: usize,
135    artifact_id: u64,
136    artifact_len: usize,
137    levels: Vec<QuantizedFloatRoutingLevel>,
138    codebook: AhCodebook,
139    anisotropic_threshold: f32,
140    /// Resolved once per model so routing never repeats feature detection.
141    kernel: QuantizedDotKernel,
142}
143
144/// Borrowed executable pairing of small model metadata with the mmap/file
145/// bytes that own its quantized centroid planes.
146#[derive(Clone, Copy, Debug)]
147pub struct QuantizedFloatScannModelView<'a> {
148    model: &'a QuantizedFloatScannModel,
149    artifact_bytes: &'a [u8],
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct EncodedFloatVector {
154    pub leaf: u32,
155    /// One unpacked 4-bit code per AH block.
156    pub codes: Vec<u8>,
157}
158
159#[derive(Clone, Debug, PartialEq)]
160pub struct FloatScannQuery {
161    routed_leaves: Vec<u32>,
162    centroid_dots: Vec<f32>,
163    ah: AhQuery,
164    /// Integer FastScan lookup table derived from `ah`. Built once here
165    /// because the query is shared (Arc) across every segment of the field.
166    fast_scan: FastScanQuery,
167}
168
169impl FloatScannModel {
170    /// Reconstruct the executable float model from a validated persisted
171    /// generation. The anisotropic threshold is an implementation invariant,
172    /// not a schema or artifact knob, so version 1 artifacts use the same
173    /// hardcoded value as training.
174    pub fn from_artifact(artifact: &ScannTrainedArtifact) -> ScannResult<Self> {
175        artifact.validate()?;
176        let dimensions_per_block = match artifact.config.encoding {
177            ScannEncoding::AsymmetricHash {
178                dimensions_per_block,
179                bits_per_code: 4,
180            } => usize::from(dimensions_per_block),
181            _ => {
182                return Err(ScannFormatError::new(
183                    "float ScaNN model requires a 4-bit asymmetric-hash artifact",
184                ));
185            }
186        };
187        let codebook_artifact = artifact.ah_codebook.as_ref().ok_or_else(|| {
188            ScannFormatError::new("float ScaNN artifact is missing its AH codebook")
189        })?;
190        if usize::from(codebook_artifact.dimensions_per_block) != dimensions_per_block {
191            return Err(ScannFormatError::new(
192                "ScaNN routing encoding and AH codebook block geometry differ",
193            ));
194        }
195        let dimension = artifact.config.dimension as usize;
196        Ok(Self {
197            routing: FloatRoutingTree::from_quantized_levels(&artifact.levels, dimension)?,
198            codebook: AhCodebook::from_artifact(dimension, codebook_artifact)?,
199            anisotropic_threshold: DEFAULT_ANISOTROPIC_THRESHOLD,
200        })
201    }
202
203    #[allow(clippy::too_many_arguments)]
204    pub fn train(
205        data: &[f32],
206        points: usize,
207        dimension: usize,
208        level_counts: &[u32],
209        dimensions_per_block: usize,
210        iterations: usize,
211        seed: u64,
212        anisotropic_threshold: f32,
213    ) -> ScannResult<(Self, Vec<EncodedFloatVector>)> {
214        let model = Self::train_model(
215            data,
216            points,
217            dimension,
218            level_counts,
219            dimensions_per_block,
220            iterations,
221            seed,
222            anisotropic_threshold,
223        )?;
224        let encoded = data
225            .chunks_exact(dimension)
226            .map(|vector| model.encode(vector))
227            .collect::<ScannResult<Vec<_>>>()?;
228        Ok((model, encoded))
229    }
230
231    /// Train only the index-global model. Production generation training uses
232    /// this path because training-sample encodings are immediately discarded;
233    /// immutable segment builders encode the actual corpus later.
234    #[allow(clippy::too_many_arguments)]
235    pub fn train_model(
236        data: &[f32],
237        points: usize,
238        dimension: usize,
239        level_counts: &[u32],
240        dimensions_per_block: usize,
241        iterations: usize,
242        seed: u64,
243        anisotropic_threshold: f32,
244    ) -> ScannResult<Self> {
245        if !anisotropic_threshold.is_finite() || !(0.0..1.0).contains(&anisotropic_threshold) {
246            return Err(ScannFormatError::new(
247                "ScaNN anisotropic threshold must be in [0, 1)",
248            ));
249        }
250        let routing = train_routing_tree(data, points, dimension, level_counts, iterations, seed)?;
251        let codebook = AhCodebook::train_from_assigned_vectors(
252            data,
253            &routing.assignments,
254            routing.tree.leaf_centroids(),
255            points,
256            dimension,
257            dimensions_per_block,
258            iterations,
259            seed.wrapping_add(0xd1b5_4a32_d192_ed03),
260        )?;
261        Ok(Self {
262            routing: routing.tree,
263            codebook,
264            anisotropic_threshold,
265        })
266    }
267
268    pub fn encode(&self, vector: &[f32]) -> ScannResult<EncodedFloatVector> {
269        let mut scratch = FloatEncodeScratch::default();
270        let (leaf, codes) = self.encode_with_scratch(vector, &mut scratch)?;
271        Ok(EncodedFloatVector {
272            leaf,
273            codes: codes.to_vec(),
274        })
275    }
276
277    pub fn encode_with_scratch<'a>(
278        &self,
279        vector: &[f32],
280        scratch: &'a mut FloatEncodeScratch,
281    ) -> ScannResult<(u32, &'a [u8])> {
282        if vector.len() != self.routing.dimension {
283            return Err(ScannFormatError::new(
284                "ScaNN vector dimension does not match trained model",
285            ));
286        }
287        let FloatEncodeScratch {
288            routing,
289            routed,
290            residual,
291            codes,
292            ah,
293        } = scratch;
294        self.routing
295            .route_with_scratch(vector, 1, routing, routed)?;
296        let leaf = routed[0].leaf;
297        let centroid = &self.routing.leaf_centroids()
298            [leaf as usize * self.routing.dimension..(leaf as usize + 1) * self.routing.dimension];
299        residual.resize(self.routing.dimension, 0.0);
300        for ((value, &original), &center) in residual.iter_mut().zip(vector).zip(centroid) {
301            *value = original - center;
302        }
303        codes.resize(self.codebook.blocks(), 0);
304        self.codebook.encode_with_scratch(
305            residual,
306            vector,
307            self.anisotropic_threshold,
308            codes,
309            ah,
310        )?;
311        Ok((leaf, codes))
312    }
313
314    pub fn prepare_query(&self, query: &[f32], probes: usize) -> ScannResult<FloatScannQuery> {
315        let routed = self.routing.route(query, probes)?;
316        let mut routed_leaves = Vec::with_capacity(routed.len());
317        let mut centroid_dots = Vec::with_capacity(routed.len());
318        for routed_leaf in routed {
319            let centroid = &self.routing.leaf_centroids()[routed_leaf.leaf as usize
320                * self.routing.dimension
321                ..(routed_leaf.leaf as usize + 1) * self.routing.dimension];
322            routed_leaves.push(routed_leaf.leaf);
323            centroid_dots.push(crate::structures::simd::dot_product_f32(
324                query,
325                centroid,
326                query.len(),
327            ));
328        }
329        Ok(FloatScannQuery::new(
330            routed_leaves,
331            centroid_dots,
332            self.codebook.query_dot_product(query)?,
333        ))
334    }
335
336    pub fn anisotropic_threshold(&self) -> f32 {
337        self.anisotropic_threshold
338    }
339}
340
341impl QuantizedFloatScannModel {
342    pub fn from_artifact_view(artifact: &ScannTrainedArtifactView<'_>) -> ScannResult<Self> {
343        let dimensions_per_block = match artifact.config.encoding {
344            ScannEncoding::AsymmetricHash {
345                dimensions_per_block,
346                bits_per_code: 4,
347            } => usize::from(dimensions_per_block),
348            _ => {
349                return Err(ScannFormatError::new(
350                    "quantized float ScaNN model requires a 4-bit AH artifact",
351                ));
352            }
353        };
354        let dimension = artifact.config.dimension as usize;
355        let codebook_ref = artifact.ah_codebook().ok_or_else(|| {
356            ScannFormatError::new("float ScaNN artifact is missing its AH codebook")
357        })?;
358        if usize::from(codebook_ref.dimensions_per_block) != dimensions_per_block {
359            return Err(ScannFormatError::new(
360                "ScaNN routing encoding and AH codebook block geometry differ",
361            ));
362        }
363        let codebook = AhCodebook::from_artifact_ref(dimension, codebook_ref)?;
364        let mut levels = Vec::with_capacity(artifact.level_count());
365        for index in 0..artifact.level_count() {
366            let level = artifact
367                .level(index)
368                .ok_or_else(|| ScannFormatError::new("ScaNN artifact routing level disappeared"))?;
369            let centroid_codes = artifact.level_centroid_codes_range(index).ok_or_else(|| {
370                ScannFormatError::new("ScaNN artifact centroid range disappeared")
371            })?;
372            let steps: Vec<f32> = level.steps().collect();
373            let code_norms = quantized_code_norms(&steps, level.centroid_codes, dimension)?;
374            levels.push(QuantizedFloatRoutingLevel {
375                centroid_count: level.centroid_count as usize,
376                centroid_codes,
377                minimums: level.minimums().collect(),
378                steps,
379                code_norms,
380                child_offsets: level.child_offsets().collect(),
381            });
382        }
383        let model = Self {
384            dimension,
385            num_leaves: artifact.config.num_leaves as usize,
386            artifact_id: artifact.artifact_id,
387            artifact_len: artifact.bytes().len(),
388            levels,
389            codebook,
390            anisotropic_threshold: DEFAULT_ANISOTROPIC_THRESHOLD,
391            kernel: QuantizedDotKernel::resolve(),
392        };
393        model.validate_metadata()?;
394        Ok(model)
395    }
396
397    /// Pair this metadata with the exact validated artifact mapping it was
398    /// created from. The fingerprint slot check is O(1); full hashing happened
399    /// once when `ScannTrainedArtifactView` was parsed.
400    pub fn view<'a>(
401        &'a self,
402        artifact_bytes: &'a [u8],
403    ) -> ScannResult<QuantizedFloatScannModelView<'a>> {
404        let stored_id = artifact_bytes
405            .get(12..20)
406            .and_then(|bytes| <[u8; 8]>::try_from(bytes).ok())
407            .map(u64::from_le_bytes);
408        if artifact_bytes.len() != self.artifact_len || stored_id != Some(self.artifact_id) {
409            return Err(ScannFormatError::new(
410                "quantized float ScaNN model was paired with a different artifact mapping",
411            ));
412        }
413        Ok(QuantizedFloatScannModelView {
414            model: self,
415            artifact_bytes,
416        })
417    }
418
419    pub fn estimated_memory_bytes(&self) -> usize {
420        self.levels
421            .iter()
422            .fold(self.codebook.estimated_memory_bytes(), |total, level| {
423                total
424                    .saturating_add(level.minimums.len() * std::mem::size_of::<f32>())
425                    .saturating_add(level.steps.len() * std::mem::size_of::<f32>())
426                    .saturating_add(level.code_norms.len() * std::mem::size_of::<f32>())
427                    .saturating_add(level.child_offsets.len() * std::mem::size_of::<u32>())
428            })
429    }
430
431    fn validate_metadata(&self) -> ScannResult<()> {
432        if self.dimension == 0
433            || self.levels.is_empty()
434            || self.levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
435            || self.levels.last().map(|level| level.centroid_count) != Some(self.num_leaves)
436            || self.codebook.dimension() != self.dimension
437        {
438            return Err(ScannFormatError::new(
439                "invalid quantized float ScaNN model metadata",
440            ));
441        }
442        for (index, level) in self.levels.iter().enumerate() {
443            if level.minimums.len() != self.dimension
444                || level.steps.len() != self.dimension
445                || level.code_norms.len() != level.centroid_count
446                || level.centroid_codes.len() != level.centroid_count.saturating_mul(self.dimension)
447                || level.centroid_codes.end > self.artifact_len
448            {
449                return Err(ScannFormatError::new(format!(
450                    "invalid quantized float ScaNN routing level {index}",
451                )));
452            }
453            if index + 1 == self.levels.len() {
454                if !level.child_offsets.is_empty() {
455                    return Err(ScannFormatError::new(
456                        "quantized ScaNN leaf level must not have children",
457                    ));
458                }
459            } else if level.child_offsets.len() != level.centroid_count + 1
460                || level.child_offsets.first() != Some(&0)
461                || level.child_offsets.last().copied()
462                    != Some(self.levels[index + 1].centroid_count as u32)
463            {
464                return Err(ScannFormatError::new(format!(
465                    "invalid quantized float ScaNN child directory at level {index}",
466                )));
467            }
468        }
469        Ok(())
470    }
471}
472
473impl QuantizedFloatScannModelView<'_> {
474    pub fn encode(&self, vector: &[f32]) -> ScannResult<EncodedFloatVector> {
475        let mut scratch = FloatEncodeScratch::default();
476        let (leaf, codes) = self.encode_with_scratch(vector, &mut scratch)?;
477        Ok(EncodedFloatVector {
478            leaf,
479            codes: codes.to_vec(),
480        })
481    }
482
483    pub fn encode_with_scratch<'a>(
484        &self,
485        vector: &[f32],
486        scratch: &'a mut FloatEncodeScratch,
487    ) -> ScannResult<(u32, &'a [u8])> {
488        if vector.len() != self.model.dimension {
489            return Err(ScannFormatError::new(
490                "ScaNN vector dimension does not match trained model",
491            ));
492        }
493        let FloatEncodeScratch {
494            routing,
495            routed,
496            residual,
497            codes: encoded,
498            ah,
499        } = scratch;
500        self.route_with_scratch(vector, 1, routing, routed)?;
501        let leaf = routed[0].leaf;
502        let level = self
503            .model
504            .levels
505            .last()
506            .expect("validated non-empty levels");
507        let codes = self.level_codes(level);
508        let row = &codes
509            [leaf as usize * self.model.dimension..(leaf as usize + 1) * self.model.dimension];
510        residual.resize(self.model.dimension, 0.0);
511        for (coordinate, (&value, residual)) in vector.iter().zip(residual.iter_mut()).enumerate() {
512            *residual = value
513                - (level.minimums[coordinate]
514                    + level.steps[coordinate] * f32::from(row[coordinate]));
515        }
516        encoded.resize(self.model.codebook.blocks(), 0);
517        self.model.codebook.encode_with_scratch(
518            residual,
519            vector,
520            self.model.anisotropic_threshold,
521            encoded,
522            ah,
523        )?;
524        Ok((leaf, encoded))
525    }
526
527    pub fn prepare_query(&self, query: &[f32], probes: usize) -> ScannResult<FloatScannQuery> {
528        with_routing_scratch(|scratch| {
529            let mut routed = Vec::with_capacity(probes);
530            self.route_with_scratch(query, probes, scratch, &mut routed)?;
531            let level = self
532                .model
533                .levels
534                .last()
535                .expect("validated non-empty levels");
536            let codes = self.level_codes(level);
537            // dot(q, c) = sum q_i * min_i + sum (q_i * step_i) * code_i: the
538            // first term is query-only, the second is one `f32 x u8` kernel
539            // call per routed leaf.
540            let dimension = self.model.dimension;
541            let scaled = &mut scratch.scaled_query;
542            scaled.clear();
543            scaled.extend(
544                query
545                    .iter()
546                    .zip(&level.steps)
547                    .map(|(&value, &step)| value.algebraic_mul(step)),
548            );
549            let offset = query
550                .iter()
551                .zip(&level.minimums)
552                .fold(0.0f64, |acc, (&value, &minimum)| {
553                    acc + f64::from(value) * f64::from(minimum)
554                }) as f32;
555            let mut routed_leaves = Vec::with_capacity(routed.len());
556            let mut centroid_dots = Vec::with_capacity(routed.len());
557            for routed_leaf in &routed {
558                let leaf = routed_leaf.leaf as usize;
559                let row = &codes[leaf * dimension..(leaf + 1) * dimension];
560                routed_leaves.push(routed_leaf.leaf);
561                centroid_dots.push(offset.algebraic_add(self.model.kernel.dot(scaled, row)));
562            }
563            Ok(FloatScannQuery::new(
564                routed_leaves,
565                centroid_dots,
566                self.model.codebook.query_dot_product(query)?,
567            ))
568        })
569    }
570
571    pub fn anisotropic_threshold(&self) -> f32 {
572        self.model.anisotropic_threshold
573    }
574
575    #[cfg(test)]
576    fn route(&self, query: &[f32], probes: usize) -> ScannResult<Vec<RoutedLeaf>> {
577        let mut output = Vec::with_capacity(probes);
578        with_routing_scratch(|scratch| {
579            self.route_with_scratch(query, probes, scratch, &mut output)
580        })?;
581        Ok(output)
582    }
583
584    fn route_with_scratch(
585        &self,
586        query: &[f32],
587        probes: usize,
588        scratch: &mut RoutingScratch,
589        output: &mut Vec<RoutedLeaf>,
590    ) -> ScannResult<()> {
591        if query.len() != self.model.dimension || query.iter().any(|value| !value.is_finite()) {
592            return Err(ScannFormatError::new(
593                "ScaNN routing query has the wrong dimension or non-finite values",
594            ));
595        }
596        if probes == 0 {
597            return Err(ScannFormatError::new(
598                "ScaNN routing probes must be positive",
599            ));
600        }
601        scratch.active.clear();
602        let scaled = Self::scale_query(&self.model.levels[0], query, &mut scratch.scaled_query);
603        self.score_range_into(
604            &self.model.levels[0],
605            &scaled,
606            &scratch.scaled_query,
607            0,
608            self.model.levels[0].centroid_count,
609            &mut scratch.active,
610        );
611        if self.model.levels.len() == 1 {
612            keep_best(&mut scratch.active, probes);
613        } else {
614            sort_routing_candidates(&mut scratch.active);
615            let initial_width = super::routing_prefix_for_child_coverage(
616                &scratch.active,
617                &self.model.levels[0].child_offsets,
618                intermediate_routing_beam(probes),
619                probes,
620                |candidate| candidate.0 as usize,
621            );
622            scratch.active.truncate(initial_width);
623        }
624        for level_index in 1..self.model.levels.len() {
625            let level = &self.model.levels[level_index];
626            let offsets = &self.model.levels[level_index - 1].child_offsets;
627            let scaled = Self::scale_query(level, query, &mut scratch.scaled_query);
628            scratch.next.clear();
629            for &(parent, _) in &scratch.active {
630                let parent = parent as usize;
631                self.score_range_into(
632                    level,
633                    &scaled,
634                    &scratch.scaled_query,
635                    offsets[parent] as usize,
636                    offsets[parent + 1] as usize,
637                    &mut scratch.next,
638                );
639            }
640            if level_index + 1 == self.model.levels.len() {
641                keep_best(&mut scratch.next, probes);
642            } else {
643                sort_routing_candidates(&mut scratch.next);
644                let width = super::routing_prefix_for_child_coverage(
645                    &scratch.next,
646                    &level.child_offsets,
647                    intermediate_routing_beam(probes),
648                    probes,
649                    |candidate| candidate.0 as usize,
650                );
651                scratch.next.truncate(width);
652            }
653            std::mem::swap(&mut scratch.active, &mut scratch.next);
654        }
655        output.clear();
656        output.extend(
657            scratch
658                .active
659                .iter()
660                .map(|&(leaf, squared_distance)| RoutedLeaf {
661                    leaf,
662                    squared_distance,
663                }),
664        );
665        Ok(())
666    }
667
668    /// Fill `scaled` with `q'_i = (q_i - min_i) * step_i` for one level and
669    /// return the query-only constant `sum (q_i - min_i)^2`. The constant is
670    /// accumulated in f64: it is the large term that the centroid-dependent
671    /// part is subtracted from, so its rounding would otherwise dominate.
672    fn scale_query(
673        level: &QuantizedFloatRoutingLevel,
674        query: &[f32],
675        scaled: &mut Vec<f32>,
676    ) -> ScaledLevelQuery {
677        scaled.clear();
678        scaled.reserve(query.len());
679        let mut constant = 0.0f64;
680        for ((&value, &minimum), &step) in query.iter().zip(&level.minimums).zip(&level.steps) {
681            let shifted = value - minimum;
682            constant += f64::from(shifted) * f64::from(shifted);
683            scaled.push(shifted.algebraic_mul(step));
684        }
685        ScaledLevelQuery {
686            constant: constant as f32,
687        }
688    }
689
690    /// Score centroids `start..end` of one level with the expanded squared
691    /// distance `C - 2 * dot(q', code) + N_c`. `N_c` was precomputed when the
692    /// artifact was opened, so the loop is one `f32 x u8` dot per centroid.
693    fn score_range_into(
694        &self,
695        level: &QuantizedFloatRoutingLevel,
696        scaled: &ScaledLevelQuery,
697        scaled_query: &[f32],
698        start: usize,
699        end: usize,
700        output: &mut Vec<RoutingCandidate>,
701    ) {
702        let codes = self.level_codes(level);
703        let dimension = self.model.dimension;
704        let kernel = self.model.kernel;
705        output.reserve(end.saturating_sub(start));
706        output.extend((start..end).map(|centroid| {
707            let row = &codes[centroid * dimension..(centroid + 1) * dimension];
708            let dot = kernel.dot(scaled_query, row);
709            let distance = scaled
710                .constant
711                .algebraic_add(level.code_norms[centroid].algebraic_sub(dot.algebraic_mul(2.0)));
712            // Rounding in the expanded form can dip a tiny distance below zero.
713            (centroid as u32, distance.max(0.0))
714        }));
715    }
716
717    fn level_codes(&self, level: &QuantizedFloatRoutingLevel) -> &[u8] {
718        &self.artifact_bytes[level.centroid_codes.clone()]
719    }
720}
721
722impl FloatScannQuery {
723    fn new(routed_leaves: Vec<u32>, centroid_dots: Vec<f32>, ah: AhQuery) -> Self {
724        let fast_scan = FastScanQuery::new(&ah);
725        if fast_scan.is_degenerate() {
726            crate::observe::scann_degenerate_fast_scan_query();
727            super::warn_rate_limited(&DEGENERATE_FAST_SCAN_QUERIES, |count| {
728                format!(
729                    "[scann] FastScan lookup table is degenerate (all-zero AH scores); every \
730                     leaf row scores as its centroid dot ({count} queries so far)"
731                )
732            });
733        }
734        Self {
735            routed_leaves,
736            centroid_dots,
737            ah,
738            fast_scan,
739        }
740    }
741
742    pub fn routed_leaves(&self) -> &[u32] {
743        &self.routed_leaves
744    }
745
746    /// Routed leaves paired with their centroid dot products, in beam order.
747    /// Segment scanners iterate this instead of looking each leaf up again.
748    pub fn routed(&self) -> impl ExactSizeIterator<Item = (u32, f32)> + '_ {
749        self.routed_leaves
750            .iter()
751            .copied()
752            .zip(self.centroid_dots.iter().copied())
753    }
754
755    /// Centroid contribution for one routed leaf. Linear in the probe count;
756    /// hot scans use [`Self::routed`] instead.
757    pub fn centroid_dot(&self, leaf: u32) -> Option<f32> {
758        self.routed_leaves
759            .iter()
760            .position(|&candidate| candidate == leaf)
761            .map(|position| self.centroid_dots[position])
762    }
763
764    /// Borrow the query-specific AH lookup table for packed/FastScan rows.
765    pub fn ah_query(&self) -> &AhQuery {
766        &self.ah
767    }
768
769    /// Integer FastScan table shared by every segment scanning this query.
770    pub fn fast_scan(&self) -> &FastScanQuery {
771        &self.fast_scan
772    }
773
774    /// Returns `None` when the row's leaf was not selected by the query beam.
775    pub fn score(&self, vector: &EncodedFloatVector) -> ScannResult<Option<f32>> {
776        let Some(position) = self
777            .routed_leaves
778            .iter()
779            .position(|&leaf| leaf == vector.leaf)
780        else {
781            return Ok(None);
782        };
783        self.ah
784            .score_unpacked(&vector.codes, self.centroid_dots[position])
785            .map(Some)
786    }
787}
788
789impl FloatRoutingTree {
790    pub fn dimension(&self) -> usize {
791        self.dimension
792    }
793
794    pub fn level_counts(&self) -> impl ExactSizeIterator<Item = usize> + '_ {
795        self.levels.iter().map(|level| level.len() / self.dimension)
796    }
797
798    pub fn leaf_centroids(&self) -> &[f32] {
799        self.levels.last().map_or(&[], Vec::as_slice)
800    }
801
802    pub fn levels(&self) -> &[Vec<f32>] {
803        &self.levels
804    }
805
806    pub fn child_offsets(&self) -> &[Vec<u32>] {
807        &self.child_offsets
808    }
809
810    /// Route to the closest terminal partitions with a bounded beam.
811    pub fn route(&self, query: &[f32], probes: usize) -> ScannResult<Vec<RoutedLeaf>> {
812        let mut output = Vec::with_capacity(probes);
813        with_routing_scratch(|scratch| {
814            self.route_with_scratch(query, probes, scratch, &mut output)
815        })?;
816        Ok(output)
817    }
818
819    /// Allocation-reusing serving-path form of [`Self::route`].
820    pub fn route_with_scratch(
821        &self,
822        query: &[f32],
823        probes: usize,
824        scratch: &mut RoutingScratch,
825        output: &mut Vec<RoutedLeaf>,
826    ) -> ScannResult<()> {
827        if query.len() != self.dimension || query.iter().any(|value| !value.is_finite()) {
828            return Err(ScannFormatError::new(
829                "ScaNN routing query has the wrong dimension or non-finite values",
830            ));
831        }
832        if probes == 0 {
833            return Err(ScannFormatError::new(
834                "ScaNN routing probes must be positive",
835            ));
836        }
837
838        scratch.active.clear();
839        score_range_into(
840            &self.levels[0],
841            self.dimension,
842            0,
843            self.level_counts().next().unwrap(),
844            query,
845            &mut scratch.active,
846        );
847        if self.levels.len() == 1 {
848            keep_best(&mut scratch.active, probes);
849        } else {
850            sort_routing_candidates(&mut scratch.active);
851            let initial_width = super::routing_prefix_for_child_coverage(
852                &scratch.active,
853                &self.child_offsets[0],
854                intermediate_routing_beam(probes),
855                probes,
856                |candidate| candidate.0 as usize,
857            );
858            scratch.active.truncate(initial_width);
859        }
860        for level in 1..self.levels.len() {
861            let offsets = &self.child_offsets[level - 1];
862            scratch.next.clear();
863            for &(parent, _) in &scratch.active {
864                let start = offsets[parent as usize] as usize;
865                let end = offsets[parent as usize + 1] as usize;
866                score_range_into(
867                    &self.levels[level],
868                    self.dimension,
869                    start,
870                    end,
871                    query,
872                    &mut scratch.next,
873                );
874            }
875            if level + 1 == self.levels.len() {
876                keep_best(&mut scratch.next, probes);
877            } else {
878                sort_routing_candidates(&mut scratch.next);
879                let width = super::routing_prefix_for_child_coverage(
880                    &scratch.next,
881                    &self.child_offsets[level],
882                    intermediate_routing_beam(probes),
883                    probes,
884                    |candidate| candidate.0 as usize,
885                );
886                scratch.next.truncate(width);
887            }
888            std::mem::swap(&mut scratch.active, &mut scratch.next);
889        }
890        output.clear();
891        output.extend(
892            scratch
893                .active
894                .iter()
895                .map(|&(leaf, squared_distance)| RoutedLeaf {
896                    leaf,
897                    squared_distance,
898                }),
899        );
900        Ok(())
901    }
902
903    /// Quantize every level independently to the persisted u8 centroid plane.
904    pub fn to_quantized_levels(&self) -> Vec<ScannRoutingLevel> {
905        self.levels
906            .iter()
907            .enumerate()
908            .map(|(level_index, centroids)| {
909                let count = centroids.len() / self.dimension;
910                let mut minimums = vec![f32::INFINITY; self.dimension];
911                let mut maximums = vec![f32::NEG_INFINITY; self.dimension];
912                for centroid in centroids.chunks_exact(self.dimension) {
913                    for coordinate in 0..self.dimension {
914                        minimums[coordinate] = minimums[coordinate].min(centroid[coordinate]);
915                        maximums[coordinate] = maximums[coordinate].max(centroid[coordinate]);
916                    }
917                }
918                let steps: Vec<f32> = minimums
919                    .iter()
920                    .zip(&maximums)
921                    .map(|(&minimum, &maximum)| {
922                        let range = maximum - minimum;
923                        if range.is_finite() && range > 0.0 {
924                            range / 255.0
925                        } else {
926                            1.0
927                        }
928                    })
929                    .collect();
930                let centroid_codes = centroids
931                    .chunks_exact(self.dimension)
932                    .flat_map(|centroid| {
933                        centroid.iter().enumerate().map(|(coordinate, &value)| {
934                            ((value - minimums[coordinate]) / steps[coordinate])
935                                .round()
936                                .clamp(0.0, 255.0) as u8
937                        })
938                    })
939                    .collect();
940                ScannRoutingLevel {
941                    centroid_count: count as u32,
942                    centroid_codes,
943                    minimums,
944                    steps,
945                    child_offsets: self
946                        .child_offsets
947                        .get(level_index)
948                        .cloned()
949                        .unwrap_or_default(),
950                }
951            })
952            .collect()
953    }
954
955    pub fn from_quantized_levels(
956        levels: &[ScannRoutingLevel],
957        dimension: usize,
958    ) -> ScannResult<Self> {
959        if dimension == 0 || levels.is_empty() || levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
960        {
961            return Err(ScannFormatError::new(
962                "invalid ScaNN quantized routing shape",
963            ));
964        }
965        let mut decoded = Vec::with_capacity(levels.len());
966        let mut child_offsets = Vec::with_capacity(levels.len().saturating_sub(1));
967        for (index, level) in levels.iter().enumerate() {
968            if level.minimums.len() != dimension
969                || level.steps.len() != dimension
970                || level.centroid_codes.len() != level.centroid_count as usize * dimension
971                || level
972                    .minimums
973                    .iter()
974                    .chain(&level.steps)
975                    .any(|value| !value.is_finite())
976            {
977                return Err(ScannFormatError::new(format!(
978                    "invalid ScaNN quantized routing level {index}"
979                )));
980            }
981            let centroids = level
982                .centroid_codes
983                .chunks_exact(dimension)
984                .flat_map(|centroid| {
985                    centroid.iter().enumerate().map(|(coordinate, &code)| {
986                        level.minimums[coordinate] + level.steps[coordinate] * f32::from(code)
987                    })
988                })
989                .collect();
990            decoded.push(centroids);
991            if index + 1 < levels.len() {
992                if level.child_offsets.len() != level.centroid_count as usize + 1
993                    || level.child_offsets.first() != Some(&0)
994                    || level.child_offsets.last() != Some(&levels[index + 1].centroid_count)
995                    || level.child_offsets.windows(2).any(|pair| pair[0] > pair[1])
996                {
997                    return Err(ScannFormatError::new(format!(
998                        "invalid ScaNN child offsets at routing level {index}"
999                    )));
1000                }
1001                child_offsets.push(level.child_offsets.clone());
1002            } else if !level.child_offsets.is_empty() {
1003                return Err(ScannFormatError::new(
1004                    "terminal ScaNN routing level must not have children",
1005                ));
1006            }
1007        }
1008        Ok(Self {
1009            dimension,
1010            levels: decoded,
1011            child_offsets,
1012        })
1013    }
1014}
1015
1016/// Maximum number of centroids considered by one Lloyd assignment. Large
1017/// requested partitions are produced recursively, so training work scales
1018/// with this local fanout rather than `points * terminal_leaves`.
1019const MAX_LOCAL_KMEANS_BRANCHES: usize = 64;
1020
1021/// Train a nested routing tree with bounded recursive partitioning. Terminal
1022/// centroids are generated by local k-means splits, then upper persisted levels
1023/// are fitted bottom-up with the same bounded algorithm. Carrying each upper
1024/// permutation through all descendants preserves contiguous child ranges.
1025pub fn train_routing_tree(
1026    data: &[f32],
1027    points: usize,
1028    dimension: usize,
1029    level_counts: &[u32],
1030    iterations: usize,
1031    seed: u64,
1032) -> ScannResult<RoutingTraining> {
1033    if points == 0
1034        || dimension == 0
1035        || data.len() != points.saturating_mul(dimension)
1036        || data.iter().any(|value| !value.is_finite())
1037        || level_counts.is_empty()
1038        || level_counts.len() > usize::from(MAX_SCANN_TREE_LEVELS)
1039        || level_counts.contains(&0)
1040        || level_counts.windows(2).any(|pair| pair[0] > pair[1])
1041        || level_counts.last().copied().unwrap_or_default() as usize > points
1042    {
1043        return Err(ScannFormatError::new(
1044            "invalid ScaNN routing training data or geometry",
1045        ));
1046    }
1047
1048    let leaf_count = *level_counts.last().unwrap() as usize;
1049    let mut stats = RoutingTrainingStats::default();
1050    let PartitionTraining {
1051        centroids: leaf_centroids,
1052        group_sizes: leaf_group_sizes,
1053        point_order: leaf_point_order,
1054    } = train_partition(
1055        data, points, dimension, leaf_count, iterations, seed, 0, &mut stats,
1056    )?;
1057    let mut assignments = vec![u32::MAX; points];
1058    let mut cursor = 0usize;
1059    for (leaf, group_size) in leaf_group_sizes.into_iter().enumerate() {
1060        let end = cursor + group_size;
1061        for &point in &leaf_point_order[cursor..end] {
1062            assignments[point] = leaf as u32;
1063        }
1064        cursor = end;
1065    }
1066    debug_assert_eq!(cursor, points);
1067    debug_assert!(!assignments.contains(&u32::MAX));
1068    let mut leaf_current_to_original: Vec<usize> = (0..leaf_count).collect();
1069    let mut levels = vec![leaf_centroids];
1070    let mut child_offsets = Vec::with_capacity(level_counts.len().saturating_sub(1));
1071
1072    for (round, &parent_count) in level_counts[..level_counts.len() - 1]
1073        .iter()
1074        .rev()
1075        .enumerate()
1076    {
1077        let children = levels[0].len() / dimension;
1078        let partition = train_partition(
1079            &levels[0],
1080            children,
1081            dimension,
1082            parent_count as usize,
1083            iterations,
1084            seed.wrapping_add(0x9e37_79b9_u64.wrapping_mul(round as u64 + 1)),
1085            round + 1,
1086            &mut stats,
1087        )?;
1088        let leaf_new_to_old = reorder_descendants(
1089            &mut levels,
1090            &mut child_offsets,
1091            &partition.point_order,
1092            dimension,
1093        )?;
1094        leaf_current_to_original = leaf_new_to_old
1095            .into_iter()
1096            .map(|old| leaf_current_to_original[old])
1097            .collect();
1098        child_offsets.insert(0, group_offsets(&partition.group_sizes)?);
1099        levels.insert(0, partition.centroids);
1100    }
1101
1102    let tree = FloatRoutingTree {
1103        dimension,
1104        levels,
1105        child_offsets,
1106    };
1107    let mut original_to_current = vec![0u32; leaf_count];
1108    for (current, original) in leaf_current_to_original.into_iter().enumerate() {
1109        original_to_current[original] = current as u32;
1110    }
1111    for assignment in &mut assignments {
1112        *assignment = original_to_current[*assignment as usize];
1113    }
1114    Ok(RoutingTraining {
1115        tree,
1116        assignments,
1117        stats,
1118    })
1119}
1120
1121struct PartitionTraining {
1122    centroids: Vec<f32>,
1123    group_sizes: Vec<usize>,
1124    point_order: Vec<usize>,
1125}
1126
1127#[allow(clippy::too_many_arguments)]
1128fn train_partition(
1129    data: &[f32],
1130    points: usize,
1131    dimension: usize,
1132    clusters: usize,
1133    iterations: usize,
1134    seed: u64,
1135    depth: usize,
1136    stats: &mut RoutingTrainingStats,
1137) -> ScannResult<PartitionTraining> {
1138    if points == 0
1139        || dimension == 0
1140        || data.len() != points.saturating_mul(dimension)
1141        || clusters == 0
1142        || clusters > points
1143    {
1144        return Err(ScannFormatError::new(
1145            "invalid ScaNN recursive partition shape",
1146        ));
1147    }
1148    let mut point_order: Vec<usize> = (0..points).collect();
1149    let mut centroids = Vec::with_capacity(clusters.saturating_mul(dimension));
1150    let mut group_sizes = Vec::with_capacity(clusters);
1151    train_partition_node(
1152        data,
1153        &mut point_order,
1154        dimension,
1155        clusters,
1156        iterations,
1157        seed,
1158        depth,
1159        &mut centroids,
1160        &mut group_sizes,
1161        stats,
1162    );
1163    if centroids.len() != clusters.saturating_mul(dimension)
1164        || group_sizes.len() != clusters
1165        || group_sizes.iter().sum::<usize>() != points
1166    {
1167        return Err(ScannFormatError::new(
1168            "ScaNN recursive partition produced the wrong shape",
1169        ));
1170    }
1171    Ok(PartitionTraining {
1172        centroids,
1173        group_sizes,
1174        point_order,
1175    })
1176}
1177
1178#[allow(clippy::too_many_arguments)]
1179fn train_partition_node(
1180    data: &[f32],
1181    point_ids: &mut [usize],
1182    dimension: usize,
1183    clusters: usize,
1184    iterations: usize,
1185    seed: u64,
1186    depth: usize,
1187    output: &mut Vec<f32>,
1188    group_sizes: &mut Vec<usize>,
1189    stats: &mut RoutingTrainingStats,
1190) {
1191    let points = point_ids.len();
1192    stats.max_depth = stats.max_depth.max(depth);
1193    if clusters == 1 {
1194        append_mean(data, point_ids, dimension, output);
1195        group_sizes.push(points);
1196        return;
1197    }
1198    if clusters == points {
1199        for &point_id in point_ids.iter() {
1200            output.extend_from_slice(&data[point_id * dimension..(point_id + 1) * dimension]);
1201        }
1202        group_sizes.resize(group_sizes.len() + points, 1);
1203        return;
1204    }
1205
1206    let branches = training_branch_factor(clusters).min(points);
1207    let model = train_local_kmeans(
1208        data, point_ids, dimension, branches, iterations, seed, stats,
1209    );
1210    stats.splits = stats.splits.saturating_add(1);
1211    stats.max_split_clusters = stats.max_split_clusters.max(branches);
1212    let sizes: Vec<usize> = model
1213        .member_offsets
1214        .windows(2)
1215        .map(|range| range[1] - range[0])
1216        .collect();
1217    let allocations = apportion_clusters(&sizes, clusters);
1218    reorder_point_ids(point_ids, &model.assignments, &model.member_offsets);
1219    for (branch, &allocation) in allocations.iter().enumerate() {
1220        let start = model.member_offsets[branch];
1221        let end = model.member_offsets[branch + 1];
1222        train_partition_node(
1223            data,
1224            &mut point_ids[start..end],
1225            dimension,
1226            allocation,
1227            iterations,
1228            mix_seed(seed, depth, branch),
1229            depth + 1,
1230            output,
1231            group_sizes,
1232            stats,
1233        );
1234    }
1235}
1236
1237struct LocalKMeans {
1238    assignments: Vec<usize>,
1239    member_offsets: Vec<usize>,
1240}
1241
1242#[allow(clippy::too_many_arguments)]
1243fn train_local_kmeans(
1244    data: &[f32],
1245    point_ids: &[usize],
1246    dimension: usize,
1247    clusters: usize,
1248    iterations: usize,
1249    seed: u64,
1250    stats: &mut RoutingTrainingStats,
1251) -> LocalKMeans {
1252    debug_assert!(clusters > 1 && clusters < point_ids.len());
1253    let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
1254    let points = point_ids.len();
1255    let mut selected = std::collections::BTreeSet::new();
1256    let mut selected_rows = Vec::with_capacity(clusters);
1257    for upper in points - clusters..points {
1258        let candidate = rng.random_range(0..=upper);
1259        let row = if selected.insert(candidate) {
1260            candidate
1261        } else {
1262            selected.insert(upper);
1263            upper
1264        };
1265        selected_rows.push(row);
1266    }
1267    let mut centroids: Vec<f32> = selected_rows
1268        .into_iter()
1269        .flat_map(|row| {
1270            let point_id = point_ids[row];
1271            data[point_id * dimension..(point_id + 1) * dimension]
1272                .iter()
1273                .copied()
1274        })
1275        .collect();
1276    let mut assignments = vec![usize::MAX; points];
1277    for _ in 0..iterations.max(1) {
1278        let mut distances = vec![0.0f32; points];
1279        for (row, &point_id) in point_ids.iter().enumerate() {
1280            let point = &data[point_id * dimension..(point_id + 1) * dimension];
1281            let (cluster, distance) = nearest_centroid(&centroids, dimension, point);
1282            assignments[row] = cluster;
1283            distances[row] = distance;
1284        }
1285        stats.distance_evaluations = stats
1286            .distance_evaluations
1287            .saturating_add(u64::try_from(points.saturating_mul(clusters)).unwrap_or(u64::MAX));
1288        ensure_non_empty(&mut assignments, &distances, clusters);
1289        let mut sums = vec![0.0f32; clusters * dimension];
1290        let mut counts = vec![0usize; clusters];
1291        for (&point_id, &cluster) in point_ids.iter().zip(&assignments) {
1292            let point = &data[point_id * dimension..(point_id + 1) * dimension];
1293            counts[cluster] += 1;
1294            for coordinate in 0..dimension {
1295                sums[cluster * dimension + coordinate] += point[coordinate];
1296            }
1297        }
1298        for cluster in 0..clusters {
1299            let inverse = (counts[cluster] as f32).recip();
1300            for coordinate in 0..dimension {
1301                sums[cluster * dimension + coordinate] *= inverse;
1302            }
1303        }
1304        if sums == centroids {
1305            break;
1306        }
1307        centroids = sums;
1308    }
1309    let mut distances = vec![0.0f32; points];
1310    for (row, &point_id) in point_ids.iter().enumerate() {
1311        let point = &data[point_id * dimension..(point_id + 1) * dimension];
1312        let (cluster, distance) = nearest_centroid(&centroids, dimension, point);
1313        assignments[row] = cluster;
1314        distances[row] = distance;
1315    }
1316    stats.distance_evaluations = stats
1317        .distance_evaluations
1318        .saturating_add(u64::try_from(points.saturating_mul(clusters)).unwrap_or(u64::MAX));
1319    ensure_non_empty(&mut assignments, &distances, clusters);
1320    let mut member_offsets = vec![0usize; clusters + 1];
1321    for &cluster in &assignments {
1322        member_offsets[cluster + 1] += 1;
1323    }
1324    for cluster in 0..clusters {
1325        member_offsets[cluster + 1] += member_offsets[cluster];
1326    }
1327    LocalKMeans {
1328        assignments,
1329        member_offsets,
1330    }
1331}
1332
1333fn training_branch_factor(clusters: usize) -> usize {
1334    if clusters <= MAX_LOCAL_KMEANS_BRANCHES {
1335        clusters
1336    } else {
1337        ((clusters as f64).sqrt().ceil() as usize).clamp(2, MAX_LOCAL_KMEANS_BRANCHES)
1338    }
1339}
1340
1341fn append_mean(data: &[f32], point_ids: &[usize], dimension: usize, output: &mut Vec<f32>) {
1342    let start = output.len();
1343    output.resize(start + dimension, 0.0);
1344    for &point_id in point_ids {
1345        let point = &data[point_id * dimension..(point_id + 1) * dimension];
1346        for (sum, &value) in output[start..].iter_mut().zip(point) {
1347            *sum += value;
1348        }
1349    }
1350    let inverse = (point_ids.len() as f32).recip();
1351    for value in &mut output[start..] {
1352        *value *= inverse;
1353    }
1354}
1355
1356fn apportion_clusters(sizes: &[usize], total_clusters: usize) -> Vec<usize> {
1357    debug_assert!(!sizes.is_empty());
1358    debug_assert!(sizes.iter().all(|&size| size > 0));
1359    debug_assert!(total_clusters >= sizes.len());
1360    debug_assert!(total_clusters <= sizes.iter().sum());
1361    let total_points: usize = sizes.iter().sum();
1362    let mut allocations = vec![1usize; sizes.len()];
1363    let mut assigned = sizes.len();
1364    for (allocation, &size) in allocations.iter_mut().zip(sizes) {
1365        let target = (total_clusters.saturating_mul(size) / total_points)
1366            .max(1)
1367            .min(size);
1368        assigned += target - 1;
1369        *allocation = target;
1370    }
1371    while assigned < total_clusters {
1372        let next = (0..sizes.len())
1373            .filter(|&index| allocations[index] < sizes[index])
1374            .max_by(|&left, &right| {
1375                let left_deficit = (total_clusters as i128) * (sizes[left] as i128)
1376                    - (allocations[left] as i128) * (total_points as i128);
1377                let right_deficit = (total_clusters as i128) * (sizes[right] as i128)
1378                    - (allocations[right] as i128) * (total_points as i128);
1379                left_deficit
1380                    .cmp(&right_deficit)
1381                    .then_with(|| right.cmp(&left))
1382            })
1383            .expect("remaining points provide centroid capacity");
1384        allocations[next] += 1;
1385        assigned += 1;
1386    }
1387    while assigned > total_clusters {
1388        let next = (0..sizes.len())
1389            .filter(|&index| allocations[index] > 1)
1390            .max_by(|&left, &right| {
1391                let left_excess = (allocations[left] as i128) * (total_points as i128)
1392                    - (total_clusters as i128) * (sizes[left] as i128);
1393                let right_excess = (allocations[right] as i128) * (total_points as i128)
1394                    - (total_clusters as i128) * (sizes[right] as i128);
1395                left_excess
1396                    .cmp(&right_excess)
1397                    .then_with(|| right.cmp(&left))
1398            })
1399            .expect("at least one branch can release a centroid");
1400        allocations[next] -= 1;
1401        assigned -= 1;
1402    }
1403    allocations
1404}
1405
1406fn reorder_point_ids(point_ids: &mut [usize], assignments: &[usize], offsets: &[usize]) {
1407    let original = point_ids.to_vec();
1408    let mut cursors = offsets[..offsets.len() - 1].to_vec();
1409    for (&point_id, &cluster) in original.iter().zip(assignments) {
1410        point_ids[cursors[cluster]] = point_id;
1411        cursors[cluster] += 1;
1412    }
1413}
1414
1415fn group_offsets(group_sizes: &[usize]) -> ScannResult<Vec<u32>> {
1416    let mut offsets = Vec::with_capacity(group_sizes.len() + 1);
1417    offsets.push(0);
1418    let mut cursor = 0usize;
1419    for &size in group_sizes {
1420        cursor = cursor
1421            .checked_add(size)
1422            .ok_or_else(|| ScannFormatError::new("ScaNN child count overflows usize"))?;
1423        offsets.push(
1424            u32::try_from(cursor)
1425                .map_err(|_| ScannFormatError::new("ScaNN child count exceeds u32"))?,
1426        );
1427    }
1428    Ok(offsets)
1429}
1430
1431fn reorder_descendants(
1432    levels: &mut [Vec<f32>],
1433    child_offsets: &mut [Vec<u32>],
1434    top_order: &[usize],
1435    dimension: usize,
1436) -> ScannResult<Vec<usize>> {
1437    if levels.len() != child_offsets.len() + 1
1438        || levels.first().map_or(0, |level| level.len() / dimension) != top_order.len()
1439    {
1440        return Err(ScannFormatError::new(
1441            "ScaNN bottom-up subtree shape mismatch",
1442        ));
1443    }
1444    let mut parent_order = top_order.to_vec();
1445    let mut permutation = Vec::new();
1446    for depth in 0..child_offsets.len() {
1447        let old_offsets = &child_offsets[depth];
1448        if old_offsets.len() != parent_order.len() + 1 {
1449            return Err(ScannFormatError::new(
1450                "ScaNN bottom-up child directory mismatch",
1451            ));
1452        }
1453        let mut next_order = Vec::with_capacity(levels[depth + 1].len() / dimension);
1454        let mut next_offsets = Vec::with_capacity(parent_order.len() + 1);
1455        next_offsets.push(0);
1456        for &old_parent in &parent_order {
1457            let start = old_offsets[old_parent] as usize;
1458            let end = old_offsets[old_parent + 1] as usize;
1459            if start > end || end > levels[depth + 1].len() / dimension {
1460                return Err(ScannFormatError::new(
1461                    "ScaNN bottom-up child range is invalid",
1462                ));
1463            }
1464            next_order.extend(start..end);
1465            next_offsets.push(
1466                u32::try_from(next_order.len())
1467                    .map_err(|_| ScannFormatError::new("ScaNN descendant count exceeds u32"))?,
1468            );
1469        }
1470        if next_order.len() != levels[depth + 1].len() / dimension {
1471            return Err(ScannFormatError::new(
1472                "ScaNN bottom-up child permutation is incomplete",
1473            ));
1474        }
1475        reorder_rows(
1476            &mut levels[depth + 1],
1477            dimension,
1478            &next_order,
1479            &mut permutation,
1480        );
1481        child_offsets[depth] = next_offsets;
1482        parent_order = next_order;
1483    }
1484    reorder_rows(&mut levels[0], dimension, top_order, &mut permutation);
1485    Ok(parent_order)
1486}
1487
1488fn reorder_rows(
1489    data: &mut [f32],
1490    dimension: usize,
1491    new_to_old: &[usize],
1492    permutation: &mut Vec<usize>,
1493) {
1494    debug_assert_eq!(data.len(), new_to_old.len() * dimension);
1495    permutation.clear();
1496    permutation.resize(new_to_old.len(), usize::MAX);
1497    for (new, &old) in new_to_old.iter().enumerate() {
1498        permutation[old] = new;
1499    }
1500    debug_assert!(!permutation.contains(&usize::MAX));
1501    for index in 0..new_to_old.len() {
1502        while permutation[index] != index {
1503            let other = permutation[index];
1504            for coordinate in 0..dimension {
1505                data.swap(
1506                    index * dimension + coordinate,
1507                    other * dimension + coordinate,
1508                );
1509            }
1510            permutation.swap(index, other);
1511        }
1512    }
1513}
1514
1515fn mix_seed(seed: u64, depth: usize, branch: usize) -> u64 {
1516    seed ^ (depth as u64 + 1).wrapping_mul(0x9e37_79b9_7f4a_7c15)
1517        ^ (branch as u64 + 1).wrapping_mul(0xbf58_476d_1ce4_e5b9)
1518}
1519
1520fn ensure_non_empty(assignments: &mut [usize], distances: &[f32], clusters: usize) {
1521    let mut counts = vec![0usize; clusters];
1522    for &cluster in assignments.iter() {
1523        counts[cluster] += 1;
1524    }
1525    for empty in 0..clusters {
1526        if counts[empty] != 0 {
1527            continue;
1528        }
1529        let donor = (0..assignments.len())
1530            .filter(|&row| counts[assignments[row]] > 1)
1531            .max_by(|&left, &right| {
1532                distances[left]
1533                    .total_cmp(&distances[right])
1534                    .then_with(|| right.cmp(&left))
1535            })
1536            .expect("clusters do not exceed points");
1537        counts[assignments[donor]] -= 1;
1538        assignments[donor] = empty;
1539        counts[empty] = 1;
1540    }
1541}
1542
1543fn nearest_centroid(centroids: &[f32], dimension: usize, point: &[f32]) -> (usize, f32) {
1544    centroids
1545        .chunks_exact(dimension)
1546        .enumerate()
1547        .map(|(index, centroid)| (index, squared_l2(point, centroid)))
1548        .min_by(|left, right| {
1549            left.1
1550                .total_cmp(&right.1)
1551                .then_with(|| left.0.cmp(&right.0))
1552        })
1553        .unwrap()
1554}
1555
1556fn score_range_into(
1557    centroids: &[f32],
1558    dimension: usize,
1559    start: usize,
1560    end: usize,
1561    query: &[f32],
1562    output: &mut Vec<RoutingCandidate>,
1563) {
1564    output.reserve(end.saturating_sub(start));
1565    output.extend((start..end).map(|index| {
1566        (
1567            index as u32,
1568            squared_l2(
1569                &centroids[index * dimension..(index + 1) * dimension],
1570                query,
1571            ),
1572        )
1573    }));
1574}
1575
1576/// Query-independent `sum step_i^2 * code_i^2` for every centroid of one
1577/// fixed-point routing level. Strict f64 accumulation per centroid: this runs
1578/// once per artifact open, and the value is subtracted from a similarly sized
1579/// query constant at query time.
1580fn quantized_code_norms(
1581    steps: &[f32],
1582    centroid_codes: &[u8],
1583    dimension: usize,
1584) -> ScannResult<Vec<f32>> {
1585    if dimension == 0 || steps.len() != dimension || !centroid_codes.len().is_multiple_of(dimension)
1586    {
1587        return Err(ScannFormatError::new(
1588            "quantized ScaNN routing level shape does not match its dimension",
1589        ));
1590    }
1591    let step_squares: Vec<f32> = steps.iter().map(|&step| step * step).collect();
1592    Ok(centroid_codes
1593        .chunks_exact(dimension)
1594        .map(|row| {
1595            row.iter()
1596                .zip(&step_squares)
1597                .fold(0.0f64, |acc, (&code, &step_square)| {
1598                    let code = f64::from(code);
1599                    acc + f64::from(step_square) * code * code
1600                }) as f32
1601        })
1602        .collect())
1603}
1604
1605static DEGENERATE_FAST_SCAN_QUERIES: std::sync::atomic::AtomicU64 =
1606    std::sync::atomic::AtomicU64::new(0);
1607
1608fn keep_best(values: &mut Vec<RoutingCandidate>, count: usize) {
1609    let compare = |left: &RoutingCandidate, right: &RoutingCandidate| {
1610        left.1
1611            .total_cmp(&right.1)
1612            .then_with(|| left.0.cmp(&right.0))
1613    };
1614    let keep = count.min(values.len());
1615    if keep == 0 {
1616        values.clear();
1617        return;
1618    }
1619    if keep < values.len() {
1620        values.select_nth_unstable_by(keep, compare);
1621        values.truncate(keep);
1622    }
1623    // Stable output order is part of deterministic routing, while the large
1624    // rejected tail no longer pays O(n log n) sorting work.
1625    values.sort_unstable_by(compare);
1626}
1627
1628fn sort_routing_candidates(values: &mut [RoutingCandidate]) {
1629    values.sort_unstable_by(|left, right| {
1630        left.1
1631            .total_cmp(&right.1)
1632            .then_with(|| left.0.cmp(&right.0))
1633    });
1634}
1635
1636#[inline]
1637fn intermediate_routing_beam(probes: usize) -> usize {
1638    if probes == 1 {
1639        // Corpus assignment follows exactly one path through the tree.
1640        1
1641    } else {
1642        QUERY_INTERMEDIATE_ROUTING_BEAM
1643    }
1644}
1645
1646#[inline]
1647fn squared_l2(left: &[f32], right: &[f32]) -> f32 {
1648    crate::structures::simd::squared_l2_f32(left, right)
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653    use super::*;
1654
1655    fn clustered_points() -> Vec<f32> {
1656        let mut points = Vec::new();
1657        for cluster in 0..8 {
1658            for row in 0..16 {
1659                points.push(cluster as f32 * 10.0 + row as f32 * 0.01);
1660                points.push((cluster % 3) as f32 * 5.0 - row as f32 * 0.005);
1661            }
1662        }
1663        points
1664    }
1665
1666    #[test]
1667    fn hierarchical_training_is_deterministic_and_nested() {
1668        let data = clustered_points();
1669        let first = train_routing_tree(&data, 128, 2, &[2, 8], 8, 17).unwrap();
1670        let second = train_routing_tree(&data, 128, 2, &[2, 8], 8, 17).unwrap();
1671        assert_eq!(first, second);
1672        assert_eq!(first.tree.level_counts().collect::<Vec<_>>(), [2, 8]);
1673        assert_eq!(first.tree.child_offsets()[0].first(), Some(&0));
1674        assert_eq!(first.tree.child_offsets()[0].last(), Some(&8));
1675    }
1676
1677    #[test]
1678    fn query_beam_is_recall_oriented_but_bounded() {
1679        assert_eq!(intermediate_routing_beam(1), 1);
1680        assert_eq!(
1681            intermediate_routing_beam(2),
1682            QUERY_INTERMEDIATE_ROUTING_BEAM
1683        );
1684        assert_eq!(
1685            intermediate_routing_beam(usize::MAX),
1686            QUERY_INTERMEDIATE_ROUTING_BEAM
1687        );
1688    }
1689
1690    #[test]
1691    fn full_probe_reaches_every_leaf_past_sixty_four_root_parents() {
1692        let root_count = 65usize;
1693        let leaf_count = root_count * root_count;
1694        let mut offsets = Vec::with_capacity(root_count + 1);
1695        for parent in 0..=root_count {
1696            offsets.push((parent * root_count) as u32);
1697        }
1698        let tree = FloatRoutingTree {
1699            dimension: 1,
1700            levels: vec![vec![0.0; root_count], vec![0.0; leaf_count]],
1701            child_offsets: vec![offsets],
1702        };
1703        let routed = tree.route(&[0.0], leaf_count).unwrap();
1704        assert_eq!(routed.len(), leaf_count);
1705        assert_eq!(
1706            routed.iter().map(|leaf| leaf.leaf).collect::<Vec<_>>(),
1707            (0..leaf_count as u32).collect::<Vec<_>>()
1708        );
1709    }
1710
1711    #[test]
1712    fn model_only_training_matches_compatibility_training() {
1713        let data = clustered_points();
1714        let model = FloatScannModel::train_model(
1715            &data,
1716            128,
1717            2,
1718            &[2, 8],
1719            1,
1720            4,
1721            0x5ca1,
1722            DEFAULT_ANISOTROPIC_THRESHOLD,
1723        )
1724        .unwrap();
1725        let (compatibility_model, encoded) = FloatScannModel::train(
1726            &data,
1727            128,
1728            2,
1729            &[2, 8],
1730            1,
1731            4,
1732            0x5ca1,
1733            DEFAULT_ANISOTROPIC_THRESHOLD,
1734        )
1735        .unwrap();
1736
1737        assert_eq!(model, compatibility_model);
1738        assert_eq!(encoded.len(), 128);
1739    }
1740
1741    #[test]
1742    fn float_encode_scratch_reuses_dimension_and_code_buffers() {
1743        let data = clustered_points();
1744        let model = FloatScannModel::train_model(
1745            &data,
1746            128,
1747            2,
1748            &[2, 8],
1749            1,
1750            4,
1751            0x5ca1,
1752            DEFAULT_ANISOTROPIC_THRESHOLD,
1753        )
1754        .unwrap();
1755        let mut scratch = FloatEncodeScratch::default();
1756
1757        model.encode_with_scratch(&data[..2], &mut scratch).unwrap();
1758        let residual_allocation = scratch.residual.as_ptr();
1759        let code_allocation = scratch.codes.as_ptr();
1760        let residual_capacity = scratch.residual.capacity();
1761        let code_capacity = scratch.codes.capacity();
1762
1763        let (leaf, codes) = model
1764            .encode_with_scratch(&data[2..4], &mut scratch)
1765            .unwrap();
1766        assert!(leaf < 8);
1767        assert_eq!(codes.len(), model.codebook.blocks());
1768        assert_eq!(scratch.residual.as_ptr(), residual_allocation);
1769        assert_eq!(scratch.codes.as_ptr(), code_allocation);
1770        assert_eq!(scratch.residual.capacity(), residual_capacity);
1771        assert_eq!(scratch.codes.capacity(), code_capacity);
1772    }
1773
1774    #[test]
1775    fn recursive_training_work_is_bounded_by_local_fanout() {
1776        let points = 4_096usize;
1777        let dimension = 4usize;
1778        let data: Vec<f32> = (0..points * dimension)
1779            .map(|index| {
1780                let row = index / dimension;
1781                let coordinate = index % dimension;
1782                (((row * 73 + coordinate * 151 + row * coordinate * 19) % 997) as f32 / 498.5) - 1.0
1783            })
1784            .collect();
1785        let trained =
1786            train_routing_tree(&data, points, dimension, &[16, 1_024], 2, 0x5ca1).unwrap();
1787
1788        assert_eq!(trained.tree.level_counts().collect::<Vec<_>>(), [16, 1_024]);
1789        assert!(trained.stats.splits > 1);
1790        assert!(
1791            trained.stats.max_split_clusters <= MAX_LOCAL_KMEANS_BRANCHES,
1792            "local split widened to {} clusters",
1793            trained.stats.max_split_clusters,
1794        );
1795        assert!(trained.stats.max_split_clusters < 1_024);
1796        assert_eq!(trained.stats.assignment_distance_evaluations, 0);
1797        assert!(trained.assignments.iter().all(|&leaf| leaf < 1_024));
1798        let one_flat_assignment = (points * 1_024) as u64;
1799        assert!(
1800            trained.stats.distance_evaluations < one_flat_assignment,
1801            "recursive trainer performed {} distance evaluations vs {one_flat_assignment} for one flat leaf pass",
1802            trained.stats.distance_evaluations,
1803        );
1804    }
1805
1806    #[test]
1807    fn quantized_tree_roundtrip_preserves_routing_recall() {
1808        let data = clustered_points();
1809        let trained = train_routing_tree(&data, 128, 2, &[2, 8], 8, 91).unwrap();
1810        let restored =
1811            FloatRoutingTree::from_quantized_levels(&trained.tree.to_quantized_levels(), 2)
1812                .unwrap();
1813        let mut recalled = 0usize;
1814        for point in data.chunks_exact(2) {
1815            let exact = nearest_centroid(trained.tree.leaf_centroids(), 2, point).0 as u32;
1816            if restored
1817                .route(point, 4)
1818                .unwrap()
1819                .iter()
1820                .any(|leaf| leaf.leaf == exact)
1821            {
1822                recalled += 1;
1823            }
1824        }
1825        assert!(recalled as f32 / 128.0 >= 0.98);
1826    }
1827
1828    #[test]
1829    fn executable_model_reopens_from_the_persisted_generation() {
1830        let data = clustered_points();
1831        let (model, _) = FloatScannModel::train(
1832            &data,
1833            128,
1834            2,
1835            &[2, 8],
1836            1,
1837            8,
1838            117,
1839            DEFAULT_ANISOTROPIC_THRESHOLD,
1840        )
1841        .unwrap();
1842        let artifact = ScannTrainedArtifact::new(
1843            19,
1844            100_000,
1845            super::super::ScannConfig {
1846                dimension: 2,
1847                tree_levels: 2,
1848                num_leaves: 8,
1849                encoding: ScannEncoding::AsymmetricHash {
1850                    dimensions_per_block: 1,
1851                    bits_per_code: 4,
1852                },
1853            },
1854            model.routing.to_quantized_levels(),
1855            Some(model.codebook.to_artifact()),
1856        )
1857        .unwrap();
1858
1859        let reopened = FloatScannModel::from_artifact(&artifact).unwrap();
1860        let artifact_bytes = artifact.to_bytes().unwrap();
1861        let artifact_view = ScannTrainedArtifactView::parse(&artifact_bytes).unwrap();
1862        let quantized = QuantizedFloatScannModel::from_artifact_view(&artifact_view).unwrap();
1863        let quantized_view = quantized.view(&artifact_bytes).unwrap();
1864        assert_eq!(reopened.codebook, model.codebook);
1865        assert_eq!(
1866            reopened.anisotropic_threshold(),
1867            DEFAULT_ANISOTROPIC_THRESHOLD
1868        );
1869        assert_eq!(reopened.routing.level_counts().collect::<Vec<_>>(), [2, 8]);
1870        for point in data.chunks_exact(2).take(16) {
1871            let encoded = reopened.encode(point).unwrap();
1872            let query = reopened.prepare_query(point, 8).unwrap();
1873            assert!(query.score(&encoded).unwrap().unwrap().is_finite());
1874            assert_eq!(quantized_view.encode(point).unwrap(), encoded);
1875            // The mmap-backed route scores the expanded squared distance with
1876            // an `f32 x u8` kernel, so centroid dots agree with the decoded
1877            // owned tree to float tolerance rather than bit-for-bit.
1878            let quantized_query = quantized_view.prepare_query(point, 8).unwrap();
1879            assert_eq!(quantized_query.routed_leaves(), query.routed_leaves());
1880            assert_eq!(quantized_query.ah_query(), query.ah_query());
1881            for ((leaf, quantized_dot), (_, exact_dot)) in
1882                quantized_query.routed().zip(query.routed())
1883            {
1884                assert!(
1885                    (quantized_dot - exact_dot).abs() <= 1e-4 * (1.0 + exact_dot.abs()),
1886                    "leaf {leaf}: quantized centroid dot {quantized_dot} vs {exact_dot}"
1887                );
1888            }
1889        }
1890    }
1891
1892    /// The expanded-distance kernel route must select the same leaves as
1893    /// decoding every centroid coordinate, up to float tolerance on ties.
1894    #[test]
1895    fn quantized_kernel_routing_matches_decoded_reference_distances() {
1896        let dimension = 24usize;
1897        let points = 512usize;
1898        let data: Vec<f32> = (0..points * dimension)
1899            .map(|index| {
1900                let row = index / dimension;
1901                let coordinate = index % dimension;
1902                (((row * 73 + coordinate * 151 + row * coordinate * 19) % 997) as f32 / 498.5) - 1.0
1903            })
1904            .collect();
1905        let (model, _) = FloatScannModel::train(
1906            &data,
1907            points,
1908            dimension,
1909            &[4, 32],
1910            4,
1911            3,
1912            0xfa57,
1913            DEFAULT_ANISOTROPIC_THRESHOLD,
1914        )
1915        .unwrap();
1916        let artifact = ScannTrainedArtifact::new(
1917            5,
1918            100_000,
1919            super::super::ScannConfig {
1920                dimension: dimension as u32,
1921                tree_levels: 2,
1922                num_leaves: 32,
1923                encoding: ScannEncoding::AsymmetricHash {
1924                    dimensions_per_block: 4,
1925                    bits_per_code: 4,
1926                },
1927            },
1928            model.routing.to_quantized_levels(),
1929            Some(model.codebook.to_artifact()),
1930        )
1931        .unwrap();
1932        let bytes = artifact.to_bytes().unwrap();
1933        let view = ScannTrainedArtifactView::parse(&bytes).unwrap();
1934        let quantized = QuantizedFloatScannModel::from_artifact_view(&view).unwrap();
1935        let quantized_view = quantized.view(&bytes).unwrap();
1936        // Reference: decode the same quantized leaf plane into an owned tree.
1937        let decoded = FloatRoutingTree::from_quantized_levels(&artifact.levels, dimension).unwrap();
1938        for point in data.chunks_exact(dimension).take(64) {
1939            let expected = decoded.route(point, 6).unwrap();
1940            let got = quantized_view.route(point, 6).unwrap();
1941            let expected_leaves: Vec<u32> = expected.iter().map(|leaf| leaf.leaf).collect();
1942            let got_leaves: Vec<u32> = got.iter().map(|leaf| leaf.leaf).collect();
1943            if expected_leaves != got_leaves {
1944                // Ties (or near-ties) may order differently; the retained
1945                // distance frontier must still match within tolerance.
1946                let worst_expected = expected.last().unwrap().squared_distance;
1947                for leaf in &got {
1948                    assert!(
1949                        leaf.squared_distance <= worst_expected + 1e-3,
1950                        "kernel route kept leaf {} at {} beyond reference frontier {worst_expected}",
1951                        leaf.leaf,
1952                        leaf.squared_distance
1953                    );
1954                }
1955            }
1956            for (reference, kernel) in expected.iter().zip(&got) {
1957                assert!(
1958                    (reference.squared_distance - kernel.squared_distance).abs()
1959                        <= 1e-3 * (1.0 + reference.squared_distance),
1960                    "distance drift {} vs {}",
1961                    reference.squared_distance,
1962                    kernel.squared_distance
1963                );
1964            }
1965        }
1966    }
1967
1968    #[test]
1969    fn float_scann_end_to_end_recall_is_deterministic() {
1970        let mut data = Vec::with_capacity(256 * 8);
1971        for row in 0..256 {
1972            let mut vector: Vec<f32> = (0..8)
1973                .map(|coordinate| {
1974                    let raw = ((row * 73 + coordinate * 151 + row * coordinate * 19) % 997) as f32;
1975                    raw / 498.5 - 1.0
1976                })
1977                .collect();
1978            let norm = vector.iter().map(|value| value * value).sum::<f32>().sqrt();
1979            vector.iter_mut().for_each(|value| *value /= norm);
1980            data.extend(vector);
1981        }
1982        let (model, encoded) =
1983            FloatScannModel::train(&data, 256, 8, &[2, 8], 2, 7, 123, 0.2).unwrap();
1984        let mut recalled = 0usize;
1985        let queries = 32usize;
1986        let k = 10usize;
1987        for query in data.chunks_exact(8).take(queries) {
1988            let mut exact: Vec<(usize, f32)> = data
1989                .chunks_exact(8)
1990                .enumerate()
1991                .map(|(row, vector)| {
1992                    (
1993                        row,
1994                        crate::structures::simd::dot_product_f32(query, vector, 8),
1995                    )
1996                })
1997                .collect();
1998            exact.sort_unstable_by(|left, right| right.1.total_cmp(&left.1));
1999            let prepared = model.prepare_query(query, 8).unwrap();
2000            let mut approximate: Vec<(usize, f32)> = encoded
2001                .iter()
2002                .enumerate()
2003                .map(|(row, vector)| (row, prepared.score(vector).unwrap().unwrap()))
2004                .collect();
2005            approximate.sort_unstable_by(|left, right| right.1.total_cmp(&left.1));
2006            recalled += approximate[..k]
2007                .iter()
2008                .filter(|(row, _)| exact[..k].iter().any(|(exact_row, _)| exact_row == row))
2009                .count();
2010        }
2011        let recall = recalled as f32 / (queries * k) as f32;
2012        assert!(recall >= 0.70, "unexpected float ScaNN recall@10: {recall}");
2013    }
2014}