Skip to main content

diskann_quantization/spherical/
quantizer.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use 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///////////////
41// Quantizer //
42///////////////
43
44#[derive(Debug)]
45#[cfg_attr(test, derive(PartialEq))]
46pub struct SphericalQuantizer<A = GlobalAllocator>
47where
48    A: Allocator,
49{
50    /// The offset to apply to each vector.
51    shift: Poly<[f32], A>,
52
53    /// The [`SphericalQuantizer`] supports several different strategies for performing the
54    /// distance-preserving transformation on dataset vectors, which may be applicable in
55    /// different scenarios.
56    ///
57    /// The different transformations may have restrictions on the number of supported dimensions.
58    /// While we will accept all non-zero input dimensions, the output dimension of a transform
59    /// may be higher or lower, depending on the configuration.
60    transform: Transform<A>,
61
62    /// The metric meant to be used by the quantizer.
63    metric: SupportedMetric,
64
65    /// When processing queries, it may be beneficial to modify the query norm to match the
66    /// dataset norm.
67    ///
68    /// This is only applicable when `InnerProduct` and `Cosine` are used, but serves to
69    /// move the query into the dynamic range of the quantization.
70    ///
71    /// You would think that the normalization step in RabitQ would mitigate this, but
72    /// that is not always right since range-adjustment happens before centering.
73    mean_norm: Positive<f32>,
74
75    /// To support 16-bit constants which have a limited dynamic range, we allow a
76    /// pre-scaling parameter that is multiplied to each value in compressed vectors.
77    ///
78    /// This allows to transparent handling of compressing integral data, which can
79    /// otherwise easily overflow `f16`.
80    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
111impl<A> SphericalQuantizer<A>
112where
113    A: Allocator,
114{
115    /// Return the number dimensions this quantizer has been trained for.
116    pub fn input_dim(&self) -> usize {
117        self.shift.len()
118    }
119
120    /// Return the dimension of the post-transformed vector.
121    ///
122    /// Output storage vectors should use this dimension instead of `self.dim()` because
123    /// in general, the output dim **may** be different from the input dimension.
124    pub fn output_dim(&self) -> usize {
125        self.transform.output_dim()
126    }
127
128    /// Return the per-dimension shift vector.
129    ///
130    /// This vector is meant to accomplish two goals:
131    ///
132    /// 1. Centers the data around the training dataset mean.
133    /// 2. Offsets each dimension into a range that can be encoded in unsigned values.
134    pub fn shift(&self) -> &[f32] {
135        &self.shift
136    }
137
138    /// Return the approximate mean norm of the training data.
139    pub fn mean_norm(&self) -> Positive<f32> {
140        self.mean_norm
141    }
142
143    /// Return the pre-scaling parameter for data. This value is multiplied to every
144    /// compressed vector to adjust its dynamic range.
145    ///
146    /// A value of 1.0 means that no scaling is occurring.
147    pub fn pre_scale(&self) -> Positive<f32> {
148        self.pre_scale
149    }
150
151    /// Return a reference to the allocator used by this data structure.
152    pub fn allocator(&self) -> &A {
153        self.shift.allocator()
154    }
155
156    /// Return an independently allocated copy of this quantizer.
157    pub fn try_clone(&self) -> Result<Self, AllocatorError> {
158        Ok(Self {
159            shift: self.shift.try_clone()?,
160            transform: self.transform.try_clone()?,
161            metric: self.metric,
162            mean_norm: self.mean_norm,
163            pre_scale: self.pre_scale,
164        })
165    }
166
167    /// A lower-level constructor that accepts a centroid, mean norm, and pre-scale directly.
168    pub fn generate(
169        mut centroid: Poly<[f32], A>,
170        mean_norm: f32,
171        transform: TransformKind,
172        metric: SupportedMetric,
173        pre_scale: Option<f32>,
174        rng: &mut dyn RngCore,
175        allocator: A,
176    ) -> Result<Self, TrainError> {
177        let pre_scale = match pre_scale {
178            Some(v) => Positive::new(v).map_err(|_| TrainError::PrescaleNotPositive)?,
179            None => crate::num::POSITIVE_ONE_F32,
180        };
181
182        let dim = match NonZeroUsize::new(centroid.len()) {
183            Some(dim) => dim,
184            None => {
185                return Err(TrainError::DimCannotBeZero);
186            }
187        };
188
189        let mean_norm = Positive::new(mean_norm).map_err(|_| TrainError::NormNotPositive)?;
190
191        // We passed in a 'rng' so `Transform::new` will not fail.
192        let transform = match Transform::new(transform, dim, Some(rng), allocator.clone()) {
193            Ok(v) => v,
194            Err(NewTransformError::RngMissing(_)) => unreachable!("An Rng was provided"),
195            Err(NewTransformError::AllocatorError(err)) => {
196                return Err(TrainError::AllocatorError(err));
197            }
198        };
199
200        // Transform the centroid by the pre-scale.
201        centroid
202            .iter_mut()
203            .for_each(|v| *v *= pre_scale.into_inner());
204
205        Ok(SphericalQuantizer {
206            shift: centroid,
207            transform,
208            metric,
209            mean_norm,
210            pre_scale,
211        })
212    }
213
214    /// Return the metric used by this quantizer.
215    pub fn metric(&self) -> SupportedMetric {
216        self.metric
217    }
218
219    /// Construct a quantizer for vectors in the distribution of `data`.
220    ///
221    /// The type of distance-preserving transform to use is selected by the [`TransformKind`].
222    ///
223    /// Vectors compressed with this quantizer will be **metric specific** and optimized for
224    /// distance computations rather than reconstruction. This means that vectors compressed
225    /// targeting the inner-product distance will not return meaningful results if used for
226    /// L2 distance computations.
227    ///
228    /// Additionally, vectors compressed when using the [`SupportedMetric::Cosine`] distance
229    /// will be implicitly normalized before being compressed to enable better compression.
230    ///
231    /// If argument `pre_scale` is given, then all vectors compressed by this quantizer will
232    /// first be scaled by this value. Note that if given, `pre_scale` **must** be positive.
233    pub fn train<T, R>(
234        data: rowmajor::Ref<T>,
235        transform: TransformKind,
236        metric: SupportedMetric,
237        pre_scale: PreScale,
238        rng: &mut R,
239        allocator: A,
240    ) -> Result<Self, TrainError>
241    where
242        T: Copy + Into<f64> + Into<f32>,
243        R: Rng,
244    {
245        // An inner implementation erasing the type of the random number generator to
246        // cut down on excess monomorphization.
247        #[inline(never)]
248        fn train<T, A>(
249            data: rowmajor::Ref<T>,
250            transform: TransformKind,
251            metric: SupportedMetric,
252            pre_scale: PreScale,
253            rng: &mut dyn RngCore,
254            allocator: A,
255        ) -> Result<SphericalQuantizer<A>, TrainError>
256        where
257            T: Copy + Into<f64> + Into<f32>,
258            A: Allocator,
259        {
260            // This check is repeated in `Self::generate`, but we prefer to bail as early
261            // as possible if we detect an error.
262            if data.ncols() == 0 {
263                return Err(TrainError::DimCannotBeZero);
264            }
265
266            let (centroid, mean_norm) = match metric {
267                SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => {
268                    compute_means_and_average_norm(data)
269                }
270                SupportedMetric::Cosine => (
271                    compute_normalized_means(data)
272                        .map_err(|_: CannotBeEmpty| TrainError::DataCannotBeEmpty)?,
273                    1.0,
274                ),
275            };
276
277            let mean_norm = mean_norm as f32;
278
279            if mean_norm <= 0.0 {
280                return Err(TrainError::NormNotPositive);
281            }
282
283            if !mean_norm.is_finite() {
284                return Err(TrainError::NormNotFinite);
285            }
286
287            // Determining if (and how) the pre-scaling term will be calculated.
288            let pre_scale: Positive<f32> = match pre_scale {
289                PreScale::None => crate::num::POSITIVE_ONE_F32,
290                PreScale::Some(v) => v,
291                PreScale::ReciprocalMeanNorm => {
292                    // We've checked that `mean_norm` is both positive and finite.
293                    //
294                    // It's possible that when converted to `f32`,
295                    //
296                    // Taking the reciprocal is well defined. However, since the norms
297                    // and scales in `compute_means_and_average_norm` are done using `f64`,
298                    // it's possible that the computed `mean_norm` is subnormal, leading
299                    // to the reciprocal being infinity.
300                    let pre_scale = Positive::new(1.0 / mean_norm)
301                        .map_err(|_| TrainError::ReciprocalNormNotFinite)?;
302
303                    if !pre_scale.into_inner().is_finite() {
304                        return Err(TrainError::ReciprocalNormNotFinite);
305                    }
306
307                    pre_scale
308                }
309            };
310
311            // Allow the pre-scaling to take place inside `Self::generate`.
312            let centroid =
313                Poly::from_iter(centroid.into_iter().map(|i| i as f32), allocator.clone())?;
314
315            SphericalQuantizer::generate(
316                centroid,
317                mean_norm,
318                transform,
319                metric,
320                Some(pre_scale.into_inner()),
321                rng,
322                allocator,
323            )
324        }
325
326        train(data, transform, metric, pre_scale, rng, allocator)
327    }
328
329    /// Rescale the argument `v` to be in the rough dynamic range of the training dataset.
330    pub fn rescale(&self, v: &mut [f32]) {
331        let norm = FastL2Norm.evaluate(&*v);
332        let m = self.mean_norm.into_inner() / norm;
333        v.iter_mut().for_each(|i| *i *= m);
334    }
335
336    /// Private helper function to do common data pre-processing.
337    ///
338    /// # Panics
339    ///
340    /// Panics if `data.len() != self.dim()`.
341    fn preprocess<'a>(
342        &self,
343        data: &[f32],
344        allocator: ScopedAllocator<'a>,
345    ) -> Result<Preprocessed<'a>, CompressionError> {
346        assert_eq!(data.len(), self.input_dim(), "Data dimension is incorrect.");
347
348        // Fold in pre-scaling with the potential norm corretion for cosine.
349        //
350        // NOTE: When we're computing Cosine Similarity, we normalize the vector. As such,
351        // the `pre_scale` parameter become irrelvant since it just gets normalized away.
352        let scale = self.pre_scale.into_inner();
353        let mul: f32 = match self.metric {
354            SupportedMetric::Cosine => {
355                let norm: f32 = (FastL2Norm).evaluate(data);
356                if norm == 0.0 { 1.0 } else { 1.0 / norm }
357            }
358            SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => scale,
359        };
360
361        // Center the vector and compute the squared norm of the shifted vector.
362        let shifted = Poly::from_iter(
363            std::iter::zip(data.iter(), self.shift.iter()).map(|(&f, &s)| mul * f - s),
364            allocator,
365        )?;
366
367        let shifted_norm = FastL2Norm.evaluate(&*shifted);
368        if !shifted_norm.is_finite() {
369            return Err(CompressionError::InputContainsNaN);
370        }
371        let inner_product_with_centroid = match self.metric {
372            SupportedMetric::SquaredL2 => None,
373            SupportedMetric::InnerProduct | SupportedMetric::Cosine => {
374                let ip: MathematicalValue<f32> = InnerProduct::evaluate(&*shifted, &*self.shift);
375                Some(ip.into_inner())
376            }
377        };
378
379        Ok(Preprocessed {
380            shifted,
381            shifted_norm,
382            inner_product_with_centroid,
383        })
384    }
385
386    /// Construct an [`iface::Quantizer`] trait object from `self`.
387    pub fn as_quantizer<const NBITS: usize>(
388        self,
389    ) -> Result<Poly<dyn iface::Quantizer>, AllocatorError>
390    where
391        A: 'static,
392        iface::Impl<NBITS, A>: iface::Constructible<A> + iface::Quantizer,
393    {
394        let iface = iface::Impl::<NBITS, A>::new(self)?;
395        crate::poly!({ iface::Quantizer }, iface, GlobalAllocator)
396    }
397}
398
399/// Pre-scaling selector for spherical quantization training. Pre-scaling adjusts the
400/// dynamic range of the data (usually decreasing it uniformly) to keep the correction terms
401/// within the range expressible by 16-bit floating point numbers.
402#[derive(Debug, Clone, Copy)]
403pub enum PreScale {
404    /// Do not use any pre-scaling.
405    None,
406    /// Pre-scale all data by the specified amount.
407    Some(Positive<f32>),
408    /// Heuristically estimate a pre-scaling parameter by using the inverse approximate
409    /// mean norm. This will nearly normalize in-distribution vectors.
410    ReciprocalMeanNorm,
411}
412
413#[cfg(feature = "flatbuffers")]
414#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
415#[derive(Debug, Clone, Error, PartialEq)]
416#[non_exhaustive]
417pub enum DeserializationError {
418    #[error(transparent)]
419    TransformError(#[from] TransformError),
420    #[error("unrecognized flatbuffer identifier")]
421    UnrecognizedIdentifier,
422    #[error("transform length not equal to centroid")]
423    DimMismatch,
424    #[error("norm is missing or is not positive")]
425    MissingNorm,
426    #[error("pre-scale is missing or is not positive")]
427    PreScaleNotPositive,
428
429    #[error(transparent)]
430    InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
431
432    #[error(transparent)]
433    InvalidMetric(#[from] InvalidMetric),
434
435    #[error(transparent)]
436    AllocatorError(#[from] AllocatorError),
437}
438
439#[cfg(feature = "flatbuffers")]
440#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
441impl<A> SphericalQuantizer<A>
442where
443    A: Allocator + Clone,
444{
445    /// Pack `self` into `buf` using the [`spherical::SphericalQuantizer`] serialized
446    /// representation.
447    pub(crate) fn pack<'a, FA>(
448        &self,
449        buf: &mut FlatBufferBuilder<'a, FA>,
450    ) -> WIPOffset<spherical::SphericalQuantizer<'a>>
451    where
452        FA: flatbuffers::Allocator + 'a,
453    {
454        // Save the centroid vector.
455        let centroid = buf.create_vector(&self.shift);
456
457        // Save the transform.
458        let transform = self.transform.pack(buf);
459
460        // Finish up.
461        spherical::SphericalQuantizer::create(
462            buf,
463            &spherical::SphericalQuantizerArgs {
464                centroid: Some(centroid),
465                transform: Some(transform),
466                metric: self.metric.into(),
467                mean_norm: self.mean_norm.into_inner(),
468                pre_scale: self.pre_scale.into_inner(),
469            },
470        )
471    }
472
473    /// Attempt to unpack `self` from a serialized [`spherical::SphericalQuantizer`]
474    /// serialized representation, returning any encountered error.
475    pub(crate) fn try_unpack(
476        alloc: A,
477        proto: spherical::SphericalQuantizer<'_>,
478    ) -> Result<Self, DeserializationError> {
479        let metric: SupportedMetric = proto.metric().try_into()?;
480
481        // Unpack the centroid.
482        let shift = Poly::from_iter(proto.centroid().into_iter(), alloc.clone())?;
483
484        // Unpack the transform.
485        let transform = Transform::try_unpack(alloc, proto.transform())?;
486
487        // Ensure consistency between the shift dimensions and the transform.
488        if shift.len() != transform.input_dim() {
489            return Err(DeserializationError::DimMismatch);
490        }
491
492        // Make sure we get a sane value for the mean norm.
493        let mean_norm =
494            Positive::new(proto.mean_norm()).map_err(|_| DeserializationError::MissingNorm)?;
495
496        let pre_scale = Positive::new(proto.pre_scale())
497            .map_err(|_| DeserializationError::PreScaleNotPositive)?;
498
499        Ok(Self {
500            shift,
501            transform,
502            metric,
503            mean_norm,
504            pre_scale,
505        })
506    }
507}
508
509struct Preprocessed<'a> {
510    shifted: Poly<[f32], ScopedAllocator<'a>>,
511    shifted_norm: f32,
512    inner_product_with_centroid: Option<f32>,
513}
514
515impl Preprocessed<'_> {
516    /// Return the metric specific correction term as sumamrized below:
517    ///
518    /// * Inner Product: The inner product between the shifted vector and the centroid.
519    /// * Squared L2: The squared norm of the shifted vector.
520    fn metric_specific(&self) -> f32 {
521        match self.inner_product_with_centroid {
522            Some(ip) => ip,
523            None => self.shifted_norm * self.shifted_norm,
524        }
525    }
526}
527
528///////////////////////
529// Distance Functors //
530///////////////////////
531
532impl<A> AsFunctor<CompensatedSquaredL2> for SphericalQuantizer<A>
533where
534    A: Allocator,
535{
536    fn as_functor(&self) -> CompensatedSquaredL2 {
537        CompensatedSquaredL2::new(self.output_dim())
538    }
539}
540
541impl<A> AsFunctor<CompensatedIP> for SphericalQuantizer<A>
542where
543    A: Allocator,
544{
545    fn as_functor(&self) -> CompensatedIP {
546        CompensatedIP::new(&self.shift, self.output_dim())
547    }
548}
549
550impl<A> AsFunctor<CompensatedCosine> for SphericalQuantizer<A>
551where
552    A: Allocator,
553{
554    fn as_functor(&self) -> CompensatedCosine {
555        CompensatedCosine::new(self.as_functor())
556    }
557}
558
559/////////////////
560// Compression //
561/////////////////
562
563#[derive(Debug, Error, Clone, Copy, PartialEq)]
564#[non_exhaustive]
565pub enum CompressionError {
566    #[error("input contains NaN")]
567    InputContainsNaN,
568
569    #[error("expected source vector to have length {expected}")]
570    SourceDimensionMismatch { expected: usize },
571
572    #[error("expected destination vector to have length {expected}")]
573    DestinationDimensionMismatch { expected: usize },
574
575    #[error(
576        "encoding error - you may need to scale the entire dataset to reduce its dynamic range"
577    )]
578    EncodingError(#[from] DataMetaError),
579
580    #[error(transparent)]
581    AllocatorError(#[from] AllocatorError),
582}
583
584fn check_dims(
585    input: usize,
586    output: usize,
587    from: usize,
588    into: usize,
589) -> Result<(), CompressionError> {
590    if from != input {
591        return Err(CompressionError::SourceDimensionMismatch { expected: input });
592    }
593    if into != output {
594        return Err(CompressionError::DestinationDimensionMismatch { expected: output });
595    }
596    Ok(())
597}
598
599/// Helper trait to dispatch to a faster 1-bit implementation and use the slower
600/// maximum-cosine algorithm when more than 1 bit is used.
601trait FinishCompressing {
602    fn finish_compressing(
603        &mut self,
604        preprocessed: &Preprocessed<'_>,
605        transformed: &[f32],
606        transformed_norm: f32,
607        allocator: ScopedAllocator<'_>,
608    ) -> Result<(), CompressionError>;
609}
610
611impl FinishCompressing for DataMut<'_, 1> {
612    fn finish_compressing(
613        &mut self,
614        preprocessed: &Preprocessed<'_>,
615        transformed: &[f32],
616        transformed_norm: f32,
617        _: ScopedAllocator<'_>,
618    ) -> Result<(), CompressionError> {
619        // Compute signed quantized vector (-1 or 1)
620        // and also populate the unsigned bit representation in `into` output vector.
621        let mut quant_raw_inner_product = 0.0f32;
622        let mut bit_sum = 0u32;
623        transformed.iter().enumerate().for_each(|(i, &r)| {
624            let bit: u8 = if r > 0.0 { 1 } else { 0 };
625
626            quant_raw_inner_product += r.abs();
627            bit_sum += <u8 as Into<u32>>::into(bit);
628
629            // SAFETY: From check 1, we know that `i < into.len()`.
630            unsafe { self.vector_mut().set_unchecked(i, bit) };
631        });
632
633        // The value we just computed for `quant_raw_inner_product` is:
634        // ```
635        // Y = <x', x> * sqrt(D)                        [1]
636        // ```
637        // The inner product correction term is
638        // ```
639        //       2 |X|
640        // -----------------                            [2]
641        // <x', x> * sqrt(D)
642        // ```
643        // [1] substitutes directly into [2] and we get
644        // ```
645        // 2 |X|
646        // -----
647        //   Y
648        // ```
649        // Therefore, the inner product correction term is
650        // ```
651        // 2.0 * shifted_norm / quant_raw_inner_product
652        // ```
653        let inner_product_correction =
654            2.0 * transformed_norm * preprocessed.shifted_norm / quant_raw_inner_product;
655        self.set_meta(DataMeta::new(
656            inner_product_correction,
657            preprocessed.metric_specific(),
658            bit_sum,
659        )?);
660        Ok(())
661    }
662}
663
664impl FinishCompressing for DataMut<'_, 2> {
665    fn finish_compressing(
666        &mut self,
667        preprocessed: &Preprocessed<'_>,
668        transformed: &[f32],
669        transformed_norm: f32,
670        allocator: ScopedAllocator<'_>,
671    ) -> Result<(), CompressionError> {
672        compress_via_maximum_cosine(
673            self.reborrow_mut(),
674            preprocessed,
675            transformed,
676            transformed_norm,
677            allocator,
678        )
679    }
680}
681
682impl FinishCompressing for DataMut<'_, 4> {
683    fn finish_compressing(
684        &mut self,
685        preprocessed: &Preprocessed<'_>,
686        transformed: &[f32],
687        transformed_norm: f32,
688        allocator: ScopedAllocator<'_>,
689    ) -> Result<(), CompressionError> {
690        compress_via_maximum_cosine(
691            self.reborrow_mut(),
692            preprocessed,
693            transformed,
694            transformed_norm,
695            allocator,
696        )
697    }
698}
699
700impl FinishCompressing for DataMut<'_, 8> {
701    fn finish_compressing(
702        &mut self,
703        preprocessed: &Preprocessed<'_>,
704        transformed: &[f32],
705        transformed_norm: f32,
706        allocator: ScopedAllocator<'_>,
707    ) -> Result<(), CompressionError> {
708        compress_via_maximum_cosine(
709            self.reborrow_mut(),
710            preprocessed,
711            transformed,
712            transformed_norm,
713            allocator,
714        )
715    }
716}
717
718//////////////////////
719// Data Compression //
720//////////////////////
721
722impl<A> CompressIntoWith<&[f32], FullQueryMut<'_>, ScopedAllocator<'_>> for SphericalQuantizer<A>
723where
724    A: Allocator,
725{
726    type Error = CompressionError;
727
728    /// Compress the input vector `from` into the bitslice `into`.
729    ///
730    /// # Error
731    ///
732    /// Returns an error if
733    /// * The input contains `NaN`.
734    /// * `from.len() != self.dim()`: Vector to be compressed must have the same
735    ///   dimensionality as the quantizer.
736    /// * `into.len() != self.output_dim()`: Compressed vector must have the same
737    ///   dimensionality as the output of the distance-preserving transform. Importantely,
738    ///   this **may** be different than `self.dim()` and should be retrieved from
739    ///   `self.output_dim()`.
740    fn compress_into_with(
741        &self,
742        from: &[f32],
743        mut into: FullQueryMut<'_>,
744        allocator: ScopedAllocator<'_>,
745    ) -> Result<(), Self::Error> {
746        let input_dim = self.shift.len();
747        let output_dim = self.output_dim();
748        check_dims(input_dim, output_dim, from.len(), into.len())?;
749
750        let mut preprocessed = self.preprocess(from, allocator)?;
751
752        // If the preprocessed norm is zero, then we tried to compress the center directly.
753        // In this case, we can get the correct behavior by setting `into` to all zeros.
754        if preprocessed.shifted_norm == 0.0 {
755            into.vector_mut().fill(0.0);
756            *into.meta_mut() = Default::default();
757            return Ok(());
758        }
759
760        preprocessed
761            .shifted
762            .iter_mut()
763            .for_each(|v| *v /= preprocessed.shifted_norm);
764
765        // Transformation can fail due to OOM - we want to handle that gracefully.
766        //
767        // If the transformation fails because we provided the wrong sizes, that is a hard
768        // program bug.
769        #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
770        match self
771            .transform
772            .transform_into(into.vector_mut(), &preprocessed.shifted, allocator)
773        {
774            Ok(()) => {}
775            Err(TransformFailed::AllocatorError(err)) => {
776                return Err(CompressionError::AllocatorError(err));
777            }
778            Err(TransformFailed::SourceMismatch { .. })
779            | Err(TransformFailed::DestinationMismatch { .. }) => {
780                panic!(
781                    "The sizes of these arrays should already be checked - this is a logic error"
782                );
783            }
784            #[cfg(feature = "linalg")]
785            Err(TransformFailed::SgemmError(_)) => {
786                panic!("SGEMM should not fail with valid dimensions - this is a logic error");
787            }
788        }
789
790        *into.meta_mut() = FullQueryMeta {
791            sum: into.vector().iter().sum::<f32>(),
792            shifted_norm: preprocessed.shifted_norm,
793            metric_specific: preprocessed.metric_specific(),
794        };
795        Ok(())
796    }
797}
798
799impl<const NBITS: usize, A> CompressIntoWith<&[f32], DataMut<'_, NBITS>, ScopedAllocator<'_>>
800    for SphericalQuantizer<A>
801where
802    A: Allocator,
803    Unsigned: Representation<NBITS>,
804    for<'a> DataMut<'a, NBITS>: FinishCompressing,
805{
806    type Error = CompressionError;
807
808    /// Compress the input vector `from` into the bitslice `into`.
809    ///
810    /// # Error
811    ///
812    /// Returns an error if
813    /// * The input contains `NaN`.
814    /// * `from.len() != self.dim()`: Vector to be compressed must have the same
815    ///   dimensionality as the quantizer.
816    /// * `into.len() != self.output_dim()`: Compressed vector must have the same
817    ///   dimensionality as the output of the distance-preserving transform. Importantely,
818    ///   this **may** be different than `self.dim()` and should be retrieved from
819    ///   `self.output_dim()`.
820    fn compress_into_with(
821        &self,
822        from: &[f32],
823        mut into: DataMut<'_, NBITS>,
824        allocator: ScopedAllocator<'_>,
825    ) -> Result<(), Self::Error> {
826        let input_dim = self.shift.len();
827        let output_dim = self.output_dim();
828        check_dims(input_dim, output_dim, from.len(), into.len())?;
829
830        let mut preprocessed = self.preprocess(from, allocator)?;
831
832        if preprocessed.shifted_norm == 0.0 {
833            into.set_meta(DataMeta::default());
834            return Ok(());
835        }
836
837        let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
838        preprocessed
839            .shifted
840            .iter_mut()
841            .for_each(|v| *v /= preprocessed.shifted_norm);
842
843        // Transformation can fail due to OOM - we want to handle that gracefully.
844        //
845        // If the transformation fails because we provided the wrong sizes, that is a hard
846        // program bug.
847        #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
848        match self
849            .transform
850            .transform_into(&mut transformed, &preprocessed.shifted, allocator)
851        {
852            Ok(()) => {}
853            Err(TransformFailed::AllocatorError(err)) => {
854                return Err(CompressionError::AllocatorError(err));
855            }
856            Err(TransformFailed::SourceMismatch { .. })
857            | Err(TransformFailed::DestinationMismatch { .. }) => {
858                panic!(
859                    "The sizes of these arrays should already be checked - this is a logic error"
860                );
861            }
862            #[cfg(feature = "linalg")]
863            Err(TransformFailed::SgemmError(_)) => {
864                panic!("SGEMM should not fail with valid dimensions - this is a logic error");
865            }
866        }
867
868        let transformed_norm = if self.transform.preserves_norms() {
869            1.0
870        } else {
871            (FastL2Norm).evaluate(&*transformed)
872        };
873
874        into.finish_compressing(&preprocessed, &transformed, transformed_norm, allocator)?;
875        Ok(())
876    }
877}
878
879struct AsNonZero<const NBITS: usize>;
880impl<const NBITS: usize> AsNonZero<NBITS> {
881    const NON_ZERO: NonZeroUsize = NonZeroUsize::new(NBITS).unwrap();
882}
883
884fn compress_via_maximum_cosine<const NBITS: usize>(
885    mut data: DataMut<'_, NBITS>,
886    preprocessed: &Preprocessed<'_>,
887    transformed: &[f32],
888    transformed_norm: f32,
889    allocator: ScopedAllocator<'_>,
890) -> Result<(), CompressionError>
891where
892    Unsigned: Representation<NBITS>,
893{
894    assert_eq!(data.len(), transformed.len());
895
896    // Find the value we will use to multiply `transformed` to round it to the lattice
897    // element that has the maximum cosine-similarity.
898    let optimal_scale =
899        maximize_cosine_similarity(transformed, AsNonZero::<NBITS>::NON_ZERO, allocator)?;
900
901    let domain = Unsigned::domain_const::<NBITS>();
902    let min = *domain.start() as f32;
903    let max = *domain.end() as f32;
904    let offset = max / 2.0;
905
906    let mut self_inner_product = 0.0f32;
907    let mut bit_sum = 0u32;
908    for (i, t) in transformed.iter().enumerate() {
909        let v = (*t * optimal_scale + offset).clamp(min, max).round();
910        let dv = v - offset;
911        self_inner_product = dv.mul_add(*t, self_inner_product);
912
913        let v = v as u8;
914        bit_sum += <u8 as Into<u32>>::into(v);
915
916        // SAFETY: We have checked that `data.len() == transformed.len()`, so this access
917        // is in-bounds.
918        //
919        // Further, by construction, `v` is encodable by the `Unsigned`.
920        unsafe { data.vector_mut().set_unchecked(i, v) };
921    }
922
923    let shifted_norm = preprocessed.shifted_norm;
924    let inner_product_correction = (transformed_norm * shifted_norm) / self_inner_product;
925    data.set_meta(DataMeta::new(
926        inner_product_correction,
927        preprocessed.metric_specific(),
928        bit_sum,
929    )?);
930    Ok(())
931}
932
933// This struct does 2 things:
934//
935// 1. Records the index in `v` and `rounded` and the scaling parameter so that
936//   `value * v[position]` gets rounded to `rounded[position] + 1` while
937//   `(value - epsilon) * v[position]` is rounded to `rounded[position]` for some small
938//   epsilon.
939//
940//   Informally, "what's the smallest scaling factor so `v[position]` gets rounded to
941//   the next value.
942//
943// 2. Imposes a total ordering on `f32` values so it can be used in a `BinaryHeap`.
944//   Additionally, ordering is reverse so that `BinaryHeap` models a min-heap instead
945//   of a max-heap.
946#[derive(Debug, Clone, Copy)]
947struct Pair {
948    value: f32,
949    position: u32,
950}
951
952impl PartialEq for Pair {
953    fn eq(&self, other: &Self) -> bool {
954        self.value.eq(&other.value)
955    }
956}
957
958impl Eq for Pair {}
959impl PartialOrd for Pair {
960    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
961        Some(self.cmp(other))
962    }
963}
964impl Ord for Pair {
965    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
966        other
967            .value
968            .partial_cmp(&self.value)
969            .unwrap_or(std::cmp::Ordering::Equal)
970    }
971}
972
973/// This is a tricky function - please read carefully.
974///
975/// Given a vector `v` compute the scaling factor `s` such that the cosine similarity
976/// betwen `v` and `r` is maximized where `r` is defined as
977/// ```math
978/// let offset = (2^(num_bits) - 1) / 2;
979/// let r = (s * v + offset).round().clamp(0, 2^num_bits - 1) - offset
980/// ```
981///
982/// More informally, maximize the inner product between `v` and the points in a square
983/// lattice with 2^num_bits values in each dimension, centered around zero. This latice
984/// takes the values (+0.5, -0.5, +1.5, -1.5 ...) to give equal weight above and below zero.
985///
986/// It works by slowly increasing the factor `s` such that the rounding of only one
987/// dimension in `v` is changed at a time. A running tally of the cosine similarity is
988/// computed for each scaling factor until we've processed `D * 2^(num_bits - 1)` possible
989/// scaling factors, where `D` is the length of `v`.
990///
991/// The best scaling factor is returned.
992///
993/// Refer to algorithm 1 in <https://arxiv.org/pdf/2409.09913>.
994///
995/// # Panics
996///
997/// Panics is `v.is_empty()`.
998///
999/// # Implementation details
1000///
1001/// We work with the absolute value of the elements in the vector `v`.
1002/// This does not affect the final result as the scaling works the same in both the
1003/// positive and negative directions but simplifies the book keeping.
1004fn maximize_cosine_similarity(
1005    v: &[f32],
1006    num_bits: NonZeroUsize,
1007    allocator: ScopedAllocator<'_>,
1008) -> Result<f32, AllocatorError> {
1009    // Initially, the lattice element has the value `0.5` for all dimensions.
1010    // This means the initial inner product between `v` and the rounded term is simply
1011    // `0.5 * sum(abs.(v))`. The absolute value is used because the latice element is
1012    // always in the direction of the components in `v`.
1013    let mut current_ip = 0.5 * v.iter().map(|i| i.abs() as f64).sum::<f64>();
1014    let mut current_square_norm = 0.25 * (v.len() as f64);
1015
1016    // Book keeping for the current value of the rounded vector.
1017    // The true numeric value is 0.5 less than this (in the direction of `v`), but we use
1018    // integers for a smaller memory footprint.
1019    let mut rounded = Poly::broadcast(1u16, v.len(), allocator)?;
1020
1021    // Compute the critical values and store them on a heap.
1022    //
1023    // The binary heap will keep track of the minimum critical value. Multiplying `v` by the
1024    // minimum critical value `s` means that `s * v` will only change `rounded` from its
1025    // current value at a single index (the position associated with `s`).
1026    let eps = 0.0001f32;
1027    let one_and_change = 1.0 + eps;
1028    let mut base = Poly::from_iter(
1029        v.iter().enumerate().map(|(position, value)| {
1030            let value = one_and_change / value.abs();
1031            Pair {
1032                value,
1033                position: position as u32,
1034            }
1035        }),
1036        allocator,
1037    )?;
1038
1039    // Lint: This is a private method and all the callers have an invariant that they check
1040    // for non-empty inputs.
1041    #[expect(clippy::expect_used)]
1042    let mut critical_values =
1043        SliceHeap::new(&mut base).expect("calling code should not allow the slice to be empty");
1044
1045    let mut max_similarity = f64::NEG_INFINITY;
1046    let mut optimal_scale = f32::default();
1047    let stop = (2usize).pow(num_bits.get() as u32 - 1) as u16;
1048
1049    loop {
1050        let mut should_break = false;
1051        critical_values.update_root(|pair| {
1052            let Pair { value, position } = *pair;
1053            if value == f32::MAX {
1054                should_break = true;
1055                return;
1056            }
1057
1058            let r = &mut rounded[position as usize];
1059            let vp = &v[position as usize];
1060
1061            let old_r = *r;
1062            // By the nature of cricital values, only `r` will change in `rounded` when
1063            // multiplying by `value`. And that change will be to increase by 1.
1064            *r += 1;
1065
1066            // The inner product estimate simply increases by `vp.abs()` because:
1067            //
1068            // * `r` is the only value in `rounded` that changes.
1069            // * `r` is increased by 1.
1070            current_ip += vp.abs() as f64;
1071
1072            // This uses the formula
1073            // ```math
1074            // (x + 1)^2 - x^2 = x^2 + 2x + 1 - x^2
1075            //                 = 2x + 1
1076            // ```
1077            // substitute `x = y - 1/2` to obtain the true value associated with rounded and
1078            // we get
1079            // ```math
1080            // 2 ( y - 1/2 ) + 1 = 2y - 1 + 1
1081            //                   = 2y
1082            // ```
1083            // Therefore, the change in the estimate for the square norm of `rounded` is
1084            // `2 * old_r`.
1085            current_square_norm += (2 * old_r) as f64;
1086
1087            // Compute the current cosine similarity and update max if needed.
1088            let similarity = current_ip / current_square_norm.sqrt();
1089            if similarity > max_similarity {
1090                max_similarity = similarity;
1091                optimal_scale = value;
1092            }
1093
1094            // Compute the scaling factor that will change this dimension to the next value.
1095            if *r < stop {
1096                *pair = Pair {
1097                    value: (*r as f32 + eps) / vp.abs(),
1098                    position,
1099                };
1100            } else {
1101                *pair = Pair {
1102                    value: f32::MAX,
1103                    position,
1104                };
1105            }
1106        });
1107        if should_break {
1108            break;
1109        }
1110    }
1111
1112    Ok(optimal_scale)
1113}
1114
1115///////////////////////
1116// Query Compression //
1117///////////////////////
1118
1119impl<const NBITS: usize, Perm, A>
1120    CompressIntoWith<&[f32], QueryMut<'_, NBITS, Perm>, ScopedAllocator<'_>>
1121    for SphericalQuantizer<A>
1122where
1123    Unsigned: Representation<NBITS>,
1124    Perm: PermutationStrategy<NBITS>,
1125    A: Allocator,
1126{
1127    type Error = CompressionError;
1128
1129    /// Compress the input vector `from` into the bitslice `into`.
1130    ///
1131    /// # Error
1132    ///
1133    /// Returns an error if
1134    /// * The input contains `NaN`.
1135    /// * `from.len() != self.dim()`: Vector to be compressed must have the same
1136    ///   dimensionality as the quantizer.
1137    /// * `into.len() != self.output_dim()`: Compressed vector must have the same
1138    ///   dimensionality as the output of the distance-preserving transform. Importantely,
1139    ///   this **may** be different than `self.dim()` and should be retrieved from
1140    ///   `self.output_dim()`.
1141    fn compress_into_with(
1142        &self,
1143        from: &[f32],
1144        mut into: QueryMut<'_, NBITS, Perm>,
1145        allocator: ScopedAllocator<'_>,
1146    ) -> Result<(), Self::Error> {
1147        let input_dim = self.shift.len();
1148        let output_dim = self.output_dim();
1149        check_dims(input_dim, output_dim, from.len(), into.len())?;
1150
1151        let mut preprocessed = self.preprocess(from, allocator)?;
1152
1153        if preprocessed.shifted_norm == 0.0 {
1154            into.set_meta(QueryMeta::default());
1155            return Ok(());
1156        }
1157
1158        preprocessed
1159            .shifted
1160            .iter_mut()
1161            .for_each(|v| *v /= preprocessed.shifted_norm);
1162
1163        let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
1164
1165        // Transformation can fail due to OOM - we want to handle that gracefully.
1166        //
1167        // If the transformation fails because we provided the wrong sizes, that is a hard
1168        // program bug.
1169        #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
1170        match self
1171            .transform
1172            .transform_into(&mut transformed, &preprocessed.shifted, allocator)
1173        {
1174            Ok(()) => {}
1175            Err(TransformFailed::AllocatorError(err)) => {
1176                return Err(CompressionError::AllocatorError(err));
1177            }
1178            Err(TransformFailed::SourceMismatch { .. })
1179            | Err(TransformFailed::DestinationMismatch { .. }) => {
1180                panic!(
1181                    "The sizes of these arrays should already be checked - this is a logic error"
1182                );
1183            }
1184            #[cfg(feature = "linalg")]
1185            Err(TransformFailed::SgemmError(_)) => {
1186                panic!("SGEMM should not fail with valid dimensions - this is a logic error");
1187            }
1188        }
1189
1190        // Compute the minimum and maximum values of the transformed vector.
1191        let (min, max) = transformed
1192            .iter()
1193            .fold((f32::MAX, f32::MIN), |(min, max), i| {
1194                (i.min(min), i.max(max))
1195            });
1196
1197        let domain = Unsigned::domain_const::<NBITS>();
1198        let lo = (*domain.start()) as f32;
1199        let hi = (*domain.end()) as f32;
1200
1201        let scale = (max - min) / hi;
1202        let mut bit_sum: f32 = 0.0;
1203        transformed.iter().enumerate().for_each(|(i, v)| {
1204            let c = ((v - min) / scale).round().clamp(lo, hi);
1205            bit_sum += c;
1206
1207            // Lint: We have verified that `into.len() == transformed.len()`, so the index
1208            // `i` is in bounds.
1209            //
1210            // Further, `c` has beem clamped to `[0, 2^NBITS - 1]` and is thus encodable
1211            // with the NBITS-bit unsigned representation.
1212            #[expect(clippy::unwrap_used)]
1213            into.vector_mut().set(i, c as i64).unwrap();
1214        });
1215
1216        // Finish up the compensation terms.
1217        into.set_meta(QueryMeta {
1218            inner_product_correction: preprocessed.shifted_norm * scale,
1219            bit_sum,
1220            offset: min / scale,
1221            metric_specific: preprocessed.metric_specific(),
1222        });
1223
1224        Ok(())
1225    }
1226}
1227
1228///////////
1229// Tests //
1230///////////
1231
1232#[cfg(not(miri))]
1233#[cfg(test)]
1234mod tests {
1235    use super::*;
1236
1237    use std::fmt::Display;
1238
1239    use diskann_utils::{ReborrowMut, lazy_format, views::rowmajor::MatrixMut};
1240    use diskann_vector::{PureDistanceFunction, norm::FastL2NormSquared};
1241    use diskann_wide::ARCH;
1242    use rand::{
1243        SeedableRng,
1244        distr::{Distribution, Uniform},
1245        rngs::StdRng,
1246    };
1247    use rand_distr::StandardNormal;
1248
1249    use crate::{
1250        algorithms::transforms::TargetDim,
1251        alloc::GlobalAllocator,
1252        bits::{BitTranspose, Dense},
1253        spherical::{Data, DataMetaF32, FullQuery, Query},
1254        test_util,
1255    };
1256
1257    // Test cosine-similarity maximizer
1258    #[test]
1259    fn test_cosine_similarity_maximizer() {
1260        let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
1261        let num_trials = 10000;
1262        let num_bits = NonZeroUsize::new(3).unwrap();
1263
1264        let scale_distribution = Uniform::new(0.5f32, 10.0f32).unwrap();
1265
1266        let run_test = |target: [f32; 4]| {
1267            let scale =
1268                maximize_cosine_similarity(&target, num_bits, ScopedAllocator::global()).unwrap();
1269
1270            let mut best: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
1271            let mut best_similarity: f32 = f32::NEG_INFINITY;
1272
1273            // This crazy series of nested loops performs an exhaustive search over the
1274            // encoding space.
1275            let min = -3.5;
1276            for i0 in 0..8 {
1277                for i1 in 0..8 {
1278                    for i2 in 0..8 {
1279                        for i3 in 0..8 {
1280                            let p: [f32; 4] = [
1281                                min + (i0 as f32),
1282                                min + (i1 as f32),
1283                                min + (i2 as f32),
1284                                min + (i3 as f32),
1285                            ];
1286
1287                            let sim: MathematicalValue<f32> =
1288                                diskann_vector::distance::Cosine::evaluate(&p, &target);
1289                            let sim = sim.into_inner();
1290                            if sim > best_similarity {
1291                                best_similarity = sim;
1292                                // Transform into an integer starting at zero.
1293                                best = p.map(|i| i - min);
1294                            }
1295                        }
1296                    }
1297                }
1298            }
1299
1300            // Now, rescale the input vector, clamp, and round.
1301            // Check if they agree.
1302            let clamped = target.map(|i| (i * scale - min).round().clamp(0.0, 7.0));
1303            let clamped_cosine: MathematicalValue<f32> =
1304                diskann_vector::distance::Cosine::evaluate(&clamped.map(|i| i + min), &target);
1305
1306            // We expect to either get the best value found via exhaustive search, or some
1307            // scalar multiple of it (since that will have the same cosine similarity).
1308            let passed = if best == clamped {
1309                true
1310            } else {
1311                let ratio: Vec<f32> = std::iter::zip(best, clamped)
1312                    .map(|(b, c)| {
1313                        let ratio = (b + min) / (c + min);
1314                        assert_ne!(
1315                            ratio, 0.0,
1316                            "ratio should never be zero because `b` is an integer and \
1317                             `min` is not"
1318                        );
1319                        ratio
1320                    })
1321                    .collect();
1322
1323                ratio.iter().all(|i| *i == ratio[0])
1324            };
1325
1326            if !passed {
1327                panic!(
1328                    "failed for input {:?}.\
1329                     Best = {:?}, Found = {:?}\
1330                     Best similarity = {}, similarity with clamped = {}",
1331                    target,
1332                    best,
1333                    clamped,
1334                    best_similarity,
1335                    clamped_cosine.into_inner()
1336                );
1337            }
1338        };
1339
1340        // Run targeted tests.
1341        let min = -3.5;
1342        for i0 in (0..8).step_by(2) {
1343            for i1 in (1..9).step_by(2) {
1344                for i2 in (0..8).step_by(2) {
1345                    for i3 in (1..9).step_by(2) {
1346                        let p: [f32; 4] = [
1347                            min + (i0 as f32),
1348                            min + (i1 as f32),
1349                            min + (i2 as f32),
1350                            min + (i3 as f32),
1351                        ];
1352                        run_test(p)
1353                    }
1354                }
1355            }
1356        }
1357
1358        for _ in 0..num_trials {
1359            let this_scale: f32 = scale_distribution.sample(&mut rng);
1360            let v: [f32; 4] = [(); 4].map(|_| {
1361                let v: f32 = StandardNormal {}.sample(&mut rng);
1362                this_scale * v
1363            });
1364            run_test(v);
1365        }
1366    }
1367
1368    #[test]
1369    #[should_panic(expected = "calling code should not allow the slice to be empty")]
1370    fn empty_slice_panics() {
1371        maximize_cosine_similarity(
1372            &[],
1373            NonZeroUsize::new(4).unwrap(),
1374            ScopedAllocator::global(),
1375        )
1376        .unwrap();
1377    }
1378
1379    struct Setup {
1380        transform: TransformKind,
1381        nrows: usize,
1382        ncols: usize,
1383        num_trials: usize,
1384    }
1385
1386    fn get_scale(scale: PreScale, quantizer: &SphericalQuantizer) -> f32 {
1387        match scale {
1388            PreScale::None => 1.0,
1389            PreScale::Some(v) => v.into_inner(),
1390            PreScale::ReciprocalMeanNorm => 1.0 / quantizer.mean_norm().into_inner(),
1391        }
1392    }
1393
1394    fn test_l2<const Q: usize, const D: usize, Perm>(
1395        setup: &Setup,
1396        problem: &test_util::TestProblem,
1397        computed_means: &[f32],
1398        pre_scale: PreScale,
1399        rng: &mut StdRng,
1400    ) where
1401        Unsigned: Representation<Q>,
1402        Unsigned: Representation<D>,
1403        Perm: PermutationStrategy<Q>,
1404        for<'a> SphericalQuantizer:
1405            CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1406        for<'a> SphericalQuantizer:
1407            CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1408    {
1409        assert_eq!(setup.nrows, problem.data.nrows());
1410        assert_eq!(setup.ncols, problem.data.ncols());
1411
1412        let scoped_global = ScopedAllocator::global();
1413        let distribution = Uniform::new(0, setup.nrows).unwrap();
1414        let quantizer = SphericalQuantizer::train(
1415            problem.data.as_view(),
1416            setup.transform,
1417            SupportedMetric::SquaredL2,
1418            pre_scale,
1419            rng,
1420            GlobalAllocator,
1421        )
1422        .unwrap();
1423
1424        let scale = get_scale(pre_scale, &quantizer);
1425
1426        let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1427        let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1428        let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1429
1430        assert_eq!(
1431            quantizer.mean_norm.into_inner(),
1432            problem.mean_norm as f32,
1433            "computed mean norm should not apply scale"
1434        );
1435        let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1436        assert_eq!(&*scaled_means, quantizer.shift());
1437
1438        let l2: CompensatedSquaredL2 = quantizer.as_functor();
1439        assert_eq!(l2.dim, quantizer.output_dim() as f32);
1440
1441        for _ in 0..setup.num_trials {
1442            let i = distribution.sample(rng);
1443            let v = problem.data.row(i);
1444
1445            quantizer
1446                .compress_into_with(v, b.reborrow_mut(), scoped_global)
1447                .unwrap();
1448            quantizer
1449                .compress_into_with(v, q.reborrow_mut(), scoped_global)
1450                .unwrap();
1451            quantizer
1452                .compress_into_with(v, f.reborrow_mut(), scoped_global)
1453                .unwrap();
1454
1455            let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1456                .map(|(a, b)| scale * a - b)
1457                .collect();
1458
1459            // Check that the compensation coefficient were chosen correctly.
1460            {
1461                let DataMetaF32 {
1462                    inner_product_correction,
1463                    bit_sum,
1464                    metric_specific,
1465                } = b.meta().to_full(ARCH);
1466
1467                let shifted_square_norm = metric_specific;
1468
1469                // Check that the bit-count is correct. let bv = b.vector();
1470                let bv = b.vector();
1471                let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1472                assert_eq!(s, bit_sum as usize);
1473
1474                // Check that the shifted norm is correct.
1475                {
1476                    let expected = FastL2NormSquared.evaluate(&*shifted);
1477                    let err = (shifted_square_norm - expected).abs() / expected.abs();
1478                    assert!(
1479                        err < 5.0e-4, // twice the minimum normal f16 value.
1480                        "failed diff check, got {}, expected {} - relative error = {}",
1481                        shifted_square_norm,
1482                        expected,
1483                        err
1484                    );
1485                }
1486
1487                // Finaly, verify that the self-inner-product is clustered around 0.8 as
1488                // the RaBitQ paper suggests.
1489                if const { D == 1 } {
1490                    let self_inner_product = 2.0 * shifted_square_norm.sqrt()
1491                        / (inner_product_correction * (bv.len() as f32).sqrt());
1492                    assert!(
1493                        (self_inner_product - 0.8).abs() < 0.13,
1494                        "self inner-product should be close to 0.8. Instead, it's {}",
1495                        self_inner_product
1496                    );
1497                }
1498            }
1499
1500            {
1501                let QueryMeta {
1502                    inner_product_correction,
1503                    bit_sum,
1504                    offset,
1505                    metric_specific,
1506                } = q.meta();
1507
1508                let shifted_square_norm = metric_specific;
1509                let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1510                preprocessed
1511                    .shifted
1512                    .iter_mut()
1513                    .for_each(|i| *i /= preprocessed.shifted_norm);
1514
1515                let mut transformed = vec![0.0f32; quantizer.output_dim()];
1516                quantizer
1517                    .transform
1518                    .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1519                    .unwrap();
1520
1521                let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1522                let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1523
1524                let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1525
1526                // Shifted Norm
1527                {
1528                    let expected = FastL2NormSquared.evaluate(&*shifted);
1529                    let err = (shifted_square_norm - expected).abs() / expected.abs();
1530                    assert!(
1531                        err < 2e-7,
1532                        "failed diff check, got {}, expected {} - relative error = {}",
1533                        shifted_square_norm,
1534                        expected,
1535                        err
1536                    );
1537                }
1538
1539                // Inner product correction
1540                {
1541                    let expected = shifted_square_norm.sqrt() * scale;
1542                    let got = inner_product_correction;
1543
1544                    let err = (expected - got).abs();
1545                    assert!(
1546                        err < 1.0e-7,
1547                        "\"innerproduct_scale\": expected {}, got {}, error = {}",
1548                        expected,
1549                        got,
1550                        err
1551                    );
1552                }
1553
1554                // Offset
1555                {
1556                    let expected = min / scale;
1557                    let got = offset;
1558
1559                    let err = (expected - got).abs();
1560                    assert!(
1561                        err < 1.0e-7,
1562                        "\"sum_scale\": expected {}, got {}, error = {}",
1563                        expected,
1564                        got,
1565                        err
1566                    );
1567                }
1568
1569                // Bit Sum
1570                {
1571                    let expected = (0..q.len())
1572                        .map(|i| q.vector().get(i).unwrap())
1573                        .sum::<i64>() as f32;
1574
1575                    let got = bit_sum;
1576
1577                    let err = (expected - got).abs();
1578                    assert!(
1579                        err < 1.0e-7,
1580                        "\"offset\": expected {}, got {}, error = {}",
1581                        expected,
1582                        got,
1583                        err
1584                    );
1585                }
1586            }
1587
1588            // Check that the compensation coefficient were chosen correctly.
1589            {
1590                // Check that the bit-count is correct.
1591                let s: f32 = f.data.iter().sum::<f32>();
1592                assert_eq!(s, f.meta.sum);
1593
1594                // Check that the shifted norm is correct.
1595                {
1596                    let expected = FastL2Norm.evaluate(&*shifted);
1597                    let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1598                    assert!(
1599                        err < 2e-7,
1600                        "failed diff check, got {}, expected {} - relative error = {}",
1601                        f.meta.shifted_norm,
1602                        expected,
1603                        err
1604                    );
1605                }
1606
1607                assert_eq!(
1608                    f.meta.metric_specific,
1609                    f.meta.shifted_norm * f.meta.shifted_norm,
1610                    "metric specific data for squared l2 is the square shifted norm",
1611                );
1612            }
1613        }
1614
1615        // Finally - test that if we compress the centroid, the metadata coefficients get
1616        // zeroed correctly.
1617        quantizer
1618            .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1619            .unwrap();
1620        assert_eq!(b.meta(), DataMeta::default());
1621
1622        quantizer
1623            .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1624            .unwrap();
1625        assert_eq!(q.meta(), QueryMeta::default());
1626
1627        f.data.fill(f32::INFINITY);
1628        quantizer
1629            .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1630            .unwrap();
1631        assert!(f.data.iter().all(|&i| i == 0.0));
1632        assert_eq!(f.meta.sum, 0.0);
1633        assert_eq!(f.meta.metric_specific, 0.0);
1634    }
1635
1636    fn test_ip<const Q: usize, const D: usize, Perm>(
1637        setup: &Setup,
1638        problem: &test_util::TestProblem,
1639        computed_means: &[f32],
1640        pre_scale: PreScale,
1641        rng: &mut StdRng,
1642        ctx: &dyn Display,
1643    ) where
1644        Unsigned: Representation<Q>,
1645        Unsigned: Representation<D>,
1646        Perm: PermutationStrategy<Q>,
1647        for<'a> SphericalQuantizer:
1648            CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1649        for<'a> SphericalQuantizer:
1650            CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1651    {
1652        assert_eq!(setup.nrows, problem.data.nrows());
1653        assert_eq!(setup.ncols, problem.data.ncols());
1654
1655        let scoped_global = ScopedAllocator::global();
1656        let distribution = Uniform::new(0, setup.nrows).unwrap();
1657        let quantizer = SphericalQuantizer::train(
1658            problem.data.as_view(),
1659            setup.transform,
1660            SupportedMetric::InnerProduct,
1661            pre_scale,
1662            rng,
1663            GlobalAllocator,
1664        )
1665        .unwrap();
1666
1667        let scale = get_scale(pre_scale, &quantizer);
1668
1669        let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1670        let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1671        let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1672
1673        assert_eq!(
1674            quantizer.mean_norm.into_inner(),
1675            problem.mean_norm as f32,
1676            "computed mean norm should not apply scale"
1677        );
1678        let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1679        assert_eq!(&*scaled_means, quantizer.shift());
1680
1681        let ip: CompensatedIP = quantizer.as_functor();
1682
1683        assert_eq!(ip.dim, quantizer.output_dim() as f32);
1684        assert_eq!(
1685            ip.squared_shift_norm,
1686            FastL2NormSquared.evaluate(quantizer.shift())
1687        );
1688
1689        for _ in 0..setup.num_trials {
1690            let i = distribution.sample(rng);
1691            let v = problem.data.row(i);
1692
1693            quantizer
1694                .compress_into_with(v, b.reborrow_mut(), scoped_global)
1695                .unwrap();
1696            quantizer
1697                .compress_into_with(v, q.reborrow_mut(), scoped_global)
1698                .unwrap();
1699            quantizer
1700                .compress_into_with(v, f.reborrow_mut(), scoped_global)
1701                .unwrap();
1702
1703            let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1704                .map(|(a, b)| scale * a - b)
1705                .collect();
1706
1707            // Check that the compensation coefficient were chosen correctly.
1708            {
1709                let DataMetaF32 {
1710                    inner_product_correction,
1711                    bit_sum,
1712                    metric_specific,
1713                } = b.meta().to_full(ARCH);
1714
1715                let inner_product_with_centroid = metric_specific;
1716
1717                // Check that the bit-count is correct.
1718                let bv = b.vector();
1719                let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1720                assert_eq!(s, bit_sum as usize);
1721
1722                // Check that the shifted norm is correct.
1723                let inner_product: MathematicalValue<f32> =
1724                    InnerProduct::evaluate(&*shifted, quantizer.shift());
1725
1726                let diff = (inner_product.into_inner() - inner_product_with_centroid).abs();
1727                assert!(
1728                    diff < 1.53e-5,
1729                    "got a diff of {}. Expected = {}, got = {} -- context: {}",
1730                    diff,
1731                    inner_product.into_inner(),
1732                    inner_product_with_centroid,
1733                    ctx,
1734                );
1735
1736                // Finaly, verify that the self-inner-product is clustered around 0.8 as
1737                // the RaBitQ paper suggests.
1738                if const { D == 1 } {
1739                    let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1740                        / (inner_product_correction * (bv.len() as f32).sqrt());
1741                    assert!(
1742                        (self_inner_product - 0.8).abs() < 0.12,
1743                        "self inner-product should be close to 0.8. Instead, it's {}",
1744                        self_inner_product
1745                    );
1746                }
1747            }
1748
1749            {
1750                let QueryMeta {
1751                    inner_product_correction,
1752                    bit_sum,
1753                    offset,
1754                    metric_specific,
1755                } = q.meta();
1756
1757                let inner_product_with_centroid = metric_specific;
1758                let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1759                preprocessed
1760                    .shifted
1761                    .iter_mut()
1762                    .for_each(|i| *i /= preprocessed.shifted_norm);
1763
1764                let mut transformed = vec![0.0f32; quantizer.output_dim()];
1765                quantizer
1766                    .transform
1767                    .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1768                    .unwrap();
1769
1770                let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1771                let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1772
1773                let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1774
1775                // Inner product correction
1776                {
1777                    let expected = (FastL2Norm).evaluate(&*shifted) * scale;
1778                    let got = inner_product_correction;
1779
1780                    let err = (expected - got).abs();
1781                    assert!(
1782                        err < 1.0e-7,
1783                        "\"innerproduct_scale\": expected {}, got {}, error = {}",
1784                        expected,
1785                        got,
1786                        err
1787                    );
1788                }
1789
1790                // Offset
1791                {
1792                    let expected = min / scale;
1793                    let got = offset;
1794
1795                    let err = (expected - got).abs();
1796                    assert!(
1797                        err < 1.0e-7,
1798                        "\"sum_scale\": expected {}, got {}, error = {}",
1799                        expected,
1800                        got,
1801                        err
1802                    );
1803                }
1804
1805                // Bit Sum
1806                {
1807                    let expected = (0..q.len())
1808                        .map(|i| q.vector().get(i).unwrap())
1809                        .sum::<i64>() as f32;
1810
1811                    let got = bit_sum;
1812
1813                    let err = (expected - got).abs();
1814                    assert!(
1815                        err < 1.0e-7,
1816                        "\"offset\": expected {}, got {}, error = {}",
1817                        expected,
1818                        got,
1819                        err
1820                    );
1821                }
1822
1823                // Inner Product with Centroid
1824                {
1825                    // Check that the shifted norm is correct.
1826                    let inner_product: MathematicalValue<f32> =
1827                        InnerProduct::evaluate(&*shifted, quantizer.shift());
1828                    assert_eq!(inner_product.into_inner(), inner_product_with_centroid);
1829                }
1830            }
1831
1832            // Check that the compensation coefficient were chosen correctly.
1833            {
1834                // Check that the bit-count is correct.
1835                let s: f32 = f.data.iter().sum::<f32>();
1836                assert_eq!(s, f.meta.sum);
1837
1838                // Check that the shifted norm is correct.
1839                {
1840                    let expected = FastL2Norm.evaluate(&*shifted);
1841                    let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1842                    assert!(
1843                        err < 2e-7,
1844                        "failed diff check, got {}, expected {} - relative error = {}",
1845                        f.meta.shifted_norm,
1846                        expected,
1847                        err
1848                    );
1849                }
1850
1851                // Check that the shifted norm is correct. s
1852                let inner_product: MathematicalValue<f32> =
1853                    InnerProduct::evaluate(&*shifted, quantizer.shift());
1854                assert_eq!(inner_product.into_inner(), f.meta.metric_specific,);
1855            }
1856        }
1857
1858        // Finally - test that if we compress the centroid, the metadata coefficients get
1859        // zeroed correctly.
1860        quantizer
1861            .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1862            .unwrap();
1863        assert_eq!(b.meta(), DataMeta::default());
1864
1865        quantizer
1866            .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1867            .unwrap();
1868        assert_eq!(q.meta(), QueryMeta::default());
1869
1870        f.data.fill(f32::INFINITY);
1871        quantizer
1872            .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1873            .unwrap();
1874        assert!(f.data.iter().all(|&i| i == 0.0));
1875        assert_eq!(f.meta.sum, 0.0);
1876        assert_eq!(f.meta.metric_specific, 0.0);
1877    }
1878
1879    fn test_cosine<const Q: usize, const D: usize, Perm>(
1880        setup: &Setup,
1881        problem: &test_util::TestProblem,
1882        pre_scale: PreScale,
1883        rng: &mut StdRng,
1884    ) where
1885        Unsigned: Representation<Q>,
1886        Unsigned: Representation<D>,
1887        Perm: PermutationStrategy<Q>,
1888        for<'a> SphericalQuantizer:
1889            CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1890        for<'a> SphericalQuantizer:
1891            CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1892    {
1893        assert_eq!(setup.nrows, problem.data.nrows());
1894        assert_eq!(setup.ncols, problem.data.ncols());
1895
1896        let scoped_global = ScopedAllocator::global();
1897        let distribution = Uniform::new(0, setup.nrows).unwrap();
1898        let quantizer = SphericalQuantizer::train(
1899            problem.data.as_view(),
1900            setup.transform,
1901            SupportedMetric::Cosine,
1902            pre_scale,
1903            rng,
1904            GlobalAllocator,
1905        )
1906        .unwrap();
1907
1908        let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1909        let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1910        let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1911
1912        let cosine: CompensatedCosine = quantizer.as_functor();
1913
1914        assert_eq!(cosine.inner.dim, quantizer.output_dim() as f32);
1915        assert_eq!(
1916            cosine.inner.squared_shift_norm,
1917            FastL2NormSquared.evaluate(quantizer.shift())
1918        );
1919
1920        const IP_BOUND: f32 = 2.6e-3;
1921
1922        let mut test_row = |v: &[f32]| {
1923            let vnorm = (FastL2Norm).evaluate(v);
1924            let v_normalized: Vec<f32> = v
1925                .iter()
1926                .map(|i| if vnorm == 0.0 { 0.0 } else { *i / vnorm })
1927                .collect();
1928
1929            quantizer
1930                .compress_into_with(v, b.reborrow_mut(), scoped_global)
1931                .unwrap();
1932
1933            quantizer
1934                .compress_into_with(v, q.reborrow_mut(), scoped_global)
1935                .unwrap();
1936
1937            quantizer
1938                .compress_into_with(v, f.reborrow_mut(), scoped_global)
1939                .unwrap();
1940
1941            let shifted: Vec<f32> = std::iter::zip(v_normalized.iter(), quantizer.shift().iter())
1942                .map(|(a, b)| a - b)
1943                .collect();
1944
1945            // Check that the compensation coefficient were chosen correctly.
1946            {
1947                let DataMetaF32 {
1948                    inner_product_correction,
1949                    bit_sum,
1950                    metric_specific,
1951                } = b.meta().to_full(ARCH);
1952
1953                let inner_product_with_centroid = metric_specific;
1954
1955                // Check that the bit-count is correct.
1956                let bv = b.vector();
1957                let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1958                assert_eq!(s, bit_sum as usize);
1959
1960                // Check that the shifted norm is correct. Since they are computed slightly
1961                // differnetly, allow a small amount of error.
1962                let inner_product: MathematicalValue<f32> =
1963                    InnerProduct::evaluate(&*shifted, quantizer.shift());
1964
1965                let abs = (inner_product.into_inner() - inner_product_with_centroid).abs();
1966                let relative = abs / inner_product.into_inner().abs();
1967
1968                assert!(
1969                    abs < 1e-7 || relative < IP_BOUND,
1970                    "got an abs/rel of {}/{} with a bound of {}/{}",
1971                    abs,
1972                    relative,
1973                    1e-7,
1974                    IP_BOUND
1975                );
1976
1977                // Finaly, verify that the self-inner-product is clustered around 0.8 as
1978                // the RaBitQ paper suggests.
1979                if const { D == 1 } {
1980                    let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1981                        / (inner_product_correction * (bv.len() as f32).sqrt());
1982                    assert!(
1983                        (self_inner_product - 0.8).abs() < 0.11,
1984                        "self inner-product should be close to 0.8. Instead, it's {}",
1985                        self_inner_product
1986                    );
1987                }
1988            }
1989
1990            {
1991                let QueryMeta {
1992                    inner_product_correction,
1993                    bit_sum,
1994                    offset,
1995                    metric_specific,
1996                } = q.meta();
1997
1998                let inner_product_with_centroid = metric_specific;
1999                let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
2000                preprocessed
2001                    .shifted
2002                    .iter_mut()
2003                    .for_each(|i| *i /= preprocessed.shifted_norm);
2004
2005                let mut transformed = vec![0.0f32; quantizer.output_dim()];
2006                quantizer
2007                    .transform
2008                    .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
2009                    .unwrap();
2010
2011                let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
2012                let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
2013
2014                let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
2015
2016                // Inner product correction
2017                {
2018                    let expected = (FastL2Norm).evaluate(&*shifted) * scale;
2019                    let got = inner_product_correction;
2020
2021                    let err = (expected - got).abs();
2022                    assert!(
2023                        err < 1.0e-7,
2024                        "\"innerproduct_scale\": expected {}, got {}, error = {}",
2025                        expected,
2026                        got,
2027                        err
2028                    );
2029                }
2030
2031                // Offset
2032                {
2033                    let expected = min / scale;
2034                    let got = offset;
2035
2036                    let err = (expected - got).abs();
2037                    assert!(
2038                        err < 1.0e-7,
2039                        "\"sum_scale\": expected {}, got {}, error = {}",
2040                        expected,
2041                        got,
2042                        err
2043                    );
2044                }
2045
2046                // Bit Sum
2047                {
2048                    let expected = (0..q.len())
2049                        .map(|i| q.vector().get(i).unwrap())
2050                        .sum::<i64>() as f32;
2051
2052                    let got = bit_sum;
2053
2054                    let err = (expected - got).abs();
2055                    assert!(
2056                        err < 1.0e-7,
2057                        "\"offset\": expected {}, got {}, error = {}",
2058                        expected,
2059                        got,
2060                        err
2061                    );
2062                }
2063
2064                // Inner Product with Centroid
2065                {
2066                    // Check that the shifted norm is correct.
2067                    let inner_product: MathematicalValue<f32> =
2068                        InnerProduct::evaluate(&*shifted, quantizer.shift());
2069
2070                    let err = (inner_product.into_inner() - inner_product_with_centroid).abs()
2071                        / inner_product.into_inner().abs();
2072                    assert!(
2073                        err < IP_BOUND,
2074                        "\"offset\": expected {}, got {}, error = {}",
2075                        inner_product.into_inner(),
2076                        inner_product_with_centroid,
2077                        err
2078                    );
2079                }
2080            }
2081
2082            // Check that the compensation coefficient were chosen correctly.
2083            {
2084                // Check that the bit-count is correct.
2085                let s: f32 = f.data.iter().sum::<f32>();
2086                assert_eq!(s, f.meta.sum);
2087
2088                // Check that the shifted norm is correct.
2089                {
2090                    let expected = FastL2Norm.evaluate(&*shifted);
2091                    let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
2092                    assert!(
2093                        err < 2e-7,
2094                        "failed diff check, got {}, expected {} - relative error = {}",
2095                        f.meta.shifted_norm,
2096                        expected,
2097                        err
2098                    );
2099                }
2100
2101                // Check that the shifted norm is correct. s
2102                let inner_product: MathematicalValue<f32> =
2103                    InnerProduct::evaluate(&*shifted, quantizer.shift());
2104                let err = (inner_product.into_inner() - f.meta.metric_specific).abs()
2105                    / inner_product.into_inner().abs();
2106                assert!(
2107                    err < IP_BOUND,
2108                    "\"offset\": expected {}, got {}, error = {}",
2109                    inner_product.into_inner(),
2110                    f.meta.metric_specific,
2111                    err
2112                );
2113            }
2114        };
2115
2116        for _ in 0..setup.num_trials {
2117            let i = distribution.sample(rng);
2118            let v = problem.data.row(i);
2119            test_row(v);
2120        }
2121
2122        // Ensure that if a zero vector is provided that we do not divide by zero.
2123        let zero = vec![0.0f32; quantizer.input_dim()];
2124        test_row(&zero);
2125    }
2126
2127    fn _test_oom_resiliance<T>(quantizer: &SphericalQuantizer, data: &[f32], dst: &mut T)
2128    where
2129        for<'a> T: ReborrowMut<'a>,
2130        for<'a> SphericalQuantizer: CompressIntoWith<
2131                &'a [f32],
2132                <T as ReborrowMut<'a>>::Target,
2133                ScopedAllocator<'a>,
2134                Error = CompressionError,
2135            >,
2136    {
2137        let mut succeeded = false;
2138        let mut failed = false;
2139        for max_allocations in 0..10 {
2140            match quantizer.compress_into_with(
2141                data,
2142                dst.reborrow_mut(),
2143                ScopedAllocator::new(&test_util::LimitedAllocator::new(max_allocations)),
2144            ) {
2145                Ok(()) => {
2146                    succeeded = true;
2147                }
2148                Err(CompressionError::AllocatorError(_)) => {
2149                    failed = true;
2150                }
2151                Err(other) => {
2152                    panic!("received an unexpected error: {:?}", other);
2153                }
2154            }
2155        }
2156        assert!(succeeded);
2157        assert!(failed);
2158    }
2159
2160    fn test_oom_resiliance<const Q: usize, const D: usize, Perm>(
2161        setup: &Setup,
2162        problem: &test_util::TestProblem,
2163        pre_scale: PreScale,
2164        rng: &mut StdRng,
2165    ) where
2166        Unsigned: Representation<Q>,
2167        Unsigned: Representation<D>,
2168        Perm: PermutationStrategy<Q>,
2169        for<'a> SphericalQuantizer: CompressIntoWith<
2170                &'a [f32],
2171                DataMut<'a, D>,
2172                ScopedAllocator<'a>,
2173                Error = CompressionError,
2174            >,
2175        for<'a> SphericalQuantizer: CompressIntoWith<
2176                &'a [f32],
2177                QueryMut<'a, Q, Perm>,
2178                ScopedAllocator<'a>,
2179                Error = CompressionError,
2180            >,
2181    {
2182        assert_eq!(setup.nrows, problem.data.nrows());
2183        assert_eq!(setup.ncols, problem.data.ncols());
2184
2185        let quantizer = SphericalQuantizer::train(
2186            problem.data.as_view(),
2187            setup.transform,
2188            SupportedMetric::SquaredL2,
2189            pre_scale,
2190            rng,
2191            GlobalAllocator,
2192        )
2193        .unwrap();
2194
2195        // Data.
2196        let data = problem.data.row(0);
2197        _test_oom_resiliance::<Data<D, _>>(
2198            &quantizer,
2199            data,
2200            &mut Data::new_boxed(quantizer.output_dim()),
2201        );
2202        _test_oom_resiliance::<Query<Q, Perm, _>>(
2203            &quantizer,
2204            data,
2205            &mut Query::new_boxed(quantizer.output_dim()),
2206        );
2207        _test_oom_resiliance::<FullQuery<_>>(
2208            &quantizer,
2209            data,
2210            &mut FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap(),
2211        );
2212    }
2213
2214    fn test_quantizer<const Q: usize, const D: usize, Perm>(setup: &Setup, rng: &mut StdRng)
2215    where
2216        Unsigned: Representation<Q>,
2217        Unsigned: Representation<D>,
2218        Perm: PermutationStrategy<Q>,
2219        for<'a> SphericalQuantizer: CompressIntoWith<
2220                &'a [f32],
2221                DataMut<'a, D>,
2222                ScopedAllocator<'a>,
2223                Error = CompressionError,
2224            >,
2225        for<'a> SphericalQuantizer: CompressIntoWith<
2226                &'a [f32],
2227                QueryMut<'a, Q, Perm>,
2228                ScopedAllocator<'a>,
2229                Error = CompressionError,
2230            >,
2231    {
2232        let problem = test_util::create_test_problem(setup.nrows, setup.ncols, rng);
2233        let computed_means_f32: Vec<_> = problem.means.iter().map(|i| *i as f32).collect();
2234
2235        let scales = [
2236            PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2237            PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2238            PreScale::ReciprocalMeanNorm,
2239        ];
2240
2241        for scale in scales {
2242            let ctx = &lazy_format!("dim = {}, scale = {:?}", setup.ncols, scale);
2243
2244            test_l2::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng);
2245            test_ip::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng, ctx);
2246            test_cosine::<Q, D, Perm>(setup, &problem, scale, rng);
2247        }
2248
2249        test_oom_resiliance::<Q, D, Perm>(setup, &problem, PreScale::ReciprocalMeanNorm, rng);
2250    }
2251
2252    #[test]
2253    fn test_spherical_quantizer() {
2254        let mut rng = StdRng::seed_from_u64(0xab516aef1ce61640);
2255        for dim in [56, 72, 128, 255] {
2256            let setup = Setup {
2257                transform: TransformKind::PaddingHadamard {
2258                    target_dim: TargetDim::Same,
2259                },
2260                nrows: 64,
2261                ncols: dim,
2262                num_trials: 10,
2263            };
2264
2265            test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2266            test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2267            test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2268            test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2269
2270            let setup = Setup {
2271                transform: TransformKind::DoubleHadamard {
2272                    target_dim: TargetDim::Same,
2273                },
2274                nrows: 64,
2275                ncols: dim,
2276                num_trials: 10,
2277            };
2278            test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2279            test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2280            test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2281            test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2282        }
2283    }
2284
2285    ////////////
2286    // Errors //
2287    ////////////
2288
2289    #[test]
2290    fn err_dim_cannot_be_zero() {
2291        let data = rowmajor::Owned::from_element(10, 0, 0.0f32);
2292        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2293        let err = SphericalQuantizer::train(
2294            data.as_view(),
2295            TransformKind::DoubleHadamard {
2296                target_dim: TargetDim::Same,
2297            },
2298            SupportedMetric::SquaredL2,
2299            PreScale::None,
2300            &mut rng,
2301            GlobalAllocator,
2302        )
2303        .unwrap_err();
2304        assert_eq!(err.to_string(), "data dim cannot be zero");
2305    }
2306
2307    #[test]
2308    fn err_norm_must_be_positive() {
2309        let data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2310        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2311        let err = SphericalQuantizer::train(
2312            data.as_view(),
2313            TransformKind::DoubleHadamard {
2314                target_dim: TargetDim::Same,
2315            },
2316            SupportedMetric::SquaredL2,
2317            PreScale::None,
2318            &mut rng,
2319            GlobalAllocator,
2320        )
2321        .unwrap_err();
2322        assert_eq!(err.to_string(), "norm must be positive");
2323    }
2324
2325    #[test]
2326    fn err_norm_cannot_be_infinity() {
2327        let mut data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2328        *data.element_mut(2, 5) = f32::INFINITY;
2329
2330        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2331        let err = SphericalQuantizer::train(
2332            data.as_view(),
2333            TransformKind::DoubleHadamard {
2334                target_dim: TargetDim::Same,
2335            },
2336            SupportedMetric::SquaredL2,
2337            PreScale::None,
2338            &mut rng,
2339            GlobalAllocator,
2340        )
2341        .unwrap_err();
2342        assert_eq!(err.to_string(), "computed norm contains infinity or NaN");
2343    }
2344
2345    #[test]
2346    fn err_reciprocal_norm_cannot_be_infinity() {
2347        let mut data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2348        *data.element_mut(2, 5) = 2.93863e-39;
2349
2350        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2351        let err = SphericalQuantizer::train(
2352            data.as_view(),
2353            TransformKind::DoubleHadamard {
2354                target_dim: TargetDim::Same,
2355            },
2356            SupportedMetric::SquaredL2,
2357            PreScale::ReciprocalMeanNorm,
2358            &mut rng,
2359            GlobalAllocator,
2360        )
2361        .unwrap_err();
2362        assert_eq!(err.to_string(), "reciprocal norm contains infinity or NaN");
2363    }
2364
2365    #[test]
2366    fn err_mean_norm_cannot_be_zero_generate() {
2367        let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2368        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2369        let err = SphericalQuantizer::generate(
2370            centroid,
2371            0.0,
2372            TransformKind::DoubleHadamard {
2373                target_dim: TargetDim::Same,
2374            },
2375            SupportedMetric::SquaredL2,
2376            None,
2377            &mut rng,
2378            GlobalAllocator,
2379        )
2380        .unwrap_err();
2381        assert_eq!(err.to_string(), "norm must be positive");
2382    }
2383
2384    #[test]
2385    fn err_scale_cannot_be_zero_generate() {
2386        let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2387        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2388        let err = SphericalQuantizer::generate(
2389            centroid,
2390            1.0,
2391            TransformKind::DoubleHadamard {
2392                target_dim: TargetDim::Same,
2393            },
2394            SupportedMetric::SquaredL2,
2395            Some(0.0),
2396            &mut rng,
2397            GlobalAllocator,
2398        )
2399        .unwrap_err();
2400        assert_eq!(err.to_string(), "pre-scale must be positive");
2401    }
2402
2403    #[test]
2404    fn compression_errors_data() {
2405        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2406        let data = rowmajor::Owned::<f32>::from_fn(16, 12, |_| StandardNormal {}.sample(&mut rng));
2407
2408        let quantizer = SphericalQuantizer::train(
2409            data.as_view(),
2410            TransformKind::PaddingHadamard {
2411                target_dim: TargetDim::Same,
2412            },
2413            SupportedMetric::SquaredL2,
2414            PreScale::None,
2415            &mut rng,
2416            GlobalAllocator,
2417        )
2418        .unwrap();
2419
2420        let scoped_global = ScopedAllocator::global();
2421
2422        // Input contains NaN.
2423        {
2424            let mut query: Vec<f32> = quantizer.shift().to_vec();
2425            let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2426            let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2427
2428            for i in 0..query.len() {
2429                let last = query[i];
2430                for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2431                    query[i] = v;
2432
2433                    let err = quantizer
2434                        .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2435                        .unwrap_err();
2436
2437                    assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2438
2439                    let err = quantizer
2440                        .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2441                        .unwrap_err();
2442
2443                    assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2444                }
2445                query[i] = last;
2446            }
2447        }
2448
2449        // Input has a large value.
2450        {
2451            let query: Vec<f32> = vec![1000000.0; quantizer.input_dim()];
2452            let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2453
2454            let err = quantizer
2455                .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2456                .unwrap_err();
2457
2458            let expected = "encoding error - you may need to scale the entire dataset to reduce its dynamic range";
2459
2460            assert_eq!(err.to_string(), expected, "failed for {:?}", query);
2461        }
2462
2463        // Input length
2464        for len in [quantizer.input_dim() - 1, quantizer.input_dim() + 1] {
2465            let query = vec![0.0f32; len];
2466            let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2467            let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2468
2469            let err = quantizer
2470                .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2471                .unwrap_err();
2472            assert_eq!(
2473                err,
2474                CompressionError::SourceDimensionMismatch {
2475                    expected: quantizer.input_dim(),
2476                }
2477            );
2478
2479            let err = quantizer
2480                .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2481                .unwrap_err();
2482            assert_eq!(
2483                err,
2484                CompressionError::SourceDimensionMismatch {
2485                    expected: quantizer.input_dim(),
2486                }
2487            );
2488        }
2489
2490        for len in [quantizer.output_dim() - 1, quantizer.output_dim() + 1] {
2491            let query = vec![0.0f32; quantizer.input_dim()];
2492            let mut d = Data::<1, _>::new_boxed(len);
2493            let mut q = Query::<4, BitTranspose, _>::new_boxed(len);
2494
2495            let err = quantizer
2496                .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2497                .unwrap_err();
2498            assert_eq!(
2499                err,
2500                CompressionError::DestinationDimensionMismatch {
2501                    expected: quantizer.output_dim(),
2502                }
2503            );
2504
2505            let err = quantizer
2506                .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2507                .unwrap_err();
2508            assert_eq!(
2509                err,
2510                CompressionError::DestinationDimensionMismatch {
2511                    expected: quantizer.output_dim(),
2512                }
2513            );
2514        }
2515    }
2516
2517    #[test]
2518    fn centroid_scaling_happens_in_generate() {
2519        let centroid = Poly::from_iter(
2520            [1088.6732f32, 1393.32, 1547.877].into_iter(),
2521            GlobalAllocator,
2522        )
2523        .unwrap();
2524        let mean_norm = 2359.27;
2525        let pre_scale = 1.0 / mean_norm;
2526
2527        let quantizer = SphericalQuantizer::generate(
2528            centroid,
2529            mean_norm,
2530            TransformKind::Null,
2531            SupportedMetric::InnerProduct,
2532            Some(pre_scale),
2533            &mut StdRng::seed_from_u64(10),
2534            GlobalAllocator,
2535        )
2536        .unwrap();
2537
2538        let mut v = Data::<4, _>::new_boxed(quantizer.input_dim());
2539        let data: &[f32] = &[1000.34, 1456.32, 1234.5446];
2540        assert!(
2541            quantizer
2542                .compress_into_with(data, v.reborrow_mut(), ScopedAllocator::global())
2543                .is_ok(),
2544            "if this failed, the likely culprit is exceeding the value of the 16-bit correction terms"
2545        );
2546    }
2547}
2548
2549#[cfg(feature = "flatbuffers")]
2550#[cfg(test)]
2551mod test_serialization {
2552    use rand::{SeedableRng, rngs::StdRng};
2553
2554    use super::*;
2555    use crate::{
2556        algorithms::transforms::TargetDim,
2557        flatbuffers::{self as fb, to_flatbuffer},
2558        poly, test_util,
2559    };
2560
2561    #[test]
2562    fn test_serialization_happy_path() {
2563        let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2564        let problem = test_util::create_test_problem(10, 128, &mut rng);
2565
2566        let low = NonZeroUsize::new(100).unwrap();
2567        let high = NonZeroUsize::new(150).unwrap();
2568
2569        let kinds = [
2570            // Null
2571            TransformKind::Null,
2572            // Double Hadamard
2573            TransformKind::DoubleHadamard {
2574                target_dim: TargetDim::Same,
2575            },
2576            TransformKind::DoubleHadamard {
2577                target_dim: TargetDim::Natural,
2578            },
2579            TransformKind::DoubleHadamard {
2580                target_dim: TargetDim::Override(low),
2581            },
2582            TransformKind::DoubleHadamard {
2583                target_dim: TargetDim::Override(high),
2584            },
2585            // Padding Hadamard
2586            TransformKind::PaddingHadamard {
2587                target_dim: TargetDim::Same,
2588            },
2589            TransformKind::PaddingHadamard {
2590                target_dim: TargetDim::Natural,
2591            },
2592            TransformKind::PaddingHadamard {
2593                target_dim: TargetDim::Override(low),
2594            },
2595            TransformKind::PaddingHadamard {
2596                target_dim: TargetDim::Override(high),
2597            },
2598            // Random Rotation
2599            #[cfg(all(not(miri), feature = "linalg"))]
2600            TransformKind::RandomRotation {
2601                target_dim: TargetDim::Same,
2602            },
2603            #[cfg(all(not(miri), feature = "linalg"))]
2604            TransformKind::RandomRotation {
2605                target_dim: TargetDim::Natural,
2606            },
2607            #[cfg(all(not(miri), feature = "linalg"))]
2608            TransformKind::RandomRotation {
2609                target_dim: TargetDim::Override(low),
2610            },
2611            #[cfg(all(not(miri), feature = "linalg"))]
2612            TransformKind::RandomRotation {
2613                target_dim: TargetDim::Override(high),
2614            },
2615        ];
2616
2617        let pre_scales = [
2618            PreScale::None,
2619            PreScale::Some(Positive::new(0.5).unwrap()),
2620            PreScale::Some(Positive::new(1.0).unwrap()),
2621            PreScale::Some(Positive::new(1.5).unwrap()),
2622            PreScale::ReciprocalMeanNorm,
2623        ];
2624
2625        let alloc = GlobalAllocator;
2626        for kind in kinds.into_iter() {
2627            for metric in SupportedMetric::all() {
2628                for pre_scale in pre_scales {
2629                    let quantizer = SphericalQuantizer::train(
2630                        problem.data.as_view(),
2631                        kind,
2632                        metric,
2633                        pre_scale,
2634                        &mut rng,
2635                        alloc,
2636                    )
2637                    .unwrap();
2638
2639                    let data = to_flatbuffer(|buf| quantizer.pack(buf));
2640                    let proto =
2641                        flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2642                    let reloaded = SphericalQuantizer::try_unpack(alloc, proto).unwrap();
2643                    assert_eq!(quantizer, reloaded, "failed on transform {:?}", kind);
2644                }
2645            }
2646        }
2647    }
2648
2649    #[test]
2650    fn test_error_checking() {
2651        let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2652        let problem = test_util::create_test_problem(10, 128, &mut rng);
2653
2654        let transform = TransformKind::DoubleHadamard {
2655            target_dim: TargetDim::Same,
2656        };
2657
2658        let alloc = GlobalAllocator;
2659        let mut make_quantizer = || {
2660            SphericalQuantizer::train(
2661                problem.data.as_view(),
2662                transform,
2663                SupportedMetric::SquaredL2,
2664                PreScale::None,
2665                &mut rng,
2666                alloc,
2667            )
2668            .unwrap()
2669        };
2670
2671        type E = DeserializationError;
2672
2673        // Missing norm: 0.0
2674        {
2675            let mut quantizer = make_quantizer();
2676            // SAFETY: We do not do anything with the created value and the compiler
2677            // does not know about the layout of `Positive`, so we don't need to worry
2678            // about violating layout restrictions.
2679            quantizer.mean_norm = unsafe { Positive::new_unchecked(0.0) };
2680
2681            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2682            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2683            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2684            assert_eq!(err, E::MissingNorm);
2685        }
2686
2687        // Missing norm: negative
2688        {
2689            let mut quantizer = make_quantizer();
2690
2691            // SAFETY: We do not do anything with the created value and the compiler
2692            // does not know about the layout of `Positive`, so we don't need to worry
2693            // about violating layout restrictions.
2694            quantizer.mean_norm = unsafe { Positive::new_unchecked(-1.0) };
2695
2696            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2697            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2698            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2699            assert_eq!(err, E::MissingNorm);
2700        }
2701
2702        // PreScaleNotPositive
2703        {
2704            let mut quantizer = make_quantizer();
2705
2706            // SAFETY: This really isn't safe, but we are not using the improper value in a
2707            // way that will trigger undefined behavior.
2708            quantizer.pre_scale = unsafe { Positive::new_unchecked(0.0) };
2709
2710            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2711            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2712            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2713            assert_eq!(err, E::PreScaleNotPositive);
2714        }
2715
2716        // Dim Mismatch.
2717        {
2718            let mut quantizer = make_quantizer();
2719            quantizer.shift = poly!([1.0, 2.0, 3.0], alloc).unwrap();
2720
2721            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2722            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2723            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2724            assert_eq!(err, E::DimMismatch);
2725        }
2726    }
2727}