1use std::num::NonZeroUsize;
7
8use diskann_utils::{ReborrowMut, views::MatrixView};
9use diskann_vector::{
10 MathematicalValue, Norm, PureDistanceFunction, distance::InnerProduct, norm::FastL2Norm,
11};
12#[cfg(feature = "flatbuffers")]
13use flatbuffers::{FlatBufferBuilder, WIPOffset};
14use rand::{Rng, RngCore};
15use thiserror::Error;
16
17use super::{
18 CompensatedCosine, CompensatedIP, CompensatedSquaredL2, DataMeta, DataMetaError, DataMut,
19 FullQueryMeta, FullQueryMut, QueryMeta, QueryMut, SupportedMetric,
20};
21use crate::{
22 AsFunctor, CompressIntoWith,
23 algorithms::{
24 heap::SliceHeap,
25 transforms::{NewTransformError, Transform, TransformFailed, TransformKind},
26 },
27 alloc::{Allocator, AllocatorError, GlobalAllocator, Poly, ScopedAllocator, TryClone},
28 bits::{PermutationStrategy, Representation, Unsigned},
29 num::Positive,
30 utils::{CannotBeEmpty, compute_means_and_average_norm, compute_normalized_means},
31};
32#[cfg(feature = "flatbuffers")]
33use crate::{
34 algorithms::transforms::TransformError, flatbuffers::spherical, spherical::InvalidMetric,
35};
36
37#[derive(Debug)]
42#[cfg_attr(test, derive(PartialEq))]
43pub struct SphericalQuantizer<A = GlobalAllocator>
44where
45 A: Allocator,
46{
47 shift: Poly<[f32], A>,
49
50 transform: Transform<A>,
58
59 metric: SupportedMetric,
61
62 mean_norm: Positive<f32>,
71
72 pre_scale: Positive<f32>,
78}
79
80impl<A> TryClone for SphericalQuantizer<A>
81where
82 A: Allocator,
83{
84 fn try_clone(&self) -> Result<Self, AllocatorError> {
85 SphericalQuantizer::try_clone(self)
86 }
87}
88
89#[derive(Debug, Clone, Copy, Error)]
90#[non_exhaustive]
91pub enum TrainError {
92 #[error("data dim cannot be zero")]
93 DimCannotBeZero,
94 #[error("data cannot be empty")]
95 DataCannotBeEmpty,
96 #[error("pre-scale must be positive")]
97 PrescaleNotPositive,
98 #[error("norm must be positive")]
99 NormNotPositive,
100 #[error("computed norm contains infinity or NaN")]
101 NormNotFinite,
102 #[error("reciprocal norm contains infinity or NaN")]
103 ReciprocalNormNotFinite,
104 #[error(transparent)]
105 AllocatorError(#[from] AllocatorError),
106}
107
108impl<A> SphericalQuantizer<A>
109where
110 A: Allocator,
111{
112 pub fn input_dim(&self) -> usize {
114 self.shift.len()
115 }
116
117 pub fn output_dim(&self) -> usize {
122 self.transform.output_dim()
123 }
124
125 pub fn shift(&self) -> &[f32] {
132 &self.shift
133 }
134
135 pub fn mean_norm(&self) -> Positive<f32> {
137 self.mean_norm
138 }
139
140 pub fn pre_scale(&self) -> Positive<f32> {
145 self.pre_scale
146 }
147
148 pub fn allocator(&self) -> &A {
150 self.shift.allocator()
151 }
152
153 pub fn try_clone(&self) -> Result<Self, AllocatorError> {
155 Ok(Self {
156 shift: self.shift.try_clone()?,
157 transform: self.transform.try_clone()?,
158 metric: self.metric,
159 mean_norm: self.mean_norm,
160 pre_scale: self.pre_scale,
161 })
162 }
163
164 pub fn generate(
166 mut centroid: Poly<[f32], A>,
167 mean_norm: f32,
168 transform: TransformKind,
169 metric: SupportedMetric,
170 pre_scale: Option<f32>,
171 rng: &mut dyn RngCore,
172 allocator: A,
173 ) -> Result<Self, TrainError> {
174 let pre_scale = match pre_scale {
175 Some(v) => Positive::new(v).map_err(|_| TrainError::PrescaleNotPositive)?,
176 None => crate::num::POSITIVE_ONE_F32,
177 };
178
179 let dim = match NonZeroUsize::new(centroid.len()) {
180 Some(dim) => dim,
181 None => {
182 return Err(TrainError::DimCannotBeZero);
183 }
184 };
185
186 let mean_norm = Positive::new(mean_norm).map_err(|_| TrainError::NormNotPositive)?;
187
188 let transform = match Transform::new(transform, dim, Some(rng), allocator.clone()) {
190 Ok(v) => v,
191 Err(NewTransformError::RngMissing(_)) => unreachable!("An Rng was provided"),
192 Err(NewTransformError::AllocatorError(err)) => {
193 return Err(TrainError::AllocatorError(err));
194 }
195 };
196
197 centroid
199 .iter_mut()
200 .for_each(|v| *v *= pre_scale.into_inner());
201
202 Ok(SphericalQuantizer {
203 shift: centroid,
204 transform,
205 metric,
206 mean_norm,
207 pre_scale,
208 })
209 }
210
211 pub fn metric(&self) -> SupportedMetric {
213 self.metric
214 }
215
216 pub fn train<T, R>(
231 data: MatrixView<T>,
232 transform: TransformKind,
233 metric: SupportedMetric,
234 pre_scale: PreScale,
235 rng: &mut R,
236 allocator: A,
237 ) -> Result<Self, TrainError>
238 where
239 T: Copy + Into<f64> + Into<f32>,
240 R: Rng,
241 {
242 #[inline(never)]
245 fn train<T, A>(
246 data: MatrixView<T>,
247 transform: TransformKind,
248 metric: SupportedMetric,
249 pre_scale: PreScale,
250 rng: &mut dyn RngCore,
251 allocator: A,
252 ) -> Result<SphericalQuantizer<A>, TrainError>
253 where
254 T: Copy + Into<f64> + Into<f32>,
255 A: Allocator,
256 {
257 if data.ncols() == 0 {
260 return Err(TrainError::DimCannotBeZero);
261 }
262
263 let (centroid, mean_norm) = match metric {
264 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => {
265 compute_means_and_average_norm(data)
266 }
267 SupportedMetric::Cosine => (
268 compute_normalized_means(data)
269 .map_err(|_: CannotBeEmpty| TrainError::DataCannotBeEmpty)?,
270 1.0,
271 ),
272 };
273
274 let mean_norm = mean_norm as f32;
275
276 if mean_norm <= 0.0 {
277 return Err(TrainError::NormNotPositive);
278 }
279
280 if !mean_norm.is_finite() {
281 return Err(TrainError::NormNotFinite);
282 }
283
284 let pre_scale: Positive<f32> = match pre_scale {
286 PreScale::None => crate::num::POSITIVE_ONE_F32,
287 PreScale::Some(v) => v,
288 PreScale::ReciprocalMeanNorm => {
289 let pre_scale = Positive::new(1.0 / mean_norm)
298 .map_err(|_| TrainError::ReciprocalNormNotFinite)?;
299
300 if !pre_scale.into_inner().is_finite() {
301 return Err(TrainError::ReciprocalNormNotFinite);
302 }
303
304 pre_scale
305 }
306 };
307
308 let centroid =
310 Poly::from_iter(centroid.into_iter().map(|i| i as f32), allocator.clone())?;
311
312 SphericalQuantizer::generate(
313 centroid,
314 mean_norm,
315 transform,
316 metric,
317 Some(pre_scale.into_inner()),
318 rng,
319 allocator,
320 )
321 }
322
323 train(data, transform, metric, pre_scale, rng, allocator)
324 }
325
326 pub fn rescale(&self, v: &mut [f32]) {
328 let norm = FastL2Norm.evaluate(&*v);
329 let m = self.mean_norm.into_inner() / norm;
330 v.iter_mut().for_each(|i| *i *= m);
331 }
332
333 fn preprocess<'a>(
339 &self,
340 data: &[f32],
341 allocator: ScopedAllocator<'a>,
342 ) -> Result<Preprocessed<'a>, CompressionError> {
343 assert_eq!(data.len(), self.input_dim(), "Data dimension is incorrect.");
344
345 let scale = self.pre_scale.into_inner();
350 let mul: f32 = match self.metric {
351 SupportedMetric::Cosine => {
352 let norm: f32 = (FastL2Norm).evaluate(data);
353 if norm == 0.0 { 1.0 } else { 1.0 / norm }
354 }
355 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => scale,
356 };
357
358 let shifted = Poly::from_iter(
360 std::iter::zip(data.iter(), self.shift.iter()).map(|(&f, &s)| mul * f - s),
361 allocator,
362 )?;
363
364 let shifted_norm = FastL2Norm.evaluate(&*shifted);
365 if !shifted_norm.is_finite() {
366 return Err(CompressionError::InputContainsNaN);
367 }
368 let inner_product_with_centroid = match self.metric {
369 SupportedMetric::SquaredL2 => None,
370 SupportedMetric::InnerProduct | SupportedMetric::Cosine => {
371 let ip: MathematicalValue<f32> = InnerProduct::evaluate(&*shifted, &*self.shift);
372 Some(ip.into_inner())
373 }
374 };
375
376 Ok(Preprocessed {
377 shifted,
378 shifted_norm,
379 inner_product_with_centroid,
380 })
381 }
382}
383
384#[derive(Debug, Clone, Copy)]
388pub enum PreScale {
389 None,
391 Some(Positive<f32>),
393 ReciprocalMeanNorm,
396}
397
398#[cfg(feature = "flatbuffers")]
399#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
400#[derive(Debug, Clone, Error, PartialEq)]
401#[non_exhaustive]
402pub enum DeserializationError {
403 #[error(transparent)]
404 TransformError(#[from] TransformError),
405 #[error("unrecognized flatbuffer identifier")]
406 UnrecognizedIdentifier,
407 #[error("transform length not equal to centroid")]
408 DimMismatch,
409 #[error("norm is missing or is not positive")]
410 MissingNorm,
411 #[error("pre-scale is missing or is not positive")]
412 PreScaleNotPositive,
413
414 #[error(transparent)]
415 InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
416
417 #[error(transparent)]
418 InvalidMetric(#[from] InvalidMetric),
419
420 #[error(transparent)]
421 AllocatorError(#[from] AllocatorError),
422}
423
424#[cfg(feature = "flatbuffers")]
425#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
426impl<A> SphericalQuantizer<A>
427where
428 A: Allocator + Clone,
429{
430 pub(crate) fn pack<'a, FA>(
433 &self,
434 buf: &mut FlatBufferBuilder<'a, FA>,
435 ) -> WIPOffset<spherical::SphericalQuantizer<'a>>
436 where
437 FA: flatbuffers::Allocator + 'a,
438 {
439 let centroid = buf.create_vector(&self.shift);
441
442 let transform = self.transform.pack(buf);
444
445 spherical::SphericalQuantizer::create(
447 buf,
448 &spherical::SphericalQuantizerArgs {
449 centroid: Some(centroid),
450 transform: Some(transform),
451 metric: self.metric.into(),
452 mean_norm: self.mean_norm.into_inner(),
453 pre_scale: self.pre_scale.into_inner(),
454 },
455 )
456 }
457
458 pub(crate) fn try_unpack(
461 alloc: A,
462 proto: spherical::SphericalQuantizer<'_>,
463 ) -> Result<Self, DeserializationError> {
464 let metric: SupportedMetric = proto.metric().try_into()?;
465
466 let shift = Poly::from_iter(proto.centroid().into_iter(), alloc.clone())?;
468
469 let transform = Transform::try_unpack(alloc, proto.transform())?;
471
472 if shift.len() != transform.input_dim() {
474 return Err(DeserializationError::DimMismatch);
475 }
476
477 let mean_norm =
479 Positive::new(proto.mean_norm()).map_err(|_| DeserializationError::MissingNorm)?;
480
481 let pre_scale = Positive::new(proto.pre_scale())
482 .map_err(|_| DeserializationError::PreScaleNotPositive)?;
483
484 Ok(Self {
485 shift,
486 transform,
487 metric,
488 mean_norm,
489 pre_scale,
490 })
491 }
492}
493
494struct Preprocessed<'a> {
495 shifted: Poly<[f32], ScopedAllocator<'a>>,
496 shifted_norm: f32,
497 inner_product_with_centroid: Option<f32>,
498}
499
500impl Preprocessed<'_> {
501 fn metric_specific(&self) -> f32 {
506 match self.inner_product_with_centroid {
507 Some(ip) => ip,
508 None => self.shifted_norm * self.shifted_norm,
509 }
510 }
511}
512
513impl<A> AsFunctor<CompensatedSquaredL2> for SphericalQuantizer<A>
518where
519 A: Allocator,
520{
521 fn as_functor(&self) -> CompensatedSquaredL2 {
522 CompensatedSquaredL2::new(self.output_dim())
523 }
524}
525
526impl<A> AsFunctor<CompensatedIP> for SphericalQuantizer<A>
527where
528 A: Allocator,
529{
530 fn as_functor(&self) -> CompensatedIP {
531 CompensatedIP::new(&self.shift, self.output_dim())
532 }
533}
534
535impl<A> AsFunctor<CompensatedCosine> for SphericalQuantizer<A>
536where
537 A: Allocator,
538{
539 fn as_functor(&self) -> CompensatedCosine {
540 CompensatedCosine::new(self.as_functor())
541 }
542}
543
544#[derive(Debug, Error, Clone, Copy, PartialEq)]
549#[non_exhaustive]
550pub enum CompressionError {
551 #[error("input contains NaN")]
552 InputContainsNaN,
553
554 #[error("expected source vector to have length {expected}")]
555 SourceDimensionMismatch { expected: usize },
556
557 #[error("expected destination vector to have length {expected}")]
558 DestinationDimensionMismatch { expected: usize },
559
560 #[error(
561 "encoding error - you may need to scale the entire dataset to reduce its dynamic range"
562 )]
563 EncodingError(#[from] DataMetaError),
564
565 #[error(transparent)]
566 AllocatorError(#[from] AllocatorError),
567}
568
569fn check_dims(
570 input: usize,
571 output: usize,
572 from: usize,
573 into: usize,
574) -> Result<(), CompressionError> {
575 if from != input {
576 return Err(CompressionError::SourceDimensionMismatch { expected: input });
577 }
578 if into != output {
579 return Err(CompressionError::DestinationDimensionMismatch { expected: output });
580 }
581 Ok(())
582}
583
584trait FinishCompressing {
587 fn finish_compressing(
588 &mut self,
589 preprocessed: &Preprocessed<'_>,
590 transformed: &[f32],
591 transformed_norm: f32,
592 allocator: ScopedAllocator<'_>,
593 ) -> Result<(), CompressionError>;
594}
595
596impl FinishCompressing for DataMut<'_, 1> {
597 fn finish_compressing(
598 &mut self,
599 preprocessed: &Preprocessed<'_>,
600 transformed: &[f32],
601 transformed_norm: f32,
602 _: ScopedAllocator<'_>,
603 ) -> Result<(), CompressionError> {
604 let mut quant_raw_inner_product = 0.0f32;
607 let mut bit_sum = 0u32;
608 transformed.iter().enumerate().for_each(|(i, &r)| {
609 let bit: u8 = if r > 0.0 { 1 } else { 0 };
610
611 quant_raw_inner_product += r.abs();
612 bit_sum += <u8 as Into<u32>>::into(bit);
613
614 unsafe { self.vector_mut().set_unchecked(i, bit) };
616 });
617
618 let inner_product_correction =
639 2.0 * transformed_norm * preprocessed.shifted_norm / quant_raw_inner_product;
640 self.set_meta(DataMeta::new(
641 inner_product_correction,
642 preprocessed.metric_specific(),
643 bit_sum,
644 )?);
645 Ok(())
646 }
647}
648
649impl FinishCompressing for DataMut<'_, 2> {
650 fn finish_compressing(
651 &mut self,
652 preprocessed: &Preprocessed<'_>,
653 transformed: &[f32],
654 transformed_norm: f32,
655 allocator: ScopedAllocator<'_>,
656 ) -> Result<(), CompressionError> {
657 compress_via_maximum_cosine(
658 self.reborrow_mut(),
659 preprocessed,
660 transformed,
661 transformed_norm,
662 allocator,
663 )
664 }
665}
666
667impl FinishCompressing for DataMut<'_, 4> {
668 fn finish_compressing(
669 &mut self,
670 preprocessed: &Preprocessed<'_>,
671 transformed: &[f32],
672 transformed_norm: f32,
673 allocator: ScopedAllocator<'_>,
674 ) -> Result<(), CompressionError> {
675 compress_via_maximum_cosine(
676 self.reborrow_mut(),
677 preprocessed,
678 transformed,
679 transformed_norm,
680 allocator,
681 )
682 }
683}
684
685impl FinishCompressing for DataMut<'_, 8> {
686 fn finish_compressing(
687 &mut self,
688 preprocessed: &Preprocessed<'_>,
689 transformed: &[f32],
690 transformed_norm: f32,
691 allocator: ScopedAllocator<'_>,
692 ) -> Result<(), CompressionError> {
693 compress_via_maximum_cosine(
694 self.reborrow_mut(),
695 preprocessed,
696 transformed,
697 transformed_norm,
698 allocator,
699 )
700 }
701}
702
703impl<A> CompressIntoWith<&[f32], FullQueryMut<'_>, ScopedAllocator<'_>> for SphericalQuantizer<A>
708where
709 A: Allocator,
710{
711 type Error = CompressionError;
712
713 fn compress_into_with(
726 &self,
727 from: &[f32],
728 mut into: FullQueryMut<'_>,
729 allocator: ScopedAllocator<'_>,
730 ) -> Result<(), Self::Error> {
731 let input_dim = self.shift.len();
732 let output_dim = self.output_dim();
733 check_dims(input_dim, output_dim, from.len(), into.len())?;
734
735 let mut preprocessed = self.preprocess(from, allocator)?;
736
737 if preprocessed.shifted_norm == 0.0 {
740 into.vector_mut().fill(0.0);
741 *into.meta_mut() = Default::default();
742 return Ok(());
743 }
744
745 preprocessed
746 .shifted
747 .iter_mut()
748 .for_each(|v| *v /= preprocessed.shifted_norm);
749
750 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
755 match self
756 .transform
757 .transform_into(into.vector_mut(), &preprocessed.shifted, allocator)
758 {
759 Ok(()) => {}
760 Err(TransformFailed::AllocatorError(err)) => {
761 return Err(CompressionError::AllocatorError(err));
762 }
763 Err(TransformFailed::SourceMismatch { .. })
764 | Err(TransformFailed::DestinationMismatch { .. }) => {
765 panic!(
766 "The sizes of these arrays should already be checked - this is a logic error"
767 );
768 }
769 #[cfg(feature = "linalg")]
770 Err(TransformFailed::SgemmError(_)) => {
771 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
772 }
773 }
774
775 *into.meta_mut() = FullQueryMeta {
776 sum: into.vector().iter().sum::<f32>(),
777 shifted_norm: preprocessed.shifted_norm,
778 metric_specific: preprocessed.metric_specific(),
779 };
780 Ok(())
781 }
782}
783
784impl<const NBITS: usize, A> CompressIntoWith<&[f32], DataMut<'_, NBITS>, ScopedAllocator<'_>>
785 for SphericalQuantizer<A>
786where
787 A: Allocator,
788 Unsigned: Representation<NBITS>,
789 for<'a> DataMut<'a, NBITS>: FinishCompressing,
790{
791 type Error = CompressionError;
792
793 fn compress_into_with(
806 &self,
807 from: &[f32],
808 mut into: DataMut<'_, NBITS>,
809 allocator: ScopedAllocator<'_>,
810 ) -> Result<(), Self::Error> {
811 let input_dim = self.shift.len();
812 let output_dim = self.output_dim();
813 check_dims(input_dim, output_dim, from.len(), into.len())?;
814
815 let mut preprocessed = self.preprocess(from, allocator)?;
816
817 if preprocessed.shifted_norm == 0.0 {
818 into.set_meta(DataMeta::default());
819 return Ok(());
820 }
821
822 let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
823 preprocessed
824 .shifted
825 .iter_mut()
826 .for_each(|v| *v /= preprocessed.shifted_norm);
827
828 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
833 match self
834 .transform
835 .transform_into(&mut transformed, &preprocessed.shifted, allocator)
836 {
837 Ok(()) => {}
838 Err(TransformFailed::AllocatorError(err)) => {
839 return Err(CompressionError::AllocatorError(err));
840 }
841 Err(TransformFailed::SourceMismatch { .. })
842 | Err(TransformFailed::DestinationMismatch { .. }) => {
843 panic!(
844 "The sizes of these arrays should already be checked - this is a logic error"
845 );
846 }
847 #[cfg(feature = "linalg")]
848 Err(TransformFailed::SgemmError(_)) => {
849 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
850 }
851 }
852
853 let transformed_norm = if self.transform.preserves_norms() {
854 1.0
855 } else {
856 (FastL2Norm).evaluate(&*transformed)
857 };
858
859 into.finish_compressing(&preprocessed, &transformed, transformed_norm, allocator)?;
860 Ok(())
861 }
862}
863
864struct AsNonZero<const NBITS: usize>;
865impl<const NBITS: usize> AsNonZero<NBITS> {
866 #[allow(clippy::unwrap_used)]
868 const NON_ZERO: NonZeroUsize = NonZeroUsize::new(NBITS).unwrap();
869}
870
871fn compress_via_maximum_cosine<const NBITS: usize>(
872 mut data: DataMut<'_, NBITS>,
873 preprocessed: &Preprocessed<'_>,
874 transformed: &[f32],
875 transformed_norm: f32,
876 allocator: ScopedAllocator<'_>,
877) -> Result<(), CompressionError>
878where
879 Unsigned: Representation<NBITS>,
880{
881 assert_eq!(data.len(), transformed.len());
882
883 let optimal_scale =
886 maximize_cosine_similarity(transformed, AsNonZero::<NBITS>::NON_ZERO, allocator)?;
887
888 let domain = Unsigned::domain_const::<NBITS>();
889 let min = *domain.start() as f32;
890 let max = *domain.end() as f32;
891 let offset = max / 2.0;
892
893 let mut self_inner_product = 0.0f32;
894 let mut bit_sum = 0u32;
895 for (i, t) in transformed.iter().enumerate() {
896 let v = (*t * optimal_scale + offset).clamp(min, max).round();
897 let dv = v - offset;
898 self_inner_product = dv.mul_add(*t, self_inner_product);
899
900 let v = v as u8;
901 bit_sum += <u8 as Into<u32>>::into(v);
902
903 unsafe { data.vector_mut().set_unchecked(i, v) };
908 }
909
910 let shifted_norm = preprocessed.shifted_norm;
911 let inner_product_correction = (transformed_norm * shifted_norm) / self_inner_product;
912 data.set_meta(DataMeta::new(
913 inner_product_correction,
914 preprocessed.metric_specific(),
915 bit_sum,
916 )?);
917 Ok(())
918}
919
920#[derive(Debug, Clone, Copy)]
934struct Pair {
935 value: f32,
936 position: u32,
937}
938
939impl PartialEq for Pair {
940 fn eq(&self, other: &Self) -> bool {
941 self.value.eq(&other.value)
942 }
943}
944
945impl Eq for Pair {}
946impl PartialOrd for Pair {
947 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
948 Some(self.cmp(other))
949 }
950}
951impl Ord for Pair {
952 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
953 other
954 .value
955 .partial_cmp(&self.value)
956 .unwrap_or(std::cmp::Ordering::Equal)
957 }
958}
959
960fn maximize_cosine_similarity(
992 v: &[f32],
993 num_bits: NonZeroUsize,
994 allocator: ScopedAllocator<'_>,
995) -> Result<f32, AllocatorError> {
996 let mut current_ip = 0.5 * v.iter().map(|i| i.abs() as f64).sum::<f64>();
1001 let mut current_square_norm = 0.25 * (v.len() as f64);
1002
1003 let mut rounded = Poly::broadcast(1u16, v.len(), allocator)?;
1007
1008 let eps = 0.0001f32;
1014 let one_and_change = 1.0 + eps;
1015 let mut base = Poly::from_iter(
1016 v.iter().enumerate().map(|(position, value)| {
1017 let value = one_and_change / value.abs();
1018 Pair {
1019 value,
1020 position: position as u32,
1021 }
1022 }),
1023 allocator,
1024 )?;
1025
1026 #[allow(clippy::expect_used)]
1029 let mut critical_values =
1030 SliceHeap::new(&mut base).expect("calling code should not allow the slice to be empty");
1031
1032 let mut max_similarity = f64::NEG_INFINITY;
1033 let mut optimal_scale = f32::default();
1034 let stop = (2usize).pow(num_bits.get() as u32 - 1) as u16;
1035
1036 loop {
1037 let mut should_break = false;
1038 critical_values.update_root(|pair| {
1039 let Pair { value, position } = *pair;
1040 if value == f32::MAX {
1041 should_break = true;
1042 return;
1043 }
1044
1045 let r = &mut rounded[position as usize];
1046 let vp = &v[position as usize];
1047
1048 let old_r = *r;
1049 *r += 1;
1052
1053 current_ip += vp.abs() as f64;
1058
1059 current_square_norm += (2 * old_r) as f64;
1073
1074 let similarity = current_ip / current_square_norm.sqrt();
1076 if similarity > max_similarity {
1077 max_similarity = similarity;
1078 optimal_scale = value;
1079 }
1080
1081 if *r < stop {
1083 *pair = Pair {
1084 value: (*r as f32 + eps) / vp.abs(),
1085 position,
1086 };
1087 } else {
1088 *pair = Pair {
1089 value: f32::MAX,
1090 position,
1091 };
1092 }
1093 });
1094 if should_break {
1095 break;
1096 }
1097 }
1098
1099 Ok(optimal_scale)
1100}
1101
1102impl<const NBITS: usize, Perm, A>
1107 CompressIntoWith<&[f32], QueryMut<'_, NBITS, Perm>, ScopedAllocator<'_>>
1108 for SphericalQuantizer<A>
1109where
1110 Unsigned: Representation<NBITS>,
1111 Perm: PermutationStrategy<NBITS>,
1112 A: Allocator,
1113{
1114 type Error = CompressionError;
1115
1116 fn compress_into_with(
1129 &self,
1130 from: &[f32],
1131 mut into: QueryMut<'_, NBITS, Perm>,
1132 allocator: ScopedAllocator<'_>,
1133 ) -> Result<(), Self::Error> {
1134 let input_dim = self.shift.len();
1135 let output_dim = self.output_dim();
1136 check_dims(input_dim, output_dim, from.len(), into.len())?;
1137
1138 let mut preprocessed = self.preprocess(from, allocator)?;
1139
1140 if preprocessed.shifted_norm == 0.0 {
1141 into.set_meta(QueryMeta::default());
1142 return Ok(());
1143 }
1144
1145 preprocessed
1146 .shifted
1147 .iter_mut()
1148 .for_each(|v| *v /= preprocessed.shifted_norm);
1149
1150 let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
1151
1152 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
1157 match self
1158 .transform
1159 .transform_into(&mut transformed, &preprocessed.shifted, allocator)
1160 {
1161 Ok(()) => {}
1162 Err(TransformFailed::AllocatorError(err)) => {
1163 return Err(CompressionError::AllocatorError(err));
1164 }
1165 Err(TransformFailed::SourceMismatch { .. })
1166 | Err(TransformFailed::DestinationMismatch { .. }) => {
1167 panic!(
1168 "The sizes of these arrays should already be checked - this is a logic error"
1169 );
1170 }
1171 #[cfg(feature = "linalg")]
1172 Err(TransformFailed::SgemmError(_)) => {
1173 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
1174 }
1175 }
1176
1177 let (min, max) = transformed
1179 .iter()
1180 .fold((f32::MAX, f32::MIN), |(min, max), i| {
1181 (i.min(min), i.max(max))
1182 });
1183
1184 let domain = Unsigned::domain_const::<NBITS>();
1185 let lo = (*domain.start()) as f32;
1186 let hi = (*domain.end()) as f32;
1187
1188 let scale = (max - min) / hi;
1189 let mut bit_sum: f32 = 0.0;
1190 transformed.iter().enumerate().for_each(|(i, v)| {
1191 let c = ((v - min) / scale).round().clamp(lo, hi);
1192 bit_sum += c;
1193
1194 #[allow(clippy::unwrap_used)]
1200 into.vector_mut().set(i, c as i64).unwrap();
1201 });
1202
1203 into.set_meta(QueryMeta {
1205 inner_product_correction: preprocessed.shifted_norm * scale,
1206 bit_sum,
1207 offset: min / scale,
1208 metric_specific: preprocessed.metric_specific(),
1209 });
1210
1211 Ok(())
1212 }
1213}
1214
1215#[cfg(not(miri))]
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223
1224 use std::fmt::Display;
1225
1226 use diskann_utils::{
1227 ReborrowMut, lazy_format,
1228 views::{self, Matrix},
1229 };
1230 use diskann_vector::{PureDistanceFunction, norm::FastL2NormSquared};
1231 use diskann_wide::ARCH;
1232 use rand::{
1233 SeedableRng,
1234 distr::{Distribution, Uniform},
1235 rngs::StdRng,
1236 };
1237 use rand_distr::StandardNormal;
1238
1239 use crate::{
1240 algorithms::transforms::TargetDim,
1241 alloc::GlobalAllocator,
1242 bits::{BitTranspose, Dense},
1243 spherical::{Data, DataMetaF32, FullQuery, Query},
1244 test_util,
1245 };
1246
1247 #[test]
1249 fn test_cosine_similarity_maximizer() {
1250 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
1251 let num_trials = 10000;
1252 let num_bits = NonZeroUsize::new(3).unwrap();
1253
1254 let scale_distribution = Uniform::new(0.5f32, 10.0f32).unwrap();
1255
1256 let run_test = |target: [f32; 4]| {
1257 let scale =
1258 maximize_cosine_similarity(&target, num_bits, ScopedAllocator::global()).unwrap();
1259
1260 let mut best: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
1261 let mut best_similarity: f32 = f32::NEG_INFINITY;
1262
1263 let min = -3.5;
1266 for i0 in 0..8 {
1267 for i1 in 0..8 {
1268 for i2 in 0..8 {
1269 for i3 in 0..8 {
1270 let p: [f32; 4] = [
1271 min + (i0 as f32),
1272 min + (i1 as f32),
1273 min + (i2 as f32),
1274 min + (i3 as f32),
1275 ];
1276
1277 let sim: MathematicalValue<f32> =
1278 diskann_vector::distance::Cosine::evaluate(&p, &target);
1279 let sim = sim.into_inner();
1280 if sim > best_similarity {
1281 best_similarity = sim;
1282 best = p.map(|i| i - min);
1284 }
1285 }
1286 }
1287 }
1288 }
1289
1290 let clamped = target.map(|i| (i * scale - min).round().clamp(0.0, 7.0));
1293 let clamped_cosine: MathematicalValue<f32> =
1294 diskann_vector::distance::Cosine::evaluate(&clamped.map(|i| i + min), &target);
1295
1296 let passed = if best == clamped {
1299 true
1300 } else {
1301 let ratio: Vec<f32> = std::iter::zip(best, clamped)
1302 .map(|(b, c)| {
1303 let ratio = (b + min) / (c + min);
1304 assert_ne!(
1305 ratio, 0.0,
1306 "ratio should never be zero because `b` is an integer and \
1307 `min` is not"
1308 );
1309 ratio
1310 })
1311 .collect();
1312
1313 ratio.iter().all(|i| *i == ratio[0])
1314 };
1315
1316 if !passed {
1317 panic!(
1318 "failed for input {:?}.\
1319 Best = {:?}, Found = {:?}\
1320 Best similarity = {}, similarity with clamped = {}",
1321 target,
1322 best,
1323 clamped,
1324 best_similarity,
1325 clamped_cosine.into_inner()
1326 );
1327 }
1328 };
1329
1330 let min = -3.5;
1332 for i0 in (0..8).step_by(2) {
1333 for i1 in (1..9).step_by(2) {
1334 for i2 in (0..8).step_by(2) {
1335 for i3 in (1..9).step_by(2) {
1336 let p: [f32; 4] = [
1337 min + (i0 as f32),
1338 min + (i1 as f32),
1339 min + (i2 as f32),
1340 min + (i3 as f32),
1341 ];
1342 run_test(p)
1343 }
1344 }
1345 }
1346 }
1347
1348 for _ in 0..num_trials {
1349 let this_scale: f32 = scale_distribution.sample(&mut rng);
1350 let v: [f32; 4] = [(); 4].map(|_| {
1351 let v: f32 = StandardNormal {}.sample(&mut rng);
1352 this_scale * v
1353 });
1354 run_test(v);
1355 }
1356 }
1357
1358 #[test]
1359 #[should_panic(expected = "calling code should not allow the slice to be empty")]
1360 fn empty_slice_panics() {
1361 maximize_cosine_similarity(
1362 &[],
1363 NonZeroUsize::new(4).unwrap(),
1364 ScopedAllocator::global(),
1365 )
1366 .unwrap();
1367 }
1368
1369 struct Setup {
1370 transform: TransformKind,
1371 nrows: usize,
1372 ncols: usize,
1373 num_trials: usize,
1374 }
1375
1376 fn get_scale(scale: PreScale, quantizer: &SphericalQuantizer) -> f32 {
1377 match scale {
1378 PreScale::None => 1.0,
1379 PreScale::Some(v) => v.into_inner(),
1380 PreScale::ReciprocalMeanNorm => 1.0 / quantizer.mean_norm().into_inner(),
1381 }
1382 }
1383
1384 fn test_l2<const Q: usize, const D: usize, Perm>(
1385 setup: &Setup,
1386 problem: &test_util::TestProblem,
1387 computed_means: &[f32],
1388 pre_scale: PreScale,
1389 rng: &mut StdRng,
1390 ) where
1391 Unsigned: Representation<Q>,
1392 Unsigned: Representation<D>,
1393 Perm: PermutationStrategy<Q>,
1394 for<'a> SphericalQuantizer:
1395 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1396 for<'a> SphericalQuantizer:
1397 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1398 {
1399 assert_eq!(setup.nrows, problem.data.nrows());
1400 assert_eq!(setup.ncols, problem.data.ncols());
1401
1402 let scoped_global = ScopedAllocator::global();
1403 let distribution = Uniform::new(0, setup.nrows).unwrap();
1404 let quantizer = SphericalQuantizer::train(
1405 problem.data.as_view(),
1406 setup.transform,
1407 SupportedMetric::SquaredL2,
1408 pre_scale,
1409 rng,
1410 GlobalAllocator,
1411 )
1412 .unwrap();
1413
1414 let scale = get_scale(pre_scale, &quantizer);
1415
1416 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1417 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1418 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1419
1420 assert_eq!(
1421 quantizer.mean_norm.into_inner(),
1422 problem.mean_norm as f32,
1423 "computed mean norm should not apply scale"
1424 );
1425 let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1426 assert_eq!(&*scaled_means, quantizer.shift());
1427
1428 let l2: CompensatedSquaredL2 = quantizer.as_functor();
1429 assert_eq!(l2.dim, quantizer.output_dim() as f32);
1430
1431 for _ in 0..setup.num_trials {
1432 let i = distribution.sample(rng);
1433 let v = problem.data.row(i);
1434
1435 quantizer
1436 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1437 .unwrap();
1438 quantizer
1439 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1440 .unwrap();
1441 quantizer
1442 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1443 .unwrap();
1444
1445 let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1446 .map(|(a, b)| scale * a - b)
1447 .collect();
1448
1449 {
1451 let DataMetaF32 {
1452 inner_product_correction,
1453 bit_sum,
1454 metric_specific,
1455 } = b.meta().to_full(ARCH);
1456
1457 let shifted_square_norm = metric_specific;
1458
1459 let bv = b.vector();
1461 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1462 assert_eq!(s, bit_sum as usize);
1463
1464 {
1466 let expected = FastL2NormSquared.evaluate(&*shifted);
1467 let err = (shifted_square_norm - expected).abs() / expected.abs();
1468 assert!(
1469 err < 5.0e-4, "failed diff check, got {}, expected {} - relative error = {}",
1471 shifted_square_norm,
1472 expected,
1473 err
1474 );
1475 }
1476
1477 if const { D == 1 } {
1480 let self_inner_product = 2.0 * shifted_square_norm.sqrt()
1481 / (inner_product_correction * (bv.len() as f32).sqrt());
1482 assert!(
1483 (self_inner_product - 0.8).abs() < 0.13,
1484 "self inner-product should be close to 0.8. Instead, it's {}",
1485 self_inner_product
1486 );
1487 }
1488 }
1489
1490 {
1491 let QueryMeta {
1492 inner_product_correction,
1493 bit_sum,
1494 offset,
1495 metric_specific,
1496 } = q.meta();
1497
1498 let shifted_square_norm = metric_specific;
1499 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1500 preprocessed
1501 .shifted
1502 .iter_mut()
1503 .for_each(|i| *i /= preprocessed.shifted_norm);
1504
1505 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1506 quantizer
1507 .transform
1508 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1509 .unwrap();
1510
1511 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1512 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1513
1514 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1515
1516 {
1518 let expected = FastL2NormSquared.evaluate(&*shifted);
1519 let err = (shifted_square_norm - expected).abs() / expected.abs();
1520 assert!(
1521 err < 2e-7,
1522 "failed diff check, got {}, expected {} - relative error = {}",
1523 shifted_square_norm,
1524 expected,
1525 err
1526 );
1527 }
1528
1529 {
1531 let expected = shifted_square_norm.sqrt() * scale;
1532 let got = inner_product_correction;
1533
1534 let err = (expected - got).abs();
1535 assert!(
1536 err < 1.0e-7,
1537 "\"innerproduct_scale\": expected {}, got {}, error = {}",
1538 expected,
1539 got,
1540 err
1541 );
1542 }
1543
1544 {
1546 let expected = min / scale;
1547 let got = offset;
1548
1549 let err = (expected - got).abs();
1550 assert!(
1551 err < 1.0e-7,
1552 "\"sum_scale\": expected {}, got {}, error = {}",
1553 expected,
1554 got,
1555 err
1556 );
1557 }
1558
1559 {
1561 let expected = (0..q.len())
1562 .map(|i| q.vector().get(i).unwrap())
1563 .sum::<i64>() as f32;
1564
1565 let got = bit_sum;
1566
1567 let err = (expected - got).abs();
1568 assert!(
1569 err < 1.0e-7,
1570 "\"offset\": expected {}, got {}, error = {}",
1571 expected,
1572 got,
1573 err
1574 );
1575 }
1576 }
1577
1578 {
1580 let s: f32 = f.data.iter().sum::<f32>();
1582 assert_eq!(s, f.meta.sum);
1583
1584 {
1586 let expected = FastL2Norm.evaluate(&*shifted);
1587 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1588 assert!(
1589 err < 2e-7,
1590 "failed diff check, got {}, expected {} - relative error = {}",
1591 f.meta.shifted_norm,
1592 expected,
1593 err
1594 );
1595 }
1596
1597 assert_eq!(
1598 f.meta.metric_specific,
1599 f.meta.shifted_norm * f.meta.shifted_norm,
1600 "metric specific data for squared l2 is the square shifted norm",
1601 );
1602 }
1603 }
1604
1605 quantizer
1608 .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1609 .unwrap();
1610 assert_eq!(b.meta(), DataMeta::default());
1611
1612 quantizer
1613 .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1614 .unwrap();
1615 assert_eq!(q.meta(), QueryMeta::default());
1616
1617 f.data.fill(f32::INFINITY);
1618 quantizer
1619 .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1620 .unwrap();
1621 assert!(f.data.iter().all(|&i| i == 0.0));
1622 assert_eq!(f.meta.sum, 0.0);
1623 assert_eq!(f.meta.metric_specific, 0.0);
1624 }
1625
1626 fn test_ip<const Q: usize, const D: usize, Perm>(
1627 setup: &Setup,
1628 problem: &test_util::TestProblem,
1629 computed_means: &[f32],
1630 pre_scale: PreScale,
1631 rng: &mut StdRng,
1632 ctx: &dyn Display,
1633 ) where
1634 Unsigned: Representation<Q>,
1635 Unsigned: Representation<D>,
1636 Perm: PermutationStrategy<Q>,
1637 for<'a> SphericalQuantizer:
1638 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1639 for<'a> SphericalQuantizer:
1640 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1641 {
1642 assert_eq!(setup.nrows, problem.data.nrows());
1643 assert_eq!(setup.ncols, problem.data.ncols());
1644
1645 let scoped_global = ScopedAllocator::global();
1646 let distribution = Uniform::new(0, setup.nrows).unwrap();
1647 let quantizer = SphericalQuantizer::train(
1648 problem.data.as_view(),
1649 setup.transform,
1650 SupportedMetric::InnerProduct,
1651 pre_scale,
1652 rng,
1653 GlobalAllocator,
1654 )
1655 .unwrap();
1656
1657 let scale = get_scale(pre_scale, &quantizer);
1658
1659 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1660 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1661 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1662
1663 assert_eq!(
1664 quantizer.mean_norm.into_inner(),
1665 problem.mean_norm as f32,
1666 "computed mean norm should not apply scale"
1667 );
1668 let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1669 assert_eq!(&*scaled_means, quantizer.shift());
1670
1671 let ip: CompensatedIP = quantizer.as_functor();
1672
1673 assert_eq!(ip.dim, quantizer.output_dim() as f32);
1674 assert_eq!(
1675 ip.squared_shift_norm,
1676 FastL2NormSquared.evaluate(quantizer.shift())
1677 );
1678
1679 for _ in 0..setup.num_trials {
1680 let i = distribution.sample(rng);
1681 let v = problem.data.row(i);
1682
1683 quantizer
1684 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1685 .unwrap();
1686 quantizer
1687 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1688 .unwrap();
1689 quantizer
1690 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1691 .unwrap();
1692
1693 let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1694 .map(|(a, b)| scale * a - b)
1695 .collect();
1696
1697 {
1699 let DataMetaF32 {
1700 inner_product_correction,
1701 bit_sum,
1702 metric_specific,
1703 } = b.meta().to_full(ARCH);
1704
1705 let inner_product_with_centroid = metric_specific;
1706
1707 let bv = b.vector();
1709 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1710 assert_eq!(s, bit_sum as usize);
1711
1712 let inner_product: MathematicalValue<f32> =
1714 InnerProduct::evaluate(&*shifted, quantizer.shift());
1715
1716 let diff = (inner_product.into_inner() - inner_product_with_centroid).abs();
1717 assert!(
1718 diff < 1.53e-5,
1719 "got a diff of {}. Expected = {}, got = {} -- context: {}",
1720 diff,
1721 inner_product.into_inner(),
1722 inner_product_with_centroid,
1723 ctx,
1724 );
1725
1726 if const { D == 1 } {
1729 let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1730 / (inner_product_correction * (bv.len() as f32).sqrt());
1731 assert!(
1732 (self_inner_product - 0.8).abs() < 0.12,
1733 "self inner-product should be close to 0.8. Instead, it's {}",
1734 self_inner_product
1735 );
1736 }
1737 }
1738
1739 {
1740 let QueryMeta {
1741 inner_product_correction,
1742 bit_sum,
1743 offset,
1744 metric_specific,
1745 } = q.meta();
1746
1747 let inner_product_with_centroid = metric_specific;
1748 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1749 preprocessed
1750 .shifted
1751 .iter_mut()
1752 .for_each(|i| *i /= preprocessed.shifted_norm);
1753
1754 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1755 quantizer
1756 .transform
1757 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1758 .unwrap();
1759
1760 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1761 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1762
1763 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1764
1765 {
1767 let expected = (FastL2Norm).evaluate(&*shifted) * scale;
1768 let got = inner_product_correction;
1769
1770 let err = (expected - got).abs();
1771 assert!(
1772 err < 1.0e-7,
1773 "\"innerproduct_scale\": expected {}, got {}, error = {}",
1774 expected,
1775 got,
1776 err
1777 );
1778 }
1779
1780 {
1782 let expected = min / scale;
1783 let got = offset;
1784
1785 let err = (expected - got).abs();
1786 assert!(
1787 err < 1.0e-7,
1788 "\"sum_scale\": expected {}, got {}, error = {}",
1789 expected,
1790 got,
1791 err
1792 );
1793 }
1794
1795 {
1797 let expected = (0..q.len())
1798 .map(|i| q.vector().get(i).unwrap())
1799 .sum::<i64>() as f32;
1800
1801 let got = bit_sum;
1802
1803 let err = (expected - got).abs();
1804 assert!(
1805 err < 1.0e-7,
1806 "\"offset\": expected {}, got {}, error = {}",
1807 expected,
1808 got,
1809 err
1810 );
1811 }
1812
1813 {
1815 let inner_product: MathematicalValue<f32> =
1817 InnerProduct::evaluate(&*shifted, quantizer.shift());
1818 assert_eq!(inner_product.into_inner(), inner_product_with_centroid);
1819 }
1820 }
1821
1822 {
1824 let s: f32 = f.data.iter().sum::<f32>();
1826 assert_eq!(s, f.meta.sum);
1827
1828 {
1830 let expected = FastL2Norm.evaluate(&*shifted);
1831 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1832 assert!(
1833 err < 2e-7,
1834 "failed diff check, got {}, expected {} - relative error = {}",
1835 f.meta.shifted_norm,
1836 expected,
1837 err
1838 );
1839 }
1840
1841 let inner_product: MathematicalValue<f32> =
1843 InnerProduct::evaluate(&*shifted, quantizer.shift());
1844 assert_eq!(inner_product.into_inner(), f.meta.metric_specific,);
1845 }
1846 }
1847
1848 quantizer
1851 .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1852 .unwrap();
1853 assert_eq!(b.meta(), DataMeta::default());
1854
1855 quantizer
1856 .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1857 .unwrap();
1858 assert_eq!(q.meta(), QueryMeta::default());
1859
1860 f.data.fill(f32::INFINITY);
1861 quantizer
1862 .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1863 .unwrap();
1864 assert!(f.data.iter().all(|&i| i == 0.0));
1865 assert_eq!(f.meta.sum, 0.0);
1866 assert_eq!(f.meta.metric_specific, 0.0);
1867 }
1868
1869 fn test_cosine<const Q: usize, const D: usize, Perm>(
1870 setup: &Setup,
1871 problem: &test_util::TestProblem,
1872 pre_scale: PreScale,
1873 rng: &mut StdRng,
1874 ) where
1875 Unsigned: Representation<Q>,
1876 Unsigned: Representation<D>,
1877 Perm: PermutationStrategy<Q>,
1878 for<'a> SphericalQuantizer:
1879 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1880 for<'a> SphericalQuantizer:
1881 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1882 {
1883 assert_eq!(setup.nrows, problem.data.nrows());
1884 assert_eq!(setup.ncols, problem.data.ncols());
1885
1886 let scoped_global = ScopedAllocator::global();
1887 let distribution = Uniform::new(0, setup.nrows).unwrap();
1888 let quantizer = SphericalQuantizer::train(
1889 problem.data.as_view(),
1890 setup.transform,
1891 SupportedMetric::Cosine,
1892 pre_scale,
1893 rng,
1894 GlobalAllocator,
1895 )
1896 .unwrap();
1897
1898 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1899 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1900 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1901
1902 let cosine: CompensatedCosine = quantizer.as_functor();
1903
1904 assert_eq!(cosine.inner.dim, quantizer.output_dim() as f32);
1905 assert_eq!(
1906 cosine.inner.squared_shift_norm,
1907 FastL2NormSquared.evaluate(quantizer.shift())
1908 );
1909
1910 const IP_BOUND: f32 = 2.6e-3;
1911
1912 let mut test_row = |v: &[f32]| {
1913 let vnorm = (FastL2Norm).evaluate(v);
1914 let v_normalized: Vec<f32> = v
1915 .iter()
1916 .map(|i| if vnorm == 0.0 { 0.0 } else { *i / vnorm })
1917 .collect();
1918
1919 quantizer
1920 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1921 .unwrap();
1922
1923 quantizer
1924 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1925 .unwrap();
1926
1927 quantizer
1928 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1929 .unwrap();
1930
1931 let shifted: Vec<f32> = std::iter::zip(v_normalized.iter(), quantizer.shift().iter())
1932 .map(|(a, b)| a - b)
1933 .collect();
1934
1935 {
1937 let DataMetaF32 {
1938 inner_product_correction,
1939 bit_sum,
1940 metric_specific,
1941 } = b.meta().to_full(ARCH);
1942
1943 let inner_product_with_centroid = metric_specific;
1944
1945 let bv = b.vector();
1947 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1948 assert_eq!(s, bit_sum as usize);
1949
1950 let inner_product: MathematicalValue<f32> =
1953 InnerProduct::evaluate(&*shifted, quantizer.shift());
1954
1955 let abs = (inner_product.into_inner() - inner_product_with_centroid).abs();
1956 let relative = abs / inner_product.into_inner().abs();
1957
1958 assert!(
1959 abs < 1e-7 || relative < IP_BOUND,
1960 "got an abs/rel of {}/{} with a bound of {}/{}",
1961 abs,
1962 relative,
1963 1e-7,
1964 IP_BOUND
1965 );
1966
1967 if const { D == 1 } {
1970 let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1971 / (inner_product_correction * (bv.len() as f32).sqrt());
1972 assert!(
1973 (self_inner_product - 0.8).abs() < 0.11,
1974 "self inner-product should be close to 0.8. Instead, it's {}",
1975 self_inner_product
1976 );
1977 }
1978 }
1979
1980 {
1981 let QueryMeta {
1982 inner_product_correction,
1983 bit_sum,
1984 offset,
1985 metric_specific,
1986 } = q.meta();
1987
1988 let inner_product_with_centroid = metric_specific;
1989 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1990 preprocessed
1991 .shifted
1992 .iter_mut()
1993 .for_each(|i| *i /= preprocessed.shifted_norm);
1994
1995 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1996 quantizer
1997 .transform
1998 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1999 .unwrap();
2000
2001 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
2002 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
2003
2004 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
2005
2006 {
2008 let expected = (FastL2Norm).evaluate(&*shifted) * scale;
2009 let got = inner_product_correction;
2010
2011 let err = (expected - got).abs();
2012 assert!(
2013 err < 1.0e-7,
2014 "\"innerproduct_scale\": expected {}, got {}, error = {}",
2015 expected,
2016 got,
2017 err
2018 );
2019 }
2020
2021 {
2023 let expected = min / scale;
2024 let got = offset;
2025
2026 let err = (expected - got).abs();
2027 assert!(
2028 err < 1.0e-7,
2029 "\"sum_scale\": expected {}, got {}, error = {}",
2030 expected,
2031 got,
2032 err
2033 );
2034 }
2035
2036 {
2038 let expected = (0..q.len())
2039 .map(|i| q.vector().get(i).unwrap())
2040 .sum::<i64>() as f32;
2041
2042 let got = bit_sum;
2043
2044 let err = (expected - got).abs();
2045 assert!(
2046 err < 1.0e-7,
2047 "\"offset\": expected {}, got {}, error = {}",
2048 expected,
2049 got,
2050 err
2051 );
2052 }
2053
2054 {
2056 let inner_product: MathematicalValue<f32> =
2058 InnerProduct::evaluate(&*shifted, quantizer.shift());
2059
2060 let err = (inner_product.into_inner() - inner_product_with_centroid).abs()
2061 / inner_product.into_inner().abs();
2062 assert!(
2063 err < IP_BOUND,
2064 "\"offset\": expected {}, got {}, error = {}",
2065 inner_product.into_inner(),
2066 inner_product_with_centroid,
2067 err
2068 );
2069 }
2070 }
2071
2072 {
2074 let s: f32 = f.data.iter().sum::<f32>();
2076 assert_eq!(s, f.meta.sum);
2077
2078 {
2080 let expected = FastL2Norm.evaluate(&*shifted);
2081 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
2082 assert!(
2083 err < 2e-7,
2084 "failed diff check, got {}, expected {} - relative error = {}",
2085 f.meta.shifted_norm,
2086 expected,
2087 err
2088 );
2089 }
2090
2091 let inner_product: MathematicalValue<f32> =
2093 InnerProduct::evaluate(&*shifted, quantizer.shift());
2094 let err = (inner_product.into_inner() - f.meta.metric_specific).abs()
2095 / inner_product.into_inner().abs();
2096 assert!(
2097 err < IP_BOUND,
2098 "\"offset\": expected {}, got {}, error = {}",
2099 inner_product.into_inner(),
2100 f.meta.metric_specific,
2101 err
2102 );
2103 }
2104 };
2105
2106 for _ in 0..setup.num_trials {
2107 let i = distribution.sample(rng);
2108 let v = problem.data.row(i);
2109 test_row(v);
2110 }
2111
2112 let zero = vec![0.0f32; quantizer.input_dim()];
2114 test_row(&zero);
2115 }
2116
2117 fn _test_oom_resiliance<T>(quantizer: &SphericalQuantizer, data: &[f32], dst: &mut T)
2118 where
2119 for<'a> T: ReborrowMut<'a>,
2120 for<'a> SphericalQuantizer: CompressIntoWith<
2121 &'a [f32],
2122 <T as ReborrowMut<'a>>::Target,
2123 ScopedAllocator<'a>,
2124 Error = CompressionError,
2125 >,
2126 {
2127 let mut succeeded = false;
2128 let mut failed = false;
2129 for max_allocations in 0..10 {
2130 match quantizer.compress_into_with(
2131 data,
2132 dst.reborrow_mut(),
2133 ScopedAllocator::new(&test_util::LimitedAllocator::new(max_allocations)),
2134 ) {
2135 Ok(()) => {
2136 succeeded = true;
2137 }
2138 Err(CompressionError::AllocatorError(_)) => {
2139 failed = true;
2140 }
2141 Err(other) => {
2142 panic!("received an unexpected error: {:?}", other);
2143 }
2144 }
2145 }
2146 assert!(succeeded);
2147 assert!(failed);
2148 }
2149
2150 fn test_oom_resiliance<const Q: usize, const D: usize, Perm>(
2151 setup: &Setup,
2152 problem: &test_util::TestProblem,
2153 pre_scale: PreScale,
2154 rng: &mut StdRng,
2155 ) where
2156 Unsigned: Representation<Q>,
2157 Unsigned: Representation<D>,
2158 Perm: PermutationStrategy<Q>,
2159 for<'a> SphericalQuantizer: CompressIntoWith<
2160 &'a [f32],
2161 DataMut<'a, D>,
2162 ScopedAllocator<'a>,
2163 Error = CompressionError,
2164 >,
2165 for<'a> SphericalQuantizer: CompressIntoWith<
2166 &'a [f32],
2167 QueryMut<'a, Q, Perm>,
2168 ScopedAllocator<'a>,
2169 Error = CompressionError,
2170 >,
2171 {
2172 assert_eq!(setup.nrows, problem.data.nrows());
2173 assert_eq!(setup.ncols, problem.data.ncols());
2174
2175 let quantizer = SphericalQuantizer::train(
2176 problem.data.as_view(),
2177 setup.transform,
2178 SupportedMetric::SquaredL2,
2179 pre_scale,
2180 rng,
2181 GlobalAllocator,
2182 )
2183 .unwrap();
2184
2185 let data = problem.data.row(0);
2187 _test_oom_resiliance::<Data<D, _>>(
2188 &quantizer,
2189 data,
2190 &mut Data::new_boxed(quantizer.output_dim()),
2191 );
2192 _test_oom_resiliance::<Query<Q, Perm, _>>(
2193 &quantizer,
2194 data,
2195 &mut Query::new_boxed(quantizer.output_dim()),
2196 );
2197 _test_oom_resiliance::<FullQuery<_>>(
2198 &quantizer,
2199 data,
2200 &mut FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap(),
2201 );
2202 }
2203
2204 fn test_quantizer<const Q: usize, const D: usize, Perm>(setup: &Setup, rng: &mut StdRng)
2205 where
2206 Unsigned: Representation<Q>,
2207 Unsigned: Representation<D>,
2208 Perm: PermutationStrategy<Q>,
2209 for<'a> SphericalQuantizer: CompressIntoWith<
2210 &'a [f32],
2211 DataMut<'a, D>,
2212 ScopedAllocator<'a>,
2213 Error = CompressionError,
2214 >,
2215 for<'a> SphericalQuantizer: CompressIntoWith<
2216 &'a [f32],
2217 QueryMut<'a, Q, Perm>,
2218 ScopedAllocator<'a>,
2219 Error = CompressionError,
2220 >,
2221 {
2222 let problem = test_util::create_test_problem(setup.nrows, setup.ncols, rng);
2223 let computed_means_f32: Vec<_> = problem.means.iter().map(|i| *i as f32).collect();
2224
2225 let scales = [
2226 PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2227 PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2228 PreScale::ReciprocalMeanNorm,
2229 ];
2230
2231 for scale in scales {
2232 let ctx = &lazy_format!("dim = {}, scale = {:?}", setup.ncols, scale);
2233
2234 test_l2::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng);
2235 test_ip::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng, ctx);
2236 test_cosine::<Q, D, Perm>(setup, &problem, scale, rng);
2237 }
2238
2239 test_oom_resiliance::<Q, D, Perm>(setup, &problem, PreScale::ReciprocalMeanNorm, rng);
2240 }
2241
2242 #[test]
2243 fn test_spherical_quantizer() {
2244 let mut rng = StdRng::seed_from_u64(0xab516aef1ce61640);
2245 for dim in [56, 72, 128, 255] {
2246 let setup = Setup {
2247 transform: TransformKind::PaddingHadamard {
2248 target_dim: TargetDim::Same,
2249 },
2250 nrows: 64,
2251 ncols: dim,
2252 num_trials: 10,
2253 };
2254
2255 test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2256 test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2257 test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2258 test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2259
2260 let setup = Setup {
2261 transform: TransformKind::DoubleHadamard {
2262 target_dim: TargetDim::Same,
2263 },
2264 nrows: 64,
2265 ncols: dim,
2266 num_trials: 10,
2267 };
2268 test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2269 test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2270 test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2271 test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2272 }
2273 }
2274
2275 #[test]
2280 fn err_dim_cannot_be_zero() {
2281 let data = Matrix::new(0.0f32, 10, 0);
2282 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2283 let err = SphericalQuantizer::train(
2284 data.as_view(),
2285 TransformKind::DoubleHadamard {
2286 target_dim: TargetDim::Same,
2287 },
2288 SupportedMetric::SquaredL2,
2289 PreScale::None,
2290 &mut rng,
2291 GlobalAllocator,
2292 )
2293 .unwrap_err();
2294 assert_eq!(err.to_string(), "data dim cannot be zero");
2295 }
2296
2297 #[test]
2298 fn err_norm_must_be_positive() {
2299 let data = Matrix::new(0.0f32, 10, 10);
2300 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2301 let err = SphericalQuantizer::train(
2302 data.as_view(),
2303 TransformKind::DoubleHadamard {
2304 target_dim: TargetDim::Same,
2305 },
2306 SupportedMetric::SquaredL2,
2307 PreScale::None,
2308 &mut rng,
2309 GlobalAllocator,
2310 )
2311 .unwrap_err();
2312 assert_eq!(err.to_string(), "norm must be positive");
2313 }
2314
2315 #[test]
2316 fn err_norm_cannot_be_infinity() {
2317 let mut data = Matrix::new(0.0f32, 10, 10);
2318 data[(2, 5)] = f32::INFINITY;
2319
2320 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2321 let err = SphericalQuantizer::train(
2322 data.as_view(),
2323 TransformKind::DoubleHadamard {
2324 target_dim: TargetDim::Same,
2325 },
2326 SupportedMetric::SquaredL2,
2327 PreScale::None,
2328 &mut rng,
2329 GlobalAllocator,
2330 )
2331 .unwrap_err();
2332 assert_eq!(err.to_string(), "computed norm contains infinity or NaN");
2333 }
2334
2335 #[test]
2336 fn err_reciprocal_norm_cannot_be_infinity() {
2337 let mut data = Matrix::new(0.0f32, 10, 10);
2338 data[(2, 5)] = 2.93863e-39;
2339
2340 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2341 let err = SphericalQuantizer::train(
2342 data.as_view(),
2343 TransformKind::DoubleHadamard {
2344 target_dim: TargetDim::Same,
2345 },
2346 SupportedMetric::SquaredL2,
2347 PreScale::ReciprocalMeanNorm,
2348 &mut rng,
2349 GlobalAllocator,
2350 )
2351 .unwrap_err();
2352 assert_eq!(err.to_string(), "reciprocal norm contains infinity or NaN");
2353 }
2354
2355 #[test]
2356 fn err_mean_norm_cannot_be_zero_generate() {
2357 let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2358 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2359 let err = SphericalQuantizer::generate(
2360 centroid,
2361 0.0,
2362 TransformKind::DoubleHadamard {
2363 target_dim: TargetDim::Same,
2364 },
2365 SupportedMetric::SquaredL2,
2366 None,
2367 &mut rng,
2368 GlobalAllocator,
2369 )
2370 .unwrap_err();
2371 assert_eq!(err.to_string(), "norm must be positive");
2372 }
2373
2374 #[test]
2375 fn err_scale_cannot_be_zero_generate() {
2376 let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2377 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2378 let err = SphericalQuantizer::generate(
2379 centroid,
2380 1.0,
2381 TransformKind::DoubleHadamard {
2382 target_dim: TargetDim::Same,
2383 },
2384 SupportedMetric::SquaredL2,
2385 Some(0.0),
2386 &mut rng,
2387 GlobalAllocator,
2388 )
2389 .unwrap_err();
2390 assert_eq!(err.to_string(), "pre-scale must be positive");
2391 }
2392
2393 #[test]
2394 fn compression_errors_data() {
2395 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2396 let data = Matrix::<f32>::new(views::Init(|| StandardNormal {}.sample(&mut rng)), 16, 12);
2397
2398 let quantizer = SphericalQuantizer::train(
2399 data.as_view(),
2400 TransformKind::PaddingHadamard {
2401 target_dim: TargetDim::Same,
2402 },
2403 SupportedMetric::SquaredL2,
2404 PreScale::None,
2405 &mut rng,
2406 GlobalAllocator,
2407 )
2408 .unwrap();
2409
2410 let scoped_global = ScopedAllocator::global();
2411
2412 {
2414 let mut query: Vec<f32> = quantizer.shift().to_vec();
2415 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2416 let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2417
2418 for i in 0..query.len() {
2419 let last = query[i];
2420 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2421 query[i] = v;
2422
2423 let err = quantizer
2424 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2425 .unwrap_err();
2426
2427 assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2428
2429 let err = quantizer
2430 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2431 .unwrap_err();
2432
2433 assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2434 }
2435 query[i] = last;
2436 }
2437 }
2438
2439 {
2441 let query: Vec<f32> = vec![1000000.0; quantizer.input_dim()];
2442 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2443
2444 let err = quantizer
2445 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2446 .unwrap_err();
2447
2448 let expected = "encoding error - you may need to scale the entire dataset to reduce its dynamic range";
2449
2450 assert_eq!(err.to_string(), expected, "failed for {:?}", query);
2451 }
2452
2453 for len in [quantizer.input_dim() - 1, quantizer.input_dim() + 1] {
2455 let query = vec![0.0f32; len];
2456 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2457 let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2458
2459 let err = quantizer
2460 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2461 .unwrap_err();
2462 assert_eq!(
2463 err,
2464 CompressionError::SourceDimensionMismatch {
2465 expected: quantizer.input_dim(),
2466 }
2467 );
2468
2469 let err = quantizer
2470 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2471 .unwrap_err();
2472 assert_eq!(
2473 err,
2474 CompressionError::SourceDimensionMismatch {
2475 expected: quantizer.input_dim(),
2476 }
2477 );
2478 }
2479
2480 for len in [quantizer.output_dim() - 1, quantizer.output_dim() + 1] {
2481 let query = vec![0.0f32; quantizer.input_dim()];
2482 let mut d = Data::<1, _>::new_boxed(len);
2483 let mut q = Query::<4, BitTranspose, _>::new_boxed(len);
2484
2485 let err = quantizer
2486 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2487 .unwrap_err();
2488 assert_eq!(
2489 err,
2490 CompressionError::DestinationDimensionMismatch {
2491 expected: quantizer.output_dim(),
2492 }
2493 );
2494
2495 let err = quantizer
2496 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2497 .unwrap_err();
2498 assert_eq!(
2499 err,
2500 CompressionError::DestinationDimensionMismatch {
2501 expected: quantizer.output_dim(),
2502 }
2503 );
2504 }
2505 }
2506
2507 #[test]
2508 fn centroid_scaling_happens_in_generate() {
2509 let centroid = Poly::from_iter(
2510 [1088.6732f32, 1393.32, 1547.877].into_iter(),
2511 GlobalAllocator,
2512 )
2513 .unwrap();
2514 let mean_norm = 2359.27;
2515 let pre_scale = 1.0 / mean_norm;
2516
2517 let quantizer = SphericalQuantizer::generate(
2518 centroid,
2519 mean_norm,
2520 TransformKind::Null,
2521 SupportedMetric::InnerProduct,
2522 Some(pre_scale),
2523 &mut StdRng::seed_from_u64(10),
2524 GlobalAllocator,
2525 )
2526 .unwrap();
2527
2528 let mut v = Data::<4, _>::new_boxed(quantizer.input_dim());
2529 let data: &[f32] = &[1000.34, 1456.32, 1234.5446];
2530 assert!(
2531 quantizer
2532 .compress_into_with(data, v.reborrow_mut(), ScopedAllocator::global())
2533 .is_ok(),
2534 "if this failed, the likely culprit is exceeding the value of the 16-bit correction terms"
2535 );
2536 }
2537}
2538
2539#[cfg(feature = "flatbuffers")]
2540#[cfg(test)]
2541mod test_serialization {
2542 use rand::{SeedableRng, rngs::StdRng};
2543
2544 use super::*;
2545 use crate::{
2546 algorithms::transforms::TargetDim,
2547 flatbuffers::{self as fb, to_flatbuffer},
2548 poly, test_util,
2549 };
2550
2551 #[test]
2552 fn test_serialization_happy_path() {
2553 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2554 let problem = test_util::create_test_problem(10, 128, &mut rng);
2555
2556 let low = NonZeroUsize::new(100).unwrap();
2557 let high = NonZeroUsize::new(150).unwrap();
2558
2559 let kinds = [
2560 TransformKind::Null,
2562 TransformKind::DoubleHadamard {
2564 target_dim: TargetDim::Same,
2565 },
2566 TransformKind::DoubleHadamard {
2567 target_dim: TargetDim::Natural,
2568 },
2569 TransformKind::DoubleHadamard {
2570 target_dim: TargetDim::Override(low),
2571 },
2572 TransformKind::DoubleHadamard {
2573 target_dim: TargetDim::Override(high),
2574 },
2575 TransformKind::PaddingHadamard {
2577 target_dim: TargetDim::Same,
2578 },
2579 TransformKind::PaddingHadamard {
2580 target_dim: TargetDim::Natural,
2581 },
2582 TransformKind::PaddingHadamard {
2583 target_dim: TargetDim::Override(low),
2584 },
2585 TransformKind::PaddingHadamard {
2586 target_dim: TargetDim::Override(high),
2587 },
2588 #[cfg(all(not(miri), feature = "linalg"))]
2590 TransformKind::RandomRotation {
2591 target_dim: TargetDim::Same,
2592 },
2593 #[cfg(all(not(miri), feature = "linalg"))]
2594 TransformKind::RandomRotation {
2595 target_dim: TargetDim::Natural,
2596 },
2597 #[cfg(all(not(miri), feature = "linalg"))]
2598 TransformKind::RandomRotation {
2599 target_dim: TargetDim::Override(low),
2600 },
2601 #[cfg(all(not(miri), feature = "linalg"))]
2602 TransformKind::RandomRotation {
2603 target_dim: TargetDim::Override(high),
2604 },
2605 ];
2606
2607 let pre_scales = [
2608 PreScale::None,
2609 PreScale::Some(Positive::new(0.5).unwrap()),
2610 PreScale::Some(Positive::new(1.0).unwrap()),
2611 PreScale::Some(Positive::new(1.5).unwrap()),
2612 PreScale::ReciprocalMeanNorm,
2613 ];
2614
2615 let alloc = GlobalAllocator;
2616 for kind in kinds.into_iter() {
2617 for metric in SupportedMetric::all() {
2618 for pre_scale in pre_scales {
2619 let quantizer = SphericalQuantizer::train(
2620 problem.data.as_view(),
2621 kind,
2622 metric,
2623 pre_scale,
2624 &mut rng,
2625 alloc,
2626 )
2627 .unwrap();
2628
2629 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2630 let proto =
2631 flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2632 let reloaded = SphericalQuantizer::try_unpack(alloc, proto).unwrap();
2633 assert_eq!(quantizer, reloaded, "failed on transform {:?}", kind);
2634 }
2635 }
2636 }
2637 }
2638
2639 #[test]
2640 fn test_error_checking() {
2641 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2642 let problem = test_util::create_test_problem(10, 128, &mut rng);
2643
2644 let transform = TransformKind::DoubleHadamard {
2645 target_dim: TargetDim::Same,
2646 };
2647
2648 let alloc = GlobalAllocator;
2649 let mut make_quantizer = || {
2650 SphericalQuantizer::train(
2651 problem.data.as_view(),
2652 transform,
2653 SupportedMetric::SquaredL2,
2654 PreScale::None,
2655 &mut rng,
2656 alloc,
2657 )
2658 .unwrap()
2659 };
2660
2661 type E = DeserializationError;
2662
2663 {
2665 let mut quantizer = make_quantizer();
2666 quantizer.mean_norm = unsafe { Positive::new_unchecked(0.0) };
2670
2671 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2672 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2673 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2674 assert_eq!(err, E::MissingNorm);
2675 }
2676
2677 {
2679 let mut quantizer = make_quantizer();
2680
2681 quantizer.mean_norm = unsafe { Positive::new_unchecked(-1.0) };
2685
2686 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2687 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2688 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2689 assert_eq!(err, E::MissingNorm);
2690 }
2691
2692 {
2694 let mut quantizer = make_quantizer();
2695
2696 quantizer.pre_scale = unsafe { Positive::new_unchecked(0.0) };
2699
2700 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2701 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2702 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2703 assert_eq!(err, E::PreScaleNotPositive);
2704 }
2705
2706 {
2708 let mut quantizer = make_quantizer();
2709 quantizer.shift = poly!([1.0, 2.0, 3.0], alloc).unwrap();
2710
2711 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2712 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2713 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2714 assert_eq!(err, E::DimMismatch);
2715 }
2716 }
2717}