Skip to main content

hermes_core/structures/vector/quantization/
pq.rs

1//! Product Quantization with OPQ and Anisotropic Loss (ScaNN-style)
2//!
3//! Implementation inspired by Google's ScaNN (Scalable Nearest Neighbors):
4//! - **True anisotropic quantization**: penalizes parallel error more than orthogonal
5//! - **OPQ rotation**: learns optimal rotation matrix before quantization
6//! - **Product quantization** with learned codebooks
7//! - **SIMD-accelerated** asymmetric distance computation
8
9use std::io::{self, Read, Write};
10
11use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
12#[cfg(not(feature = "native"))]
13use rand::prelude::*;
14use serde::{Deserialize, Serialize};
15
16use super::super::ivf::cluster::QuantizedCode;
17use super::Quantizer;
18
19#[cfg(target_arch = "aarch64")]
20#[allow(unused_imports)]
21use std::arch::aarch64::*;
22
23#[cfg(all(target_arch = "x86_64", feature = "native"))]
24#[allow(unused_imports)]
25use std::arch::x86_64::*;
26
27/// Magic number for codebook file
28const CODEBOOK_MAGIC: u32 = 0x5343424B; // "SCBK" - ScaNN CodeBook
29
30/// Default number of centroids per subspace (K) - must be 256 for u8 codes
31pub const DEFAULT_NUM_CENTROIDS: usize = 256;
32
33/// Default dimensions per block (ScaNN recommends 2 for best accuracy)
34pub const DEFAULT_DIMS_PER_BLOCK: usize = 2;
35
36/// Configuration for Product Quantization with OPQ and Anisotropic Loss
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PQConfig {
39    /// Vector dimension
40    pub dim: usize,
41    /// Number of subspaces (M) - computed from dim / dims_per_block
42    pub num_subspaces: usize,
43    /// Dimensions per subspace block (ScaNN recommends 2)
44    pub dims_per_block: usize,
45    /// Number of centroids per subspace (K) - typically 256 for u8 codes
46    pub num_centroids: usize,
47    /// Random seed for reproducibility
48    pub seed: u64,
49    /// Use anisotropic quantization (true ScaNN-style parallel/orthogonal weighting)
50    pub anisotropic: bool,
51    /// Anisotropic eta: ratio of parallel to orthogonal error weight (η)
52    pub aniso_eta: f32,
53    /// Anisotropic threshold T: only consider inner products >= T
54    pub aniso_threshold: f32,
55    /// Use OPQ rotation matrix (learned via SVD)
56    pub use_opq: bool,
57    /// Number of OPQ iterations
58    pub opq_iters: usize,
59}
60
61impl PQConfig {
62    /// Create config with ScaNN-recommended defaults
63    pub fn new(dim: usize) -> Self {
64        let dims_per_block = DEFAULT_DIMS_PER_BLOCK;
65        let num_subspaces = dim / dims_per_block;
66
67        Self {
68            dim,
69            num_subspaces,
70            dims_per_block,
71            num_centroids: DEFAULT_NUM_CENTROIDS,
72            seed: 42,
73            anisotropic: true,
74            aniso_eta: 10.0,
75            aniso_threshold: 0.2,
76            use_opq: true,
77            opq_iters: 10,
78        }
79    }
80
81    /// Create config with larger subspaces (faster but less accurate)
82    pub fn new_fast(dim: usize) -> Self {
83        let num_subspaces = if dim >= 256 {
84            8
85        } else if dim >= 64 {
86            4
87        } else {
88            2
89        };
90        let dims_per_block = dim / num_subspaces;
91
92        Self {
93            dim,
94            num_subspaces,
95            dims_per_block,
96            num_centroids: DEFAULT_NUM_CENTROIDS,
97            seed: 42,
98            anisotropic: true,
99            aniso_eta: 10.0,
100            aniso_threshold: 0.2,
101            use_opq: false,
102            opq_iters: 0,
103        }
104    }
105
106    /// Create balanced config (good recall/speed tradeoff)
107    /// Uses 16 subspaces for 128D+ vectors, 8 for smaller
108    pub fn new_balanced(dim: usize) -> Self {
109        let num_subspaces = if dim >= 128 {
110            16
111        } else if dim >= 64 {
112            8
113        } else {
114            4
115        };
116        let dims_per_block = dim / num_subspaces;
117
118        Self {
119            dim,
120            num_subspaces,
121            dims_per_block,
122            num_centroids: DEFAULT_NUM_CENTROIDS,
123            seed: 42,
124            anisotropic: true,
125            aniso_eta: 10.0,
126            aniso_threshold: 0.2,
127            use_opq: false,
128            opq_iters: 0,
129        }
130    }
131
132    pub fn with_dims_per_block(mut self, d: usize) -> Self {
133        assert!(
134            self.dim.is_multiple_of(d),
135            "Dimension must be divisible by dims_per_block"
136        );
137        self.dims_per_block = d;
138        self.num_subspaces = self.dim / d;
139        self
140    }
141
142    pub fn with_subspaces(mut self, m: usize) -> Self {
143        assert!(
144            self.dim.is_multiple_of(m),
145            "Dimension must be divisible by num_subspaces"
146        );
147        self.num_subspaces = m;
148        self.dims_per_block = self.dim / m;
149        self
150    }
151
152    pub fn with_centroids(mut self, k: usize) -> Self {
153        assert!(k <= 256, "Max 256 centroids for u8 codes");
154        self.num_centroids = k;
155        self
156    }
157
158    pub fn with_anisotropic(mut self, enabled: bool, eta: f32) -> Self {
159        self.anisotropic = enabled;
160        self.aniso_eta = eta;
161        self
162    }
163
164    pub fn with_opq(mut self, enabled: bool, iters: usize) -> Self {
165        self.use_opq = enabled;
166        self.opq_iters = iters;
167        self
168    }
169
170    /// Dimension of each subspace
171    pub fn subspace_dim(&self) -> usize {
172        self.dims_per_block
173    }
174}
175
176/// Quantized vector using Product Quantization
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct PQVector {
179    /// PQ codes (M bytes, one per subspace)
180    pub codes: Vec<u8>,
181    /// Original vector norm (for re-ranking or normalization)
182    pub norm: f32,
183}
184
185impl PQVector {
186    pub fn new(codes: Vec<u8>, norm: f32) -> Self {
187        Self { codes, norm }
188    }
189}
190
191impl QuantizedCode for PQVector {
192    fn size_bytes(&self) -> usize {
193        self.codes.len() + 4 // codes + norm
194    }
195}
196
197/// Learned codebook for Product Quantization with OPQ rotation
198///
199/// Trained once, shared across all segments (like CoarseCentroids).
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PQCodebook {
202    /// Configuration
203    pub config: PQConfig,
204    /// OPQ rotation matrix (dim × dim), stored row-major
205    pub rotation_matrix: Option<Vec<f32>>,
206    /// Centroids: M subspaces × K centroids × subspace_dim
207    pub centroids: Vec<f32>,
208    /// Version for merge compatibility checking
209    pub version: u64,
210    /// Precomputed centroid norms for faster distance computation
211    pub centroid_norms: Option<Vec<f32>>,
212}
213
214impl PQCodebook {
215    pub(crate) fn validate(&self) -> Result<(), String> {
216        let config = &self.config;
217        if config.dim == 0
218            || config.num_subspaces == 0
219            || config.dims_per_block == 0
220            || config.num_centroids == 0
221            || config.num_centroids > 256
222        {
223            return Err("PQ codebook has invalid zero/unbounded dimensions".to_string());
224        }
225        let covered_dim = config
226            .num_subspaces
227            .checked_mul(config.dims_per_block)
228            .ok_or_else(|| "PQ subspace dimension overflow".to_string())?;
229        if covered_dim != config.dim {
230            return Err(format!(
231                "PQ subspaces cover {covered_dim} dimensions, expected {}",
232                config.dim
233            ));
234        }
235        if !config.aniso_eta.is_finite()
236            || config.aniso_eta < 0.0
237            || !config.aniso_threshold.is_finite()
238        {
239            return Err(
240                "PQ anisotropic eta must be non-negative and all parameters must be finite"
241                    .to_string(),
242            );
243        }
244        let expected_centroids = config
245            .num_subspaces
246            .checked_mul(config.num_centroids)
247            .and_then(|count| count.checked_mul(config.dims_per_block))
248            .ok_or_else(|| "PQ centroid size overflow".to_string())?;
249        if self.centroids.len() != expected_centroids
250            || self.centroids.iter().any(|value| !value.is_finite())
251        {
252            return Err(format!(
253                "PQ centroid table is invalid: got {}, expected {expected_centroids}",
254                self.centroids.len()
255            ));
256        }
257        if let Some(rotation) = &self.rotation_matrix {
258            let expected = config
259                .dim
260                .checked_mul(config.dim)
261                .ok_or_else(|| "PQ rotation size overflow".to_string())?;
262            if rotation.len() != expected || rotation.iter().any(|value| !value.is_finite()) {
263                return Err(format!(
264                    "PQ rotation matrix is invalid: got {}, expected {expected}",
265                    rotation.len()
266                ));
267            }
268        }
269        if let Some(norms) = &self.centroid_norms {
270            let expected = config
271                .num_subspaces
272                .checked_mul(config.num_centroids)
273                .ok_or_else(|| "PQ norm table size overflow".to_string())?;
274            if norms.len() != expected
275                || norms.iter().any(|value| !value.is_finite() || *value < 0.0)
276            {
277                return Err(format!(
278                    "PQ centroid norm table is invalid: got {}, expected {expected}",
279                    norms.len()
280                ));
281            }
282        }
283        Ok(())
284    }
285
286    /// Train codebook with OPQ rotation and anisotropic loss
287    #[cfg(feature = "native")]
288    pub fn train(config: PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Self {
289        use kentro::KMeans;
290        use ndarray::Array2;
291
292        assert!(!vectors.is_empty(), "Cannot train on empty vector set");
293        assert_eq!(vectors[0].len(), config.dim, "Vector dimension mismatch");
294
295        let m = config.num_subspaces;
296        let k = config.num_centroids;
297        let sub_dim = config.subspace_dim();
298        let n = vectors.len();
299
300        // Step 1: Learn OPQ rotation matrix if enabled
301        let rotation_matrix = if config.use_opq && config.opq_iters > 0 {
302            Some(Self::learn_opq_rotation(&config, vectors, max_iters))
303        } else {
304            None
305        };
306
307        // Step 2: Apply rotation to vectors
308        let rotated_vectors: Vec<Vec<f32>> = if let Some(ref r) = rotation_matrix {
309            vectors
310                .iter()
311                .map(|v| Self::apply_rotation(r, v, config.dim))
312                .collect()
313        } else {
314            vectors.to_vec()
315        };
316
317        // Step 3: Train k-means for each subspace
318        let mut centroids = Vec::with_capacity(m * k * sub_dim);
319
320        for subspace_idx in 0..m {
321            let offset = subspace_idx * sub_dim;
322
323            let subdata: Vec<f32> = rotated_vectors
324                .iter()
325                .flat_map(|v| v[offset..offset + sub_dim].iter().copied())
326                .collect();
327
328            let actual_k = k.min(n);
329
330            let data = Array2::from_shape_vec((n, sub_dim), subdata)
331                .expect("Failed to create subspace array");
332            let mut kmeans = KMeans::new(actual_k)
333                .with_euclidean(true)
334                .with_iterations(max_iters);
335            let _ = kmeans
336                .train(data.view(), None)
337                .expect("K-means training failed");
338
339            let subspace_centroids: Vec<f32> = kmeans
340                .centroids()
341                .expect("No centroids")
342                .iter()
343                .copied()
344                .collect();
345
346            centroids.extend(subspace_centroids);
347
348            // Pad if needed
349            while centroids.len() < (subspace_idx + 1) * k * sub_dim {
350                let last_start = centroids.len() - sub_dim;
351                let last: Vec<f32> = centroids[last_start..].to_vec();
352                centroids.extend(last);
353            }
354        }
355
356        // Precompute centroid norms
357        let centroid_norms: Vec<f32> = (0..m * k)
358            .map(|i| {
359                let start = i * sub_dim;
360                if start + sub_dim <= centroids.len() {
361                    centroids[start..start + sub_dim]
362                        .iter()
363                        .map(|x| x * x)
364                        .sum::<f32>()
365                        .sqrt()
366                } else {
367                    0.0
368                }
369            })
370            .collect();
371
372        let version = std::time::SystemTime::now()
373            .duration_since(std::time::UNIX_EPOCH)
374            .unwrap_or_default()
375            .as_millis() as u64;
376
377        Self {
378            config,
379            rotation_matrix,
380            centroids,
381            version,
382            centroid_norms: Some(centroid_norms),
383        }
384    }
385
386    /// Fallback training for non-native builds (WASM)
387    #[cfg(not(feature = "native"))]
388    pub fn train(config: PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Self {
389        assert!(!vectors.is_empty(), "Cannot train on empty vector set");
390        assert_eq!(vectors[0].len(), config.dim, "Vector dimension mismatch");
391
392        let m = config.num_subspaces;
393        let k = config.num_centroids;
394        let sub_dim = config.subspace_dim();
395        let mut rng = rand::rngs::StdRng::seed_from_u64(config.seed);
396
397        let rotation_matrix = None;
398        let mut centroids = Vec::with_capacity(m * k * sub_dim);
399
400        for subspace_idx in 0..m {
401            let offset = subspace_idx * sub_dim;
402            let subvectors: Vec<Vec<f32>> = vectors
403                .iter()
404                .map(|v| v[offset..offset + sub_dim].to_vec())
405                .collect();
406
407            let subspace_centroids =
408                Self::train_subspace_scalar(&subvectors, k, sub_dim, max_iters, &mut rng);
409            centroids.extend(subspace_centroids);
410        }
411
412        let centroid_norms: Vec<f32> = (0..m * k)
413            .map(|i| {
414                let start = i * sub_dim;
415                centroids[start..start + sub_dim]
416                    .iter()
417                    .map(|x| x * x)
418                    .sum::<f32>()
419                    .sqrt()
420            })
421            .collect();
422
423        let version = std::time::SystemTime::now()
424            .duration_since(std::time::UNIX_EPOCH)
425            .unwrap_or_default()
426            .as_millis() as u64;
427
428        Self {
429            config,
430            rotation_matrix,
431            centroids,
432            version,
433            centroid_norms: Some(centroid_norms),
434        }
435    }
436
437    /// Learn OPQ rotation matrix using SVD
438    #[cfg(feature = "native")]
439    fn learn_opq_rotation(config: &PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Vec<f32> {
440        use nalgebra::DMatrix;
441
442        let dim = config.dim;
443        let n = vectors.len();
444
445        let mut rotation = DMatrix::<f32>::identity(dim, dim);
446        let data: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
447        let x = DMatrix::from_row_slice(n, dim, &data);
448
449        for _iter in 0..config.opq_iters.min(max_iters) {
450            let rotated = &x * &rotation;
451            let assignments = Self::compute_pq_assignments(config, &rotated);
452            let reconstructed = Self::reconstruct_from_assignments(config, &rotated, &assignments);
453
454            let xtx_hat = x.transpose() * &reconstructed;
455            let svd = xtx_hat.svd(true, true);
456            if let (Some(u), Some(vt)) = (svd.u, svd.v_t) {
457                let new_rotation: DMatrix<f32> = vt.transpose() * u.transpose();
458                rotation = new_rotation;
459            }
460        }
461
462        rotation.iter().copied().collect()
463    }
464
465    #[cfg(feature = "native")]
466    fn compute_pq_assignments(
467        config: &PQConfig,
468        rotated: &nalgebra::DMatrix<f32>,
469    ) -> Vec<Vec<usize>> {
470        use kentro::KMeans;
471        use ndarray::Array2;
472
473        let m = config.num_subspaces;
474        let k = config.num_centroids.min(rotated.nrows());
475        let sub_dim = config.subspace_dim();
476        let n = rotated.nrows();
477
478        let mut all_assignments = vec![vec![0usize; m]; n];
479
480        for subspace_idx in 0..m {
481            let mut subdata: Vec<f32> = Vec::with_capacity(n * sub_dim);
482            for row in 0..n {
483                for col in 0..sub_dim {
484                    subdata.push(rotated[(row, subspace_idx * sub_dim + col)]);
485                }
486            }
487
488            let data = Array2::from_shape_vec((n, sub_dim), subdata)
489                .expect("Failed to create subspace array");
490            let mut kmeans = KMeans::new(k).with_euclidean(true).with_iterations(5);
491            let clusters = kmeans
492                .train(data.view(), None)
493                .expect("K-means training failed");
494
495            // Invert cluster assignments: clusters[cluster_id] = [point_indices]
496            for (cluster_id, point_indices) in clusters.iter().enumerate() {
497                for &point_idx in point_indices {
498                    all_assignments[point_idx][subspace_idx] = cluster_id;
499                }
500            }
501        }
502
503        all_assignments
504    }
505
506    #[cfg(feature = "native")]
507    fn reconstruct_from_assignments(
508        config: &PQConfig,
509        rotated: &nalgebra::DMatrix<f32>,
510        assignments: &[Vec<usize>],
511    ) -> nalgebra::DMatrix<f32> {
512        use kentro::KMeans;
513        use ndarray::Array2;
514
515        let m = config.num_subspaces;
516        let sub_dim = config.subspace_dim();
517        let n = rotated.nrows();
518        let dim = config.dim;
519
520        let mut reconstructed = nalgebra::DMatrix::<f32>::zeros(n, dim);
521
522        for subspace_idx in 0..m {
523            let mut subdata: Vec<f32> = Vec::with_capacity(n * sub_dim);
524            for row in 0..n {
525                for col in 0..sub_dim {
526                    subdata.push(rotated[(row, subspace_idx * sub_dim + col)]);
527                }
528            }
529
530            let k = config.num_centroids.min(n);
531            let data = Array2::from_shape_vec((n, sub_dim), subdata)
532                .expect("Failed to create subspace array");
533            let mut kmeans = KMeans::new(k).with_euclidean(true).with_iterations(5);
534            let _ = kmeans
535                .train(data.view(), None)
536                .expect("K-means training failed");
537
538            let centroids = kmeans.centroids().expect("No centroids");
539
540            for (row, assignment) in assignments.iter().enumerate() {
541                let centroid_idx = assignment[subspace_idx];
542                if centroid_idx < k {
543                    for col in 0..sub_dim {
544                        reconstructed[(row, subspace_idx * sub_dim + col)] =
545                            centroids[[centroid_idx, col]];
546                    }
547                }
548            }
549        }
550
551        reconstructed
552    }
553
554    /// Apply rotation matrix to vector (SIMD-accelerated dot product per row)
555    fn apply_rotation(rotation: &[f32], vector: &[f32], dim: usize) -> Vec<f32> {
556        let mut result = vec![0.0f32; dim];
557        for i in 0..dim {
558            result[i] = crate::structures::simd::dot_product_f32(
559                &rotation[i * dim..(i + 1) * dim],
560                vector,
561                dim,
562            );
563        }
564        result
565    }
566
567    /// Scalar k-means for WASM fallback
568    #[cfg(not(feature = "native"))]
569    fn train_subspace_scalar(
570        subvectors: &[Vec<f32>],
571        k: usize,
572        sub_dim: usize,
573        max_iters: usize,
574        rng: &mut impl Rng,
575    ) -> Vec<f32> {
576        let actual_k = k.min(subvectors.len());
577        let mut centroids = Self::kmeans_plusplus_init_scalar(subvectors, actual_k, sub_dim, rng);
578
579        for _ in 0..max_iters {
580            let assignments: Vec<usize> = subvectors
581                .iter()
582                .map(|v| Self::find_nearest_scalar(&centroids, v, sub_dim))
583                .collect();
584
585            let mut new_centroids = vec![0.0f32; actual_k * sub_dim];
586            let mut counts = vec![0usize; actual_k];
587
588            for (subvec, &assignment) in subvectors.iter().zip(assignments.iter()) {
589                counts[assignment] += 1;
590                let offset = assignment * sub_dim;
591                for (j, &val) in subvec.iter().enumerate() {
592                    new_centroids[offset + j] += val;
593                }
594            }
595
596            for (c, &count) in counts.iter().enumerate().take(actual_k) {
597                if count > 0 {
598                    let offset = c * sub_dim;
599                    for j in 0..sub_dim {
600                        new_centroids[offset + j] /= count as f32;
601                    }
602                }
603            }
604
605            centroids = new_centroids;
606        }
607
608        while centroids.len() < k * sub_dim {
609            let last_start = centroids.len() - sub_dim;
610            let last: Vec<f32> = centroids[last_start..].to_vec();
611            centroids.extend(last);
612        }
613
614        centroids
615    }
616
617    #[cfg(not(feature = "native"))]
618    fn kmeans_plusplus_init_scalar(
619        subvectors: &[Vec<f32>],
620        k: usize,
621        sub_dim: usize,
622        rng: &mut impl Rng,
623    ) -> Vec<f32> {
624        let mut centroids = Vec::with_capacity(k * sub_dim);
625        let first_idx = rng.random_range(0..subvectors.len());
626        centroids.extend_from_slice(&subvectors[first_idx]);
627
628        for _ in 1..k {
629            let distances: Vec<f32> = subvectors
630                .iter()
631                .map(|v| Self::min_dist_to_centroids_scalar(&centroids, v, sub_dim))
632                .collect();
633
634            let total: f32 = distances.iter().sum();
635            let mut r = rng.random::<f32>() * total;
636            let mut chosen_idx = 0;
637            for (i, &d) in distances.iter().enumerate() {
638                r -= d;
639                if r <= 0.0 {
640                    chosen_idx = i;
641                    break;
642                }
643            }
644            centroids.extend_from_slice(&subvectors[chosen_idx]);
645        }
646
647        centroids
648    }
649
650    #[cfg(not(feature = "native"))]
651    fn min_dist_to_centroids_scalar(centroids: &[f32], vector: &[f32], sub_dim: usize) -> f32 {
652        let num_centroids = centroids.len() / sub_dim;
653        (0..num_centroids)
654            .map(|c| {
655                let offset = c * sub_dim;
656                vector
657                    .iter()
658                    .zip(&centroids[offset..offset + sub_dim])
659                    .map(|(&a, &b)| (a - b) * (a - b))
660                    .sum()
661            })
662            .fold(f32::MAX, f32::min)
663    }
664
665    #[cfg(not(feature = "native"))]
666    fn find_nearest_scalar(centroids: &[f32], vector: &[f32], sub_dim: usize) -> usize {
667        let num_centroids = centroids.len() / sub_dim;
668        (0..num_centroids)
669            .map(|c| {
670                let offset = c * sub_dim;
671                let dist: f32 = vector
672                    .iter()
673                    .zip(&centroids[offset..offset + sub_dim])
674                    .map(|(&a, &b)| (a - b) * (a - b))
675                    .sum();
676                (c, dist)
677            })
678            .min_by(|a, b| a.1.total_cmp(&b.1))
679            .map(|(c, _)| c)
680            .unwrap_or(0)
681    }
682
683    /// Find nearest centroid index
684    fn find_nearest(centroids: &[f32], vector: &[f32], sub_dim: usize) -> usize {
685        let num_centroids = centroids.len() / sub_dim;
686        let mut best_idx = 0;
687        let mut best_dist = f32::MAX;
688
689        for c in 0..num_centroids {
690            let offset = c * sub_dim;
691            let dist: f32 = vector
692                .iter()
693                .zip(&centroids[offset..offset + sub_dim])
694                .map(|(&a, &b)| (a - b) * (a - b))
695                .sum();
696
697            if dist < best_dist {
698                best_dist = dist;
699                best_idx = c;
700            }
701        }
702
703        best_idx
704    }
705
706    /// Encode a vector to PQ codes
707    pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> PQVector {
708        let m = self.config.num_subspaces;
709        let k = self.config.num_centroids;
710        let sub_dim = self.config.subspace_dim();
711
712        // Compute residual if centroid provided
713        let residual: Vec<f32> = if let Some(c) = centroid {
714            vector.iter().zip(c).map(|(&v, &c)| v - c).collect()
715        } else {
716            vector.to_vec()
717        };
718
719        // Apply rotation if present
720        let rotated: Vec<f32>;
721        let vec_to_encode = if let Some(ref r) = self.rotation_matrix {
722            rotated = Self::apply_rotation(r, &residual, self.config.dim);
723            &rotated
724        } else {
725            &residual
726        };
727
728        let mut codes = Vec::with_capacity(m);
729
730        for subspace_idx in 0..m {
731            let vec_offset = subspace_idx * sub_dim;
732            let subvec = &vec_to_encode[vec_offset..vec_offset + sub_dim];
733
734            let centroid_base = subspace_idx * k * sub_dim;
735            let centroids_slice = &self.centroids[centroid_base..centroid_base + k * sub_dim];
736
737            let nearest = Self::find_nearest(centroids_slice, subvec, sub_dim);
738            codes.push(nearest as u8);
739        }
740
741        let norm = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
742        PQVector::new(codes, norm)
743    }
744
745    /// Decode PQ codes back to approximate vector
746    pub fn decode(&self, codes: &[u8]) -> Vec<f32> {
747        let m = self.config.num_subspaces;
748        let k = self.config.num_centroids;
749        let sub_dim = self.config.subspace_dim();
750
751        let mut rotated_vector = Vec::with_capacity(self.config.dim);
752
753        for (subspace_idx, &code) in codes.iter().enumerate().take(m) {
754            let centroid_base = subspace_idx * k * sub_dim;
755            let centroid_offset = centroid_base + (code as usize) * sub_dim;
756            rotated_vector
757                .extend_from_slice(&self.centroids[centroid_offset..centroid_offset + sub_dim]);
758        }
759
760        // Apply inverse rotation if present
761        if let Some(ref r) = self.rotation_matrix {
762            Self::apply_rotation_transpose(r, &rotated_vector, self.config.dim)
763        } else {
764            rotated_vector
765        }
766    }
767
768    /// Apply transpose of rotation matrix
769    fn apply_rotation_transpose(rotation: &[f32], vector: &[f32], dim: usize) -> Vec<f32> {
770        let mut result = vec![0.0f32; dim];
771        for i in 0..dim {
772            for j in 0..dim {
773                result[i] += rotation[j * dim + i] * vector[j];
774            }
775        }
776        result
777    }
778
779    /// Get centroid for a specific subspace and code
780    #[inline]
781    pub fn get_centroid(&self, subspace_idx: usize, code: u8) -> &[f32] {
782        let k = self.config.num_centroids;
783        let sub_dim = self.config.subspace_dim();
784        let offset = subspace_idx * k * sub_dim + (code as usize) * sub_dim;
785        &self.centroids[offset..offset + sub_dim]
786    }
787
788    /// Rotate a query vector
789    pub fn rotate_query(&self, query: &[f32]) -> Vec<f32> {
790        if let Some(ref r) = self.rotation_matrix {
791            Self::apply_rotation(r, query, self.config.dim)
792        } else {
793            query.to_vec()
794        }
795    }
796
797    /// Save to binary file
798    pub fn save(&self, path: &std::path::Path) -> io::Result<()> {
799        let mut file = std::fs::File::create(path)?;
800        self.write_to(&mut file)
801    }
802
803    /// Write to any writer
804    pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
805        writer.write_u32::<LittleEndian>(CODEBOOK_MAGIC)?;
806        writer.write_u32::<LittleEndian>(2)?;
807        writer.write_u64::<LittleEndian>(self.version)?;
808        writer.write_u32::<LittleEndian>(self.config.dim as u32)?;
809        writer.write_u32::<LittleEndian>(self.config.num_subspaces as u32)?;
810        writer.write_u32::<LittleEndian>(self.config.dims_per_block as u32)?;
811        writer.write_u32::<LittleEndian>(self.config.num_centroids as u32)?;
812        writer.write_u8(if self.config.anisotropic { 1 } else { 0 })?;
813        writer.write_f32::<LittleEndian>(self.config.aniso_eta)?;
814        writer.write_f32::<LittleEndian>(self.config.aniso_threshold)?;
815        writer.write_u8(if self.config.use_opq { 1 } else { 0 })?;
816        writer.write_u32::<LittleEndian>(self.config.opq_iters as u32)?;
817
818        if let Some(ref rotation) = self.rotation_matrix {
819            writer.write_u8(1)?;
820            for &val in rotation {
821                writer.write_f32::<LittleEndian>(val)?;
822            }
823        } else {
824            writer.write_u8(0)?;
825        }
826
827        for &val in &self.centroids {
828            writer.write_f32::<LittleEndian>(val)?;
829        }
830
831        if let Some(ref norms) = self.centroid_norms {
832            writer.write_u8(1)?;
833            for &val in norms {
834                writer.write_f32::<LittleEndian>(val)?;
835            }
836        } else {
837            writer.write_u8(0)?;
838        }
839
840        Ok(())
841    }
842
843    /// Load from binary file
844    pub fn load(path: &std::path::Path) -> io::Result<Self> {
845        let data = std::fs::read(path)?;
846        Self::read_from(&mut std::io::Cursor::new(data))
847    }
848
849    /// Read from any reader
850    pub fn read_from<R: Read>(reader: &mut R) -> io::Result<Self> {
851        let magic = reader.read_u32::<LittleEndian>()?;
852        if magic != CODEBOOK_MAGIC {
853            return Err(io::Error::new(
854                io::ErrorKind::InvalidData,
855                "Invalid codebook file magic",
856            ));
857        }
858
859        let file_version = reader.read_u32::<LittleEndian>()?;
860        let version = reader.read_u64::<LittleEndian>()?;
861        let dim = reader.read_u32::<LittleEndian>()? as usize;
862        let num_subspaces = reader.read_u32::<LittleEndian>()? as usize;
863
864        let (
865            dims_per_block,
866            num_centroids,
867            anisotropic,
868            aniso_eta,
869            aniso_threshold,
870            use_opq,
871            opq_iters,
872        ) = if file_version >= 2 {
873            let dpb = reader.read_u32::<LittleEndian>()? as usize;
874            let nc = reader.read_u32::<LittleEndian>()? as usize;
875            let aniso = reader.read_u8()? != 0;
876            let eta = reader.read_f32::<LittleEndian>()?;
877            let thresh = reader.read_f32::<LittleEndian>()?;
878            let opq = reader.read_u8()? != 0;
879            let iters = reader.read_u32::<LittleEndian>()? as usize;
880            (dpb, nc, aniso, eta, thresh, opq, iters)
881        } else {
882            let nc = reader.read_u32::<LittleEndian>()? as usize;
883            let aniso = reader.read_u8()? != 0;
884            let thresh = reader.read_f32::<LittleEndian>()?;
885            let dpb = dim / num_subspaces;
886            (dpb, nc, aniso, 10.0, thresh, false, 0)
887        };
888
889        let config = PQConfig {
890            dim,
891            num_subspaces,
892            dims_per_block,
893            num_centroids,
894            seed: 42,
895            anisotropic,
896            aniso_eta,
897            aniso_threshold,
898            use_opq,
899            opq_iters,
900        };
901
902        let rotation_matrix = if file_version >= 2 {
903            let has_rotation = reader.read_u8()? != 0;
904            if has_rotation {
905                let mut rotation = vec![0.0f32; dim * dim];
906                for val in &mut rotation {
907                    *val = reader.read_f32::<LittleEndian>()?;
908                }
909                Some(rotation)
910            } else {
911                None
912            }
913        } else {
914            None
915        };
916
917        let centroid_count = num_subspaces * num_centroids * config.subspace_dim();
918        let mut centroids = vec![0.0f32; centroid_count];
919        for val in &mut centroids {
920            *val = reader.read_f32::<LittleEndian>()?;
921        }
922
923        let has_norms = reader.read_u8()? != 0;
924        let centroid_norms = if has_norms {
925            let mut norms = vec![0.0f32; num_subspaces * num_centroids];
926            for val in &mut norms {
927                *val = reader.read_f32::<LittleEndian>()?;
928            }
929            Some(norms)
930        } else {
931            None
932        };
933
934        Ok(Self {
935            config,
936            rotation_matrix,
937            centroids,
938            version,
939            centroid_norms,
940        })
941    }
942
943    /// Memory usage in bytes
944    pub fn size_bytes(&self) -> usize {
945        let centroids_size = self.centroids.len() * 4;
946        let norms_size = self
947            .centroid_norms
948            .as_ref()
949            .map(|n| n.len() * 4)
950            .unwrap_or(0);
951        let rotation_size = self
952            .rotation_matrix
953            .as_ref()
954            .map(|r| r.len() * 4)
955            .unwrap_or(0);
956        centroids_size + norms_size + rotation_size + 64
957    }
958
959    /// Estimated memory usage in bytes (alias for size_bytes)
960    pub fn estimated_memory_bytes(&self) -> usize {
961        self.size_bytes()
962    }
963}
964
965/// Precomputed distance table for fast asymmetric distance computation
966#[derive(Debug, Clone)]
967pub struct DistanceTable {
968    /// M × K table of squared distances
969    pub distances: Vec<f32>,
970    /// Number of subspaces
971    pub num_subspaces: usize,
972    /// Number of centroids per subspace
973    pub num_centroids: usize,
974}
975
976impl DistanceTable {
977    /// Build distance table for a query vector
978    pub fn build(codebook: &PQCodebook, query: &[f32], centroid: Option<&[f32]>) -> Self {
979        let m = codebook.config.num_subspaces;
980        let k = codebook.config.num_centroids;
981        let sub_dim = codebook.config.subspace_dim();
982
983        // Compute residual if centroid provided
984        let residual: Vec<f32> = if let Some(c) = centroid {
985            query.iter().zip(c).map(|(&v, &c)| v - c).collect()
986        } else {
987            query.to_vec()
988        };
989
990        // Apply rotation if present
991        let rotated_query = codebook.rotate_query(&residual);
992
993        let mut distances = Vec::with_capacity(m * k);
994
995        for subspace_idx in 0..m {
996            let query_offset = subspace_idx * sub_dim;
997            let query_sub = &rotated_query[query_offset..query_offset + sub_dim];
998
999            let centroid_base = subspace_idx * k * sub_dim;
1000
1001            for centroid_idx in 0..k {
1002                let centroid_offset = centroid_base + centroid_idx * sub_dim;
1003                let centroid = &codebook.centroids[centroid_offset..centroid_offset + sub_dim];
1004
1005                let dist: f32 = query_sub
1006                    .iter()
1007                    .zip(centroid.iter())
1008                    .map(|(&a, &b)| (a - b) * (a - b))
1009                    .sum();
1010
1011                distances.push(dist);
1012            }
1013        }
1014
1015        Self {
1016            distances,
1017            num_subspaces: m,
1018            num_centroids: k,
1019        }
1020    }
1021
1022    /// Compute approximate distance using PQ codes
1023    #[inline]
1024    pub fn compute_distance(&self, codes: &[u8]) -> f32 {
1025        let k = self.num_centroids;
1026        let mut total = 0.0f32;
1027
1028        for (subspace_idx, &code) in codes.iter().enumerate() {
1029            let table_offset = subspace_idx * k + code as usize;
1030            total += self.distances[table_offset];
1031        }
1032
1033        total
1034    }
1035}
1036
1037impl Quantizer for PQCodebook {
1038    type Code = PQVector;
1039    type Config = PQConfig;
1040    type QueryData = DistanceTable;
1041
1042    fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> Self::Code {
1043        self.encode(vector, centroid)
1044    }
1045
1046    fn prepare_query(&self, query: &[f32], centroid: Option<&[f32]>) -> Self::QueryData {
1047        DistanceTable::build(self, query, centroid)
1048    }
1049
1050    fn compute_distance(&self, query_data: &Self::QueryData, code: &Self::Code) -> f32 {
1051        query_data.compute_distance(&code.codes)
1052    }
1053
1054    fn decode(&self, code: &Self::Code) -> Option<Vec<f32>> {
1055        Some(self.decode(&code.codes))
1056    }
1057
1058    fn size_bytes(&self) -> usize {
1059        self.size_bytes()
1060    }
1061}
1062
1063#[cfg(test)]
1064mod tests {
1065    use super::*;
1066    use rand::prelude::*;
1067
1068    #[test]
1069    fn test_pq_config() {
1070        let config = PQConfig::new(128);
1071        assert_eq!(config.dim, 128);
1072        assert_eq!(config.dims_per_block, 2);
1073        assert_eq!(config.num_subspaces, 64);
1074    }
1075
1076    #[test]
1077    fn test_pq_encode_decode() {
1078        let dim = 32;
1079        let config = PQConfig::new(dim).with_opq(false, 0);
1080
1081        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
1082        let vectors: Vec<Vec<f32>> = (0..100)
1083            .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
1084            .collect();
1085
1086        let codebook = PQCodebook::train(config, &vectors, 10);
1087
1088        let test_vec: Vec<f32> = (0..dim).map(|i| i as f32 / dim as f32).collect();
1089        let code = codebook.encode(&test_vec, None);
1090
1091        assert_eq!(code.codes.len(), 16); // 32 dims / 2 dims_per_block
1092    }
1093
1094    #[test]
1095    fn test_distance_table() {
1096        let dim = 16;
1097        let config = PQConfig::new(dim).with_opq(false, 0);
1098
1099        let mut rng = rand::rngs::StdRng::seed_from_u64(123);
1100        let vectors: Vec<Vec<f32>> = (0..50)
1101            .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
1102            .collect();
1103
1104        let codebook = PQCodebook::train(config, &vectors, 5);
1105
1106        let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>()).collect();
1107        let table = DistanceTable::build(&codebook, &query, None);
1108
1109        let code = codebook.encode(&vectors[0], None);
1110        let dist = table.compute_distance(&code.codes);
1111
1112        assert!(dist >= 0.0);
1113    }
1114}