1use half::f16;
22
23use crate::{
24 bitpack::unpack_indices, codec::FibCodeV1, profile::FibQuantProfileV1, FibQuantError,
25 FibQuantizer, Result,
26};
27
28pub struct GramTable {
38 values: Vec<f32>,
40 n: usize,
41}
42
43impl GramTable {
44 pub fn build(codewords: &[f32], n: usize, k: usize) -> Result<Self> {
49 if codewords.len() != n * k {
50 return Err(FibQuantError::CorruptPayload(format!(
51 "codewords has {} values, expected {} (n={} k={})",
52 codewords.len(),
53 n * k,
54 n,
55 k
56 )));
57 }
58 let mut values = vec![0.0f32; n * n];
59 for i in 0..n {
60 let mut dot_ii = 0.0f32;
62 for d in 0..k {
63 let vi = codewords[i * k + d];
64 dot_ii += vi * vi;
65 }
66 values[i * n + i] = dot_ii;
67 for j in (i + 1)..n {
69 let mut dot = 0.0f32;
70 for d in 0..k {
71 dot += codewords[i * k + d] * codewords[j * k + d];
72 }
73 values[i * n + j] = dot;
74 values[j * n + i] = dot;
75 }
76 }
77 Ok(Self { values, n })
78 }
79
80 #[inline]
82 pub fn get(&self, i: usize, j: usize) -> f32 {
83 debug_assert!(i < self.n && j < self.n);
84 self.values[i * self.n + j]
85 }
86
87 pub fn n(&self) -> usize {
89 self.n
90 }
91
92 pub fn values(&self) -> &[f32] {
94 &self.values
95 }
96}
97
98pub struct FibScorer {
107 quantizer: FibQuantizer,
108 gram: GramTable,
109}
110
111#[derive(Debug, Clone)]
113pub struct ScoredItem {
114 pub idx: usize,
116 pub score: f32,
118}
119
120#[derive(Debug, Clone)]
130pub struct FibPreparedQuery {
131 pub rotated_query: Vec<f32>,
133 pub query_norm: f64,
135 pub query_indices: Vec<u32>,
137}
138
139impl FibScorer {
140 pub fn new(quantizer: FibQuantizer) -> Result<Self> {
142 let n = quantizer.profile().codebook_size as usize;
143 let k = quantizer.profile().block_dim as usize;
144 let gram = GramTable::build(&quantizer.codebook().codewords, n, k)?;
145 Ok(Self { quantizer, gram })
146 }
147
148 pub fn quantizer(&self) -> &FibQuantizer {
150 &self.quantizer
151 }
152
153 pub fn gram_table(&self) -> &GramTable {
155 &self.gram
156 }
157
158 pub fn inner_product_estimate(&self, query: &[f32], code: &FibCodeV1) -> Result<f32> {
169 let d = self.quantizer.profile().ambient_dim as usize;
170 let k = self.quantizer.profile().block_dim as usize;
171 if query.len() != d {
172 return Err(FibQuantError::CorruptPayload(format!(
173 "query dimension {}, expected {}",
174 query.len(),
175 d
176 )));
177 }
178 if query.iter().any(|v| !v.is_finite()) {
180 return Err(FibQuantError::NonFiniteInput(0));
181 }
182 let query_norm: f64 = query
183 .iter()
184 .map(|v| (*v as f64) * (*v as f64))
185 .sum::<f64>()
186 .sqrt();
187 if query_norm == 0.0 {
188 return Ok(0.0);
189 }
190 let normalized: Vec<f64> = query.iter().map(|v| f64::from(*v) / query_norm).collect();
192 let rotated = self.quantizer.profile();
193 let _ = rotated; let rotated_query = self.quantizer_codebook_rotation_apply(&normalized)?;
195
196 let stored_norm = decode_stored_norm(code, self.quantizer.profile())?;
198
199 let block_count = self.quantizer.profile().block_count() as usize;
201 let stored_indices = unpack_indices(
202 &code.indices,
203 block_count,
204 self.quantizer.profile().wire_index_bits,
205 )?;
206
207 let rotated_query_f32: Vec<f32> = rotated_query.iter().map(|&v| v as f32).collect();
209 let codewords = &self.quantizer.codebook().codewords;
210 let n = self.quantizer.profile().codebook_size as usize;
211
212 let mut total = 0.0f32;
213 for (block_idx, stored_idx) in stored_indices.iter().enumerate() {
214 let stored_idx = *stored_idx as usize;
215 if stored_idx >= n {
216 return Err(FibQuantError::IndexOutOfRange {
217 index: stored_idx as u32,
218 codebook_size: n as u32,
219 });
220 }
221 let query_block = &rotated_query_f32[block_idx * k..(block_idx + 1) * k];
222 let query_idx = gpu_backend::nearest_codeword_f32(query_block, codewords, k) as usize;
223 total += self.gram.get(query_idx, stored_idx);
225 }
226
227 Ok(total * (query_norm as f32) * (stored_norm as f32))
229 }
230
231 pub fn score_batch(&self, query: &[f32], codes: &[FibCodeV1]) -> Result<Vec<ScoredItem>> {
235 let mut results = Vec::with_capacity(codes.len());
236 for (idx, code) in codes.iter().enumerate() {
237 let score = self.inner_product_estimate(query, code)?;
238 results.push(ScoredItem { idx, score });
239 }
240 results.sort_by(|a, b| {
241 b.score
242 .partial_cmp(&a.score)
243 .unwrap_or(std::cmp::Ordering::Equal)
244 });
245 Ok(results)
246 }
247
248 pub fn search(
254 &self,
255 query: &[f32],
256 codes: &[FibCodeV1],
257 top_k: usize,
258 oversample: usize,
259 ) -> Result<Vec<ScoredItem>> {
260 let limit = top_k.saturating_mul(oversample.max(1)).min(codes.len());
261 let scored = self.score_batch(query, codes)?;
262 Ok(scored.into_iter().take(limit).collect())
263 }
264
265 pub fn prepare_query(&self, query: &[f32]) -> Result<FibPreparedQuery> {
278 let d = self.quantizer.profile().ambient_dim as usize;
279 let k = self.quantizer.profile().block_dim as usize;
280 if query.len() != d {
281 return Err(FibQuantError::CorruptPayload(format!(
282 "query dimension {}, expected {}",
283 query.len(),
284 d
285 )));
286 }
287 if query.iter().any(|v| !v.is_finite()) {
288 return Err(FibQuantError::NonFiniteInput(0));
289 }
290
291 let query_norm: f64 = query
292 .iter()
293 .map(|v| (*v as f64) * (*v as f64))
294 .sum::<f64>()
295 .sqrt();
296 if query_norm == 0.0 {
297 let block_count = self.quantizer.profile().block_count() as usize;
299 return Ok(FibPreparedQuery {
300 rotated_query: vec![0.0f32; d],
301 query_norm: 0.0,
302 query_indices: vec![0u32; block_count],
303 });
304 }
305
306 let normalized: Vec<f64> = query.iter().map(|v| f64::from(*v) / query_norm).collect();
308 let rotated_query = self.quantizer_codebook_rotation_apply(&normalized)?;
309 let rotated_query_f32: Vec<f32> = rotated_query.iter().map(|&v| v as f32).collect();
310
311 let block_count = self.quantizer.profile().block_count() as usize;
313 let codewords = &self.quantizer.codebook().codewords;
314 let mut query_indices = Vec::with_capacity(block_count);
315 for block_idx in 0..block_count {
316 let block = &rotated_query_f32[block_idx * k..(block_idx + 1) * k];
317 let idx = gpu_backend::nearest_codeword_f32(block, codewords, k) as u32;
318 query_indices.push(idx);
319 }
320
321 Ok(FibPreparedQuery {
322 rotated_query: rotated_query_f32,
323 query_norm,
324 query_indices,
325 })
326 }
327
328 pub fn score_prepared(&self, prepared: &FibPreparedQuery, code: &FibCodeV1) -> Result<f32> {
336 if prepared.query_norm == 0.0 {
337 return Ok(0.0);
338 }
339
340 let block_count = self.quantizer.profile().block_count() as usize;
341 let stored_indices = unpack_indices(
342 &code.indices,
343 block_count,
344 self.quantizer.profile().wire_index_bits,
345 )?;
346
347 let stored_norm = decode_stored_norm(code, self.quantizer.profile())?;
348 let n = self.quantizer.profile().codebook_size as usize;
349
350 let mut total = 0.0f32;
351 for (block_idx, stored_idx) in stored_indices.iter().enumerate() {
352 let stored_idx = *stored_idx as usize;
353 if stored_idx >= n {
354 return Err(FibQuantError::IndexOutOfRange {
355 index: stored_idx as u32,
356 codebook_size: n as u32,
357 });
358 }
359 let query_idx = prepared.query_indices[block_idx] as usize;
360 total += self.gram.get(query_idx, stored_idx);
361 }
362
363 Ok(total * (prepared.query_norm as f32) * (stored_norm as f32))
364 }
365
366 pub fn score_batch_prepared(
372 &self,
373 prepared: &FibPreparedQuery,
374 codes: &[FibCodeV1],
375 ) -> Result<Vec<ScoredItem>> {
376 let mut results = Vec::with_capacity(codes.len());
377 for (idx, code) in codes.iter().enumerate() {
378 let score = self.score_prepared(prepared, code)?;
379 results.push(ScoredItem { idx, score });
380 }
381 results.sort_by(|a, b| {
382 b.score
383 .partial_cmp(&a.score)
384 .unwrap_or(std::cmp::Ordering::Equal)
385 });
386 Ok(results)
387 }
388
389 pub fn search_prepared(
393 &self,
394 prepared: &FibPreparedQuery,
395 codes: &[FibCodeV1],
396 top_k: usize,
397 oversample: usize,
398 ) -> Result<Vec<ScoredItem>> {
399 let limit = top_k.saturating_mul(oversample.max(1)).min(codes.len());
400 let scored = self.score_batch_prepared(prepared, codes)?;
401 Ok(scored.into_iter().take(limit).collect())
402 }
403
404 fn quantizer_codebook_rotation_apply(&self, x: &[f64]) -> Result<Vec<f64>> {
406 self.quantizer.rotation().apply(x)
408 }
409
410 pub fn l2_distance_sq_estimate(&self, query: &[f32], code: &FibCodeV1) -> Result<f32> {
417 let ip = self.inner_product_estimate(query, code)?;
418 let q_norm_sq: f32 = query.iter().map(|v| v * v).sum();
419 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
420 let v_norm_sq = stored_norm * stored_norm;
421 Ok((q_norm_sq + v_norm_sq - 2.0 * ip).max(0.0))
422 }
423
424 pub fn cosine_estimate(&self, query: &[f32], code: &FibCodeV1) -> Result<f32> {
427 let ip = self.inner_product_estimate(query, code)?;
428 let q_norm: f32 = query.iter().map(|v| v * v).sum::<f32>().sqrt();
429 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
430 if q_norm == 0.0 || stored_norm == 0.0 {
431 return Ok(0.0);
432 }
433 Ok(ip / (q_norm * stored_norm))
434 }
435
436 pub fn l2_distance_sq_prepared(
438 &self,
439 prepared: &FibPreparedQuery,
440 code: &FibCodeV1,
441 ) -> Result<f32> {
442 let ip = self.score_prepared(prepared, code)?;
443 let q_norm_sq = (prepared.query_norm * prepared.query_norm) as f32;
444 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
445 let v_norm_sq = stored_norm * stored_norm;
446 Ok((q_norm_sq + v_norm_sq - 2.0 * ip).max(0.0))
447 }
448
449 pub fn cosine_prepared(&self, prepared: &FibPreparedQuery, code: &FibCodeV1) -> Result<f32> {
451 let ip = self.score_prepared(prepared, code)?;
452 let q_norm = prepared.query_norm as f32;
453 let stored_norm = decode_stored_norm(code, self.quantizer.profile())? as f32;
454 if q_norm == 0.0 || stored_norm == 0.0 {
455 return Ok(0.0);
456 }
457 Ok(ip / (q_norm * stored_norm))
458 }
459}
460
461pub fn decode_stored_norm(code: &FibCodeV1, _profile: &FibQuantProfileV1) -> Result<f64> {
462 use crate::profile::NormFormat;
466 match code.norm_format {
467 NormFormat::Fp16Paper => {
468 let bytes: [u8; 2] =
469 code.norm_payload.as_slice().try_into().map_err(|_| {
470 FibQuantError::CorruptPayload("fp16 norm payload length".into())
471 })?;
472 let value = f16::from_le_bytes(bytes).to_f32() as f64;
473 if value.is_finite() && value > 0.0 {
474 Ok(value)
475 } else {
476 Err(FibQuantError::CorruptPayload("invalid fp16 norm".into()))
477 }
478 }
479 NormFormat::F32Reference => {
480 let bytes: [u8; 4] = code
481 .norm_payload
482 .as_slice()
483 .try_into()
484 .map_err(|_| FibQuantError::CorruptPayload("f32 norm payload length".into()))?;
485 let value = f32::from_le_bytes(bytes) as f64;
486 if value.is_finite() && value > 0.0 {
487 Ok(value)
488 } else {
489 Err(FibQuantError::CorruptPayload("invalid f32 norm".into()))
490 }
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 fn build_test_scorer() -> Result<FibScorer> {
500 let mut profile = FibQuantProfileV1::paper_default(8, 2, 8, 7)?;
501 profile.training_samples = 128;
502 profile.lloyd_restarts = 1;
503 profile.lloyd_iterations = 2;
504 let quantizer = FibQuantizer::new(profile)?;
505 FibScorer::new(quantizer)
506 }
507
508 #[test]
509 fn gram_table_diagonal_matches_codeword_norms() -> Result<()> {
510 let scorer = build_test_scorer()?;
511 let k = scorer.quantizer.profile().block_dim as usize;
512 let n = scorer.quantizer.profile().codebook_size as usize;
513 let codewords = &scorer.quantizer.codebook().codewords;
514 for i in 0..n {
515 let mut norm_sq = 0.0f32;
516 for d in 0..k {
517 let v = codewords[i * k + d];
518 norm_sq += v * v;
519 }
520 let gram_diag = scorer.gram.get(i, i);
521 assert!(
522 (norm_sq - gram_diag).abs() < 1e-5,
523 "gram diagonal mismatch at {}: ||cw||^2 = {}, gram = {}",
524 i,
525 norm_sq,
526 gram_diag
527 );
528 }
529 Ok(())
530 }
531
532 #[test]
533 fn gram_table_symmetric() -> Result<()> {
534 let scorer = build_test_scorer()?;
535 let n = scorer.gram.n();
536 for i in 0..n {
537 for j in 0..n {
538 assert!(
539 (scorer.gram.get(i, j) - scorer.gram.get(j, i)).abs() < 1e-6,
540 "gram not symmetric at ({}, {})",
541 i,
542 j
543 );
544 }
545 }
546 Ok(())
547 }
548
549 #[test]
550 fn inner_product_estimate_positive_for_self() -> Result<()> {
551 let scorer = build_test_scorer()?;
552 let d = scorer.quantizer.profile().ambient_dim as usize;
553 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
554 assert_eq!(input.len(), d);
555 let code = scorer.quantizer.encode(&input)?;
556 let est = scorer.inner_product_estimate(&input, &code)?;
557 assert!(
559 est > 0.0,
560 "inner product estimate of self should be positive, got {}",
561 est
562 );
563 let true_ip: f32 = input.iter().map(|v| v * v).sum();
565 let ratio = est / true_ip;
566 assert!(
567 ratio > 0.5 && ratio < 2.0,
568 "estimate {} vs true {} — ratio {} out of [0.5, 2.0]",
569 est,
570 true_ip,
571 ratio
572 );
573 Ok(())
574 }
575
576 #[test]
577 fn search_returns_sorted_descending() -> Result<()> {
578 let scorer = build_test_scorer()?;
579 let d = scorer.quantizer.profile().ambient_dim as usize;
580 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
581 assert_eq!(query.len(), d);
582
583 let vectors: Vec<Vec<f32>> = (0..16)
585 .map(|seed| {
586 (0..d)
587 .map(|i| (seed as f32 * 0.1 + i as f32 * 0.05 - 0.3).sin())
588 .collect()
589 })
590 .collect();
591 let codes: Vec<FibCodeV1> = vectors
592 .iter()
593 .map(|v| scorer.quantizer.encode(v).unwrap())
594 .collect();
595
596 let results = scorer.search(&query, &codes, 5, 2)?;
597 assert_eq!(results.len(), 10);
599 for w in results.windows(2) {
600 assert!(
601 w[0].score >= w[1].score,
602 "results not sorted: {} before {}",
603 w[0].score,
604 w[1].score
605 );
606 }
607 Ok(())
608 }
609
610 #[test]
611 fn score_batch_handles_empty() -> Result<()> {
612 let scorer = build_test_scorer()?;
613 let d = scorer.quantizer.profile().ambient_dim as usize;
614 let query = vec![0.0f32; d];
615 let results = scorer.score_batch(&query, &[])?;
616 assert!(results.is_empty());
617 Ok(())
618 }
619
620 #[test]
621 fn prepared_query_matches_inner_product_estimate() -> Result<()> {
622 let scorer = build_test_scorer()?;
623 let d = scorer.quantizer.profile().ambient_dim as usize;
624 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
625 assert_eq!(query.len(), d);
626
627 let vectors: Vec<Vec<f32>> = (0..16)
629 .map(|seed| {
630 (0..d)
631 .map(|i| (seed as f32 * 0.1 + i as f32 * 0.05 - 0.3).sin())
632 .collect()
633 })
634 .collect();
635 let codes: Vec<FibCodeV1> = vectors
636 .iter()
637 .map(|v| scorer.quantizer.encode(v).unwrap())
638 .collect();
639
640 let prepared = scorer.prepare_query(&query)?;
641 for (i, code) in codes.iter().enumerate() {
642 let direct = scorer.inner_product_estimate(&query, code)?;
643 let prepared_score = scorer.score_prepared(&prepared, code)?;
644 assert!(
645 (direct - prepared_score).abs() < 1e-4,
646 "mismatch at code {}: direct={}, prepared={}",
647 i,
648 direct,
649 prepared_score
650 );
651 }
652 Ok(())
653 }
654
655 #[test]
656 fn prepared_batch_matches_score_batch() -> Result<()> {
657 let scorer = build_test_scorer()?;
658 let d = scorer.quantizer.profile().ambient_dim as usize;
659 let query: Vec<f32> = vec![0.3, 0.7, -0.2, 0.9, -0.5, 0.1, -0.8, 0.4];
660 assert_eq!(query.len(), d);
661
662 let vectors: Vec<Vec<f32>> = (0..24)
663 .map(|seed| {
664 (0..d)
665 .map(|i| ((seed as f32 + i as f32) * 0.13).cos())
666 .collect()
667 })
668 .collect();
669 let codes: Vec<FibCodeV1> = vectors
670 .iter()
671 .map(|v| scorer.quantizer.encode(v).unwrap())
672 .collect();
673
674 let batch = scorer.score_batch(&query, &codes)?;
675 let prepared = scorer.prepare_query(&query)?;
676 let batch_prepared = scorer.score_batch_prepared(&prepared, &codes)?;
677
678 assert_eq!(batch.len(), batch_prepared.len());
679 for (a, b) in batch.iter().zip(batch_prepared.iter()) {
680 assert_eq!(a.idx, b.idx);
681 assert!(
682 (a.score - b.score).abs() < 1e-4,
683 "score mismatch at idx {}: batch={}, prepared={}",
684 a.idx,
685 a.score,
686 b.score
687 );
688 }
689 Ok(())
690 }
691
692 #[test]
693 fn prepared_search_matches_search() -> Result<()> {
694 let scorer = build_test_scorer()?;
695 let d = scorer.quantizer.profile().ambient_dim as usize;
696 let query: Vec<f32> = vec![0.6, -0.1, 0.3, -0.7, 0.8, 0.2, -0.4, 0.5];
697 assert_eq!(query.len(), d);
698
699 let vectors: Vec<Vec<f32>> = (0..32)
700 .map(|seed| {
701 (0..d)
702 .map(|i| (seed as f32 * 0.17 + i as f32 * 0.03).sin())
703 .collect()
704 })
705 .collect();
706 let codes: Vec<FibCodeV1> = vectors
707 .iter()
708 .map(|v| scorer.quantizer.encode(v).unwrap())
709 .collect();
710
711 let direct = scorer.search(&query, &codes, 5, 2)?;
712 let prepared = scorer.prepare_query(&query)?;
713 let prepared_results = scorer.search_prepared(&prepared, &codes, 5, 2)?;
714
715 assert_eq!(direct.len(), prepared_results.len());
716 for (a, b) in direct.iter().zip(prepared_results.iter()) {
717 assert_eq!(a.idx, b.idx);
718 assert!(
719 (a.score - b.score).abs() < 1e-4,
720 "search mismatch at idx {}: direct={}, prepared={}",
721 a.idx,
722 a.score,
723 b.score
724 );
725 }
726 Ok(())
727 }
728
729 #[test]
730 fn prepared_query_zero_norm() -> Result<()> {
731 let scorer = build_test_scorer()?;
732 let d = scorer.quantizer.profile().ambient_dim as usize;
733 let query = vec![0.0f32; d];
734 let prepared = scorer.prepare_query(&query)?;
735 assert_eq!(prepared.query_norm, 0.0);
736
737 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
738 let code = scorer.quantizer.encode(&input)?;
739 let score = scorer.score_prepared(&prepared, &code)?;
740 assert!(score.abs() < 1e-6, "zero query should give zero score");
741 Ok(())
742 }
743
744 #[test]
745 fn l2_distance_is_non_negative() -> Result<()> {
746 let scorer = build_test_scorer()?;
747 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
748 let code = scorer.quantizer.encode(&input)?;
749 let dist = scorer.l2_distance_sq_estimate(&input, &code)?;
750 assert!(
751 dist >= 0.0,
752 "L2 distance squared should be non-negative, got {}",
753 dist
754 );
755 Ok(())
756 }
757
758 #[test]
759 fn cosine_estimate_in_valid_range() -> Result<()> {
760 let scorer = build_test_scorer()?;
761 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
762 let code = scorer.quantizer.encode(&input)?;
763 let cos = scorer.cosine_estimate(&input, &code)?;
764 assert!(
765 (-1.5..=1.5).contains(&cos),
766 "cosine should be in [-1.5, 1.5], got {}",
767 cos
768 );
769 Ok(())
770 }
771
772 #[test]
773 fn cosine_prepared_matches_cosine_estimate() -> Result<()> {
774 let scorer = build_test_scorer()?;
775 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
776 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
777 let code = scorer.quantizer.encode(&input)?;
778 let cos_direct = scorer.cosine_estimate(&query, &code)?;
779 let prepared = scorer.prepare_query(&query)?;
780 let cos_prepared = scorer.cosine_prepared(&prepared, &code)?;
781 assert!(
782 (cos_direct - cos_prepared).abs() < 1e-5,
783 "prepared cosine {} should match direct {}",
784 cos_prepared,
785 cos_direct
786 );
787 Ok(())
788 }
789
790 #[test]
791 fn l2_prepared_matches_l2_estimate() -> Result<()> {
792 let scorer = build_test_scorer()?;
793 let query: Vec<f32> = vec![0.5, -0.3, 0.8, -0.1, 0.2, -0.4, 0.7, -0.6];
794 let input: Vec<f32> = vec![0.25, -0.5, 0.75, 1.0, -1.25, 0.5, 0.125, -0.875];
795 let code = scorer.quantizer.encode(&input)?;
796 let dist_direct = scorer.l2_distance_sq_estimate(&query, &code)?;
797 let prepared = scorer.prepare_query(&query)?;
798 let dist_prepared = scorer.l2_distance_sq_prepared(&prepared, &code)?;
799 assert!(
800 (dist_direct - dist_prepared).abs() < 1e-5,
801 "prepared L2 {} should match direct {}",
802 dist_prepared,
803 dist_direct
804 );
805 Ok(())
806 }
807}