type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
use crate::filters::*;
use crate::interpolation::*;
use crate::measurements::*;
use crate::morphology::*;
use scirs2_core::ndarray::{Array2, ArrayView2};
use std::collections::HashMap;
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct PerformanceResult {
pub operation: String,
pub shape: Vec<usize>,
pub dtype: String,
pub execution_time_ms: f64,
pub memory_usage_bytes: usize,
pub success: bool,
pub parameters: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct AccuracyResult {
pub operation: String,
pub max_abs_diff: f64,
pub mean_abs_diff: f64,
pub rmse: f64,
pub relative_error: f64,
pub numerically_equivalent: bool,
pub tolerance: f64,
}
#[derive(Debug, Clone)]
pub struct CompatibilityResult {
pub function: String,
pub parameter: String,
pub compatible: bool,
pub error_message: Option<String>,
pub suggestion: Option<String>,
}
#[derive(Debug, Clone)]
pub struct BenchmarkConfig {
pub array_sizes: Vec<Vec<usize>>,
pub dtypes: Vec<String>,
pub iterations: usize,
pub warmup_iterations: usize,
pub profile_memory: bool,
pub numerical_tolerance: f64,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
array_sizes: vec![
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], ],
dtypes: vec!["f32".to_string(), "f64".to_string()],
iterations: 10,
warmup_iterations: 3,
profile_memory: true,
numerical_tolerance: 1e-6,
}
}
}
pub struct SciPyBenchmarkSuite {
config: BenchmarkConfig,
results: Vec<PerformanceResult>,
accuracy_results: Vec<AccuracyResult>,
compatibility_results: Vec<CompatibilityResult>,
}
impl SciPyBenchmarkSuite {
pub fn new() -> Self {
Self::with_config(BenchmarkConfig::default())
}
pub fn with_config(config: BenchmarkConfig) -> Self {
Self {
config,
results: Vec::new(),
accuracy_results: Vec::new(),
compatibility_results: Vec::new(),
}
}
pub fn benchmark_filters(&mut self) -> Result<()> {
for shape in &self.config.array_sizes.clone() {
if shape.len() > 2 {
continue;
}
for dtype in &self.config.dtypes.clone() {
match dtype.as_str() {
"f32" => self.benchmark_filters_f32(shape)?,
"f64" => self.benchmark_filters_f64(shape)?,
_ => continue,
}
}
}
Ok(())
}
fn benchmark_filters_f32(&mut self, shape: &[usize]) -> Result<()> {
let input = Array2::zeros((shape[0], shape[1]));
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = gaussian_filter(&input, 1.0, None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = gaussian_filter(&input, 1.0, None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "gaussian_filter".to_string(),
shape: shape.to_vec(),
dtype: "f32".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 4), success: true,
parameters: [("sigma".to_string(), "1.0".to_string())]
.iter()
.cloned()
.collect(),
});
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = median_filter(&input, &[3, 3], None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = median_filter(&input, &[3, 3], None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "median_filter".to_string(),
shape: shape.to_vec(),
dtype: "f32".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 4),
success: true,
parameters: [("size".to_string(), "[3,3]".to_string())]
.iter()
.cloned()
.collect(),
});
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = uniform_filter(&input, &[3, 3], None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = uniform_filter(&input, &[3, 3], None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "uniform_filter".to_string(),
shape: shape.to_vec(),
dtype: "f32".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 4),
success: true,
parameters: [("size".to_string(), "[3,3]".to_string())]
.iter()
.cloned()
.collect(),
});
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = sobel(&input, 0, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = sobel(&input, 0, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "sobel_filter".to_string(),
shape: shape.to_vec(),
dtype: "f32".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 4),
success: true,
parameters: [("axis".to_string(), "0".to_string())]
.iter()
.cloned()
.collect(),
});
Ok(())
}
fn benchmark_filters_f64(&mut self, shape: &[usize]) -> Result<()> {
let input = Array2::<f64>::zeros((shape[0], shape[1]));
let _start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = gaussian_filter(&input, 1.0, None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = gaussian_filter(&input, 1.0, None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "gaussian_filter".to_string(),
shape: shape.to_vec(),
dtype: "f64".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 8), success: true,
parameters: [("sigma".to_string(), "1.0".to_string())]
.iter()
.cloned()
.collect(),
});
Ok(())
}
pub fn benchmark_morphology(&mut self) -> Result<()> {
for shape in &self.config.array_sizes.clone() {
if shape.len() > 2 {
continue; }
let binary_input = Array2::from_elem((shape[0], shape[1]), true);
let _start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = binary_erosion(&binary_input, None, None, None, None, None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = binary_erosion(&binary_input, None, None, None, None, None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "binary_erosion".to_string(),
shape: shape.to_vec(),
dtype: "bool".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 1), success: true,
parameters: HashMap::new(),
});
let grayscale_input = Array2::<f64>::zeros((shape[0], shape[1]));
let _start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = grey_erosion(&grayscale_input, None, None, None, None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = grey_erosion(&grayscale_input, None, None, None, None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "grey_erosion".to_string(),
shape: shape.to_vec(),
dtype: "f64".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 8),
success: true,
parameters: HashMap::new(),
});
}
Ok(())
}
pub fn benchmark_interpolation(&mut self) -> Result<()> {
for shape in &self.config.array_sizes.clone() {
if shape.len() > 2 {
continue;
}
let input = Array2::<f64>::zeros((shape[0], shape[1]));
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = zoom(&input, 2.0, None, None, None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = zoom(&input, 2.0, None, None, None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "zoom".to_string(),
shape: shape.to_vec(),
dtype: "f64".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 8) * 4, success: true,
parameters: [("zoom".to_string(), "[2.0,2.0]".to_string())]
.iter()
.cloned()
.collect(),
});
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = rotate(&input, 45.0, None, None, None, None, None, None)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = rotate(&input, 45.0, None, None, None, None, None, None)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "rotate".to_string(),
shape: shape.to_vec(),
dtype: "f64".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 8),
success: true,
parameters: [("angle".to_string(), "45.0".to_string())]
.iter()
.cloned()
.collect(),
});
}
Ok(())
}
pub fn benchmark_measurements(&mut self) -> Result<()> {
for shape in &self.config.array_sizes.clone() {
if shape.len() > 2 {
continue;
}
let input = Array2::<f64>::ones((shape[0], shape[1]));
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = center_of_mass(&input)?;
}
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = center_of_mass(&input)?;
}
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "center_of_mass".to_string(),
shape: shape.to_vec(),
dtype: "f64".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 8),
success: true,
parameters: HashMap::new(),
});
let start = Instant::now();
for _ in 0..self.config.warmup_iterations {
let _ = moments(&input, 2)?; }
let start = Instant::now();
for _ in 0..self.config.iterations {
let _ = moments(&input, 2)?; }
let duration = start.elapsed();
self.results.push(PerformanceResult {
operation: "moments".to_string(),
shape: shape.to_vec(),
dtype: "f64".to_string(),
execution_time_ms: duration.as_millis() as f64 / self.config.iterations as f64,
memory_usage_bytes: estimate_memory_usage(shape, 8),
success: true,
parameters: HashMap::new(),
});
}
Ok(())
}
pub fn run_all_benchmarks(&mut self) -> Result<()> {
println!("Running comprehensive SciPy ndimage performance comparison...");
println!("Benchmarking filters...");
self.benchmark_filters()?;
println!("Benchmarking morphology...");
self.benchmark_morphology()?;
println!("Benchmarking interpolation...");
self.benchmark_interpolation()?;
println!("Benchmarking measurements...");
self.benchmark_measurements()?;
println!("Benchmark suite completed!");
Ok(())
}
pub fn generate_report(&self) -> String {
let mut report = String::new();
report.push_str("# SciPy ndimage Performance Comparison Report\n\n");
report.push_str(&format!(
"Total operations benchmarked: {}\n",
self.results.len()
));
report.push_str(&format!(
"Configuration: {} iterations, {} warmup\n\n",
self.config.iterations, self.config.warmup_iterations
));
let mut operations: HashMap<String, Vec<&PerformanceResult>> = HashMap::new();
for result in &self.results {
operations
.entry(result.operation.clone())
.or_insert_with(Vec::new)
.push(result);
}
for (operation, results) in operations {
report.push_str(&format!("## {}\n", operation));
for result in results {
report.push_str(&format!(
"- Shape: {:?}, Type: {}, Time: {:.2}ms, Memory: {:.2}MB\n",
result.shape,
result.dtype,
result.execution_time_ms,
result.memory_usage_bytes as f64 / 1_000_000.0
));
}
report.push('\n');
}
if !self.accuracy_results.is_empty() {
report.push_str("## Numerical Accuracy Validation\n\n");
for result in &self.accuracy_results {
report.push_str(&format!(
"- {}: Max, diff: {:.2e}, Mean diff: {:.2e}, RMSE: {:.2e}, Compatible: {}\n",
result.operation,
result.max_abs_diff,
result.mean_abs_diff,
result.rmse,
result.numerically_equivalent
));
}
report.push('\n');
}
if !self.compatibility_results.is_empty() {
report.push_str("## API Compatibility Results\n\n");
for result in &self.compatibility_results {
report.push_str(&format!(
"- {}.{}: {}\n",
result.function,
result.parameter,
if result.compatible {
"✓ Compatible"
} else {
"✗ Incompatible"
}
));
if let Some(msg) = &result.error_message {
report.push_str(&format!(" Error: {}\n", msg));
}
if let Some(suggestion) = &result.suggestion {
report.push_str(&format!(" Suggestion: {}\n", suggestion));
}
}
}
report
}
pub fn get_results(&self) -> &[PerformanceResult] {
&self.results
}
pub fn get_accuracy_results(&self) -> &[AccuracyResult] {
&self.accuracy_results
}
pub fn get_compatibility_results(&self) -> &[CompatibilityResult] {
&self.compatibility_results
}
}
#[allow(dead_code)]
fn estimate_memory_usage(shape: &[usize], dtype_size: usize) -> usize {
shape.iter().product::<usize>() * dtype_size
}
#[allow(dead_code)]
pub fn calculate_accuracy_metrics<T>(
reference: &ArrayView2<T>,
computed: &ArrayView2<T>,
tolerance: f64,
) -> AccuracyResult
where
T: Clone + Copy + Into<f64> + PartialOrd,
{
let mut max_abs_diff = 0.0;
let mut sum_abs_diff = 0.0;
let mut sum_squared_diff = 0.0;
let mut sum_relative_error = 0.0;
let mut count = 0;
let mut count_nonzero = 0;
for (r, c) in reference.iter().zip(computed.iter()) {
let ref_val: f64 = (*r).into();
let comp_val: f64 = (*c).into();
let abs_diff = (ref_val - comp_val).abs();
max_abs_diff = f64::max(max_abs_diff, abs_diff);
sum_abs_diff += abs_diff;
sum_squared_diff += abs_diff * abs_diff;
count += 1;
if ref_val.abs() > 1e-15 {
sum_relative_error += abs_diff / ref_val.abs();
count_nonzero += 1;
}
}
let mean_abs_diff = sum_abs_diff / count as f64;
let rmse = (sum_squared_diff / count as f64).sqrt();
let relative_error = if count_nonzero > 0 {
sum_relative_error / count_nonzero as f64
} else {
0.0
};
AccuracyResult {
operation: "comparison".to_string(),
max_abs_diff,
mean_abs_diff,
rmse,
relative_error,
numerically_equivalent: max_abs_diff < tolerance,
tolerance,
}
}
#[allow(dead_code)]
pub fn validate_api_compatibility(
function_name: &str,
parameter_tests: &[(String, fn() -> bool, Option<String>)],
) -> Vec<CompatibilityResult> {
let mut results = Vec::new();
for (param_name, test_fn, suggestion) in parameter_tests {
let compatible = test_fn();
results.push(CompatibilityResult {
function: function_name.to_string(),
parameter: param_name.clone(),
compatible,
error_message: if compatible {
None
} else {
Some("Parameter validation failed".to_string())
},
suggestion: suggestion.clone(),
});
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_benchmark_suite_creation() {
let suite = SciPyBenchmarkSuite::new();
assert_eq!(suite.results.len(), 0);
assert_eq!(suite.accuracy_results.len(), 0);
assert_eq!(suite.compatibility_results.len(), 0);
}
#[test]
fn test_memory_estimation() {
assert_eq!(estimate_memory_usage(&[100, 100], 4), 40000);
assert_eq!(estimate_memory_usage(&[50, 50, 50], 8), 1000000);
}
#[test]
fn test_accuracy_calculation() {
let ref_array = Array2::from_elem((3, 3), 1.0);
let comp_array = Array2::from_elem((3, 3), 1.1);
let accuracy = calculate_accuracy_metrics(&ref_array.view(), &comp_array.view(), 0.2);
assert!(accuracy.max_abs_diff > 0.0);
assert!(accuracy.mean_abs_diff > 0.0);
assert!(accuracy.numerically_equivalent); }
#[test]
fn test_performance_result_creation() {
let result = PerformanceResult {
operation: "test_op".to_string(),
shape: vec![100, 100],
dtype: "f64".to_string(),
execution_time_ms: 10.5,
memory_usage_bytes: 80000,
success: true,
parameters: HashMap::new(),
};
assert_eq!(result.operation, "test_op");
assert_eq!(result.shape, vec![100, 100]);
assert!(result.success);
}
}