1use 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#[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
40const QUERY_INTERMEDIATE_ROUTING_BEAM: usize = 64;
44
45#[derive(Clone, Debug, PartialEq)]
46pub struct RoutingTraining {
47 pub tree: FloatRoutingTree,
48 pub assignments: Vec<u32>,
50 pub stats: RoutingTrainingStats,
52}
53
54#[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 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#[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#[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#[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 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 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 #[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), ¢er) 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 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 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 pub fn ah_query(&self) -> &AhQuery {
649 &self.ah
650 }
651
652 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 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 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 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
892const MAX_LOCAL_KMEANS_BRANCHES: usize = 64;
896
897pub 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(¢roids, 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(¢roids, 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 ¢roids[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 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 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}