//! Benchmark runner for PINN problems use crate::error::{PINNError, Result}; use pinn_benchmark_shared::ProblemType; /// Benchmark runner configuration #[derive(Debug, Clone)] pub struct BenchmarkConfig { /// Problem to benchmark pub problem_type: ProblemType, /// Hidden layer sizes pub hidden_layers: Vec, /// Learning rate pub learning_rate: f64, /// Number of training epochs pub num_epochs: usize, /// Number of collocation points pub num_collocation_points: usize, /// Number of boundary points pub num_boundary_points: usize, } impl BenchmarkConfig { /// Creates a new benchmark configuration /// /// # Errors /// /// Returns an error if configuration is invalid pub fn new( problem_type: ProblemType, hidden_layers: Vec, learning_rate: f64, num_epochs: usize, num_collocation_points: usize, num_boundary_points: usize, ) -> Result { if hidden_layers.is_empty() { return Err(PINNError::invalid_config( "must have at least one hidden layer", )); } if learning_rate <= 0.0 { return Err(PINNError::invalid_config("learning rate must be positive")); } if num_epochs == 0 { return Err(PINNError::invalid_config("num_epochs must be positive")); } if num_collocation_points == 0 { return Err(PINNError::invalid_config( "num_collocation_points must be positive", )); } if num_boundary_points == 0 { return Err(PINNError::invalid_config( "num_boundary_points must be positive", )); } Ok(Self { problem_type, hidden_layers, learning_rate, num_epochs, num_collocation_points, num_boundary_points, }) } } /// Computes accuracy metrics between predictions and reference pub fn compute_accuracy(predictions: &[f64], reference: &[f64]) -> Result<(f64, f64)> { if predictions.len() != reference.len() { return Err(PINNError::benchmark( "prediction and reference size mismatch", )); } if predictions.is_empty() { return Err(PINNError::benchmark("empty arrays")); } // L2 error let l2_error: f64 = predictions .iter() .zip(reference.iter()) .map(|(pred, ref_val)| (pred - ref_val).powi(2)) .sum::() .sqrt(); // L-infinity error (max absolute error) let linf_error = predictions .iter() .zip(reference.iter()) .map(|(pred, ref_val)| (pred - ref_val).abs()) .fold(0.0_f64, f64::max); Ok((l2_error, linf_error)) } #[cfg(test)] mod tests { use super::*; use approx::assert_abs_diff_eq; #[test] fn test_benchmark_config_creation_valid() { let config = BenchmarkConfig::new(ProblemType::Heat1D, vec![32, 32], 0.001, 1000, 10000, 100); assert!(config.is_ok()); let config = config.unwrap(); assert_eq!(config.problem_type, ProblemType::Heat1D); assert_eq!(config.hidden_layers, vec![32, 32]); assert_abs_diff_eq!(config.learning_rate, 0.001); } #[test] fn test_benchmark_config_empty_hidden_layers() { let config = BenchmarkConfig::new(ProblemType::Heat1D, vec![], 0.001, 1000, 10000, 100); assert!(config.is_err()); } #[test] fn test_benchmark_config_invalid_learning_rate() { let config = BenchmarkConfig::new(ProblemType::Heat1D, vec![32], 0.0, 1000, 10000, 100); assert!(config.is_err()); } #[test] fn test_benchmark_config_zero_epochs() { let config = BenchmarkConfig::new(ProblemType::Heat1D, vec![32], 0.001, 0, 10000, 100); assert!(config.is_err()); } #[test] fn test_benchmark_config_zero_collocation_points() { let config = BenchmarkConfig::new(ProblemType::Heat1D, vec![32], 0.001, 1000, 0, 100); assert!(config.is_err()); } #[test] fn test_benchmark_config_zero_boundary_points() { let config = BenchmarkConfig::new(ProblemType::Heat1D, vec![32], 0.001, 1000, 10000, 0); assert!(config.is_err()); } #[test] fn test_compute_accuracy_perfect() { let predictions = vec![1.0, 2.0, 3.0, 4.0]; let reference = vec![1.0, 2.0, 3.0, 4.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (l2_error, linf_error) = result.unwrap(); assert_abs_diff_eq!(l2_error, 0.0, epsilon = 1e-10); assert_abs_diff_eq!(linf_error, 0.0, epsilon = 1e-10); } #[test] fn test_compute_accuracy_with_errors() { let predictions = vec![1.1, 2.2, 2.9, 4.1]; let reference = vec![1.0, 2.0, 3.0, 4.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (l2_error, linf_error) = result.unwrap(); assert!(l2_error > 0.0); assert!(linf_error > 0.0); // L-inf should be max error (0.2 at index 1) assert_abs_diff_eq!(linf_error, 0.2, epsilon = 1e-10); } #[test] fn test_compute_accuracy_l2_calculation() { let predictions = vec![1.0, 2.0]; let reference = vec![2.0, 3.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (l2_error, _) = result.unwrap(); // L2 = sqrt(1^2 + 1^2) = sqrt(2) ≈ 1.414 assert_abs_diff_eq!(l2_error, 2.0_f64.sqrt(), epsilon = 1e-10); } #[test] fn test_compute_accuracy_length_mismatch() { let predictions = vec![1.0, 2.0]; let reference = vec![1.0, 2.0, 3.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_err()); } #[test] fn test_compute_accuracy_empty_arrays() { let predictions: Vec = vec![]; let reference: Vec = vec![]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_err()); } #[test] fn test_compute_accuracy_single_value() { let predictions = vec![5.0]; let reference = vec![3.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (l2_error, linf_error) = result.unwrap(); // Both should be 2.0 for single value assert_abs_diff_eq!(l2_error, 2.0, epsilon = 1e-10); assert_abs_diff_eq!(linf_error, 2.0, epsilon = 1e-10); } #[test] fn test_compute_accuracy_linf_is_max() { let predictions = vec![1.0, 2.0, 3.0, 4.0]; let reference = vec![1.1, 1.9, 3.5, 4.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (_, linf_error) = result.unwrap(); // Max error is 0.5 at index 2 assert_abs_diff_eq!(linf_error, 0.5, epsilon = 1e-10); } #[test] fn test_compute_accuracy_negative_errors() { let predictions = vec![0.0, 0.0]; let reference = vec![1.0, -1.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (l2_error, linf_error) = result.unwrap(); // L2 = sqrt(1 + 1) = sqrt(2) assert_abs_diff_eq!(l2_error, 2.0_f64.sqrt(), epsilon = 1e-10); // Linf = max(1, 1) = 1 assert_abs_diff_eq!(linf_error, 1.0, epsilon = 1e-10); } #[test] fn test_compute_accuracy_large_values() { let predictions = vec![1000.0, 2000.0]; let reference = vec![1001.0, 1999.0]; let result = compute_accuracy(&predictions, &reference); assert!(result.is_ok()); let (l2_error, linf_error) = result.unwrap(); // L2 = sqrt(1 + 1) = sqrt(2) assert_abs_diff_eq!(l2_error, 2.0_f64.sqrt(), epsilon = 1e-10); // Linf = max(1, 1) = 1 assert_abs_diff_eq!(linf_error, 1.0, epsilon = 1e-10); } #[test] fn test_benchmark_config_all_problem_types() { let problem_types = vec![ ProblemType::Heat1D, ProblemType::Burgers1D, ProblemType::Heat2D, ProblemType::Poisson2D, ProblemType::NavierStokes2D, ]; for problem_type in problem_types { let config = BenchmarkConfig::new(problem_type, vec![32], 0.001, 100, 1000, 50); assert!(config.is_ok()); } } #[test] fn test_benchmark_config_multiple_hidden_layers() { let config = BenchmarkConfig::new( ProblemType::Heat1D, vec![64, 64, 64, 64], 0.001, 1000, 10000, 100, ); assert!(config.is_ok()); let config = config.unwrap(); assert_eq!(config.hidden_layers.len(), 4); } }