Skip to main content

torsh_quantization/
metrics.rs

1//! Quantization quality metrics and analysis tools
2//!
3//! This module provides comprehensive tools for measuring and analyzing the quality
4//! of quantization operations, including performance metrics, benchmarking utilities,
5//! and automated analysis reports.
6//!
7//! # Features
8//!
9//! - **Quality Metrics**: MSE, PSNR, SNR, MAE, cosine similarity calculations
10//! - **Performance Analysis**: Timing, compression ratio, and efficiency metrics
11//! - **Configuration Comparison**: Side-by-side comparison of quantization schemes
12//! - **Auto-calibration**: Automated optimal configuration selection
13//! - **Report Generation**: Comprehensive analysis reports in Markdown format
14//! - **Outlier Detection**: Statistical analysis and recommendation systems
15
16use crate::config::{ObserverType, QuantConfig};
17
18#[cfg(not(feature = "std"))]
19extern crate alloc;
20
21#[cfg(not(feature = "std"))]
22use alloc::{collections::BTreeMap as HashMap, format, string::String, vec::Vec};
23
24use torsh_core::{
25    dtype::DType,
26    error::{Result as TorshResult, TorshError},
27};
28use torsh_tensor::Tensor;
29
30/// Quantization quality metrics for measuring accuracy loss
31#[derive(Debug, Clone)]
32pub struct QuantizationMetrics {
33    /// Mean Squared Error between original and quantized tensors
34    pub mse: f32,
35    /// Peak Signal-to-Noise Ratio (PSNR) in dB
36    pub psnr: f32,
37    /// Signal-to-Noise Ratio (SNR) in dB
38    pub snr: f32,
39    /// Mean Absolute Error between original and quantized tensors
40    pub mae: f32,
41    /// Maximum absolute error
42    pub max_error: f32,
43    /// Percentage of values with zero error
44    pub zero_error_percentage: f32,
45    /// Cosine similarity between original and quantized tensors
46    pub cosine_similarity: f32,
47    /// Compression ratio achieved
48    pub compression_ratio: f32,
49}
50
51impl Default for QuantizationMetrics {
52    fn default() -> Self {
53        Self {
54            mse: 0.0,
55            psnr: 0.0,
56            snr: 0.0,
57            mae: 0.0,
58            max_error: 0.0,
59            zero_error_percentage: 100.0,
60            cosine_similarity: 1.0,
61            compression_ratio: 1.0,
62        }
63    }
64}
65
66/// Calculate comprehensive quantization quality metrics
67pub fn calculate_quantization_metrics(
68    original: &Tensor,
69    quantized: &Tensor,
70    original_bits: u32,
71    quantized_bits: u32,
72) -> TorshResult<QuantizationMetrics> {
73    if original.shape() != quantized.shape() {
74        return Err(TorshError::InvalidArgument(format!(
75            "Shape mismatch: expected {:?}, got {:?}",
76            original.shape(),
77            quantized.shape()
78        )));
79    }
80
81    let original_data = original.data()?;
82    let quantized_data = quantized.data()?;
83
84    if original_data.len() != quantized_data.len() {
85        return Err(TorshError::InvalidArgument(
86            "Data length mismatch between tensors".to_string(),
87        ));
88    }
89
90    if original_data.is_empty() {
91        return Ok(QuantizationMetrics::default());
92    }
93
94    // Calculate MSE
95    let mse = original_data
96        .iter()
97        .zip(quantized_data.iter())
98        .map(|(a, b)| (a - b).powi(2))
99        .sum::<f32>()
100        / original_data.len() as f32;
101
102    // Calculate MAE
103    let mae = original_data
104        .iter()
105        .zip(quantized_data.iter())
106        .map(|(a, b)| (a - b).abs())
107        .sum::<f32>()
108        / original_data.len() as f32;
109
110    // Calculate max error
111    let max_error = original_data
112        .iter()
113        .zip(quantized_data.iter())
114        .map(|(a, b)| (a - b).abs())
115        .fold(0.0, f32::max);
116
117    // Calculate zero error percentage
118    let zero_errors = original_data
119        .iter()
120        .zip(quantized_data.iter())
121        .filter(|(a, b)| (*a - *b).abs() < 1e-7)
122        .count();
123    let zero_error_percentage = (zero_errors as f32 / original_data.len() as f32) * 100.0;
124
125    // Calculate signal power for SNR/PSNR
126    let signal_power =
127        original_data.iter().map(|x| x.powi(2)).sum::<f32>() / original_data.len() as f32;
128
129    // Calculate PSNR (assuming signal range is [0, 1])
130    let max_signal = original_data
131        .iter()
132        .fold(0.0f32, |acc, &x| acc.max(x.abs()));
133    let psnr = if mse > 0.0 {
134        20.0 * (max_signal / mse.sqrt()).log10()
135    } else {
136        f32::INFINITY
137    };
138
139    // Calculate SNR
140    let snr = if mse > 0.0 && signal_power > 0.0 {
141        10.0 * (signal_power / mse).log10()
142    } else {
143        f32::INFINITY
144    };
145
146    // Calculate cosine similarity
147    let dot_product = original_data
148        .iter()
149        .zip(quantized_data.iter())
150        .map(|(a, b)| a * b)
151        .sum::<f32>();
152
153    let original_norm = original_data.iter().map(|x| x.powi(2)).sum::<f32>().sqrt();
154    let quantized_norm = quantized_data.iter().map(|x| x.powi(2)).sum::<f32>().sqrt();
155
156    let cosine_similarity = if original_norm > 0.0 && quantized_norm > 0.0 {
157        dot_product / (original_norm * quantized_norm)
158    } else {
159        0.0
160    };
161
162    // Calculate compression ratio
163    let compression_ratio = original_bits as f32 / quantized_bits as f32;
164
165    Ok(QuantizationMetrics {
166        mse,
167        psnr,
168        snr,
169        mae,
170        max_error,
171        zero_error_percentage,
172        cosine_similarity,
173        compression_ratio,
174    })
175}
176
177/// Compare multiple quantization configurations and return ranked results
178pub fn compare_quantization_configs(
179    tensor: &Tensor,
180    configs: &[QuantConfig],
181) -> TorshResult<Vec<(QuantConfig, QuantizationMetrics, f64)>> {
182    let mut results = Vec::new();
183
184    for config in configs {
185        // Time the quantization process
186        let start = std::time::Instant::now();
187
188        // Quantize the tensor
189        // Use the parameter-preserving API so per-channel / group-wise schemes
190        // are dequantized with their own parameters instead of channel 0's.
191        let quantize_result = crate::algorithms::quantize_with_config_full(tensor, config);
192
193        let duration = start.elapsed().as_secs_f64();
194
195        match quantize_result {
196            Ok(quantized) => {
197                // Dequantize back to original precision
198                let dequantized = quantized.dequantize()?;
199
200                // Calculate metrics
201                let original_bits = match tensor.dtype() {
202                    DType::F32 => 32,
203                    DType::F16 => 16,
204                    _ => 8,
205                };
206
207                let quantized_bits = match config.dtype {
208                    DType::I8 | DType::U8 => 8,
209                    DType::I16 => 16,
210                    DType::I32 => 32,
211                    DType::F16 => 16,
212                    DType::F32 => 32,
213                    _ => 8,
214                };
215
216                let metrics = calculate_quantization_metrics(
217                    tensor,
218                    &dequantized,
219                    original_bits,
220                    quantized_bits,
221                )?;
222
223                results.push((config.clone(), metrics, duration));
224            }
225            Err(_) => {
226                // If quantization fails, create a worst-case metrics entry
227                let worst_metrics = QuantizationMetrics {
228                    mse: f32::INFINITY,
229                    psnr: f32::NEG_INFINITY,
230                    snr: f32::NEG_INFINITY,
231                    mae: f32::INFINITY,
232                    max_error: f32::INFINITY,
233                    zero_error_percentage: 0.0,
234                    cosine_similarity: 0.0,
235                    compression_ratio: 1.0,
236                };
237
238                results.push((config.clone(), worst_metrics, duration));
239            }
240        }
241    }
242
243    // Sort by PSNR (higher is better)
244    results.sort_by(|a, b| {
245        b.1.psnr
246            .partial_cmp(&a.1.psnr)
247            .unwrap_or(core::cmp::Ordering::Equal)
248    });
249
250    Ok(results)
251}
252
253/// Automatic calibration assistant to find optimal quantization configuration
254pub fn auto_calibrate_quantization(
255    calibration_tensors: &[&Tensor],
256    target_accuracy_threshold: f32,
257    max_compression_ratio: f32,
258) -> TorshResult<QuantConfig> {
259    if calibration_tensors.is_empty() {
260        return Err(TorshError::InvalidArgument(
261            "No calibration tensors provided".to_string(),
262        ));
263    }
264
265    // Define candidate configurations to test
266    let candidate_configs = vec![
267        QuantConfig::int8(),
268        QuantConfig::int8().with_observer(ObserverType::Histogram),
269        QuantConfig::per_channel(0),
270        QuantConfig::per_channel(1),
271        QuantConfig::group_wise(0, 8),
272        QuantConfig::group_wise(1, 16),
273        QuantConfig::int4(),
274        QuantConfig::ternary(),
275    ];
276
277    let mut best_config = None;
278    let mut best_score = f32::NEG_INFINITY;
279
280    // Test each configuration with all calibration tensors
281    for config in candidate_configs {
282        let mut total_metrics = QuantizationMetrics::default();
283        let mut successful_tests = 0;
284
285        for tensor in calibration_tensors {
286            if let Ok(comparison) =
287                compare_quantization_configs(tensor, std::slice::from_ref(&config))
288            {
289                if let Some((_, metrics, _)) = comparison.first() {
290                    if metrics.psnr.is_finite() {
291                        total_metrics.mse += metrics.mse;
292                        total_metrics.psnr += metrics.psnr;
293                        total_metrics.snr += metrics.snr;
294                        total_metrics.mae += metrics.mae;
295                        total_metrics.max_error = total_metrics.max_error.max(metrics.max_error);
296                        total_metrics.zero_error_percentage += metrics.zero_error_percentage;
297                        total_metrics.cosine_similarity += metrics.cosine_similarity;
298                        total_metrics.compression_ratio += metrics.compression_ratio;
299                        successful_tests += 1;
300                    }
301                }
302            }
303        }
304
305        if successful_tests > 0 {
306            // Average the metrics
307            let avg_metrics = QuantizationMetrics {
308                mse: total_metrics.mse / successful_tests as f32,
309                psnr: total_metrics.psnr / successful_tests as f32,
310                snr: total_metrics.snr / successful_tests as f32,
311                mae: total_metrics.mae / successful_tests as f32,
312                max_error: total_metrics.max_error,
313                zero_error_percentage: total_metrics.zero_error_percentage
314                    / successful_tests as f32,
315                cosine_similarity: total_metrics.cosine_similarity / successful_tests as f32,
316                compression_ratio: total_metrics.compression_ratio / successful_tests as f32,
317            };
318
319            // Calculate a composite score (higher is better)
320            let score = if avg_metrics.psnr >= target_accuracy_threshold
321                && avg_metrics.compression_ratio <= max_compression_ratio
322            {
323                // Prioritize compression ratio if accuracy threshold is met
324                avg_metrics.compression_ratio + avg_metrics.psnr / 100.0
325            } else {
326                // Otherwise prioritize accuracy
327                avg_metrics.psnr / avg_metrics.compression_ratio
328            };
329
330            if score > best_score {
331                best_score = score;
332                best_config = Some(config.clone());
333            }
334        }
335    }
336
337    best_config
338        .ok_or_else(|| TorshError::InvalidArgument("No suitable configuration found".to_string()))
339}
340
341/// Generate a comprehensive quantization report
342pub fn generate_quantization_report(
343    original: &Tensor,
344    configs: &[QuantConfig],
345) -> TorshResult<String> {
346    let mut report = String::new();
347
348    report.push_str("# Quantization Analysis Report\n\n");
349    report.push_str(&format!(
350        "**Original Tensor Shape:** {:?}\n",
351        original.shape()
352    ));
353    report.push_str(&format!(
354        "**Original Tensor DType:** {:?}\n",
355        original.dtype()
356    ));
357    report.push_str(&format!(
358        "**Number of Elements:** {}\n\n",
359        original.shape().numel()
360    ));
361
362    // Get tensor statistics
363    let data = original.data()?;
364    let min_val = data.iter().fold(f32::INFINITY, |acc, &x| acc.min(x));
365    let max_val = data.iter().fold(f32::NEG_INFINITY, |acc, &x| acc.max(x));
366    let mean = data.iter().sum::<f32>() / data.len() as f32;
367    let std_dev = (data.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / data.len() as f32).sqrt();
368
369    report.push_str("**Original Tensor Statistics:**\n");
370    report.push_str(&format!("- Min: {min_val:.6}\n"));
371    report.push_str(&format!("- Max: {max_val:.6}\n"));
372    report.push_str(&format!("- Mean: {mean:.6}\n"));
373    report.push_str(&format!("- Std Dev: {std_dev:.6}\n"));
374    report.push_str(&format!("- Dynamic Range: {:.6}\n\n", max_val - min_val));
375
376    // Compare configurations
377    let comparison_results = compare_quantization_configs(original, configs)?;
378
379    report.push_str("## Quantization Configuration Comparison\n\n");
380    report.push_str(
381        "| Rank | Scheme | Observer | PSNR (dB) | SNR (dB) | MAE | Compression | Time (ms) |\n",
382    );
383    report.push_str(
384        "|------|--------|----------|-----------|----------|-----|-------------|----------|\n",
385    );
386
387    for (rank, (config, metrics, duration)) in comparison_results.iter().enumerate() {
388        report.push_str(&format!(
389            "| {} | {:?} | {:?} | {:.2} | {:.2} | {:.6} | {:.1}x | {:.2} |\n",
390            rank + 1,
391            config.scheme,
392            config.observer_type,
393            metrics.psnr,
394            metrics.snr,
395            metrics.mae,
396            metrics.compression_ratio,
397            duration * 1000.0
398        ));
399    }
400
401    report.push_str("\n## Detailed Metrics\n\n");
402
403    for (rank, (config, metrics, _)) in comparison_results.iter().enumerate() {
404        report.push_str(&format!(
405            "### Configuration #{} - {:?}\n",
406            rank + 1,
407            config.scheme
408        ));
409        report.push_str(&format!("- **MSE:** {:.8}\n", metrics.mse));
410        report.push_str(&format!("- **PSNR:** {:.2} dB\n", metrics.psnr));
411        report.push_str(&format!("- **SNR:** {:.2} dB\n", metrics.snr));
412        report.push_str(&format!("- **MAE:** {:.6}\n", metrics.mae));
413        report.push_str(&format!("- **Max Error:** {:.6}\n", metrics.max_error));
414        report.push_str(&format!(
415            "- **Zero Error %:** {:.2}%\n",
416            metrics.zero_error_percentage
417        ));
418        report.push_str(&format!(
419            "- **Cosine Similarity:** {:.6}\n",
420            metrics.cosine_similarity
421        ));
422        report.push_str(&format!(
423            "- **Compression Ratio:** {:.1}x\n\n",
424            metrics.compression_ratio
425        ));
426    }
427
428    report.push_str("## Recommendations\n\n");
429
430    if let Some((best_config, best_metrics, _)) = comparison_results.first() {
431        report.push_str(&format!(
432            "**Best Configuration:** {:?} with {:?} observer\n",
433            best_config.scheme, best_config.observer_type
434        ));
435        report.push_str(&format!(
436            "- Achieves {:.2} dB PSNR with {:.1}x compression\n",
437            best_metrics.psnr, best_metrics.compression_ratio
438        ));
439
440        if best_metrics.psnr > 40.0 {
441            report.push_str("- ✅ Excellent quality preservation\n");
442        } else if best_metrics.psnr > 30.0 {
443            report.push_str("- ✅ Good quality preservation\n");
444        } else if best_metrics.psnr > 20.0 {
445            report.push_str("- ⚠️ Moderate quality loss\n");
446        } else {
447            report.push_str("- ❌ Significant quality loss\n");
448        }
449    }
450
451    Ok(report)
452}
453
454/// Generate optimization hints for tensor and configuration
455pub fn generate_optimization_hints(
456    tensor: &Tensor,
457    config: &QuantConfig,
458) -> TorshResult<Vec<String>> {
459    let mut hints = Vec::new();
460    let shape = tensor.shape();
461    let data = tensor.data()?;
462
463    // Data distribution analysis
464    if !data.is_empty() {
465        let min_val = data.iter().fold(f32::INFINITY, |acc, &x| acc.min(x));
466        let max_val = data.iter().fold(f32::NEG_INFINITY, |acc, &x| acc.max(x));
467        let mean = data.iter().sum::<f32>() / data.len() as f32;
468        let std_dev =
469            (data.iter().map(|x| (x - mean).powi(2)).sum::<f32>() / data.len() as f32).sqrt();
470
471        // Range analysis
472        let dynamic_range = max_val - min_val;
473        if dynamic_range > 100.0 {
474            hints.push("Large dynamic range detected. Consider using Histogram or Percentile observer for better quantization parameters.".to_string());
475        }
476
477        // Sparsity analysis
478        let zero_count = data.iter().filter(|&&x| x.abs() < 1e-6).count();
479        let sparsity = zero_count as f32 / data.len() as f32;
480        if sparsity > 0.5 {
481            hints.push(
482                "High sparsity detected. Sparse quantization schemes may be more efficient."
483                    .to_string(),
484            );
485        }
486
487        // Outlier detection
488        let outlier_threshold = mean + 3.0 * std_dev;
489        let outlier_count = data
490            .iter()
491            .filter(|&&x| x.abs() > outlier_threshold)
492            .count();
493        if outlier_count > 0 {
494            hints.push("Outliers detected. Percentile-based observers may provide better quantization parameters.".to_string());
495        }
496
497        // Memory efficiency hints
498        if data.len() > 1_000_000 {
499            hints.push("For large tensors, Histogram observer may be more memory-efficient than Percentile observer.".to_string());
500        }
501    }
502
503    // Shape-based hints
504    if shape.dims().len() >= 2 && shape.dims().iter().any(|&dim| dim > 16) {
505        hints.push("Multi-channel tensor detected. Per-channel or group-wise quantization may provide better accuracy.".to_string());
506    }
507
508    // Scheme-specific hints
509    match config.scheme {
510        crate::config::QScheme::PerChannelAffine | crate::config::QScheme::PerChannelSymmetric => {
511            if let Some(axis) = config.ch_axis {
512                if axis >= shape.dims().len() {
513                    hints.push(
514                        "Channel axis is out of bounds. This will cause an error.".to_string(),
515                    );
516                } else if shape.dims()[axis] < 4 {
517                    hints.push(
518                        "Few channels detected. Per-tensor quantization might be sufficient."
519                            .to_string(),
520                    );
521                }
522            }
523        }
524        crate::config::QScheme::GroupWise => {
525            if let (Some(axis), Some(group_size)) = (config.ch_axis, config.group_size) {
526                if axis < shape.dims().len() {
527                    let num_channels = shape.dims()[axis];
528                    let num_groups = num_channels.div_ceil(group_size);
529                    if num_groups == 1 {
530                        hints.push("Only one group will be created. Consider per-tensor quantization instead.".to_string());
531                    } else if num_groups == num_channels {
532                        hints.push("Each channel forms its own group. Consider per-channel quantization instead.".to_string());
533                    }
534                }
535            }
536        }
537        _ => {}
538    }
539
540    Ok(hints)
541}
542
543/// Benchmark quantization performance for different configurations
544pub fn benchmark_quantization_performance(
545    tensor: &Tensor,
546    configs: &[QuantConfig],
547    num_iterations: usize,
548) -> TorshResult<Vec<(QuantConfig, f64, f64)>> {
549    let mut results = Vec::new();
550
551    for config in configs {
552        let mut total_quantize_time = 0.0;
553        let mut total_dequantize_time = 0.0;
554        let mut successful_runs = 0;
555
556        for _ in 0..num_iterations {
557            // Benchmark quantization
558            let quantize_start = std::time::Instant::now();
559            let quantize_result = crate::algorithms::quantize_with_config(tensor, config);
560            let quantize_time = quantize_start.elapsed().as_secs_f64();
561
562            if let Ok((quantized, scale, zero_point)) = quantize_result {
563                // Benchmark dequantization
564                let dequantize_start = std::time::Instant::now();
565                let _dequantized = crate::algorithms::dequantize(&quantized, scale, zero_point)?;
566                let dequantize_time = dequantize_start.elapsed().as_secs_f64();
567
568                total_quantize_time += quantize_time;
569                total_dequantize_time += dequantize_time;
570                successful_runs += 1;
571            }
572        }
573
574        if successful_runs > 0 {
575            let avg_quantize_time = total_quantize_time / successful_runs as f64;
576            let avg_dequantize_time = total_dequantize_time / successful_runs as f64;
577            results.push((config.clone(), avg_quantize_time, avg_dequantize_time));
578        }
579    }
580
581    Ok(results)
582}
583
584#[cfg(test)]
585mod tests {
586    use super::*;
587
588    use torsh_tensor::creation::tensor_1d;
589
590    #[test]
591    fn test_calculate_quantization_metrics() {
592        let original_data = vec![1.0, 2.0, 3.0, 4.0];
593        let quantized_data = vec![1.1, 2.1, 2.9, 3.9];
594
595        let original = tensor_1d(&original_data).unwrap();
596        let quantized = tensor_1d(&quantized_data).unwrap();
597
598        let metrics = calculate_quantization_metrics(&original, &quantized, 32, 8).unwrap();
599
600        // Verify basic metrics
601        assert!(metrics.mse > 0.0);
602        assert!(metrics.mse < 1.0); // Should be small error
603        assert!(metrics.mae > 0.0);
604        assert!(metrics.mae < 1.0);
605        assert!(metrics.psnr > 0.0);
606        assert!(metrics.snr > 0.0);
607        assert!(metrics.max_error >= 0.0);
608        assert!(metrics.zero_error_percentage >= 0.0);
609        assert!(metrics.zero_error_percentage <= 100.0);
610        assert!(metrics.cosine_similarity > 0.8); // Should be high similarity
611        assert_eq!(metrics.compression_ratio, 4.0); // 32-bit to 8-bit
612
613        // Test perfect match (zero error)
614        let metrics_perfect = calculate_quantization_metrics(&original, &original, 32, 16).unwrap();
615        assert_eq!(metrics_perfect.mse, 0.0);
616        assert_eq!(metrics_perfect.mae, 0.0);
617        assert_eq!(metrics_perfect.max_error, 0.0);
618        assert_eq!(metrics_perfect.zero_error_percentage, 100.0);
619        assert!((metrics_perfect.cosine_similarity - 1.0).abs() < 1e-6);
620        assert!(metrics_perfect.psnr.is_infinite());
621        assert!(metrics_perfect.snr.is_infinite());
622        assert_eq!(metrics_perfect.compression_ratio, 2.0); // 32-bit to 16-bit
623    }
624
625    #[test]
626    fn test_compare_quantization_configs() {
627        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
628        let tensor = tensor_1d(&data).unwrap();
629
630        let configs = vec![
631            QuantConfig::int8(),
632            QuantConfig::binary(),
633            QuantConfig::ternary(),
634        ];
635
636        let results = compare_quantization_configs(&tensor, &configs).unwrap();
637
638        // Should return results for all configs
639        assert_eq!(results.len(), 3);
640
641        // Verify all results have valid metrics
642        for (config, metrics, duration) in &results {
643            assert!(configs.iter().any(|c| c.scheme == config.scheme));
644            assert!(duration >= &0.0);
645
646            // Metrics should be reasonable (not infinity for successful quantization)
647            if metrics.psnr.is_finite() {
648                assert!(metrics.psnr > 0.0);
649                assert!(metrics.compression_ratio >= 1.0);
650                assert!(metrics.mae >= 0.0);
651                assert!(metrics.mse >= 0.0);
652            }
653        }
654
655        // Results should be sorted by PSNR (higher is better)
656        for i in 1..results.len() {
657            let prev_psnr = results[i - 1].1.psnr;
658            let curr_psnr = results[i].1.psnr;
659            if prev_psnr.is_finite() && curr_psnr.is_finite() {
660                assert!(prev_psnr >= curr_psnr);
661            }
662        }
663    }
664
665    #[test]
666    fn test_auto_calibrate_quantization() {
667        let tensor1 = tensor_1d(&[1.0, 2.0, 3.0, 4.0]).unwrap();
668        let tensor2 = tensor_1d(&[2.0, 3.0, 4.0, 5.0]).unwrap();
669        let tensor3 = tensor_1d(&[0.5, 1.5, 2.5, 3.5]).unwrap();
670
671        let calibration_tensors = vec![&tensor1, &tensor2, &tensor3];
672
673        // Test with reasonable thresholds
674        let result = auto_calibrate_quantization(&calibration_tensors, 20.0, 10.0);
675        assert!(result.is_ok());
676
677        let config = result.unwrap();
678        assert!(config.validate().is_ok());
679
680        // Test with impossible thresholds (should still return a config)
681        let result_strict = auto_calibrate_quantization(&calibration_tensors, 100.0, 1.1);
682        assert!(result_strict.is_ok());
683
684        // Test empty calibration tensors
685        let empty_tensors = vec![];
686        let result_empty = auto_calibrate_quantization(&empty_tensors, 20.0, 10.0);
687        assert!(result_empty.is_err());
688    }
689
690    #[test]
691    fn test_generate_quantization_report() {
692        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
693        let tensor = tensor_1d(&data).unwrap();
694
695        let configs = vec![QuantConfig::int8(), QuantConfig::binary()];
696
697        let report_result = generate_quantization_report(&tensor, &configs);
698        assert!(report_result.is_ok());
699
700        let report = report_result.unwrap();
701
702        // Verify report contains expected sections
703        assert!(report.contains("# Quantization Analysis Report"));
704        assert!(report.contains("**Original Tensor Shape:**"));
705        assert!(report.contains("**Original Tensor Statistics:**"));
706        assert!(report.contains("## Quantization Configuration Comparison"));
707        assert!(report.contains("## Detailed Metrics"));
708        assert!(report.contains("## Recommendations"));
709
710        // Verify it contains data about our configs
711        assert!(report.contains("PerTensorAffine"));
712        assert!(report.contains("Binary"));
713
714        // Verify it contains statistical information
715        assert!(report.contains("Min:"));
716        assert!(report.contains("Max:"));
717        assert!(report.contains("Mean:"));
718        assert!(report.contains("Std Dev:"));
719        assert!(report.contains("Dynamic Range:"));
720
721        // Verify it contains metrics columns
722        assert!(report.contains("PSNR (dB)"));
723        assert!(report.contains("SNR (dB)"));
724        assert!(report.contains("MAE"));
725        assert!(report.contains("Compression"));
726        assert!(report.contains("Time (ms)"));
727
728        // Verify it contains recommendations
729        assert!(report.contains("**Best Configuration:**"));
730    }
731
732    #[test]
733    fn test_generate_optimization_hints() {
734        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
735        let tensor = tensor_1d(&data).unwrap();
736        let config = QuantConfig::int8();
737
738        let hints = generate_optimization_hints(&tensor, &config).unwrap();
739        // Hints can be empty or non-empty - both are valid outcomes
740        assert!(hints.is_empty() || !hints.is_empty());
741
742        // Test with per-channel config
743        let per_channel_config = QuantConfig::per_channel(0);
744        let hints = generate_optimization_hints(&tensor, &per_channel_config).unwrap();
745        // Just verify the call succeeds - hints may or may not be present
746        assert!(hints.is_empty() || !hints.is_empty());
747    }
748
749    #[test]
750    fn test_benchmark_quantization_performance() {
751        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
752        let tensor = tensor_1d(&data).unwrap();
753
754        let configs = vec![QuantConfig::int8(), QuantConfig::binary()];
755
756        let results = benchmark_quantization_performance(&tensor, &configs, 3).unwrap();
757
758        // Should return timing results for successful configs
759        assert!(results.len() <= configs.len());
760
761        for (config, quantize_time, dequantize_time) in &results {
762            assert!(configs.iter().any(|c| c.scheme == config.scheme));
763            assert!(quantize_time >= &0.0);
764            assert!(dequantize_time >= &0.0);
765        }
766    }
767
768    #[test]
769    fn test_quantization_metrics_edge_cases() {
770        // Test with different shaped tensors (should fail)
771        let tensor1 = tensor_1d(&[1.0, 2.0]).unwrap();
772        let tensor2 = tensor_1d(&[1.0, 2.0, 3.0]).unwrap();
773
774        let result = calculate_quantization_metrics(&tensor1, &tensor2, 32, 8);
775        assert!(result.is_err());
776
777        // Test with zero tensors
778        let zero_tensor = tensor_1d(&[0.0, 0.0, 0.0]).unwrap();
779        let metrics = calculate_quantization_metrics(&zero_tensor, &zero_tensor, 32, 8).unwrap();
780
781        assert_eq!(metrics.mse, 0.0);
782        assert_eq!(metrics.mae, 0.0);
783        assert_eq!(metrics.max_error, 0.0);
784        assert_eq!(metrics.zero_error_percentage, 100.0);
785        assert!(metrics.psnr.is_infinite());
786        assert_eq!(metrics.cosine_similarity, 0.0); // Both vectors are zero
787
788        // Test with very small differences
789        let original = tensor_1d(&[1.0, 2.0, 3.0]).unwrap();
790        let almost_same = tensor_1d(&[1.0000001, 2.0000001, 3.0000001]).unwrap();
791
792        let metrics = calculate_quantization_metrics(&original, &almost_same, 32, 8).unwrap();
793        assert!(metrics.mse < 1e-12);
794        assert!(metrics.mae < 1e-6);
795        assert!(metrics.cosine_similarity > 0.999999);
796        assert!(metrics.psnr > 100.0); // Very high PSNR for very small error
797    }
798
799    #[test]
800    fn test_metrics_default() {
801        let default_metrics = QuantizationMetrics::default();
802        assert_eq!(default_metrics.mse, 0.0);
803        assert_eq!(default_metrics.psnr, 0.0);
804        assert_eq!(default_metrics.snr, 0.0);
805        assert_eq!(default_metrics.mae, 0.0);
806        assert_eq!(default_metrics.max_error, 0.0);
807        assert_eq!(default_metrics.zero_error_percentage, 100.0);
808        assert_eq!(default_metrics.cosine_similarity, 1.0);
809        assert_eq!(default_metrics.compression_ratio, 1.0);
810    }
811}