Skip to main content

ipfrs_semantic/quantization/
product.rs

1//! Product Quantization (PQ, OPQ) and benchmarking utilities.
2
3use ipfrs_core::{Error, Result};
4use nalgebra::{DMatrix, DVector};
5use serde::{Deserialize, Serialize};
6
7use super::scalar::{QuantizedVector, ScalarQuantizer};
8
9/// Product Quantization configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ProductQuantizerConfig {
12    /// Vector dimension
13    pub dimension: usize,
14    /// Number of sub-quantizers (sub-vectors)
15    pub num_subquantizers: usize,
16    /// Bits per sub-quantizer (usually 8)
17    pub bits_per_subquantizer: u8,
18    /// Codebooks for each sub-quantizer (centroids)
19    pub codebooks: Vec<Vec<Vec<f32>>>,
20}
21
22/// Product Quantizer for high compression
23///
24/// Achieves 8-32x compression by dividing vectors into sub-vectors
25/// and quantizing each with a codebook.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ProductQuantizer {
28    /// Configuration
29    pub(crate) config: ProductQuantizerConfig,
30    /// Sub-vector dimension
31    subdimension: usize,
32    /// Number of centroids per sub-quantizer
33    num_centroids: usize,
34    /// Whether trained
35    trained: bool,
36}
37
38impl ProductQuantizer {
39    /// Create a new product quantizer
40    ///
41    /// # Arguments
42    /// * `dimension` - Vector dimension (must be divisible by num_subquantizers)
43    /// * `num_subquantizers` - Number of sub-quantizers (typically 8, 16, or 32)
44    /// * `bits` - Bits per code (typically 8, giving 256 centroids)
45    pub fn new(dimension: usize, num_subquantizers: usize, bits: u8) -> Result<Self> {
46        if !dimension.is_multiple_of(num_subquantizers) {
47            return Err(Error::InvalidInput(format!(
48                "Dimension {} must be divisible by num_subquantizers {}",
49                dimension, num_subquantizers
50            )));
51        }
52
53        if bits > 16 {
54            return Err(Error::InvalidInput(
55                "Bits per subquantizer must be <= 16".to_string(),
56            ));
57        }
58
59        let subdimension = dimension / num_subquantizers;
60        let num_centroids = 1 << bits;
61
62        Ok(Self {
63            config: ProductQuantizerConfig {
64                dimension,
65                num_subquantizers,
66                bits_per_subquantizer: bits,
67                codebooks: Vec::new(),
68            },
69            subdimension,
70            num_centroids,
71            trained: false,
72        })
73    }
74
75    /// Create a standard PQ with 8 sub-quantizers and 8 bits
76    pub fn standard(dimension: usize) -> Result<Self> {
77        Self::new(dimension, 8, 8)
78    }
79
80    /// Train the product quantizer using k-means clustering
81    ///
82    /// # Arguments
83    /// * `vectors` - Training vectors
84    /// * `max_iterations` - Maximum k-means iterations
85    pub fn train(&mut self, vectors: &[Vec<f32>], max_iterations: usize) -> Result<()> {
86        if vectors.is_empty() {
87            return Err(Error::InvalidInput(
88                "Cannot train on empty vector set".to_string(),
89            ));
90        }
91
92        // Validate dimensions
93        for (i, vec) in vectors.iter().enumerate() {
94            if vec.len() != self.config.dimension {
95                return Err(Error::InvalidInput(format!(
96                    "Vector {} has dimension {}, expected {}",
97                    i,
98                    vec.len(),
99                    self.config.dimension
100                )));
101            }
102        }
103
104        // Train each sub-quantizer independently
105        self.config.codebooks = Vec::with_capacity(self.config.num_subquantizers);
106
107        for sq in 0..self.config.num_subquantizers {
108            let start = sq * self.subdimension;
109            let end = start + self.subdimension;
110
111            // Extract sub-vectors for this sub-quantizer
112            let subvectors: Vec<Vec<f32>> =
113                vectors.iter().map(|v| v[start..end].to_vec()).collect();
114
115            // Run k-means to find centroids
116            let centroids = self.kmeans(&subvectors, self.num_centroids, max_iterations)?;
117            self.config.codebooks.push(centroids);
118        }
119
120        self.trained = true;
121        Ok(())
122    }
123
124    /// Simple k-means implementation
125    fn kmeans(&self, data: &[Vec<f32>], k: usize, max_iterations: usize) -> Result<Vec<Vec<f32>>> {
126        if data.is_empty() {
127            return Err(Error::InvalidInput("Empty data for k-means".to_string()));
128        }
129
130        let dim = data[0].len();
131        let n = data.len();
132        let actual_k = k.min(n); // Can't have more centroids than data points
133
134        // Initialize centroids using k-means++ style
135        let mut centroids = Vec::with_capacity(actual_k);
136
137        // Pick first centroid deterministically using first vector
138        centroids.push(data[0].clone());
139
140        // Pick remaining centroids with probability proportional to distance
141        for _ in 1..actual_k {
142            let mut best_idx = 0;
143            let mut best_dist = 0.0f32;
144
145            for (i, vec) in data.iter().enumerate() {
146                let min_dist = centroids
147                    .iter()
148                    .map(|c| self.l2_distance(vec, c))
149                    .fold(f32::MAX, |a, b| a.min(b));
150
151                if min_dist > best_dist {
152                    best_dist = min_dist;
153                    best_idx = i;
154                }
155            }
156
157            centroids.push(data[best_idx].clone());
158        }
159
160        // Run k-means iterations
161        let mut assignments = vec![0usize; n];
162
163        for _iter in 0..max_iterations {
164            // Assign points to nearest centroid
165            let mut changed = false;
166            for (i, vec) in data.iter().enumerate() {
167                let nearest = centroids
168                    .iter()
169                    .enumerate()
170                    .map(|(j, c)| (j, self.l2_distance(vec, c)))
171                    .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
172                    .map(|(j, _)| j)
173                    .unwrap_or(0);
174
175                if assignments[i] != nearest {
176                    assignments[i] = nearest;
177                    changed = true;
178                }
179            }
180
181            if !changed {
182                break;
183            }
184
185            // Update centroids
186            let mut new_centroids = vec![vec![0.0f32; dim]; actual_k];
187            let mut counts = vec![0usize; actual_k];
188
189            for (i, vec) in data.iter().enumerate() {
190                let cluster = assignments[i];
191                counts[cluster] += 1;
192                for (j, &val) in vec.iter().enumerate() {
193                    new_centroids[cluster][j] += val;
194                }
195            }
196
197            for (i, centroid) in new_centroids.iter_mut().enumerate() {
198                if counts[i] > 0 {
199                    for val in centroid.iter_mut() {
200                        *val /= counts[i] as f32;
201                    }
202                } else {
203                    // Keep old centroid if empty
204                    *centroid = centroids[i].clone();
205                }
206            }
207
208            centroids = new_centroids;
209        }
210
211        // Ensure we have exactly k centroids (pad with duplicates if needed)
212        while centroids.len() < k {
213            centroids.push(centroids[centroids.len() - 1].clone());
214        }
215
216        Ok(centroids)
217    }
218
219    fn l2_distance(&self, a: &[f32], b: &[f32]) -> f32 {
220        a.iter()
221            .zip(b.iter())
222            .map(|(x, y)| (x - y) * (x - y))
223            .sum::<f32>()
224            .sqrt()
225    }
226
227    /// Quantize a vector to PQ codes
228    pub fn quantize(&self, vector: &[f32]) -> Result<PQCode> {
229        if !self.trained {
230            return Err(Error::InvalidInput(
231                "Product quantizer must be trained before use".to_string(),
232            ));
233        }
234
235        if vector.len() != self.config.dimension {
236            return Err(Error::InvalidInput(format!(
237                "Vector has dimension {}, expected {}",
238                vector.len(),
239                self.config.dimension
240            )));
241        }
242
243        let mut codes = Vec::with_capacity(self.config.num_subquantizers);
244
245        for sq in 0..self.config.num_subquantizers {
246            let start = sq * self.subdimension;
247            let end = start + self.subdimension;
248            let subvector = &vector[start..end];
249
250            // Find nearest centroid
251            let codebook = &self.config.codebooks[sq];
252            let (best_idx, _) = codebook
253                .iter()
254                .enumerate()
255                .map(|(i, c)| (i, self.l2_distance(subvector, c)))
256                .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
257                .unwrap_or((0, 0.0));
258
259            codes.push(best_idx as u8);
260        }
261
262        Ok(PQCode { codes })
263    }
264
265    /// Dequantize PQ codes back to approximate vector
266    pub fn dequantize(&self, code: &PQCode) -> Result<Vec<f32>> {
267        if !self.trained {
268            return Err(Error::InvalidInput(
269                "Product quantizer must be trained before use".to_string(),
270            ));
271        }
272
273        if code.codes.len() != self.config.num_subquantizers {
274            return Err(Error::InvalidInput(format!(
275                "PQ code has {} elements, expected {}",
276                code.codes.len(),
277                self.config.num_subquantizers
278            )));
279        }
280
281        let mut result = Vec::with_capacity(self.config.dimension);
282
283        for (sq, &idx) in code.codes.iter().enumerate() {
284            let centroid = &self.config.codebooks[sq][idx as usize];
285            result.extend_from_slice(centroid);
286        }
287
288        Ok(result)
289    }
290
291    /// Compute asymmetric distance (query is not quantized)
292    ///
293    /// This is the preferred method for search as it's more accurate
294    pub fn asymmetric_distance(&self, query: &[f32], code: &PQCode) -> Result<f32> {
295        if !self.trained {
296            return Err(Error::InvalidInput(
297                "Product quantizer must be trained".to_string(),
298            ));
299        }
300
301        let mut total_dist_sq = 0.0f32;
302
303        for sq in 0..self.config.num_subquantizers {
304            let start = sq * self.subdimension;
305            let end = start + self.subdimension;
306            let subquery = &query[start..end];
307            let centroid = &self.config.codebooks[sq][code.codes[sq] as usize];
308
309            for (q, c) in subquery.iter().zip(centroid.iter()) {
310                let diff = q - c;
311                total_dist_sq += diff * diff;
312            }
313        }
314
315        Ok(total_dist_sq.sqrt())
316    }
317
318    /// Precompute distance tables for fast ADC (Asymmetric Distance Computation)
319    ///
320    /// Returns a table\[sq\]\[centroid\] = distance from query subvector to centroid
321    pub fn compute_distance_table(&self, query: &[f32]) -> Result<Vec<Vec<f32>>> {
322        if !self.trained {
323            return Err(Error::InvalidInput(
324                "Product quantizer must be trained".to_string(),
325            ));
326        }
327
328        let mut table = Vec::with_capacity(self.config.num_subquantizers);
329
330        for sq in 0..self.config.num_subquantizers {
331            let start = sq * self.subdimension;
332            let end = start + self.subdimension;
333            let subquery = &query[start..end];
334
335            let distances: Vec<f32> = self.config.codebooks[sq]
336                .iter()
337                .map(|c| {
338                    subquery
339                        .iter()
340                        .zip(c.iter())
341                        .map(|(q, c)| (q - c) * (q - c))
342                        .sum::<f32>()
343                })
344                .collect();
345
346            table.push(distances);
347        }
348
349        Ok(table)
350    }
351
352    /// Fast distance computation using precomputed table
353    pub fn distance_from_table(&self, table: &[Vec<f32>], code: &PQCode) -> f32 {
354        let mut total = 0.0f32;
355        for (sq, &idx) in code.codes.iter().enumerate() {
356            total += table[sq][idx as usize];
357        }
358        total.sqrt()
359    }
360
361    /// Get compression ratio
362    pub fn compression_ratio(&self) -> f32 {
363        // Original: dimension * 4 bytes (f32)
364        // Compressed: num_subquantizers * 1 byte
365        (self.config.dimension * 4) as f32 / self.config.num_subquantizers as f32
366    }
367
368    /// Check if trained
369    pub fn is_trained(&self) -> bool {
370        self.trained
371    }
372}
373
374/// Product Quantization code
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct PQCode {
377    /// Centroid indices for each sub-quantizer
378    pub codes: Vec<u8>,
379}
380
381impl PQCode {
382    /// Get memory size in bytes
383    pub fn size_bytes(&self) -> usize {
384        self.codes.len()
385    }
386}
387
388/// Optimized Product Quantization (OPQ)
389///
390/// OPQ extends PQ by learning a rotation matrix that minimizes quantization error.
391/// It achieves better accuracy than standard PQ at the same compression ratio.
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct OptimizedProductQuantizer {
394    /// Base product quantizer
395    pq: ProductQuantizer,
396    /// Rotation matrix (dimension x dimension)
397    #[serde(with = "rotation_matrix_serde")]
398    rotation: Option<DMatrix<f32>>,
399    /// Whether rotation is trained
400    rotation_trained: bool,
401}
402
403// Custom serialization for DMatrix
404mod rotation_matrix_serde {
405    use super::DMatrix;
406    use serde::{Deserialize, Deserializer, Serialize, Serializer};
407
408    #[derive(Serialize, Deserialize)]
409    struct MatrixData {
410        nrows: usize,
411        ncols: usize,
412        data: Vec<f32>,
413    }
414
415    pub fn serialize<S>(
416        matrix: &Option<DMatrix<f32>>,
417        serializer: S,
418    ) -> std::result::Result<S::Ok, S::Error>
419    where
420        S: Serializer,
421    {
422        let opt_data = matrix.as_ref().map(|m| MatrixData {
423            nrows: m.nrows(),
424            ncols: m.ncols(),
425            data: m.as_slice().to_vec(),
426        });
427        opt_data.serialize(serializer)
428    }
429
430    pub fn deserialize<'de, D>(
431        deserializer: D,
432    ) -> std::result::Result<Option<DMatrix<f32>>, D::Error>
433    where
434        D: Deserializer<'de>,
435    {
436        let opt: Option<MatrixData> = Option::deserialize(deserializer)?;
437        Ok(opt.map(|data| DMatrix::from_vec(data.nrows, data.ncols, data.data)))
438    }
439}
440
441impl OptimizedProductQuantizer {
442    /// Create a new OPQ quantizer
443    ///
444    /// # Arguments
445    /// * `dimension` - Vector dimension (must be divisible by num_subquantizers)
446    /// * `num_subquantizers` - Number of sub-quantizers
447    /// * `bits` - Bits per code (typically 8)
448    pub fn new(dimension: usize, num_subquantizers: usize, bits: u8) -> Result<Self> {
449        let pq = ProductQuantizer::new(dimension, num_subquantizers, bits)?;
450        Ok(Self {
451            pq,
452            rotation: None,
453            rotation_trained: false,
454        })
455    }
456
457    /// Create standard OPQ with 8 sub-quantizers and 8 bits
458    pub fn standard(dimension: usize) -> Result<Self> {
459        Self::new(dimension, 8, 8)
460    }
461
462    /// Train OPQ with rotation learning
463    ///
464    /// Uses iterative optimization: alternate between learning rotation and PQ codebooks
465    ///
466    /// # Arguments
467    /// * `vectors` - Training vectors
468    /// * `max_iterations` - Max iterations for PQ k-means
469    /// * `rotation_iterations` - Iterations for rotation learning (typically 5-10)
470    #[allow(clippy::too_many_arguments)]
471    pub fn train(
472        &mut self,
473        vectors: &[Vec<f32>],
474        max_iterations: usize,
475        rotation_iterations: usize,
476    ) -> Result<()> {
477        if vectors.is_empty() {
478            return Err(Error::InvalidInput(
479                "Cannot train on empty vector set".to_string(),
480            ));
481        }
482
483        let dim = self.pq.config.dimension;
484
485        // Validate dimensions
486        for (i, vec) in vectors.iter().enumerate() {
487            if vec.len() != dim {
488                return Err(Error::InvalidInput(format!(
489                    "Vector {} has dimension {}, expected {}",
490                    i,
491                    vec.len(),
492                    dim
493                )));
494            }
495        }
496
497        // Initialize with identity rotation
498        let mut rotation = DMatrix::<f32>::identity(dim, dim);
499
500        // Iteratively optimize rotation and PQ
501        for iteration in 0..rotation_iterations {
502            // Step 1: Rotate vectors
503            let rotated = self.apply_rotation_batch(vectors, &rotation);
504
505            // Step 2: Train PQ on rotated vectors
506            self.pq.train(&rotated, max_iterations)?;
507
508            // Step 3: Learn better rotation (only if not last iteration)
509            if iteration < rotation_iterations - 1 {
510                rotation = self.learn_rotation(vectors, &self.pq)?;
511            }
512        }
513
514        self.rotation = Some(rotation);
515        self.rotation_trained = true;
516
517        Ok(())
518    }
519
520    /// Learn rotation matrix using SVD
521    ///
522    /// Finds rotation that aligns data with PQ structure
523    #[allow(dead_code)]
524    fn learn_rotation(&self, vectors: &[Vec<f32>], pq: &ProductQuantizer) -> Result<DMatrix<f32>> {
525        let dim = pq.config.dimension;
526        let n = vectors.len();
527
528        // Compute covariance between original and reconstructed vectors
529        let mut cov = DMatrix::<f32>::zeros(dim, dim);
530
531        for vec in vectors {
532            // Quantize and reconstruct
533            let code = pq.quantize(vec)?;
534            let reconstructed = pq.dequantize(&code)?;
535
536            // Compute outer product
537            let v = DVector::from_vec(vec.clone());
538            let r = DVector::from_vec(reconstructed);
539            cov += v * r.transpose();
540        }
541
542        cov /= n as f32;
543
544        // SVD to find optimal rotation
545        let svd = cov.svd(true, true);
546
547        // Rotation = U * V^T
548        match (svd.u, svd.v_t) {
549            (Some(u), Some(vt)) => Ok(u * vt),
550            _ => {
551                // If SVD fails, return identity
552                Ok(DMatrix::identity(dim, dim))
553            }
554        }
555    }
556
557    /// Apply rotation to a batch of vectors
558    fn apply_rotation_batch(&self, vectors: &[Vec<f32>], rotation: &DMatrix<f32>) -> Vec<Vec<f32>> {
559        vectors
560            .iter()
561            .map(|v| self.apply_rotation(v, rotation))
562            .collect()
563    }
564
565    /// Apply rotation to a single vector
566    fn apply_rotation(&self, vector: &[f32], rotation: &DMatrix<f32>) -> Vec<f32> {
567        let v = DVector::from_vec(vector.to_vec());
568        let rotated = rotation * v;
569        rotated.as_slice().to_vec()
570    }
571
572    /// Quantize a vector
573    pub fn quantize(&self, vector: &[f32]) -> Result<PQCode> {
574        if !self.is_trained() {
575            return Err(Error::InvalidInput("OPQ must be trained".to_string()));
576        }
577
578        // Apply rotation before quantization
579        let rotated = match &self.rotation {
580            Some(r) => self.apply_rotation(vector, r),
581            None => vector.to_vec(),
582        };
583
584        self.pq.quantize(&rotated)
585    }
586
587    /// Dequantize a code back to vector
588    pub fn dequantize(&self, code: &PQCode) -> Result<Vec<f32>> {
589        if !self.is_trained() {
590            return Err(Error::InvalidInput("OPQ must be trained".to_string()));
591        }
592
593        // Dequantize
594        let rotated = self.pq.dequantize(code)?;
595
596        // Apply inverse rotation
597        match &self.rotation {
598            Some(r) => {
599                // For orthogonal matrices, inverse = transpose
600                let r_inv = r.transpose();
601                Ok(self.apply_rotation(&rotated, &r_inv))
602            }
603            None => Ok(rotated),
604        }
605    }
606
607    /// Compute asymmetric distance (query is not quantized)
608    pub fn asymmetric_distance(&self, query: &[f32], code: &PQCode) -> Result<f32> {
609        if !self.is_trained() {
610            return Err(Error::InvalidInput("OPQ must be trained".to_string()));
611        }
612
613        // Rotate query
614        let rotated_query = match &self.rotation {
615            Some(r) => self.apply_rotation(query, r),
616            None => query.to_vec(),
617        };
618
619        self.pq.asymmetric_distance(&rotated_query, code)
620    }
621
622    /// Compute distance table for fast batch queries
623    pub fn compute_distance_table(&self, query: &[f32]) -> Result<Vec<Vec<f32>>> {
624        if !self.is_trained() {
625            return Err(Error::InvalidInput("OPQ must be trained".to_string()));
626        }
627
628        // Rotate query
629        let rotated_query = match &self.rotation {
630            Some(r) => self.apply_rotation(query, r),
631            None => query.to_vec(),
632        };
633
634        self.pq.compute_distance_table(&rotated_query)
635    }
636
637    /// Fast distance using precomputed table
638    pub fn distance_from_table(&self, table: &[Vec<f32>], code: &PQCode) -> f32 {
639        self.pq.distance_from_table(table, code)
640    }
641
642    /// Get compression ratio
643    pub fn compression_ratio(&self) -> f32 {
644        self.pq.compression_ratio()
645    }
646
647    /// Check if trained
648    pub fn is_trained(&self) -> bool {
649        self.pq.is_trained() && self.rotation_trained
650    }
651
652    /// Get the underlying PQ (for testing)
653    #[allow(dead_code)]
654    pub fn inner_pq(&self) -> &ProductQuantizer {
655        &self.pq
656    }
657}
658
659/// Quantization benchmark results
660#[derive(Debug, Clone, Serialize, Deserialize)]
661pub struct QuantizationBenchmark {
662    /// Recall at k for various k values
663    pub recall_at_k: Vec<(usize, f32)>,
664    /// Compression ratio achieved
665    pub compression_ratio: f32,
666    /// Average quantization error
667    pub avg_quantization_error: f32,
668    /// Max quantization error
669    pub max_quantization_error: f32,
670    /// Memory savings in bytes
671    pub memory_savings: usize,
672    /// Speed improvement factor (approximate)
673    pub speed_factor: f32,
674}
675
676impl QuantizationBenchmark {
677    /// Generate a summary string
678    pub fn summary(&self) -> String {
679        let recall_str: Vec<String> = self
680            .recall_at_k
681            .iter()
682            .map(|(k, r)| format!("R@{}: {:.2}%", k, r * 100.0))
683            .collect();
684
685        format!(
686            "Compression: {:.1}x, Avg Error: {:.4}, {}, Memory Saved: {} bytes",
687            self.compression_ratio,
688            self.avg_quantization_error,
689            recall_str.join(", "),
690            self.memory_savings
691        )
692    }
693}
694
695/// Benchmark utilities for quantization evaluation
696pub struct QuantizationBenchmarker;
697
698impl QuantizationBenchmarker {
699    /// Benchmark scalar quantization on a dataset
700    ///
701    /// # Arguments
702    /// * `quantizer` - Trained scalar quantizer
703    /// * `vectors` - Test vectors
704    /// * `queries` - Query vectors
705    /// * `ground_truth` - Ground truth k-NN for each query (indices into vectors)
706    /// * `k_values` - Values of k to measure recall at
707    pub fn benchmark_scalar(
708        quantizer: &ScalarQuantizer,
709        vectors: &[Vec<f32>],
710        queries: &[Vec<f32>],
711        ground_truth: &[Vec<usize>],
712        k_values: &[usize],
713    ) -> Result<QuantizationBenchmark> {
714        if !quantizer.is_trained() {
715            return Err(Error::InvalidInput("Quantizer must be trained".to_string()));
716        }
717
718        // Quantize all vectors
719        let quantized: Vec<QuantizedVector> = vectors
720            .iter()
721            .map(|v| quantizer.quantize(v))
722            .collect::<Result<Vec<_>>>()?;
723
724        // Compute quantization error
725        let mut total_error = 0.0f32;
726        let mut max_error = 0.0f32;
727
728        for (i, qv) in quantized.iter().enumerate() {
729            let restored = quantizer.dequantize(qv)?;
730            let error: f32 = vectors[i]
731                .iter()
732                .zip(restored.iter())
733                .map(|(a, b)| (a - b).powi(2))
734                .sum::<f32>()
735                .sqrt();
736            total_error += error;
737            max_error = max_error.max(error);
738        }
739
740        let avg_error = total_error / vectors.len() as f32;
741
742        // Compute recall at k
743        let mut recall_at_k = Vec::new();
744
745        for &k in k_values {
746            let mut total_recall = 0.0f32;
747
748            for (qi, query) in queries.iter().enumerate() {
749                let query_quantized = quantizer.quantize(query)?;
750
751                // Find k nearest using quantized distances
752                let mut distances: Vec<(usize, f32)> = quantized
753                    .iter()
754                    .enumerate()
755                    .map(|(i, qv)| {
756                        let dist = quantizer
757                            .distance_l2_quantized(&query_quantized, qv)
758                            .unwrap_or(f32::MAX);
759                        (i, dist)
760                    })
761                    .collect();
762
763                distances
764                    .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
765
766                let found: std::collections::HashSet<usize> =
767                    distances.iter().take(k).map(|(i, _)| *i).collect();
768
769                let gt: std::collections::HashSet<usize> =
770                    ground_truth[qi].iter().take(k).copied().collect();
771
772                let intersection = found.intersection(&gt).count();
773                total_recall += intersection as f32 / k.min(gt.len()) as f32;
774            }
775
776            let recall = total_recall / queries.len() as f32;
777            recall_at_k.push((k, recall));
778        }
779
780        // Calculate memory savings
781        let original_size = vectors.len() * vectors[0].len() * 4; // f32 = 4 bytes
782        let quantized_size = vectors.len() * vectors[0].len(); // u8 = 1 byte
783        let memory_savings = original_size - quantized_size;
784
785        Ok(QuantizationBenchmark {
786            recall_at_k,
787            compression_ratio: quantizer.compression_ratio(),
788            avg_quantization_error: avg_error,
789            max_quantization_error: max_error,
790            memory_savings,
791            speed_factor: 2.0, // Approximate for int ops vs float ops
792        })
793    }
794
795    /// Benchmark product quantization on a dataset
796    pub fn benchmark_pq(
797        pq: &ProductQuantizer,
798        vectors: &[Vec<f32>],
799        queries: &[Vec<f32>],
800        ground_truth: &[Vec<usize>],
801        k_values: &[usize],
802    ) -> Result<QuantizationBenchmark> {
803        if !pq.is_trained() {
804            return Err(Error::InvalidInput("PQ must be trained".to_string()));
805        }
806
807        // Quantize all vectors
808        let codes: Vec<PQCode> = vectors
809            .iter()
810            .map(|v| pq.quantize(v))
811            .collect::<Result<Vec<_>>>()?;
812
813        // Compute quantization error
814        let mut total_error = 0.0f32;
815        let mut max_error = 0.0f32;
816
817        for (i, code) in codes.iter().enumerate() {
818            let restored = pq.dequantize(code)?;
819            let error: f32 = vectors[i]
820                .iter()
821                .zip(restored.iter())
822                .map(|(a, b)| (a - b).powi(2))
823                .sum::<f32>()
824                .sqrt();
825            total_error += error;
826            max_error = max_error.max(error);
827        }
828
829        let avg_error = total_error / vectors.len() as f32;
830
831        // Compute recall at k using asymmetric distance
832        let mut recall_at_k = Vec::new();
833
834        for &k in k_values {
835            let mut total_recall = 0.0f32;
836
837            for (qi, query) in queries.iter().enumerate() {
838                // Use distance table for fast computation
839                let table = pq.compute_distance_table(query)?;
840
841                let mut distances: Vec<(usize, f32)> = codes
842                    .iter()
843                    .enumerate()
844                    .map(|(i, code)| (i, pq.distance_from_table(&table, code)))
845                    .collect();
846
847                distances
848                    .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
849
850                let found: std::collections::HashSet<usize> =
851                    distances.iter().take(k).map(|(i, _)| *i).collect();
852
853                let gt: std::collections::HashSet<usize> =
854                    ground_truth[qi].iter().take(k).copied().collect();
855
856                let intersection = found.intersection(&gt).count();
857                total_recall += intersection as f32 / k.min(gt.len()) as f32;
858            }
859
860            let recall = total_recall / queries.len() as f32;
861            recall_at_k.push((k, recall));
862        }
863
864        // Calculate memory savings
865        let original_size = vectors.len() * vectors[0].len() * 4;
866        let quantized_size = vectors.len() * codes[0].size_bytes();
867        let memory_savings = original_size.saturating_sub(quantized_size);
868
869        Ok(QuantizationBenchmark {
870            recall_at_k,
871            compression_ratio: pq.compression_ratio(),
872            avg_quantization_error: avg_error,
873            max_quantization_error: max_error,
874            memory_savings,
875            speed_factor: 4.0, // Approximate for table lookup vs float ops
876        })
877    }
878
879    /// Compute ground truth k-NN using brute force L2 distance
880    pub fn compute_ground_truth(
881        vectors: &[Vec<f32>],
882        queries: &[Vec<f32>],
883        k: usize,
884    ) -> Vec<Vec<usize>> {
885        queries
886            .iter()
887            .map(|query| {
888                let mut distances: Vec<(usize, f32)> = vectors
889                    .iter()
890                    .enumerate()
891                    .map(|(i, v)| {
892                        let dist: f32 = query
893                            .iter()
894                            .zip(v.iter())
895                            .map(|(a, b)| (a - b).powi(2))
896                            .sum();
897                        (i, dist)
898                    })
899                    .collect();
900
901                distances
902                    .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
903
904                distances.iter().take(k).map(|(i, _)| *i).collect()
905            })
906            .collect()
907    }
908
909    /// Compare multiple quantization methods
910    pub fn compare_methods(
911        vectors: &[Vec<f32>],
912        queries: &[Vec<f32>],
913        k_values: &[usize],
914    ) -> Result<QuantizationComparison> {
915        let max_k = *k_values.iter().max().unwrap_or(&10);
916        let ground_truth = Self::compute_ground_truth(vectors, queries, max_k);
917
918        // Benchmark scalar quantization
919        let mut sq = ScalarQuantizer::uint8(vectors[0].len());
920        sq.train(vectors)?;
921        let scalar_results =
922            Self::benchmark_scalar(&sq, vectors, queries, &ground_truth, k_values)?;
923
924        // Benchmark PQ (if dimension allows)
925        let dim = vectors[0].len();
926        let pq_results = if dim >= 8 && dim.is_multiple_of(8) {
927            let mut pq = ProductQuantizer::new(dim, 8, 8)?;
928            pq.train(vectors, 20)?;
929            Some(Self::benchmark_pq(
930                &pq,
931                vectors,
932                queries,
933                &ground_truth,
934                k_values,
935            )?)
936        } else {
937            None
938        };
939
940        Ok(QuantizationComparison {
941            scalar: scalar_results,
942            product: pq_results,
943            dataset_size: vectors.len(),
944            dimension: dim,
945        })
946    }
947}
948
949/// Comparison of multiple quantization methods
950#[derive(Debug, Clone, Serialize, Deserialize)]
951pub struct QuantizationComparison {
952    /// Scalar quantization results
953    pub scalar: QuantizationBenchmark,
954    /// Product quantization results (if applicable)
955    pub product: Option<QuantizationBenchmark>,
956    /// Dataset size
957    pub dataset_size: usize,
958    /// Vector dimension
959    pub dimension: usize,
960}
961
962impl QuantizationComparison {
963    /// Generate a comparison summary
964    pub fn summary(&self) -> String {
965        let mut result = format!(
966            "Dataset: {} vectors, {} dimensions\n\nScalar Quantization:\n  {}\n",
967            self.dataset_size,
968            self.dimension,
969            self.scalar.summary()
970        );
971
972        if let Some(ref pq) = self.product {
973            result.push_str(&format!("\nProduct Quantization:\n  {}\n", pq.summary()));
974        }
975
976        result
977    }
978
979    /// Get the best method for a given k value based on recall
980    pub fn best_method_for_k(&self, k: usize) -> (&str, f32) {
981        let scalar_recall = self
982            .scalar
983            .recall_at_k
984            .iter()
985            .find(|(kv, _)| *kv == k)
986            .map(|(_, r)| *r)
987            .unwrap_or(0.0);
988
989        if let Some(ref pq) = self.product {
990            let pq_recall = pq
991                .recall_at_k
992                .iter()
993                .find(|(kv, _)| *kv == k)
994                .map(|(_, r)| *r)
995                .unwrap_or(0.0);
996
997            if pq_recall > scalar_recall {
998                return ("ProductQuantization", pq_recall);
999            }
1000        }
1001
1002        ("ScalarQuantization", scalar_recall)
1003    }
1004}