1use std::fmt;
29use thiserror::Error;
30
31#[derive(Debug, Clone, Error)]
37pub enum VqError {
38 #[error("quantizer has not been trained yet")]
40 NotTrained,
41 #[error("dimension mismatch: expected {expected}, got {got}")]
43 DimensionMismatch { expected: usize, got: usize },
44 #[error("insufficient training data: needed {needed} vectors, got {got}")]
46 InsufficientData { needed: usize, got: usize },
47 #[error("invalid quantizer code: {0}")]
49 InvalidCode(String),
50}
51
52#[derive(Debug, Clone, PartialEq)]
61pub struct QuantizerCode(pub Vec<u8>);
62
63impl QuantizerCode {
64 #[inline]
66 pub fn len(&self) -> usize {
67 self.0.len()
68 }
69
70 #[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#[derive(Debug, Clone)]
91pub struct Codebook {
92 pub centroids: Vec<Vec<f64>>,
94 pub subspace_dim: usize,
96 pub num_codes: u8,
98}
99
100impl Codebook {
101 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 #[inline]
118 pub fn centroid(&self, code: u8) -> &[f64] {
119 &self.centroids[code as usize]
120 }
121}
122
123#[derive(Debug, Clone)]
129pub struct QuantizationConfig {
130 pub num_subspaces: usize,
132 pub codes_per_subspace: u8,
134 pub max_iterations: usize,
136 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 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#[derive(Debug, Clone, Default)]
174pub struct QuantizationStats {
175 pub codebooks_trained: usize,
177 pub total_encoded: u64,
179 pub total_decoded: u64,
181 pub avg_encode_error: f64,
183}
184
185pub struct VectorQuantizer {
210 pub config: QuantizationConfig,
212 pub codebooks: Vec<Codebook>,
214 pub trained: bool,
216 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 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 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 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), got: dim,
272 });
273 }
274
275 let sub_dim = dim / m;
276
277 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 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 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 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 let call_error = total_sq_err / expected_dim as f64;
368
369 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 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 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 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 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 Ok(squared_euclidean_f64(&decoded_a, &decoded_b))
513 }
514
515 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 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 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 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 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
610fn 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 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 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 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, ¢roids[j]).sqrt();
675 if shift > max_shift {
676 max_shift = shift;
677 }
678 new_centroids[j] = new_c;
679 }
680 }
682
683 centroids = new_centroids;
684
685 if max_shift < tol {
686 break;
687 }
688 }
689
690 centroids
691}
692
693#[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#[cfg(test)]
714mod tests {
715 use crate::vector_quantizer::{
716 Codebook, QuantizationConfig, QuantizationStats, QuantizerCode, VectorQuantizer, VqError,
717 };
718
719 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 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 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 #[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 #[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 #[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 #[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); let result = vq.train(&data);
807 assert!(matches!(result, Err(VqError::InsufficientData { .. })));
808 }
809
810 #[test]
815 fn test_train_dimension_not_divisible() {
816 let cfg = QuantizationConfig::new(3, 4, 50, 1e-6); let mut vq = VectorQuantizer::new(cfg);
818 let data = make_vectors(10, 10);
820 let result = vq.train(&data);
821 assert!(matches!(result, Err(VqError::DimensionMismatch { .. })));
822 }
823
824 #[test]
829 fn test_train_sets_trained_flag() {
830 let vq = trained_vq(16, 4, 4, 20);
831 assert!(vq.trained);
832 }
833
834 #[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 #[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 #[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 #[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]); assert!(matches!(
876 result,
877 Err(VqError::DimensionMismatch {
878 expected: 16,
879 got: 8
880 })
881 ));
882 }
883
884 #[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 #[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 #[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 #[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]); let result = vq.decode(&code);
932 assert!(matches!(result, Err(VqError::InvalidCode(_))));
933 }
934
935 #[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 #[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 #[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 #[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 #[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]); let result = vq.encode_batch(&vecs);
1012 assert!(result.is_err());
1013 }
1014
1015 #[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 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 #[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 #[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 #[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 #[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 #[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 #[test]
1136 fn test_quantization_error_centroid_is_zero() {
1137 let cfg = QuantizationConfig::new(1, 1, 10, 1e-10);
1139 let mut vq = VectorQuantizer::new(cfg);
1140 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 #[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 #[test]
1171 fn test_avg_error_single_vector() {
1172 let dim = 16;
1173 let vec = vec![0.5f64; dim];
1174 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
1290 fn test_encode_cluster_assignment() {
1291 let dim = 8;
1292 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 assert_ne!(
1314 code_near_zero, code_near_hundred,
1315 "well-separated vectors should get different codes"
1316 );
1317 }
1318
1319 #[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 #[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 #[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 #[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 assert!(e1 >= 0.0);
1418 assert!(e2 >= 0.0);
1419 }
1420
1421 #[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 #[test]
1444 fn test_decode_out_of_range_code() {
1445 let dim = 4;
1446 let cfg = QuantizationConfig::new(1, 2, 10, 1e-6); let mut vq = VectorQuantizer::new(cfg);
1448 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 let code = QuantizerCode(vec![200u8]);
1455 let result = vq.decode(&code);
1456 assert!(matches!(result, Err(VqError::InvalidCode(_))));
1457 }
1458}