1type 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#[derive(Debug, Clone)]
19pub struct PerformanceResult {
20 pub operation: String,
22 pub shape: Vec<usize>,
24 pub dtype: String,
26 pub execution_time_ms: f64,
28 pub memory_usage_bytes: usize,
30 pub success: bool,
32 pub parameters: HashMap<String, String>,
34}
35
36#[derive(Debug, Clone)]
38pub struct AccuracyResult {
39 pub operation: String,
41 pub max_abs_diff: f64,
43 pub mean_abs_diff: f64,
45 pub rmse: f64,
47 pub relative_error: f64,
49 pub numerically_equivalent: bool,
51 pub tolerance: f64,
53}
54
55#[derive(Debug, Clone)]
57pub struct CompatibilityResult {
58 pub function: String,
60 pub parameter: String,
62 pub compatible: bool,
64 pub error_message: Option<String>,
66 pub suggestion: Option<String>,
68}
69
70#[derive(Debug, Clone)]
72pub struct BenchmarkConfig {
73 pub array_sizes: Vec<Vec<usize>>,
75 pub dtypes: Vec<String>,
77 pub iterations: usize,
79 pub warmup_iterations: usize,
81 pub profile_memory: bool,
83 pub numerical_tolerance: f64,
85}
86
87impl Default for BenchmarkConfig {
88 fn default() -> Self {
89 Self {
90 array_sizes: vec![
91 vec![100, 100], vec![512, 512], vec![1024, 1024], vec![50, 50, 50], vec![100, 100, 100], vec![200, 200, 200], vec![10000], vec![1000, 1000], ],
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
109pub struct SciPyBenchmarkSuite {
111 config: BenchmarkConfig,
112 results: Vec<PerformanceResult>,
113 accuracy_results: Vec<AccuracyResult>,
114 compatibility_results: Vec<CompatibilityResult>,
115}
116
117impl SciPyBenchmarkSuite {
118 pub fn new() -> Self {
120 Self::with_config(BenchmarkConfig::default())
121 }
122
123 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 pub fn benchmark_filters(&mut self) -> Result<()> {
135 for shape in &self.config.array_sizes.clone() {
136 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 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), success: true,
174 parameters: [("sigma".to_string(), "1.0".to_string())]
175 .iter()
176 .cloned()
177 .collect(),
178 });
179
180 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 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 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 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), success: true,
281 parameters: [("sigma".to_string(), "1.0".to_string())]
282 .iter()
283 .cloned()
284 .collect(),
285 });
286
287 Ok(())
288 }
289
290 pub fn benchmark_morphology(&mut self) -> Result<()> {
292 for shape in &self.config.array_sizes.clone() {
293 if shape.len() > 2 {
294 continue; }
296
297 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), success: true,
318 parameters: HashMap::new(),
319 });
320
321 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 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 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, success: true,
377 parameters: [("zoom".to_string(), "[2.0,2.0]".to_string())]
378 .iter()
379 .cloned()
380 .collect(),
381 });
382
383 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 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 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 let start = Instant::now();
445 for _ in 0..self.config.warmup_iterations {
446 let _ = moments(&input, 2)?; }
448
449 let start = Instant::now();
450 for _ in 0..self.config.iterations {
451 let _ = moments(&input, 2)?; }
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 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 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 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 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 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 pub fn get_results(&self) -> &[PerformanceResult] {
571 &self.results
572 }
573
574 pub fn get_accuracy_results(&self) -> &[AccuracyResult] {
576 &self.accuracy_results
577 }
578
579 pub fn get_compatibility_results(&self) -> &[CompatibilityResult] {
581 &self.compatibility_results
582 }
583}
584
585#[allow(dead_code)]
587fn estimate_memory_usage(shape: &[usize], dtype_size: usize) -> usize {
588 shape.iter().product::<usize>() * dtype_size
589}
590
591#[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#[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); }
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}