Skip to main content

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