1use serde::{Deserialize, Serialize};
7
8use super::routing::{
9 HIERARCHICAL_TRAINING_THRESHOLD, HnswRoutingGraph, IvfProbePlan, IvfRoutingTopology,
10 allocate_child_clusters, effective_routing_mode, float_probe_fingerprint, routing_parent_count,
11 select_best_candidates, select_parent_beam, select_parent_beam_for_build,
12};
13use super::soar::{MultiAssignment, SoarConfig};
14use crate::dsl::IvfRoutingMode;
15
16const SOAR_LAMBDA: f32 = 1.0;
20const MAX_SELECTIVE_SECONDARY_TO_PRIMARY_DISTANCE_RATIO_SQ: f32 = 4.0;
28const BUILD_ASSIGNMENT_CANDIDATES: usize = 128;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CoarseConfig {
36 pub num_clusters: usize,
38 pub dim: usize,
40 pub max_iters: usize,
42 pub seed: u64,
44 pub soar: Option<SoarConfig>,
46 pub routing: IvfRoutingMode,
48}
49
50impl CoarseConfig {
51 pub fn new(dim: usize, num_clusters: usize) -> Self {
52 Self {
53 num_clusters,
54 dim,
55 max_iters: 25,
56 seed: 42,
57 soar: None,
58 routing: IvfRoutingMode::Auto,
59 }
60 }
61
62 pub fn with_soar(mut self, config: SoarConfig) -> Self {
63 self.soar = Some(config);
64 self
65 }
66
67 pub fn with_seed(mut self, seed: u64) -> Self {
68 self.seed = seed;
69 self
70 }
71
72 pub fn with_max_iters(mut self, iters: usize) -> Self {
73 self.max_iters = iters;
74 self
75 }
76
77 pub fn with_routing(mut self, routing: IvfRoutingMode) -> Self {
78 self.routing = routing;
79 self
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct CoarseCentroids {
86 pub num_clusters: u32,
88 pub dim: usize,
90 pub centroids: Vec<f32>,
92 pub version: u64,
94 pub soar_config: Option<SoarConfig>,
96 pub(crate) routing_index: Option<FloatCentroidRouter>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub(crate) enum FloatCentroidRouter {
102 TwoLevel {
103 parent_centroids: Vec<f32>,
104 topology: IvfRoutingTopology,
105 },
106 Hnsw(HnswRoutingGraph),
107}
108
109struct FlatClusterMemberships {
110 offsets: Vec<usize>,
111 members: Vec<usize>,
112}
113
114impl FlatClusterMemberships {
115 fn cluster(&self, cluster: usize) -> &[usize] {
116 &self.members[self.offsets[cluster]..self.offsets[cluster + 1]]
117 }
118}
119
120impl CoarseCentroids {
121 pub fn train(config: &CoarseConfig, vectors: &[Vec<f32>]) -> Self {
125 assert!(!vectors.is_empty(), "Cannot train on empty vector set");
126 assert!(config.num_clusters > 0, "Need at least 1 cluster");
127 assert!(vectors.iter().all(|vector| vector.len() == config.dim));
128
129 let flat = vectors
133 .iter()
134 .flat_map(|vector| vector.iter().copied())
135 .collect::<Vec<_>>();
136 Self::train_contiguous(config, &flat, vectors.len())
137 }
138
139 pub(crate) fn train_contiguous(
141 config: &CoarseConfig,
142 vectors: &[f32],
143 vector_count: usize,
144 ) -> Self {
145 assert!(vector_count > 0, "Cannot train on empty vector set");
146 assert!(config.num_clusters > 0, "Need at least 1 cluster");
147 assert_eq!(vectors.len(), vector_count.saturating_mul(config.dim));
148
149 let actual_clusters = config.num_clusters.min(vector_count);
150 let (centroids, routing_index) =
151 match effective_routing_mode(config.routing, actual_clusters) {
152 IvfRoutingMode::TwoLevel => {
153 let (leaves, router) =
154 Self::train_hierarchical(config, vectors, vector_count, actual_clusters);
155 (leaves, Some(router))
156 }
157 IvfRoutingMode::Hnsw => {
158 let leaves = if actual_clusters >= HIERARCHICAL_TRAINING_THRESHOLD {
159 Self::train_hierarchical(config, vectors, vector_count, actual_clusters).0
160 } else {
161 Self::train_flat(config, vectors, vector_count, actual_clusters)
162 };
163 let graph = HnswRoutingGraph::build(
164 actual_clusters,
165 |left, right| {
166 let left = left as usize * config.dim;
167 let right = right as usize * config.dim;
168 squared_l2(
169 &leaves[left..left + config.dim],
170 &leaves[right..right + config.dim],
171 )
172 },
173 config.seed,
174 );
175 (leaves, Some(FloatCentroidRouter::Hnsw(graph)))
176 }
177 IvfRoutingMode::Flat | IvfRoutingMode::Auto => (
178 Self::train_flat(config, vectors, vector_count, actual_clusters),
179 None,
180 ),
181 };
182
183 let version = std::time::SystemTime::now()
184 .duration_since(std::time::UNIX_EPOCH)
185 .unwrap_or_default()
186 .as_nanos() as u64;
187
188 let mut soar_config = config.soar.clone();
189 if let Some(soar) = &mut soar_config
190 && soar.num_secondary > 1
191 {
192 log::warn!(
193 "SOAR currently implements the published primary + one-secondary objective; \
194 clamping {} requested secondary assignments to one",
195 soar.num_secondary,
196 );
197 soar.num_secondary = 1;
198 }
199 let calibration_target = soar_config
200 .as_ref()
201 .and_then(SoarConfig::calibration_target);
202 let mut trained = Self {
203 num_clusters: actual_clusters as u32,
204 dim: config.dim,
205 centroids,
206 version,
207 soar_config: None,
210 routing_index,
211 };
212 if let Some(target) = calibration_target {
213 let threshold = trained.calibrate_selective_spill_threshold(
214 vectors,
215 vector_count,
216 config.routing,
217 target,
218 );
219 if let Some(soar) = &mut soar_config {
220 soar.spill_threshold = threshold;
221 }
222 log::info!(
223 "Calibrated SOAR selective spilling to at most {:.1}% of the training sample \
224 (residual threshold {:.6})",
225 target * 100.0,
226 threshold,
227 );
228 }
229 trained.soar_config = soar_config;
230 trained
231 }
232
233 fn calibrate_selective_spill_threshold(
234 &self,
235 vectors: &[f32],
236 vector_count: usize,
237 routing: IvfRoutingMode,
238 target_fraction: f32,
239 ) -> f32 {
240 let routing = effective_routing_mode(routing, self.num_clusters as usize);
244 let sample_limit = match routing {
245 IvfRoutingMode::Flat | IvfRoutingMode::Auto => {
246 let work_per_vector = (self.num_clusters as usize).saturating_mul(self.dim).max(1);
247 (64_000_000usize / work_per_vector).clamp(32, 4_096)
248 }
249 IvfRoutingMode::TwoLevel | IvfRoutingMode::Hnsw => 8_192,
250 }
251 .min(vector_count);
252 let mut residual_norms = Vec::with_capacity(sample_limit);
253 for sample in 0..sample_limit {
254 let vector_index = sample.saturating_mul(vector_count) / sample_limit;
255 let offset = vector_index * self.dim;
256 let vector = &vectors[offset..offset + self.dim];
257 let primary = self.assign_with_routing(vector, routing).primary_cluster;
258 residual_norms.push(squared_l2(vector, self.get_centroid(primary)).sqrt());
259 }
260 residual_norms.sort_unstable_by(f32::total_cmp);
261 if residual_norms.is_empty() {
262 return 0.0;
263 }
264 let spill_count = ((residual_norms.len() as f32 * target_fraction.clamp(0.0, 1.0)).round()
265 as usize)
266 .min(residual_norms.len());
267 if spill_count == 0 {
268 let largest = residual_norms.last().copied().unwrap_or(0.0);
269 return threshold_strictly_above(largest);
270 }
271 let boundary = residual_norms[residual_norms.len() - spill_count];
272 let at_or_above =
273 residual_norms.len() - residual_norms.partition_point(|&norm| norm < boundary);
274 if at_or_above > spill_count {
275 threshold_strictly_above(boundary)
280 } else {
281 boundary
282 }
283 }
284
285 fn train_flat(
286 config: &CoarseConfig,
287 vectors: &[f32],
288 vector_count: usize,
289 clusters: usize,
290 ) -> Vec<f32> {
291 Self::run_kmeans(config, vectors, vector_count, clusters).centroids
292 }
293
294 fn train_flat_with_memberships(
295 config: &CoarseConfig,
296 vectors: &[f32],
297 vector_count: usize,
298 clusters: usize,
299 ) -> (Vec<f32>, FlatClusterMemberships) {
300 let trained = Self::run_kmeans(config, vectors, vector_count, clusters);
301 (
302 trained.centroids,
303 FlatClusterMemberships {
304 offsets: trained.member_offsets,
305 members: trained.members,
306 },
307 )
308 }
309
310 fn run_kmeans(
311 config: &CoarseConfig,
312 vectors: &[f32],
313 vector_count: usize,
314 clusters: usize,
315 ) -> crate::structures::vector::kmeans::EuclideanKMeans {
316 crate::structures::vector::kmeans::train_euclidean_kmeans(
317 vectors,
318 vector_count,
319 config.dim,
320 clusters,
321 config.max_iters,
322 config.seed,
323 )
324 }
325
326 fn train_hierarchical(
327 config: &CoarseConfig,
328 vectors: &[f32],
329 vector_count: usize,
330 leaf_count: usize,
331 ) -> (Vec<f32>, FloatCentroidRouter) {
332 let parent_count = routing_parent_count(leaf_count).min(vector_count);
333 let mut parent_config = config.clone();
334 parent_config.routing = IvfRoutingMode::Flat;
335 parent_config.num_clusters = parent_count;
336 let (parent_centroids, groups) =
337 Self::train_flat_with_memberships(&parent_config, vectors, vector_count, parent_count);
338 let group_sizes: Vec<usize> = (0..parent_count)
339 .map(|parent| groups.cluster(parent).len())
340 .collect();
341 let child_counts = allocate_child_clusters(&group_sizes, leaf_count);
342 let mut leaves = Vec::with_capacity(leaf_count.saturating_mul(config.dim));
343 let mut children = vec![Vec::new(); parent_count];
344 let mut group_vectors = Vec::new();
345
346 for (parent, &child_count) in child_counts.iter().enumerate() {
347 if child_count == 0 {
348 continue;
349 }
350 let indices = groups.cluster(parent);
351 group_vectors.clear();
352 group_vectors.reserve(indices.len().saturating_mul(config.dim));
353 for &index in indices {
354 let offset = index * config.dim;
355 group_vectors.extend_from_slice(&vectors[offset..offset + config.dim]);
356 }
357 let mut child_config = config.clone();
358 child_config.routing = IvfRoutingMode::Flat;
359 child_config.num_clusters = child_count;
360 child_config.seed = config.seed ^ (parent as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15);
361 let first_leaf = leaves.len() / config.dim;
362 leaves.extend_from_slice(&Self::train_flat(
363 &child_config,
364 &group_vectors,
365 indices.len(),
366 child_count,
367 ));
368 children[parent].extend((first_leaf..first_leaf + child_count).map(|leaf| leaf as u32));
369 }
370 debug_assert_eq!(leaves.len(), leaf_count * config.dim);
371
372 (
373 leaves,
374 FloatCentroidRouter::TwoLevel {
375 parent_centroids,
376 topology: IvfRoutingTopology::from_children(&children),
377 },
378 )
379 }
380
381 fn find_nearest_idx_static(vector: &[f32], centroids: &[f32], dim: usize) -> usize {
383 let num_clusters = centroids.len() / dim;
384 let mut best_idx = 0;
385 let mut best_dist = f32::MAX;
386
387 for c in 0..num_clusters {
388 let offset = c * dim;
389 let dist: f32 = vector
390 .iter()
391 .zip(¢roids[offset..offset + dim])
392 .map(|(&a, &b)| (a - b) * (a - b))
393 .sum();
394
395 if dist < best_dist {
396 best_dist = dist;
397 best_idx = c;
398 }
399 }
400
401 best_idx
402 }
403
404 pub fn find_nearest(&self, vector: &[f32]) -> u32 {
406 Self::find_nearest_idx_static(vector, &self.centroids, self.dim) as u32
407 }
408
409 pub fn find_k_nearest(&self, vector: &[f32], k: usize) -> Vec<u32> {
411 let mut distances: Vec<(u32, f32)> = (0..self.num_clusters)
412 .map(|c| {
413 let offset = c as usize * self.dim;
414 let dist: f32 = vector
415 .iter()
416 .zip(&self.centroids[offset..offset + self.dim])
417 .map(|(&a, &b)| (a - b) * (a - b))
418 .sum();
419 (c, dist)
420 })
421 .collect();
422
423 if distances.len() > k {
425 distances.select_nth_unstable_by(k, |a, b| a.1.total_cmp(&b.1));
426 distances.truncate(k);
427 }
428 distances.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
429 distances.into_iter().map(|(c, _)| c).collect()
430 }
431
432 pub fn probe(&self, vector: &[f32], k: usize, mode: IvfRoutingMode) -> IvfProbePlan {
437 let take = k.clamp(1, self.num_clusters as usize);
438 let clusters = match effective_routing_mode(mode, self.num_clusters as usize) {
439 IvfRoutingMode::Flat | IvfRoutingMode::Auto => self.find_k_nearest(vector, take),
440 IvfRoutingMode::TwoLevel => self.find_k_nearest_two_level(vector, take),
441 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw(vector, take),
442 };
443 IvfProbePlan::new(
444 self.version,
445 float_probe_fingerprint(vector, take, mode),
446 clusters,
447 )
448 }
449
450 pub fn validate_routing(&self, mode: IvfRoutingMode) -> Result<(), String> {
451 match effective_routing_mode(mode, self.num_clusters as usize) {
452 IvfRoutingMode::Flat | IvfRoutingMode::Auto => Ok(()),
453 IvfRoutingMode::TwoLevel => {
454 let Some(FloatCentroidRouter::TwoLevel {
455 parent_centroids,
456 topology,
457 }) = self.routing_index.as_ref()
458 else {
459 return Err(
460 "two-level IVF routing was requested but the global codebook has no matching router"
461 .to_string(),
462 );
463 };
464 let parent_count = topology.parent_count();
465 if parent_count == 0
466 || parent_centroids.len() != parent_count.saturating_mul(self.dim)
467 || !topology.validate(self.num_clusters as usize)
468 || parent_centroids.iter().any(|value| !value.is_finite())
469 {
470 return Err("invalid float two-level IVF routing index".to_string());
471 }
472 Ok(())
473 }
474 IvfRoutingMode::Hnsw => {
475 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
476 return Err(
477 "HNSW IVF routing was requested but the global codebook has no HNSW graph"
478 .to_string(),
479 );
480 };
481 if !graph.validate(self.num_clusters as usize) {
482 return Err("invalid float HNSW routing graph".to_string());
483 }
484 Ok(())
485 }
486 }
487 }
488
489 fn find_k_nearest_two_level(&self, vector: &[f32], k: usize) -> Vec<u32> {
490 self.find_k_nearest_two_level_impl::<false>(vector, k)
491 }
492
493 fn find_k_nearest_two_level_for_build(&self, vector: &[f32], k: usize) -> Vec<u32> {
494 self.find_k_nearest_two_level_impl::<true>(vector, k)
495 }
496
497 fn find_k_nearest_two_level_impl<const FOR_BUILD: bool>(
498 &self,
499 vector: &[f32],
500 k: usize,
501 ) -> Vec<u32> {
502 let Some(FloatCentroidRouter::TwoLevel {
503 parent_centroids,
504 topology,
505 }) = self.routing_index.as_ref()
506 else {
507 return self.find_k_nearest(vector, k);
508 };
509 if topology.parent_count() <= 1 {
510 return self.find_k_nearest(vector, k);
511 }
512
513 let mut parent_scores = vec![0.0; topology.parent_count()];
514 for (parent_id, score) in parent_scores.iter_mut().enumerate() {
515 let offset = parent_id * self.dim;
516 *score = squared_l2(vector, &parent_centroids[offset..offset + self.dim]);
517 }
518 let parents = if FOR_BUILD {
519 select_parent_beam_for_build::<false>(&parent_scores, topology, k)
520 } else {
521 select_parent_beam::<false>(&parent_scores, topology, k)
522 };
523 let candidate_capacity = parents
524 .iter()
525 .map(|&parent| topology.children(parent as usize).len())
526 .sum();
527 let mut candidates = Vec::with_capacity(candidate_capacity);
528 for parent in parents {
529 for &leaf in topology.children(parent as usize) {
530 candidates.push((leaf, squared_l2(vector, self.get_centroid(leaf))));
531 }
532 }
533 select_best_candidates::<false>(&mut candidates, k)
534 }
535
536 fn find_k_nearest_hnsw(&self, vector: &[f32], k: usize) -> Vec<u32> {
537 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
538 return self.find_k_nearest(vector, k);
539 };
540 graph.search(|leaf| squared_l2(vector, self.get_centroid(leaf)), k)
541 }
542
543 pub fn find_k_nearest_with_distances(&self, vector: &[f32], k: usize) -> Vec<(u32, f32)> {
545 let mut distances: Vec<(u32, f32)> = (0..self.num_clusters)
546 .map(|c| {
547 let offset = c as usize * self.dim;
548 let dist: f32 = vector
549 .iter()
550 .zip(&self.centroids[offset..offset + self.dim])
551 .map(|(&a, &b)| (a - b) * (a - b))
552 .sum();
553 (c, dist)
554 })
555 .collect();
556
557 if distances.len() > k {
559 distances.select_nth_unstable_by(k, |a, b| a.1.total_cmp(&b.1));
560 distances.truncate(k);
561 }
562 distances.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
563 distances
564 }
565
566 pub fn assign(&self, vector: &[f32]) -> MultiAssignment {
568 self.assign_with_routing(vector, IvfRoutingMode::Flat)
569 }
570
571 pub fn assign_with_routing(&self, vector: &[f32], routing: IvfRoutingMode) -> MultiAssignment {
575 if let Some(ref soar_config) = self.soar_config {
576 self.assign_with_soar_and_routing(vector, soar_config, routing)
577 } else {
578 let primary_cluster = match effective_routing_mode(routing, self.num_clusters as usize)
579 {
580 IvfRoutingMode::Hnsw => self.find_nearest_hnsw_for_build(vector),
581 IvfRoutingMode::TwoLevel => self
582 .find_k_nearest_two_level_for_build(
583 vector,
584 BUILD_ASSIGNMENT_CANDIDATES.min(self.num_clusters as usize),
585 )
586 .first()
587 .copied()
588 .unwrap_or(0),
589 IvfRoutingMode::Flat | IvfRoutingMode::Auto => self.find_nearest(vector),
590 };
591 MultiAssignment {
592 primary_cluster,
593 secondary_clusters: Vec::new(),
594 }
595 }
596 }
597
598 pub fn assign_with_soar(&self, vector: &[f32], config: &SoarConfig) -> MultiAssignment {
600 self.assign_with_soar_and_routing(vector, config, IvfRoutingMode::Flat)
601 }
602
603 fn assign_with_soar_and_routing(
604 &self,
605 vector: &[f32],
606 config: &SoarConfig,
607 routing: IvfRoutingMode,
608 ) -> MultiAssignment {
609 let num_secondary = config.num_secondary.min(1);
614 let candidate_budget =
618 soar_build_candidate_budget(num_secondary, self.num_clusters as usize);
619 let leaf_ids: Vec<u32> = match effective_routing_mode(routing, self.num_clusters as usize) {
620 IvfRoutingMode::TwoLevel => {
621 self.two_level_candidate_leaves_for_build(vector, candidate_budget)
622 }
623 IvfRoutingMode::Hnsw => self.find_k_nearest_hnsw_for_build(vector, candidate_budget),
624 IvfRoutingMode::Flat | IvfRoutingMode::Auto => (0..self.num_clusters).collect(),
625 };
626 let leaf_distances: Vec<(u32, f32)> = leaf_ids
629 .into_iter()
630 .map(|cluster| (cluster, squared_l2(vector, self.get_centroid(cluster))))
631 .collect();
632 let primary = leaf_distances
633 .iter()
634 .min_by(|left, right| scored_cluster_order(left, right))
635 .map(|&(cluster, _)| cluster)
636 .unwrap_or(0);
637 let primary_centroid = self.get_centroid(primary);
638
639 let residual: Vec<f32> = vector
641 .iter()
642 .zip(primary_centroid)
643 .map(|(v, c)| v - c)
644 .collect();
645
646 let residual_norm_sq: f32 = residual.iter().map(|x| x * x).sum();
647
648 if config.selective && residual_norm_sq < config.spill_threshold * config.spill_threshold {
650 return MultiAssignment {
651 primary_cluster: primary,
652 secondary_clusters: Vec::new(),
653 };
654 }
655
656 let mut candidates: Vec<(u32, f32)> = leaf_distances
664 .into_iter()
665 .filter(|&(cluster, secondary_residual_norm_sq)| {
666 cluster != primary
667 && (!config.selective
668 || secondary_residual_norm_sq
669 <= MAX_SELECTIVE_SECONDARY_TO_PRIMARY_DISTANCE_RATIO_SQ
670 * residual_norm_sq)
671 })
672 .map(|(cluster, secondary_residual_norm_sq)| {
673 (
674 cluster,
675 soar_secondary_loss(
676 vector,
677 self.get_centroid(cluster),
678 &residual,
679 residual_norm_sq,
680 secondary_residual_norm_sq,
681 ),
682 )
683 })
684 .collect();
685
686 let take = num_secondary.min(candidates.len());
689 if candidates.len() > take {
690 candidates.select_nth_unstable_by(take, scored_cluster_order);
691 candidates.truncate(take);
692 }
693 candidates.sort_unstable_by(scored_cluster_order);
694
695 MultiAssignment {
696 primary_cluster: primary,
697 secondary_clusters: candidates
698 .iter()
699 .take(num_secondary)
700 .map(|(c, _)| *c)
701 .collect(),
702 }
703 }
704
705 fn two_level_candidate_leaves_for_build(&self, vector: &[f32], k: usize) -> Vec<u32> {
706 let Some(FloatCentroidRouter::TwoLevel {
707 parent_centroids,
708 topology,
709 }) = self.routing_index.as_ref()
710 else {
711 return (0..self.num_clusters).collect();
712 };
713 let mut parent_scores = vec![0.0; topology.parent_count()];
714 for (parent_id, score) in parent_scores.iter_mut().enumerate() {
715 let offset = parent_id * self.dim;
716 *score = squared_l2(vector, &parent_centroids[offset..offset + self.dim]);
717 }
718 let parents = select_parent_beam_for_build::<false>(&parent_scores, topology, k);
719 let capacity = parents
720 .iter()
721 .map(|&parent| topology.children(parent as usize).len())
722 .sum();
723 let mut leaves = Vec::with_capacity(capacity);
724 for parent in parents {
725 leaves.extend_from_slice(topology.children(parent as usize));
726 }
727 leaves
728 }
729
730 fn find_k_nearest_hnsw_for_build(&self, vector: &[f32], k: usize) -> Vec<u32> {
731 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
732 return self.find_k_nearest(vector, k);
733 };
734 graph.search_for_build(|leaf| squared_l2(vector, self.get_centroid(leaf)), k)
735 }
736
737 fn find_nearest_hnsw_for_build(&self, vector: &[f32]) -> u32 {
740 let Some(FloatCentroidRouter::Hnsw(graph)) = self.routing_index.as_ref() else {
741 return self.find_nearest(vector);
742 };
743 graph
744 .search_best_for_build(|leaf| squared_l2(vector, self.get_centroid(leaf)))
745 .unwrap_or(0)
746 }
747
748 pub fn get_centroid(&self, cluster_id: u32) -> &[f32] {
750 let offset = cluster_id as usize * self.dim;
751 &self.centroids[offset..offset + self.dim]
752 }
753
754 pub fn compute_residual(&self, vector: &[f32], cluster_id: u32) -> Vec<f32> {
756 let centroid = self.get_centroid(cluster_id);
757 vector.iter().zip(centroid).map(|(&v, &c)| v - c).collect()
758 }
759
760 pub fn size_bytes(&self) -> usize {
762 let routing_bytes = self
763 .routing_index
764 .as_ref()
765 .map_or(0, |router| match router {
766 FloatCentroidRouter::TwoLevel {
767 parent_centroids,
768 topology,
769 } => {
770 parent_centroids.len() * size_of::<f32>()
771 + topology.parent_count() * size_of::<u32>()
772 + self.num_clusters as usize * size_of::<u32>()
773 }
774 FloatCentroidRouter::Hnsw(graph) => graph.size_bytes(),
775 });
776 self.centroids.len() * size_of::<f32>() + routing_bytes + 64
777 }
778
779 #[cfg(feature = "native")]
782 pub(crate) fn visit_routing_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
783 if let Some(router) = &self.routing_index {
784 match router {
785 FloatCentroidRouter::TwoLevel {
786 parent_centroids,
787 topology,
788 } => {
789 topology.visit_resident_regions(visit);
790 visit(
791 "float parent centroids",
792 super::routing::bytes_of_slice(parent_centroids),
793 );
794 }
795 FloatCentroidRouter::Hnsw(graph) => graph.visit_resident_regions(visit),
796 }
797 }
798 }
799
800 #[cfg(feature = "native")]
801 pub(crate) fn visit_leaf_centroid_region(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
802 visit(
803 "float leaf centroids",
804 super::routing::bytes_of_slice(&self.centroids),
805 );
806 }
807
808 pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
810 bincode::serde::encode_to_vec(self, bincode::config::standard())
811 .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
812 }
813}
814
815#[inline]
816fn squared_l2(left: &[f32], right: &[f32]) -> f32 {
817 left.iter()
818 .zip(right)
819 .map(|(&a, &b)| {
820 let delta = a - b;
821 delta * delta
822 })
823 .sum()
824}
825
826#[inline]
827fn threshold_strictly_above(value: f32) -> f32 {
828 if !value.is_finite() {
829 return f32::INFINITY;
830 }
831 let next = value + value.abs().max(1.0) * (4.0 * f32::EPSILON);
836 if next > value { next } else { f32::INFINITY }
837}
838
839#[inline]
840fn soar_build_candidate_budget(num_secondary: usize, num_clusters: usize) -> usize {
841 num_secondary
842 .saturating_add(1)
843 .saturating_mul(64)
844 .max(BUILD_ASSIGNMENT_CANDIDATES)
845 .min(num_clusters)
846}
847
848#[inline]
849fn soar_secondary_loss(
850 vector: &[f32],
851 secondary_centroid: &[f32],
852 primary_residual: &[f32],
853 primary_residual_norm_sq: f32,
854 secondary_residual_norm_sq: f32,
855) -> f32 {
856 let residual_dot = vector
857 .iter()
858 .zip(secondary_centroid)
859 .zip(primary_residual)
860 .map(|((&value, ¢roid), &primary)| primary * (value - centroid))
861 .sum::<f32>();
862
863 let projection_norm_sq = if primary_residual_norm_sq > 0.0 {
866 residual_dot * residual_dot / primary_residual_norm_sq
867 } else {
868 0.0
869 };
870 secondary_residual_norm_sq + SOAR_LAMBDA * projection_norm_sq
871}
872
873#[inline]
874fn scored_cluster_order(left: &(u32, f32), right: &(u32, f32)) -> std::cmp::Ordering {
875 left.1
876 .total_cmp(&right.1)
877 .then_with(|| left.0.cmp(&right.0))
878}
879
880#[cfg(test)]
881mod tests {
882 use super::*;
883 use rand::prelude::*;
884
885 #[test]
886 fn test_coarse_centroids_basic() {
887 let dim = 64;
888 let n = 1000;
889 let num_clusters = 16;
890
891 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
892 let vectors: Vec<Vec<f32>> = (0..n)
893 .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
894 .collect();
895
896 let config = CoarseConfig::new(dim, num_clusters);
897 let centroids = CoarseCentroids::train(&config, &vectors);
898
899 assert_eq!(centroids.num_clusters, num_clusters as u32);
900 assert_eq!(centroids.dim, dim);
901 }
902
903 #[test]
904 fn contiguous_training_matches_row_wrapper() {
905 let dim = 8;
906 let vectors: Vec<Vec<f32>> = (0..128)
907 .map(|row| {
908 (0..dim)
909 .map(|column| ((row * 31 + column * 17) % 101) as f32 / 101.0)
910 .collect()
911 })
912 .collect();
913 let flat = vectors.iter().flatten().copied().collect::<Vec<_>>();
914 let config = CoarseConfig::new(dim, 12)
915 .with_seed(91)
916 .with_routing(IvfRoutingMode::TwoLevel);
917
918 let rows = CoarseCentroids::train(&config, &vectors);
919 let contiguous = CoarseCentroids::train_contiguous(&config, &flat, vectors.len());
920
921 assert_eq!(rows.centroids, contiguous.centroids);
922 assert_eq!(
923 bincode::serde::encode_to_vec(&rows.routing_index, bincode::config::standard())
924 .unwrap(),
925 bincode::serde::encode_to_vec(&contiguous.routing_index, bincode::config::standard())
926 .unwrap(),
927 );
928 }
929
930 #[test]
931 fn test_find_nearest() {
932 let dim = 32;
933 let n = 500;
934 let num_clusters = 8;
935
936 let mut rng = rand::rngs::StdRng::seed_from_u64(123);
937 let vectors: Vec<Vec<f32>> = (0..n)
938 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
939 .collect();
940
941 let config = CoarseConfig::new(dim, num_clusters);
942 let centroids = CoarseCentroids::train(&config, &vectors);
943
944 for v in &vectors {
946 let cluster = centroids.find_nearest(v);
947 assert!(cluster < centroids.num_clusters);
948 }
949 }
950
951 #[test]
952 fn scaled_l2_probes_keep_distinct_cache_identities() {
953 let centroids = CoarseCentroids {
954 num_clusters: 2,
955 dim: 2,
956 centroids: vec![1.0, 0.0, 10.0, 0.0],
957 version: 7,
958 soar_config: None,
959 routing_index: None,
960 };
961
962 let near = centroids.probe(&[1.0, 0.0], 1, IvfRoutingMode::Flat);
963 let scaled = centroids.probe(&[100.0, 0.0], 1, IvfRoutingMode::Flat);
964
965 assert_eq!(&*near.cluster_ids, &[0]);
966 assert_eq!(&*scaled.cluster_ids, &[1]);
967 assert_ne!(near.request_fingerprint, scaled.request_fingerprint);
968 }
969
970 #[test]
971 fn test_soar_assignment() {
972 let dim = 32;
973 let n = 100;
974 let num_clusters = 8;
975
976 let mut rng = rand::rngs::StdRng::seed_from_u64(456);
977 let vectors: Vec<Vec<f32>> = (0..n)
978 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
979 .collect();
980
981 let soar_config = SoarConfig {
982 num_secondary: 2,
983 selective: false,
984 spill_threshold: 0.0,
985 };
986 let config = CoarseConfig::new(dim, num_clusters).with_soar(soar_config);
987 let centroids = CoarseCentroids::train(&config, &vectors);
988
989 let assignment = centroids.assign(&vectors[0]);
991 assert!(assignment.primary_cluster < centroids.num_clusters);
992 assert_eq!(centroids.soar_config.as_ref().unwrap().num_secondary, 1);
993 assert_eq!(assignment.secondary_clusters.len(), 1);
994
995 for &sec in &assignment.secondary_clusters {
997 assert_ne!(sec, assignment.primary_cluster);
998 }
999 }
1000
1001 #[test]
1002 fn soar_loss_includes_distortion_and_normalized_projection() {
1003 let vector = [0.0, 0.0];
1004 let primary_residual = [2.0, 0.0];
1005 let secondary_centroid = [-3.0, -4.0];
1006
1007 let loss = soar_secondary_loss(&vector, &secondary_centroid, &primary_residual, 4.0, 25.0);
1010 assert!((loss - 34.0).abs() <= f32::EPSILON);
1011 }
1012
1013 #[test]
1014 fn soar_routing_keeps_an_oversampled_secondary_candidate_pool() {
1015 assert_eq!(soar_build_candidate_budget(1, 1_000), 128);
1016 assert_eq!(soar_build_candidate_budget(2, 1_000), 192);
1017 assert_eq!(soar_build_candidate_budget(8, 64), 64);
1018 }
1019
1020 #[test]
1021 fn two_level_build_assignment_checks_four_parents_for_primary_and_soar() {
1022 let leaves_per_parent = 512;
1023 let children: Vec<Vec<u32>> = (0..4)
1024 .map(|parent| {
1025 let first = parent * leaves_per_parent;
1026 (first..first + leaves_per_parent)
1027 .map(|leaf| leaf as u32)
1028 .collect()
1029 })
1030 .collect();
1031 let mut leaf_centroids = vec![1.0; 4 * leaves_per_parent];
1032 let best_leaf = 3 * leaves_per_parent;
1033 leaf_centroids[best_leaf] = 0.0;
1034 let centroids = CoarseCentroids {
1035 num_clusters: leaf_centroids.len() as u32,
1036 dim: 1,
1037 centroids: leaf_centroids,
1038 version: 1,
1039 soar_config: None,
1040 routing_index: Some(FloatCentroidRouter::TwoLevel {
1041 parent_centroids: vec![0.0, 10.0, 20.0, 30.0],
1042 topology: IvfRoutingTopology::from_children(&children),
1043 }),
1044 };
1045
1046 let query = [0.0];
1047 assert_eq!(
1048 &*centroids
1049 .probe(&query, 1, IvfRoutingMode::TwoLevel)
1050 .cluster_ids,
1051 &[0]
1052 );
1053 assert_eq!(
1054 centroids
1055 .assign_with_routing(&query, IvfRoutingMode::TwoLevel)
1056 .primary_cluster,
1057 best_leaf as u32
1058 );
1059 assert_eq!(
1060 centroids
1061 .assign_with_soar_and_routing(
1062 &query,
1063 &SoarConfig::full(),
1064 IvfRoutingMode::TwoLevel,
1065 )
1066 .primary_cluster,
1067 best_leaf as u32
1068 );
1069 }
1070
1071 #[test]
1072 fn selective_soar_calibrates_to_a_storage_budget() {
1073 let centroids = CoarseCentroids {
1074 num_clusters: 2,
1075 dim: 2,
1076 centroids: vec![0.0, 0.0, 10.0, 0.0],
1077 version: 1,
1078 soar_config: None,
1079 routing_index: None,
1080 };
1081 let vectors: Vec<Vec<f32>> = (0..100)
1082 .map(|index| vec![index as f32 / 100.0, 0.0])
1083 .collect();
1084 let flat = vectors.iter().flatten().copied().collect::<Vec<_>>();
1085 let threshold = centroids.calibrate_selective_spill_threshold(
1086 &flat,
1087 vectors.len(),
1088 IvfRoutingMode::Flat,
1089 0.30,
1090 );
1091 let spilled = vectors
1092 .iter()
1093 .filter(|vector| squared_l2(vector, centroids.get_centroid(0)).sqrt() >= threshold)
1094 .count();
1095 assert!((29..=31).contains(&spilled), "{spilled}");
1096 }
1097
1098 #[test]
1099 fn selective_soar_never_exceeds_budget_when_residuals_tie() {
1100 let centroids = CoarseCentroids {
1101 num_clusters: 2,
1102 dim: 2,
1103 centroids: vec![0.0, 0.0, 10.0, 0.0],
1104 version: 1,
1105 soar_config: None,
1106 routing_index: None,
1107 };
1108 let vectors = [1.0f32, 0.0].repeat(100);
1109 let threshold = centroids.calibrate_selective_spill_threshold(
1110 &vectors,
1111 100,
1112 IvfRoutingMode::Flat,
1113 0.30,
1114 );
1115 let config = SoarConfig::new().threshold(threshold);
1116 let spilled = vectors
1117 .chunks_exact(2)
1118 .filter(|vector| centroids.assign_with_soar(vector, &config).is_spilled())
1119 .count();
1120
1121 assert!(threshold > 1.0);
1122 assert!(spilled <= 30, "{spilled}");
1123 }
1124
1125 #[test]
1126 fn selective_soar_preserves_boundary_query_candidate_recall_with_bounded_postings() {
1127 const DIM: usize = 16;
1128 const CLUSTERS: usize = 16;
1129 const MEMBERS_PER_CLUSTER: usize = 128;
1130 const TOP_K: usize = 20;
1131 const TARGET_SPILL: f32 = 0.30;
1132
1133 fn normalize(values: &mut [f32]) {
1134 let norm = values.iter().map(|value| value * value).sum::<f32>().sqrt();
1135 for value in values {
1136 *value /= norm;
1137 }
1138 }
1139
1140 let mut rng = rand::rngs::StdRng::seed_from_u64(0x50a4_5eed);
1141 let source_centers: Vec<Vec<f32>> = (0..CLUSTERS)
1142 .map(|_| {
1143 let mut center: Vec<f32> = (0..DIM).map(|_| rng.random::<f32>() - 0.5).collect();
1144 normalize(&mut center);
1145 center
1146 })
1147 .collect();
1148 let corpus: Vec<Vec<f32>> = source_centers
1149 .iter()
1150 .flat_map(|center| {
1151 (0..MEMBERS_PER_CLUSTER)
1152 .map(|_| {
1153 let mut noise: Vec<f32> =
1154 (0..DIM).map(|_| rng.random::<f32>() - 0.5).collect();
1155 normalize(&mut noise);
1156 let mut vector: Vec<f32> = center
1157 .iter()
1158 .zip(noise)
1159 .map(|(&value, noise)| value + 0.75 * noise)
1160 .collect();
1161 normalize(&mut vector);
1162 vector
1163 })
1164 .collect::<Vec<_>>()
1165 })
1166 .collect();
1167
1168 let selective = CoarseCentroids::train(
1169 &CoarseConfig::new(DIM, CLUSTERS)
1170 .with_seed(0x1f4)
1171 .with_routing(IvfRoutingMode::Flat)
1172 .with_soar(SoarConfig::new().target_spill_fraction(TARGET_SPILL)),
1173 &corpus,
1174 );
1175 let mut primary_only = selective.clone();
1178 primary_only.soar_config = None;
1179
1180 let primary_assignments: Vec<MultiAssignment> = corpus
1181 .iter()
1182 .map(|vector| primary_only.assign(vector))
1183 .collect();
1184 let selective_assignments: Vec<MultiAssignment> = corpus
1185 .iter()
1186 .map(|vector| selective.assign(vector))
1187 .collect();
1188 for (primary, spilled) in primary_assignments.iter().zip(&selective_assignments) {
1189 assert_eq!(
1190 primary.primary_cluster, spilled.primary_cluster,
1191 "SOAR policy must not change the primary posting"
1192 );
1193 }
1194
1195 let spilled = selective_assignments
1196 .iter()
1197 .filter(|assignment| assignment.is_spilled())
1198 .count();
1199 let posting_factor = selective_assignments
1200 .iter()
1201 .map(MultiAssignment::num_assignments)
1202 .sum::<usize>() as f32
1203 / corpus.len() as f32;
1204 let spill_budget = (corpus.len() as f32 * TARGET_SPILL).round() as usize;
1205 assert!(
1206 spilled > 0,
1207 "the smoke corpus must exercise selective spilling"
1208 );
1209 assert!(
1210 spilled <= spill_budget,
1211 "{spilled} spilled vectors exceeded the calibrated budget of {spill_budget}"
1212 );
1213 assert!(
1214 posting_factor <= 1.0 + TARGET_SPILL + f32::EPSILON,
1215 "posting amplification {posting_factor:.4} exceeded the 1.30 calibration target"
1216 );
1217
1218 let queries: Vec<Vec<f32>> = (0..selective.num_clusters)
1222 .map(|left| {
1223 let left_centroid = selective.get_centroid(left);
1224 let right = (0..selective.num_clusters)
1225 .filter(|&candidate| candidate != left)
1226 .min_by(|&a, &b| {
1227 squared_l2(left_centroid, selective.get_centroid(a))
1228 .total_cmp(&squared_l2(left_centroid, selective.get_centroid(b)))
1229 .then_with(|| a.cmp(&b))
1230 })
1231 .unwrap();
1232 let mut query: Vec<f32> = left_centroid
1233 .iter()
1234 .zip(selective.get_centroid(right))
1235 .map(|(&a, &b)| 0.51 * a + 0.49 * b)
1236 .collect();
1237 normalize(&mut query);
1238 query
1239 })
1240 .collect();
1241
1242 let mut gained_queries = 0usize;
1243 for nprobe in [1, 2] {
1244 let mut primary_hits = 0usize;
1245 let mut selective_hits = 0usize;
1246 for query in &queries {
1247 let primary_plan = primary_only.probe(query, nprobe, IvfRoutingMode::Flat);
1248 let selective_plan = selective.probe(query, nprobe, IvfRoutingMode::Flat);
1249 assert_eq!(
1250 primary_plan.cluster_ids, selective_plan.cluster_ids,
1251 "SOAR must not alter query routing"
1252 );
1253
1254 let mut truth: Vec<(usize, f32)> = corpus
1255 .iter()
1256 .enumerate()
1257 .map(|(document, vector)| (document, squared_l2(query, vector)))
1258 .collect();
1259 truth.select_nth_unstable_by(TOP_K, |left, right| {
1260 left.1
1261 .total_cmp(&right.1)
1262 .then_with(|| left.0.cmp(&right.0))
1263 });
1264 truth.truncate(TOP_K);
1265
1266 let query_primary_hits = truth
1267 .iter()
1268 .filter(|&&(document, _)| {
1269 primary_assignments[document]
1270 .all_clusters()
1271 .any(|cluster| primary_plan.cluster_ids.contains(&cluster))
1272 })
1273 .count();
1274 let query_selective_hits = truth
1275 .iter()
1276 .filter(|&&(document, _)| {
1277 selective_assignments[document]
1278 .all_clusters()
1279 .any(|cluster| selective_plan.cluster_ids.contains(&cluster))
1280 })
1281 .count();
1282 primary_hits += query_primary_hits;
1283 selective_hits += query_selective_hits;
1284 gained_queries += usize::from(query_selective_hits > query_primary_hits);
1285 }
1286
1287 let denominator = (queries.len() * TOP_K) as f32;
1288 let primary_recall = primary_hits as f32 / denominator;
1289 let selective_recall = selective_hits as f32 / denominator;
1290 assert!(
1291 selective_recall + 0.005 >= primary_recall,
1292 "selective SOAR candidate recall regressed at nprobe={nprobe}: \
1293 {selective_recall:.4} vs {primary_recall:.4}"
1294 );
1295 if nprobe == 1 {
1296 assert!(
1297 selective_recall >= primary_recall + 0.01,
1298 "selective SOAR must recover boundary candidates at nprobe=1: \
1299 {selective_recall:.4} vs {primary_recall:.4}"
1300 );
1301 }
1302 }
1303 assert!(
1304 gained_queries > 0,
1305 "boundary-query smoke did not exercise a selective SOAR recall gain"
1306 );
1307 }
1308
1309 #[test]
1310 fn soar_does_not_choose_an_arbitrarily_distant_orthogonal_centroid() {
1311 let centroids = CoarseCentroids {
1312 num_clusters: 3,
1313 dim: 2,
1314 centroids: vec![-1.0, 0.0, 0.0, -100.0, 1.1, 0.0],
1319 version: 1,
1320 soar_config: None,
1321 routing_index: None,
1322 };
1323
1324 let assignment = centroids.assign_with_soar(&[0.0, 0.0], &SoarConfig::full());
1325 assert_eq!(assignment.primary_cluster, 0);
1326 assert_eq!(assignment.secondary_clusters, vec![2]);
1327 }
1328
1329 #[test]
1330 fn selective_soar_rejects_a_far_secondary_residual() {
1331 let centroids = CoarseCentroids {
1332 num_clusters: 2,
1333 dim: 2,
1334 centroids: vec![0.0, 0.0, 10.0, 0.0],
1335 version: 1,
1336 soar_config: None,
1337 routing_index: None,
1338 };
1339 let config = SoarConfig::new().threshold(0.0);
1340
1341 let assignment = centroids.assign_with_soar(&[1.0, 0.0], &config);
1344 assert_eq!(assignment.primary_cluster, 0);
1345 assert!(assignment.secondary_clusters.is_empty());
1346 }
1347
1348 #[test]
1349 fn selective_soar_keeps_a_comparable_boundary_secondary() {
1350 let centroids = CoarseCentroids {
1351 num_clusters: 2,
1352 dim: 2,
1353 centroids: vec![0.0, 0.0, 2.0, 0.0],
1354 version: 1,
1355 soar_config: None,
1356 routing_index: None,
1357 };
1358 let config = SoarConfig::new().threshold(0.0);
1359
1360 let assignment = centroids.assign_with_soar(&[0.9, 0.1], &config);
1363 assert_eq!(assignment.primary_cluster, 0);
1364 assert_eq!(assignment.secondary_clusters, vec![1]);
1365 }
1366
1367 #[test]
1368 fn soar_secondary_ties_are_ordered_by_cluster_id_and_capped_to_one() {
1369 let centroids = CoarseCentroids {
1370 num_clusters: 3,
1371 dim: 2,
1372 centroids: vec![0.0, 0.0, 1.0, 0.0, -1.0, 0.0],
1373 version: 1,
1374 soar_config: None,
1375 routing_index: None,
1376 };
1377 let config = SoarConfig {
1378 num_secondary: 2,
1379 selective: false,
1380 spill_threshold: 0.0,
1381 };
1382
1383 let assignment = centroids.assign_with_soar(&[0.0, 0.0], &config);
1384 assert_eq!(assignment.primary_cluster, 0);
1385 assert_eq!(assignment.secondary_clusters, vec![1]);
1386 }
1387
1388 #[test]
1389 fn test_serialization() {
1390 let dim = 16;
1391 let n = 50;
1392 let num_clusters = 4;
1393
1394 let mut rng = rand::rngs::StdRng::seed_from_u64(789);
1395 let vectors: Vec<Vec<f32>> = (0..n)
1396 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
1397 .collect();
1398
1399 let config = CoarseConfig::new(dim, num_clusters);
1400 let centroids = CoarseCentroids::train(&config, &vectors);
1401
1402 let bytes = bincode::serde::encode_to_vec(¢roids, bincode::config::standard()).unwrap();
1404 let (loaded, consumed): (CoarseCentroids, usize) =
1405 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
1406 assert_eq!(consumed, bytes.len());
1407
1408 assert_eq!(loaded.num_clusters, centroids.num_clusters);
1409 assert_eq!(loaded.dim, centroids.dim);
1410 assert_eq!(loaded.centroids.len(), centroids.centroids.len());
1411 }
1412
1413 #[test]
1414 fn persisted_hnsw_and_two_level_routers_are_valid() {
1415 let dim = 4;
1416 let mut rng = rand::rngs::StdRng::seed_from_u64(991);
1417 let vectors: Vec<Vec<f32>> = (0..256)
1418 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
1419 .collect();
1420
1421 for routing in [IvfRoutingMode::Hnsw, IvfRoutingMode::TwoLevel] {
1422 let trained =
1423 CoarseCentroids::train(&CoarseConfig::new(dim, 16).with_routing(routing), &vectors);
1424 trained.validate_routing(routing).unwrap();
1425 let plan = trained.probe(&vectors[0], 8, routing);
1426 assert_eq!(plan.cluster_ids.len(), 8);
1427 assert!(
1428 plan.cluster_ids
1429 .iter()
1430 .all(|&cluster| cluster < trained.num_clusters)
1431 );
1432
1433 let bytes =
1434 bincode::serde::encode_to_vec(&trained, bincode::config::standard()).unwrap();
1435 let (loaded, consumed): (CoarseCentroids, usize) =
1436 bincode::serde::decode_from_slice(&bytes, bincode::config::standard()).unwrap();
1437 assert_eq!(consumed, bytes.len());
1438 loaded.validate_routing(routing).unwrap();
1439 }
1440 }
1441}