Skip to main content

ipfrs_semantic/
vector_quantizer.rs

1//! # Vector Quantizer
2//!
3//! Production-grade product quantization (PQ) for compressing high-dimensional embedding
4//! vectors into compact codes for efficient approximate nearest-neighbor (ANN) search.
5//!
6//! ## Overview
7//!
8//! Product Quantization divides a high-dimensional vector into `M` subspaces, each of
9//! dimension `D/M`, and quantizes each subspace independently using a small codebook of
10//! `K` centroids learned via k-means. Each sub-vector is then represented by the index
11//! of its nearest centroid — a single `u8` value — so the full vector is stored as `M`
12//! bytes regardless of the original dimensionality.
13//!
14//! ## Algorithm
15//!
16//! 1. **Training**: For each subspace, run k-means on the projected sub-vectors to learn
17//!    `codes_per_subspace` centroids. Initialization picks every `N/K`-th training vector
18//!    to seed the codebook (deterministic, no external RNG dependency).
19//!
20//! 2. **Encoding**: Map each sub-vector to the index (`u8`) of its nearest centroid.
21//!
22//! 3. **Decoding**: Reconstruct the approximate full vector by concatenating the centroid
23//!    vectors retrieved from each codebook.
24//!
25//! 4. **Distance**: Asymmetric distance computes exact sub-vector distances from query
26//!    to reconstructed codes; symmetric distance decodes both codes first.
27
28use std::fmt;
29use thiserror::Error;
30
31// ---------------------------------------------------------------------------
32// VqError
33// ---------------------------------------------------------------------------
34
35/// Errors produced by [`VectorQuantizer`] operations.
36#[derive(Debug, Clone, Error)]
37pub enum VqError {
38    /// Operation requires the quantizer to be trained first.
39    #[error("quantizer has not been trained yet")]
40    NotTrained,
41    /// Input vector has wrong dimensionality.
42    #[error("dimension mismatch: expected {expected}, got {got}")]
43    DimensionMismatch { expected: usize, got: usize },
44    /// Not enough training vectors to populate all codebook entries.
45    #[error("insufficient training data: needed {needed} vectors, got {got}")]
46    InsufficientData { needed: usize, got: usize },
47    /// A quantizer code is malformed or inconsistent.
48    #[error("invalid quantizer code: {0}")]
49    InvalidCode(String),
50}
51
52// ---------------------------------------------------------------------------
53// QuantizerCode
54// ---------------------------------------------------------------------------
55
56/// Compact quantized representation of a vector.
57///
58/// Contains one `u8` index per subspace — the index of the nearest centroid in
59/// the corresponding codebook.
60#[derive(Debug, Clone, PartialEq)]
61pub struct QuantizerCode(pub Vec<u8>);
62
63impl QuantizerCode {
64    /// Number of codes (= number of subspaces).
65    #[inline]
66    pub fn len(&self) -> usize {
67        self.0.len()
68    }
69
70    /// Returns `true` if the code vector is empty.
71    #[inline]
72    pub fn is_empty(&self) -> bool {
73        self.0.is_empty()
74    }
75}
76
77impl fmt::Display for QuantizerCode {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "QuantizerCode({} subspaces)", self.0.len())
80    }
81}
82
83// ---------------------------------------------------------------------------
84// Codebook
85// ---------------------------------------------------------------------------
86
87/// A learned codebook for one PQ subspace.
88///
89/// Contains `num_codes` centroids each of dimension `subspace_dim`.
90#[derive(Debug, Clone)]
91pub struct Codebook {
92    /// Centroid vectors; `centroids[i]` has length `subspace_dim`.
93    pub centroids: Vec<Vec<f64>>,
94    /// Dimensionality of each centroid (= full dim / num_subspaces).
95    pub subspace_dim: usize,
96    /// Number of centroids in this codebook (≤ 256).
97    pub num_codes: u8,
98}
99
100impl Codebook {
101    /// Return the index of the centroid nearest to `sub_vec` in squared Euclidean distance.
102    pub fn nearest_centroid(&self, sub_vec: &[f64]) -> usize {
103        let mut best_idx = 0usize;
104        let mut best_dist = f64::MAX;
105
106        for (idx, centroid) in self.centroids.iter().enumerate() {
107            let dist = squared_euclidean_f64(sub_vec, centroid);
108            if dist < best_dist {
109                best_dist = dist;
110                best_idx = idx;
111            }
112        }
113        best_idx
114    }
115
116    /// Return a reference to the centroid identified by `code`.
117    #[inline]
118    pub fn centroid(&self, code: u8) -> &[f64] {
119        &self.centroids[code as usize]
120    }
121}
122
123// ---------------------------------------------------------------------------
124// QuantizationConfig
125// ---------------------------------------------------------------------------
126
127/// Configuration for [`VectorQuantizer`].
128#[derive(Debug, Clone)]
129pub struct QuantizationConfig {
130    /// Number of PQ subspaces `M`. The input dimension must be divisible by this value.
131    pub num_subspaces: usize,
132    /// Number of centroids per codebook (`K`). Must be ≤ 256 (fits in a `u8`).
133    pub codes_per_subspace: u8,
134    /// Maximum k-means iterations per subspace.
135    pub max_iterations: usize,
136    /// K-means convergence threshold: stop when max centroid shift < this value.
137    pub convergence_threshold: f64,
138}
139
140impl Default for QuantizationConfig {
141    fn default() -> Self {
142        Self {
143            num_subspaces: 8,
144            codes_per_subspace: u8::MAX,
145            max_iterations: 100,
146            convergence_threshold: 1e-6,
147        }
148    }
149}
150
151impl QuantizationConfig {
152    /// Create a new configuration with custom parameters.
153    pub fn new(
154        num_subspaces: usize,
155        codes_per_subspace: u8,
156        max_iterations: usize,
157        convergence_threshold: f64,
158    ) -> Self {
159        Self {
160            num_subspaces,
161            codes_per_subspace,
162            max_iterations,
163            convergence_threshold,
164        }
165    }
166}
167
168// ---------------------------------------------------------------------------
169// QuantizationStats
170// ---------------------------------------------------------------------------
171
172/// Runtime statistics for a [`VectorQuantizer`].
173#[derive(Debug, Clone, Default)]
174pub struct QuantizationStats {
175    /// Number of codebooks trained (equals `num_subspaces` after training).
176    pub codebooks_trained: usize,
177    /// Total number of vectors encoded since creation.
178    pub total_encoded: u64,
179    /// Total number of decode operations performed.
180    pub total_decoded: u64,
181    /// Running mean squared reconstruction error per dimension across all encode calls.
182    pub avg_encode_error: f64,
183}
184
185// ---------------------------------------------------------------------------
186// VectorQuantizer
187// ---------------------------------------------------------------------------
188
189/// Product-quantization based vector compressor.
190///
191/// # Example
192///
193/// ```rust
194/// use ipfrs_semantic::vector_quantizer::{VectorQuantizer, QuantizationConfig};
195///
196/// let config = QuantizationConfig::new(4, 16, 50, 1e-6);
197/// let mut vq = VectorQuantizer::new(config);
198///
199/// // Train on representative data (must have >= codes_per_subspace vectors)
200/// let training_data: Vec<Vec<f64>> = (0..32)
201///     .map(|i| (0..16).map(|d| (i * 16 + d) as f64 * 0.01).collect())
202///     .collect();
203/// vq.train(&training_data).unwrap();
204///
205/// let code = vq.encode(&vec![0.5_f64; 16]).unwrap();
206/// let reconstructed = vq.decode(&code).unwrap();
207/// assert_eq!(reconstructed.len(), 16);
208/// ```
209pub struct VectorQuantizer {
210    /// Quantization parameters.
211    pub config: QuantizationConfig,
212    /// One codebook per subspace; populated after [`train`](VectorQuantizer::train).
213    pub codebooks: Vec<Codebook>,
214    /// Whether the quantizer has been trained.
215    pub trained: bool,
216    /// Runtime statistics.
217    pub stats: QuantizationStats,
218}
219
220impl fmt::Debug for VectorQuantizer {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.debug_struct("VectorQuantizer")
223            .field("num_subspaces", &self.config.num_subspaces)
224            .field("codes_per_subspace", &self.config.codes_per_subspace)
225            .field("trained", &self.trained)
226            .field("total_encoded", &self.stats.total_encoded)
227            .finish()
228    }
229}
230
231impl VectorQuantizer {
232    /// Create a new, untrained quantizer with the given configuration.
233    pub fn new(config: QuantizationConfig) -> Self {
234        Self {
235            config,
236            codebooks: Vec::new(),
237            trained: false,
238            stats: QuantizationStats::default(),
239        }
240    }
241
242    /// Train the quantizer by running k-means over `vectors` for each subspace.
243    ///
244    /// # Errors
245    ///
246    /// - [`VqError::InsufficientData`] when `vectors.len() < codes_per_subspace`.
247    /// - [`VqError::DimensionMismatch`] when vectors have inconsistent length.
248    pub fn train(&mut self, vectors: &[Vec<f64>]) -> Result<(), VqError> {
249        let k = self.config.codes_per_subspace as usize;
250
251        if vectors.len() < k {
252            return Err(VqError::InsufficientData {
253                needed: k,
254                got: vectors.len(),
255            });
256        }
257
258        // Infer dimension from the first vector.
259        let dim = vectors[0].len();
260        let m = self.config.num_subspaces;
261
262        if m == 0 {
263            return Err(VqError::InvalidCode(
264                "num_subspaces must be > 0".to_string(),
265            ));
266        }
267
268        if !dim.is_multiple_of(m) {
269            return Err(VqError::DimensionMismatch {
270                expected: dim - (dim % m), // nearest divisible value
271                got: dim,
272            });
273        }
274
275        let sub_dim = dim / m;
276
277        // Validate that all vectors have the correct dimension.
278        for (i, v) in vectors.iter().enumerate() {
279            if v.len() != dim {
280                return Err(VqError::DimensionMismatch {
281                    expected: dim,
282                    got: v.len(),
283                });
284            }
285            let _ = i;
286        }
287
288        let mut codebooks = Vec::with_capacity(m);
289
290        for s in 0..m {
291            let start = s * sub_dim;
292            let end = start + sub_dim;
293
294            // Collect sub-vectors for this subspace.
295            let sub_vecs: Vec<&[f64]> = vectors.iter().map(|v| &v[start..end]).collect();
296
297            let centroids = kmeans_f64(
298                &sub_vecs,
299                k,
300                self.config.max_iterations,
301                self.config.convergence_threshold,
302            );
303
304            codebooks.push(Codebook {
305                centroids,
306                subspace_dim: sub_dim,
307                num_codes: self.config.codes_per_subspace,
308            });
309        }
310
311        self.codebooks = codebooks;
312        self.trained = true;
313        self.stats.codebooks_trained = m;
314
315        Ok(())
316    }
317
318    /// Encode a vector into a compact [`QuantizerCode`].
319    ///
320    /// For each subspace the sub-vector is mapped to the index of its nearest centroid.
321    ///
322    /// # Errors
323    ///
324    /// - [`VqError::NotTrained`] when the quantizer has not been trained.
325    /// - [`VqError::DimensionMismatch`] when `vector.len()` does not match training dimension.
326    pub fn encode(&mut self, vector: &[f64]) -> Result<QuantizerCode, VqError> {
327        if !self.trained {
328            return Err(VqError::NotTrained);
329        }
330
331        let expected_dim = self.expected_dim();
332        if vector.len() != expected_dim {
333            return Err(VqError::DimensionMismatch {
334                expected: expected_dim,
335                got: vector.len(),
336            });
337        }
338
339        let m = self.config.num_subspaces;
340        let sub_dim = expected_dim / m;
341        let mut codes = Vec::with_capacity(m);
342        let mut total_sq_err = 0.0f64;
343
344        for (s, cb) in self.codebooks.iter().enumerate() {
345            let start = s * sub_dim;
346            let end = start + sub_dim;
347            let sub_vec = &vector[start..end];
348
349            let idx = cb.nearest_centroid(sub_vec);
350            let code = idx as u8;
351            codes.push(code);
352
353            // Accumulate per-element squared error for this subspace.
354            let centroid = cb.centroid(code);
355            let sq_err: f64 = sub_vec
356                .iter()
357                .zip(centroid.iter())
358                .map(|(a, b)| {
359                    let d = a - b;
360                    d * d
361                })
362                .sum();
363            total_sq_err += sq_err;
364        }
365
366        // Per-dimension mean squared error across the full vector.
367        let call_error = total_sq_err / expected_dim as f64;
368
369        // Welford-style running mean.
370        let n = self.stats.total_encoded;
371        self.stats.avg_encode_error = if n == 0 {
372            call_error
373        } else {
374            self.stats.avg_encode_error
375                + (call_error - self.stats.avg_encode_error) / (n + 1) as f64
376        };
377        self.stats.total_encoded += 1;
378
379        Ok(QuantizerCode(codes))
380    }
381
382    /// Decode a [`QuantizerCode`] back into an approximate full-dimensional vector.
383    ///
384    /// Reconstructs the vector by concatenating the centroid vectors from each codebook.
385    ///
386    /// # Errors
387    ///
388    /// - [`VqError::NotTrained`] when the quantizer has not been trained.
389    /// - [`VqError::InvalidCode`] when the code length does not match the number of subspaces.
390    pub fn decode(&mut self, code: &QuantizerCode) -> Result<Vec<f64>, VqError> {
391        if !self.trained {
392            return Err(VqError::NotTrained);
393        }
394
395        let m = self.config.num_subspaces;
396        if code.len() != m {
397            return Err(VqError::InvalidCode(format!(
398                "code length {} does not match num_subspaces {}",
399                code.len(),
400                m
401            )));
402        }
403
404        let sub_dim = self.codebooks.first().map_or(0, |cb| cb.subspace_dim);
405        let mut result = Vec::with_capacity(m * sub_dim);
406
407        for (s, &c) in code.0.iter().enumerate() {
408            let cb = &self.codebooks[s];
409            if c as usize >= cb.centroids.len() {
410                return Err(VqError::InvalidCode(format!(
411                    "code {} at subspace {} is out of range (codebook has {} entries)",
412                    c,
413                    s,
414                    cb.centroids.len()
415                )));
416            }
417            result.extend_from_slice(cb.centroid(c));
418        }
419
420        self.stats.total_decoded += 1;
421
422        Ok(result)
423    }
424
425    /// Encode a batch of vectors.
426    ///
427    /// All vectors must have the same dimension as the training data.
428    ///
429    /// # Errors
430    ///
431    /// Propagates the first error encountered (see [`encode`](VectorQuantizer::encode)).
432    pub fn encode_batch(&mut self, vectors: &[Vec<f64>]) -> Result<Vec<QuantizerCode>, VqError> {
433        let mut codes = Vec::with_capacity(vectors.len());
434        for v in vectors {
435            codes.push(self.encode(v)?);
436        }
437        Ok(codes)
438    }
439
440    /// Compute the asymmetric squared L2 distance between a raw query vector and a code.
441    ///
442    /// This is more accurate than [`symmetric_distance`](VectorQuantizer::symmetric_distance)
443    /// because the query is not quantized — only the database vector is approximated.
444    ///
445    /// # Errors
446    ///
447    /// - [`VqError::NotTrained`] when not trained.
448    /// - [`VqError::DimensionMismatch`] when query length is wrong.
449    /// - [`VqError::InvalidCode`] when the code is malformed.
450    pub fn asymmetric_distance(&self, query: &[f64], code: &QuantizerCode) -> Result<f64, VqError> {
451        if !self.trained {
452            return Err(VqError::NotTrained);
453        }
454
455        let expected_dim = self.expected_dim();
456        if query.len() != expected_dim {
457            return Err(VqError::DimensionMismatch {
458                expected: expected_dim,
459                got: query.len(),
460            });
461        }
462
463        let m = self.config.num_subspaces;
464        if code.len() != m {
465            return Err(VqError::InvalidCode(format!(
466                "code length {} does not match num_subspaces {}",
467                code.len(),
468                m
469            )));
470        }
471
472        let sub_dim = expected_dim / m;
473        let mut total_dist = 0.0f64;
474
475        for (s, &c) in code.0.iter().enumerate() {
476            let cb = &self.codebooks[s];
477            if c as usize >= cb.centroids.len() {
478                return Err(VqError::InvalidCode(format!(
479                    "code {} at subspace {} is out of range",
480                    c, s
481                )));
482            }
483            let start = s * sub_dim;
484            let end = start + sub_dim;
485            let sub_vec = &query[start..end];
486            let centroid = cb.centroid(c);
487            total_dist += squared_euclidean_f64(sub_vec, centroid);
488        }
489
490        Ok(total_dist)
491    }
492
493    /// Compute the symmetric squared L2 distance between two quantizer codes.
494    ///
495    /// Both codes are decoded to full vectors before computing the distance.
496    /// This is less accurate than [`asymmetric_distance`](VectorQuantizer::asymmetric_distance)
497    /// but useful when the query is also stored as a code.
498    ///
499    /// # Errors
500    ///
501    /// - [`VqError::NotTrained`] when not trained.
502    /// - [`VqError::InvalidCode`] when either code is malformed.
503    pub fn symmetric_distance(
504        &mut self,
505        a: &QuantizerCode,
506        b: &QuantizerCode,
507    ) -> Result<f64, VqError> {
508        let decoded_a = self.decode_immutable(a)?;
509        let decoded_b = self.decode_immutable(b)?;
510        // Don't double-count the stat; only decode() increments the counter.
511        // symmetric_distance calls the internal helper that does not mutate stats.
512        Ok(squared_euclidean_f64(&decoded_a, &decoded_b))
513    }
514
515    /// Compute the per-dimension mean squared reconstruction error for a vector.
516    ///
517    /// `||vector - decode(encode(vector))||^2 / dim`
518    ///
519    /// # Errors
520    ///
521    /// Propagates errors from [`encode`](VectorQuantizer::encode) and
522    /// [`decode`](VectorQuantizer::decode).
523    pub fn quantization_error(&mut self, vector: &[f64]) -> Result<f64, VqError> {
524        let code = self.encode(vector)?;
525        let reconstructed = self.decode(&code)?;
526        let sq_err: f64 = vector
527            .iter()
528            .zip(reconstructed.iter())
529            .map(|(a, b)| {
530                let d = a - b;
531                d * d
532            })
533            .sum();
534        Ok(sq_err / vector.len() as f64)
535    }
536
537    /// Compute the mean quantization error over a batch of vectors.
538    ///
539    /// # Errors
540    ///
541    /// Propagates the first error encountered.
542    pub fn avg_error_on_batch(&mut self, vectors: &[Vec<f64>]) -> Result<f64, VqError> {
543        if vectors.is_empty() {
544            return Ok(0.0);
545        }
546        let total: f64 = vectors
547            .iter()
548            .map(|v| self.quantization_error(v))
549            .collect::<Result<Vec<f64>, VqError>>()?
550            .into_iter()
551            .sum();
552        Ok(total / vectors.len() as f64)
553    }
554
555    /// Return `(subspace_idx, num_centroids)` pairs for each codebook.
556    pub fn codebook_stats(&self) -> Vec<(usize, usize)> {
557        self.codebooks
558            .iter()
559            .enumerate()
560            .map(|(i, cb)| (i, cb.centroids.len()))
561            .collect()
562    }
563
564    // ---------------------------------------------------------------------------
565    // Private helpers
566    // ---------------------------------------------------------------------------
567
568    /// Expected input dimension based on the first codebook's sub_dim.
569    fn expected_dim(&self) -> usize {
570        self.codebooks
571            .first()
572            .map_or(0, |cb| cb.subspace_dim * self.config.num_subspaces)
573    }
574
575    /// Decode a code without mutating stats (used internally for distance computation).
576    fn decode_immutable(&self, code: &QuantizerCode) -> Result<Vec<f64>, VqError> {
577        if !self.trained {
578            return Err(VqError::NotTrained);
579        }
580
581        let m = self.config.num_subspaces;
582        if code.len() != m {
583            return Err(VqError::InvalidCode(format!(
584                "code length {} does not match num_subspaces {}",
585                code.len(),
586                m
587            )));
588        }
589
590        let sub_dim = self.codebooks.first().map_or(0, |cb| cb.subspace_dim);
591        let mut result = Vec::with_capacity(m * sub_dim);
592
593        for (s, &c) in code.0.iter().enumerate() {
594            let cb = &self.codebooks[s];
595            if c as usize >= cb.centroids.len() {
596                return Err(VqError::InvalidCode(format!(
597                    "code {} at subspace {} is out of range (codebook has {} entries)",
598                    c,
599                    s,
600                    cb.centroids.len()
601                )));
602            }
603            result.extend_from_slice(cb.centroid(c));
604        }
605
606        Ok(result)
607    }
608}
609
610// ---------------------------------------------------------------------------
611// Internal k-means implementation (f64)
612// ---------------------------------------------------------------------------
613
614/// Run Lloyd's k-means algorithm on a slice of sub-vectors.
615///
616/// - `data`: equal-length sub-vectors to cluster.
617/// - `k`: desired number of centroids (clamped to `data.len()`).
618/// - `max_iters`: maximum Lloyd iterations.
619/// - `tol`: convergence threshold; stops when max centroid shift < `tol`.
620///
621/// Centroid initialisation: pick every `n / k`-th vector (stride-based deterministic
622/// seeding — no random number generator required).
623fn kmeans_f64(data: &[&[f64]], k: usize, max_iters: usize, tol: f64) -> Vec<Vec<f64>> {
624    if data.is_empty() || k == 0 {
625        return Vec::new();
626    }
627
628    let dim = data[0].len();
629    let n = data.len();
630    let actual_k = k.min(n);
631
632    // Stride-based deterministic initialisation.
633    let stride = if actual_k >= n { 1 } else { n / actual_k };
634    let mut centroids: Vec<Vec<f64>> = (0..actual_k)
635        .map(|i| data[(i * stride).min(n - 1)].to_vec())
636        .collect();
637
638    let mut assignments = vec![0usize; n];
639
640    for _iter in 0..max_iters {
641        // ---- Assignment step ------------------------------------------------
642        for (i, sv) in data.iter().enumerate() {
643            let mut best = 0usize;
644            let mut best_dist = f64::MAX;
645            for (j, c) in centroids.iter().enumerate() {
646                let d = squared_euclidean_f64(sv, c);
647                if d < best_dist {
648                    best_dist = d;
649                    best = j;
650                }
651            }
652            assignments[i] = best;
653        }
654
655        // ---- Update step ----------------------------------------------------
656        let mut sums = vec![vec![0.0f64; dim]; actual_k];
657        let mut counts = vec![0usize; actual_k];
658
659        for (i, sv) in data.iter().enumerate() {
660            let c = assignments[i];
661            counts[c] += 1;
662            for (d, &x) in sv.iter().enumerate() {
663                sums[c][d] += x;
664            }
665        }
666
667        let mut max_shift = 0.0f64;
668        let mut new_centroids = centroids.clone();
669
670        for j in 0..actual_k {
671            if counts[j] > 0 {
672                let inv = 1.0 / counts[j] as f64;
673                let new_c: Vec<f64> = sums[j].iter().map(|&s| s * inv).collect();
674                let shift = squared_euclidean_f64(&new_c, &centroids[j]).sqrt();
675                if shift > max_shift {
676                    max_shift = shift;
677                }
678                new_centroids[j] = new_c;
679            }
680            // Centroid with no assigned points keeps its previous position.
681        }
682
683        centroids = new_centroids;
684
685        if max_shift < tol {
686            break;
687        }
688    }
689
690    centroids
691}
692
693// ---------------------------------------------------------------------------
694// Utility functions
695// ---------------------------------------------------------------------------
696
697/// Squared Euclidean distance between two equal-length f64 slices.
698#[inline]
699fn squared_euclidean_f64(a: &[f64], b: &[f64]) -> f64 {
700    a.iter()
701        .zip(b.iter())
702        .map(|(x, y)| {
703            let d = x - y;
704            d * d
705        })
706        .sum()
707}
708
709// ---------------------------------------------------------------------------
710// Tests
711// ---------------------------------------------------------------------------
712
713#[cfg(test)]
714mod tests {
715    use crate::vector_quantizer::{
716        Codebook, QuantizationConfig, QuantizationStats, QuantizerCode, VectorQuantizer, VqError,
717    };
718
719    // -----------------------------------------------------------------------
720    // Helpers
721    // -----------------------------------------------------------------------
722
723    /// Build a deterministic `VectorQuantizer` config with small parameters.
724    fn small_config(num_subspaces: usize, codes_per_subspace: u8) -> QuantizationConfig {
725        QuantizationConfig::new(num_subspaces, codes_per_subspace, 50, 1e-6)
726    }
727
728    /// Generate `n` linearly spaced vectors of `dim` dimensions.
729    fn make_vectors(n: usize, dim: usize) -> Vec<Vec<f64>> {
730        (0..n)
731            .map(|i| (0..dim).map(|d| (i * dim + d) as f64 * 0.01).collect())
732            .collect()
733    }
734
735    /// Build a trained `VectorQuantizer` with `dim`-dimensional vectors.
736    ///
737    /// Uses `n` training vectors (must be ≥ codes_per_subspace).
738    fn trained_vq(dim: usize, num_subspaces: usize, codes: u8, n: usize) -> VectorQuantizer {
739        let cfg = small_config(num_subspaces, codes);
740        let mut vq = VectorQuantizer::new(cfg);
741        let data = make_vectors(n, dim);
742        vq.train(&data).expect("training should succeed");
743        vq
744    }
745
746    // -----------------------------------------------------------------------
747    // 1. QuantizationConfig defaults
748    // -----------------------------------------------------------------------
749
750    #[test]
751    fn test_config_default_values() {
752        let cfg = QuantizationConfig::default();
753        assert_eq!(cfg.num_subspaces, 8);
754        assert_eq!(cfg.codes_per_subspace, u8::MAX);
755        assert_eq!(cfg.max_iterations, 100);
756        assert!((cfg.convergence_threshold - 1e-6).abs() < f64::EPSILON * 100.0);
757    }
758
759    // -----------------------------------------------------------------------
760    // 2. QuantizationConfig custom constructor
761    // -----------------------------------------------------------------------
762
763    #[test]
764    fn test_config_custom_values() {
765        let cfg = QuantizationConfig::new(4, 32, 50, 1e-4);
766        assert_eq!(cfg.num_subspaces, 4);
767        assert_eq!(cfg.codes_per_subspace, 32u8);
768        assert_eq!(cfg.max_iterations, 50);
769        assert!((cfg.convergence_threshold - 1e-4).abs() < 1e-12);
770    }
771
772    // -----------------------------------------------------------------------
773    // 3. VectorQuantizer::new initial state
774    // -----------------------------------------------------------------------
775
776    #[test]
777    fn test_new_is_untrained() {
778        let vq = VectorQuantizer::new(QuantizationConfig::default());
779        assert!(!vq.trained);
780    }
781
782    #[test]
783    fn test_new_has_empty_codebooks() {
784        let vq = VectorQuantizer::new(QuantizationConfig::default());
785        assert!(vq.codebooks.is_empty());
786    }
787
788    #[test]
789    fn test_new_stats_are_zero() {
790        let vq = VectorQuantizer::new(QuantizationConfig::default());
791        assert_eq!(vq.stats.total_encoded, 0);
792        assert_eq!(vq.stats.total_decoded, 0);
793        assert_eq!(vq.stats.codebooks_trained, 0);
794        assert_eq!(vq.stats.avg_encode_error, 0.0);
795    }
796
797    // -----------------------------------------------------------------------
798    // 4. train — insufficient data error
799    // -----------------------------------------------------------------------
800
801    #[test]
802    fn test_train_insufficient_data() {
803        let cfg = small_config(4, 16);
804        let mut vq = VectorQuantizer::new(cfg);
805        let data = make_vectors(4, 16); // only 4 vectors, need 16
806        let result = vq.train(&data);
807        assert!(matches!(result, Err(VqError::InsufficientData { .. })));
808    }
809
810    // -----------------------------------------------------------------------
811    // 5. train — dimension mismatch error
812    // -----------------------------------------------------------------------
813
814    #[test]
815    fn test_train_dimension_not_divisible() {
816        let cfg = QuantizationConfig::new(3, 4, 50, 1e-6); // subspaces=3
817        let mut vq = VectorQuantizer::new(cfg);
818        // dim=10, not divisible by 3
819        let data = make_vectors(10, 10);
820        let result = vq.train(&data);
821        assert!(matches!(result, Err(VqError::DimensionMismatch { .. })));
822    }
823
824    // -----------------------------------------------------------------------
825    // 6. train — sets trained flag
826    // -----------------------------------------------------------------------
827
828    #[test]
829    fn test_train_sets_trained_flag() {
830        let vq = trained_vq(16, 4, 4, 20);
831        assert!(vq.trained);
832    }
833
834    // -----------------------------------------------------------------------
835    // 7. train — codebook count equals num_subspaces
836    // -----------------------------------------------------------------------
837
838    #[test]
839    fn test_train_codebook_count() {
840        let m = 4;
841        let vq = trained_vq(16, m, 4, 20);
842        assert_eq!(vq.codebooks.len(), m);
843    }
844
845    // -----------------------------------------------------------------------
846    // 8. train — codebook stats populated correctly
847    // -----------------------------------------------------------------------
848
849    #[test]
850    fn test_train_stats_codebooks_trained() {
851        let m = 4;
852        let vq = trained_vq(16, m, 4, 20);
853        assert_eq!(vq.stats.codebooks_trained, m);
854    }
855
856    // -----------------------------------------------------------------------
857    // 9. encode — fails when not trained
858    // -----------------------------------------------------------------------
859
860    #[test]
861    fn test_encode_not_trained_error() {
862        let mut vq = VectorQuantizer::new(small_config(4, 4));
863        let result = vq.encode(&[0.0f64; 16]);
864        assert!(matches!(result, Err(VqError::NotTrained)));
865    }
866
867    // -----------------------------------------------------------------------
868    // 10. encode — fails on wrong dimension
869    // -----------------------------------------------------------------------
870
871    #[test]
872    fn test_encode_dimension_mismatch() {
873        let mut vq = trained_vq(16, 4, 4, 20);
874        let result = vq.encode(&[0.0f64; 8]); // wrong: 8 instead of 16
875        assert!(matches!(
876            result,
877            Err(VqError::DimensionMismatch {
878                expected: 16,
879                got: 8
880            })
881        ));
882    }
883
884    // -----------------------------------------------------------------------
885    // 11. encode — code length equals num_subspaces
886    // -----------------------------------------------------------------------
887
888    #[test]
889    fn test_encode_code_length() {
890        let m = 4;
891        let mut vq = trained_vq(16, m, 4, 20);
892        let code = vq.encode(&[0.5f64; 16]).expect("encode succeeded");
893        assert_eq!(code.len(), m);
894    }
895
896    // -----------------------------------------------------------------------
897    // 12. encode — increments total_encoded
898    // -----------------------------------------------------------------------
899
900    #[test]
901    fn test_encode_increments_stat() {
902        let mut vq = trained_vq(16, 4, 4, 20);
903        assert_eq!(vq.stats.total_encoded, 0);
904        vq.encode(&[0.1f64; 16])
905            .expect("test: encode 0.1 vector should succeed");
906        vq.encode(&[0.2f64; 16])
907            .expect("test: encode 0.2 vector should succeed");
908        assert_eq!(vq.stats.total_encoded, 2);
909    }
910
911    // -----------------------------------------------------------------------
912    // 13. decode — fails when not trained
913    // -----------------------------------------------------------------------
914
915    #[test]
916    fn test_decode_not_trained_error() {
917        let mut vq = VectorQuantizer::new(small_config(4, 4));
918        let code = QuantizerCode(vec![0u8; 4]);
919        let result = vq.decode(&code);
920        assert!(matches!(result, Err(VqError::NotTrained)));
921    }
922
923    // -----------------------------------------------------------------------
924    // 14. decode — fails on wrong code length
925    // -----------------------------------------------------------------------
926
927    #[test]
928    fn test_decode_invalid_code_length() {
929        let mut vq = trained_vq(16, 4, 4, 20);
930        let code = QuantizerCode(vec![0u8; 3]); // wrong: 3 instead of 4
931        let result = vq.decode(&code);
932        assert!(matches!(result, Err(VqError::InvalidCode(_))));
933    }
934
935    // -----------------------------------------------------------------------
936    // 15. decode — reconstructed vector has correct length
937    // -----------------------------------------------------------------------
938
939    #[test]
940    fn test_decode_output_length() {
941        let dim = 16;
942        let mut vq = trained_vq(dim, 4, 4, 20);
943        let code = vq
944            .encode(&vec![0.5f64; dim])
945            .expect("test: encode 0.5 vector should succeed");
946        let decoded = vq
947            .decode(&code)
948            .expect("test: decode of valid code should succeed");
949        assert_eq!(decoded.len(), dim);
950    }
951
952    // -----------------------------------------------------------------------
953    // 16. decode — increments total_decoded
954    // -----------------------------------------------------------------------
955
956    #[test]
957    fn test_decode_increments_stat() {
958        let mut vq = trained_vq(16, 4, 4, 20);
959        let code = vq
960            .encode(&[0.5f64; 16])
961            .expect("test: encode 0.5 vector should succeed");
962        let before = vq.stats.total_decoded;
963        vq.decode(&code)
964            .expect("test: decode of valid code should succeed");
965        assert_eq!(vq.stats.total_decoded, before + 1);
966    }
967
968    // -----------------------------------------------------------------------
969    // 17. encode + decode round-trip: dimension preserved
970    // -----------------------------------------------------------------------
971
972    #[test]
973    fn test_encode_decode_round_trip_dim() {
974        let dim = 32;
975        let mut vq = trained_vq(dim, 4, 4, 20);
976        let vec = make_vectors(1, dim).remove(0);
977        let code = vq
978            .encode(&vec)
979            .expect("test: encode of valid vector should succeed");
980        let decoded = vq
981            .decode(&code)
982            .expect("test: decode of valid code should succeed");
983        assert_eq!(decoded.len(), dim);
984    }
985
986    // -----------------------------------------------------------------------
987    // 18. encode_batch — returns same count as input
988    // -----------------------------------------------------------------------
989
990    #[test]
991    fn test_encode_batch_count() {
992        let dim = 16;
993        let mut vq = trained_vq(dim, 4, 4, 20);
994        let vecs = make_vectors(5, dim);
995        let codes = vq
996            .encode_batch(&vecs)
997            .expect("test: encode_batch of valid vectors should succeed");
998        assert_eq!(codes.len(), 5);
999    }
1000
1001    // -----------------------------------------------------------------------
1002    // 19. encode_batch — fails if any vector has wrong dimension
1003    // -----------------------------------------------------------------------
1004
1005    #[test]
1006    fn test_encode_batch_dimension_error() {
1007        let dim = 16;
1008        let mut vq = trained_vq(dim, 4, 4, 20);
1009        let mut vecs = make_vectors(3, dim);
1010        vecs.push(vec![0.0f64; 8]); // wrong dimension
1011        let result = vq.encode_batch(&vecs);
1012        assert!(result.is_err());
1013    }
1014
1015    // -----------------------------------------------------------------------
1016    // 20. asymmetric_distance — self distance is zero
1017    // -----------------------------------------------------------------------
1018
1019    #[test]
1020    fn test_asymmetric_distance_self_zero() {
1021        let dim = 16;
1022        let mut vq = trained_vq(dim, 4, 4, 20);
1023        let vec = vec![0.5f64; dim];
1024        let code = vq
1025            .encode(&vec)
1026            .expect("test: encode 0.5 vector should succeed");
1027        // Decode the code to get the reconstructed vector, then measure asymmetric distance.
1028        let decoded = vq
1029            .decode(&code)
1030            .expect("test: decode of valid code should succeed");
1031        let dist = vq
1032            .asymmetric_distance(&decoded, &code)
1033            .expect("test: asymmetric_distance to self should succeed");
1034        assert!(
1035            dist < 1e-10,
1036            "asymmetric distance to self should be ~0, got {dist}"
1037        );
1038    }
1039
1040    // -----------------------------------------------------------------------
1041    // 21. asymmetric_distance — fails when not trained
1042    // -----------------------------------------------------------------------
1043
1044    #[test]
1045    fn test_asymmetric_distance_not_trained() {
1046        let vq = VectorQuantizer::new(small_config(4, 4));
1047        let code = QuantizerCode(vec![0u8; 4]);
1048        let result = vq.asymmetric_distance(&[0.0f64; 16], &code);
1049        assert!(matches!(result, Err(VqError::NotTrained)));
1050    }
1051
1052    // -----------------------------------------------------------------------
1053    // 22. asymmetric_distance — dimension mismatch error
1054    // -----------------------------------------------------------------------
1055
1056    #[test]
1057    fn test_asymmetric_distance_dimension_mismatch() {
1058        let dim = 16;
1059        let mut vq = trained_vq(dim, 4, 4, 20);
1060        let code = vq
1061            .encode(&vec![0.5f64; dim])
1062            .expect("test: encode 0.5 vector should succeed");
1063        let result = vq.asymmetric_distance(&[0.0f64; 8], &code);
1064        assert!(matches!(result, Err(VqError::DimensionMismatch { .. })));
1065    }
1066
1067    // -----------------------------------------------------------------------
1068    // 23. symmetric_distance — self distance is zero
1069    // -----------------------------------------------------------------------
1070
1071    #[test]
1072    fn test_symmetric_distance_self_zero() {
1073        let dim = 16;
1074        let mut vq = trained_vq(dim, 4, 4, 20);
1075        let vec = vec![0.5f64; dim];
1076        let code = vq
1077            .encode(&vec)
1078            .expect("test: encode 0.5 vector should succeed");
1079        let dist = vq
1080            .symmetric_distance(&code.clone(), &code)
1081            .expect("test: symmetric_distance to self should succeed");
1082        assert!(
1083            dist < 1e-10,
1084            "symmetric distance to self should be 0, got {dist}"
1085        );
1086    }
1087
1088    // -----------------------------------------------------------------------
1089    // 24. symmetric_distance — is symmetric (a,b) == (b,a)
1090    // -----------------------------------------------------------------------
1091
1092    #[test]
1093    fn test_symmetric_distance_is_symmetric() {
1094        let dim = 16;
1095        let mut vq = trained_vq(dim, 4, 4, 20);
1096        let code_a = vq
1097            .encode(&vec![0.1f64; dim])
1098            .expect("test: encode 0.1 vector should succeed");
1099        let code_b = vq
1100            .encode(&vec![0.9f64; dim])
1101            .expect("test: encode 0.9 vector should succeed");
1102        let dist_ab = vq
1103            .symmetric_distance(&code_a, &code_b)
1104            .expect("test: symmetric_distance(a,b) should succeed");
1105        let dist_ba = vq
1106            .symmetric_distance(&code_b, &code_a)
1107            .expect("test: symmetric_distance(b,a) should succeed");
1108        assert!(
1109            (dist_ab - dist_ba).abs() < 1e-10,
1110            "distance must be symmetric"
1111        );
1112    }
1113
1114    // -----------------------------------------------------------------------
1115    // 25. quantization_error — is non-negative
1116    // -----------------------------------------------------------------------
1117
1118    #[test]
1119    fn test_quantization_error_non_negative() {
1120        let dim = 16;
1121        let mut vq = trained_vq(dim, 4, 4, 20);
1122        let err = vq
1123            .quantization_error(&vec![0.5f64; dim])
1124            .expect("test: quantization_error should succeed on trained vq");
1125        assert!(
1126            err >= 0.0,
1127            "quantization error must be non-negative, got {err}"
1128        );
1129    }
1130
1131    // -----------------------------------------------------------------------
1132    // 26. quantization_error — exact match gives zero error
1133    // -----------------------------------------------------------------------
1134
1135    #[test]
1136    fn test_quantization_error_centroid_is_zero() {
1137        // Build a VQ manually where the single centroid equals our query.
1138        let cfg = QuantizationConfig::new(1, 1, 10, 1e-10);
1139        let mut vq = VectorQuantizer::new(cfg);
1140        // One training vector, one subspace, one centroid → centroid == training vector.
1141        let query = vec![1.0f64, 2.0, 3.0, 4.0];
1142        vq.train(std::slice::from_ref(&query))
1143            .expect("test: training single-vector single-subspace should succeed");
1144        let err = vq
1145            .quantization_error(&query)
1146            .expect("test: quantization_error on exact centroid match should succeed");
1147        assert!(
1148            err < 1e-10,
1149            "error should be ~0 for exact centroid match, got {err}"
1150        );
1151    }
1152
1153    // -----------------------------------------------------------------------
1154    // 27. avg_error_on_batch — empty batch returns 0
1155    // -----------------------------------------------------------------------
1156
1157    #[test]
1158    fn test_avg_error_empty_batch() {
1159        let mut vq = trained_vq(16, 4, 4, 20);
1160        let result = vq
1161            .avg_error_on_batch(&[])
1162            .expect("test: avg_error_on_batch of empty slice should return Ok(0.0)");
1163        assert_eq!(result, 0.0);
1164    }
1165
1166    // -----------------------------------------------------------------------
1167    // 28. avg_error_on_batch — single vector matches quantization_error
1168    // -----------------------------------------------------------------------
1169
1170    #[test]
1171    fn test_avg_error_single_vector() {
1172        let dim = 16;
1173        let vec = vec![0.5f64; dim];
1174        // Use two fresh quantizers trained on the same data for a fair comparison.
1175        let cfg = small_config(4, 4);
1176        let mut vq2 = VectorQuantizer::new(cfg);
1177        let data = make_vectors(20, dim);
1178        vq2.train(&data).expect("test: training vq2 should succeed");
1179        let single_err = vq2
1180            .quantization_error(&vec)
1181            .expect("test: quantization_error on vq2 should succeed");
1182
1183        let cfg2 = small_config(4, 4);
1184        let mut vq3 = VectorQuantizer::new(cfg2);
1185        vq3.train(&data).expect("test: training vq3 should succeed");
1186        let batch_err = vq3
1187            .avg_error_on_batch(&[vec])
1188            .expect("test: avg_error_on_batch on vq3 should succeed");
1189
1190        assert!(
1191            (single_err - batch_err).abs() < 1e-10,
1192            "avg error of one vector should equal its individual error"
1193        );
1194    }
1195
1196    // -----------------------------------------------------------------------
1197    // 29. codebook_stats — returns correct count
1198    // -----------------------------------------------------------------------
1199
1200    #[test]
1201    fn test_codebook_stats_length() {
1202        let m = 4;
1203        let vq = trained_vq(16, m, 4, 20);
1204        let stats = vq.codebook_stats();
1205        assert_eq!(stats.len(), m);
1206    }
1207
1208    // -----------------------------------------------------------------------
1209    // 30. codebook_stats — subspace indices are 0..m-1
1210    // -----------------------------------------------------------------------
1211
1212    #[test]
1213    fn test_codebook_stats_subspace_indices() {
1214        let m = 4;
1215        let vq = trained_vq(16, m, 4, 20);
1216        let stats = vq.codebook_stats();
1217        for (i, (subspace_idx, _)) in stats.iter().enumerate() {
1218            assert_eq!(*subspace_idx, i);
1219        }
1220    }
1221
1222    // -----------------------------------------------------------------------
1223    // 31. codebook_stats — centroid count bounded by codes_per_subspace
1224    // -----------------------------------------------------------------------
1225
1226    #[test]
1227    fn test_codebook_stats_centroid_count() {
1228        let codes: u8 = 4;
1229        let vq = trained_vq(16, 4, codes, 20);
1230        let stats = vq.codebook_stats();
1231        for (_, num_centroids) in &stats {
1232            assert!(*num_centroids <= codes as usize);
1233        }
1234    }
1235
1236    // -----------------------------------------------------------------------
1237    // 32. QuantizerCode::is_empty
1238    // -----------------------------------------------------------------------
1239
1240    #[test]
1241    fn test_quantizer_code_is_empty() {
1242        let empty = QuantizerCode(vec![]);
1243        let non_empty = QuantizerCode(vec![0u8]);
1244        assert!(empty.is_empty());
1245        assert!(!non_empty.is_empty());
1246    }
1247
1248    // -----------------------------------------------------------------------
1249    // 33. QuantizerCode — clone and PartialEq
1250    // -----------------------------------------------------------------------
1251
1252    #[test]
1253    fn test_quantizer_code_clone_and_eq() {
1254        let code = QuantizerCode(vec![1u8, 2, 3]);
1255        let cloned = code.clone();
1256        assert_eq!(code, cloned);
1257        let different = QuantizerCode(vec![1u8, 2, 4]);
1258        assert_ne!(code, different);
1259    }
1260
1261    // -----------------------------------------------------------------------
1262    // 34. VectorQuantizer::debug format
1263    // -----------------------------------------------------------------------
1264
1265    #[test]
1266    fn test_vector_quantizer_debug_format() {
1267        let vq = VectorQuantizer::new(small_config(4, 4));
1268        let dbg = format!("{vq:?}");
1269        assert!(dbg.contains("VectorQuantizer"));
1270    }
1271
1272    // -----------------------------------------------------------------------
1273    // 35. QuantizationStats default values
1274    // -----------------------------------------------------------------------
1275
1276    #[test]
1277    fn test_quantization_stats_default() {
1278        let stats = QuantizationStats::default();
1279        assert_eq!(stats.codebooks_trained, 0);
1280        assert_eq!(stats.total_encoded, 0);
1281        assert_eq!(stats.total_decoded, 0);
1282        assert_eq!(stats.avg_encode_error, 0.0);
1283    }
1284
1285    // -----------------------------------------------------------------------
1286    // 36. encode well-separated clusters — nearest gets assigned correctly
1287    // -----------------------------------------------------------------------
1288
1289    #[test]
1290    fn test_encode_cluster_assignment() {
1291        let dim = 8;
1292        // Two clearly separated clusters: zeros and ones.
1293        let mut data: Vec<Vec<f64>> = Vec::new();
1294        for _ in 0..5 {
1295            data.push(vec![0.0f64; dim]);
1296        }
1297        for _ in 0..5 {
1298            data.push(vec![100.0f64; dim]);
1299        }
1300        let cfg = QuantizationConfig::new(2, 2, 50, 1e-8);
1301        let mut vq = VectorQuantizer::new(cfg);
1302        vq.train(&data)
1303            .expect("test: training on two-cluster data should succeed");
1304
1305        let code_near_zero = vq
1306            .encode(&vec![0.01f64; dim])
1307            .expect("test: encode of near-zero vector should succeed");
1308        let code_near_hundred = vq
1309            .encode(&vec![99.99f64; dim])
1310            .expect("test: encode of near-hundred vector should succeed");
1311
1312        // The two queries should land in different codes.
1313        assert_ne!(
1314            code_near_zero, code_near_hundred,
1315            "well-separated vectors should get different codes"
1316        );
1317    }
1318
1319    // -----------------------------------------------------------------------
1320    // 37. asymmetric_distance — closer vector gives smaller distance
1321    // -----------------------------------------------------------------------
1322
1323    #[test]
1324    fn test_asymmetric_distance_ordering() {
1325        let dim = 8;
1326        let mut data: Vec<Vec<f64>> = Vec::new();
1327        for i in 0..5 {
1328            data.push(vec![i as f64; dim]);
1329        }
1330        let cfg = QuantizationConfig::new(2, 2, 50, 1e-8);
1331        let mut vq = VectorQuantizer::new(cfg);
1332        vq.train(&data)
1333            .expect("test: training on linear-spaced data should succeed");
1334
1335        let query = vec![0.0f64; dim];
1336        let code_near = vq
1337            .encode(&vec![0.5f64; dim])
1338            .expect("test: encode near vector should succeed");
1339        let code_far = vq
1340            .encode(&vec![4.5f64; dim])
1341            .expect("test: encode far vector should succeed");
1342
1343        let dist_near = vq
1344            .asymmetric_distance(&query, &code_near)
1345            .expect("test: asymmetric_distance to near code should succeed");
1346        let dist_far = vq
1347            .asymmetric_distance(&query, &code_far)
1348            .expect("test: asymmetric_distance to far code should succeed");
1349
1350        assert!(
1351            dist_near <= dist_far,
1352            "closer code should have smaller asymmetric distance: near={dist_near}, far={dist_far}"
1353        );
1354    }
1355
1356    // -----------------------------------------------------------------------
1357    // 38. Codebook::nearest_centroid — single centroid always returns 0
1358    // -----------------------------------------------------------------------
1359
1360    #[test]
1361    fn test_codebook_nearest_centroid_single() {
1362        let cb = Codebook {
1363            centroids: vec![vec![1.0f64, 2.0, 3.0]],
1364            subspace_dim: 3,
1365            num_codes: 1,
1366        };
1367        assert_eq!(cb.nearest_centroid(&[0.0, 0.0, 0.0]), 0);
1368        assert_eq!(cb.nearest_centroid(&[10.0, 10.0, 10.0]), 0);
1369    }
1370
1371    // -----------------------------------------------------------------------
1372    // 39. VqError variants have informative messages
1373    // -----------------------------------------------------------------------
1374
1375    #[test]
1376    fn test_vq_error_messages() {
1377        let e1 = VqError::NotTrained;
1378        assert!(!format!("{e1}").is_empty());
1379
1380        let e2 = VqError::DimensionMismatch {
1381            expected: 16,
1382            got: 8,
1383        };
1384        let msg = format!("{e2}");
1385        assert!(msg.contains("16") && msg.contains("8"));
1386
1387        let e3 = VqError::InsufficientData {
1388            needed: 256,
1389            got: 10,
1390        };
1391        let msg3 = format!("{e3}");
1392        assert!(msg3.contains("256") && msg3.contains("10"));
1393
1394        let e4 = VqError::InvalidCode("bad code".to_string());
1395        assert!(format!("{e4}").contains("bad code"));
1396    }
1397
1398    // -----------------------------------------------------------------------
1399    // 40. avg_encode_error updates as a running mean (non-negative)
1400    // -----------------------------------------------------------------------
1401
1402    #[test]
1403    fn test_avg_encode_error_running_mean() {
1404        let dim = 16;
1405        let mut vq = trained_vq(dim, 4, 4, 20);
1406
1407        vq.encode(&vec![0.0f64; dim])
1408            .expect("test: encode 0.0 vector should succeed");
1409        let e1 = vq.stats.avg_encode_error;
1410        vq.encode(&vec![0.5f64; dim])
1411            .expect("test: encode 0.5 vector should succeed");
1412        let e2 = vq.stats.avg_encode_error;
1413        vq.encode(&vec![1.0f64; dim])
1414            .expect("test: encode 1.0 vector should succeed");
1415
1416        // All errors must be non-negative.
1417        assert!(e1 >= 0.0);
1418        assert!(e2 >= 0.0);
1419    }
1420
1421    // -----------------------------------------------------------------------
1422    // 41. encode_batch — all codes have correct length
1423    // -----------------------------------------------------------------------
1424
1425    #[test]
1426    fn test_encode_batch_code_lengths() {
1427        let dim = 16;
1428        let m = 4;
1429        let mut vq = trained_vq(dim, m, 4, 20);
1430        let vecs = make_vectors(8, dim);
1431        let codes = vq
1432            .encode_batch(&vecs)
1433            .expect("test: encode_batch for code lengths check");
1434        for code in &codes {
1435            assert_eq!(code.len(), m);
1436        }
1437    }
1438
1439    // -----------------------------------------------------------------------
1440    // 42. decode with out-of-range code returns InvalidCode
1441    // -----------------------------------------------------------------------
1442
1443    #[test]
1444    fn test_decode_out_of_range_code() {
1445        let dim = 4;
1446        let cfg = QuantizationConfig::new(1, 2, 10, 1e-6); // only 2 centroids (codes 0,1)
1447        let mut vq = VectorQuantizer::new(cfg);
1448        // Need >= 2 training vectors.
1449        let data = vec![vec![0.0f64; dim], vec![1.0f64; dim]];
1450        vq.train(&data)
1451            .expect("test: train for out-of-range code decode test");
1452
1453        // Code=200 is out of range since codebook only has ≤2 entries.
1454        let code = QuantizerCode(vec![200u8]);
1455        let result = vq.decode(&code);
1456        assert!(matches!(result, Err(VqError::InvalidCode(_))));
1457    }
1458}