type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
use crate::filters::*;
use crate::interpolation::zoom;
use crate::interpolation::*;
use crate::measurements::*;
use crate::morphology::*;
use scirs2_core::ndarray::{Array2, ArrayView2};
use scirs2_core::numeric::ToPrimitive;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub test_name: String,
pub function_name: String,
pub parameters: HashMap<String, String>,
pub passed: bool,
pub max_abs_diff: f64,
pub mean_abs_diff: f64,
pub rmse: f64,
pub relative_error: f64,
pub reference_source: String,
pub reference_sample: Vec<f64>,
pub computed_sample: Vec<f64>,
pub tolerance: f64,
pub notes: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ValidationConfig {
pub tolerance: f64,
pub test_edge_cases: bool,
pub test_large_arrays: bool,
pub max_test_size: usize,
pub num_random_tests: usize,
pub random_seed: u64,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
tolerance: 1e-10,
test_edge_cases: true,
test_large_arrays: false, max_test_size: 1000,
num_random_tests: 10,
random_seed: 42,
}
}
}
pub struct SciPyValidationSuite {
config: ValidationConfig,
results: Vec<ValidationResult>,
passed_tests: usize,
failed_tests: usize,
}
impl SciPyValidationSuite {
pub fn new() -> Self {
Self::with_config(ValidationConfig::default())
}
pub fn with_config(config: ValidationConfig) -> Self {
Self {
config,
results: Vec::new(),
passed_tests: 0,
failed_tests: 0,
}
}
pub fn validate_gaussian_filter(&mut self) -> Result<()> {
let input = scirs2_core::ndarray::array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let reference = scirs2_core::ndarray::array![
[2.9347, 3.9745, 4.5966],
[4.6665, 5.0000, 5.3335],
[5.4034, 6.0255, 7.0653]
];
let result = gaussian_filter(&input, 1.0, None, None)?;
let validation = self.calculate_validationmetrics(
&reference.view(),
&result.view(),
"gaussian_filter_3x3_sigma1".to_string(),
"gaussian_filter".to_string(),
[("sigma".to_string(), "1.0".to_string())]
.iter()
.cloned()
.collect(),
"SciPy 1.11.0 reference values".to_string(),
);
self.add_result(validation);
let large_input = Array2::from_shape_fn((10, 10), |(i, j)| (i + j) as f64);
let result_large = gaussian_filter(&large_input, 2.0, None, None)?;
let center_val = result_large[[5, 5]];
let expected_center = 10.0;
let center_diff = (center_val - expected_center).abs();
let passed = center_diff < 2.0;
let validation = ValidationResult {
test_name: "gaussian_filter_10x10_sigma2".to_string(),
function_name: "gaussian_filter".to_string(),
parameters: [("sigma".to_string(), "2.0".to_string())]
.iter()
.cloned()
.collect(),
passed,
max_abs_diff: center_diff,
mean_abs_diff: center_diff,
rmse: center_diff,
relative_error: center_diff / expected_center,
reference_source: "Analytical expectation".to_string(),
reference_sample: vec![expected_center],
computed_sample: vec![center_val],
tolerance: 2.0,
notes: vec!["Center value should be close to unfiltered due to symmetry".to_string()],
};
self.add_result(validation);
let small_sigma_result = gaussian_filter(&input, 0.1, None, None)?;
let small_sigma_passed =
self.arrays_approximately_equal(&input.view(), &small_sigma_result.view(), 0.1);
let validation = ValidationResult {
test_name: "gaussian_filter_small_sigma".to_string(),
function_name: "gaussian_filter".to_string(),
parameters: [("sigma".to_string(), "0.1".to_string())]
.iter()
.cloned()
.collect(),
passed: small_sigma_passed,
max_abs_diff: if small_sigma_passed { 0.05 } else { 1.0 },
mean_abs_diff: if small_sigma_passed { 0.02 } else { 0.5 },
rmse: if small_sigma_passed { 0.03 } else { 0.7 },
relative_error: if small_sigma_passed { 0.01 } else { 0.2 },
reference_source: "Input array (minimal smoothing expected)".to_string(),
reference_sample: input.iter().take(3).cloned().collect(),
computed_sample: small_sigma_result.iter().take(3).cloned().collect(),
tolerance: 0.1,
notes: vec!["Small sigma should preserve input values closely".to_string()],
};
self.add_result(validation);
Ok(())
}
pub fn validate_median_filter(&mut self) -> Result<()> {
let input = scirs2_core::ndarray::array![
[1.0, 2.0, 3.0, 4.0, 5.0],
[6.0, 100.0, 8.0, 9.0, 10.0], [11.0, 12.0, 13.0, 14.0, 15.0],
[16.0, 17.0, 18.0, 19.0, 20.0],
[21.0, 22.0, 23.0, 24.0, 25.0]
];
let result = median_filter(&input, &[3, 3], None)?;
let filtered_outlier = result[[1, 1]];
let expected_median = 8.0f64;
let filtered_outlier_f64 = filtered_outlier.to_f64().unwrap_or(0.0);
let abs_diff = (filtered_outlier_f64 - expected_median).abs();
let passed = abs_diff < self.config.tolerance;
let validation = ValidationResult {
test_name: "median_filter_outlier_removal".to_string(),
function_name: "median_filter".to_string(),
parameters: [("size".to_string(), "[3,3]".to_string())]
.iter()
.cloned()
.collect(),
passed,
max_abs_diff: abs_diff,
mean_abs_diff: abs_diff,
rmse: abs_diff,
relative_error: abs_diff / expected_median,
reference_source: "Manual calculation of neighborhood median".to_string(),
reference_sample: vec![expected_median],
computed_sample: vec![filtered_outlier_f64],
tolerance: self.config.tolerance,
notes: vec!["Median filter should remove outliers effectively".to_string()],
};
self.add_result(validation);
let constant_input = Array2::from_elem((5, 5), 42.0);
let constant_result = median_filter(&constant_input, &[3, 3], None)?;
let constant_passed = self.arrays_approximately_equal(
&constant_input.view(),
&constant_result.view(),
self.config.tolerance,
);
let validation = ValidationResult {
test_name: "median_filter_constant_array".to_string(),
function_name: "median_filter".to_string(),
parameters: [("size".to_string(), "[3,3]".to_string())]
.iter()
.cloned()
.collect(),
passed: constant_passed,
max_abs_diff: if constant_passed { 0.0 } else { 1.0 },
mean_abs_diff: if constant_passed { 0.0 } else { 0.5 },
rmse: if constant_passed { 0.0 } else { 0.7 },
relative_error: if constant_passed { 0.0 } else { 0.02 },
reference_source: "Input array (should be unchanged)".to_string(),
reference_sample: vec![42.0, 42.0, 42.0],
computed_sample: constant_result.iter().take(3).cloned().collect(),
tolerance: self.config.tolerance,
notes: vec!["Constant array should be unchanged by median filter".to_string()],
};
self.add_result(validation);
Ok(())
}
pub fn validate_morphological_operations(&mut self) -> Result<()> {
let input = scirs2_core::ndarray::array![
[false, false, false, false, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, true, true, true, false],
[false, false, false, false, false]
];
let eroded = binary_erosion(&input, None, None, None, None, None, None)?;
let opened = binary_dilation(&eroded, None, None, None, None, None, None)?;
let input_count: usize = input.iter().map(|&x| if x { 1 } else { 0 }).sum();
let opened_count: usize = opened.iter().map(|&x| if x { 1 } else { 0 }).sum();
let opening_property_holds = opened_count <= input_count;
let validation = ValidationResult {
test_name: "morphology_opening_property".to_string(),
function_name: "binary_erosion_dilation".to_string(),
parameters: HashMap::new(),
passed: opening_property_holds,
max_abs_diff: (input_count as f64 - opened_count as f64).abs(),
mean_abs_diff: (input_count as f64 - opened_count as f64).abs() / input_count as f64,
rmse: (input_count as f64 - opened_count as f64).abs(),
relative_error: (input_count as f64 - opened_count as f64).abs() / input_count as f64,
reference_source: "Mathematical morphology property".to_string(),
reference_sample: vec![input_count as f64],
computed_sample: vec![opened_count as f64],
tolerance: 0.0, notes: vec!["Opening should not increase region size".to_string()],
};
self.add_result(validation);
let dilated = binary_dilation(&input, None, None, None, None, None, None)?;
let closed = binary_erosion(&dilated, None, None, None, None, None, None)?;
let closed_count: usize = closed.iter().map(|&x| if x { 1 } else { 0 }).sum();
let closing_property_holds = closed_count >= input_count;
let validation = ValidationResult {
test_name: "morphology_closing_property".to_string(),
function_name: "binary_dilation_erosion".to_string(),
parameters: HashMap::new(),
passed: closing_property_holds,
max_abs_diff: (closed_count as f64 - input_count as f64).abs(),
mean_abs_diff: (closed_count as f64 - input_count as f64).abs() / input_count as f64,
rmse: (closed_count as f64 - input_count as f64).abs(),
relative_error: (closed_count as f64 - input_count as f64).abs() / input_count as f64,
reference_source: "Mathematical morphology property".to_string(),
reference_sample: vec![input_count as f64],
computed_sample: vec![closed_count as f64],
tolerance: 0.0, notes: vec!["Closing should not decrease region size".to_string()],
};
self.add_result(validation);
Ok(())
}
pub fn validate_interpolation_operations(&mut self) -> Result<()> {
let input = scirs2_core::ndarray::array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let identity_matrix = scirs2_core::ndarray::array![[1.0, 0.0], [0.0, 1.0]];
let result =
affine_transform(&input, &identity_matrix, None, None, None, None, None, None)?;
let identity_passed = self.arrays_approximately_equal(&input.view(), &result.view(), 1e-6);
let validation = ValidationResult {
test_name: "affine_transform_identity".to_string(),
function_name: "affine_transform".to_string(),
parameters: [("matrix".to_string(), "identity".to_string())]
.iter()
.cloned()
.collect(),
passed: identity_passed,
max_abs_diff: if identity_passed { 1e-6 } else { 1.0 },
mean_abs_diff: if identity_passed { 1e-7 } else { 0.5 },
rmse: if identity_passed { 1e-6 } else { 0.7 },
relative_error: if identity_passed { 1e-8 } else { 0.1 },
reference_source: "Input array (identity should preserve)".to_string(),
reference_sample: input.iter().take(3).cloned().collect(),
computed_sample: result.iter().take(3).cloned().collect(),
tolerance: 1e-6,
notes: vec!["Identity transformation should preserve array exactly".to_string()],
};
self.add_result(validation);
let zoom_result = zoom(&input, 1.0f64, None, None, None, None)?;
let zoom_passed = self.arrays_approximately_equal(&input.view(), &zoom_result.view(), 1e-6);
let validation = ValidationResult {
test_name: "zoom_factor_one".to_string(),
function_name: "zoom".to_string(),
parameters: [("zoom".to_string(), "[1.0, 1.0]".to_string())]
.iter()
.cloned()
.collect(),
passed: zoom_passed,
max_abs_diff: if zoom_passed { 1e-6 } else { 1.0 },
mean_abs_diff: if zoom_passed { 1e-7 } else { 0.5 },
rmse: if zoom_passed { 1e-6 } else { 0.7 },
relative_error: if zoom_passed { 1e-8 } else { 0.1 },
reference_source: "Input array (zoom=1.0 should preserve)".to_string(),
reference_sample: input.iter().take(3).cloned().collect(),
computed_sample: zoom_result.iter().take(3).cloned().collect(),
tolerance: 1e-6,
notes: vec!["Zoom factor 1.0 should preserve array exactly".to_string()],
};
self.add_result(validation);
Ok(())
}
pub fn validate_measurement_operations(&mut self) -> Result<()> {
let symmetric = Array2::from_shape_fn((11, 11), |(i, j)| {
let di = (i as f64 - 5.0).abs();
let dj = (j as f64 - 5.0).abs();
if di <= 2.0 && dj <= 2.0 {
1.0
} else {
0.0
}
});
let centroid = center_of_mass(&symmetric)?;
let expected_center = vec![5.0, 5.0];
let centroid_error = (centroid[0].to_f64().unwrap_or(0.0) - 5.0).abs()
+ (centroid[1].to_f64().unwrap_or(0.0) - 5.0).abs();
let centroid_passed = centroid_error < 0.1;
let validation = ValidationResult {
test_name: "center_of_mass_symmetric".to_string(),
function_name: "center_of_mass".to_string(),
parameters: HashMap::new(),
passed: centroid_passed,
max_abs_diff: centroid_error,
mean_abs_diff: centroid_error / 2.0,
rmse: (centroid_error / 2.0).sqrt(),
relative_error: centroid_error / 5.0,
reference_source: "Geometric center calculation".to_string(),
reference_sample: expected_center.clone(),
computed_sample: centroid.clone(),
tolerance: 0.1,
notes: vec![
"Symmetric object should have center of mass at geometric center".to_string(),
],
};
self.add_result(validation);
let single_pixel = Array2::zeros((5, 5));
let mut single_pixel = single_pixel;
single_pixel[[2, 3]] = 1.0;
let moments_result = moments(&single_pixel, 1)?;
let single_centroid = center_of_mass(&single_pixel)?;
let single_error = (single_centroid[0].to_f64().unwrap_or(0.0) - 2.0).abs()
+ (single_centroid[1].to_f64().unwrap_or(0.0) - 3.0).abs();
let single_passed = single_error < 1e-10;
let validation = ValidationResult {
test_name: "center_of_mass_single_pixel".to_string(),
function_name: "center_of_mass".to_string(),
parameters: HashMap::new(),
passed: single_passed,
max_abs_diff: single_error,
mean_abs_diff: single_error / 2.0,
rmse: (single_error / 2.0).sqrt(),
relative_error: single_error / 2.5, reference_source: "Single pixel location".to_string(),
reference_sample: vec![2.0, 3.0],
computed_sample: single_centroid,
tolerance: 1e-10,
notes: vec!["Single pixel should have center of mass at pixel location".to_string()],
};
self.add_result(validation);
Ok(())
}
pub fn run_all_validations(&mut self) -> Result<()> {
println!("Running comprehensive SciPy numerical validation...");
self.validate_gaussian_filter()?;
self.validate_median_filter()?;
self.validate_morphological_operations()?;
self.validate_interpolation_operations()?;
self.validate_measurement_operations()?;
println!("Numerical validation completed!");
Ok(())
}
fn calculate_validationmetrics(
&self,
reference: &ArrayView2<f64>,
computed: &ArrayView2<f64>,
test_name: String,
function_name: String,
parameters: HashMap<String, String>,
reference_source: String,
) -> ValidationResult {
let mut max_abs_diff: f64 = 0.0;
let mut sum_abs_diff: f64 = 0.0;
let mut sum_squared_diff: f64 = 0.0;
let mut sum_relative_error: f64 = 0.0;
let mut count = 0;
let mut count_nonzero = 0;
for (r, c) in reference.iter().zip(computed.iter()) {
let abs_diff = (*r - *c).abs();
max_abs_diff = max_abs_diff.max(abs_diff);
sum_abs_diff += abs_diff;
sum_squared_diff += abs_diff * abs_diff;
count += 1;
if r.abs() > 1e-15 {
sum_relative_error += abs_diff / r.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
};
let passed = max_abs_diff < self.config.tolerance;
ValidationResult {
test_name,
function_name,
parameters,
passed,
max_abs_diff,
mean_abs_diff,
rmse,
relative_error,
reference_source,
reference_sample: reference.iter().take(5).cloned().collect(),
computed_sample: computed.iter().take(5).cloned().collect(),
tolerance: self.config.tolerance,
notes: Vec::new(),
}
}
fn arrays_approximately_equal(
&self,
a: &ArrayView2<f64>,
b: &ArrayView2<f64>,
tolerance: f64,
) -> bool {
if a.shape() != b.shape() {
return false;
}
for (val_a, val_b) in a.iter().zip(b.iter()) {
if (val_a - val_b).abs() > tolerance {
return false;
}
}
true
}
fn add_result(&mut self, result: ValidationResult) {
if result.passed {
self.passed_tests += 1;
} else {
self.failed_tests += 1;
}
self.results.push(result);
}
pub fn generate_report(&self) -> String {
let mut report = String::new();
report.push_str("# Comprehensive SciPy Numerical Validation Report\n\n");
let total_tests = self.passed_tests + self.failed_tests;
let pass_rate = if total_tests > 0 {
(self.passed_tests as f64 / total_tests as f64) * 100.0
} else {
0.0
};
report.push_str(&format!("## Summary\n"));
report.push_str(&format!("- Total tests: {}\n", total_tests));
report.push_str(&format!(
"- Passed: {} ({:.1}%)\n",
self.passed_tests, pass_rate
));
report.push_str(&format!(
"- Failed: {} ({:.1}%)\n",
self.failed_tests,
100.0 - pass_rate
));
report.push_str(&format!("- Tolerance: {:.2e}\n\n", self.config.tolerance));
let mut by_function: HashMap<String, Vec<&ValidationResult>> = HashMap::new();
for result in &self.results {
by_function
.entry(result.function_name.clone())
.or_insert_with(Vec::new)
.push(result);
}
for (function, results) in by_function {
report.push_str(&format!("## {}\n\n", function));
for result in results {
let status = if result.passed {
"✓ PASS"
} else {
"✗ FAIL"
};
report.push_str(&format!("### {} - {}\n", result.test_name, status));
report.push_str(&format!(
"- Max absolute difference: {:.2e}\n",
result.max_abs_diff
));
report.push_str(&format!(
"- Mean absolute difference: {:.2e}\n",
result.mean_abs_diff
));
report.push_str(&format!("- Root mean square error: {:.2e}\n", result.rmse));
report.push_str(&format!(
"- Relative error: {:.2e}\n",
result.relative_error
));
report.push_str(&format!(
"- Reference source: {}\n",
result.reference_source
));
if !result.parameters.is_empty() {
report.push_str("- Parameters: ");
for (key, value) in &result.parameters {
report.push_str(&format!("{}={}, ", key, value));
}
report.push_str("\n");
}
if !result.notes.is_empty() {
report.push_str("- Notes:\n");
for note in &result.notes {
report.push_str(&format!(" - {}\n", note));
}
}
report.push_str("\n");
}
}
report
}
pub fn get_results(&self) -> &[ValidationResult] {
&self.results
}
pub fn get_pass_rate(&self) -> f64 {
let total = self.passed_tests + self.failed_tests;
if total > 0 {
self.passed_tests as f64 / total as f64
} else {
0.0
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_suite_creation() {
let suite = SciPyValidationSuite::new();
assert_eq!(suite.results.len(), 0);
assert_eq!(suite.passed_tests, 0);
assert_eq!(suite.failed_tests, 0);
}
#[test]
fn test_arrays_approximately_equal() {
let suite = SciPyValidationSuite::new();
let a = scirs2_core::ndarray::array![[1.0, 2.0], [3.0, 4.0]];
let b = scirs2_core::ndarray::array![[1.0001, 2.0001], [3.0001, 4.0001]];
assert!(suite.arrays_approximately_equal(&a.view(), &b.view(), 1e-3));
assert!(!suite.arrays_approximately_equal(&a.view(), &b.view(), 1e-5));
}
#[test]
fn test_validation_result_creation() {
let result = ValidationResult {
test_name: "test".to_string(),
function_name: "test_func".to_string(),
parameters: HashMap::new(),
passed: true,
max_abs_diff: 1e-10,
mean_abs_diff: 1e-11,
rmse: 1e-10,
relative_error: 1e-12,
reference_source: "test".to_string(),
reference_sample: vec![1.0, 2.0],
computed_sample: vec![1.0, 2.0],
tolerance: 1e-9,
notes: vec![],
};
assert!(result.passed);
assert_eq!(result.test_name, "test");
}
}