1use std::collections::VecDeque;
31
32use thiserror::Error;
33
34#[derive(Debug, Error, PartialEq, Eq)]
40pub enum QErrorError {
41 #[error("vector length mismatch: expected {expected}, got {got}")]
43 LengthMismatch {
44 expected: usize,
46 got: usize,
48 },
49 #[error("cannot compute quantization error for empty vectors")]
51 EmptyVector,
52}
53
54#[derive(Debug, Clone, PartialEq)]
60pub struct QuantizationError {
61 pub mse: f64,
63 pub mae: f64,
65 pub max_error: f64,
67 pub snr_db: f64,
75}
76
77impl QuantizationError {
78 #[inline]
83 pub fn is_acceptable(&self, max_mse: f64) -> bool {
84 self.mse <= max_mse
85 }
86}
87
88const HISTORY_CAPACITY: usize = 256;
94
95#[derive(Debug, Clone)]
101pub struct QuantizationErrorTracker {
102 pub history: VecDeque<QuantizationError>,
104 pub total_vectors: u64,
106}
107
108impl QuantizationErrorTracker {
109 pub fn new() -> Self {
111 Self {
112 history: VecDeque::with_capacity(HISTORY_CAPACITY),
113 total_vectors: 0,
114 }
115 }
116
117 pub fn compute_error(
123 &self,
124 original: &[f32],
125 quantized: &[f32],
126 ) -> Result<QuantizationError, QErrorError> {
127 let n = original.len();
128
129 if n == 0 {
130 return Err(QErrorError::EmptyVector);
131 }
132 if quantized.len() != n {
133 return Err(QErrorError::LengthMismatch {
134 expected: n,
135 got: quantized.len(),
136 });
137 }
138
139 let n_f64 = n as f64;
140 let mut sum_sq_err = 0.0_f64;
141 let mut sum_abs_err = 0.0_f64;
142 let mut max_abs_err = 0.0_f64;
143 let mut sum_sq_orig = 0.0_f64;
144
145 for (&o, &q) in original.iter().zip(quantized.iter()) {
146 let o64 = o as f64;
147 let q64 = q as f64;
148 let diff = o64 - q64;
149 let abs_diff = diff.abs();
150
151 sum_sq_err += diff * diff;
152 sum_abs_err += abs_diff;
153 if abs_diff > max_abs_err {
154 max_abs_err = abs_diff;
155 }
156 sum_sq_orig += o64 * o64;
157 }
158
159 let mse = sum_sq_err / n_f64;
160 let mae = sum_abs_err / n_f64;
161 let max_error = max_abs_err;
162 let signal_power = sum_sq_orig / n_f64;
163
164 let snr_db = if signal_power == 0.0 && mse == 0.0 {
170 0.0
171 } else if mse == 0.0 {
172 f64::INFINITY
173 } else {
174 10.0 * (signal_power / mse).log10()
175 };
176
177 Ok(QuantizationError {
178 mse,
179 mae,
180 max_error,
181 snr_db,
182 })
183 }
184
185 pub fn record(&mut self, error: QuantizationError) {
189 if self.history.len() == HISTORY_CAPACITY {
190 self.history.pop_front();
191 }
192 self.history.push_back(error);
193 self.total_vectors += 1;
194 }
195
196 pub fn rolling_mse(&self) -> f64 {
200 if self.history.is_empty() {
201 return 0.0;
202 }
203 let sum: f64 = self.history.iter().map(|e| e.mse).sum();
204 sum / self.history.len() as f64
205 }
206
207 pub fn rolling_mae(&self) -> f64 {
211 if self.history.is_empty() {
212 return 0.0;
213 }
214 let sum: f64 = self.history.iter().map(|e| e.mae).sum();
215 sum / self.history.len() as f64
216 }
217
218 pub fn p99_mse(&self) -> f64 {
225 if self.history.is_empty() {
226 return 0.0;
227 }
228 let mut values: Vec<f64> = self.history.iter().map(|e| e.mse).collect();
229 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
230
231 let n = values.len();
232 let rank = ((0.99_f64 * n as f64).ceil() as usize)
234 .saturating_sub(1)
235 .min(n - 1);
236 values[rank]
237 }
238
239 pub fn is_quality_acceptable(&self, max_mse: f64) -> bool {
241 self.rolling_mse() <= max_mse
242 }
243
244 pub fn history_len(&self) -> usize {
246 self.history.len()
247 }
248
249 pub fn clear_history(&mut self) {
251 self.history.clear();
252 }
253}
254
255impl Default for QuantizationErrorTracker {
256 fn default() -> Self {
257 Self::new()
258 }
259}
260
261#[cfg(test)]
266mod tests {
267 use super::*;
268
269 fn make_tracker() -> QuantizationErrorTracker {
272 QuantizationErrorTracker::new()
273 }
274
275 #[test]
277 fn test_compute_error_identical_vectors() {
278 let t = make_tracker();
279 let v = vec![0.1_f32, 0.5, -0.3, 0.9, 0.0];
280 let err = t.compute_error(&v, &v).expect("should succeed");
281 assert!(
282 err.mse < 1e-12,
283 "MSE for identical vectors must be ~0, got {}",
284 err.mse
285 );
286 assert!(
287 err.mae < 1e-12,
288 "MAE for identical vectors must be ~0, got {}",
289 err.mae
290 );
291 assert!(
292 err.max_error < 1e-12,
293 "max_error for identical vectors must be ~0, got {}",
294 err.max_error
295 );
296 }
297
298 #[test]
300 fn test_compute_error_identical_snr_infinite() {
301 let t = make_tracker();
302 let v = vec![1.0_f32, 2.0, 3.0];
303 let err = t.compute_error(&v, &v).expect("should succeed");
304 assert!(
305 err.snr_db.is_infinite() && err.snr_db.is_sign_positive(),
306 "SNR should be +∞ for perfect reconstruction, got {}",
307 err.snr_db
308 );
309 }
310
311 #[test]
313 fn test_compute_error_constant_offset() {
314 let t = make_tracker();
315 let offset = 0.5_f32;
316 let original = vec![1.0_f32, 2.0, 3.0, 4.0];
317 let quantized: Vec<f32> = original.iter().map(|x| x + offset).collect();
318
319 let err = t
320 .compute_error(&original, &quantized)
321 .expect("should succeed");
322
323 let expected_mse = (offset as f64).powi(2);
324 let expected_mae = offset as f64;
325
326 assert!(
327 (err.mse - expected_mse).abs() < 1e-9,
328 "MSE = {}, expected {}",
329 err.mse,
330 expected_mse
331 );
332 assert!(
333 (err.mae - expected_mae).abs() < 1e-9,
334 "MAE = {}, expected {}",
335 err.mae,
336 expected_mae
337 );
338 assert!(
339 (err.max_error - offset as f64).abs() < 1e-9,
340 "max_error = {}, expected {}",
341 err.max_error,
342 offset
343 );
344 }
345
346 #[test]
348 fn test_compute_error_length_mismatch() {
349 let t = make_tracker();
350 let original = vec![1.0_f32, 2.0, 3.0];
351 let quantized = vec![1.0_f32, 2.0];
352
353 match t.compute_error(&original, &quantized) {
354 Err(QErrorError::LengthMismatch {
355 expected: 3,
356 got: 2,
357 }) => {}
358 other => panic!("expected LengthMismatch(3, 2), got {:?}", other),
359 }
360 }
361
362 #[test]
364 fn test_compute_error_empty_vector() {
365 let t = make_tracker();
366 match t.compute_error(&[], &[]) {
367 Err(QErrorError::EmptyVector) => {}
368 other => panic!("expected EmptyVector, got {:?}", other),
369 }
370 }
371
372 #[test]
374 fn test_snr_positive_for_low_noise() {
375 let t = make_tracker();
376 let original: Vec<f32> = (0..64).map(|i| (i as f32) * 0.1).collect();
378 let quantized: Vec<f32> = original.iter().map(|&x| x + 1e-4).collect();
379
380 let err = t
381 .compute_error(&original, &quantized)
382 .expect("should succeed");
383 assert!(
384 err.snr_db > 0.0,
385 "SNR should be positive for low-noise quantization, got {}",
386 err.snr_db
387 );
388 }
389
390 #[test]
392 fn test_record_appends_to_history() {
393 let mut tracker = make_tracker();
394 assert_eq!(tracker.history_len(), 0);
395
396 let t = make_tracker();
397 let v = vec![0.5_f32; 4];
398 let err = t.compute_error(&v, &v).expect("should succeed");
399
400 tracker.record(err);
401 assert_eq!(tracker.history_len(), 1);
402 assert_eq!(tracker.total_vectors, 1);
403 }
404
405 #[test]
407 fn test_record_capped_at_256() {
408 let mut tracker = make_tracker();
409 let t = make_tracker();
410 let v = vec![1.0_f32; 8];
411
412 for _ in 0..300 {
413 let err = t.compute_error(&v, &v).expect("should succeed");
414 tracker.record(err);
415 }
416
417 assert_eq!(tracker.history_len(), 256, "history must be capped at 256");
418 assert_eq!(
419 tracker.total_vectors, 300,
420 "total_vectors must reflect all recordings"
421 );
422 }
423
424 #[test]
426 fn test_rolling_mse_average() {
427 let mut tracker = make_tracker();
428 let t = make_tracker();
429
430 for offset in [1.0_f32, 2.0, 3.0] {
433 let original = vec![0.0_f32; 4];
434 let quantized = vec![offset; 4];
435 let err = t
436 .compute_error(&original, &quantized)
437 .expect("should succeed");
438 tracker.record(err);
439 }
440
441 let expected = (1.0_f64 + 4.0 + 9.0) / 3.0;
443 let got = tracker.rolling_mse();
444 assert!(
445 (got - expected).abs() < 1e-9,
446 "rolling_mse = {}, expected {}",
447 got,
448 expected
449 );
450 }
451
452 #[test]
454 fn test_rolling_mae_average() {
455 let mut tracker = make_tracker();
456 let t = make_tracker();
457
458 for offset in [1.0_f32, 3.0] {
459 let original = vec![0.0_f32; 4];
460 let quantized = vec![offset; 4];
461 let err = t
462 .compute_error(&original, &quantized)
463 .expect("should succeed");
464 tracker.record(err);
465 }
466
467 let got = tracker.rolling_mae();
469 assert!(
470 (got - 2.0).abs() < 1e-9,
471 "rolling_mae = {}, expected 2.0",
472 got
473 );
474 }
475
476 #[test]
478 fn test_p99_mse_correctness() {
479 let mut tracker = make_tracker();
480 let t = make_tracker();
481 let original = vec![0.0_f32; 1];
482
483 for i in 1_u32..=100 {
485 let offset = i as f32;
486 let quantized = vec![offset];
487 let err = t.compute_error(&original, &quantized).expect("ok");
488 tracker.record(err);
489 }
490
491 let got = tracker.p99_mse();
495 let expected = 99.0_f64 * 99.0;
496 assert!(
497 (got - expected).abs() < 1e-6,
498 "p99_mse = {}, expected {}",
499 got,
500 expected
501 );
502 }
503
504 #[test]
506 fn test_p99_mse_empty_history() {
507 let tracker = make_tracker();
508 assert_eq!(tracker.p99_mse(), 0.0);
509 }
510
511 #[test]
513 fn test_is_quality_acceptable_threshold() {
514 let mut tracker = make_tracker();
515 let t = make_tracker();
516
517 let original = vec![0.0_f32; 4];
518 let quantized = vec![1.0_f32; 4]; let err = t.compute_error(&original, &quantized).expect("ok");
521 tracker.record(err);
522
523 assert!(
524 tracker.is_quality_acceptable(1.0),
525 "rolling_mse == max_mse should be acceptable"
526 );
527 assert!(
528 tracker.is_quality_acceptable(2.0),
529 "rolling_mse < max_mse should be acceptable"
530 );
531 assert!(
532 !tracker.is_quality_acceptable(0.5),
533 "rolling_mse > max_mse should be unacceptable"
534 );
535 }
536
537 #[test]
539 fn test_clear_history_empties() {
540 let mut tracker = make_tracker();
541 let t = make_tracker();
542 let v = vec![1.0_f32; 4];
543
544 for _ in 0..10 {
545 let err = t.compute_error(&v, &v).expect("ok");
546 tracker.record(err);
547 }
548
549 assert_eq!(tracker.history_len(), 10);
550 tracker.clear_history();
551 assert_eq!(
552 tracker.history_len(),
553 0,
554 "clear_history must empty the buffer"
555 );
556 assert_eq!(
558 tracker.total_vectors, 10,
559 "total_vectors must survive clear_history"
560 );
561 }
562
563 #[test]
565 fn test_quantization_error_is_acceptable() {
566 let err = QuantizationError {
567 mse: 0.01,
568 mae: 0.05,
569 max_error: 0.1,
570 snr_db: 20.0,
571 };
572 assert!(err.is_acceptable(0.01));
573 assert!(err.is_acceptable(0.05));
574 assert!(!err.is_acceptable(0.009));
575 }
576
577 #[test]
579 fn test_snr_zero_vector_pair() {
580 let t = make_tracker();
581 let zeros = vec![0.0_f32; 4];
582 let err = t.compute_error(&zeros, &zeros).expect("ok");
583 assert_eq!(err.snr_db, 0.0);
585 }
586
587 #[test]
589 fn test_default_equals_new() {
590 let a = QuantizationErrorTracker::new();
591 let b = QuantizationErrorTracker::default();
592 assert_eq!(a.history_len(), b.history_len());
593 assert_eq!(a.total_vectors, b.total_vectors);
594 }
595}