1use std::num::NonZeroUsize;
7
8use diskann_utils::{
9 ReborrowMut,
10 views::rowmajor::{self, Matrix},
11};
12use diskann_vector::{
13 MathematicalValue, Norm, PureDistanceFunction, distance::InnerProduct, norm::FastL2Norm,
14};
15#[cfg(feature = "flatbuffers")]
16use flatbuffers::{FlatBufferBuilder, WIPOffset};
17use rand::{Rng, RngCore};
18use thiserror::Error;
19
20use super::{
21 CompensatedCosine, CompensatedIP, CompensatedSquaredL2, DataMeta, DataMetaError, DataMut,
22 FullQueryMeta, FullQueryMut, QueryMeta, QueryMut, SupportedMetric, iface,
23};
24use crate::{
25 AsFunctor, CompressIntoWith,
26 algorithms::{
27 heap::SliceHeap,
28 transforms::{NewTransformError, Transform, TransformFailed, TransformKind},
29 },
30 alloc::{Allocator, AllocatorError, GlobalAllocator, Poly, ScopedAllocator, TryClone},
31 bits::{PermutationStrategy, Representation, Unsigned},
32 num::Positive,
33 utils::{CannotBeEmpty, compute_means_and_average_norm, compute_normalized_means},
34};
35#[cfg(feature = "flatbuffers")]
36use crate::{
37 algorithms::transforms::TransformError, flatbuffers::spherical, spherical::InvalidMetric,
38};
39
40#[derive(Debug)]
45#[cfg_attr(test, derive(PartialEq))]
46pub struct SphericalQuantizer<A = GlobalAllocator>
47where
48 A: Allocator,
49{
50 shift: Poly<[f32], A>,
52
53 transform: Transform<A>,
61
62 metric: SupportedMetric,
64
65 mean_norm: Positive<f32>,
74
75 pre_scale: Positive<f32>,
81}
82
83impl<A> TryClone for SphericalQuantizer<A>
84where
85 A: Allocator,
86{
87 fn try_clone(&self) -> Result<Self, AllocatorError> {
88 SphericalQuantizer::try_clone(self)
89 }
90}
91
92#[derive(Debug, Clone, Copy, Error)]
93#[non_exhaustive]
94pub enum TrainError {
95 #[error("data dim cannot be zero")]
96 DimCannotBeZero,
97 #[error("data cannot be empty")]
98 DataCannotBeEmpty,
99 #[error("pre-scale must be positive")]
100 PrescaleNotPositive,
101 #[error("norm must be positive")]
102 NormNotPositive,
103 #[error("computed norm contains infinity or NaN")]
104 NormNotFinite,
105 #[error("reciprocal norm contains infinity or NaN")]
106 ReciprocalNormNotFinite,
107 #[error(transparent)]
108 AllocatorError(#[from] AllocatorError),
109}
110
111#[derive(Debug, Clone, Copy, Error, PartialEq)]
112#[non_exhaustive]
113pub enum CentroidDistanceError {
114 #[error("centroid distance is not finite")]
115 NonFiniteDistance,
116 #[error("expected centroid to have length {expected}")]
117 DimensionMismatch { expected: usize },
118}
119
120impl<A> SphericalQuantizer<A>
121where
122 A: Allocator,
123{
124 pub fn input_dim(&self) -> usize {
126 self.shift.len()
127 }
128
129 pub fn output_dim(&self) -> usize {
134 self.transform.output_dim()
135 }
136
137 pub fn shift(&self) -> &[f32] {
144 &self.shift
145 }
146
147 pub fn mean_norm(&self) -> Positive<f32> {
149 self.mean_norm
150 }
151
152 pub fn pre_scale(&self) -> Positive<f32> {
157 self.pre_scale
158 }
159
160 pub fn centroid_squared_distance(
172 &self,
173 centroid: &[f32],
174 ) -> Result<f32, CentroidDistanceError> {
175 if centroid.len() != self.input_dim() {
176 return Err(CentroidDistanceError::DimensionMismatch {
177 expected: self.input_dim(),
178 });
179 }
180
181 let stored_scale = match self.metric {
182 SupportedMetric::Cosine => 1.0,
183 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => {
184 self.pre_scale.into_inner()
185 }
186 };
187
188 let distance = centroid
189 .iter()
190 .zip(self.shift.iter())
191 .map(|(&candidate, &shift)| {
192 let training = shift / stored_scale;
193 let delta = candidate - training;
194 delta * delta
195 })
196 .sum::<f32>();
197
198 if !distance.is_finite() {
199 return Err(CentroidDistanceError::NonFiniteDistance);
200 }
201
202 Ok(distance)
203 }
204
205 pub fn allocator(&self) -> &A {
207 self.shift.allocator()
208 }
209
210 pub fn try_clone(&self) -> Result<Self, AllocatorError> {
212 Ok(Self {
213 shift: self.shift.try_clone()?,
214 transform: self.transform.try_clone()?,
215 metric: self.metric,
216 mean_norm: self.mean_norm,
217 pre_scale: self.pre_scale,
218 })
219 }
220
221 pub fn generate(
223 mut centroid: Poly<[f32], A>,
224 mean_norm: f32,
225 transform: TransformKind,
226 metric: SupportedMetric,
227 pre_scale: Option<f32>,
228 rng: &mut dyn RngCore,
229 allocator: A,
230 ) -> Result<Self, TrainError> {
231 let pre_scale = match pre_scale {
232 Some(v) => Positive::new(v).map_err(|_| TrainError::PrescaleNotPositive)?,
233 None => crate::num::POSITIVE_ONE_F32,
234 };
235
236 let dim = match NonZeroUsize::new(centroid.len()) {
237 Some(dim) => dim,
238 None => {
239 return Err(TrainError::DimCannotBeZero);
240 }
241 };
242
243 let mean_norm = Positive::new(mean_norm).map_err(|_| TrainError::NormNotPositive)?;
244
245 let transform = match Transform::new(transform, dim, Some(rng), allocator.clone()) {
247 Ok(v) => v,
248 Err(NewTransformError::RngMissing(_)) => unreachable!("An Rng was provided"),
249 Err(NewTransformError::AllocatorError(err)) => {
250 return Err(TrainError::AllocatorError(err));
251 }
252 };
253
254 if metric != SupportedMetric::Cosine {
257 centroid
258 .iter_mut()
259 .for_each(|v| *v *= pre_scale.into_inner());
260 }
261
262 Ok(SphericalQuantizer {
263 shift: centroid,
264 transform,
265 metric,
266 mean_norm,
267 pre_scale,
268 })
269 }
270
271 pub fn metric(&self) -> SupportedMetric {
273 self.metric
274 }
275
276 pub fn train<T, R>(
292 data: rowmajor::Ref<T>,
293 transform: TransformKind,
294 metric: SupportedMetric,
295 pre_scale: PreScale,
296 rng: &mut R,
297 allocator: A,
298 ) -> Result<Self, TrainError>
299 where
300 T: Copy + Into<f64> + Into<f32>,
301 R: Rng,
302 {
303 #[inline(never)]
306 fn train<T, A>(
307 data: rowmajor::Ref<T>,
308 transform: TransformKind,
309 metric: SupportedMetric,
310 pre_scale: PreScale,
311 rng: &mut dyn RngCore,
312 allocator: A,
313 ) -> Result<SphericalQuantizer<A>, TrainError>
314 where
315 T: Copy + Into<f64> + Into<f32>,
316 A: Allocator,
317 {
318 if data.ncols() == 0 {
321 return Err(TrainError::DimCannotBeZero);
322 }
323
324 let (centroid, mean_norm) = match metric {
325 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => {
326 compute_means_and_average_norm(data)
327 }
328 SupportedMetric::Cosine => (
329 compute_normalized_means(data)
330 .map_err(|_: CannotBeEmpty| TrainError::DataCannotBeEmpty)?,
331 1.0,
332 ),
333 };
334
335 let mean_norm = mean_norm as f32;
336
337 if mean_norm <= 0.0 {
338 return Err(TrainError::NormNotPositive);
339 }
340
341 if !mean_norm.is_finite() {
342 return Err(TrainError::NormNotFinite);
343 }
344
345 let pre_scale: Positive<f32> = match pre_scale {
347 PreScale::None => crate::num::POSITIVE_ONE_F32,
348 PreScale::Some(v) => v,
349 PreScale::ReciprocalMeanNorm => {
350 let pre_scale = Positive::new(1.0 / mean_norm)
359 .map_err(|_| TrainError::ReciprocalNormNotFinite)?;
360
361 if !pre_scale.into_inner().is_finite() {
362 return Err(TrainError::ReciprocalNormNotFinite);
363 }
364
365 pre_scale
366 }
367 };
368
369 let centroid =
371 Poly::from_iter(centroid.into_iter().map(|i| i as f32), allocator.clone())?;
372
373 SphericalQuantizer::generate(
374 centroid,
375 mean_norm,
376 transform,
377 metric,
378 Some(pre_scale.into_inner()),
379 rng,
380 allocator,
381 )
382 }
383
384 train(data, transform, metric, pre_scale, rng, allocator)
385 }
386
387 pub fn rescale(&self, v: &mut [f32]) {
389 let norm = FastL2Norm.evaluate(&*v);
390 if norm != 0.0 {
391 let m = self.mean_norm.into_inner() / norm;
392 v.iter_mut().for_each(|i| *i *= m);
393 }
394 }
395
396 fn preprocess<'a>(
402 &self,
403 data: &[f32],
404 allocator: ScopedAllocator<'a>,
405 ) -> Result<Preprocessed<'a>, CompressionError> {
406 assert_eq!(data.len(), self.input_dim(), "Data dimension is incorrect.");
407
408 let scale = self.pre_scale.into_inner();
413 let mul: f32 = match self.metric {
414 SupportedMetric::Cosine => {
415 let norm: f32 = (FastL2Norm).evaluate(data);
416 if norm == 0.0 { 1.0 } else { 1.0 / norm }
417 }
418 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => scale,
419 };
420
421 let shifted = Poly::from_iter(
423 std::iter::zip(data.iter(), self.shift.iter()).map(|(&f, &s)| mul * f - s),
424 allocator,
425 )?;
426
427 let shifted_norm = FastL2Norm.evaluate(&*shifted);
428 if !shifted_norm.is_finite() {
429 return Err(CompressionError::InputContainsNaN);
430 }
431 let inner_product_with_centroid = match self.metric {
432 SupportedMetric::SquaredL2 => None,
433 SupportedMetric::InnerProduct | SupportedMetric::Cosine => {
434 let ip: MathematicalValue<f32> = InnerProduct::evaluate(&*shifted, &*self.shift);
435 Some(ip.into_inner())
436 }
437 };
438
439 Ok(Preprocessed {
440 shifted,
441 shifted_norm,
442 inner_product_with_centroid,
443 })
444 }
445
446 pub fn as_quantizer_impl<const NBITS: usize>(
448 self,
449 ) -> Result<iface::Impl<NBITS, A>, AllocatorError>
450 where
451 A: 'static,
452 iface::Impl<NBITS, A>: iface::Constructible<A> + iface::Quantizer,
453 {
454 iface::Impl::<NBITS, A>::new(self)
455 }
456
457 pub fn as_quantizer<const NBITS: usize>(
459 self,
460 ) -> Result<Poly<dyn iface::Quantizer>, AllocatorError>
461 where
462 A: 'static,
463 iface::Impl<NBITS, A>: iface::Constructible<A> + iface::Quantizer,
464 {
465 let iface = self.as_quantizer_impl()?;
466 crate::poly!({ iface::Quantizer }, iface, GlobalAllocator)
467 }
468}
469
470#[derive(Debug, Clone, Copy)]
474pub enum PreScale {
475 None,
477 Some(Positive<f32>),
479 ReciprocalMeanNorm,
482}
483
484#[cfg(feature = "flatbuffers")]
485#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
486#[derive(Debug, Clone, Error, PartialEq)]
487#[non_exhaustive]
488pub enum DeserializationError {
489 #[error(transparent)]
490 TransformError(#[from] TransformError),
491 #[error("unrecognized flatbuffer identifier")]
492 UnrecognizedIdentifier,
493 #[error("transform length not equal to centroid")]
494 DimMismatch,
495 #[error("norm is missing or is not positive")]
496 MissingNorm,
497 #[error("pre-scale is missing or is not positive")]
498 PreScaleNotPositive,
499
500 #[error(transparent)]
501 InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
502
503 #[error(transparent)]
504 InvalidMetric(#[from] InvalidMetric),
505
506 #[error(transparent)]
507 AllocatorError(#[from] AllocatorError),
508}
509
510#[cfg(feature = "flatbuffers")]
511#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
512impl<A> SphericalQuantizer<A>
513where
514 A: Allocator + Clone,
515{
516 pub(crate) fn pack<'a, FA>(
519 &self,
520 buf: &mut FlatBufferBuilder<'a, FA>,
521 ) -> WIPOffset<spherical::SphericalQuantizer<'a>>
522 where
523 FA: flatbuffers::Allocator + 'a,
524 {
525 let centroid = buf.create_vector(&self.shift);
527
528 let transform = self.transform.pack(buf);
530
531 spherical::SphericalQuantizer::create(
533 buf,
534 &spherical::SphericalQuantizerArgs {
535 centroid: Some(centroid),
536 transform: Some(transform),
537 metric: self.metric.into(),
538 mean_norm: self.mean_norm.into_inner(),
539 pre_scale: self.pre_scale.into_inner(),
540 },
541 )
542 }
543
544 pub(crate) fn try_unpack(
547 alloc: A,
548 proto: spherical::SphericalQuantizer<'_>,
549 ) -> Result<Self, DeserializationError> {
550 let metric: SupportedMetric = proto.metric().try_into()?;
551
552 let shift = Poly::from_iter(proto.centroid().into_iter(), alloc.clone())?;
554
555 let transform = Transform::try_unpack(alloc, proto.transform())?;
557
558 if shift.len() != transform.input_dim() {
560 return Err(DeserializationError::DimMismatch);
561 }
562
563 let mean_norm =
565 Positive::new(proto.mean_norm()).map_err(|_| DeserializationError::MissingNorm)?;
566
567 let pre_scale = Positive::new(proto.pre_scale())
568 .map_err(|_| DeserializationError::PreScaleNotPositive)?;
569
570 Ok(Self {
571 shift,
572 transform,
573 metric,
574 mean_norm,
575 pre_scale,
576 })
577 }
578}
579
580struct Preprocessed<'a> {
581 shifted: Poly<[f32], ScopedAllocator<'a>>,
582 shifted_norm: f32,
583 inner_product_with_centroid: Option<f32>,
584}
585
586impl Preprocessed<'_> {
587 fn metric_specific(&self) -> f32 {
592 match self.inner_product_with_centroid {
593 Some(ip) => ip,
594 None => self.shifted_norm * self.shifted_norm,
595 }
596 }
597}
598
599impl<A> AsFunctor<CompensatedSquaredL2> for SphericalQuantizer<A>
604where
605 A: Allocator,
606{
607 fn as_functor(&self) -> CompensatedSquaredL2 {
608 CompensatedSquaredL2::new(self.output_dim())
609 }
610}
611
612impl<A> AsFunctor<CompensatedIP> for SphericalQuantizer<A>
613where
614 A: Allocator,
615{
616 fn as_functor(&self) -> CompensatedIP {
617 CompensatedIP::new(&self.shift, self.output_dim())
618 }
619}
620
621impl<A> AsFunctor<CompensatedCosine> for SphericalQuantizer<A>
622where
623 A: Allocator,
624{
625 fn as_functor(&self) -> CompensatedCosine {
626 CompensatedCosine::new(self.as_functor())
627 }
628}
629
630#[derive(Debug, Error, Clone, Copy, PartialEq)]
635#[non_exhaustive]
636pub enum CompressionError {
637 #[error("input contains NaN")]
638 InputContainsNaN,
639
640 #[error("expected source vector to have length {expected}")]
641 SourceDimensionMismatch { expected: usize },
642
643 #[error("expected destination vector to have length {expected}")]
644 DestinationDimensionMismatch { expected: usize },
645
646 #[error(
647 "encoding error - you may need to scale the entire dataset to reduce its dynamic range"
648 )]
649 EncodingError(#[from] DataMetaError),
650
651 #[error(transparent)]
652 AllocatorError(#[from] AllocatorError),
653}
654
655fn check_dims(
656 input: usize,
657 output: usize,
658 from: usize,
659 into: usize,
660) -> Result<(), CompressionError> {
661 if from != input {
662 return Err(CompressionError::SourceDimensionMismatch { expected: input });
663 }
664 if into != output {
665 return Err(CompressionError::DestinationDimensionMismatch { expected: output });
666 }
667 Ok(())
668}
669
670trait FinishCompressing {
673 fn finish_compressing(
674 &mut self,
675 preprocessed: &Preprocessed<'_>,
676 transformed: &[f32],
677 transformed_norm: f32,
678 allocator: ScopedAllocator<'_>,
679 ) -> Result<(), CompressionError>;
680}
681
682impl FinishCompressing for DataMut<'_, 1> {
683 fn finish_compressing(
684 &mut self,
685 preprocessed: &Preprocessed<'_>,
686 transformed: &[f32],
687 transformed_norm: f32,
688 _: ScopedAllocator<'_>,
689 ) -> Result<(), CompressionError> {
690 let mut quant_raw_inner_product = 0.0f32;
693 let mut bit_sum = 0u32;
694 transformed.iter().enumerate().for_each(|(i, &r)| {
695 let bit: u8 = if r > 0.0 { 1 } else { 0 };
696
697 quant_raw_inner_product += r.abs();
698 bit_sum += <u8 as Into<u32>>::into(bit);
699
700 unsafe { self.vector_mut().set_unchecked(i, bit) };
702 });
703
704 let inner_product_correction =
725 2.0 * transformed_norm * preprocessed.shifted_norm / quant_raw_inner_product;
726 self.set_meta(DataMeta::new(
727 inner_product_correction,
728 preprocessed.metric_specific(),
729 bit_sum,
730 )?);
731 Ok(())
732 }
733}
734
735impl FinishCompressing for DataMut<'_, 2> {
736 fn finish_compressing(
737 &mut self,
738 preprocessed: &Preprocessed<'_>,
739 transformed: &[f32],
740 transformed_norm: f32,
741 allocator: ScopedAllocator<'_>,
742 ) -> Result<(), CompressionError> {
743 compress_via_maximum_cosine(
744 self.reborrow_mut(),
745 preprocessed,
746 transformed,
747 transformed_norm,
748 allocator,
749 )
750 }
751}
752
753impl FinishCompressing for DataMut<'_, 3> {
754 fn finish_compressing(
755 &mut self,
756 preprocessed: &Preprocessed<'_>,
757 transformed: &[f32],
758 transformed_norm: f32,
759 allocator: ScopedAllocator<'_>,
760 ) -> Result<(), CompressionError> {
761 compress_via_maximum_cosine(
762 self.reborrow_mut(),
763 preprocessed,
764 transformed,
765 transformed_norm,
766 allocator,
767 )
768 }
769}
770
771impl FinishCompressing for DataMut<'_, 4> {
772 fn finish_compressing(
773 &mut self,
774 preprocessed: &Preprocessed<'_>,
775 transformed: &[f32],
776 transformed_norm: f32,
777 allocator: ScopedAllocator<'_>,
778 ) -> Result<(), CompressionError> {
779 compress_via_maximum_cosine(
780 self.reborrow_mut(),
781 preprocessed,
782 transformed,
783 transformed_norm,
784 allocator,
785 )
786 }
787}
788
789impl FinishCompressing for DataMut<'_, 8> {
790 fn finish_compressing(
791 &mut self,
792 preprocessed: &Preprocessed<'_>,
793 transformed: &[f32],
794 transformed_norm: f32,
795 allocator: ScopedAllocator<'_>,
796 ) -> Result<(), CompressionError> {
797 compress_via_maximum_cosine(
798 self.reborrow_mut(),
799 preprocessed,
800 transformed,
801 transformed_norm,
802 allocator,
803 )
804 }
805}
806
807impl<A> CompressIntoWith<&[f32], FullQueryMut<'_>, ScopedAllocator<'_>> for SphericalQuantizer<A>
812where
813 A: Allocator,
814{
815 type Error = CompressionError;
816
817 fn compress_into_with(
830 &self,
831 from: &[f32],
832 mut into: FullQueryMut<'_>,
833 allocator: ScopedAllocator<'_>,
834 ) -> Result<(), Self::Error> {
835 let input_dim = self.shift.len();
836 let output_dim = self.output_dim();
837 check_dims(input_dim, output_dim, from.len(), into.len())?;
838
839 let mut preprocessed = self.preprocess(from, allocator)?;
840
841 if preprocessed.shifted_norm == 0.0 {
844 into.vector_mut().fill(0.0);
845 *into.meta_mut() = Default::default();
846 return Ok(());
847 }
848
849 preprocessed
850 .shifted
851 .iter_mut()
852 .for_each(|v| *v /= preprocessed.shifted_norm);
853
854 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
859 match self
860 .transform
861 .transform_into(into.vector_mut(), &preprocessed.shifted, allocator)
862 {
863 Ok(()) => {}
864 Err(TransformFailed::AllocatorError(err)) => {
865 return Err(CompressionError::AllocatorError(err));
866 }
867 Err(TransformFailed::SourceMismatch { .. })
868 | Err(TransformFailed::DestinationMismatch { .. }) => {
869 panic!(
870 "The sizes of these arrays should already be checked - this is a logic error"
871 );
872 }
873 #[cfg(feature = "linalg")]
874 Err(TransformFailed::SgemmError(_)) => {
875 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
876 }
877 }
878
879 *into.meta_mut() = FullQueryMeta {
880 sum: into.vector().iter().sum::<f32>(),
881 shifted_norm: preprocessed.shifted_norm,
882 metric_specific: preprocessed.metric_specific(),
883 };
884 Ok(())
885 }
886}
887
888impl<const NBITS: usize, A> CompressIntoWith<&[f32], DataMut<'_, NBITS>, ScopedAllocator<'_>>
889 for SphericalQuantizer<A>
890where
891 A: Allocator,
892 Unsigned: Representation<NBITS>,
893 for<'a> DataMut<'a, NBITS>: FinishCompressing,
894{
895 type Error = CompressionError;
896
897 fn compress_into_with(
910 &self,
911 from: &[f32],
912 mut into: DataMut<'_, NBITS>,
913 allocator: ScopedAllocator<'_>,
914 ) -> Result<(), Self::Error> {
915 let input_dim = self.shift.len();
916 let output_dim = self.output_dim();
917 check_dims(input_dim, output_dim, from.len(), into.len())?;
918
919 let mut preprocessed = self.preprocess(from, allocator)?;
920
921 if preprocessed.shifted_norm == 0.0 {
922 into.set_meta(DataMeta::default());
923 return Ok(());
924 }
925
926 let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
927 preprocessed
928 .shifted
929 .iter_mut()
930 .for_each(|v| *v /= preprocessed.shifted_norm);
931
932 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
937 match self
938 .transform
939 .transform_into(&mut transformed, &preprocessed.shifted, allocator)
940 {
941 Ok(()) => {}
942 Err(TransformFailed::AllocatorError(err)) => {
943 return Err(CompressionError::AllocatorError(err));
944 }
945 Err(TransformFailed::SourceMismatch { .. })
946 | Err(TransformFailed::DestinationMismatch { .. }) => {
947 panic!(
948 "The sizes of these arrays should already be checked - this is a logic error"
949 );
950 }
951 #[cfg(feature = "linalg")]
952 Err(TransformFailed::SgemmError(_)) => {
953 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
954 }
955 }
956
957 let transformed_norm = if self.transform.preserves_norms() {
958 1.0
959 } else {
960 (FastL2Norm).evaluate(&*transformed)
961 };
962
963 into.finish_compressing(&preprocessed, &transformed, transformed_norm, allocator)?;
964 Ok(())
965 }
966}
967
968struct AsNonZero<const NBITS: usize>;
969impl<const NBITS: usize> AsNonZero<NBITS> {
970 const NON_ZERO: NonZeroUsize = NonZeroUsize::new(NBITS).unwrap();
971}
972
973fn compress_via_maximum_cosine<const NBITS: usize>(
974 mut data: DataMut<'_, NBITS>,
975 preprocessed: &Preprocessed<'_>,
976 transformed: &[f32],
977 transformed_norm: f32,
978 allocator: ScopedAllocator<'_>,
979) -> Result<(), CompressionError>
980where
981 Unsigned: Representation<NBITS>,
982{
983 assert_eq!(data.len(), transformed.len());
984
985 let optimal_scale =
988 maximize_cosine_similarity(transformed, AsNonZero::<NBITS>::NON_ZERO, allocator)?;
989
990 let domain = Unsigned::domain_const::<NBITS>();
991 let min = *domain.start() as f32;
992 let max = *domain.end() as f32;
993 let offset = max / 2.0;
994
995 let mut self_inner_product = 0.0f32;
996 let mut bit_sum = 0u32;
997 for (i, t) in transformed.iter().enumerate() {
998 let v = (*t * optimal_scale + offset).clamp(min, max).round();
999 let dv = v - offset;
1000 self_inner_product = dv.mul_add(*t, self_inner_product);
1001
1002 let v = v as u8;
1003 bit_sum += <u8 as Into<u32>>::into(v);
1004
1005 unsafe { data.vector_mut().set_unchecked(i, v) };
1010 }
1011
1012 let shifted_norm = preprocessed.shifted_norm;
1013 let inner_product_correction = (transformed_norm * shifted_norm) / self_inner_product;
1014 data.set_meta(DataMeta::new(
1015 inner_product_correction,
1016 preprocessed.metric_specific(),
1017 bit_sum,
1018 )?);
1019 Ok(())
1020}
1021
1022#[derive(Debug, Clone, Copy)]
1036struct Pair {
1037 value: f32,
1038 position: u32,
1039}
1040
1041impl PartialEq for Pair {
1042 fn eq(&self, other: &Self) -> bool {
1043 self.value.to_bits().eq(&other.value.to_bits())
1044 }
1045}
1046
1047impl Eq for Pair {}
1048impl PartialOrd for Pair {
1049 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1050 Some(self.cmp(other))
1051 }
1052}
1053impl Ord for Pair {
1054 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1055 other.value.to_bits().cmp(&self.value.to_bits())
1056 }
1057}
1058
1059fn maximize_cosine_similarity(
1095 v: &[f32],
1096 num_bits: NonZeroUsize,
1097 allocator: ScopedAllocator<'_>,
1098) -> Result<f32, CompressionError> {
1099 let mut current_ip = 0.5 * v.iter().map(|i| i.abs() as f64).sum::<f64>();
1104 if current_ip.is_nan() {
1105 return Err(CompressionError::InputContainsNaN);
1106 }
1107 let mut current_square_norm = 0.25 * (v.len() as f64);
1108
1109 let mut rounded = Poly::broadcast(1u16, v.len(), allocator)?;
1113
1114 let eps = 0.0001f32;
1120 let one_and_change = 1.0 + eps;
1121 let mut base = Poly::from_iter(
1122 v.iter().enumerate().map(|(position, value)| {
1123 let value = one_and_change / value.abs();
1124 Pair {
1125 value,
1126 position: position as u32,
1127 }
1128 }),
1129 allocator,
1130 )?;
1131
1132 #[expect(clippy::expect_used)]
1135 let mut critical_values =
1136 SliceHeap::new(&mut base).expect("calling code should not allow the slice to be empty");
1137
1138 let mut max_similarity = f64::NEG_INFINITY;
1139 let mut optimal_scale = f32::default();
1140 let stop = (2usize).pow(num_bits.get() as u32 - 1) as u16;
1141
1142 loop {
1143 let mut should_break = false;
1144 critical_values.update_root(|pair| {
1145 let Pair { value, position } = *pair;
1146 if value == f32::MAX {
1147 should_break = true;
1148 return;
1149 }
1150
1151 let r = &mut rounded[position as usize];
1152 let vp = &v[position as usize];
1153
1154 let old_r = *r;
1155 *r += 1;
1158
1159 current_ip += vp.abs() as f64;
1164
1165 current_square_norm += (2 * old_r) as f64;
1179
1180 let similarity = current_ip / current_square_norm.sqrt();
1182 if similarity > max_similarity {
1183 max_similarity = similarity;
1184 optimal_scale = value;
1185 }
1186
1187 if *r < stop {
1189 *pair = Pair {
1190 value: (*r as f32 + eps) / vp.abs(),
1191 position,
1192 };
1193 } else {
1194 *pair = Pair {
1195 value: f32::MAX,
1196 position,
1197 };
1198 }
1199 });
1200 if should_break {
1201 break;
1202 }
1203 }
1204
1205 Ok(optimal_scale)
1206}
1207
1208impl<const NBITS: usize, Perm, A>
1213 CompressIntoWith<&[f32], QueryMut<'_, NBITS, Perm>, ScopedAllocator<'_>>
1214 for SphericalQuantizer<A>
1215where
1216 Unsigned: Representation<NBITS>,
1217 Perm: PermutationStrategy<NBITS>,
1218 A: Allocator,
1219{
1220 type Error = CompressionError;
1221
1222 fn compress_into_with(
1235 &self,
1236 from: &[f32],
1237 mut into: QueryMut<'_, NBITS, Perm>,
1238 allocator: ScopedAllocator<'_>,
1239 ) -> Result<(), Self::Error> {
1240 let input_dim = self.shift.len();
1241 let output_dim = self.output_dim();
1242 check_dims(input_dim, output_dim, from.len(), into.len())?;
1243
1244 let mut preprocessed = self.preprocess(from, allocator)?;
1245
1246 if preprocessed.shifted_norm == 0.0 {
1247 into.set_meta(QueryMeta::default());
1248 return Ok(());
1249 }
1250
1251 preprocessed
1252 .shifted
1253 .iter_mut()
1254 .for_each(|v| *v /= preprocessed.shifted_norm);
1255
1256 let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
1257
1258 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
1263 match self
1264 .transform
1265 .transform_into(&mut transformed, &preprocessed.shifted, allocator)
1266 {
1267 Ok(()) => {}
1268 Err(TransformFailed::AllocatorError(err)) => {
1269 return Err(CompressionError::AllocatorError(err));
1270 }
1271 Err(TransformFailed::SourceMismatch { .. })
1272 | Err(TransformFailed::DestinationMismatch { .. }) => {
1273 panic!(
1274 "The sizes of these arrays should already be checked - this is a logic error"
1275 );
1276 }
1277 #[cfg(feature = "linalg")]
1278 Err(TransformFailed::SgemmError(_)) => {
1279 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
1280 }
1281 }
1282
1283 let (min, max) = transformed
1285 .iter()
1286 .fold((f32::MAX, f32::MIN), |(min, max), i| {
1287 (i.min(min), i.max(max))
1288 });
1289
1290 let domain = Unsigned::domain_const::<NBITS>();
1291 let lo = (*domain.start()) as f32;
1292 let hi = (*domain.end()) as f32;
1293
1294 let scale = (max - min) / hi;
1295 let mut bit_sum: f32 = 0.0;
1296 transformed.iter().enumerate().for_each(|(i, v)| {
1297 let c = ((v - min) / scale).round().clamp(lo, hi);
1298 bit_sum += c;
1299
1300 #[expect(clippy::unwrap_used)]
1306 into.vector_mut().set(i, c as i64).unwrap();
1307 });
1308
1309 into.set_meta(QueryMeta {
1311 inner_product_correction: preprocessed.shifted_norm * scale,
1312 bit_sum,
1313 offset: min / scale,
1314 metric_specific: preprocessed.metric_specific(),
1315 });
1316
1317 Ok(())
1318 }
1319}
1320
1321#[cfg(not(miri))]
1326#[cfg(test)]
1327mod tests {
1328 use super::*;
1329
1330 use std::fmt::Display;
1331
1332 use diskann_utils::{ReborrowMut, lazy_format, views::rowmajor::MatrixMut};
1333 use diskann_vector::{PureDistanceFunction, norm::FastL2NormSquared};
1334 use diskann_wide::ARCH;
1335 use rand::{
1336 SeedableRng,
1337 distr::{Distribution, Uniform},
1338 rngs::StdRng,
1339 };
1340 use rand_distr::StandardNormal;
1341
1342 use crate::{
1343 algorithms::transforms::TargetDim,
1344 alloc::GlobalAllocator,
1345 bits::{BitTranspose, Dense},
1346 spherical::{Data, DataMetaF32, FullQuery, Query},
1347 test_util,
1348 };
1349
1350 #[test]
1351 fn cosine_similarity_maximizer_errors() {
1352 let num_bits = NonZeroUsize::new(4).unwrap();
1353 for nan in [f32::NAN, f32::from_bits(0xffc0_0001)] {
1354 for position in 0..4 {
1355 let mut target = [1.0; 4];
1356 target[position] = nan;
1357 assert_eq!(
1358 maximize_cosine_similarity(
1359 &target,
1360 num_bits,
1361 ScopedAllocator::new(&test_util::AlwaysFails),
1362 ),
1363 Err(CompressionError::InputContainsNaN),
1364 );
1365 }
1366 }
1367 for budget in 0..=2 {
1368 let result = maximize_cosine_similarity(
1369 &[1.0, 0.5],
1370 num_bits,
1371 ScopedAllocator::new(&test_util::LimitedAllocator::new(budget)),
1372 );
1373 if budget < 2 {
1374 assert_eq!(
1375 result,
1376 Err(CompressionError::AllocatorError(AllocatorError))
1377 );
1378 } else {
1379 assert!(result.is_ok());
1380 }
1381 }
1382 }
1383
1384 #[test]
1385 fn test_cosine_similarity_maximizer() {
1386 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
1387 let num_trials = 10000;
1388 let num_bits = NonZeroUsize::new(3).unwrap();
1389
1390 let scale_distribution = Uniform::new(0.5f32, 10.0f32).unwrap();
1391
1392 let run_test = |target: [f32; 4]| {
1393 let scale =
1394 maximize_cosine_similarity(&target, num_bits, ScopedAllocator::global()).unwrap();
1395
1396 let mut best: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
1397 let mut best_similarity: f32 = f32::NEG_INFINITY;
1398
1399 let min = -3.5;
1402 for i0 in 0..8 {
1403 for i1 in 0..8 {
1404 for i2 in 0..8 {
1405 for i3 in 0..8 {
1406 let p: [f32; 4] = [
1407 min + (i0 as f32),
1408 min + (i1 as f32),
1409 min + (i2 as f32),
1410 min + (i3 as f32),
1411 ];
1412
1413 let sim: MathematicalValue<f32> =
1414 diskann_vector::distance::Cosine::evaluate(&p, &target);
1415 let sim = sim.into_inner();
1416 if sim > best_similarity {
1417 best_similarity = sim;
1418 best = p.map(|i| i - min);
1420 }
1421 }
1422 }
1423 }
1424 }
1425
1426 let clamped = target.map(|i| (i * scale - min).round().clamp(0.0, 7.0));
1429 let clamped_cosine: MathematicalValue<f32> =
1430 diskann_vector::distance::Cosine::evaluate(&clamped.map(|i| i + min), &target);
1431
1432 let passed = if best == clamped
1435 || (best_similarity == clamped_cosine.into_inner()
1436 && std::iter::zip(target, std::iter::zip(best, clamped))
1437 .all(|(input, (expected, actual))| input == 0.0 || expected == actual))
1438 {
1439 true
1440 } else {
1441 let ratio: Vec<f32> = std::iter::zip(best, clamped)
1442 .map(|(b, c)| {
1443 let ratio = (b + min) / (c + min);
1444 assert_ne!(
1445 ratio, 0.0,
1446 "ratio should never be zero because `b` is an integer and \
1447 `min` is not"
1448 );
1449 ratio
1450 })
1451 .collect();
1452
1453 ratio.iter().all(|i| *i == ratio[0])
1454 };
1455
1456 if !passed {
1457 panic!(
1458 "failed for input {:?}.\
1459 Best = {:?}, Found = {:?}\
1460 Best similarity = {}, similarity with clamped = {}",
1461 target,
1462 best,
1463 clamped,
1464 best_similarity,
1465 clamped_cosine.into_inner()
1466 );
1467 }
1468 };
1469
1470 for target in [
1471 [1.0, 0.0, -0.0, -1.0],
1472 [0.0, 0.5, -0.5, 0.0],
1473 [1.0, 1.0, -1.0, -1.0],
1474 [
1475 f32::from_bits(1.0f32.to_bits() - 1),
1476 1.0,
1477 f32::from_bits(1.0f32.to_bits() + 1),
1478 -1.0,
1479 ],
1480 ] {
1481 run_test(target);
1482 }
1483
1484 let min = -3.5;
1486 for i0 in (0..8).step_by(2) {
1487 for i1 in (1..9).step_by(2) {
1488 for i2 in (0..8).step_by(2) {
1489 for i3 in (1..9).step_by(2) {
1490 let p: [f32; 4] = [
1491 min + (i0 as f32),
1492 min + (i1 as f32),
1493 min + (i2 as f32),
1494 min + (i3 as f32),
1495 ];
1496 run_test(p)
1497 }
1498 }
1499 }
1500 }
1501
1502 for _ in 0..num_trials {
1503 let this_scale: f32 = scale_distribution.sample(&mut rng);
1504 let v: [f32; 4] = [(); 4].map(|_| {
1505 let v: f32 = StandardNormal {}.sample(&mut rng);
1506 this_scale * v
1507 });
1508 run_test(v);
1509 }
1510 }
1511
1512 #[test]
1513 #[should_panic(expected = "calling code should not allow the slice to be empty")]
1514 fn empty_slice_panics() {
1515 maximize_cosine_similarity(
1516 &[],
1517 NonZeroUsize::new(4).unwrap(),
1518 ScopedAllocator::global(),
1519 )
1520 .unwrap();
1521 }
1522
1523 struct Setup {
1524 transform: TransformKind,
1525 nrows: usize,
1526 ncols: usize,
1527 num_trials: usize,
1528 }
1529
1530 fn get_scale(scale: PreScale, quantizer: &SphericalQuantizer) -> f32 {
1531 match scale {
1532 PreScale::None => 1.0,
1533 PreScale::Some(v) => v.into_inner(),
1534 PreScale::ReciprocalMeanNorm => 1.0 / quantizer.mean_norm().into_inner(),
1535 }
1536 }
1537
1538 fn test_l2<const Q: usize, const D: usize, Perm>(
1539 setup: &Setup,
1540 problem: &test_util::TestProblem,
1541 computed_means: &[f32],
1542 pre_scale: PreScale,
1543 rng: &mut StdRng,
1544 ) where
1545 Unsigned: Representation<Q>,
1546 Unsigned: Representation<D>,
1547 Perm: PermutationStrategy<Q>,
1548 for<'a> SphericalQuantizer:
1549 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1550 for<'a> SphericalQuantizer:
1551 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1552 {
1553 assert_eq!(setup.nrows, problem.data.nrows());
1554 assert_eq!(setup.ncols, problem.data.ncols());
1555
1556 let scoped_global = ScopedAllocator::global();
1557 let distribution = Uniform::new(0, setup.nrows).unwrap();
1558 let quantizer = SphericalQuantizer::train(
1559 problem.data.as_view(),
1560 setup.transform,
1561 SupportedMetric::SquaredL2,
1562 pre_scale,
1563 rng,
1564 GlobalAllocator,
1565 )
1566 .unwrap();
1567
1568 let scale = get_scale(pre_scale, &quantizer);
1569
1570 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1571 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1572 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1573
1574 assert_eq!(
1575 quantizer.mean_norm.into_inner(),
1576 problem.mean_norm as f32,
1577 "computed mean norm should not apply scale"
1578 );
1579 let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1580 assert_eq!(&*scaled_means, quantizer.shift());
1581
1582 let l2: CompensatedSquaredL2 = quantizer.as_functor();
1583 assert_eq!(l2.dim, quantizer.output_dim() as f32);
1584
1585 for _ in 0..setup.num_trials {
1586 let i = distribution.sample(rng);
1587 let v = problem.data.row(i);
1588
1589 quantizer
1590 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1591 .unwrap();
1592 quantizer
1593 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1594 .unwrap();
1595 quantizer
1596 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1597 .unwrap();
1598
1599 let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1600 .map(|(a, b)| scale * a - b)
1601 .collect();
1602
1603 {
1605 let DataMetaF32 {
1606 inner_product_correction,
1607 bit_sum,
1608 metric_specific,
1609 } = b.meta().to_full(ARCH);
1610
1611 let shifted_square_norm = metric_specific;
1612
1613 let bv = b.vector();
1615 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1616 assert_eq!(s, bit_sum as usize);
1617
1618 {
1620 let expected = FastL2NormSquared.evaluate(&*shifted);
1621 let err = (shifted_square_norm - expected).abs() / expected.abs();
1622 assert!(
1623 err < 5.0e-4, "failed diff check, got {}, expected {} - relative error = {}",
1625 shifted_square_norm,
1626 expected,
1627 err
1628 );
1629 }
1630
1631 if const { D == 1 } {
1634 let self_inner_product = 2.0 * shifted_square_norm.sqrt()
1635 / (inner_product_correction * (bv.len() as f32).sqrt());
1636 assert!(
1637 (self_inner_product - 0.8).abs() < 0.13,
1638 "self inner-product should be close to 0.8. Instead, it's {}",
1639 self_inner_product
1640 );
1641 }
1642 }
1643
1644 {
1645 let QueryMeta {
1646 inner_product_correction,
1647 bit_sum,
1648 offset,
1649 metric_specific,
1650 } = q.meta();
1651
1652 let shifted_square_norm = metric_specific;
1653 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1654 preprocessed
1655 .shifted
1656 .iter_mut()
1657 .for_each(|i| *i /= preprocessed.shifted_norm);
1658
1659 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1660 quantizer
1661 .transform
1662 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1663 .unwrap();
1664
1665 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1666 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1667
1668 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1669
1670 {
1672 let expected = FastL2NormSquared.evaluate(&*shifted);
1673 let err = (shifted_square_norm - expected).abs() / expected.abs();
1674 assert!(
1675 err < 2e-7,
1676 "failed diff check, got {}, expected {} - relative error = {}",
1677 shifted_square_norm,
1678 expected,
1679 err
1680 );
1681 }
1682
1683 {
1685 let expected = shifted_square_norm.sqrt() * scale;
1686 let got = inner_product_correction;
1687
1688 let err = (expected - got).abs();
1689 assert!(
1690 err < 1.0e-7,
1691 "\"innerproduct_scale\": expected {}, got {}, error = {}",
1692 expected,
1693 got,
1694 err
1695 );
1696 }
1697
1698 {
1700 let expected = min / scale;
1701 let got = offset;
1702
1703 let err = (expected - got).abs();
1704 assert!(
1705 err < 1.0e-7,
1706 "\"sum_scale\": expected {}, got {}, error = {}",
1707 expected,
1708 got,
1709 err
1710 );
1711 }
1712
1713 {
1715 let expected = (0..q.len())
1716 .map(|i| q.vector().get(i).unwrap())
1717 .sum::<i64>() as f32;
1718
1719 let got = bit_sum;
1720
1721 let err = (expected - got).abs();
1722 assert!(
1723 err < 1.0e-7,
1724 "\"offset\": expected {}, got {}, error = {}",
1725 expected,
1726 got,
1727 err
1728 );
1729 }
1730 }
1731
1732 {
1734 let s: f32 = f.data.iter().sum::<f32>();
1736 assert_eq!(s, f.meta.sum);
1737
1738 {
1740 let expected = FastL2Norm.evaluate(&*shifted);
1741 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1742 assert!(
1743 err < 2e-7,
1744 "failed diff check, got {}, expected {} - relative error = {}",
1745 f.meta.shifted_norm,
1746 expected,
1747 err
1748 );
1749 }
1750
1751 assert_eq!(
1752 f.meta.metric_specific,
1753 f.meta.shifted_norm * f.meta.shifted_norm,
1754 "metric specific data for squared l2 is the square shifted norm",
1755 );
1756 }
1757 }
1758
1759 quantizer
1762 .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1763 .unwrap();
1764 assert_eq!(b.meta(), DataMeta::default());
1765
1766 quantizer
1767 .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1768 .unwrap();
1769 assert_eq!(q.meta(), QueryMeta::default());
1770
1771 f.data.fill(f32::INFINITY);
1772 quantizer
1773 .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1774 .unwrap();
1775 assert!(f.data.iter().all(|&i| i == 0.0));
1776 assert_eq!(f.meta.sum, 0.0);
1777 assert_eq!(f.meta.metric_specific, 0.0);
1778 }
1779
1780 fn test_ip<const Q: usize, const D: usize, Perm>(
1781 setup: &Setup,
1782 problem: &test_util::TestProblem,
1783 computed_means: &[f32],
1784 pre_scale: PreScale,
1785 rng: &mut StdRng,
1786 ctx: &dyn Display,
1787 ) where
1788 Unsigned: Representation<Q>,
1789 Unsigned: Representation<D>,
1790 Perm: PermutationStrategy<Q>,
1791 for<'a> SphericalQuantizer:
1792 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1793 for<'a> SphericalQuantizer:
1794 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1795 {
1796 assert_eq!(setup.nrows, problem.data.nrows());
1797 assert_eq!(setup.ncols, problem.data.ncols());
1798
1799 let scoped_global = ScopedAllocator::global();
1800 let distribution = Uniform::new(0, setup.nrows).unwrap();
1801 let quantizer = SphericalQuantizer::train(
1802 problem.data.as_view(),
1803 setup.transform,
1804 SupportedMetric::InnerProduct,
1805 pre_scale,
1806 rng,
1807 GlobalAllocator,
1808 )
1809 .unwrap();
1810
1811 let scale = get_scale(pre_scale, &quantizer);
1812
1813 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1814 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1815 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1816
1817 assert_eq!(
1818 quantizer.mean_norm.into_inner(),
1819 problem.mean_norm as f32,
1820 "computed mean norm should not apply scale"
1821 );
1822 let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1823 assert_eq!(&*scaled_means, quantizer.shift());
1824
1825 let ip: CompensatedIP = quantizer.as_functor();
1826
1827 assert_eq!(ip.dim, quantizer.output_dim() as f32);
1828 assert_eq!(
1829 ip.squared_shift_norm,
1830 FastL2NormSquared.evaluate(quantizer.shift())
1831 );
1832
1833 for _ in 0..setup.num_trials {
1834 let i = distribution.sample(rng);
1835 let v = problem.data.row(i);
1836
1837 quantizer
1838 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1839 .unwrap();
1840 quantizer
1841 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1842 .unwrap();
1843 quantizer
1844 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1845 .unwrap();
1846
1847 let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1848 .map(|(a, b)| scale * a - b)
1849 .collect();
1850
1851 {
1853 let DataMetaF32 {
1854 inner_product_correction,
1855 bit_sum,
1856 metric_specific,
1857 } = b.meta().to_full(ARCH);
1858
1859 let inner_product_with_centroid = metric_specific;
1860
1861 let bv = b.vector();
1863 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1864 assert_eq!(s, bit_sum as usize);
1865
1866 let inner_product: MathematicalValue<f32> =
1868 InnerProduct::evaluate(&*shifted, quantizer.shift());
1869
1870 let diff = (inner_product.into_inner() - inner_product_with_centroid).abs();
1871 assert!(
1872 diff < 1.53e-5,
1873 "got a diff of {}. Expected = {}, got = {} -- context: {}",
1874 diff,
1875 inner_product.into_inner(),
1876 inner_product_with_centroid,
1877 ctx,
1878 );
1879
1880 if const { D == 1 } {
1883 let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1884 / (inner_product_correction * (bv.len() as f32).sqrt());
1885 assert!(
1886 (self_inner_product - 0.8).abs() < 0.12,
1887 "self inner-product should be close to 0.8. Instead, it's {}",
1888 self_inner_product
1889 );
1890 }
1891 }
1892
1893 {
1894 let QueryMeta {
1895 inner_product_correction,
1896 bit_sum,
1897 offset,
1898 metric_specific,
1899 } = q.meta();
1900
1901 let inner_product_with_centroid = metric_specific;
1902 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1903 preprocessed
1904 .shifted
1905 .iter_mut()
1906 .for_each(|i| *i /= preprocessed.shifted_norm);
1907
1908 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1909 quantizer
1910 .transform
1911 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1912 .unwrap();
1913
1914 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1915 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1916
1917 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1918
1919 {
1921 let expected = (FastL2Norm).evaluate(&*shifted) * scale;
1922 let got = inner_product_correction;
1923
1924 let err = (expected - got).abs();
1925 assert!(
1926 err < 1.0e-7,
1927 "\"innerproduct_scale\": expected {}, got {}, error = {}",
1928 expected,
1929 got,
1930 err
1931 );
1932 }
1933
1934 {
1936 let expected = min / scale;
1937 let got = offset;
1938
1939 let err = (expected - got).abs();
1940 assert!(
1941 err < 1.0e-7,
1942 "\"sum_scale\": expected {}, got {}, error = {}",
1943 expected,
1944 got,
1945 err
1946 );
1947 }
1948
1949 {
1951 let expected = (0..q.len())
1952 .map(|i| q.vector().get(i).unwrap())
1953 .sum::<i64>() as f32;
1954
1955 let got = bit_sum;
1956
1957 let err = (expected - got).abs();
1958 assert!(
1959 err < 1.0e-7,
1960 "\"offset\": expected {}, got {}, error = {}",
1961 expected,
1962 got,
1963 err
1964 );
1965 }
1966
1967 {
1969 let inner_product: MathematicalValue<f32> =
1971 InnerProduct::evaluate(&*shifted, quantizer.shift());
1972 assert_eq!(inner_product.into_inner(), inner_product_with_centroid);
1973 }
1974 }
1975
1976 {
1978 let s: f32 = f.data.iter().sum::<f32>();
1980 assert_eq!(s, f.meta.sum);
1981
1982 {
1984 let expected = FastL2Norm.evaluate(&*shifted);
1985 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1986 assert!(
1987 err < 2e-7,
1988 "failed diff check, got {}, expected {} - relative error = {}",
1989 f.meta.shifted_norm,
1990 expected,
1991 err
1992 );
1993 }
1994
1995 let inner_product: MathematicalValue<f32> =
1997 InnerProduct::evaluate(&*shifted, quantizer.shift());
1998 assert_eq!(inner_product.into_inner(), f.meta.metric_specific,);
1999 }
2000 }
2001
2002 quantizer
2005 .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
2006 .unwrap();
2007 assert_eq!(b.meta(), DataMeta::default());
2008
2009 quantizer
2010 .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
2011 .unwrap();
2012 assert_eq!(q.meta(), QueryMeta::default());
2013
2014 f.data.fill(f32::INFINITY);
2015 quantizer
2016 .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
2017 .unwrap();
2018 assert!(f.data.iter().all(|&i| i == 0.0));
2019 assert_eq!(f.meta.sum, 0.0);
2020 assert_eq!(f.meta.metric_specific, 0.0);
2021 }
2022
2023 fn test_cosine<const Q: usize, const D: usize, Perm>(
2024 setup: &Setup,
2025 problem: &test_util::TestProblem,
2026 pre_scale: PreScale,
2027 rng: &mut StdRng,
2028 ) where
2029 Unsigned: Representation<Q>,
2030 Unsigned: Representation<D>,
2031 Perm: PermutationStrategy<Q>,
2032 for<'a> SphericalQuantizer:
2033 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
2034 for<'a> SphericalQuantizer:
2035 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
2036 {
2037 assert_eq!(setup.nrows, problem.data.nrows());
2038 assert_eq!(setup.ncols, problem.data.ncols());
2039
2040 let scoped_global = ScopedAllocator::global();
2041 let distribution = Uniform::new(0, setup.nrows).unwrap();
2042 let quantizer = SphericalQuantizer::train(
2043 problem.data.as_view(),
2044 setup.transform,
2045 SupportedMetric::Cosine,
2046 pre_scale,
2047 rng,
2048 GlobalAllocator,
2049 )
2050 .unwrap();
2051
2052 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
2053 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
2054 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
2055
2056 let cosine: CompensatedCosine = quantizer.as_functor();
2057
2058 assert_eq!(cosine.inner.dim, quantizer.output_dim() as f32);
2059 assert_eq!(
2060 cosine.inner.squared_shift_norm,
2061 FastL2NormSquared.evaluate(quantizer.shift())
2062 );
2063
2064 const IP_BOUND: f32 = 2.6e-3;
2065
2066 let mut test_row = |v: &[f32]| {
2067 let vnorm = (FastL2Norm).evaluate(v);
2068 let v_normalized: Vec<f32> = v
2069 .iter()
2070 .map(|i| if vnorm == 0.0 { 0.0 } else { *i / vnorm })
2071 .collect();
2072
2073 quantizer
2074 .compress_into_with(v, b.reborrow_mut(), scoped_global)
2075 .unwrap();
2076
2077 quantizer
2078 .compress_into_with(v, q.reborrow_mut(), scoped_global)
2079 .unwrap();
2080
2081 quantizer
2082 .compress_into_with(v, f.reborrow_mut(), scoped_global)
2083 .unwrap();
2084
2085 let shifted: Vec<f32> = std::iter::zip(v_normalized.iter(), quantizer.shift().iter())
2086 .map(|(a, b)| a - b)
2087 .collect();
2088
2089 {
2091 let DataMetaF32 {
2092 inner_product_correction,
2093 bit_sum,
2094 metric_specific,
2095 } = b.meta().to_full(ARCH);
2096
2097 let inner_product_with_centroid = metric_specific;
2098
2099 let bv = b.vector();
2101 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
2102 assert_eq!(s, bit_sum as usize);
2103
2104 let inner_product: MathematicalValue<f32> =
2107 InnerProduct::evaluate(&*shifted, quantizer.shift());
2108
2109 let abs = (inner_product.into_inner() - inner_product_with_centroid).abs();
2110 let relative = abs / inner_product.into_inner().abs();
2111
2112 assert!(
2113 abs < 1e-7 || relative < IP_BOUND,
2114 "got an abs/rel of {}/{} with a bound of {}/{}",
2115 abs,
2116 relative,
2117 1e-7,
2118 IP_BOUND
2119 );
2120
2121 if const { D == 1 } {
2124 let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
2125 / (inner_product_correction * (bv.len() as f32).sqrt());
2126 assert!(
2127 (self_inner_product - 0.8).abs() < 0.11,
2128 "self inner-product should be close to 0.8. Instead, it's {}",
2129 self_inner_product
2130 );
2131 }
2132 }
2133
2134 {
2135 let QueryMeta {
2136 inner_product_correction,
2137 bit_sum,
2138 offset,
2139 metric_specific,
2140 } = q.meta();
2141
2142 let inner_product_with_centroid = metric_specific;
2143 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
2144 preprocessed
2145 .shifted
2146 .iter_mut()
2147 .for_each(|i| *i /= preprocessed.shifted_norm);
2148
2149 let mut transformed = vec![0.0f32; quantizer.output_dim()];
2150 quantizer
2151 .transform
2152 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
2153 .unwrap();
2154
2155 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
2156 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
2157
2158 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
2159
2160 {
2162 let expected = (FastL2Norm).evaluate(&*shifted) * scale;
2163 let got = inner_product_correction;
2164
2165 let err = (expected - got).abs();
2166 assert!(
2167 err < 1.0e-7,
2168 "\"innerproduct_scale\": expected {}, got {}, error = {}",
2169 expected,
2170 got,
2171 err
2172 );
2173 }
2174
2175 {
2177 let expected = min / scale;
2178 let got = offset;
2179
2180 let err = (expected - got).abs();
2181 assert!(
2182 err < 1.0e-7,
2183 "\"sum_scale\": expected {}, got {}, error = {}",
2184 expected,
2185 got,
2186 err
2187 );
2188 }
2189
2190 {
2192 let expected = (0..q.len())
2193 .map(|i| q.vector().get(i).unwrap())
2194 .sum::<i64>() as f32;
2195
2196 let got = bit_sum;
2197
2198 let err = (expected - got).abs();
2199 assert!(
2200 err < 1.0e-7,
2201 "\"offset\": expected {}, got {}, error = {}",
2202 expected,
2203 got,
2204 err
2205 );
2206 }
2207
2208 {
2210 let inner_product: MathematicalValue<f32> =
2212 InnerProduct::evaluate(&*shifted, quantizer.shift());
2213
2214 let err = (inner_product.into_inner() - inner_product_with_centroid).abs()
2215 / inner_product.into_inner().abs();
2216 assert!(
2217 err < IP_BOUND,
2218 "\"offset\": expected {}, got {}, error = {}",
2219 inner_product.into_inner(),
2220 inner_product_with_centroid,
2221 err
2222 );
2223 }
2224 }
2225
2226 {
2228 let s: f32 = f.data.iter().sum::<f32>();
2230 assert_eq!(s, f.meta.sum);
2231
2232 {
2234 let expected = FastL2Norm.evaluate(&*shifted);
2235 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
2236 assert!(
2237 err < 2e-7,
2238 "failed diff check, got {}, expected {} - relative error = {}",
2239 f.meta.shifted_norm,
2240 expected,
2241 err
2242 );
2243 }
2244
2245 let inner_product: MathematicalValue<f32> =
2247 InnerProduct::evaluate(&*shifted, quantizer.shift());
2248 let err = (inner_product.into_inner() - f.meta.metric_specific).abs()
2249 / inner_product.into_inner().abs();
2250 assert!(
2251 err < IP_BOUND,
2252 "\"offset\": expected {}, got {}, error = {}",
2253 inner_product.into_inner(),
2254 f.meta.metric_specific,
2255 err
2256 );
2257 }
2258 };
2259
2260 for _ in 0..setup.num_trials {
2261 let i = distribution.sample(rng);
2262 let v = problem.data.row(i);
2263 test_row(v);
2264 }
2265
2266 let zero = vec![0.0f32; quantizer.input_dim()];
2268 test_row(&zero);
2269 }
2270
2271 fn _test_oom_resiliance<T>(quantizer: &SphericalQuantizer, data: &[f32], dst: &mut T)
2272 where
2273 for<'a> T: ReborrowMut<'a>,
2274 for<'a> SphericalQuantizer: CompressIntoWith<
2275 &'a [f32],
2276 <T as ReborrowMut<'a>>::Target,
2277 ScopedAllocator<'a>,
2278 Error = CompressionError,
2279 >,
2280 {
2281 let mut succeeded = false;
2282 let mut failed = false;
2283 for max_allocations in 0..10 {
2284 match quantizer.compress_into_with(
2285 data,
2286 dst.reborrow_mut(),
2287 ScopedAllocator::new(&test_util::LimitedAllocator::new(max_allocations)),
2288 ) {
2289 Ok(()) => {
2290 succeeded = true;
2291 }
2292 Err(CompressionError::AllocatorError(_)) => {
2293 failed = true;
2294 }
2295 Err(other) => {
2296 panic!("received an unexpected error: {:?}", other);
2297 }
2298 }
2299 }
2300 assert!(succeeded);
2301 assert!(failed);
2302 }
2303
2304 fn test_oom_resiliance<const Q: usize, const D: usize, Perm>(
2305 setup: &Setup,
2306 problem: &test_util::TestProblem,
2307 pre_scale: PreScale,
2308 rng: &mut StdRng,
2309 ) where
2310 Unsigned: Representation<Q>,
2311 Unsigned: Representation<D>,
2312 Perm: PermutationStrategy<Q>,
2313 for<'a> SphericalQuantizer: CompressIntoWith<
2314 &'a [f32],
2315 DataMut<'a, D>,
2316 ScopedAllocator<'a>,
2317 Error = CompressionError,
2318 >,
2319 for<'a> SphericalQuantizer: CompressIntoWith<
2320 &'a [f32],
2321 QueryMut<'a, Q, Perm>,
2322 ScopedAllocator<'a>,
2323 Error = CompressionError,
2324 >,
2325 {
2326 assert_eq!(setup.nrows, problem.data.nrows());
2327 assert_eq!(setup.ncols, problem.data.ncols());
2328
2329 let quantizer = SphericalQuantizer::train(
2330 problem.data.as_view(),
2331 setup.transform,
2332 SupportedMetric::SquaredL2,
2333 pre_scale,
2334 rng,
2335 GlobalAllocator,
2336 )
2337 .unwrap();
2338
2339 let data = problem.data.row(0);
2341 _test_oom_resiliance::<Data<D, _>>(
2342 &quantizer,
2343 data,
2344 &mut Data::new_boxed(quantizer.output_dim()),
2345 );
2346 _test_oom_resiliance::<Query<Q, Perm, _>>(
2347 &quantizer,
2348 data,
2349 &mut Query::new_boxed(quantizer.output_dim()),
2350 );
2351 _test_oom_resiliance::<FullQuery<_>>(
2352 &quantizer,
2353 data,
2354 &mut FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap(),
2355 );
2356 }
2357
2358 fn test_quantizer<const Q: usize, const D: usize, Perm>(setup: &Setup, rng: &mut StdRng)
2359 where
2360 Unsigned: Representation<Q>,
2361 Unsigned: Representation<D>,
2362 Perm: PermutationStrategy<Q>,
2363 for<'a> SphericalQuantizer: CompressIntoWith<
2364 &'a [f32],
2365 DataMut<'a, D>,
2366 ScopedAllocator<'a>,
2367 Error = CompressionError,
2368 >,
2369 for<'a> SphericalQuantizer: CompressIntoWith<
2370 &'a [f32],
2371 QueryMut<'a, Q, Perm>,
2372 ScopedAllocator<'a>,
2373 Error = CompressionError,
2374 >,
2375 {
2376 let problem = test_util::create_test_problem(setup.nrows, setup.ncols, rng);
2377 let computed_means_f32: Vec<_> = problem.means.iter().map(|i| *i as f32).collect();
2378
2379 let scales = [
2380 PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2381 PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2382 PreScale::ReciprocalMeanNorm,
2383 ];
2384
2385 for scale in scales {
2386 let ctx = &lazy_format!("dim = {}, scale = {:?}", setup.ncols, scale);
2387
2388 test_l2::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng);
2389 test_ip::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng, ctx);
2390 test_cosine::<Q, D, Perm>(setup, &problem, scale, rng);
2391 }
2392
2393 test_oom_resiliance::<Q, D, Perm>(setup, &problem, PreScale::ReciprocalMeanNorm, rng);
2394 }
2395
2396 #[test]
2397 fn test_spherical_quantizer() {
2398 let mut rng = StdRng::seed_from_u64(0xab516aef1ce61640);
2399 for dim in [56, 72, 128, 255] {
2400 let setup = Setup {
2401 transform: TransformKind::PaddingHadamard {
2402 target_dim: TargetDim::Same,
2403 },
2404 nrows: 64,
2405 ncols: dim,
2406 num_trials: 10,
2407 };
2408
2409 test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2410 test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2411 test_quantizer::<3, 3, Dense>(&setup, &mut rng);
2412 test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2413 test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2414
2415 let setup = Setup {
2416 transform: TransformKind::DoubleHadamard {
2417 target_dim: TargetDim::Same,
2418 },
2419 nrows: 64,
2420 ncols: dim,
2421 num_trials: 10,
2422 };
2423 test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2424 test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2425 test_quantizer::<3, 3, Dense>(&setup, &mut rng);
2426 test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2427 test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2428 }
2429 }
2430
2431 #[test]
2436 fn err_dim_cannot_be_zero() {
2437 let data = rowmajor::Owned::from_element(10, 0, 0.0f32);
2438 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2439 let err = SphericalQuantizer::train(
2440 data.as_view(),
2441 TransformKind::DoubleHadamard {
2442 target_dim: TargetDim::Same,
2443 },
2444 SupportedMetric::SquaredL2,
2445 PreScale::None,
2446 &mut rng,
2447 GlobalAllocator,
2448 )
2449 .unwrap_err();
2450 assert_eq!(err.to_string(), "data dim cannot be zero");
2451 }
2452
2453 #[test]
2454 fn err_norm_must_be_positive() {
2455 let data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2456 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2457 let err = SphericalQuantizer::train(
2458 data.as_view(),
2459 TransformKind::DoubleHadamard {
2460 target_dim: TargetDim::Same,
2461 },
2462 SupportedMetric::SquaredL2,
2463 PreScale::None,
2464 &mut rng,
2465 GlobalAllocator,
2466 )
2467 .unwrap_err();
2468 assert_eq!(err.to_string(), "norm must be positive");
2469 }
2470
2471 #[test]
2472 fn err_norm_cannot_be_infinity() {
2473 let mut data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2474 *data.element_mut(2, 5) = f32::INFINITY;
2475
2476 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2477 let err = SphericalQuantizer::train(
2478 data.as_view(),
2479 TransformKind::DoubleHadamard {
2480 target_dim: TargetDim::Same,
2481 },
2482 SupportedMetric::SquaredL2,
2483 PreScale::None,
2484 &mut rng,
2485 GlobalAllocator,
2486 )
2487 .unwrap_err();
2488 assert_eq!(err.to_string(), "computed norm contains infinity or NaN");
2489 }
2490
2491 #[test]
2492 fn err_reciprocal_norm_cannot_be_infinity() {
2493 let mut data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2494 *data.element_mut(2, 5) = 2.93863e-39;
2495
2496 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2497 let err = SphericalQuantizer::train(
2498 data.as_view(),
2499 TransformKind::DoubleHadamard {
2500 target_dim: TargetDim::Same,
2501 },
2502 SupportedMetric::SquaredL2,
2503 PreScale::ReciprocalMeanNorm,
2504 &mut rng,
2505 GlobalAllocator,
2506 )
2507 .unwrap_err();
2508 assert_eq!(err.to_string(), "reciprocal norm contains infinity or NaN");
2509 }
2510
2511 #[test]
2512 fn err_mean_norm_cannot_be_zero_generate() {
2513 let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2514 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2515 let err = SphericalQuantizer::generate(
2516 centroid,
2517 0.0,
2518 TransformKind::DoubleHadamard {
2519 target_dim: TargetDim::Same,
2520 },
2521 SupportedMetric::SquaredL2,
2522 None,
2523 &mut rng,
2524 GlobalAllocator,
2525 )
2526 .unwrap_err();
2527 assert_eq!(err.to_string(), "norm must be positive");
2528 }
2529
2530 #[test]
2531 fn err_scale_cannot_be_zero_generate() {
2532 let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2533 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2534 let err = SphericalQuantizer::generate(
2535 centroid,
2536 1.0,
2537 TransformKind::DoubleHadamard {
2538 target_dim: TargetDim::Same,
2539 },
2540 SupportedMetric::SquaredL2,
2541 Some(0.0),
2542 &mut rng,
2543 GlobalAllocator,
2544 )
2545 .unwrap_err();
2546 assert_eq!(err.to_string(), "pre-scale must be positive");
2547 }
2548
2549 #[test]
2550 fn compression_errors_data() {
2551 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2552 let data = rowmajor::Owned::<f32>::from_fn(16, 12, |_| StandardNormal {}.sample(&mut rng));
2553
2554 let quantizer = SphericalQuantizer::train(
2555 data.as_view(),
2556 TransformKind::PaddingHadamard {
2557 target_dim: TargetDim::Same,
2558 },
2559 SupportedMetric::SquaredL2,
2560 PreScale::None,
2561 &mut rng,
2562 GlobalAllocator,
2563 )
2564 .unwrap();
2565
2566 let scoped_global = ScopedAllocator::global();
2567
2568 {
2570 let mut query: Vec<f32> = quantizer.shift().to_vec();
2571 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2572 let mut multibit_data = Data::<4, _>::new_boxed(quantizer.output_dim());
2573 let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2574
2575 for i in 0..query.len() {
2576 let last = query[i];
2577 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2578 query[i] = v;
2579
2580 let err = quantizer
2581 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2582 .unwrap_err();
2583
2584 assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2585
2586 let err = quantizer
2587 .compress_into_with(&*query, multibit_data.reborrow_mut(), scoped_global)
2588 .unwrap_err();
2589
2590 assert_eq!(err, CompressionError::InputContainsNaN);
2591
2592 let err = quantizer
2593 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2594 .unwrap_err();
2595
2596 assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2597 }
2598 query[i] = last;
2599 }
2600 }
2601
2602 {
2604 let query: Vec<f32> = vec![1000000.0; quantizer.input_dim()];
2605 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2606
2607 let err = quantizer
2608 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2609 .unwrap_err();
2610
2611 let expected = "encoding error - you may need to scale the entire dataset to reduce its dynamic range";
2612
2613 assert_eq!(err.to_string(), expected, "failed for {:?}", query);
2614 }
2615
2616 for len in [quantizer.input_dim() - 1, quantizer.input_dim() + 1] {
2618 let query = vec![0.0f32; len];
2619 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2620 let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2621
2622 let err = quantizer
2623 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2624 .unwrap_err();
2625 assert_eq!(
2626 err,
2627 CompressionError::SourceDimensionMismatch {
2628 expected: quantizer.input_dim(),
2629 }
2630 );
2631
2632 let err = quantizer
2633 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2634 .unwrap_err();
2635 assert_eq!(
2636 err,
2637 CompressionError::SourceDimensionMismatch {
2638 expected: quantizer.input_dim(),
2639 }
2640 );
2641 }
2642
2643 for len in [quantizer.output_dim() - 1, quantizer.output_dim() + 1] {
2644 let query = vec![0.0f32; quantizer.input_dim()];
2645 let mut d = Data::<1, _>::new_boxed(len);
2646 let mut q = Query::<4, BitTranspose, _>::new_boxed(len);
2647
2648 let err = quantizer
2649 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2650 .unwrap_err();
2651 assert_eq!(
2652 err,
2653 CompressionError::DestinationDimensionMismatch {
2654 expected: quantizer.output_dim(),
2655 }
2656 );
2657
2658 let err = quantizer
2659 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2660 .unwrap_err();
2661 assert_eq!(
2662 err,
2663 CompressionError::DestinationDimensionMismatch {
2664 expected: quantizer.output_dim(),
2665 }
2666 );
2667 }
2668 }
2669
2670 #[test]
2671 fn centroid_scaling_happens_in_generate() {
2672 let centroid = Poly::from_iter(
2673 [1088.6732f32, 1393.32, 1547.877].into_iter(),
2674 GlobalAllocator,
2675 )
2676 .unwrap();
2677 let mean_norm = 2359.27;
2678 let pre_scale = 1.0 / mean_norm;
2679
2680 let quantizer = SphericalQuantizer::generate(
2681 centroid,
2682 mean_norm,
2683 TransformKind::Null,
2684 SupportedMetric::InnerProduct,
2685 Some(pre_scale),
2686 &mut StdRng::seed_from_u64(10),
2687 GlobalAllocator,
2688 )
2689 .unwrap();
2690
2691 let mut v = Data::<4, _>::new_boxed(quantizer.input_dim());
2692 let data: &[f32] = &[1000.34, 1456.32, 1234.5446];
2693 assert!(
2694 quantizer
2695 .compress_into_with(data, v.reborrow_mut(), ScopedAllocator::global())
2696 .is_ok(),
2697 "if this failed, the likely culprit is exceeding the value of the 16-bit correction terms"
2698 );
2699 }
2700
2701 #[test]
2702 fn centroid_squared_distance_validates_input() {
2703 let quantizer = SphericalQuantizer::generate(
2704 Poly::from_iter([2.0f32, 4.0].into_iter(), GlobalAllocator).unwrap(),
2705 1.0,
2706 TransformKind::Null,
2707 SupportedMetric::SquaredL2,
2708 Some(1.0),
2709 &mut StdRng::seed_from_u64(10),
2710 GlobalAllocator,
2711 )
2712 .unwrap();
2713
2714 assert_eq!(
2715 quantizer.centroid_squared_distance(&[2.0]),
2716 Err(CentroidDistanceError::DimensionMismatch { expected: 2 })
2717 );
2718 assert_eq!(
2719 quantizer.centroid_squared_distance(&[f32::NAN, 4.0]),
2720 Err(CentroidDistanceError::NonFiniteDistance)
2721 );
2722 assert_eq!(
2723 quantizer.centroid_squared_distance(&[f32::MAX, f32::MAX]),
2724 Err(CentroidDistanceError::NonFiniteDistance)
2725 );
2726 }
2727
2728 #[test]
2729 fn cosine_centroid_squared_distance_ignores_pre_scale() {
2730 let quantizer = SphericalQuantizer::generate(
2731 Poly::from_iter([0.5f32, 0.5].into_iter(), GlobalAllocator).unwrap(),
2732 1.0,
2733 TransformKind::Null,
2734 SupportedMetric::Cosine,
2735 Some(2.0),
2736 &mut StdRng::seed_from_u64(10),
2737 GlobalAllocator,
2738 )
2739 .unwrap();
2740
2741 assert_eq!(quantizer.shift(), &[0.5, 0.5]);
2742
2743 let actual = quantizer.centroid_squared_distance(&[0.6, 0.4]).unwrap();
2744 let expected = 0.02;
2745 assert!((actual - expected).abs() <= 1.0e-6);
2746 }
2747
2748 #[test]
2749 fn non_cosine_centroid_squared_distance_removes_pre_scale() {
2750 for metric in [SupportedMetric::SquaredL2, SupportedMetric::InnerProduct] {
2751 let quantizer = SphericalQuantizer::generate(
2752 Poly::from_iter([0.5f32, 0.5].into_iter(), GlobalAllocator).unwrap(),
2753 1.0,
2754 TransformKind::Null,
2755 metric,
2756 Some(2.0),
2757 &mut StdRng::seed_from_u64(10),
2758 GlobalAllocator,
2759 )
2760 .unwrap();
2761
2762 assert_eq!(quantizer.shift(), &[1.0, 1.0]);
2763
2764 let actual = quantizer.centroid_squared_distance(&[0.6, 0.4]).unwrap();
2765 let expected = 0.02;
2766 assert!((actual - expected).abs() <= 1.0e-6);
2767 }
2768 }
2769}
2770
2771#[cfg(feature = "flatbuffers")]
2772#[cfg(test)]
2773mod test_serialization {
2774 use rand::{SeedableRng, rngs::StdRng};
2775
2776 use super::*;
2777 use crate::{
2778 algorithms::transforms::TargetDim,
2779 flatbuffers::{self as fb, to_flatbuffer},
2780 poly, test_util,
2781 };
2782
2783 #[test]
2784 fn test_serialization_happy_path() {
2785 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2786 let problem = test_util::create_test_problem(10, 128, &mut rng);
2787
2788 let low = NonZeroUsize::new(100).unwrap();
2789 let high = NonZeroUsize::new(150).unwrap();
2790
2791 let kinds = [
2792 TransformKind::Null,
2794 TransformKind::DoubleHadamard {
2796 target_dim: TargetDim::Same,
2797 },
2798 TransformKind::DoubleHadamard {
2799 target_dim: TargetDim::Natural,
2800 },
2801 TransformKind::DoubleHadamard {
2802 target_dim: TargetDim::Override(low),
2803 },
2804 TransformKind::DoubleHadamard {
2805 target_dim: TargetDim::Override(high),
2806 },
2807 TransformKind::PaddingHadamard {
2809 target_dim: TargetDim::Same,
2810 },
2811 TransformKind::PaddingHadamard {
2812 target_dim: TargetDim::Natural,
2813 },
2814 TransformKind::PaddingHadamard {
2815 target_dim: TargetDim::Override(low),
2816 },
2817 TransformKind::PaddingHadamard {
2818 target_dim: TargetDim::Override(high),
2819 },
2820 #[cfg(all(not(miri), feature = "linalg"))]
2822 TransformKind::RandomRotation {
2823 target_dim: TargetDim::Same,
2824 },
2825 #[cfg(all(not(miri), feature = "linalg"))]
2826 TransformKind::RandomRotation {
2827 target_dim: TargetDim::Natural,
2828 },
2829 #[cfg(all(not(miri), feature = "linalg"))]
2830 TransformKind::RandomRotation {
2831 target_dim: TargetDim::Override(low),
2832 },
2833 #[cfg(all(not(miri), feature = "linalg"))]
2834 TransformKind::RandomRotation {
2835 target_dim: TargetDim::Override(high),
2836 },
2837 ];
2838
2839 let pre_scales = [
2840 PreScale::None,
2841 PreScale::Some(Positive::new(0.5).unwrap()),
2842 PreScale::Some(Positive::new(1.0).unwrap()),
2843 PreScale::Some(Positive::new(1.5).unwrap()),
2844 PreScale::ReciprocalMeanNorm,
2845 ];
2846
2847 let alloc = GlobalAllocator;
2848 for kind in kinds.into_iter() {
2849 for metric in SupportedMetric::all() {
2850 for pre_scale in pre_scales {
2851 let quantizer = SphericalQuantizer::train(
2852 problem.data.as_view(),
2853 kind,
2854 metric,
2855 pre_scale,
2856 &mut rng,
2857 alloc,
2858 )
2859 .unwrap();
2860
2861 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2862 let proto =
2863 flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2864 let reloaded = SphericalQuantizer::try_unpack(alloc, proto).unwrap();
2865 assert_eq!(quantizer, reloaded, "failed on transform {:?}", kind);
2866 }
2867 }
2868 }
2869 }
2870
2871 #[test]
2872 fn test_error_checking() {
2873 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2874 let problem = test_util::create_test_problem(10, 128, &mut rng);
2875
2876 let transform = TransformKind::DoubleHadamard {
2877 target_dim: TargetDim::Same,
2878 };
2879
2880 let alloc = GlobalAllocator;
2881 let mut make_quantizer = || {
2882 SphericalQuantizer::train(
2883 problem.data.as_view(),
2884 transform,
2885 SupportedMetric::SquaredL2,
2886 PreScale::None,
2887 &mut rng,
2888 alloc,
2889 )
2890 .unwrap()
2891 };
2892
2893 type E = DeserializationError;
2894
2895 {
2897 let mut quantizer = make_quantizer();
2898 quantizer.mean_norm = unsafe { Positive::new_unchecked(0.0) };
2902
2903 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2904 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2905 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2906 assert_eq!(err, E::MissingNorm);
2907 }
2908
2909 {
2911 let mut quantizer = make_quantizer();
2912
2913 quantizer.mean_norm = unsafe { Positive::new_unchecked(-1.0) };
2917
2918 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2919 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2920 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2921 assert_eq!(err, E::MissingNorm);
2922 }
2923
2924 {
2926 let mut quantizer = make_quantizer();
2927
2928 quantizer.pre_scale = unsafe { Positive::new_unchecked(0.0) };
2931
2932 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2933 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2934 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2935 assert_eq!(err, E::PreScaleNotPositive);
2936 }
2937
2938 {
2940 let mut quantizer = make_quantizer();
2941 quantizer.shift = poly!([1.0, 2.0, 3.0], alloc).unwrap();
2942
2943 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2944 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2945 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2946 assert_eq!(err, E::DimMismatch);
2947 }
2948 }
2949}