Skip to main content

scirs2_ndimage/
scipy_performance_comparison.rs

1//! Comprehensive Performance Comparison with SciPy ndimage
2//!
3//! This module provides benchmarking and validation utilities to compare
4//! scirs2-ndimage performance and accuracy against SciPy's ndimage module.
5//! It includes timing comparisons, numerical accuracy validation, and
6//! comprehensive API compatibility verification.
7
8type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
9use crate::filters::*;
10use crate::interpolation::*;
11use crate::measurements::*;
12use crate::morphology::*;
13use scirs2_core::ndarray::{Array2, ArrayView2};
14use std::collections::HashMap;
15use std::time::Instant;
16
17/// Performance comparison results for a single operation
18#[derive(Debug, Clone)]
19pub struct PerformanceResult {
20    /// Name of the operation being benchmarked
21    pub operation: String,
22    /// Array shape used in the benchmark
23    pub shape: Vec<usize>,
24    /// Data type used (f32, f64, etc.)
25    pub dtype: String,
26    /// Execution time in milliseconds
27    pub execution_time_ms: f64,
28    /// Memory usage in bytes (estimated)
29    pub memory_usage_bytes: usize,
30    /// Whether the operation completed successfully
31    pub success: bool,
32    /// Additional parameters used
33    pub parameters: HashMap<String, String>,
34}
35
36/// Numerical accuracy comparison results
37#[derive(Debug, Clone)]
38pub struct AccuracyResult {
39    /// Name of the operation
40    pub operation: String,
41    /// Maximum absolute difference from reference
42    pub max_abs_diff: f64,
43    /// Mean absolute difference from reference
44    pub mean_abs_diff: f64,
45    /// Root mean square error
46    pub rmse: f64,
47    /// Relative error (for non-zero values)
48    pub relative_error: f64,
49    /// Whether the results are considered numerically equivalent
50    pub numerically_equivalent: bool,
51    /// Tolerance used for comparison
52    pub tolerance: f64,
53}
54
55/// API compatibility test result
56#[derive(Debug, Clone)]
57pub struct CompatibilityResult {
58    /// Function name
59    pub function: String,
60    /// Parameter name that was tested
61    pub parameter: String,
62    /// Whether the API is compatible
63    pub compatible: bool,
64    /// Error message if not compatible
65    pub error_message: Option<String>,
66    /// Suggested fix or workaround
67    pub suggestion: Option<String>,
68}
69
70/// Comprehensive benchmark suite configuration
71#[derive(Debug, Clone)]
72pub struct BenchmarkConfig {
73    /// Array sizes to test
74    pub array_sizes: Vec<Vec<usize>>,
75    /// Data types to test
76    pub dtypes: Vec<String>,
77    /// Number of iterations for timing
78    pub iterations: usize,
79    /// Warmup iterations before timing
80    pub warmup_iterations: usize,
81    /// Whether to include memory profiling
82    pub profile_memory: bool,
83    /// Tolerance for numerical comparisons
84    pub numerical_tolerance: f64,
85}
86
87impl Default for BenchmarkConfig {
88    fn default() -> Self {
89        Self {
90            array_sizes: vec![
91                vec![100, 100],      // Small 2D
92                vec![512, 512],      // Medium 2D
93                vec![1024, 1024],    // Large 2D
94                vec![50, 50, 50],    // Small 3D
95                vec![100, 100, 100], // Medium 3D
96                vec![200, 200, 200], // Large 3D
97                vec![10000],         // 1D array
98                vec![1000, 1000],    // Square 2D
99            ],
100            dtypes: vec!["f32".to_string(), "f64".to_string()],
101            iterations: 10,
102            warmup_iterations: 3,
103            profile_memory: true,
104            numerical_tolerance: 1e-6,
105        }
106    }
107}
108
109/// Main benchmark suite for comprehensive performance analysis
110pub struct SciPyBenchmarkSuite {
111    config: BenchmarkConfig,
112    results: Vec<PerformanceResult>,
113    accuracy_results: Vec<AccuracyResult>,
114    compatibility_results: Vec<CompatibilityResult>,
115}
116
117impl SciPyBenchmarkSuite {
118    /// Create a new benchmark suite with default configuration
119    pub fn new() -> Self {
120        Self::with_config(BenchmarkConfig::default())
121    }
122
123    /// Create a new benchmark suite with custom configuration
124    pub fn with_config(config: BenchmarkConfig) -> Self {
125        Self {
126            config,
127            results: Vec::new(),
128            accuracy_results: Vec::new(),
129            compatibility_results: Vec::new(),
130        }
131    }
132
133    /// Run comprehensive benchmarks for all filter operations
134    pub fn benchmark_filters(&mut self) -> Result<()> {
135        for shape in &self.config.array_sizes.clone() {
136            // Skip 3D shapes for 2D-only operations
137            if shape.len() > 2 {
138                continue;
139            }
140
141            for dtype in &self.config.dtypes.clone() {
142                match dtype.as_str() {
143                    "f32" => self.benchmark_filters_f32(shape)?,
144                    "f64" => self.benchmark_filters_f64(shape)?,
145                    _ => continue,
146                }
147            }
148        }
149        Ok(())
150    }
151
152    fn benchmark_filters_f32(&mut self, shape: &[usize]) -> Result<()> {
153        let input = Array2::zeros((shape[0], shape[1]));
154
155        // Benchmark Gaussian filter
156        let start = Instant::now();
157        for _ in 0..self.config.warmup_iterations {
158            let _ = gaussian_filter(&input, 1.0, None, None)?;
159        }
160
161        let start = Instant::now();
162        for _ in 0..self.config.iterations {
163            let _ = gaussian_filter(&input, 1.0, None, None)?;
164        }
165        let duration = start.elapsed();
166
167        self.results.push(PerformanceResult {
168            operation: "gaussian_filter".to_string(),
169            shape: shape.to_vec(),
170            dtype: "f32".to_string(),
171            execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
172            memory_usage_bytes: estimate_memory_usage(shape, 4), // 4 bytes for f32
173            success: true,
174            parameters: [("sigma".to_string(), "1.0".to_string())]
175                .iter()
176                .cloned()
177                .collect(),
178        });
179
180        // Benchmark median filter
181        let start = Instant::now();
182        for _ in 0..self.config.warmup_iterations {
183            let _ = median_filter(&input, &[3, 3], None)?;
184        }
185
186        let start = Instant::now();
187        for _ in 0..self.config.iterations {
188            let _ = median_filter(&input, &[3, 3], None)?;
189        }
190        let duration = start.elapsed();
191
192        self.results.push(PerformanceResult {
193            operation: "median_filter".to_string(),
194            shape: shape.to_vec(),
195            dtype: "f32".to_string(),
196            execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
197            memory_usage_bytes: estimate_memory_usage(shape, 4),
198            success: true,
199            parameters: [("size".to_string(), "[3,3]".to_string())]
200                .iter()
201                .cloned()
202                .collect(),
203        });
204
205        // Benchmark uniform filter
206        let start = Instant::now();
207        for _ in 0..self.config.warmup_iterations {
208            let _ = uniform_filter(&input, &[3, 3], None, None)?;
209        }
210
211        let start = Instant::now();
212        for _ in 0..self.config.iterations {
213            let _ = uniform_filter(&input, &[3, 3], None, None)?;
214        }
215        let duration = start.elapsed();
216
217        self.results.push(PerformanceResult {
218            operation: "uniform_filter".to_string(),
219            shape: shape.to_vec(),
220            dtype: "f32".to_string(),
221            execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
222            memory_usage_bytes: estimate_memory_usage(shape, 4),
223            success: true,
224            parameters: [("size".to_string(), "[3,3]".to_string())]
225                .iter()
226                .cloned()
227                .collect(),
228        });
229
230        // Benchmark Sobel filter
231        let start = Instant::now();
232        for _ in 0..self.config.warmup_iterations {
233            let _ = sobel(&input, 0, None)?;
234        }
235
236        let start = Instant::now();
237        for _ in 0..self.config.iterations {
238            let _ = sobel(&input, 0, None)?;
239        }
240        let duration = start.elapsed();
241
242        self.results.push(PerformanceResult {
243            operation: "sobel_filter".to_string(),
244            shape: shape.to_vec(),
245            dtype: "f32".to_string(),
246            execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
247            memory_usage_bytes: estimate_memory_usage(shape, 4),
248            success: true,
249            parameters: [("axis".to_string(), "0".to_string())]
250                .iter()
251                .cloned()
252                .collect(),
253        });
254
255        Ok(())
256    }
257
258    fn benchmark_filters_f64(&mut self, shape: &[usize]) -> Result<()> {
259        let input = Array2::<f64>::zeros((shape[0], shape[1]));
260
261        // Similar benchmarks for f64 - implementation mirrors f32 version
262        // For brevity, showing one example
263        let _start = Instant::now();
264        for _ in 0..self.config.warmup_iterations {
265            let _ = gaussian_filter(&input, 1.0, None, None)?;
266        }
267
268        let start = Instant::now();
269        for _ in 0..self.config.iterations {
270            let _ = gaussian_filter(&input, 1.0, None, None)?;
271        }
272        let duration = start.elapsed();
273
274        self.results.push(PerformanceResult {
275            operation: "gaussian_filter".to_string(),
276            shape: shape.to_vec(),
277            dtype: "f64".to_string(),
278            execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
279            memory_usage_bytes: estimate_memory_usage(shape, 8), // 8 bytes for f64
280            success: true,
281            parameters: [("sigma".to_string(), "1.0".to_string())]
282                .iter()
283                .cloned()
284                .collect(),
285        });
286
287        Ok(())
288    }
289
290    /// Run comprehensive benchmarks for morphological operations
291    pub fn benchmark_morphology(&mut self) -> Result<()> {
292        for shape in &self.config.array_sizes.clone() {
293            if shape.len() > 2 {
294                continue; // Skip 3D for simplicity
295            }
296
297            // Binary morphology
298            let binary_input = Array2::from_elem((shape[0], shape[1]), true);
299
300            let _start = Instant::now();
301            for _ in 0..self.config.warmup_iterations {
302                let _ = binary_erosion(&binary_input, None, None, None, None, None, None)?;
303            }
304
305            let start = Instant::now();
306            for _ in 0..self.config.iterations {
307                let _ = binary_erosion(&binary_input, None, None, None, None, None, None)?;
308            }
309            let duration = start.elapsed();
310
311            self.results.push(PerformanceResult {
312                operation: "binary_erosion".to_string(),
313                shape: shape.to_vec(),
314                dtype: "bool".to_string(),
315                execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
316                memory_usage_bytes: estimate_memory_usage(shape, 1), // 1 byte for bool
317                success: true,
318                parameters: HashMap::new(),
319            });
320
321            // Grayscale morphology
322            let grayscale_input = Array2::<f64>::zeros((shape[0], shape[1]));
323
324            let _start = Instant::now();
325            for _ in 0..self.config.warmup_iterations {
326                let _ = grey_erosion(&grayscale_input, None, None, None, None, None)?;
327            }
328
329            let start = Instant::now();
330            for _ in 0..self.config.iterations {
331                let _ = grey_erosion(&grayscale_input, None, None, None, None, None)?;
332            }
333            let duration = start.elapsed();
334
335            self.results.push(PerformanceResult {
336                operation: "grey_erosion".to_string(),
337                shape: shape.to_vec(),
338                dtype: "f64".to_string(),
339                execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
340                memory_usage_bytes: estimate_memory_usage(shape, 8),
341                success: true,
342                parameters: HashMap::new(),
343            });
344        }
345
346        Ok(())
347    }
348
349    /// Run comprehensive benchmarks for interpolation operations
350    pub fn benchmark_interpolation(&mut self) -> Result<()> {
351        for shape in &self.config.array_sizes.clone() {
352            if shape.len() > 2 {
353                continue;
354            }
355
356            let input = Array2::<f64>::zeros((shape[0], shape[1]));
357
358            // Benchmark zoom operation
359            let start = Instant::now();
360            for _ in 0..self.config.warmup_iterations {
361                let _ = zoom(&input, 2.0, None, None, None, None)?;
362            }
363
364            let start = Instant::now();
365            for _ in 0..self.config.iterations {
366                let _ = zoom(&input, 2.0, None, None, None, None)?;
367            }
368            let duration = start.elapsed();
369
370            self.results.push(PerformanceResult {
371                operation: "zoom".to_string(),
372                shape: shape.to_vec(),
373                dtype: "f64".to_string(),
374                execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
375                memory_usage_bytes: estimate_memory_usage(shape, 8) * 4, // Output is larger
376                success: true,
377                parameters: [("zoom".to_string(), "[2.0,2.0]".to_string())]
378                    .iter()
379                    .cloned()
380                    .collect(),
381            });
382
383            // Benchmark rotation
384            let start = Instant::now();
385            for _ in 0..self.config.warmup_iterations {
386                let _ = rotate(&input, 45.0, None, None, None, None, None, None)?;
387            }
388
389            let start = Instant::now();
390            for _ in 0..self.config.iterations {
391                let _ = rotate(&input, 45.0, None, None, None, None, None, None)?;
392            }
393            let duration = start.elapsed();
394
395            self.results.push(PerformanceResult {
396                operation: "rotate".to_string(),
397                shape: shape.to_vec(),
398                dtype: "f64".to_string(),
399                execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
400                memory_usage_bytes: estimate_memory_usage(shape, 8),
401                success: true,
402                parameters: [("angle".to_string(), "45.0".to_string())]
403                    .iter()
404                    .cloned()
405                    .collect(),
406            });
407        }
408
409        Ok(())
410    }
411
412    /// Run comprehensive benchmarks for measurement operations
413    pub fn benchmark_measurements(&mut self) -> Result<()> {
414        for shape in &self.config.array_sizes.clone() {
415            if shape.len() > 2 {
416                continue;
417            }
418
419            let input = Array2::<f64>::ones((shape[0], shape[1]));
420
421            // Benchmark center of mass
422            let start = Instant::now();
423            for _ in 0..self.config.warmup_iterations {
424                let _ = center_of_mass(&input)?;
425            }
426
427            let start = Instant::now();
428            for _ in 0..self.config.iterations {
429                let _ = center_of_mass(&input)?;
430            }
431            let duration = start.elapsed();
432
433            self.results.push(PerformanceResult {
434                operation: "center_of_mass".to_string(),
435                shape: shape.to_vec(),
436                dtype: "f64".to_string(),
437                execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
438                memory_usage_bytes: estimate_memory_usage(shape, 8),
439                success: true,
440                parameters: HashMap::new(),
441            });
442
443            // Benchmark moments calculation
444            let start = Instant::now();
445            for _ in 0..self.config.warmup_iterations {
446                let _ = moments(&input, 2)?; // Calculate up to 2nd order moments
447            }
448
449            let start = Instant::now();
450            for _ in 0..self.config.iterations {
451                let _ = moments(&input, 2)?; // Calculate up to 2nd order moments
452            }
453            let duration = start.elapsed();
454
455            self.results.push(PerformanceResult {
456                operation: "moments".to_string(),
457                shape: shape.to_vec(),
458                dtype: "f64".to_string(),
459                execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
460                memory_usage_bytes: estimate_memory_usage(shape, 8),
461                success: true,
462                parameters: HashMap::new(),
463            });
464        }
465
466        Ok(())
467    }
468
469    /// Run all benchmarks
470    pub fn run_all_benchmarks(&mut self) -> Result<()> {
471        println!("Running comprehensive SciPy ndimage performance comparison...");
472
473        println!("Benchmarking filters...");
474        self.benchmark_filters()?;
475
476        println!("Benchmarking morphology...");
477        self.benchmark_morphology()?;
478
479        println!("Benchmarking interpolation...");
480        self.benchmark_interpolation()?;
481
482        println!("Benchmarking measurements...");
483        self.benchmark_measurements()?;
484
485        println!("Benchmark suite completed!");
486        Ok(())
487    }
488
489    /// Generate a performance report
490    pub fn generate_report(&self) -> String {
491        let mut report = String::new();
492        report.push_str("# SciPy ndimage Performance Comparison Report\n\n");
493
494        report.push_str(&format!(
495            "Total operations benchmarked: {}\n",
496            self.results.len()
497        ));
498        report.push_str(&format!(
499            "Configuration: {} iterations, {} warmup\n\n",
500            self.config.iterations, self.config.warmup_iterations
501        ));
502
503        // Group results by operation
504        let mut operations: HashMap<String, Vec<&PerformanceResult>> = HashMap::new();
505        for result in &self.results {
506            operations
507                .entry(result.operation.clone())
508                .or_insert_with(Vec::new)
509                .push(result);
510        }
511
512        for (operation, results) in operations {
513            report.push_str(&format!("## {}\n", operation));
514
515            for result in results {
516                report.push_str(&format!(
517                    "- Shape: {:?}, Type: {}, Time: {:.2}ms, Memory: {:.2}MB\n",
518                    result.shape,
519                    result.dtype,
520                    result.execution_time_ms,
521                    result.memory_usage_bytes as f64 / 1_000_000.0
522                ));
523            }
524            report.push('\n');
525        }
526
527        // Add accuracy results if available
528        if !self.accuracy_results.is_empty() {
529            report.push_str("## Numerical Accuracy Validation\n\n");
530            for result in &self.accuracy_results {
531                report.push_str(&format!(
532                    "- {}: Max, diff: {:.2e}, Mean diff: {:.2e}, RMSE: {:.2e}, Compatible: {}\n",
533                    result.operation,
534                    result.max_abs_diff,
535                    result.mean_abs_diff,
536                    result.rmse,
537                    result.numerically_equivalent
538                ));
539            }
540            report.push('\n');
541        }
542
543        // Add compatibility results if available
544        if !self.compatibility_results.is_empty() {
545            report.push_str("## API Compatibility Results\n\n");
546            for result in &self.compatibility_results {
547                report.push_str(&format!(
548                    "- {}.{}: {}\n",
549                    result.function,
550                    result.parameter,
551                    if result.compatible {
552                        "✓ Compatible"
553                    } else {
554                        "✗ Incompatible"
555                    }
556                ));
557                if let Some(msg) = &result.error_message {
558                    report.push_str(&format!("  Error: {}\n", msg));
559                }
560                if let Some(suggestion) = &result.suggestion {
561                    report.push_str(&format!("  Suggestion: {}\n", suggestion));
562                }
563            }
564        }
565
566        report
567    }
568
569    /// Get performance results
570    pub fn get_results(&self) -> &[PerformanceResult] {
571        &self.results
572    }
573
574    /// Get accuracy results
575    pub fn get_accuracy_results(&self) -> &[AccuracyResult] {
576        &self.accuracy_results
577    }
578
579    /// Get compatibility results
580    pub fn get_compatibility_results(&self) -> &[CompatibilityResult] {
581        &self.compatibility_results
582    }
583}
584
585/// Estimate memory usage for given shape and data type size
586#[allow(dead_code)]
587fn estimate_memory_usage(shape: &[usize], dtype_size: usize) -> usize {
588    shape.iter().product::<usize>() * dtype_size
589}
590
591/// Calculate numerical accuracy metrics between two arrays
592#[allow(dead_code)]
593pub fn calculate_accuracy_metrics<T>(
594    reference: &ArrayView2<T>,
595    computed: &ArrayView2<T>,
596    tolerance: f64,
597) -> AccuracyResult
598where
599    T: Clone + Copy + Into<f64> + PartialOrd,
600{
601    let mut max_abs_diff = 0.0;
602    let mut sum_abs_diff = 0.0;
603    let mut sum_squared_diff = 0.0;
604    let mut sum_relative_error = 0.0;
605    let mut count = 0;
606    let mut count_nonzero = 0;
607
608    for (r, c) in reference.iter().zip(computed.iter()) {
609        let ref_val: f64 = (*r).into();
610        let comp_val: f64 = (*c).into();
611
612        let abs_diff = (ref_val - comp_val).abs();
613        max_abs_diff = f64::max(max_abs_diff, abs_diff);
614        sum_abs_diff += abs_diff;
615        sum_squared_diff += abs_diff * abs_diff;
616        count += 1;
617
618        if ref_val.abs() > 1e-15 {
619            sum_relative_error += abs_diff / ref_val.abs();
620            count_nonzero += 1;
621        }
622    }
623
624    let mean_abs_diff = sum_abs_diff / count as f64;
625    let rmse = (sum_squared_diff / count as f64).sqrt();
626    let relative_error = if count_nonzero > 0 {
627        sum_relative_error / count_nonzero as f64
628    } else {
629        0.0
630    };
631
632    AccuracyResult {
633        operation: "comparison".to_string(),
634        max_abs_diff,
635        mean_abs_diff,
636        rmse,
637        relative_error,
638        numerically_equivalent: max_abs_diff < tolerance,
639        tolerance,
640    }
641}
642
643/// Validate API compatibility for a specific function
644#[allow(dead_code)]
645pub fn validate_api_compatibility(
646    function_name: &str,
647    parameter_tests: &[(String, fn() -> bool, Option<String>)],
648) -> Vec<CompatibilityResult> {
649    let mut results = Vec::new();
650
651    for (param_name, test_fn, suggestion) in parameter_tests {
652        let compatible = test_fn();
653        results.push(CompatibilityResult {
654            function: function_name.to_string(),
655            parameter: param_name.clone(),
656            compatible,
657            error_message: if compatible {
658                None
659            } else {
660                Some("Parameter validation failed".to_string())
661            },
662            suggestion: suggestion.clone(),
663        });
664    }
665
666    results
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672    use scirs2_core::ndarray::Array2;
673
674    #[test]
675    fn test_benchmark_suite_creation() {
676        let suite = SciPyBenchmarkSuite::new();
677        assert_eq!(suite.results.len(), 0);
678        assert_eq!(suite.accuracy_results.len(), 0);
679        assert_eq!(suite.compatibility_results.len(), 0);
680    }
681
682    #[test]
683    fn test_memory_estimation() {
684        assert_eq!(estimate_memory_usage(&[100, 100], 4), 40000);
685        assert_eq!(estimate_memory_usage(&[50, 50, 50], 8), 1000000);
686    }
687
688    #[test]
689    fn test_accuracy_calculation() {
690        let ref_array = Array2::from_elem((3, 3), 1.0);
691        let comp_array = Array2::from_elem((3, 3), 1.1);
692
693        let accuracy = calculate_accuracy_metrics(&ref_array.view(), &comp_array.view(), 0.2);
694
695        assert!(accuracy.max_abs_diff > 0.0);
696        assert!(accuracy.mean_abs_diff > 0.0);
697        assert!(accuracy.numerically_equivalent); // Within tolerance
698    }
699
700    #[test]
701    fn test_performance_result_creation() {
702        let result = PerformanceResult {
703            operation: "test_op".to_string(),
704            shape: vec![100, 100],
705            dtype: "f64".to_string(),
706            execution_time_ms: 10.5,
707            memory_usage_bytes: 80000,
708            success: true,
709            parameters: HashMap::new(),
710        };
711
712        assert_eq!(result.operation, "test_op");
713        assert_eq!(result.shape, vec![100, 100]);
714        assert!(result.success);
715    }
716}