diff --git a/examples/basic_usage.rs b/examples/basic_usage.rs index 5923b0f5..85145bdd 100644 --- a/examples/basic_usage.rs +++ b/examples/basic_usage.rs @@ -8,19 +8,16 @@ use anyhow::Result; use candle_core::{Device, Tensor}; -use qqn_optimizer::utils::math::SeparateFunctions; -use qqn_optimizer::{ - OptimizationProblem, Optimizer, QQNConfig, - QQNOptimizer, -}; -use std::sync::Arc; use qqn_optimizer::benchmarks::analytic_functions::RosenbrockFunction; use qqn_optimizer::line_search::{LineSearchConfig, LineSearchMethod}; +use qqn_optimizer::utils::math::SeparateFunctions; +use qqn_optimizer::{OptimizationProblem, Optimizer, QQNConfig, QQNOptimizer}; +use std::sync::Arc; fn main() -> Result<()> { // Configure the QQN optimizer let config = QQNConfig { - lbfgs_history: 10, // L-BFGS history length + lbfgs_history: 10, // L-BFGS history length min_lbfgs_iterations: 2, line_search: LineSearchConfig { method: LineSearchMethod::StrongWolfe, @@ -30,11 +27,11 @@ fn main() -> Result<()> { initial_step: 1.0, min_step: 1e-16, max_step: 1e16, - verbose: false, // Enable verbose output for line search + verbose: false, // Enable verbose output for line search line_bracket_method: 1, // 1: gradient-based bracketing, 2: function-value-based bracketing }, - epsilon: 1e-8, // Numerical stability constant - verbose: false, // Enable verbose output + epsilon: 1e-8, // Numerical stability constant + verbose: false, // Enable verbose output min_step_persist: 0.0, min_step_size: 0.0, gradient_scale_factor: 1.0, @@ -49,7 +46,10 @@ fn main() -> Result<()> { println!("Starting optimization of 2D Rosenbrock function"); println!("Initial point: {:?}", initial_point); - println!("Initial value: {:.6}", problem.evaluate_f64(&initial_point)?); + println!( + "Initial value: {:.6}", + problem.evaluate_f64(&initial_point)? + ); // Optimization loop let mut iteration = 0; @@ -63,7 +63,10 @@ fn main() -> Result<()> { // Print progress if iteration % 10 == 0 { let f_val = problem.evaluate_f64(&initial_point)?; - println!("Iteration {}: f = {:.6}, ||∇f|| = {:.6}", iteration, f_val, grad_norm); + println!( + "Iteration {}: f = {:.6}, ||∇f|| = {:.6}", + iteration, f_val, grad_norm + ); } // Check convergence @@ -78,7 +81,9 @@ fn main() -> Result<()> { let problem = problem.clone(); move |params: &[Tensor]| -> candle_core::Result { let x_vec = params[0].to_vec1::()?; - problem.evaluate_f64(&x_vec).map_err(|e| candle_core::Error::Msg(e.to_string())) + problem + .evaluate_f64(&x_vec) + .map_err(|e| candle_core::Error::Msg(e.to_string())) } }, { @@ -86,14 +91,21 @@ fn main() -> Result<()> { let device = device.clone(); move |params: &[Tensor]| -> candle_core::Result> { let x_vec = params[0].to_vec1::()?; - let grad = problem.gradient_f64(&x_vec).map_err(|e| candle_core::Error::Msg(e.to_string()))?; - Ok(vec![Tensor::from_slice(&grad, grad.len(), &device).map_err(|e| candle_core::Error::Msg(e.to_string()))?]) + let grad = problem + .gradient_f64(&x_vec) + .map_err(|e| candle_core::Error::Msg(e.to_string()))?; + Ok(vec![Tensor::from_slice(&grad, grad.len(), &device) + .map_err(|e| candle_core::Error::Msg(e.to_string()))?]) } }, )); // Convert Vec to Tensor for optimizer - let mut x_tensor = vec![Tensor::from_slice(&initial_point, initial_point.len(), &device)?]; + let mut x_tensor = vec![Tensor::from_slice( + &initial_point, + initial_point.len(), + &device, + )?]; // Perform optimization step let _step_result = optimizer.step(&mut x_tensor, function.clone())?; @@ -122,7 +134,8 @@ fn main() -> Result<()> { // Compare with known optimum let optimum = vec![1.0, 1.0]; - let distance_to_optimum = initial_point.iter() + let distance_to_optimum = initial_point + .iter() .zip(&optimum) .map(|(xi, opt)| (xi - opt).powi(2)) .sum::() @@ -137,4 +150,4 @@ fn main() -> Result<()> { } Ok(()) -} \ No newline at end of file +} diff --git a/examples/custom_problem.rs b/examples/custom_problem.rs index 49a4cf64..7f30b7f2 100644 --- a/examples/custom_problem.rs +++ b/examples/custom_problem.rs @@ -10,8 +10,7 @@ use anyhow::Result; use candle_core::{Device, Tensor}; use qqn_optimizer::utils::math::DifferentiableFunction; use qqn_optimizer::{ - LBFGSConfig, LBFGSOptimizer, OptimizationProblem, Optimizer, - QQNConfig, QQNOptimizer, + LBFGSConfig, LBFGSOptimizer, OptimizationProblem, Optimizer, QQNConfig, QQNOptimizer, }; use std::sync::Arc; @@ -20,11 +19,11 @@ use std::sync::Arc; pub struct QuadraticProblem { name: String, dimension: usize, - matrix_a: Vec>, // Positive definite matrix - vector_b: Vec, // Linear term - constant_c: f64, // Constant term - optimal_point: Vec, // Known optimal point: x* = -A^(-1) * b - optimal_value: f64, // Known optimal value + matrix_a: Vec>, // Positive definite matrix + vector_b: Vec, // Linear term + constant_c: f64, // Constant term + optimal_point: Vec, // Known optimal point: x* = -A^(-1) * b + optimal_value: f64, // Known optimal value } impl QuadraticProblem { @@ -40,15 +39,14 @@ impl QuadraticProblem { } // Create a random linear term - let vector_b: Vec = (0..dimension) - .map(|i| (i as f64 + 1.0) * 0.1) - .collect(); + let vector_b: Vec = (0..dimension).map(|i| (i as f64 + 1.0) * 0.1).collect(); let constant_c = 5.0; // Compute optimal point: x* = -A^(-1) * b // For diagonal A, this is simple: x*[i] = -b[i] / A[i][i] - let optimal_point: Vec = vector_b.iter() + let optimal_point: Vec = vector_b + .iter() .enumerate() .map(|(i, &bi)| -bi / matrix_a[i][i]) .collect(); @@ -137,23 +135,21 @@ impl OptimizationProblem for QuadraticProblem { impl DifferentiableFunction for QuadraticProblem { fn evaluate(&self, params: &[Tensor]) -> candle_core::Result { // Convert tensors to f64 vector - let x: Result, _> = params.iter() - .map(|t| t.to_scalar::()) - .collect(); + let x: Result, _> = params.iter().map(|t| t.to_scalar::()).collect(); let x = x?; // Evaluate using f64 implementation - let result = self.evaluate_f64(&x) + let result = self + .evaluate_f64(&x) .map_err(|e| candle_core::Error::Msg(format!("Evaluation error: {}", e)))?; Ok(result) } fn gradient(&self, params: &[Tensor]) -> candle_core::Result> { // Convert tensors to f64 vector - let x: Result, _> = params.iter() - .map(|t| t.to_scalar::()) - .collect(); + let x: Result, _> = params.iter().map(|t| t.to_scalar::()).collect(); let x = x?; // Compute gradient using f64 implementation - let grad = self.gradient_f64(&x) + let grad = self + .gradient_f64(&x) .map_err(|e| candle_core::Error::Msg(format!("Gradient error: {}", e)))?; // Convert back to tensors grad.iter() @@ -162,7 +158,6 @@ impl DifferentiableFunction for QuadraticProblem { } } - fn main() -> Result<()> { println!("Custom Optimization Problem Example"); println!("==================================="); @@ -191,8 +186,14 @@ fn main() -> Result<()> { )?; // Compare results println!("\n--- Comparison ---"); - println!("QQN: {} iterations, final value: {:.6}", qqn_result.0, qqn_result.1); - println!("L-BFGS: {} iterations, final value: {:.6}", lbfgs_result.0, lbfgs_result.1); + println!( + "QQN: {} iterations, final value: {:.6}", + qqn_result.0, qqn_result.1 + ); + println!( + "L-BFGS: {} iterations, final value: {:.6}", + lbfgs_result.0, lbfgs_result.1 + ); let qqn_error = (qqn_result.1 - problem.optimal_value().unwrap()).abs(); let lbfgs_error = (lbfgs_result.1 - problem.optimal_value().unwrap()).abs(); println!("QQN error: {:.2e}", qqn_error); @@ -214,7 +215,8 @@ fn run_optimizer( let initial_point = problem.initial_point(); let device = Device::Cpu; // Convert initial point to tensors - let mut params: Vec = initial_point.iter() + let mut params: Vec = initial_point + .iter() .map(|&val| Tensor::from_slice(&[val], (1,), &device)) .collect::>>() .map_err(|e| anyhow::anyhow!("Failed to create tensors: {}", e))?; @@ -223,31 +225,38 @@ fn run_optimizer( println!("Starting {} optimization...", name); while iteration < max_iterations { // Convert tensors back to f64 for convergence checking - let x: Vec = params.iter() + let x: Vec = params + .iter() .map(|t| t.to_scalar::()) .collect::>>() .map_err(|e| anyhow::anyhow!("Failed to extract values: {}", e))?; let gradient = problem.gradient_f64(&x)?; let grad_norm = gradient.iter().map(|g| g * g).sum::().sqrt(); // Perform optimization step - let _step_result = optimizer.step(&mut params, problem.clone()) + let _step_result = optimizer + .step(&mut params, problem.clone()) .map_err(|e| anyhow::anyhow!("Optimizer step failed: {}", e))?; iteration += 1; // Print progress occasionally if iteration % 50 == 0 { - let x: Vec = params.iter() + let x: Vec = params + .iter() .map(|t| t.to_scalar::()) .collect::>>() .map_err(|e| anyhow::anyhow!("Failed to extract values: {}", e))?; let f_val = problem.evaluate_f64(&x)?; - println!(" Iteration {}: f = {:.6}, ||∇f|| = {:.2e}", iteration, f_val, grad_norm); + println!( + " Iteration {}: f = {:.6}, ||∇f|| = {:.2e}", + iteration, f_val, grad_norm + ); } } // Convert final parameters back to f64 for evaluation - let final_x: Vec = params.iter() + let final_x: Vec = params + .iter() .map(|t| t.to_scalar::()) .collect::>>() .map_err(|e| anyhow::anyhow!("Failed to extract final values: {}", e))?; let final_value = problem.evaluate_f64(&final_x)?; Ok((iteration, final_value)) -} \ No newline at end of file +} diff --git a/src/analysis/plotting.rs b/src/analysis/plotting.rs index d271211f..4eb40611 100644 --- a/src/analysis/plotting.rs +++ b/src/analysis/plotting.rs @@ -29,7 +29,7 @@ fn has_font_support() -> bool { }); let ok = result.is_ok() && result.unwrap().is_ok(); - println!("[Plotting] Font support available: {}", ok); + println!("[Plotting] Font support available: {ok}"); ok } @@ -172,7 +172,7 @@ impl PlottingEngine { } else { "Unknown panic in convergence_plot".to_string() }; - eprintln!("[Plotting] Panic caught in convergence_plot: {}", msg); + eprintln!("[Plotting] Panic caught in convergence_plot: {msg}"); Err(anyhow::anyhow!("Plotting failed due to panic: {}", msg)) } } @@ -201,7 +201,7 @@ impl PlottingEngine { for trace in &sanitized_traces { optimizer_traces .entry(trace.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(trace); } // Create a sorted list of unique optimizer names for consistent color assignment @@ -232,10 +232,7 @@ impl PlottingEngine { }); // Check if we have valid bounds if !min_obj.is_finite() || !max_obj.is_finite() || min_obj >= max_obj { - eprintln!( - "[Plotting] Warning: Invalid objective value bounds: [{}, {}]", - min_obj, max_obj - ); + eprintln!("[Plotting] Warning: Invalid objective value bounds: [{min_obj}, {max_obj}]"); return Ok(()); } @@ -334,7 +331,7 @@ impl PlottingEngine { } root.present()?; - println!("Convergence plot saved to: {}", output_path); + println!("Convergence plot saved to: {output_path}"); Ok(()) } @@ -358,7 +355,7 @@ impl PlottingEngine { } else { "Unknown panic in log_convergence_plot".to_string() }; - eprintln!("[Plotting] Panic caught in log_convergence_plot: {}", msg); + eprintln!("[Plotting] Panic caught in log_convergence_plot: {msg}"); Err(anyhow::anyhow!("Plotting failed due to panic: {}", msg)) } } @@ -387,7 +384,7 @@ impl PlottingEngine { for trace in &sanitized_traces { optimizer_traces .entry(trace.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(trace); } // Create a sorted list of unique optimizer names for consistent color assignment @@ -499,7 +496,7 @@ impl PlottingEngine { } root.present()?; - println!("Log convergence plot saved to: {}", output_path); + println!("Log convergence plot saved to: {output_path}"); Ok(()) } @@ -519,7 +516,7 @@ impl PlottingEngine { } else { "Unknown panic in performance_comparison".to_string() }; - eprintln!("[Plotting] Panic caught in performance_comparison: {}", msg); + eprintln!("[Plotting] Panic caught in performance_comparison: {msg}"); Err(anyhow::anyhow!("Plotting failed due to panic: {}", msg)) } } @@ -545,9 +542,9 @@ impl PlottingEngine { for result in &results.results { problem_results .entry(result.problem_name.clone()) - .or_insert_with(HashMap::new) + .or_default() .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(if result.final_value.is_finite() { result.final_value } else { @@ -680,7 +677,7 @@ impl PlottingEngine { } root.present()?; - println!("Performance comparison saved to: {}", output_path); + println!("Performance comparison saved to: {output_path}"); Ok(()) } @@ -700,7 +697,7 @@ impl PlottingEngine { } else { "Unknown panic in performance_boxplot".to_string() }; - eprintln!("[Plotting] Panic caught in performance_boxplot: {}", msg); + eprintln!("[Plotting] Panic caught in performance_boxplot: {msg}"); Err(anyhow::anyhow!("Plotting failed due to panic: {}", msg)) } } @@ -722,7 +719,7 @@ impl PlottingEngine { for result in &results.results { optimizer_results .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(if result.final_value.is_finite() { result.final_value } else { @@ -741,10 +738,7 @@ impl PlottingEngine { // Filter out non-finite values values.retain(|&v| v.is_finite()); if values.is_empty() { - eprintln!( - "[Plotting] Warning: No finite values for optimizer {}", - optimizer - ); + eprintln!("[Plotting] Warning: No finite values for optimizer {optimizer}"); continue; } @@ -819,8 +813,8 @@ impl PlottingEngine { // Draw box (Q1 to Q3) chart.draw_series(std::iter::once(Rectangle::new( [ - (x as f64 - box_width / 2.0, data.q1), - (x as f64 + box_width / 2.0, data.q3), + (x - box_width / 2.0, data.q1), + (x + box_width / 2.0, data.q3), ], BLUE.mix(0.3).filled(), )))?; @@ -828,21 +822,21 @@ impl PlottingEngine { // Draw median line chart.draw_series(std::iter::once(PathElement::new( vec![ - (x as f64 - box_width / 2.0, data.median), - (x as f64 + box_width / 2.0, data.median), + (x - box_width / 2.0, data.median), + (x + box_width / 2.0, data.median), ], RED.stroke_width(2), )))?; // Draw whiskers chart.draw_series(std::iter::once(PathElement::new( - vec![(x as f64, data.min), (x as f64, data.q1)], - &BLACK, + vec![(x, data.min), (x, data.q1)], + BLACK, )))?; chart.draw_series(std::iter::once(PathElement::new( - vec![(x as f64, data.q3), (x as f64, data.max)], - &BLACK, + vec![(x, data.q3), (x, data.max)], + BLACK, )))?; // Draw whisker caps let cap_width = box_width / 3.0; @@ -851,14 +845,14 @@ impl PlottingEngine { (x - cap_width / 2.0, data.min), (x + cap_width / 2.0, data.min), ], - &BLACK, + BLACK, )))?; chart.draw_series(std::iter::once(PathElement::new( vec![ (x - cap_width / 2.0, data.max), (x + cap_width / 2.0, data.max), ], - &BLACK, + BLACK, )))?; // Add optimizer names as x-axis labels if fonts are available @@ -878,7 +872,7 @@ impl PlottingEngine { } root.present()?; - println!("Performance boxplot saved to: {}", output_path); + println!("Performance boxplot saved to: {output_path}"); Ok(()) } @@ -907,7 +901,7 @@ impl PlottingEngine { } fs::write(&csv_path, csv_content) .map_err(|e| anyhow::anyhow!("Failed to write CSV file {}: {}", csv_path, e))?; - println!("Convergence data exported to: {}", csv_path); + println!("Convergence data exported to: {csv_path}"); Ok(()) } /// Export log convergence plot data to CSV @@ -938,7 +932,7 @@ impl PlottingEngine { } fs::write(&csv_path, csv_content) .map_err(|e| anyhow::anyhow!("Failed to write CSV file {}: {}", csv_path, e))?; - println!("Log convergence data exported to: {}", csv_path); + println!("Log convergence data exported to: {csv_path}"); Ok(()) } /// Export performance comparison data to CSV @@ -954,9 +948,9 @@ impl PlottingEngine { for result in &results.results { problem_results .entry(result.problem_name.clone()) - .or_insert_with(HashMap::new) + .or_default() .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result.final_value); } for (problem, optimizers) in problem_results { @@ -1004,22 +998,14 @@ impl PlottingEngine { .count(); let success_rate = success_count as f64 / optimizer_results.len() as f64; csv_content.push_str(&format!( - "{},{},{:.6e},{:.6e},{:.1},{:.1},{:.1},{:.3}\n", - problem, - optimizer, - mean_final, - std_final, - mean_iterations, - mean_function_evals, - mean_gradient_evals, - success_rate + "{problem},{optimizer},{mean_final:.6e},{std_final:.6e},{mean_iterations:.1},{mean_function_evals:.1},{mean_gradient_evals:.1},{success_rate:.3}\n" )); } } } fs::write(&csv_path, csv_content) .map_err(|e| anyhow::anyhow!("Failed to write CSV file {}: {}", csv_path, e))?; - println!("Performance comparison data exported to: {}", csv_path); + println!("Performance comparison data exported to: {csv_path}"); Ok(()) } /// Export performance boxplot data to CSV @@ -1035,7 +1021,7 @@ impl PlottingEngine { for result in &results.results { optimizer_results .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result.final_value); } for (optimizer, mut values) in optimizer_results { @@ -1050,18 +1036,17 @@ impl PlottingEngine { // Convert all values to a comma-separated string let all_values_str = values .iter() - .map(|v| format!("{:.6e}", v)) + .map(|v| format!("{v:.6e}")) .collect::>() .join(";"); // Use semicolon to avoid CSV parsing issues csv_content.push_str(&format!( - "{},{:.6e},{:.6e},{:.6e},{:.6e},{:.6e},\"{}\"\n", - optimizer, min, q1, median, q3, max, all_values_str + "{optimizer},{min:.6e},{q1:.6e},{median:.6e},{q3:.6e},{max:.6e},\"{all_values_str}\"\n" )); } } fs::write(&csv_path, csv_content) .map_err(|e| anyhow::anyhow!("Failed to write CSV file {}: {}", csv_path, e))?; - println!("Performance boxplot data exported to: {}", csv_path); + println!("Performance boxplot data exported to: {csv_path}"); Ok(()) } /// Helper function to add legend for unique optimizers diff --git a/src/analysis/reporting.rs b/src/analysis/reporting.rs index ff6f0315..8bec20de 100644 --- a/src/analysis/reporting.rs +++ b/src/analysis/reporting.rs @@ -34,6 +34,12 @@ pub struct ReportSection { /// LaTeX exporter pub struct LaTeXExporter; +impl Default for LaTeXExporter { + fn default() -> Self { + Self::new() + } +} + impl LaTeXExporter { pub fn new() -> Self { Self @@ -50,6 +56,12 @@ impl LaTeXExporter { /// CSV exporter pub struct CSVExporter; +impl Default for CSVExporter { + fn default() -> Self { + Self::new() + } +} + impl CSVExporter { pub fn new() -> Self { Self diff --git a/src/analysis/statistics.rs b/src/analysis/statistics.rs index 0846dea5..b6d5fdbb 100644 --- a/src/analysis/statistics.rs +++ b/src/analysis/statistics.rs @@ -200,7 +200,7 @@ impl StatisticalAnalysis { for result in &results.results { grouped_results .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } @@ -296,14 +296,14 @@ impl StatisticalAnalysis { for result in results_a { problems_a .entry(result.problem_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } for result in results_b { problems_b .entry(result.problem_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } @@ -413,7 +413,7 @@ impl StatisticalAnalysis { for result in &results.results { grouped_results .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } @@ -528,7 +528,7 @@ impl StatisticalAnalysis { for result in &results.results { grouped_results .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } @@ -770,7 +770,7 @@ impl StatisticalAnalysis { } else { t_dist.cdf(abs_t) }; - if cdf_result.is_finite() && cdf_result >= 0.0 && cdf_result <= 1.0 { + if cdf_result.is_finite() && (0.0..=1.0).contains(&cdf_result) { 2.0 * (1.0 - cdf_result) } else { 0.5 // Fallback for invalid CDF result diff --git a/src/benchmarks/analytic_functions.rs b/src/benchmarks/analytic_functions.rs index 82a26b81..7655a244 100644 --- a/src/benchmarks/analytic_functions.rs +++ b/src/benchmarks/analytic_functions.rs @@ -195,7 +195,7 @@ impl StyblinskiTangFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("StyblinskiTang_{}D", dimension), + name: format!("StyblinskiTang_{dimension}D"), } } } @@ -260,7 +260,7 @@ impl MichalewiczFunction { Self { dimension, m, - name: format!("Michalewicz_{}D_m{}", dimension, m), + name: format!("Michalewicz_{dimension}D_m{m}"), } } } @@ -358,7 +358,7 @@ impl RosenbrockFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Rosenbrock_{}D", dimension), + name: format!("Rosenbrock_{dimension}D"), } } } @@ -431,7 +431,7 @@ impl RastriginFunction { Self { dimension, a: 10.0, - name: format!("Rastrigin_{}D", dimension), + name: format!("Rastrigin_{dimension}D"), } } } @@ -495,7 +495,7 @@ impl SphereFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Sphere_{}D", dimension), + name: format!("Sphere_{dimension}D"), } } } @@ -719,7 +719,7 @@ impl AckleyFunction { a, b, c, - name: format!("Ackley_{}D_a{}_b{}_c{:0.2e}", dimension, a, b, c), + name: format!("Ackley_{dimension}D_a{a}_b{b}_c{c:0.2e}"), } } } @@ -808,7 +808,7 @@ impl GriewankFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Griewank_{}D", dimension), + name: format!("Griewank_{dimension}D"), } } } @@ -891,7 +891,7 @@ impl SchwefelFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Schwefel_{}D", dimension), + name: format!("Schwefel_{dimension}D"), } } } @@ -965,7 +965,7 @@ impl LevyFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Levy_{}D", dimension), + name: format!("Levy_{dimension}D"), } } } @@ -1075,7 +1075,7 @@ impl ZakharovFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Zakharov_{}D", dimension), + name: format!("Zakharov_{dimension}D"), } } } @@ -1152,7 +1152,7 @@ impl IllConditionedRosenbrock { Self { dimension, alpha, - name: format!("IllConditionedRosenbrock_{}D_alpha{}", dimension, alpha), + name: format!("IllConditionedRosenbrock_{dimension}D_alpha{alpha}"), } } } @@ -1211,7 +1211,7 @@ impl TrigonometricFunction { pub fn new(dimension: usize) -> Self { Self { dimension, - name: format!("Trigonometric_{}D", dimension), + name: format!("Trigonometric_{dimension}D"), } } } @@ -1281,7 +1281,7 @@ impl PenaltyFunctionI { Self { dimension, alpha, - name: format!("PenaltyI_{}D_alpha{:.0e}", dimension, alpha), + name: format!("PenaltyI_{dimension}D_alpha{alpha:.0e}"), } } } @@ -1347,7 +1347,7 @@ impl BarrierFunction { Self { dimension, mu, - name: format!("Barrier_{}D_mu{}", dimension, mu), + name: format!("Barrier_{dimension}D_mu{mu}"), } } } @@ -1409,7 +1409,7 @@ impl NoisySphere { dimension, noise_level, seed, - name: format!("NoisySphere_{}D_sigma{}", dimension, noise_level), + name: format!("NoisySphere_{dimension}D_sigma{noise_level}"), } } } @@ -1484,7 +1484,7 @@ impl SparseRosenbrock { } Self { dimension, - name: format!("SparseRosenbrock_{}D", dimension), + name: format!("SparseRosenbrock_{dimension}D"), } } } @@ -1550,10 +1550,7 @@ impl SparseQuadratic { Self { dimension, sparsity_pattern: sparsity_pattern.clone(), - name: format!( - "SparseQuadratic_{}D_pattern{:?}", - dimension, sparsity_pattern - ), + name: format!("SparseQuadratic_{dimension}D_pattern{sparsity_pattern:?}"), } } } @@ -2183,9 +2180,13 @@ mod tests { // Test sparsity structure let point = vec![0.0, 0.0, 1.0, 1.0]; let grad = problem.gradient_f64(&point).unwrap(); - // Variables 0,1 should not affect gradients of 2,3 - assert_ne!(grad[0], 0.0); - assert_ne!(grad[1], 0.0); + // For SparseRosenbrock with pairs (0,1) and (2,3): + // grad[0] = -400*x[0]*(x[1]-x[0]²) - 2*(1-x[0]) = -400*0*(0-0) - 2*(1-0) = -2.0 + // grad[1] = 200*(x[1]-x[0]²) = 200*(0-0) = 0.0 + // grad[2] = -400*x[2]*(x[3]-x[2]²) - 2*(1-x[2]) = -400*1*(1-1) - 2*(1-1) = 0.0 + // grad[3] = 200*(x[3]-x[2]²) = 200*(1-1) = 0.0 + assert_eq!(grad[0], -2.0); + assert_eq!(grad[1], 0.0); assert_eq!(grad[2], 0.0); assert_eq!(grad[3], 0.0); } @@ -2206,7 +2207,8 @@ mod tests { let custom = SparseQuadratic::with_pattern(4, vec![2]); let grad = custom.gradient_f64(&vec![1.0, 0.0, 0.0, 0.0]).unwrap(); // Only x[0] and x[2] should interact - assert_ne!(grad[0], 2.0); // Not just diagonal + // grad[0] = 2.0 * x[0] + 0.1 * x[2] = 2.0 * 1.0 + 0.1 * 0.0 = 2.0 + assert_eq!(grad[0], 2.0); // This is the expected diagonal contribution assert_eq!(grad[1], 0.0); // No interaction } #[test] diff --git a/src/benchmarks/evaluation.rs b/src/benchmarks/evaluation.rs index ee3966e1..e8379426 100644 --- a/src/benchmarks/evaluation.rs +++ b/src/benchmarks/evaluation.rs @@ -103,6 +103,12 @@ impl IterationData { } } +impl Default for OptimizationTrace { + fn default() -> Self { + Self::new() + } +} + impl OptimizationTrace { pub fn new() -> Self { Self { @@ -355,9 +361,9 @@ impl BenchmarkRunner { let mut x = problem.problem.initial_point(); // Validate initial point if x.iter().any(|&xi| !xi.is_finite()) { - return Err(BenchmarkError::ProblemError(format!( - "Initial point contains non-finite values" - ))); + return Err(BenchmarkError::ProblemError( + "Initial point contains non-finite values".to_string(), + )); } // Randomize initial point to ensure variability let mut rng = rand::rng(); @@ -424,8 +430,7 @@ impl BenchmarkRunner { .map_err(|e| BenchmarkError::ProblemError(e.to_string()))?; if !final_value.is_finite() { return Err(BenchmarkError::ProblemError(format!( - "Final function value is not finite: {}", - final_value + "Final function value is not finite: {final_value}" ))); } let final_gradient = problem @@ -464,10 +469,7 @@ impl BenchmarkRunner { }, }; if iteration == 0 { - warn!( - "No iterations performed, convergence reason: {:?}", - convergence_reason - ); + warn!("No iterations performed, convergence reason: {convergence_reason:?}"); Err(BenchmarkError::ProblemError( "No iterations performed, likely due to initial evaluation failure".to_string(), )) @@ -517,8 +519,7 @@ impl BenchmarkRunner { Ok(val) => val, Err(e) => { return Err(BenchmarkError::ProblemError(format!( - "Initial function evaluation failed: {}", - e + "Initial function evaluation failed: {e}" ))); } }; @@ -527,8 +528,7 @@ impl BenchmarkRunner { Ok(grad) => grad, Err(e) => { return Err(BenchmarkError::ProblemError(format!( - "Initial gradient evaluation failed: {}", - e + "Initial gradient evaluation failed: {e}" ))); } }; @@ -563,10 +563,7 @@ impl BenchmarkRunner { let f_val = match problem.problem.evaluate_f64(input_floats) { Ok(val) => val, Err(e) => { - warn!( - "Function evaluation failed at iteration {}: {}", - iteration, e - ); + warn!("Function evaluation failed at iteration {iteration}: {e}"); numerical_error_count += 1; if numerical_error_count >= MAX_NUMERICAL_ERRORS { return Ok(ConvergenceReason::NumericalError); @@ -577,10 +574,7 @@ impl BenchmarkRunner { *function_evaluations += 1; if !f_val.is_finite() { - warn!( - "Non-finite function value at iteration {}: {}", - iteration, f_val - ); + warn!("Non-finite function value at iteration {iteration}: {f_val}"); numerical_error_count += 1; if numerical_error_count >= MAX_NUMERICAL_ERRORS { return Ok(ConvergenceReason::NumericalError); @@ -602,16 +596,14 @@ impl BenchmarkRunner { if (improvement_percent / stagnation_multiplier) >= self.config.min_improvement_percent { debug!( - "Iteration {}: Improvement {:.3e}%, best value updated to {:.6e}", - iteration, improvement_percent, f_val + "Iteration {iteration}: Improvement {improvement_percent:.3e}%, best value updated to {f_val:.6e}" ); best_f_val = f_val; no_improvement_count = 0; } else { no_improvement_count += 1; debug!( - "Iteration {}: Improvement {:.3e}%, no improvement count: {}", - iteration, improvement_percent, no_improvement_count + "Iteration {iteration}: Improvement {improvement_percent:.3e}%, no improvement count: {no_improvement_count}" ); if no_improvement_count >= (MAX_NO_IMPROVEMENT + stagnation_tolerance) { info!( @@ -625,10 +617,7 @@ impl BenchmarkRunner { let gradient = match problem.problem.gradient_f64(input_floats) { Ok(grad) => grad, Err(e) => { - warn!( - "Gradient evaluation failed at iteration {}: {}", - iteration, e - ); + warn!("Gradient evaluation failed at iteration {iteration}: {e}"); numerical_error_count += 1; if numerical_error_count >= MAX_NUMERICAL_ERRORS { return Ok(ConvergenceReason::NumericalError); @@ -640,7 +629,7 @@ impl BenchmarkRunner { // Check for non-finite gradients if gradient.iter().any(|&g| !g.is_finite()) { - warn!("Non-finite gradient at iteration {}", iteration); + warn!("Non-finite gradient at iteration {iteration}"); numerical_error_count += 1; if numerical_error_count >= MAX_NUMERICAL_ERRORS { return Ok(ConvergenceReason::NumericalError); @@ -652,16 +641,13 @@ impl BenchmarkRunner { // Check convergence let gradient_norm = gradient.iter().map(|g| g * g).sum::().sqrt(); - debug!( - "Iteration {}: f_val={:.6e}, grad_norm={:.6e}", - iteration, f_val, gradient_norm - ); + debug!("Iteration {iteration}: f_val={f_val:.6e}, grad_norm={gradient_norm:.6e}"); // Use the more lenient of the two tolerances to ensure convergence is achievable // Check function value convergence if optimal value is known if !is_no_threshold_mode() { if let Some(optimal_value) = problem.problem.optimal_value() { if f_val < optimal_value { - info!("Converged by function tolerance at iteration {}", iteration); + info!("Converged by function tolerance at iteration {iteration}"); // Record final iteration data before returning trace.check_convergence_with_optimizer( *iteration, @@ -748,7 +734,7 @@ impl BenchmarkRunner { } for (i, &value) in values.iter().enumerate() { if !value.is_finite() { - warn!("Non-finite parameter detected at iteration {}", iteration); + warn!("Non-finite parameter detected at iteration {iteration}"); numerical_error_count += 1; if numerical_error_count >= MAX_NUMERICAL_ERRORS { return Ok(ConvergenceReason::NumericalError); @@ -778,7 +764,7 @@ impl BenchmarkRunner { // Check for numerical errors if input_floats.iter().any(|&xi| !xi.is_finite()) { - warn!("Non-finite parameter detected at iteration {}", iteration); + warn!("Non-finite parameter detected at iteration {iteration}"); return Ok(ConvergenceReason::NumericalError); } } @@ -892,7 +878,7 @@ impl BenchmarkResults { if !results.is_empty() { let avg = - results.iter().map(|r| r.best_value).sum::() / results.len() as f64; + results.iter().map(|r| r.final_value).sum::() / results.len() as f64; averages.insert((problem_name.clone(), optimizer_name.clone()), avg); } } diff --git a/src/benchmarks/ml_problems.rs b/src/benchmarks/ml_problems.rs index a68a5ca8..9a9b98e2 100644 --- a/src/benchmarks/ml_problems.rs +++ b/src/benchmarks/ml_problems.rs @@ -23,8 +23,7 @@ impl LogisticRegression { let n_samples = x_data.len(); let n_features = x_data.first().map(|x| x.len()).unwrap_or(0); let name = format!( - "LogisticRegression_{}samples_{}features_reg{}", - n_samples, n_features, regularization + "LogisticRegression_{n_samples}samples_{n_features}features_reg{regularization}" ); // Convert to tensors @@ -182,7 +181,7 @@ impl NeuralNetworkTraining { .map(|&s| s.to_string()) .collect::>() .join("_"); - let name = format!("NeuralNetwork_{}samples_layers_{}", n_samples, layer_str); + let name = format!("NeuralNetwork_{n_samples}samples_layers_{layer_str}"); // Convert to tensors let input_dim = x_data.first().map(|x| x.len()).unwrap_or(0); @@ -406,10 +405,8 @@ impl LinearRegression { let device = Device::Cpu; let n_samples = x_data.len(); let n_features = x_data.first().map(|x| x.len()).unwrap_or(0); - let name = format!( - "LinearRegression_{}samples_{}features_reg{}", - n_samples, n_features, regularization - ); + let name = + format!("LinearRegression_{n_samples}samples_{n_features}features_reg{regularization}"); // Convert to tensors let x_flat: Vec = x_data.into_iter().flatten().collect(); @@ -520,7 +517,7 @@ impl SupportVectorMachine { let device = Device::Cpu; let n_samples = x_data.len(); let n_features = x_data.first().map(|x| x.len()).unwrap_or(0); - let name = format!("SVM_{}samples_{}features_C{}", n_samples, n_features, c); + let name = format!("SVM_{n_samples}samples_{n_features}features_C{c}"); // Convert to tensors let x_flat: Vec = x_data.into_iter().flatten().collect(); diff --git a/src/benchmarks/mnist.rs b/src/benchmarks/mnist.rs index be2155af..81966ce1 100644 --- a/src/benchmarks/mnist.rs +++ b/src/benchmarks/mnist.rs @@ -40,7 +40,7 @@ impl MLP { // Create hidden layers for (i, &hidden_dim) in hidden_dims.iter().enumerate() { - layers.push(linear(prev_dim, hidden_dim, vs.pp(format!("ln{}", i)))?); + layers.push(linear(prev_dim, hidden_dim, vs.pp(format!("ln{i}")))?); prev_dim = hidden_dim; } @@ -137,10 +137,7 @@ impl MnistNeuralNetwork { .map(|s| s.to_string()) .collect::>() .join("x"); - let name = format!( - "MNIST_NN_{}samples_hidden{}_{}", - n_samples, hidden_str, activation_name - ); + let name = format!("MNIST_NN_{n_samples}samples_hidden{hidden_str}_{activation_name}"); let input_dim = x_data.first().map(|x| x.len()).unwrap_or(784); let output_dim = y_data.first().map(|y| y.len()).unwrap_or(10); @@ -265,7 +262,7 @@ impl MnistNeuralNetwork { // Download files if they don't exist for (url, path) in &urls { if !Path::new(path).exists() { - println!("Downloading {}...", url); + println!("Downloading {url}..."); Self::download_file(url, path)?; } } @@ -286,7 +283,7 @@ impl MnistNeuralNetwork { fn download_file(url: &str, path: &str) -> anyhow::Result<()> { // Try curl first if let Ok(output) = std::process::Command::new("curl") - .args(&["-L", "-f", "-s", "-o", path, url]) + .args(["-L", "-f", "-s", "-o", path, url]) .output() { if output.status.success() { @@ -296,7 +293,7 @@ impl MnistNeuralNetwork { // Fallback to wget if let Ok(output) = std::process::Command::new("wget") - .args(&["-q", "-O", path, url]) + .args(["-q", "-O", path, url]) .output() { if output.status.success() { @@ -336,7 +333,7 @@ impl MnistNeuralNetwork { for (gz_path, out_path) in &files { if Path::new(gz_path).exists() && !Path::new(out_path).exists() { - println!("Decompressing {}...", gz_path); + println!("Decompressing {gz_path}..."); let gz_file = File::open(gz_path)?; let mut decoder = GzDecoder::new(BufReader::new(gz_file)); let mut out_file = File::create(out_path)?; @@ -582,16 +579,13 @@ impl MnistNeuralNetwork { .filter(|&&x| x.abs() > 3.0 * std_dev + mean.abs()) .count(); let extreme_percentage = (extreme_count as f64 / values.len() as f64) * 100.0; - println!("\nParameter: {}", name); + println!("\nParameter: {name}"); println!(" Shape: {:?}", tensor.shape()); - println!(" Mean: {:.6}", mean); - println!(" Std Dev: {:.6}", std_dev); - println!(" Min/Max: {:.6} / {:.6}", min, max); - println!(" Zero values: {} ({:.2}%)", zero_count, zero_percentage); - println!( - " Extreme values (>3σ): {} ({:.2}%)", - extreme_count, extreme_percentage - ); + println!(" Mean: {mean:.6}"); + println!(" Std Dev: {std_dev:.6}"); + println!(" Min/Max: {min:.6} / {max:.6}"); + println!(" Zero values: {zero_count} ({zero_percentage:.2}%)"); + println!(" Extreme values (>3σ): {extreme_count} ({extreme_percentage:.2}%)"); // Determine if this is a weight or bias based on shape let dims = tensor.shape().dims(); if dims.len() == 2 { @@ -609,10 +603,10 @@ impl MnistNeuralNetwork { ActivationType::Logistic => "Xavier/Glorot", ActivationType::Sinewave => "Small Xavier", }; - println!(" Expected std ({}): {:.6}", init_name, expected_std); - println!(" Actual/Expected ratio: {:.3}", std_ratio); - if std_ratio < 0.8 || std_ratio > 1.2 { - println!(" ⚠️ Warning: Standard deviation deviates significantly from {} initialization", init_name); + println!(" Expected std ({init_name}): {expected_std:.6}"); + println!(" Actual/Expected ratio: {std_ratio:.3}"); + if !(0.8..=1.2).contains(&std_ratio) { + println!(" ⚠️ Warning: Standard deviation deviates significantly from {init_name} initialization"); } else { println!(" ✓ Standard deviation is within expected range"); } @@ -662,7 +656,7 @@ impl OptimizationProblem for MnistNeuralNetwork { self.set_parameters(params)?; let n_samples = self.x_data.len(); - let n_batches = (n_samples + self.batch_size - 1) / self.batch_size; + let n_batches = n_samples.div_ceil(self.batch_size); let mut total_loss = 0.0; // Process batches in parallel using rayon @@ -744,7 +738,7 @@ impl OptimizationProblem for MnistNeuralNetwork { // Set parameters self.set_parameters(params)?; let n_samples = self.x_data.len(); - let n_batches = (n_samples + self.batch_size - 1) / self.batch_size; + let n_batches = n_samples.div_ceil(self.batch_size); // Accumulate gradients across batches let mut accumulated_grads = vec![0.0; self.param_count]; diff --git a/src/experiment_runner/experiment_runner.rs b/src/experiment_runner/experiment_runner.rs index 6d41550a..df319530 100644 --- a/src/experiment_runner/experiment_runner.rs +++ b/src/experiment_runner/experiment_runner.rs @@ -37,12 +37,9 @@ impl ExperimentRunner { // Ensure output directory exists fs::create_dir_all(&self.output_dir)?; // Run benchmarks for each problem with its specific optimizers - for (problem_name, _optimizers) in &problem_optimizer_map { + for problem_name in problem_optimizer_map.keys() { // Find the problem by name (we'll need to pass problems separately or store them) - info!( - "Running championship benchmarks for problem: {}", - problem_name - ); + info!("Running championship benchmarks for problem: {problem_name}"); // This will be handled by the calling function } Ok(()) @@ -159,7 +156,7 @@ impl ExperimentRunner { problem, &mut optimizer.clone_box(), run_id, - &opt_name, + opt_name, self.config.initial_point_noise, ) .await @@ -176,7 +173,7 @@ impl ExperimentRunner { let mut failed_result = SingleResult::new(opt_name.clone(), run_id); failed_result.convergence_achieved = false; failed_result.final_value = f64::INFINITY; - failed_result.error_message = Some(format!("Evaluation error: {}", e)); + failed_result.error_message = Some(format!("Evaluation error: {e}")); results.add_result(failed_result); continue; } @@ -224,7 +221,7 @@ pub async fn run_benchmark( enable_no_threshold_mode(); let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); - let output_dir_name = format!("{}{}", report_path_prefix, timestamp); + let output_dir_name = format!("{report_path_prefix}{timestamp}"); let output_dir = std::path::PathBuf::from(&output_dir_name); fs::create_dir_all(&output_dir)?; println!("Creating benchmark results in: {}", output_dir.display()); @@ -248,7 +245,7 @@ pub async fn run_benchmark( println!("Benchmark completed successfully"); } Ok(Err(e)) => { - eprintln!("Benchmark failed: {}", e); + eprintln!("Benchmark failed: {e}"); return Err(e.into()); } Err(_) => { @@ -287,10 +284,7 @@ pub fn get_optimizer_family(optimizer_name: &str) -> String { } else if optimizer_name.starts_with("Adam") { "Adam".to_string() } else { - warn!( - "Unknown optimizer family for '{}', using full name", - optimizer_name - ); + warn!("Unknown optimizer family for '{optimizer_name}', using full name"); optimizer_name.to_string() } } diff --git a/src/experiment_runner/mod.rs b/src/experiment_runner/mod.rs index b191b4aa..86d95fe5 100644 --- a/src/experiment_runner/mod.rs +++ b/src/experiment_runner/mod.rs @@ -3,8 +3,8 @@ pub mod optimizer_sets; pub mod plotting_manager; pub mod problem_sets; pub mod report_generator; -pub mod statistical_analysis; mod reports; +pub mod statistical_analysis; pub use experiment_runner::ExperimentRunner; pub use plotting_manager::PlottingManager; diff --git a/src/experiment_runner/plotting_manager.rs b/src/experiment_runner/plotting_manager.rs index 9d4a402d..29843a22 100644 --- a/src/experiment_runner/plotting_manager.rs +++ b/src/experiment_runner/plotting_manager.rs @@ -71,7 +71,7 @@ impl PlottingManager { let filename = format!("convergence/{}", problem_name.replace(" ", "_")); self.generate_plot_with_fallback( || self.plotting_engine.convergence_plot(&traces, &filename), - &format!("convergence plot for {}", problem_name), + &format!("convergence plot for {problem_name}"), ) .await; @@ -79,9 +79,9 @@ impl PlottingManager { self.generate_plot_with_fallback( || { self.plotting_engine - .log_convergence_plot(&traces, &format!("{}_log", filename)) + .log_convergence_plot(&traces, &format!("{filename}_log")) }, - &format!("log convergence plot for {}", problem_name), + &format!("log convergence plot for {problem_name}"), ) .await; } @@ -127,15 +127,12 @@ impl PlottingManager { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(plot_fn)); match result { - Ok(Ok(_)) => info!("Generated {}", plot_description), + Ok(Ok(_)) => info!("Generated {plot_description}"), Ok(Err(e)) => { - warn!("Failed to generate {}: {}", plot_description, e); + warn!("Failed to generate {plot_description}: {e}"); } Err(_) => { - warn!( - "Skipping {} due to panic in plotting library", - plot_description - ); + warn!("Skipping {plot_description} due to panic in plotting library"); } } } diff --git a/src/experiment_runner/problem_sets.rs b/src/experiment_runner/problem_sets.rs index 4d62c977..b8ef0399 100644 --- a/src/experiment_runner/problem_sets.rs +++ b/src/experiment_runner/problem_sets.rs @@ -34,145 +34,109 @@ pub fn analytic_problems() -> Vec { 42, ), ProblemSpec::new( - Arc::new({ - RosenbrockFunction::new(2) - }), + Arc::new(RosenbrockFunction::new(2)), "Rosenbrock".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - RosenbrockFunction::new(5) - }), + Arc::new(RosenbrockFunction::new(5)), "Rosenbrock".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - RosenbrockFunction::new(10) - }), + Arc::new(RosenbrockFunction::new(10)), "Rosenbrock".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - MichalewiczFunction::new(2) - }), + Arc::new(MichalewiczFunction::new(2)), "Michalewicz".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - MichalewiczFunction::new(5) - }), + Arc::new(MichalewiczFunction::new(5)), "Michalewicz".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - MichalewiczFunction::new(10) - }), + Arc::new(MichalewiczFunction::new(10)), "Michalewicz".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - RastriginFunction::new(2) - }), + Arc::new(RastriginFunction::new(2)), "Rastrigin".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - RastriginFunction::new(5) - }), + Arc::new(RastriginFunction::new(5)), "Rastrigin".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - RastriginFunction::new(10) - }), + Arc::new(RastriginFunction::new(10)), "Rastrigin".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - AckleyFunction::new(2) - }), + Arc::new(AckleyFunction::new(2)), "Ackley".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - AckleyFunction::new(5) - }), + Arc::new(AckleyFunction::new(5)), "Ackley".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - AckleyFunction::new(10) - }), + Arc::new(AckleyFunction::new(10)), "Ackley".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - StyblinskiTangFunction::new(2) - }), + Arc::new(StyblinskiTangFunction::new(2)), "StyblinskiTang".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - StyblinskiTangFunction::new(5) - }), + Arc::new(StyblinskiTangFunction::new(5)), "StyblinskiTang".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - StyblinskiTangFunction::new(10) - }), + Arc::new(StyblinskiTangFunction::new(10)), "StyblinskiTang".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - BealeFunction::new() - }), + Arc::new(BealeFunction::new()), "Beale".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - LeviFunction::new() - }), + Arc::new(LeviFunction::new()), "Levi".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - GoldsteinPriceFunction::new() - }), + Arc::new(GoldsteinPriceFunction::new()), "GoldsteinPrice".to_string(), Some(2), 42, @@ -196,49 +160,37 @@ pub fn analytic_problems() -> Vec { 42, ), ProblemSpec::new( - Arc::new({ - GriewankFunction::new(2) - }), + Arc::new(GriewankFunction::new(2)), "Griewank".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - GriewankFunction::new(5) - }), + Arc::new(GriewankFunction::new(5)), "Griewank".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - GriewankFunction::new(10) - }), + Arc::new(GriewankFunction::new(10)), "Griewank".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - SchwefelFunction::new(2) - }), + Arc::new(SchwefelFunction::new(2)), "Schwefel".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - SchwefelFunction::new(5) - }), + Arc::new(SchwefelFunction::new(5)), "Schwefel".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - SchwefelFunction::new(10) - }), + Arc::new(SchwefelFunction::new(10)), "Schwefel".to_string(), Some(10), 42, @@ -280,25 +232,19 @@ pub fn analytic_problems() -> Vec { 42, ), ProblemSpec::new( - Arc::new({ - IllConditionedRosenbrock::new(2, 100.0) - }), + Arc::new(IllConditionedRosenbrock::new(2, 100.0)), "IllConditionedRosenbrock".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - IllConditionedRosenbrock::new(5, 100.0) - }), + Arc::new(IllConditionedRosenbrock::new(5, 100.0)), "IllConditionedRosenbrock".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - IllConditionedRosenbrock::new(10, 100.0) - }), + Arc::new(IllConditionedRosenbrock::new(10, 100.0)), "IllConditionedRosenbrock".to_string(), Some(10), 42, @@ -310,105 +256,79 @@ pub fn analytic_problems() -> Vec { 42, ), ProblemSpec::new( - Arc::new({ - TrigonometricFunction::new(5) - }), + Arc::new(TrigonometricFunction::new(5)), "Trigonometric".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - TrigonometricFunction::new(10) - }), + Arc::new(TrigonometricFunction::new(10)), "Trigonometric".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - PenaltyFunctionI::new(2) - }), + Arc::new(PenaltyFunctionI::new(2)), "PenaltyI".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - PenaltyFunctionI::new(5) - }), + Arc::new(PenaltyFunctionI::new(5)), "PenaltyI".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - PenaltyFunctionI::new(10) - }), + Arc::new(PenaltyFunctionI::new(10)), "PenaltyI".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - BarrierFunction::new(2) - }), + Arc::new(BarrierFunction::new(2)), "Barrier".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - BarrierFunction::new(5) - }), + Arc::new(BarrierFunction::new(5)), "Barrier".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - BarrierFunction::new(10) - }), + Arc::new(BarrierFunction::new(10)), "Barrier".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - NoisySphere::new(2, 0.01) - }), + Arc::new(NoisySphere::new(2, 0.01)), "NoisySphere".to_string(), Some(2), 42, ), ProblemSpec::new( - Arc::new({ - NoisySphere::new(5, 0.01) - }), + Arc::new(NoisySphere::new(5, 0.01)), "NoisySphere".to_string(), Some(5), 42, ), ProblemSpec::new( - Arc::new({ - NoisySphere::new(10, 0.01) - }), + Arc::new(NoisySphere::new(10, 0.01)), "NoisySphere".to_string(), Some(10), 42, ), ProblemSpec::new( - Arc::new({ - SparseRosenbrock::new(4) - }), + Arc::new(SparseRosenbrock::new(4)), "SparseRosenbrock".to_string(), Some(4), 42, ), ProblemSpec::new( - Arc::new({ - SparseRosenbrock::new(10) - }), + Arc::new(SparseRosenbrock::new(10)), "SparseRosenbrock".to_string(), Some(10), 42, diff --git a/src/experiment_runner/report_generator.rs b/src/experiment_runner/report_generator.rs index 805cc92c..bb5daab5 100644 --- a/src/experiment_runner/report_generator.rs +++ b/src/experiment_runner/report_generator.rs @@ -3,22 +3,37 @@ use crate::benchmarks::evaluation::{ is_no_threshold_mode, BenchmarkConfig, BenchmarkResults, ConvergenceReason, ProblemSpec, SingleResult, }; +use crate::experiment_runner::experiment_runner::get_optimizer_family; +use crate::experiment_runner::reports::comparison_matrix; +use crate::experiment_runner::reports::comparison_matrix::{ + generate_comparison_matrix_latex_table, generate_comparison_matrix_table_content, + generate_family_comparison_matrix_table_content, +}; +use crate::experiment_runner::reports::convergence_analysis::{ + generate_convergence_analysis, generate_convergence_speed_latex_table, + generate_convergence_speed_table_content, +}; +use crate::experiment_runner::reports::efficiency_matrix::generate_efficiency_matrix_latex_table; +use crate::experiment_runner::reports::family_vs_family::{ + generate_family_vs_family_comparison_table, generate_family_vs_family_latex_table, + generate_family_vs_family_table_content, +}; +use crate::experiment_runner::reports::heatmap::{ + generate_success_rate_heatmap_latex_table, generate_success_rate_heatmap_table_content, +}; +use crate::experiment_runner::reports::performance_analysis::generate_performance_analysis; +use crate::experiment_runner::reports::performance_table::{ + generate_main_performance_latex_table, generate_main_performance_table_content, +}; +use crate::experiment_runner::reports::summary_statistics::{ + generate_summary_statistics_latex_table, generate_summary_statistics_table_content, +}; use crate::OptimizationProblem; use anyhow::Context; use log::warn; use std::collections::HashMap; use std::fs; use std::path::Path; -use crate::experiment_runner::experiment_runner::get_optimizer_family; -use crate::experiment_runner::reports::comparison_matrix; -use crate::experiment_runner::reports::comparison_matrix::{generate_comparison_matrix_latex_table, generate_comparison_matrix_table_content, generate_family_comparison_matrix_table_content}; -use crate::experiment_runner::reports::convergence_analysis::{generate_convergence_analysis, generate_convergence_speed_latex_table, generate_convergence_speed_table_content}; -use crate::experiment_runner::reports::efficiency_matrix::generate_efficiency_matrix_latex_table; -use crate::experiment_runner::reports::family_vs_family::{generate_family_vs_family_comparison_table, generate_family_vs_family_latex_table, generate_family_vs_family_table_content}; -use crate::experiment_runner::reports::heatmap::{generate_success_rate_heatmap_latex_table, generate_success_rate_heatmap_table_content}; -use crate::experiment_runner::reports::performance_analysis::generate_performance_analysis; -use crate::experiment_runner::reports::performance_table::{generate_main_performance_latex_table, generate_main_performance_table_content}; -use crate::experiment_runner::reports::summary_statistics::{generate_summary_statistics_latex_table, generate_summary_statistics_table_content}; /// Data structure for family performance comparison #[derive(Debug, Clone)] @@ -85,23 +100,23 @@ impl ReportGenerator { ) -> anyhow::Result<()> { fs::create_dir_all(&self.output_dir) .with_context(|| format!("Failed to create output directory: {}", self.output_dir))?; - // Create hierarchical directory structure - let reports_dir = Path::new(&self.output_dir).join("reports"); - let data_dir = Path::new(&self.output_dir).join("data"); - let convergence_dir = Path::new(&self.output_dir).join("convergence"); - let plots_dir = Path::new(&self.output_dir).join("plots"); - let latex_dir = Path::new(&self.output_dir).join("latex"); - fs::create_dir_all(&reports_dir)?; - fs::create_dir_all(&data_dir)?; - fs::create_dir_all(&convergence_dir)?; - fs::create_dir_all(&plots_dir)?; - fs::create_dir_all(&latex_dir)?; - + // Create hierarchical directory structure + let reports_dir = Path::new(&self.output_dir).join("reports"); + let data_dir = Path::new(&self.output_dir).join("data"); + let convergence_dir = Path::new(&self.output_dir).join("convergence"); + let plots_dir = Path::new(&self.output_dir).join("plots"); + let latex_dir = Path::new(&self.output_dir).join("latex"); + fs::create_dir_all(&reports_dir)?; + fs::create_dir_all(&data_dir)?; + fs::create_dir_all(&convergence_dir)?; + fs::create_dir_all(&plots_dir)?; + fs::create_dir_all(&latex_dir)?; + println!("Generating report in directory: {}", self.output_dir); // Generate detailed optimizer-problem reports first generate_detailed_reports( - &reports_dir.to_string_lossy(), + &reports_dir.to_string_lossy(), all_results, use_optimizer_families, ) @@ -114,19 +129,17 @@ impl ReportGenerator { html_content.push_str(&generate_problem_section( problem, results, - &plots_dir.to_string_lossy(), + &plots_dir.to_string_lossy(), )?); } // Add optimizer family vs problem family comparison - html_content.push_str(&generate_family_vs_family_comparison_table( - all_results, - )?); + html_content.push_str(&generate_family_vs_family_comparison_table(all_results)?); if !all_results.is_empty() && all_results.iter().any(|(_, r)| !r.results.is_empty()) { html_content.push_str(&self.statistical_analysis.generate_statistical_analysis( all_results, &self.config, - &data_dir.to_string_lossy(), + &data_dir.to_string_lossy(), use_optimizer_families, )?); } @@ -140,25 +153,14 @@ impl ReportGenerator { format!("Failed to write Markdown report to: {}", md_path.display()) })?; - generate_csv_exports(&data_dir.to_string_lossy(), all_results)?; + generate_csv_exports(&data_dir.to_string_lossy(), all_results)?; // Generate LaTeX tables - generate_latex_tables( - &latex_dir.to_string_lossy(), - all_results, - self, - ) - .await?; + generate_latex_tables(&latex_dir.to_string_lossy(), all_results, self).await?; // Generate comprehensive LaTeX document - generate_comprehensive_latex_document( - &self.config, - all_results, - &latex_dir, - self, - )?; + generate_comprehensive_latex_document(&self.config, all_results, &latex_dir, self)?; Ok(()) } - } /// Generate efficiency matrix table content (without document wrapper) @@ -203,17 +205,13 @@ fn generate_efficiency_matrix_table_content( ); // Same calculation logic as the standalone table for optimizer_family in &optimizer_families { - content.push_str(&format!( - "\\textbf{{{}}} ", - escape_latex(optimizer_family) - )); + content.push_str(&format!("\\textbf{{{}}} ", escape_latex(optimizer_family))); for problem_family in &problem_families { let mut successful_evaluations = Vec::new(); for (problem, results) in all_results { if get_family(&problem.get_name()) == *problem_family { for result in &results.results { - let result_optimizer_family = - get_optimizer_family(&result.optimizer_name); + let result_optimizer_family = get_optimizer_family(&result.optimizer_name); if result_optimizer_family == *optimizer_family && result.convergence_achieved { @@ -233,9 +231,9 @@ fn generate_efficiency_matrix_table_content( .sum::() / successful_evaluations.len() as f64; let std_dev = variance.sqrt(); - format!("{:.0} $\\pm$ {:.0}", mean, std_dev) + format!("{mean:.0} $\\pm$ {std_dev:.0}") }; - content.push_str(&format!("& {} ", cell_content)); + content.push_str(&format!("& {cell_content} ")); } content.push_str("\\\\\n"); } @@ -289,8 +287,7 @@ fn generate_problem_table_content( if final_values.is_empty() { continue; } - let function_evals: Vec = - runs.iter().map(|r| r.function_evaluations as f64).collect(); + let function_evals: Vec = runs.iter().map(|r| r.function_evaluations as f64).collect(); let success_count = runs.iter().filter(|r| r.convergence_achieved).count(); let execution_times: Vec = runs .iter() @@ -310,8 +307,7 @@ fn generate_problem_table_content( .iter() .cloned() .fold(f64::NEG_INFINITY, f64::max); - let mean_function_evals = - function_evals.iter().sum::() / function_evals.len() as f64; + let mean_function_evals = function_evals.iter().sum::() / function_evals.len() as f64; let success_rate = success_count as f64 / runs.len() as f64 * 100.0; let mean_time = execution_times.iter().sum::() / execution_times.len() as f64; perf_data.push(( @@ -354,15 +350,7 @@ fn generate_problem_table_content( escape_latex(optimizer) }; content.push_str(&format!( - "{} & {:.2e} & {:.2e} & {:.2e} & {:.2e} & {:.1} & {:.1} & {:.3} \\\\\n", - optimizer_style, - mean_final, - std_final, - best_final, - worst_final, - mean_func_evals, - success_rate, - mean_time + "{optimizer_style} & {mean_final:.2e} & {std_final:.2e} & {best_final:.2e} & {worst_final:.2e} & {mean_func_evals:.1} & {success_rate:.1} & {mean_time:.3} \\\\\n" )); } content.push_str( @@ -427,7 +415,7 @@ async fn generate_detailed_reports( &optimizer_name, &optimizer_runs, ) - .await?; + .await?; } } Ok(()) @@ -442,7 +430,7 @@ async fn generate_optimizer_problem_report( let problem_name = problem.name(); let problem_filename = problem_name.replace(" ", "_"); let optimizer_filename = optimizer_name.replace(" ", "_"); - let filename = format!("detailed_{}_{}.md", problem_filename, optimizer_filename); + let filename = format!("detailed_{problem_filename}_{optimizer_filename}.md"); let filepath = Path::new(output_dir).join(&filename); let mut content = generate_detailed_report_header(problem, optimizer_name, runs); content.push_str(&generate_run_by_run_analysis(runs)?); @@ -454,9 +442,8 @@ async fn generate_optimizer_problem_report( problem_name, optimizer_name, )); - fs::write(&filepath, content).with_context(|| { - format!("Failed to write detailed report to: {}", filepath.display()) - })?; + fs::write(&filepath, content) + .with_context(|| format!("Failed to write detailed report to: {}", filepath.display()))?; Ok(()) } fn generate_detailed_report_header( @@ -658,11 +645,11 @@ fn generate_parameter_evolution_analysis(runs: &[&SingleResult]) -> anyhow::Resu .parameters .iter() .take(5) - .map(|p| format!("{:.3e}", p)) + .map(|p| format!("{p:.3e}")) .collect::>() .join(", "); let params_display = if iter_data.parameters.len() > 5 { - format!("{}, ...", params_str) + format!("{params_str}, ...") } else { params_str }; @@ -707,12 +694,11 @@ fn generate_failure_analysis(runs: &[&SingleResult]) -> anyhow::Result { } content.push_str(&format!( r#"### Failure Patterns -- **Early Failures (< 10 iterations):** {} -- **Timeout Failures:** {} -- **Numerical Errors:** {} -- **Maximum Iterations Reached:** {} -"#, - early_failures, timeout_failures, numerical_failures, max_iter_failures +- **Early Failures (< 10 iterations):** {early_failures} +- **Timeout Failures:** {timeout_failures} +- **Numerical Errors:** {numerical_failures} +- **Maximum Iterations Reached:** {max_iter_failures} +"# )); // Show details of failed runs if failed_runs.len() <= 5 { @@ -735,7 +721,7 @@ fn generate_failure_analysis(runs: &[&SingleResult]) -> anyhow::Result { run.function_evaluations, run.convergence_reason, if let Some(ref error) = run.error_message { - format!("- Error: {}", error) + format!("- Error: {error}") } else { String::new() } @@ -851,10 +837,9 @@ fn generate_winner_summary_table(all_results: &[(&ProblemSpec, BenchmarkResults) // Create link to detailed problem analysis let problem_filename = problem_name.replace(" ", "_"); let problem_link = format!( - r#"{}"#, - problem_filename, problem_name + r#"{problem_name}"# ); - + summary.push_str(&format!( r#" {} @@ -959,7 +944,7 @@ fn generate_problem_section( "#, problem_name, problem.family, - dimension.or(Some(0)).unwrap(), + dimension.unwrap_or(0), optimal_value, results.results.len(), problem_name.replace(" ", "_") @@ -984,12 +969,9 @@ fn generate_problem_section( // Also check for NaN or infinite values if !result.final_value.is_finite() || !result.final_gradient_norm.is_finite() { warn!( - "Non-finite values detected for {} on {}: final_value={}, gradient_norm={}", - result.optimizer_name, - problem_name, - result.final_value, - result.final_gradient_norm - ); + "Non-finite values detected for {} on {}: final_value={}, gradient_norm={}", + result.optimizer_name, problem_name, result.final_value, result.final_gradient_norm + ); } } @@ -1001,8 +983,7 @@ fn generate_problem_section( ); for (optimizer, evaluations, final_value) in suspicious_results { section.push_str(&format!( - "> {} claimed convergence with {} function evaluations (final_value: {:.2e}) \n", - optimizer, evaluations, final_value + "> {optimizer} claimed convergence with {evaluations} function evaluations (final_value: {final_value:.2e}) \n" )); } section.push_str(r#"> @@ -1039,10 +1020,8 @@ fn generate_problem_section( continue; // Skip optimizers with no valid results } - let function_evals: Vec = - runs.iter().map(|r| r.function_evaluations as f64).collect(); - let gradient_evals: Vec = - runs.iter().map(|r| r.gradient_evaluations as f64).collect(); + let function_evals: Vec = runs.iter().map(|r| r.function_evaluations as f64).collect(); + let gradient_evals: Vec = runs.iter().map(|r| r.gradient_evaluations as f64).collect(); let success_count = if is_no_threshold_mode() { runs.iter().filter(|r| r.convergence_achieved).count() } else { @@ -1056,15 +1035,13 @@ fn generate_problem_section( let mean_final = final_values.iter().sum::() / final_values.len() as f64; if !mean_final.is_finite() { warn!( - "Mean final value for optimizer '{}' is not finite (mean: {})", - optimizer, mean_final - ); + "Mean final value for optimizer '{optimizer}' is not finite (mean: {mean_final})" + ); continue; } // Separate statistics for successful and unsuccessful runs let successful_runs: Vec<_> = runs.iter().filter(|r| r.convergence_achieved).collect(); - let unsuccessful_runs: Vec<_> = - runs.iter().filter(|r| !r.convergence_achieved).collect(); + let unsuccessful_runs: Vec<_> = runs.iter().filter(|r| !r.convergence_achieved).collect(); // Calculate separate statistics for successful runs let (mean_final_success, mean_func_evals_success, mean_grad_evals_success) = if !successful_runs.is_empty() { @@ -1135,10 +1112,8 @@ fn generate_problem_section( .iter() .cloned() .fold(f64::NEG_INFINITY, f64::max); - let mean_function_evals = - function_evals.iter().sum::() / function_evals.len() as f64; - let mean_gradient_evals = - gradient_evals.iter().sum::() / gradient_evals.len() as f64; + let mean_function_evals = function_evals.iter().sum::() / function_evals.len() as f64; + let mean_gradient_evals = gradient_evals.iter().sum::() / gradient_evals.len() as f64; let success_rate = success_count as f64 / runs.len() as f64; let mean_time = execution_times.iter().sum::() / execution_times.len() as f64; @@ -1206,66 +1181,54 @@ fn generate_problem_section( // Create hyperlink to detailed report let problem_filename = problem_name.replace(" ", "_"); let optimizer_filename = optimizer.replace(" ", "_"); - let detailed_report_filename = format!( - "reports/detailed_{}_{}.md", - problem_filename, - optimizer_filename - ); + let detailed_report_filename = + format!("reports/detailed_{problem_filename}_{optimizer_filename}.md"); let optimizer_link = format!( - r#"{}"#, - detailed_report_filename, optimizer + r#"{optimizer}"# ); // Create formatted strings for success/fail values let success_str = if mean_final_success.is_nan() || !mean_final_success.is_finite() { "-".to_string() } else { - format!("{:.2e}", mean_final_success) + format!("{mean_final_success:.2e}") }; let fail_str = if mean_final_fail.is_nan() || !mean_final_fail.is_finite() { "-".to_string() } else { - format!("{:.2e}", mean_final_fail) + format!("{mean_final_fail:.2e}") }; let final_value_str = if mean_final.is_finite() { - format!("{:.2e} / {} / {}", mean_final, success_str, fail_str) + format!("{mean_final:.2e} / {success_str} / {fail_str}") } else { - format!("- / {} / {}", success_str, fail_str) + format!("- / {success_str} / {fail_str}") }; // Create formatted strings for function evaluations let func_success_str = if mean_func_evals_success.is_nan() || !mean_func_evals_success.is_finite() { "-".to_string() } else { - format!("{:.1}", mean_func_evals_success) + format!("{mean_func_evals_success:.1}") }; - let func_fail_str = - if mean_func_evals_fail.is_nan() || !mean_func_evals_fail.is_finite() { - "-".to_string() - } else { - format!("{:.1}", mean_func_evals_fail) - }; - let func_evals_str = format!( - "{:.1} / {} / {}", - mean_func_evals, func_success_str, func_fail_str - ); + let func_fail_str = if mean_func_evals_fail.is_nan() || !mean_func_evals_fail.is_finite() { + "-".to_string() + } else { + format!("{mean_func_evals_fail:.1}") + }; + let func_evals_str = format!("{mean_func_evals:.1} / {func_success_str} / {func_fail_str}"); // Create formatted strings for gradient evaluations let grad_success_str = if mean_grad_evals_success.is_nan() || !mean_grad_evals_success.is_finite() { "-".to_string() } else { - format!("{:.1}", mean_grad_evals_success) - }; - let grad_fail_str = - if mean_grad_evals_fail.is_nan() || !mean_grad_evals_fail.is_finite() { - "-".to_string() - } else { - format!("{:.1}", mean_grad_evals_fail) + format!("{mean_grad_evals_success:.1}") }; - let grad_evals_str = format!( - "{:.1} / {} / {}", - mean_grad_evals, grad_success_str, grad_fail_str - ); + let grad_fail_str = if mean_grad_evals_fail.is_nan() || !mean_grad_evals_fail.is_finite() { + "-".to_string() + } else { + format!("{mean_grad_evals_fail:.1}") + }; + let grad_evals_str = format!("{mean_grad_evals:.1} / {grad_success_str} / {grad_fail_str}"); section.push_str(&format!( r#" @@ -1307,37 +1270,33 @@ fn generate_problem_section( ); // Add convergence plots for this problem let problem_filename = problem_name.replace(" ", "_"); - let convergence_plot = format!("plots/convergence/{}.png", problem_filename); - let log_convergence_plot = format!("plots/convergence/{}_log.png", problem_filename); + let convergence_plot = format!("plots/convergence/{problem_filename}.png"); + let log_convergence_plot = format!("plots/convergence/{problem_filename}_log.png"); // Check if convergence plot exists - let convergence_path = Path::new(plots_dir).join(format!("convergence/{}.png", problem_filename)); + let convergence_path = Path::new(plots_dir).join(format!("convergence/{problem_filename}.png")); if convergence_path.exists() { section.push_str(&format!( - r#"Convergence plot for {} -"#, - convergence_plot, problem_name + r#"Convergence plot for {problem_name} +"# )); } // Check if log convergence plot exists - let log_convergence_path = Path::new(plots_dir).join(format!("convergence/{}_log.png", problem_filename)); + let log_convergence_path = + Path::new(plots_dir).join(format!("convergence/{problem_filename}_log.png")); if log_convergence_path.exists() { section.push_str(&format!( - r#"Log convergence plot for {} -"#, - log_convergence_plot, problem_name + r#"Log convergence plot for {problem_name} +"# )); } section.push_str(&format!( r#" -**Figure:** Convergence plots for {} showing objective value vs iterations. +**Figure:** Convergence plots for {problem_name} showing objective value vs iterations. Left: Linear scale, Right: Log scale for better visualization of convergence behavior. -**Data:** [Linear scale data (CSV)](data/convergence/{}_data.csv) | [Log scale data (CSV)](data/convergence/{}_log_data.csv) +**Data:** [Linear scale data (CSV)](data/convergence/{problem_filename}_data.csv) | [Log scale data (CSV)](data/convergence/{problem_filename}_log_data.csv) -"#, - problem_name, - problem_filename, - problem_filename +"# )); Ok(section) } @@ -1459,8 +1418,8 @@ fn generate_csv_exports( all_results: &[(&ProblemSpec, BenchmarkResults)], ) -> anyhow::Result<()> { fs::create_dir_all(output_dir) - .with_context(|| format!("Failed to create output directory: {}", output_dir))?; - println!("Exporting CSV files to: {}", output_dir); + .with_context(|| format!("Failed to create output directory: {output_dir}"))?; + println!("Exporting CSV files to: {output_dir}"); // Enhanced CSV with more fields let mut csv_content = String::from("Problem,ProblemFamily,Dimension,Optimizer,Run,FinalValue,FinalGradientNorm,Iterations,FunctionEvals,GradientEvals,Time,Converged,ConvergenceReason\n"); @@ -1518,8 +1477,7 @@ fn generate_csv_exports( continue; // Skip if no valid results } // Separate successful and unsuccessful runs - let successful_runs: Vec<_> = - runs.iter().filter(|r| r.convergence_achieved).collect(); + let successful_runs: Vec<_> = runs.iter().filter(|r| r.convergence_achieved).collect(); let unsuccessful_runs: Vec<_> = runs.iter().filter(|r| !r.convergence_achieved).collect(); // Calculate statistics for successful runs @@ -1644,9 +1602,8 @@ fn generate_csv_exports( let summary_path = Path::new(output_dir).join("summary_statistics.csv"); println!("Writing summary statistics to: {}", summary_path.display()); - fs::write(&summary_path, summary_csv).with_context(|| { - format!("Failed to write summary CSV to: {}", summary_path.display()) - })?; + fs::write(&summary_path, summary_csv) + .with_context(|| format!("Failed to write summary CSV to: {}", summary_path.display()))?; // Generate problem-specific CSV files for easier analysis generate_problem_specific_csvs(output_dir, all_results)?; @@ -1667,10 +1624,10 @@ fn generate_problem_specific_csvs( // Also generate a problem analysis report for each problem let reports_dir = Path::new(output_dir).parent().unwrap().join("reports"); fs::create_dir_all(&reports_dir)?; - + for (problem, results) in all_results { let problem_name = problem.get_name().replace(" ", "_"); - let csv_path = problems_dir.join(format!("{}_results.csv", problem_name)); + let csv_path = problems_dir.join(format!("{problem_name}_results.csv")); let mut csv_content = String::from("Optimizer,Run,FinalValue,FinalGradientNorm,Iterations,FunctionEvals,GradientEvals,Time,Converged\n"); for result in &results.results { csv_content.push_str(&format!( @@ -1705,8 +1662,8 @@ fn generate_problem_analysis_report( ) -> anyhow::Result<()> { let problem_name = problem.get_name(); let problem_filename = problem_name.replace(" ", "_"); - let report_path = reports_dir.join(format!("problem_analysis_{}.md", problem_filename)); - + let report_path = reports_dir.join(format!("problem_analysis_{problem_filename}.md")); + let mut content = format!( r#"# Comprehensive Analysis: {} @@ -1730,10 +1687,10 @@ fn generate_problem_analysis_report( problem.problem.optimal_value().unwrap_or(f64::NEG_INFINITY), results.results.len() ); - + // Add detailed performance table content.push_str(&generate_problem_performance_table(results)?); - + // Add convergence analysis content.push_str(&format!( r#" @@ -1758,7 +1715,7 @@ fn generate_problem_analysis_report( problem_filename, chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC") )); - + fs::write(&report_path, content)?; Ok(()) } @@ -1802,21 +1759,35 @@ fn generate_problem_performance_table(results: &BenchmarkResults) -> anyhow::Res let success_count = runs.iter().filter(|r| r.convergence_achieved).count(); let mean_final = final_values.iter().sum::() / final_values.len() as f64; let success_rate = success_count as f64 / runs.len() as f64; - let mean_func_evals = runs.iter().map(|r| r.function_evaluations as f64).sum::() / runs.len() as f64; - let mean_time = runs.iter().map(|r| r.execution_time.as_secs_f64()).sum::() / runs.len() as f64; + let mean_func_evals = runs + .iter() + .map(|r| r.function_evaluations as f64) + .sum::() + / runs.len() as f64; + let mean_time = runs + .iter() + .map(|r| r.execution_time.as_secs_f64()) + .sum::() + / runs.len() as f64; - perf_data.push((optimizer.clone(), mean_final, success_rate, mean_func_evals, mean_time)); + perf_data.push(( + optimizer.clone(), + mean_final, + success_rate, + mean_func_evals, + mean_time, + )); } // Sort by success rate first, then by mean final value - perf_data.sort_by(|a, b| { - match b.2.total_cmp(&a.2) { - std::cmp::Ordering::Equal => a.1.total_cmp(&b.1), - other => other, - } + perf_data.sort_by(|a, b| match b.2.total_cmp(&a.2) { + std::cmp::Ordering::Equal => a.1.total_cmp(&b.1), + other => other, }); - for (i, (optimizer, mean_final, success_rate, mean_func_evals, mean_time)) in perf_data.iter().enumerate() { + for (i, (optimizer, mean_final, success_rate, mean_func_evals, mean_time)) in + perf_data.iter().enumerate() + { let style = if i == 0 { "background-color: #d4edda; font-weight: bold;" } else if i == 1 { @@ -1852,7 +1823,6 @@ fn generate_problem_performance_table(results: &BenchmarkResults) -> anyhow::Res Ok(content) } - /// Generate LaTeX tables for all results async fn generate_latex_tables( output_dir: &str, @@ -1860,34 +1830,29 @@ async fn generate_latex_tables( slf: &ReportGenerator, ) -> anyhow::Result<()> { let latex_dir = Path::new(output_dir); - fs::create_dir_all(&latex_dir).with_context(|| { - format!("Failed to create LaTeX directory: {}", latex_dir.display()) - })?; + fs::create_dir_all(latex_dir) + .with_context(|| format!("Failed to create LaTeX directory: {}", latex_dir.display()))?; println!("Generating LaTeX tables in: {}", latex_dir.display()); // Generate main performance table - generate_main_performance_latex_table(all_results, &latex_dir)?; + generate_main_performance_latex_table(all_results, latex_dir)?; // Generate problem-specific tables for (problem, results) in all_results { - generate_problem_latex_table(problem, results, &latex_dir)?; + generate_problem_latex_table(problem, results, latex_dir)?; } // Generate summary statistics table - generate_summary_statistics_latex_table(all_results, &latex_dir)?; + generate_summary_statistics_latex_table(all_results, latex_dir)?; // Generate comparison matrix table - generate_comparison_matrix_latex_table( - all_results, - &latex_dir, - slf, - )?; + generate_comparison_matrix_latex_table(all_results, latex_dir, slf)?; // Generate family comparison matrix table - comparison_matrix::generate_family_comparison_matrix_latex_table(all_results, &latex_dir, slf)?; + comparison_matrix::generate_family_comparison_matrix_latex_table(all_results, latex_dir, slf)?; // Generate family vs family comparison matrix table - generate_family_vs_family_latex_table(all_results, &latex_dir).await?; + generate_family_vs_family_latex_table(all_results, latex_dir).await?; // Generate efficiency matrix table - generate_efficiency_matrix_latex_table(all_results, &latex_dir)?; + generate_efficiency_matrix_latex_table(all_results, latex_dir)?; // Generate success rate heatmap table - generate_success_rate_heatmap_latex_table(all_results, &latex_dir)?; + generate_success_rate_heatmap_latex_table(all_results, latex_dir)?; // Generate convergence speed analysis table - generate_convergence_speed_latex_table(all_results, &latex_dir)?; + generate_convergence_speed_latex_table(all_results, latex_dir)?; Ok(()) } @@ -1945,8 +1910,7 @@ fn generate_problem_latex_table( if final_values.is_empty() { continue; } - let function_evals: Vec = - runs.iter().map(|r| r.function_evaluations as f64).collect(); + let function_evals: Vec = runs.iter().map(|r| r.function_evaluations as f64).collect(); let success_count = runs.iter().filter(|r| r.convergence_achieved).count(); let execution_times: Vec = runs .iter() @@ -1966,8 +1930,7 @@ fn generate_problem_latex_table( .iter() .cloned() .fold(f64::NEG_INFINITY, f64::max); - let mean_function_evals = - function_evals.iter().sum::() / function_evals.len() as f64; + let mean_function_evals = function_evals.iter().sum::() / function_evals.len() as f64; let success_rate = success_count as f64 / runs.len() as f64 * 100.0; let mean_time = execution_times.iter().sum::() / execution_times.len() as f64; perf_data.push(( @@ -2010,15 +1973,7 @@ fn generate_problem_latex_table( escape_latex(optimizer) }; latex_content.push_str(&format!( - "{} & {:.2e} & {:.2e} & {:.2e} & {:.2e} & {:.1} & {:.1} & {:.3} \\\\\n", - optimizer_style, - mean_final, - std_final, - best_final, - worst_final, - mean_func_evals, - success_rate, - mean_time + "{optimizer_style} & {mean_final:.2e} & {std_final:.2e} & {best_final:.2e} & {worst_final:.2e} & {mean_func_evals:.1} & {success_rate:.1} & {mean_time:.3} \\\\\n" )); } latex_content.push_str( @@ -2029,13 +1984,12 @@ fn generate_problem_latex_table( \end{document} "#, ); - let latex_path = latex_dir.join(format!("{}_performance.tex", problem_filename)); + let latex_path = latex_dir.join(format!("{problem_filename}_performance.tex")); fs::write(&latex_path, latex_content) .with_context(|| format!("Failed to write LaTeX table to: {}", latex_path.display()))?; Ok(()) } - /// Generate comprehensive LaTeX document with all tables fn generate_comprehensive_latex_document( config: &BenchmarkConfig, @@ -2100,19 +2054,14 @@ The following sections present detailed performance comparisons across all teste "#, ); // Include summary statistics table content - latex_content.push_str(&generate_summary_statistics_table_content( - all_results, - )?); + latex_content.push_str(&generate_summary_statistics_table_content(all_results)?); latex_content.push_str( r#" \subsection{QQN vs Non-QQN Comparison Matrix} "#, ); // Include comparison matrix content - latex_content.push_str(&generate_comparison_matrix_table_content( - all_results, - slf, - )?); + latex_content.push_str(&generate_comparison_matrix_table_content(all_results, slf)?); latex_content.push_str( r#" \subsection{Optimizer Family Comparison Matrix} @@ -2129,36 +2078,28 @@ The following sections present detailed performance comparisons across all teste "#, ); // Include family vs family comparison matrix content - latex_content.push_str(&generate_family_vs_family_table_content( - all_results, - )?); + latex_content.push_str(&generate_family_vs_family_table_content(all_results)?); latex_content.push_str( r#" \subsection{Algorithm Efficiency Matrix} "#, ); // Include efficiency matrix content - latex_content.push_str(&generate_efficiency_matrix_table_content( - all_results, - )?); + latex_content.push_str(&generate_efficiency_matrix_table_content(all_results)?); latex_content.push_str( r#" \subsection{Success Rate Heatmap} "#, ); // Include success rate heatmap content - latex_content.push_str(&generate_success_rate_heatmap_table_content( - all_results, - )?); + latex_content.push_str(&generate_success_rate_heatmap_table_content(all_results)?); latex_content.push_str( r#" \subsection{Convergence Speed Analysis} "#, ); // Include convergence speed analysis content - latex_content.push_str(&generate_convergence_speed_table_content( - all_results, - )?); + latex_content.push_str(&generate_convergence_speed_table_content(all_results)?); latex_content.push_str( r#" @@ -2172,7 +2113,7 @@ The following subsections present detailed results for each individual problem. latex_content.push_str(&format!( r#"\subsection{{{}}} "#, - escape_latex(&*problem_name) + escape_latex(&problem_name) )); latex_content.push_str(&generate_problem_table_content(problem, results)?); } @@ -2209,4 +2150,4 @@ All raw experimental data, convergence plots, and additional analysis files are latex_path.display() ); Ok(()) -} \ No newline at end of file +} diff --git a/src/experiment_runner/reports/comparison_matrix.rs b/src/experiment_runner/reports/comparison_matrix.rs index 6336fbc7..78aa5f86 100644 --- a/src/experiment_runner/reports/comparison_matrix.rs +++ b/src/experiment_runner/reports/comparison_matrix.rs @@ -1,10 +1,10 @@ -use std::path::Path; -use std::collections::HashMap; -use std::fs; -use anyhow::Context; use crate::benchmarks::evaluation::{BenchmarkResults, ProblemSpec, SingleResult}; -use crate::experiment_runner::{report_generator, ReportGenerator}; use crate::experiment_runner::experiment_runner::get_optimizer_family; +use crate::experiment_runner::{report_generator, ReportGenerator}; +use anyhow::Context; +use std::collections::HashMap; +use std::fs; +use std::path::Path; /// Generate comparison matrix LaTeX table pub fn generate_comparison_matrix_latex_table( @@ -68,16 +68,15 @@ pub fn generate_comparison_matrix_latex_table( .join(" ") )); // Group results by problem for comparison - let mut problem_results: HashMap>> = - HashMap::new(); + let mut problem_results: HashMap>> = HashMap::new(); for (problem, results) in all_results { let problem_name = problem.get_name(); for result in &results.results { problem_results .entry(problem_name.to_string()) - .or_insert_with(HashMap::new) + .or_default() .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } } @@ -90,7 +89,7 @@ pub fn generate_comparison_matrix_latex_table( let mut wins = 0; let mut losses = 0; let mut ties = 0; - for (_, optimizers) in &problem_results { + for optimizers in problem_results.values() { if let (Some(qqn_results), Some(non_qqn_results)) = (optimizers.get(qqn_opt), optimizers.get(non_qqn_opt)) { @@ -131,19 +130,13 @@ pub fn generate_comparison_matrix_latex_table( } } let cell_content = if wins > losses { - format!( - "\\textcolor{{green!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{green!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else if losses > wins { - format!( - "\\textcolor{{red!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{red!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else { - format!("{}W-{}L-{}T", wins, losses, ties) + format!("{wins}W-{losses}L-{ties}T") }; - latex_content.push_str(&format!("& {} ", cell_content)); + latex_content.push_str(&format!("& {cell_content} ")); } latex_content.push_str("\\\\\n"); } @@ -212,26 +205,28 @@ pub fn generate_comparison_matrix_table_content( .join(" ") ); // Same comparison logic as before... - let mut problem_results: HashMap>> = - HashMap::new(); + let mut problem_results: HashMap>> = HashMap::new(); for (problem, results) in all_results { let problem_name = problem.get_name(); for result in &results.results { problem_results .entry(problem_name.to_string()) - .or_insert_with(HashMap::new) + .or_default() .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } } for qqn_opt in &qqn_optimizers { - content.push_str(&format!("\\textbf{{{}}} ", report_generator::escape_latex(qqn_opt))); + content.push_str(&format!( + "\\textbf{{{}}} ", + report_generator::escape_latex(qqn_opt) + )); for non_qqn_opt in &non_qqn_optimizers { let mut wins = 0; let mut losses = 0; let mut ties = 0; - for (_, optimizers) in &problem_results { + for optimizers in problem_results.values() { if let (Some(qqn_results), Some(non_qqn_results)) = (optimizers.get(qqn_opt), optimizers.get(non_qqn_opt)) { @@ -272,19 +267,13 @@ pub fn generate_comparison_matrix_table_content( } } let cell_content = if wins > losses { - format!( - "\\textcolor{{green!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{green!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else if losses > wins { - format!( - "\\textcolor{{red!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{red!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else { - format!("{}W-{}L-{}T", wins, losses, ties) + format!("{wins}W-{losses}L-{ties}T") }; - content.push_str(&format!("& {} ", cell_content)); + content.push_str(&format!("& {cell_content} ")); } content.push_str("\\\\\n"); } @@ -353,19 +342,22 @@ pub fn generate_family_comparison_matrix_table_content( let family = get_optimizer_family(&result.optimizer_name); problem_family_results .entry(problem_name.to_string()) - .or_insert_with(HashMap::new) + .or_default() .entry(family) - .or_insert_with(Vec::new) + .or_default() .push(result); } } for qqn_fam in &qqn_families { - content.push_str(&format!("\\textbf{{{}}} ", report_generator::escape_latex(qqn_fam))); + content.push_str(&format!( + "\\textbf{{{}}} ", + report_generator::escape_latex(qqn_fam) + )); for non_qqn_fam in &non_qqn_families { let mut wins = 0; let mut losses = 0; let mut ties = 0; - for (_, families) in &problem_family_results { + for families in problem_family_results.values() { if let (Some(qqn_results), Some(non_qqn_results)) = (families.get(qqn_fam), families.get(non_qqn_fam)) { @@ -406,19 +398,13 @@ pub fn generate_family_comparison_matrix_table_content( } } let cell_content = if wins > losses { - format!( - "\\textcolor{{green!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{green!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else if losses > wins { - format!( - "\\textcolor{{red!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{red!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else { - format!("{}W-{}L-{}T", wins, losses, ties) + format!("{wins}W-{losses}L-{ties}T") }; - content.push_str(&format!("& {} ", cell_content)); + content.push_str(&format!("& {cell_content} ")); } content.push_str("\\\\\n"); } @@ -501,19 +487,22 @@ pub fn generate_family_comparison_matrix_latex_table( let family = get_optimizer_family(&result.optimizer_name); problem_family_results .entry(problem_name.to_string()) - .or_insert_with(HashMap::new) + .or_default() .entry(family) - .or_insert_with(Vec::new) + .or_default() .push(result); } } for qqn_fam in &qqn_families { - latex_content.push_str(&format!("\\textbf{{{}}} ", report_generator::escape_latex(qqn_fam))); + latex_content.push_str(&format!( + "\\textbf{{{}}} ", + report_generator::escape_latex(qqn_fam) + )); for non_qqn_fam in &non_qqn_families { let mut wins = 0; let mut losses = 0; let mut ties = 0; - for (_, families) in &problem_family_results { + for families in problem_family_results.values() { if let (Some(qqn_results), Some(non_qqn_results)) = (families.get(qqn_fam), families.get(non_qqn_fam)) { @@ -554,19 +543,13 @@ pub fn generate_family_comparison_matrix_latex_table( } } let cell_content = if wins > losses { - format!( - "\\textcolor{{green!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{green!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else if losses > wins { - format!( - "\\textcolor{{red!70!black}}{{{}W-{}L-{}T}}", - wins, losses, ties - ) + format!("\\textcolor{{red!70!black}}{{{wins}W-{losses}L-{ties}T}}") } else { - format!("{}W-{}L-{}T", wins, losses, ties) + format!("{wins}W-{losses}L-{ties}T") }; - latex_content.push_str(&format!("& {} ", cell_content)); + latex_content.push_str(&format!("& {cell_content} ")); } latex_content.push_str("\\\\\n"); } @@ -587,4 +570,4 @@ pub fn generate_family_comparison_matrix_latex_table( latex_path.display() ); Ok(()) -} \ No newline at end of file +} diff --git a/src/experiment_runner/reports/convergence_analysis.rs b/src/experiment_runner/reports/convergence_analysis.rs index 9f7124eb..96c96258 100644 --- a/src/experiment_runner/reports/convergence_analysis.rs +++ b/src/experiment_runner/reports/convergence_analysis.rs @@ -1,8 +1,8 @@ -use std::path::Path; -use std::fs; -use anyhow::Context; use crate::benchmarks::evaluation::{BenchmarkResults, ProblemSpec, SingleResult}; use crate::experiment_runner::report_generator; +use anyhow::Context; +use std::fs; +use std::path::Path; /// Generate convergence speed table content (without document wrapper) pub fn generate_convergence_speed_table_content( @@ -100,8 +100,7 @@ pub fn generate_convergence_speed_table_content( report_generator::escape_latex(optimizer) }; content.push_str(&format!( - "{} & {:.1} & {:.1} & {:.1} \\\\\n", - optimizer_style, avg_50, avg_90, avg_final + "{optimizer_style} & {avg_50:.1} & {avg_90:.1} & {avg_final:.1} \\\\\n" )); } content.push_str( @@ -179,9 +178,9 @@ pub fn generate_convergence_analysis(runs: &[&SingleResult]) -> anyhow::Result() / successful_evaluations.len() as f64; let std_dev = variance.sqrt(); - format!("{:.0} $\\pm$ {:.0}", mean, std_dev) + format!("{mean:.0} $\\pm$ {std_dev:.0}") }; - latex_content.push_str(&format!("& {} ", cell_content)); + latex_content.push_str(&format!("& {cell_content} ")); } latex_content.push_str("\\\\\n"); } @@ -137,4 +136,4 @@ pub fn generate_efficiency_matrix_latex_table( latex_path.display() ); Ok(()) -} \ No newline at end of file +} diff --git a/src/experiment_runner/reports/family_vs_family.rs b/src/experiment_runner/reports/family_vs_family.rs index e77bc586..c734bc0a 100644 --- a/src/experiment_runner/reports/family_vs_family.rs +++ b/src/experiment_runner/reports/family_vs_family.rs @@ -1,18 +1,17 @@ -use std::path::Path; -use std::fs; -use anyhow::Context; -use std::collections::HashMap; use crate::benchmarks::evaluation::{is_no_threshold_mode, BenchmarkResults, ProblemSpec}; use crate::experiment_runner::experiment_runner::get_optimizer_family; use crate::experiment_runner::report_generator; use crate::experiment_runner::report_generator::FamilyPerformanceData; +use anyhow::Context; +use std::collections::HashMap; +use std::fs; +use std::path::Path; // Define consistent colors const BEST_COLOR_LATEX: &str = "\\cellcolor{bestgreen!30}"; const WORST_COLOR_LATEX: &str = "\\cellcolor{worstred!20}"; const BEST_COLOR_LATEX_INLINE: &str = "\\cellcolor{green!20}"; const WORST_COLOR_LATEX_INLINE: &str = "\\cellcolor{red!15}"; - /// Generate family vs family comparison LaTeX table pub async fn generate_family_vs_family_latex_table( all_results: &[(&ProblemSpec, BenchmarkResults)], @@ -34,9 +33,6 @@ pub async fn generate_family_vs_family_latex_table( optimizer_families.sort(); problem_families.sort(); - - - if optimizer_families.is_empty() || problem_families.is_empty() { return Ok(()); } @@ -64,15 +60,21 @@ pub async fn generate_family_vs_family_latex_table( for problem_family in &problem_families { let problems_in_family: Vec<_> = all_results .iter() - .filter(|(problem, _)| report_generator::get_family(&problem.get_name()) == *problem_family) + .filter(|(problem, _)| { + report_generator::get_family(&problem.get_name()) == *problem_family + }) .collect(); for optimizer_family in &optimizer_families { - let cell_data = calculate_family_performance_data(&problems_in_family, optimizer_family)?; - let key = format!("{}_{}", problem_family, optimizer_family); - family_scores.insert(key, vec![cell_data.average_ranking, cell_data.best_rank_average]); + let cell_data = + calculate_family_performance_data(&problems_in_family, optimizer_family)?; + let key = format!("{problem_family}_{optimizer_family}"); + family_scores.insert( + key, + vec![cell_data.average_ranking, cell_data.best_rank_average], + ); } } - + latex_content.push_str(&format!( r#"\begin{{longtable}}{{{col_spec}}} \caption{{Optimizer Family vs Problem Family Performance Matrix}} @@ -111,18 +113,20 @@ pub async fn generate_family_vs_family_latex_table( .join(" "), optimizer_families.len() + 1 )); - + // For each problem family, calculate statistics for problem_family in &problem_families { latex_content.push_str(&format!( "\\textbf{{{}}} ", report_generator::escape_latex(problem_family) )); - + // Get all problems in this family let problems_in_family: Vec<_> = all_results .iter() - .filter(|(problem, _)| report_generator::get_family(&problem.get_name()) == *problem_family) + .filter(|(problem, _)| { + report_generator::get_family(&problem.get_name()) == *problem_family + }) .collect(); // Collect scores for this row to find best/worst let mut row_scores = Vec::new(); @@ -131,21 +135,23 @@ pub async fn generate_family_vs_family_latex_table( calculate_family_performance_data(&problems_in_family, optimizer_family)?; row_scores.push((optimizer_family.clone(), cell_data.average_ranking)); } - + // Find best and worst in this row - let best_family = row_scores.iter() + let best_family = row_scores + .iter() .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(f, _)| f.clone()) .unwrap_or_default(); - let worst_family = row_scores.iter() + let worst_family = row_scores + .iter() .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(f, _)| f.clone()) .unwrap_or_default(); - + for optimizer_family in &optimizer_families { let cell_data = calculate_family_performance_data(&problems_in_family, optimizer_family)?; - + // Determine cell color let color_cmd = if cell_data.average_ranking.is_finite() { if optimizer_family == &best_family { @@ -158,7 +164,7 @@ pub async fn generate_family_vs_family_latex_table( } else { "" }; - + let cell_content = format!( "& {} \\begin{{tabular}}{{@{{}}c@{{}}}} {:.1} / {:.1} \\\\ \\scriptsize{{{}}} \\\\ \\scriptsize{{{}}} \\end{{tabular}}", color_cmd, @@ -211,7 +217,6 @@ fn truncate_name(name: &str, max_len: usize) -> String { } } - /// Generate family vs family table content (without document wrapper) pub fn generate_family_vs_family_table_content( all_results: &[(&ProblemSpec, BenchmarkResults)], @@ -239,15 +244,21 @@ pub fn generate_family_vs_family_table_content( for problem_family in &problem_families { let problems_in_family: Vec<_> = all_results .iter() - .filter(|(problem, _)| report_generator::get_family(&problem.get_name()) == *problem_family) + .filter(|(problem, _)| { + report_generator::get_family(&problem.get_name()) == *problem_family + }) .collect(); for optimizer_family in &optimizer_families { - let cell_data = calculate_family_performance_data(&problems_in_family, optimizer_family)?; - let key = format!("{}_{}", problem_family, optimizer_family); - family_scores.insert(key, vec![cell_data.average_ranking, cell_data.best_rank_average]); + let cell_data = + calculate_family_performance_data(&problems_in_family, optimizer_family)?; + let key = format!("{problem_family}_{optimizer_family}"); + family_scores.insert( + key, + vec![cell_data.average_ranking, cell_data.best_rank_average], + ); } } - + let mut content = format!( r#"\begin{{longtable}}{{l{}}} \caption{{Optimizer Family vs Problem Family Performance Matrix}} @@ -287,18 +298,20 @@ pub fn generate_family_vs_family_table_content( .join(" "), optimizer_families.len() + 1 ); - + // For each problem family, calculate statistics for problem_family in &problem_families { content.push_str(&format!( "\\textbf{{{}}} ", report_generator::escape_latex(problem_family) )); - + // Get all problems in this family let problems_in_family: Vec<_> = all_results .iter() - .filter(|(problem, _)| report_generator::get_family(&problem.get_name()) == *problem_family) + .filter(|(problem, _)| { + report_generator::get_family(&problem.get_name()) == *problem_family + }) .collect(); // Collect scores for this row to find best/worst let mut row_scores = Vec::new(); @@ -307,21 +320,23 @@ pub fn generate_family_vs_family_table_content( calculate_family_performance_data(&problems_in_family, optimizer_family)?; row_scores.push((optimizer_family.clone(), cell_data.average_ranking)); } - + // Find best and worst in this row - let best_family = row_scores.iter() + let best_family = row_scores + .iter() .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(f, _)| f.clone()) .unwrap_or_default(); - let worst_family = row_scores.iter() + let worst_family = row_scores + .iter() .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(f, _)| f.clone()) .unwrap_or_default(); - + for optimizer_family in &optimizer_families { let cell_data = calculate_family_performance_data(&problems_in_family, optimizer_family)?; - + // Determine cell color let color_cmd = if cell_data.average_ranking.is_finite() { if optimizer_family == &best_family { @@ -334,7 +349,7 @@ pub fn generate_family_vs_family_table_content( } else { "" }; - + let cell_content = format!( "& {} \\begin{{tabular}}{{@{{}}c@{{}}}} {:.1} / {:.1} \\\\ \\scriptsize{{{}}} \\\\ \\scriptsize{{{}}} \\end{{tabular}}", color_cmd, @@ -400,9 +415,8 @@ This table shows how different optimizer families perform across different probl "#); for optimizer_family in &optimizer_families { content.push_str(&format!( - r#"{} -"#, - optimizer_family + r#"{optimizer_family} +"# )); } content.push_str("\n"); @@ -410,23 +424,24 @@ This table shows how different optimizer families perform across different probl for problem_family in &problem_families { content.push_str(&format!( r#" -{} -"#, - problem_family +{problem_family} +"# )); // Get all problems in this family let problems_in_family: Vec<_> = all_results .iter() - .filter(|(problem, _)| report_generator::get_family(&problem.get_name()) == *problem_family) + .filter(|(problem, _)| { + report_generator::get_family(&problem.get_name()) == *problem_family + }) .collect(); if problems_in_family.is_empty() { continue; } - + for optimizer_family in &optimizer_families { let cell_data = calculate_family_performance_data(&problems_in_family, optimizer_family)?; - + // Collect scores for this row to find best/worst let mut row_scores = Vec::new(); for opt_fam in &optimizer_families { @@ -435,15 +450,17 @@ This table shows how different optimizer families perform across different probl row_scores.push((opt_fam.clone(), data.average_ranking)); } } - + // Find best and worst in this row - let best_family = row_scores.iter() + let best_family = row_scores + .iter() .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(f, _)| f.as_str()); - let worst_family = row_scores.iter() + let worst_family = row_scores + .iter() .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)) .map(|(f, _)| f.as_str()); - + let cell_style = if cell_data.average_ranking.is_finite() { if Some(optimizer_family.as_str()) == best_family { "border: 1px solid #ddd; padding: 6px; text-align: center; background-color: #90EE90; font-size: 10px;" @@ -455,7 +472,7 @@ This table shows how different optimizer families perform across different probl } else { "border: 1px solid #ddd; padding: 6px; text-align: center; font-size: 10px;" }; - + content.push_str(&format!( r#"
Avg Rank: {:.1}
@@ -496,7 +513,7 @@ pub(crate) fn calculate_family_performance_data( worst_variant: "N/A".to_string(), }); } - + let mut all_rankings = Vec::new(); let mut best_ranks_per_problem = Vec::new(); let mut variant_performance = std::collections::HashMap::new(); @@ -520,7 +537,7 @@ pub(crate) fn calculate_family_performance_data( continue; } let success_count = if is_no_threshold_mode() { - 0 // In no-threshold mode, we don't use convergence_achieved + 0 // In no-threshold mode, we don't use convergence_achieved } else { runs.iter().filter(|r| r.convergence_achieved).count() }; @@ -618,4 +635,4 @@ pub(crate) fn calculate_family_performance_data( best_variant, worst_variant, }) -} \ No newline at end of file +} diff --git a/src/experiment_runner/reports/heatmap.rs b/src/experiment_runner/reports/heatmap.rs index 44660adc..276c250b 100644 --- a/src/experiment_runner/reports/heatmap.rs +++ b/src/experiment_runner/reports/heatmap.rs @@ -1,8 +1,8 @@ -use std::path::Path; -use std::fs; -use anyhow::Context; use crate::benchmarks::evaluation::{BenchmarkResults, ProblemSpec}; use crate::experiment_runner::report_generator; +use anyhow::Context; +use std::fs; +use std::path::Path; /// Generate success rate heatmap table content (without document wrapper) pub fn generate_success_rate_heatmap_table_content( @@ -75,11 +75,10 @@ pub fn generate_success_rate_heatmap_table_content( ("red!70", "white") }; let cell_content = if optimizer_results.is_empty() { - format!("& \\cellcolor{{gray!30}}\\textcolor{{white}}{{N/A}}") + "& \\cellcolor{gray!30}\\textcolor{white}{N/A}".to_string() } else { format!( - "& \\cellcolor{{{}}}\\textcolor{{{}}}{{{:.0}\\%}}", - color, text_color, success_rate + "& \\cellcolor{{{color}}}\\textcolor{{{text_color}}}{{{success_rate:.0}\\%}}" ) }; content.push_str(&cell_content); @@ -186,11 +185,10 @@ pub fn generate_success_rate_heatmap_latex_table( ("red!70", "white") }; let cell_content = if optimizer_results.is_empty() { - format!("& \\cellcolor{{gray!30}}\\textcolor{{white}}{{N/A}}") + "& \\cellcolor{gray!30}\\textcolor{white}{N/A}".to_string() } else { format!( - "& \\cellcolor{{{}}}\\textcolor{{{}}}{{{:.0}\\%}}", - color, text_color, success_rate + "& \\cellcolor{{{color}}}\\textcolor{{{text_color}}}{{{success_rate:.0}\\%}}" ) }; latex_content.push_str(&cell_content); @@ -224,4 +222,4 @@ Quickly identifies which optimizers work on which problem types. latex_path.display() ); Ok(()) -} \ No newline at end of file +} diff --git a/src/experiment_runner/reports/mod.rs b/src/experiment_runner/reports/mod.rs index 633120e3..23133e8c 100644 --- a/src/experiment_runner/reports/mod.rs +++ b/src/experiment_runner/reports/mod.rs @@ -1,8 +1,8 @@ -pub mod performance_table; -pub mod family_vs_family; pub mod comparison_matrix; -pub mod summary_statistics; +pub mod convergence_analysis; pub mod efficiency_matrix; +pub mod family_vs_family; pub mod heatmap; -pub mod convergence_analysis; pub mod performance_analysis; +pub mod performance_table; +pub mod summary_statistics; diff --git a/src/experiment_runner/reports/performance_analysis.rs b/src/experiment_runner/reports/performance_analysis.rs index fc9acaa8..f111a8e0 100644 --- a/src/experiment_runner/reports/performance_analysis.rs +++ b/src/experiment_runner/reports/performance_analysis.rs @@ -48,4 +48,4 @@ pub fn generate_performance_analysis(runs: &[&SingleResult]) -> anyhow::Result>> = - HashMap::new(); + let mut family_results: HashMap>> = HashMap::new(); for (problem, results) in all_results { let family = report_generator::get_family(&problem.get_name()); for result in &results.results { family_results .entry(family.clone()) - .or_insert_with(HashMap::new) + .or_default() .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } } @@ -113,14 +112,7 @@ pub fn generate_summary_statistics_latex_table( report_generator::escape_latex(optimizer) }; latex_content.push_str(&format!( - "{} & {} & {:.1} & {:.2e} & {:.1} & {:.1} & {:.3} \\\\\n", - family_cell, - optimizer_style, - success_rate, - avg_final, - avg_func_evals, - avg_grad_evals, - avg_time + "{family_cell} & {optimizer_style} & {success_rate:.1} & {avg_final:.2e} & {avg_func_evals:.1} & {avg_grad_evals:.1} & {avg_time:.3} \\\\\n" )); } if !optimizer_data.is_empty() { @@ -165,16 +157,15 @@ pub fn generate_summary_statistics_table_content( "#, ); // Group by problem family (same logic as before) - let mut family_results: HashMap>> = - HashMap::new(); + let mut family_results: HashMap>> = HashMap::new(); for (problem, results) in all_results { let family = report_generator::get_family(&problem.get_name()); for result in &results.results { family_results .entry(family.clone()) - .or_insert_with(HashMap::new) + .or_default() .entry(result.optimizer_name.clone()) - .or_insert_with(Vec::new) + .or_default() .push(result); } } @@ -242,14 +233,7 @@ pub fn generate_summary_statistics_table_content( report_generator::escape_latex(optimizer) }; content.push_str(&format!( - "{} & {} & {:.1} & {:.2e} & {:.1} & {:.1} & {:.3} \\\\\n", - family_cell, - optimizer_style, - success_rate, - avg_final, - avg_func_evals, - avg_grad_evals, - avg_time + "{family_cell} & {optimizer_style} & {success_rate:.1} & {avg_final:.2e} & {avg_func_evals:.1} & {avg_grad_evals:.1} & {avg_time:.3} \\\\\n" )); } if !optimizer_data.is_empty() { @@ -265,4 +249,4 @@ pub fn generate_summary_statistics_table_content( "#, ); Ok(content) -} \ No newline at end of file +} diff --git a/src/experiment_runner/statistical_analysis.rs b/src/experiment_runner/statistical_analysis.rs index 3aca0e00..191b4762 100644 --- a/src/experiment_runner/statistical_analysis.rs +++ b/src/experiment_runner/statistical_analysis.rs @@ -52,6 +52,12 @@ const COHEN_D_LARGE: f64 = 0.8; #[derive(Debug, Clone)] pub struct StatisticalAnalysis; +impl Default for StatisticalAnalysis { + fn default() -> Self { + Self::new() + } +} + impl StatisticalAnalysis { /// Creates a new StatisticalAnalysis instance /// @@ -101,7 +107,6 @@ impl StatisticalAnalysis { /// ``` /// /// This indicates QQN found better solutions but required more function evaluations. - pub fn generate_statistical_analysis( &self, all_results: &[(&ProblemSpec, BenchmarkResults)], @@ -135,10 +140,11 @@ impl StatisticalAnalysis { result.optimizer_name.clone() }; - optimizer_results - .entry(optimizer_key) - .or_insert_with(Vec::new) - .push((result.final_value, cost, problem_name.to_string())); + optimizer_results.entry(optimizer_key).or_default().push(( + result.final_value, + cost, + problem_name.to_string(), + )); } } @@ -196,9 +202,9 @@ impl StatisticalAnalysis { grouped_optimizer_results // Note: Could group by problem family instead of individual problems .entry(problem.to_string()) - .or_insert_with(HashMap::new) + .or_default() .entry(optimizer_key) - .or_insert_with(Vec::new) + .or_default() .push((*final_value, *cost)); } } @@ -271,24 +277,12 @@ impl StatisticalAnalysis { }; csv_data.push(format!( - "{},{},{},Final_Objective_Value,{},{:.6},{:.6},{},{:.3}", - family_name, - qqn_opt, - non_qqn_opt, - winner, - t_stat, - p_value, - significant, - effect_size + "{family_name},{qqn_opt},{non_qqn_opt},Final_Objective_Value,{winner},{t_stat:.6},{p_value:.6},{significant},{effect_size:.3}" )); } Err(e) => { log::warn!( - "Statistical test failed for {} vs {} on {}: {}", - qqn_opt, - non_qqn_opt, - family_name, - e + "Statistical test failed for {qqn_opt} vs {non_qqn_opt} on {family_name}: {e}" ); } } @@ -319,24 +313,12 @@ impl StatisticalAnalysis { }; csv_data.push(format!( - "{},{},{},Computational_Cost,{},{:.6},{:.6},{},{:.3}", - family_name, - qqn_opt, - non_qqn_opt, - winner_name, - t_stat, - p_value, - significant, - effect_size + "{family_name},{qqn_opt},{non_qqn_opt},Computational_Cost,{winner_name},{t_stat:.6},{p_value:.6},{significant},{effect_size:.3}" )); } Err(e) => { log::warn!( - "Cost comparison test failed for {} vs {} on {}: {}", - qqn_opt, - non_qqn_opt, - family_name, - e + "Cost comparison test failed for {qqn_opt} vs {non_qqn_opt} on {family_name}: {e}" ); } } @@ -360,11 +342,8 @@ impl StatisticalAnalysis { .context("Failed to save statistical analysis CSV")?; // Generate LaTeX tables for academic reporting - self.generate_pairwise_statistical_matrix( - &optimizer_results, - output_dir, - ) - .context("Failed to generate pairwise statistical matrix")?; + self.generate_pairwise_statistical_matrix(&optimizer_results, output_dir) + .context("Failed to generate pairwise statistical matrix")?; self.generate_problem_difficulty_ranking(all_results, output_dir) .context("Failed to generate problem difficulty ranking")?; @@ -424,12 +403,11 @@ impl StatisticalAnalysis { /// - P_Value: Statistical significance level /// - Significant: Boolean indicating p < 0.05 /// - Effect_Size: Cohen's d effect size measure - fn save_statistical_analysis_csv(&self, csv_data: &[String], output_dir: &str) -> Result<()> { let csv_content = csv_data.join("\n"); let csv_path = Path::new(output_dir).join("statistical_analysis_raw_data.csv"); fs::write(&csv_path, csv_content) - .with_context(|| format!("Failed to write CSV to {:?}", csv_path))?; + .with_context(|| format!("Failed to write CSV to {csv_path:?}"))?; Ok(()) } /// Performs Welch's t-test for comparing two independent samples @@ -475,7 +453,6 @@ impl StatisticalAnalysis { /// let (t_stat, p_val) = analysis.welch_t_test(&qqn_results, &bfgs_results)?; /// println!("t = {:.3}, p = {:.3}", t_stat, p_val); /// ``` - fn welch_t_test(&self, sample_a: &[f64], sample_b: &[f64]) -> Result<(f64, f64)> { if sample_a.len() < MIN_SAMPLE_SIZE || sample_b.len() < MIN_SAMPLE_SIZE { return Err(anyhow::anyhow!("Insufficient sample size for t-test")); @@ -551,8 +528,12 @@ impl StatisticalAnalysis { /// The approximation is most accurate for common significance levels: /// - p < 0.001, p < 0.01, p < 0.05, p < 0.10 /// - Less precise for intermediate p-values, but sufficient for hypothesis testing - fn t_distribution_p_value(&self, t_abs: f64, df: f64) -> f64 { + // Special case: t = 0 means no difference, p = 1.0 + if t_abs == 0.0 { + return 1.0; + } + // For large df, t-distribution approaches normal distribution if df > 30.0 { let z = t_abs; @@ -637,7 +618,6 @@ impl StatisticalAnalysis { /// d = 0.8: QQN is 0.8 standard deviations better (large effect) /// d = 1.2: QQN is 1.2 standard deviations better (very large effect) /// ``` - fn cohens_d(&self, sample_a: &[f64], sample_b: &[f64]) -> f64 { if sample_a.len() < MIN_SAMPLE_SIZE || sample_b.len() < MIN_SAMPLE_SIZE { return 0.0; @@ -730,7 +710,6 @@ impl StatisticalAnalysis { /// /// This indicates QQN found better solutions (green, negative delta) /// but used more function evaluations (red, positive delta). - fn generate_comparison_matrix( &self, grouped_results: &HashMap>>, @@ -782,13 +761,13 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa // Create header row with non-QQN optimizer names for non_qqn in &non_qqn_optimizers { - matrix_section.push_str(&format!(" {}\n", non_qqn)); + matrix_section.push_str(&format!(" {non_qqn}\n")); } matrix_section.push_str(" \n"); // Generate data rows (one per QQN optimizer) for qqn_opt in &qqn_optimizers { - matrix_section.push_str(&format!(" \n {}\n", qqn_opt)); + matrix_section.push_str(&format!(" \n {qqn_opt}\n")); // Generate cells for each QQN vs non-QQN comparison for non_qqn_opt in &non_qqn_optimizers { matrix_section.push_str(" "); @@ -806,8 +785,7 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa { if qqn_results.len() >= 2 && non_qqn_results.len() >= 2 { let mut problem_section = format!( - "
{}", - problem_name + "
{problem_name}" ); // Analyze objective function performance @@ -839,16 +817,15 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa // Format delta value with appropriate precision let delta_obj_str = if delta_obj.abs() >= 1000.0 { - format!("{:.1e}", delta_obj) + format!("{delta_obj:.1e}") } else if delta_obj.abs() >= 1.0 { - format!("{:.2}", delta_obj) + format!("{delta_obj:.2}") } else { - format!("{:.3}", delta_obj) + format!("{delta_obj:.3}") }; problem_section.push_str(&format!( - "
obj: {}", - obj_color, delta_obj_str + "
obj: {delta_obj_str}" )); // Analyze computational efficiency @@ -880,16 +857,15 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa // Format cost delta with appropriate precision let delta_cost_str = if delta_cost.abs() >= 1000.0 { - format!("{:.1e}", delta_cost) + format!("{delta_cost:.1e}") } else if delta_cost.abs() >= 1.0 { - format!("{:.2}", delta_cost) + format!("{delta_cost:.2}") } else { - format!("{:.3}", delta_cost) + format!("{delta_cost:.3}") }; problem_section.push_str(&format!( - "
cost: {}", - cost_color, delta_cost_str + "
cost: {delta_cost_str}" )); problem_section.push_str("
"); @@ -902,7 +878,7 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa if !cell_content.is_empty() { matrix_section.push_str(&cell_content.join("")); } else { - matrix_section.push_str("—"); + matrix_section.push('—'); } matrix_section.push_str("\n"); } @@ -968,13 +944,13 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa let latex_dir = Path::new(output_dir).join("latex"); fs::create_dir_all(&latex_dir) - .with_context(|| format!("Failed to create LaTeX directory: {:?}", latex_dir))?; + .with_context(|| format!("Failed to create LaTeX directory: {latex_dir:?}"))?; let mut latex_content = String::new(); // LaTeX table header with dynamic column count let col_spec = "l".repeat(optimizers.len() + 1); - latex_content.push_str(&format!("\\begin{{tabular}}{{{}}}\n\\toprule\n", col_spec)); + latex_content.push_str(&format!("\\begin{{tabular}}{{{col_spec}}}\n\\toprule\n")); // Header row latex_content.push_str("Optimizer"); for opt in &optimizers { @@ -1001,22 +977,22 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa &optimizer_results[opt_j], ); let (color, p_str) = if p_value < 0.001 { - ("red!30", format!("{:.3e}", p_value)) + ("red!30", format!("{p_value:.3e}")) } else if p_value < 0.01 { - ("orange!30", format!("{:.3e}", p_value)) + ("orange!30", format!("{p_value:.3e}")) } else if p_value < 0.05 { - ("yellow!30", format!("{:.3e}", p_value)) + ("yellow!30", format!("{p_value:.3e}")) } else { - ("white", format!("{:.3e}", p_value)) + ("white", format!("{p_value:.3e}")) }; - latex_content.push_str(&format!(" & \\cellcolor{{{}}}{}", color, p_str)); + latex_content.push_str(&format!(" & \\cellcolor{{{color}}}{p_str}")); } else { // Lower triangle: Cohen's d effect sizes let effect_size = self.calculate_pairwise_cohens_d( &optimizer_results[opt_i], &optimizer_results[opt_j], ); - latex_content.push_str(&format!(" & {:.2e}", effect_size)); + latex_content.push_str(&format!(" & {effect_size:.2e}")); } } latex_content.push_str(" \\\\\n"); @@ -1033,7 +1009,7 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa let latex_path = latex_dir.join("statistical_pairwise_matrix.tex"); fs::write(&latex_path, latex_content) - .with_context(|| format!("Failed to write LaTeX file: {:?}", latex_path))?; + .with_context(|| format!("Failed to write LaTeX file: {latex_path:?}"))?; Ok(()) } @@ -1071,7 +1047,7 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa ) -> Result<()> { let latex_dir = Path::new(output_dir).join("latex"); fs::create_dir_all(&latex_dir) - .with_context(|| format!("Failed to create LaTeX directory: {:?}", latex_dir))?; + .with_context(|| format!("Failed to create LaTeX directory: {latex_dir:?}"))?; // Analyze each problem's difficulty metrics let mut problem_stats = Vec::new(); @@ -1210,7 +1186,7 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa let latex_path = latex_dir.join("problem_difficulty_ranking.tex"); fs::write(&latex_path, latex_content) - .with_context(|| format!("Failed to write LaTeX file: {:?}", latex_path))?; + .with_context(|| format!("Failed to write LaTeX file: {latex_path:?}"))?; Ok(()) } @@ -1252,16 +1228,22 @@ Matrix showing all comparisons. Green indicates QQN won (statistically significa /// Helper function to escape LaTeX special characters fn escape_latex(&self, text: &str) -> String { - text.replace('&', "\\&") - .replace('%', "\\%") - .replace('$', "\\$") - .replace('#', "\\#") - .replace('^', "\\textasciicircum{}") - .replace('_', "\\_") - .replace('{', "\\{") - .replace('}', "\\}") - .replace('~', "\\textasciitilde{}") - .replace('\\', "\\textbackslash{}") + // Handle each character individually to avoid replacement conflicts + text.chars() + .map(|c| match c { + '\\' => "\\textbackslash{}".to_string(), + '&' => "\\&".to_string(), + '%' => "\\%".to_string(), + '$' => "\\$".to_string(), + '#' => "\\#".to_string(), + '^' => "\\textasciicircum{}".to_string(), + '_' => "\\_".to_string(), + '{' => "\\{".to_string(), + '}' => "\\}".to_string(), + '~' => "\\textasciitilde{}".to_string(), + _ => c.to_string(), + }) + .collect() } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 7257d3da..51fa8102 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,18 @@ +#![allow(clippy::manual_clamp)] +#![allow(clippy::empty_line_after_doc_comments)] +#![allow(clippy::needless_range_loop)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] +#![allow(clippy::ptr_arg)] +#![allow(clippy::upper_case_acronyms)] +#![allow(clippy::module_inception)] +#![allow(clippy::doc_overindented_list_items)] +#![allow(clippy::doc_lazy_continuation)] +#![allow(clippy::legacy_numeric_constants)] +#![allow(clippy::never_loop)] +#![allow(dead_code)] +#![allow(unused_assignments)] + pub mod analysis; pub mod benchmarks; pub mod experiment_runner; diff --git a/src/line_search/backtracking.rs b/src/line_search/backtracking.rs index 53ee32ae..d94b1f40 100644 --- a/src/line_search/backtracking.rs +++ b/src/line_search/backtracking.rs @@ -259,10 +259,7 @@ impl LineSearch for BacktrackingLineSearch { /// 4. If α becomes smaller than min_step, try the minimum step /// 5. If max iterations reached, return the best point found /// 6. As a last resort, try machine epsilon step size - fn optimize_1d<'a>( - &mut self, - problem: &'a OneDimensionalProblem, - ) -> anyhow::Result { + fn optimize_1d(&mut self, problem: &OneDimensionalProblem) -> anyhow::Result { let f0 = (problem.objective)(0.0)?; let directional_derivative = problem.initial_directional_derivative; @@ -336,7 +333,6 @@ impl LineSearch for BacktrackingLineSearch { /// /// For backtracking line search, this is a no-op since the algorithm is stateless. /// Each call to `optimize_1d` is independent of previous calls. - fn reset(&mut self) { // Backtracking line search is stateless, nothing to reset } @@ -351,7 +347,6 @@ impl LineSearch for BacktrackingLineSearch { /// /// This allows users to access backtracking-specific methods like /// `set_initial_step` when they have a `Box`. - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } diff --git a/src/line_search/bisection.rs b/src/line_search/bisection.rs index 6c2f331a..5584d345 100644 --- a/src/line_search/bisection.rs +++ b/src/line_search/bisection.rs @@ -129,10 +129,9 @@ pub struct BisectionLineSearch { impl LineSearch for BisectionLineSearch { fn optimize_1d(&mut self, problem: &OneDimensionalProblem) -> anyhow::Result { let directional_derivative = problem.initial_directional_derivative; - self.log_verbose(&format!("Starting bisection line search")); + self.log_verbose("Starting bisection line search"); self.log_verbose(&format!( - "Initial directional derivative: {:.3e}", - directional_derivative + "Initial directional derivative: {directional_derivative:.3e}" )); if directional_derivative >= 0.0 { @@ -171,8 +170,7 @@ impl LineSearch for BisectionLineSearch { let grad_far = (problem.gradient)(far_point)?; self.log_verbose(&format!( - "Bracket: grad(0)={:.3e}, grad({:.3e})={:.3e}", - grad_0, far_point, grad_far + "Bracket: grad(0)={grad_0:.3e}, grad({far_point:.3e})={grad_far:.3e}" )); // Step 3: Perform bisection search for zero gradient @@ -184,9 +182,7 @@ impl LineSearch for BisectionLineSearch { let f0 = (problem.objective)(0.0)?; let f_far = (problem.objective)(far_point)?; if f_far < f0 { - self.log_verbose(&format!( - "No gradient sign change, but far point provides improvement" - )); + self.log_verbose("No gradient sign change, but far point provides improvement"); far_point } else { // Use a small step that provides some improvement @@ -209,8 +205,7 @@ impl LineSearch for BisectionLineSearch { if best_step > 0.0 { self.log_verbose(&format!( - "Found improvement with small step: {:.3e}", - best_step + "Found improvement with small step: {best_step:.3e}" )); best_step } else { @@ -290,7 +285,7 @@ impl BisectionLineSearch { /// Log line search details if verbose mode is enabled pub(crate) fn log_verbose(&self, message: &str) { if self.config.verbose { - debug!("Bisection: {}", message); + debug!("Bisection: {message}"); } } @@ -312,16 +307,14 @@ impl BisectionLineSearch { let mut b = right; self.log_verbose(&format!( - "Finding zero gradient in interval [{:.3e}, {:.3e}]", - a, b + "Finding zero gradient in interval [{a:.3e}, {b:.3e}]" )); // Verify we have a proper bracket with opposite gradient signs let grad_a = (problem.gradient)(a)?; let grad_b = (problem.gradient)(b)?; if grad_a * grad_b > 0.0 { self.log_verbose(&format!( - "Warning: gradients have same sign at endpoints: grad({:.3e})={:.3e}, grad({:.3e})={:.3e}", - a, grad_a, b, grad_b + "Warning: gradients have same sign at endpoints: grad({a:.3e})={grad_a:.3e}, grad({b:.3e})={grad_b:.3e}" )); // Return the point with smaller absolute gradient return Ok(if grad_a.abs() < grad_b.abs() { a } else { b }); @@ -332,17 +325,16 @@ impl BisectionLineSearch { // Evaluate gradient at midpoint let grad_mid = (problem.gradient)(mid)?; self.log_verbose(&format!( - " Line Search Iteration {}: mid={:.3e}, grad={:.3e}", - i, mid, grad_mid + " Line Search Iteration {i}: mid={mid:.3e}, grad={grad_mid:.3e}" )); // Check if gradient is close enough to zero if grad_mid.abs() <= self.config.gradient_tolerance { - self.log_verbose(&format!("Found zero gradient at alpha={:.3e}", mid)); + self.log_verbose(&format!("Found zero gradient at alpha={mid:.3e}")); return Ok(mid); } // Check if interval is too small if (b - a) < self.config.min_step { - self.log_verbose(&format!("Interval too small, returning mid={:.3e}", mid)); + self.log_verbose(&format!("Interval too small, returning mid={mid:.3e}")); return Ok(mid); } // Update interval based on sign of gradient @@ -358,8 +350,7 @@ impl BisectionLineSearch { // Return midpoint if max iterations reached let final_alpha = 0.5 * (a + b); self.log_verbose(&format!( - "Max iterations reached, returning alpha={:.3e}", - final_alpha + "Max iterations reached, returning alpha={final_alpha:.3e}" )); Ok(final_alpha) } @@ -389,23 +380,22 @@ pub(crate) fn find_far_point_1( ) -> anyhow::Result { let mut t = initial_step; let mut iteration = 0; - debug!("Finding far point starting from t={:.3e}", t); + debug!("Finding far point starting from t={t:.3e}"); while iteration < max_iterations { let f_t = (problem.objective)(t)?; let grad_t = (problem.gradient)(t)?; debug!( - " Line Search Iteration {}: t={:.3e}, f={:.3e}, grad={:.3e}, f0={:.3e}", - iteration, t, f_t, grad_t, f0 + " Line Search Iteration {iteration}: t={t:.3e}, f={f_t:.3e}, grad={grad_t:.3e}, f0={f0:.3e}" ); // Check if this point satisfies our far point criteria: // 1. Function value is still better than f(0) // 2. Gradient is positive (function is increasing) if f_t < f0 && grad_t > 0.0 { - debug!("Found far point at t={:.3e}", t); + debug!("Found far point at t={t:.3e}"); return Ok(t); } if f_t >= f0 { - debug!("Function value too high at t={:.3e}, reducing step", t); + debug!("Function value too high at t={t:.3e}, reducing step"); t *= 0.5; if t < min_step { debug!("Step size too small, using minimum step"); @@ -414,7 +404,7 @@ pub(crate) fn find_far_point_1( } // If gradient is still negative, step is too small else if grad_t < 0.0 { - debug!("Gradient still negative at t={:.3e}, increasing step", t); + debug!("Gradient still negative at t={t:.3e}, increasing step"); t *= 2.0; if t > max_step { debug!("Step size too large, using maximum step"); @@ -423,13 +413,13 @@ pub(crate) fn find_far_point_1( } // If gradient is approximately zero, we found our point else if grad_t.abs() <= gradient_tolerance { - debug!("Found zero gradient at t={:.3e}", t); + debug!("Found zero gradient at t={t:.3e}"); return Ok(t); } iteration += 1; } // If we can't find a proper far point, return the last valid step - debug!("Max iterations reached, returning t={:.3e}", t); + debug!("Max iterations reached, returning t={t:.3e}"); Ok(t) } @@ -453,26 +443,20 @@ pub(crate) fn find_far_point_2( ) -> anyhow::Result { let mut t = initial_steop; let mut iteration = 0; - debug!("Finding far point starting from t={:.3e}", t); + debug!("Finding far point starting from t={t:.3e}"); while iteration < max_iterations { let f_t = (problem.objective)(t)?; - debug!( - " Line Search Iteration {}: t={:.3e}, f={:.3e}, f0={:.3e}", - iteration, t, f_t, f0 - ); + debug!(" Line Search Iteration {iteration}: t={t:.3e}, f={f_t:.3e}, f0={f0:.3e}"); // Check if this point satisfies our far point criteria: // 1. Function value is worse than f(0) if f_t > f0 { - debug!("Found far point at t={:.3e}", t); + debug!("Found far point at t={t:.3e}"); return Ok(t); } // If function value is still better than f(0), increase step size if f_t <= f0 { - debug!( - "Function value still better at t={:.3e}, increasing step", - t - ); + debug!("Function value still better at t={t:.3e}, increasing step"); t *= 2.0; if t > max_step { debug!("Step size too large, using maximum step"); @@ -483,7 +467,7 @@ pub(crate) fn find_far_point_2( iteration += 1; } // If we can't find a proper far point, return the last valid step - debug!("Max iterations reached, returning t={:.3e}", t); + debug!("Max iterations reached, returning t={t:.3e}"); Ok(t) } diff --git a/src/line_search/cubic_quadratic.rs b/src/line_search/cubic_quadratic.rs index 88d9d7d9..3dd72ad1 100644 --- a/src/line_search/cubic_quadratic.rs +++ b/src/line_search/cubic_quadratic.rs @@ -167,7 +167,7 @@ impl CubicQuadraticLineSearch { /// Log a message if verbose mode is enabled fn log_verbose(&self, message: &str) { if self.config.verbose { - debug!("CubicQuadratic: {}", message); + debug!("CubicQuadratic: {message}"); } } @@ -348,8 +348,7 @@ impl LineSearch for CubicQuadraticLineSearch { let mut bracket_high = self.config.max_step; self.log_verbose(&format!( - "Starting with f(0)={:.3e}, g(0)={:.3e}, initial_step={:.3e}", - f0, g0, alpha + "Starting with f(0)={f0:.3e}, g(0)={g0:.3e}, initial_step={alpha:.3e}" )); for iter in 0..self.config.max_iterations { // Evaluate at current step @@ -362,8 +361,7 @@ impl LineSearch for CubicQuadraticLineSearch { } self.log_verbose(&format!( - "Line Search Iteration {}: alpha={:.3e}, f={:.3e}, g={:.3e}", - iter, alpha, f_alpha, g_alpha + "Line Search Iteration {iter}: alpha={alpha:.3e}, f={f_alpha:.3e}, g={g_alpha:.3e}" )); // Check Wolfe conditions let (armijo, curvature) = self.check_wolfe(f0, f_alpha, g_alpha, alpha, g0); @@ -409,7 +407,7 @@ impl LineSearch for CubicQuadraticLineSearch { .max(self.config.min_step) .min(self.config.max_step); - self.log_verbose(&format!("Interpolated new alpha: {:.3e}", alpha)); + self.log_verbose(&format!("Interpolated new alpha: {alpha:.3e}")); continue; } // If curvature condition fails but Armijo is satisfied @@ -421,7 +419,7 @@ impl LineSearch for CubicQuadraticLineSearch { g_prev = g_alpha; alpha = (alpha * self.config.extrapolation_factor).min(self.config.max_step); - self.log_verbose(&format!("Extrapolated for curvature: {:.3e}", alpha)); + self.log_verbose(&format!("Extrapolated for curvature: {alpha:.3e}")); continue; } } diff --git a/src/line_search/golden_section.rs b/src/line_search/golden_section.rs index afa5db02..564f8bd0 100644 --- a/src/line_search/golden_section.rs +++ b/src/line_search/golden_section.rs @@ -197,7 +197,7 @@ impl GoldenSectionLineSearch { } fn log_verbose(&self, message: &str) { if self.config.verbose { - debug!("GoldenSection: {}", message); + debug!("GoldenSection: {message}"); } } /// Golden ratio constant @@ -211,10 +211,7 @@ impl GoldenSectionLineSearch { fn find_minimum(&self, problem: &OneDimensionalProblem) -> anyhow::Result { // First, establish a proper bracket [a, b, c] where f(b) < f(a) and f(b) < f(c) let (a, b, c) = self.find_bracket(problem)?; - self.log_verbose(&format!( - "Initial bracket: [{:.6e}, {:.6e}, {:.6e}]", - a, b, c - )); + self.log_verbose(&format!("Initial bracket: [{a:.6e}, {b:.6e}, {c:.6e}]")); // Golden section search let mut left = a; let mut right = c; @@ -224,8 +221,7 @@ impl GoldenSectionLineSearch { let mut f2 = (problem.objective)(x2)?; for i in 0..self.config.max_iterations { self.log_verbose(&format!( - "Line Search Iteration {}: interval=[{:.3e}, {:.3e}], x1={:.3e}, x2={:.3e}, f1={:.3e}, f2={:.3e}", - i, left, right, x1, x2, f1, f2 + "Line Search Iteration {i}: interval=[{left:.3e}, {right:.3e}], x1={x1:.3e}, x2={x2:.3e}, f1={f1:.3e}, f2={f2:.3e}" )); if (right - left) < self.config.tolerance { break; @@ -247,7 +243,7 @@ impl GoldenSectionLineSearch { } } let final_x = if f1 < f2 { x1 } else { x2 }; - self.log_verbose(&format!("Golden section completed with x={:.3e}", final_x)); + self.log_verbose(&format!("Golden section completed with x={final_x:.3e}")); Ok(final_x) } diff --git a/src/line_search/line_search.rs b/src/line_search/line_search.rs index f0b3d701..1c2dbe44 100644 --- a/src/line_search/line_search.rs +++ b/src/line_search/line_search.rs @@ -69,8 +69,7 @@ pub fn create_1d_problem( .map_err(|e| anyhow!("Objective evaluation failed: {}", e))?; let initial_gradient = gradient_fn(&initial_position)?; // This is ∇f let initial_directional_derivative = dot_product_f64(&initial_gradient, &initial_direction)?; - debug!("create_1d_problem: initial_derivative={:?}, initial_direction={:?}, initial_directional_derivative={:.3e}", - initial_gradient, initial_direction, initial_directional_derivative); + debug!("create_1d_problem: initial_derivative={initial_gradient:?}, initial_direction={initial_direction:?}, initial_directional_derivative={initial_directional_derivative:.3e}"); // Check for zero direction let direction_norm = initial_direction.iter().map(|x| x * x).sum::().sqrt(); if direction_norm < 1e-16 { @@ -84,8 +83,7 @@ pub fn create_1d_problem( if initial_directional_derivative > 0.0 { // Warn and flip the direction of the gradient fn warn!( - "Initial directional derivative is positive ({:.3e}), flipping direction", - initial_directional_derivative + "Initial directional derivative is positive ({initial_directional_derivative:.3e}), flipping direction" ); let negative_gradient_fn = { let gradient_fn = gradient_fn.clone(); @@ -140,10 +138,7 @@ pub fn create_1d_problem( // Compute directional derivative: ∇f(x(t)) · dx/dt dot_product_f64(&g, &curve_derivative) })?; - debug!( - "1-D gradient result at t={:.3e}; p={:?} = {:.3e}", - t, result_vec, result - ); + debug!("1-D gradient result at t={t:.3e}; p={result_vec:?} = {result:.3e}"); Ok(result) }); Ok(OneDimensionalProblem::new( @@ -164,13 +159,9 @@ pub fn create_1d_problem_linear( // Debug: let's verify the curve works correctly let test_val_0 = curve.position(0.0)?; let test_val_1 = curve.position(1.0)?; - debug!( - "Curve test: f(t=0) -> {:?}, f(t=1) -> {:?}", - test_val_0, test_val_1 - ); + debug!("Curve test: f(t=0) -> {test_val_0:?}, f(t=1) -> {test_val_1:?}"); - let result = create_1d_problem(Box::new(curve), objective_fn, gradient_fn); - result + create_1d_problem(Box::new(curve), objective_fn, gradient_fn) } /// Linear parametric curve: x(t) = x0 + t * direction diff --git a/src/line_search/more_thuente.rs b/src/line_search/more_thuente.rs index 78ee9c6c..904ce584 100644 --- a/src/line_search/more_thuente.rs +++ b/src/line_search/more_thuente.rs @@ -215,7 +215,7 @@ impl MoreThuenteLineSearch { /// Uses the `log` crate's debug level for output fn log_verbose(&self, message: &str) { if self.config.verbose { - debug!("MoreThuente: {}", message); + debug!("MoreThuente: {message}"); } } /// Check strong Wolfe conditions @@ -367,39 +367,35 @@ impl MoreThuenteLineSearch { } else { stpq } + } else if (stp - stpc).abs() > (stp - stpq).abs() { + stpc } else { - if (stp - stpc).abs() > (stp - stpq).abs() { - stpc - } else { - stpq - } + stpq }; stpf.clamp(*bound.start(), *bound.end()) } // Case 4: Lower function value, derivatives have same sign, not decreasing - else { - if *brackt { - let theta = 3.0 * (fp - *fy) / (*sty - stp) + *gy + gp; - let s = theta.abs().max(gy.abs()).max(gp.abs()); - let discriminant = (theta / s).powi(2) - (*gy / s) * (gp / s); - let mut gamma = if discriminant > 0.0 { - s * discriminant.sqrt() - } else { - 0.0 - }; - if stp > *sty { - gamma = -gamma; - } - let p = (gamma - gp) + theta; - let q = ((gamma - gp) + gamma) + *gy; - let r = if q.abs() > EPSILON { p / q } else { 0.5 }; - let stpc = stp + r * (*sty - stp); - stpc.clamp(*bound.start(), *bound.end()) - } else if stp > *stx { - self.config.max_step + else if *brackt { + let theta = 3.0 * (fp - *fy) / (*sty - stp) + *gy + gp; + let s = theta.abs().max(gy.abs()).max(gp.abs()); + let discriminant = (theta / s).powi(2) - (*gy / s) * (gp / s); + let mut gamma = if discriminant > 0.0 { + s * discriminant.sqrt() } else { - self.config.min_step + 0.0 + }; + if stp > *sty { + gamma = -gamma; } + let p = (gamma - gp) + theta; + let q = ((gamma - gp) + gamma) + *gy; + let r = if q.abs() > EPSILON { p / q } else { 0.5 }; + let stpc = stp + r * (*sty - stp); + stpc.clamp(*bound.start(), *bound.end()) + } else if stp > *stx { + self.config.max_step + } else { + self.config.min_step } } /// Update the interval endpoints based on function values and gradients @@ -485,8 +481,7 @@ impl LineSearch for MoreThuenteLineSearch { let mut best_f = f0; self.log_verbose(&format!( - "Starting More-Thuente with f(0)={:.3e}, g(0)={:.3e}", - f0, g0 + "Starting More-Thuente with f(0)={f0:.3e}, g(0)={g0:.3e}" )); for iter in 0..self.config.max_iterations { @@ -504,10 +499,7 @@ impl LineSearch for MoreThuenteLineSearch { let gp = (problem.gradient)(stp)?; // Check for NaN or infinite values if !fp.is_finite() || !gp.is_finite() { - self.log_verbose(&format!( - "Non-finite values at step {}: f={}, g={}", - stp, fp, gp - )); + self.log_verbose(&format!("Non-finite values at step {stp}: f={fp}, g={gp}")); // Return best point found so far if best_stp > 0.0 && best_f < f0 { return Ok(LineSearchResult { @@ -526,8 +518,7 @@ impl LineSearch for MoreThuenteLineSearch { } self.log_verbose(&format!( - "Line Search Iteration {}: stp={:.3e}, f={:.3e}, g={:.3e}", - iter, stp, fp, gp + "Line Search Iteration {iter}: stp={stp:.3e}, f={fp:.3e}, g={gp:.3e}" )); // Check Wolfe conditions diff --git a/src/line_search/strong_wolfe.rs b/src/line_search/strong_wolfe.rs index e820cceb..64cc1d54 100644 --- a/src/line_search/strong_wolfe.rs +++ b/src/line_search/strong_wolfe.rs @@ -21,7 +21,6 @@ use serde::{Deserialize, Serialize}; /// - **Strong curvature**: |f'(αₖ)| ≤ c₂|f'(0)| /// /// where c₁ and c₂ are parameters with 0 < c₁ < c₂ < 1. - /// Configuration for Strong Wolfe line search #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StrongWolfeConfig { @@ -194,7 +193,7 @@ impl StrongWolfeLineSearch { /// Log line search details if verbose mode is enabled fn log_verbose(&self, message: &str) { if self.config.verbose { - debug!("StrongWolfe: {}", message); + debug!("StrongWolfe: {message}"); } } @@ -366,10 +365,9 @@ impl LineSearch for StrongWolfeLineSearch { let f0 = (problem.objective)(0.0)?; let directional_derivative = problem.initial_directional_derivative; - self.log_verbose(&format!("Starting 1D optimization with f(0)={:.3e}", f0)); + self.log_verbose(&format!("Starting 1D optimization with f(0)={f0:.3e}")); self.log_verbose(&format!( - "Directional derivative: {:.3e}", - directional_derivative + "Directional derivative: {directional_derivative:.3e}" )); if directional_derivative >= 0.0 { @@ -382,17 +380,16 @@ impl LineSearch for StrongWolfeLineSearch { let mut best_alpha = 0.0; let mut best_f = f0; - self.log_verbose(&format!("Initial step size: {:.3e}", alpha)); + self.log_verbose(&format!("Initial step size: {alpha:.3e}")); for i in 0..self.config.max_iterations { self.log_verbose(&format!( - "Line Search Iteration {}: trying alpha={:.3e}", - i, alpha + "Line Search Iteration {i}: trying alpha={alpha:.3e}" )); // Evaluate function at current step size let f_alpha = (problem.objective)(alpha)?; - self.log_verbose(&format!(" f({:.3e}) = {:.3e}", alpha, f_alpha)); + self.log_verbose(&format!(" f({alpha:.3e}) = {f_alpha:.3e}")); // Track best point found if f_alpha < best_f { best_f = f_alpha; @@ -404,13 +401,12 @@ impl LineSearch for StrongWolfeLineSearch { || (i > 0 && f_alpha >= f_prev) { self.log_verbose(&format!( - " Armijo failed or insufficient decrease, zooming between {:.3e} and {:.3e}", - alpha_prev, alpha + " Armijo failed or insufficient decrease, zooming between {alpha_prev:.3e} and {alpha:.3e}" )); // Zoom between alpha_prev and alpha let final_alpha = - self.zoom(alpha_prev, alpha, f0, directional_derivative, &problem)?; - self.log_verbose(&format!("Zoom completed with alpha={:.3e}", final_alpha)); + self.zoom(alpha_prev, alpha, f0, directional_derivative, problem)?; + self.log_verbose(&format!("Zoom completed with alpha={final_alpha:.3e}")); return Ok(LineSearchResult { step_size: final_alpha, @@ -425,8 +421,7 @@ impl LineSearch for StrongWolfeLineSearch { // Check curvature condition if self.curvature_condition(grad_alpha, directional_derivative) { self.log_verbose(&format!( - "Both Wolfe conditions satisfied at alpha={:.3e}", - alpha + "Both Wolfe conditions satisfied at alpha={alpha:.3e}" )); return Ok(LineSearchResult { step_size: alpha, @@ -438,11 +433,10 @@ impl LineSearch for StrongWolfeLineSearch { // Check if gradient indicates we should look further if grad_alpha >= 0.0 { self.log_verbose(&format!( - " Gradient indicates overshoot, zooming between {:.3e} and {:.3e}", - alpha, alpha_prev + " Gradient indicates overshoot, zooming between {alpha:.3e} and {alpha_prev:.3e}" )); let final_alpha = - self.zoom(alpha, alpha_prev, f0, directional_derivative, &problem)?; + self.zoom(alpha, alpha_prev, f0, directional_derivative, problem)?; return Ok(LineSearchResult { step_size: final_alpha, @@ -458,8 +452,7 @@ impl LineSearch for StrongWolfeLineSearch { // If we have any improvement, use it if best_alpha > 0.0 && best_f < f0 { self.log_verbose(&format!( - "Returning best point found: alpha={:.3e}, f={:.3e}", - best_alpha, best_f + "Returning best point found: alpha={best_alpha:.3e}, f={best_f:.3e}" )); return Ok(LineSearchResult { step_size: best_alpha, @@ -472,7 +465,7 @@ impl LineSearch for StrongWolfeLineSearch { let eps_step = f64::EPSILON.sqrt(); let f_eps = (problem.objective)(eps_step)?; if f_eps < f0 { - self.log_verbose(&format!("Using machine epsilon step {:.3e}", eps_step)); + self.log_verbose(&format!("Using machine epsilon step {eps_step:.3e}")); return Ok(LineSearchResult { step_size: eps_step, success: true, diff --git a/src/optimizers/adam.rs b/src/optimizers/adam.rs index 5599df8a..d51cd720 100644 --- a/src/optimizers/adam.rs +++ b/src/optimizers/adam.rs @@ -126,7 +126,7 @@ pub struct AdamConfig { /// **Effect:** /// - Higher values: More stable adaptive learning rates, slower adaptation /// - Lower values: Faster adaptation to gradient changes, potentially less stable - /// **Critical:** This parameter significantly affects convergence behavior + /// **Critical:** This parameter significantly affects convergence behavior pub beta2: f64, /// Small constant added to denominator for numerical stability @@ -319,6 +319,12 @@ pub struct AdamState { pub v_max: Option>, } +impl Default for AdamState { + fn default() -> Self { + Self::new() + } +} + impl AdamState { /// Create a new Adam state with default initialization. /// @@ -430,7 +436,7 @@ impl AdamOptimizer { if !self.config.verbose { return; } - debug!("=== Adam: {} ===", name); + debug!("=== Adam: {name} ==="); for (i, tensor) in tensors.iter().enumerate() { match tensor.flatten_all().and_then(|t| t.to_vec1::()) { Ok(values) => { @@ -441,7 +447,7 @@ impl AdamOptimizer { values.len() ); if values.len() <= 10 { - debug!(" Full data: {:?}", values); + debug!(" Full data: {values:?}"); } else { debug!( " First 5: {:?}, Last 5: {:?}", @@ -478,7 +484,7 @@ impl AdamOptimizer { /// Log scalar value if verbose mode is enabled fn log_scalar(&self, name: &str, value: f64) { if self.config.verbose { - debug!(" Adam {}: {:.12e}", name, value); + debug!(" Adam {name}: {value:.12e}"); } } @@ -668,13 +674,7 @@ impl AdamOptimizer { if self.config.verbose && (grad_converged || func_converged) { debug!( - "Convergence check: grad_norm={:.6e} < {:.6e} = {}, func_change={:?} < {:.6e} = {}", - gradient_norm, - grad_tolerance, - grad_converged, - function_change, - func_tolerance, - func_converged + "Convergence check: grad_norm={gradient_norm:.6e} < {grad_tolerance:.6e} = {grad_converged}, func_change={function_change:?} < {func_tolerance:.6e} = {func_converged}" ); } @@ -768,8 +768,7 @@ impl Optimizer for AdamOptimizer { let param_vec = param.flatten_all()?.to_vec1::()?; if param_vec.iter().any(|&x| !x.is_finite()) { return Err(candle_core::Error::Msg(format!( - "Non-finite parameter detected at index {} after update", - i + "Non-finite parameter detected at index {i} after update" ))); } } @@ -783,13 +782,13 @@ impl Optimizer for AdamOptimizer { if self.config.verbose { debug!("=== Adam Step {} Completed ===", self.state.iteration - 1); - debug!(" Step Duration: {:?}", step_duration); + debug!(" Step Duration: {step_duration:?}"); debug!(" Converged: {}", convergence_info.converged); debug!(" Current LR: {:.6e}", self.current_lr); - debug!(" Line Search Alpha: {:.3}", step_size); - debug!(" Function Value: {:.6e}", current_value); + debug!(" Line Search Alpha: {step_size:.3}"); + debug!(" Function Value: {current_value:.6e}"); if let Some(change) = function_change { - debug!(" Function Change: {:.6e}", change); + debug!(" Function Change: {change:.6e}"); } } diff --git a/src/optimizers/gd.rs b/src/optimizers/gd.rs index 26d6a45e..25de57f6 100644 --- a/src/optimizers/gd.rs +++ b/src/optimizers/gd.rs @@ -308,6 +308,12 @@ pub struct GDState { pub momentum_buffer: Option>, } +impl Default for GDState { + fn default() -> Self { + Self::new() + } +} + impl GDState { /// Create a new GD state. /// @@ -419,7 +425,7 @@ impl GDOptimizer { if !self.config.verbose { return; } - debug!("=== GD: {} ===", name); + debug!("=== GD: {name} ==="); for (i, tensor) in tensors.iter().enumerate() { match tensor.flatten_all().and_then(|t| t.to_vec1::()) { Ok(values) => { @@ -430,7 +436,7 @@ impl GDOptimizer { values.len() ); if values.len() <= 10 { - debug!(" Full data: {:?}", values); + debug!(" Full data: {values:?}"); } else { debug!( " First 5: {:?}, Last 5: {:?}", @@ -467,7 +473,7 @@ impl GDOptimizer { /// Log scalar value if verbose mode is enabled fn log_scalar(&self, name: &str, value: f64) { if self.config.verbose { - debug!(" GD {}: {:.12e}", name, value); + debug!(" GD {name}: {value:.12e}"); } } @@ -572,7 +578,7 @@ impl GDOptimizer { let base_tolerance = 1e-4; // Less strict base tolerance // Scale tolerance based on problem characteristics - let lr_factor = (self.config.learning_rate / 0.01).max(0.1).min(10.0); + let lr_factor = (self.config.learning_rate / 0.01).clamp(0.1, 10.0); let momentum_factor = if self.config.momentum > 0.0 { 0.8 // Less aggressive scaling for momentum } else { @@ -677,7 +683,7 @@ impl Optimizer for GDOptimizer { crate::utils::math::compute_magnitude(&changes)? }; if self.config.verbose { - debug!("Parameter change norm: {:.6e}", param_change_norm); + debug!("Parameter change norm: {param_change_norm:.6e}"); } // Check for NaN/Inf in updated parameters @@ -685,8 +691,7 @@ impl Optimizer for GDOptimizer { let param_vec = param.flatten_all()?.to_vec1::()?; if param_vec.iter().any(|&x| !x.is_finite()) { return Err(candle_core::Error::Msg(format!( - "Non-finite parameter detected at index {} after update", - i + "Non-finite parameter detected at index {i} after update" ))); } } @@ -700,7 +705,7 @@ impl Optimizer for GDOptimizer { if self.config.verbose { debug!("=== GD Step {} Completed ===", self.state.iteration - 1); - debug!(" Step Duration: {:?}", step_duration); + debug!(" Step Duration: {step_duration:?}"); debug!(" Converged: {}", convergence_info.converged); } @@ -734,7 +739,7 @@ impl Optimizer for GDOptimizer { if grad_norm > 100.0 { grad_norm * 1e-6 } else { - 1e-4 * (self.config.learning_rate / 0.01).max(0.1).min(10.0) + 1e-4 * (self.config.learning_rate / 0.01).clamp(0.1, 10.0) } }); diff --git a/src/optimizers/lbfgs.rs b/src/optimizers/lbfgs.rs index 995f4ae8..1e415ce3 100644 --- a/src/optimizers/lbfgs.rs +++ b/src/optimizers/lbfgs.rs @@ -453,44 +453,38 @@ impl LBFGSState { // Check gradient magnitude to avoid numerical issues let grad_norm = compute_magnitude(gradient)?; if grad_norm < self.epsilon { - debug!( - "L-BFGS: Very small gradient norm {:.6e}, using steepest descent", - grad_norm - ); - return Ok(gradient + debug!("L-BFGS: Very small gradient norm {grad_norm:.6e}, using steepest descent"); + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } // Check for extremely large gradients if grad_norm > self.max_gradient_norm { - warn!( - "L-BFGS: Extremely large gradient norm {:.6e}, scaling down", - grad_norm - ); + warn!("L-BFGS: Extremely large gradient norm {grad_norm:.6e}, scaling down"); let scale = self.max_gradient_norm / grad_norm; - return Ok(gradient + return gradient .iter() .map(|g| g.affine(-scale, 0.0)) - .collect::>>()?); + .collect::>>(); } // Check for NaN/Inf in gradient if !self.check_finite_tensors(gradient, "gradient")? { warn!("L-BFGS: Non-finite gradient detected, using steepest descent"); - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } } if self.s_history.is_empty() { debug!("L-BFGS: No history, using steepest descent"); - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } let mut q = gradient.to_vec(); @@ -502,17 +496,14 @@ impl LBFGSState { let rho_i = self.rho_history[i]; // Check for numerical issues if !rho_i.is_finite() || rho_i.abs() < 1e-16 { - warn!( - "L-BFGS: Skipping history pair {} due to numerical issues (rho={})", - i, rho_i - ); + warn!("L-BFGS: Skipping history pair {i} due to numerical issues (rho={rho_i})"); alpha.push(0.0); // Push zero alpha to maintain indexing continue; } let alpha_i = rho_i * dot_product(s_i, &q)?; if !alpha_i.is_finite() { - warn!("L-BFGS: Non-finite alpha detected at iteration {}", i); + warn!("L-BFGS: Non-finite alpha detected at iteration {i}"); alpha.push(0.0); // Push zero alpha to maintain indexing continue; } @@ -527,10 +518,10 @@ impl LBFGSState { if !self.disable_checks { // Check if q has become non-finite if !self.check_finite_tensors(&q, "q (first loop)")? { - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } } } @@ -567,7 +558,7 @@ impl LBFGSState { let beta = rho_i * dot_product(y_i, &r)?; let correction_factor = alpha_i - beta; if !correction_factor.is_finite() { - warn!("L-BFGS: Non-finite correction factor at iteration {}", i); + warn!("L-BFGS: Non-finite correction factor at iteration {i}"); continue; } @@ -578,10 +569,10 @@ impl LBFGSState { if !self.disable_checks { // Check if r has become non-finite if !self.check_finite_tensors(&r, "r (second loop)")? { - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } } } @@ -596,10 +587,10 @@ impl LBFGSState { // Final check on the direction // Verify the direction is finite if !self.check_finite_tensors(&direction, "final direction")? { - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } } @@ -617,21 +608,20 @@ impl LBFGSState { let grad_norm = compute_magnitude(gradient)?; if grad_norm < self.epsilon { debug!( - "L-BFGS: Very small gradient norm {:.6e}, returning negative gradient", - grad_norm + "L-BFGS: Very small gradient norm {grad_norm:.6e}, returning negative gradient" ); - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } } if self.s_history.is_empty() { debug!("L-BFGS: No history, returning negative gradient"); - return Ok(gradient + return gradient .iter() .map(|g| g.neg()) - .collect::>>()?); + .collect::>>(); } let mut q = gradient.to_vec(); let mut alpha = Vec::with_capacity(self.s_history.len()); @@ -640,16 +630,13 @@ impl LBFGSState { let s_i = &self.s_history[i]; let rho_i = self.rho_history[i]; if !rho_i.is_finite() || rho_i.abs() < 1e-16 { - warn!( - "L-BFGS: Skipping history pair {} due to numerical issues (rho={})", - i, rho_i - ); + warn!("L-BFGS: Skipping history pair {i} due to numerical issues (rho={rho_i})"); alpha.push(0.0); continue; } let alpha_i = rho_i * dot_product(s_i, &q)?; if !alpha_i.is_finite() { - warn!("L-BFGS: Non-finite alpha detected at iteration {}", i); + warn!("L-BFGS: Non-finite alpha detected at iteration {i}"); alpha.push(0.0); continue; } @@ -681,7 +668,7 @@ impl LBFGSState { let beta = rho_i * dot_product(y_i, &r)?; let correction_factor = alpha_i - beta; if !correction_factor.is_finite() { - warn!("L-BFGS: Non-finite correction factor at iteration {}", i); + warn!("L-BFGS: Non-finite correction factor at iteration {i}"); continue; } // r = r + (alpha_i - beta) * s_i @@ -733,10 +720,7 @@ impl LBFGSState { let s_k_norm = compute_magnitude(&s_k)?; // Use epsilon-based threshold for consistency if s_k_norm < self.epsilon { - debug!( - "L-BFGS: Parameter change too small ({:.6e}), skipping update", - s_k_norm - ); + debug!("L-BFGS: Parameter change too small ({s_k_norm:.6e}), skipping update"); // Still update the previous gradient for next iteration self.prev_gradient = Some(new_gradient.to_vec()); return Ok(()); @@ -779,7 +763,7 @@ impl LBFGSState { }; if theta < 1.0 { - debug!("L-BFGS: Applying Powell damping with theta = {:.6e}", theta); + debug!("L-BFGS: Applying Powell damping with theta = {theta:.6e}"); // y_k_damped = theta * y_k + (1 - theta) * B_k * s_k // For simplicity, we'll use a scaled identity approximation for B_k let scaled_s = vector_scale(&s_k, self.gamma)?; @@ -825,20 +809,18 @@ impl LBFGSState { // Ensure gamma is finite before updating if new_gamma.is_finite() && new_gamma > 0.0 { // Less conservative gamma clamping for better performance - self.gamma = new_gamma.max(1e-8).min(1e8); + self.gamma = new_gamma.clamp(1e-8, 1e8); if (new_gamma - self.gamma).abs() > 1e-10 { debug!("L-BFGS: Gamma clamped from {} to {}", new_gamma, self.gamma); } } else { debug!( - "L-BFGS: Invalid gamma computed: {}, keeping current value", - new_gamma + "L-BFGS: Invalid gamma computed: {new_gamma}, keeping current value" ); } } } else { - debug!("L-BFGS: Curvature condition not satisfied even after damping (s_dot_y = {:.6e}, threshold = {:.6e}), skipping update", - s_dot_y_final, curvature_threshold); + debug!("L-BFGS: Curvature condition not satisfied even after damping (s_dot_y = {s_dot_y_final:.6e}, threshold = {curvature_threshold:.6e}), skipping update"); } } @@ -915,7 +897,7 @@ impl LBFGSState { for (i, tensor) in tensors.iter().enumerate() { let values = tensor.flatten_all()?.to_vec1::()?; if values.iter().any(|&x| !x.is_finite()) { - warn!("L-BFGS: Non-finite {} detected at index {}", context, i); + warn!("L-BFGS: Non-finite {context} detected at index {i}"); return Ok(false); } } @@ -964,13 +946,13 @@ impl LBFGSOptimizer { if !self.config.verbose { return; } - debug!("=== L-BFGS: {} ===", name); + debug!("=== L-BFGS: {name} ==="); log_tensor(tensors); } /// Log scalar value if verbose mode is enabled fn log_scalar(&self, name: &str, value: f64) { if self.config.verbose { - debug!(" L-BFGS {}: {:.12e}", name, value); + debug!(" L-BFGS {name}: {value:.12e}"); } } /// Log L-BFGS state if verbose mode is enabled @@ -982,7 +964,7 @@ impl LBFGSOptimizer { debug!(" Iteration: {}", self.state.iteration()); debug!(" History Length: {}", self.state.history_length()); debug!(" Gamma: {:.6e}", self.state.gamma()); - debug!(" Additional Info: {}", additional_info); + debug!(" Additional Info: {additional_info}"); } /// Get a reference to the internal L-BFGS state. @@ -1065,8 +1047,7 @@ impl Optimizer for LBFGSOptimizer { if !direction_norm.is_finite() || direction_norm < self.config.epsilon { warn!( - "L-BFGS: Invalid search direction norm: {}, using steepest descent", - direction_norm + "L-BFGS: Invalid search direction norm: {direction_norm}, using steepest descent" ); // Fall back to steepest descent let search_direction = gradients @@ -1140,7 +1121,7 @@ impl Optimizer for LBFGSOptimizer { } else { 1.0 }; - initial_step.max(1e-4).min(10.0) + initial_step.clamp(1e-4, 10.0) } else { // Subsequent iterations: use gamma-based scaling let dir_norm = compute_magnitude(&search_direction)?; @@ -1154,15 +1135,14 @@ impl Optimizer for LBFGSOptimizer { self.config.min_step_size } }; - debug!("L-BFGS: Initial step size = {:.6e}", step_size); + debug!("L-BFGS: Initial step size = {step_size:.6e}"); // Use the configured line search let mut line_search = self.line_search.clone_box(); // Create a more conservative line search configuration for problematic cases if grad_norm > 1e6 || direction_norm > 1e6 { - warn!("L-BFGS: Large gradients detected (grad_norm={:.2e}, dir_norm={:.2e}), using very conservative step size", - grad_norm, direction_norm); + warn!("L-BFGS: Large gradients detected (grad_norm={grad_norm:.2e}, dir_norm={direction_norm:.2e}), using very conservative step size"); // For very large gradients, use an extremely conservative fixed step - let conservative_step = (1e-6 / (grad_norm + 1.0)).max(1e-12).min(1e-6); + let conservative_step = (1e-6 / (grad_norm + 1.0)).clamp(1e-12, 1e-6); // Update parameters with conservative step let old_params = params.to_vec(); for (param, direction) in params.iter_mut().zip(&search_direction) { @@ -1221,11 +1201,11 @@ impl Optimizer for LBFGSOptimizer { Arc::new(objective_fn), Arc::new(gradient_fn), ) - .map_err(|e| candle_core::Error::Msg(format!("Failed to create 1D problem: {}", e)))?; + .map_err(|e| candle_core::Error::Msg(format!("Failed to create 1D problem: {e}")))?; // Perform line search line_search .optimize_1d(&problem) - .map_err(|e| candle_core::Error::Msg(format!("Line search failed: {}", e)))? + .map_err(|e| candle_core::Error::Msg(format!("Line search failed: {e}")))? }; if self.config.verbose { @@ -1328,9 +1308,7 @@ impl Optimizer for LBFGSOptimizer { .collect::>>()? .into_iter() .fold(0.0_f64, |a, b| a.max(b)); - self.state.gamma = (1.0 / (grad_norm / param_scale.max(1.0))) - .max(0.1) - .min(10.0); + self.state.gamma = (1.0 / (grad_norm / param_scale.max(1.0))).clamp(0.1, 10.0); self.state.no_improvement_count = 0; debug!( "L-BFGS: Recovery triggered, new gamma = {:.6e}", @@ -1350,7 +1328,7 @@ impl Optimizer for LBFGSOptimizer { "=== L-BFGS Step {} Completed ===", self.state.iteration() - 1 ); - debug!(" Step Duration: {:?}", step_duration); + debug!(" Step Duration: {step_duration:?}"); debug!(" Converged: {}", convergence_info.converged); } diff --git a/src/optimizers/optimizer.rs b/src/optimizers/optimizer.rs index 39e05bb2..57b6250b 100644 --- a/src/optimizers/optimizer.rs +++ b/src/optimizers/optimizer.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use std::time::Duration; /// Additional metadata that optimizers can provide -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OptimizationMetadata { /// Optimizer-specific data (e.g., QQN magnitude ratios, L-BFGS curvature info) pub optimizer_data: std::collections::HashMap, @@ -22,15 +22,6 @@ pub struct OptimizationMetadata { /// Memory usage information pub memory_info: MemoryInfo, } -impl Default for OptimizationMetadata { - fn default() -> Self { - Self { - optimizer_data: std::collections::HashMap::new(), - timing_info: TimingInfo::default(), - memory_info: MemoryInfo::default(), - } - } -} /// Core trait that all optimization algorithms must implement. /// /// This trait provides a unified interface for different optimization methods, @@ -40,7 +31,7 @@ pub trait Optimizer: Send + Sync + Debug + 'static { fn clone_box(&self) -> Box; /// Get optimizer configuration as a string for serialization fn config_string(&self) -> String { - format!("{:?}", self) + format!("{self:?}") } /// Perform a single optimization step using a differentiable function @@ -99,7 +90,7 @@ pub struct StepResult { } /// Information about convergence status and criteria -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ConvergenceInfo { /// Whether the optimizer has converged pub converged: bool, @@ -108,15 +99,6 @@ pub struct ConvergenceInfo { pub function_change: Option, } -impl Default for ConvergenceInfo { - fn default() -> Self { - Self { - converged: false, - function_change: None, - } - } -} - impl ConvergenceInfo { /// Create convergence info indicating convergence pub fn converged() -> Self { @@ -179,7 +161,7 @@ impl Default for TimingInfo { } /// Memory usage information -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct MemoryInfo { /// Peak memory usage during this step (in bytes) pub peak_memory: Option, @@ -191,16 +173,6 @@ pub struct MemoryInfo { pub temp_memory: Option, } -impl Default for MemoryInfo { - fn default() -> Self { - Self { - peak_memory: None, - state_memory: None, - temp_memory: None, - } - } -} - /// Configuration for convergence checking #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConvergenceConfig { diff --git a/src/optimizers/qqn.rs b/src/optimizers/qqn.rs index 27784bae..64651b4c 100644 --- a/src/optimizers/qqn.rs +++ b/src/optimizers/qqn.rs @@ -172,14 +172,14 @@ impl QQNOptimizer { if !self.config.verbose { return; } - debug!("=== QQN: {} ===", name); + debug!("=== QQN: {name} ==="); log_tensor(tensors); } /// Log scalar value if verbose mode is enabled fn log_scalar(&self, name: &str, value: f64) { if self.config.verbose { - debug!(" {}: {:.3e}", name, value); + debug!(" {name}: {value:.3e}"); } } @@ -188,13 +188,13 @@ impl QQNOptimizer { if !self.config.verbose { return; } - debug!("=== QQN Optimization State (Iteration {}) ===", iteration); + debug!("=== QQN Optimization State (Iteration {iteration}) ==="); debug!( " L-BFGS History Length: {}", self.state.lbfgs_state.history_length() ); debug!(" L-BFGS Gamma: {:.6e}", self.state.lbfgs_state.gamma()); - debug!(" Additional Info: {}", additional_info); + debug!(" Additional Info: {additional_info}"); } /// Log line search details if verbose mode is enabled @@ -203,7 +203,7 @@ impl QQNOptimizer { return; } debug!("=== Line Search Results ==="); - debug!(" Optimal t: {:.3e}", optimal_t); + debug!(" Optimal t: {optimal_t:.3e}"); } pub fn create_quadratic_path( @@ -245,8 +245,7 @@ impl QQNOptimizer { for (i, tensor) in start_point.iter().enumerate() { if tensor.elem_count() == 0 { return Err(Error::Msg(format!( - "Empty tensor at index {} in start_point", - i + "Empty tensor at index {i} in start_point" ))); } } @@ -264,8 +263,7 @@ impl QQNOptimizer { let grad_norm = compute_magnitude(&negative_gradient)?; let lbfgs_norm = compute_magnitude(lbfgs_direction)?; debug!( - "Quadratic path created: ||gradient||={:.3e}, ||lbfgs_dir||={:.3e}", - grad_norm, lbfgs_norm + "Quadratic path created: ||gradient||={grad_norm:.3e}, ||lbfgs_dir||={lbfgs_norm:.3e}" ); self.log_scalar("Gradient Norm", grad_norm); self.log_scalar("L-BFGS Direction Norm", lbfgs_norm); @@ -324,7 +322,7 @@ impl QQNOptimizer { Arc::new(value_fn), Arc::new(gradient_fn), ) - .map_err(|e| Error::Msg(format!("Failed to create 1D problem: {}", e))); + .map_err(|e| Error::Msg(format!("Failed to create 1D problem: {e}"))); if problem.is_err() { warn!( "Failed to create 1D problem for line search: {}", @@ -338,7 +336,7 @@ impl QQNOptimizer { // Perform line search let mut line_search: Box = self.line_search.clone_box(); let result = line_search.optimize_1d(&problem?).unwrap_or_else(|e| { - warn!("Line search failed: {}", e); + warn!("Line search failed: {e}"); LineSearchResult { step_size: 1.0, // Default to 1.0 if search fails success: false, @@ -360,7 +358,7 @@ impl QQNOptimizer { function: Arc, reason: &str, ) -> CandleResult { - info!("Using steepest descent: {}", reason); + info!("Using steepest descent: {reason}"); // Check for convergence before attempting steepest descent let grad_norm = compute_magnitude(gradients)?; if grad_norm < self.config.epsilon { @@ -388,10 +386,7 @@ impl QQNOptimizer { // Evaluate function at current parameters to check for increasing steps let initial_function_value = function.evaluate(nd_params)?; - debug!( - "Initial function value (steepest descent): {:.6e}", - initial_function_value - ); + debug!("Initial function value (steepest descent): {initial_function_value:.6e}"); // Create steepest descent direction (negative gradient) with scaling factor // This allows line search to explore larger steps while operating in [0,1] @@ -404,10 +399,7 @@ impl QQNOptimizer { // Check if direction is essentially zero (this should be caught above, but double-check) let direction_norm = compute_magnitude(&direction)?; if direction_norm < self.config.epsilon { - warn!( - "Direction norm {:.3e} is too small, indicating convergence", - direction_norm - ); + warn!("Direction norm {direction_norm:.3e} is too small, indicating convergence"); return Ok(StepResult { step_size: 0.0, convergence_info: ConvergenceInfo { @@ -502,12 +494,12 @@ impl QQNOptimizer { Arc::new(objective_fn), Arc::new(gradient_fn), ) - .map_err(|e| Error::Msg(format!("Failed to create 1D problem: {}", e)))?; + .map_err(|e| Error::Msg(format!("Failed to create 1D problem: {e}")))?; // Perform line search self.line_search.optimize_1d(&problem).map_err(|e| { - warn!("Line search failed: {}", e); - Error::Msg(format!("Line search failed: {}", e)) + warn!("Line search failed: {e}"); + Error::Msg(format!("Line search failed: {e}")) }) }; @@ -548,26 +540,18 @@ impl QQNOptimizer { // FATAL ERROR CHECK: Verify that the steepest descent step decreased the function value let final_function_value = function.evaluate(nd_params)?; - debug!( - "Final function value (steepest descent): {:.6e}", - final_function_value - ); + debug!("Final function value (steepest descent): {final_function_value:.6e}"); if final_function_value > initial_function_value { let increase = final_function_value - initial_function_value; error!( - "FATAL ERROR: Steepest descent step increased function value by {:.6e} (from {:.6e} to {:.6e}). This should never happen!", - increase, initial_function_value, final_function_value + "FATAL ERROR: Steepest descent step increased function value by {increase:.6e} (from {initial_function_value:.6e} to {final_function_value:.6e}). This should never happen!" ); return Err(Error::Msg(format!( - "FATAL ERROR: Steepest descent step increased function value by {:.6e} (from {:.6e} to {:.6e}). This violates the descent property and should never happen.", - increase, initial_function_value, final_function_value + "FATAL ERROR: Steepest descent step increased function value by {increase:.6e} (from {initial_function_value:.6e} to {final_function_value:.6e}). This violates the descent property and should never happen." ))); } let function_decrease = initial_function_value - final_function_value; - debug!( - "Function decreased by (steepest descent): {:.6e}", - function_decrease - ); + debug!("Function decreased by (steepest descent): {function_decrease:.6e}"); self.log_scalar("Function Decrease (Steepest Descent)", function_decrease); // Update L-BFGS state with the new gradient at the updated position @@ -625,7 +609,7 @@ impl QQNOptimizer { }) } - fn is_all_finite(tensor_vec: &Vec) -> bool { + fn is_all_finite(tensor_vec: &[Tensor]) -> bool { tensor_vec.iter().all(|d| { d.flatten_all() .and_then(|f| f.to_vec1::()) @@ -676,7 +660,7 @@ impl Optimizer for QQNOptimizer { self.log_tensor_data("Initial Parameters", params); let initial_function_value = function.evaluate(params)?; - debug!("Initial function value: {:.6e}", initial_function_value); + debug!("Initial function value: {initial_function_value:.6e}"); let initial_gradients = function.gradient(params)?; self.log_tensor_data("Computed Gradients", &initial_gradients); // Check for convergence based on gradient norm @@ -709,8 +693,7 @@ impl Optimizer for QQNOptimizer { let grad_vec = grad.flatten_all()?.to_vec1::()?; if grad_vec.iter().any(|&x| !x.is_finite()) { return Err(Error::Msg(format!( - "Non-finite gradient detected at index {}", - i + "Non-finite gradient detected at index {i}" ))); } } @@ -732,7 +715,7 @@ impl Optimizer for QQNOptimizer { let new_gradient = function.gradient(params)?; self.state .lbfgs_state - .update(¶ms, params, &new_gradient)?; + .update(params, params, &new_gradient)?; return Ok(result); } @@ -756,10 +739,7 @@ impl Optimizer for QQNOptimizer { return Ok(result); } - debug!( - "L-BFGS direction computed successfully: {:?}->{:?}", - params, lbfgs_direction - ); + debug!("L-BFGS direction computed successfully: {params:?}->{lbfgs_direction:?}"); let quadratic_path = self.create_quadratic_path( params, &initial_gradients, @@ -768,10 +748,7 @@ impl Optimizer for QQNOptimizer { )?; // Configure line search with previous step size if available if let Some(prev_step) = self.state.previous_step_size { - debug!( - "Using previous step size {:.3e} as initial step for line search", - prev_step - ); + debug!("Using previous step size {prev_step:.3e} as initial step for line search"); self.set_initial_step(prev_step); } let line_search_result = self.find_optimal_t_line_search(quadratic_path.clone()); @@ -810,7 +787,7 @@ impl Optimizer for QQNOptimizer { ); let grad_norm = compute_magnitude(&initial_gradients)?; if grad_norm < 1e-3 { - info!("Converged with small gradient norm {:.3e}", grad_norm); + info!("Converged with small gradient norm {grad_norm:.3e}"); self.state.iteration += 1; return Ok(StepResult { step_size: line_search_result.step_size, @@ -829,7 +806,7 @@ impl Optimizer for QQNOptimizer { if line_search_result.step_size > self.config.min_step_persist { let step_size = line_search_result.step_size; self.state.previous_step_size = Some(step_size); - debug!("Persisted step size {:.3e} for next iteration", step_size); + debug!("Persisted step size {step_size:.3e} for next iteration"); } else { debug!( "Line search returned step size {:.3e}, below persistence threshold", @@ -850,7 +827,7 @@ impl Optimizer for QQNOptimizer { } // Calculate function decrease before L-BFGS update let final_function_value = function.evaluate(params)?; - debug!("Final function value: {:.6e}", final_function_value); + debug!("Final function value: {final_function_value:.6e}"); let function_decrease = initial_function_value - final_function_value; debug!("Updating L-BFGS history"); @@ -871,33 +848,28 @@ impl Optimizer for QQNOptimizer { if final_function_value > initial_function_value { let increase = final_function_value - initial_function_value; error!( - "FATAL ERROR: QQN step increased function value by {:.6e} (from {:.6e} to {:.6e}). This should never happen!", - increase, initial_function_value, final_function_value + "FATAL ERROR: QQN step increased function value by {increase:.6e} (from {initial_function_value:.6e} to {final_function_value:.6e}). This should never happen!" ); return Err(Error::Msg(format!( - "FATAL ERROR: QQN step increased function value by {:.6e} (from {:.6e} to {:.6e}). This violates the descent property and should never happen.", - increase, initial_function_value, final_function_value + "FATAL ERROR: QQN step increased function value by {increase:.6e} (from {initial_function_value:.6e} to {final_function_value:.6e}). This violates the descent property and should never happen." ))); } - debug!("Function decreased by: {:.6e}", function_decrease); + debug!("Function decreased by: {function_decrease:.6e}"); self.log_scalar("Function Decrease", function_decrease); // Check for NaN/Inf in updated parameters for (i, param) in params.iter().enumerate() { let param_vec = param.flatten_all()?.to_vec1::()?; if param_vec.iter().any(|&x| !x.is_finite()) { - warn!("Non-finite parameter detected at index {} after update", i); + warn!("Non-finite parameter detected at index {i} after update"); return Err(Error::Msg( "Non-finite parameter detected after update".into(), )); } // Also check for extremely large values if param_vec.iter().any(|&x| x.abs() > 1e10) { - warn!( - "Extremely large parameter detected at index {} after update", - i - ); + warn!("Extremely large parameter detected at index {i} after update"); return Err(Error::Msg("Parameter values too large after update".into())); } } @@ -1026,13 +998,9 @@ impl QuadraticPath { /// d(t) = t(1-t) * (-g) + t² * d_lbfgs pub fn evaluate_direction(&self, t: f64) -> CandleResult> { // Clamp t to valid range - let t_clamped = t.max(0.0).min(1.0); + let t_clamped = t.clamp(0.0, 1.0); if (t - t_clamped).abs() > 1e-10 { - trace!( - "QuadraticPath::evaluate_direction: clamped t from {} to {}", - t, - t_clamped - ); + trace!("QuadraticPath::evaluate_direction: clamped t from {t} to {t_clamped}"); } let t = t_clamped; @@ -1040,10 +1008,7 @@ impl QuadraticPath { let gradient_coeff = t * (1.0 - t); let lbfgs_coeff = t * t; trace!( - "QuadraticPath::evaluate_direction(t={}): gradient_coeff={}, lbfgs_coeff={}", - t, - gradient_coeff, - lbfgs_coeff + "QuadraticPath::evaluate_direction(t={t}): gradient_coeff={gradient_coeff}, lbfgs_coeff={lbfgs_coeff}" ); let tensors = &self.negative_gradient; @@ -1069,14 +1034,12 @@ impl QuadraticPath { /// /// d'(t) = (1-2t) * (-g) + 2t * d_lbfgs pub fn derivative(&self, t: f64) -> CandleResult> { - trace!("QuadraticPath::derivative(t={})", t); + trace!("QuadraticPath::derivative(t={t})"); let gradient_coeff = 1.0 - 2.0 * t; let lbfgs_coeff = 2.0 * t; trace!( - "QuadraticPath::derivative: gradient_coeff={}, lbfgs_coeff={}", - gradient_coeff, - lbfgs_coeff + "QuadraticPath::derivative: gradient_coeff={gradient_coeff}, lbfgs_coeff={lbfgs_coeff}" ); let tensors = &self.negative_gradient; @@ -1106,7 +1069,7 @@ impl QuadraticPath { (position_cache.get(&key), gradient_cache.get(&key)) { // We have both position and gradient for this t, update L-BFGS - trace!("Updating L-BFGS state for t={}", t); + trace!("Updating L-BFGS state for t={t}"); // Convert f64 vectors back to tensors let device = self.start_point[0].device(); let mut position_tensors = Vec::new(); @@ -1133,21 +1096,21 @@ impl QuadraticPath { if let Err(e) = lbfgs_state.update(&self.start_point, &position_tensors, &gradient_tensors) { - warn!("Failed to update L-BFGS state: {}", e); + warn!("Failed to update L-BFGS state: {e}"); } } } Ok(()) } } -impl<'a> ParametricCurve for QuadraticPath { +impl ParametricCurve for QuadraticPath { fn position(&self, t: f64) -> AnyhowResult> { let key = OrderedFloat(t); // Check cache first { let cache = self.position_cache.lock().unwrap(); if let Some(cached_position) = cache.get(&key) { - trace!("Using cached position for t={}", t); + trace!("Using cached position for t={t}"); self.cache_hits.fetch_add(1, Ordering::Relaxed); return Ok(cached_position.clone()); } @@ -1170,7 +1133,7 @@ impl<'a> ParametricCurve for QuadraticPath { // Check if we can update L-BFGS if let Err(e) = self.maybe_update_lbfgs(t) { - warn!("Failed to update L-BFGS in position evaluation: {}", e); + warn!("Failed to update L-BFGS in position evaluation: {e}"); } Ok(position_f64) @@ -1182,7 +1145,7 @@ impl<'a> ParametricCurve for QuadraticPath { { let cache = self.gradient_cache.lock().unwrap(); if let Some(cached_gradient) = cache.get(&key) { - trace!("Using cached gradient for t={}", t); + trace!("Using cached gradient for t={t}"); self.cache_hits.fetch_add(1, Ordering::Relaxed); return Ok(cached_gradient.clone()); } @@ -1224,7 +1187,7 @@ impl<'a> ParametricCurve for QuadraticPath { // Check if we can update L-BFGS if let Err(e) = self.maybe_update_lbfgs(t) { - warn!("Failed to update L-BFGS in gradient evaluation: {}", e); + warn!("Failed to update L-BFGS in gradient evaluation: {e}"); } Ok(gradient_f64) diff --git a/src/optimizers/trust_region.rs b/src/optimizers/trust_region.rs index aaa471f0..2fd01c6f 100644 --- a/src/optimizers/trust_region.rs +++ b/src/optimizers/trust_region.rs @@ -231,10 +231,10 @@ impl TrustRegionOptimizer { if grad_norm < 1e-12 { // Zero gradient, return zero step - return Ok(gradient + return gradient .iter() - .map(|g| Tensor::zeros_like(g)) - .collect::>>()?); + .map(Tensor::zeros_like) + .collect::>>(); } // Cauchy point: p = -τ * (radius / ||g||) * g @@ -275,10 +275,7 @@ impl TrustRegionOptimizer { let newton_norm = compute_magnitude(&newton_step)?; if self.config.verbose { - debug!( - "Newton step norm: {:.6e}, trust region radius: {:.6e}", - newton_norm, radius - ); + debug!("Newton step norm: {newton_norm:.6e}, trust region radius: {radius:.6e}"); } if newton_norm <= radius { @@ -291,7 +288,7 @@ impl TrustRegionOptimizer { // Scale Newton step to trust region boundary let scale = radius / newton_norm; if self.config.verbose { - debug!("Scaling Newton step by factor: {:.6e}", scale); + debug!("Scaling Newton step by factor: {scale:.6e}"); } newton_step .iter() @@ -336,10 +333,7 @@ impl Optimizer for TrustRegionOptimizer { let grad_norm = compute_magnitude(&gradient)?; if self.config.verbose { - debug!( - "Current function value: {:.6e}, gradient norm: {:.6e}", - current_value, grad_norm - ); + debug!("Current function value: {current_value:.6e}, gradient norm: {grad_norm:.6e}"); } // Update best function value @@ -404,8 +398,7 @@ impl Optimizer for TrustRegionOptimizer { if self.config.verbose { debug!( - "Step norm: {:.6e}, model reduction: {:.6e}, actual reduction: {:.6e}, rho: {:.6e}", - step_norm, model_reduction, actual_reduction, rho + "Step norm: {step_norm:.6e}, model reduction: {model_reduction:.6e}, actual reduction: {actual_reduction:.6e}, rho: {rho:.6e}" ); } diff --git a/src/utils/math.rs b/src/utils/math.rs index 406fb5a9..508082d6 100644 --- a/src/utils/math.rs +++ b/src/utils/math.rs @@ -35,7 +35,7 @@ pub fn compute_magnitude(tensors: &[Tensor]) -> CandleResult { let values = tensor.flatten_all()?.to_vec1::()?; for &val in &values { if !val.is_finite() { - warn!("Tensor contains non-finite value: {}", val); + warn!("Tensor contains non-finite value: {val}"); return Ok(f64::INFINITY); } max_abs = max_abs.max(val.abs()); @@ -241,7 +241,7 @@ pub fn log_tensor(tensors: &[Tensor]) { tensor.device() ); if values.len() <= 10 { - debug!(" Full data: {:?}", values); + debug!(" Full data: {values:?}"); } else { debug!( " First 5: {:?}, Last 5: {:?}", diff --git a/src/utils/mod.rs b/src/utils/mod.rs index afe67b50..bf5bbf55 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -38,7 +38,7 @@ pub mod paths { /// Generate timestamped filename pub fn timestamped_filename(base: &str, extension: &str) -> String { let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); - format!("{}_{}.{}", base, timestamp, extension) + format!("{base}_{timestamp}.{extension}") } /// Get results directory path @@ -61,8 +61,7 @@ pub mod validation { for (i, &val) in values.iter().enumerate() { if !val.is_finite() { return Err(crate::optimizers::OptError::InvalidInput(format!( - "Non-finite value {} at index {}", - val, i + "Non-finite value {val} at index {i}" ))); } } @@ -73,8 +72,7 @@ pub mod validation { pub fn validate_bounds(value: f64, min: f64, max: f64) -> OptResult<()> { if value < min || value > max { return Err(crate::optimizers::OptError::InvalidInput(format!( - "Value {} outside bounds [{}, {}]", - value, min, max + "Value {value} outside bounds [{min}, {max}]" ))); } Ok(()) diff --git a/tests/benchmark_reports.rs b/tests/benchmark_reports.rs index 9d730858..397aeca6 100644 --- a/tests/benchmark_reports.rs +++ b/tests/benchmark_reports.rs @@ -1,10 +1,14 @@ use std::error::Error; use std::time::Duration; -use qqn_optimizer::benchmarks::evaluation::{disable_no_threshold_mode, enable_no_threshold_mode, ProblemSpec}; +use qqn_optimizer::benchmarks::evaluation::{ + disable_no_threshold_mode, enable_no_threshold_mode, ProblemSpec, +}; use qqn_optimizer::experiment_runner::experiment_runner::run_benchmark; use qqn_optimizer::init_logging; -use qqn_optimizer::optimizer_sets::{adam_variants, gd_variants, lbfgs_variants, qqn_variants, trust_region_variants}; +use qqn_optimizer::optimizer_sets::{ + adam_variants, gd_variants, lbfgs_variants, qqn_variants, trust_region_variants, +}; use qqn_optimizer::problem_sets::{analytic_problems, ml_problems, mnist_problems}; #[tokio::test] @@ -13,11 +17,12 @@ async fn calibration() -> Result<(), Box> { // Enable no threshold mode for this test enable_no_threshold_mode(); - test_all("results/calibration_",{ + test_all("results/calibration_", { let mut problems = analytic_problems(); problems.extend(ml_problems()); problems - }).await?; + }) + .await?; // Explicitly flush any pending async operations tokio::task::yield_now().await; @@ -31,11 +36,12 @@ async fn full_test() -> Result<(), Box> { // Disable no threshold mode for this test disable_no_threshold_mode(); - test_all("results/full_",{ + test_all("results/full_", { let mut problems = analytic_problems(); problems.extend(ml_problems()); problems - }).await?; + }) + .await?; // Explicitly flush any pending async operations tokio::task::yield_now().await; @@ -45,7 +51,7 @@ async fn full_test() -> Result<(), Box> { async fn test_all( prefix: &str, - problems: Vec + problems: Vec, ) -> Result<(), Box> { let max_evals = 1000; let num_runs = 10; @@ -63,7 +69,8 @@ async fn test_all( optimizers.extend(trust_region_variants()); optimizers }, - ).await + ) + .await } // #[tokio::test] @@ -82,7 +89,7 @@ async fn test_mnist() -> Result<(), Box> { async fn test( prefix: &str, - problems: Vec + problems: Vec, ) -> Result<(), Box> { let max_evals = 1000; let num_runs = 10; @@ -93,7 +100,8 @@ async fn test( Duration::from_secs(60), problems.clone(), qqn_variants(), - ).await?; + ) + .await?; run_benchmark( &format!("{}qqn_variants_", prefix), @@ -102,7 +110,8 @@ async fn test( Duration::from_secs(60), problems.clone(), qqn_variants(), - ).await?; + ) + .await?; run_benchmark( &format!("{}lbfgs_variants_", prefix), @@ -111,7 +120,8 @@ async fn test( Duration::from_secs(60), problems.clone(), lbfgs_variants(), - ).await?; + ) + .await?; run_benchmark( &format!("{}gd_variants_", prefix), @@ -120,7 +130,8 @@ async fn test( Duration::from_secs(60), problems.clone(), gd_variants(), - ).await?; + ) + .await?; run_benchmark( &format!("{}adam_variants_", prefix), @@ -129,7 +140,8 @@ async fn test( Duration::from_secs(60), problems.clone(), adam_variants(), - ).await?; + ) + .await?; run_benchmark( &format!("{}trust_region_variants_", prefix), @@ -138,6 +150,7 @@ async fn test( Duration::from_secs(60), problems.clone(), trust_region_variants(), - ).await?; + ) + .await?; Ok(()) -} \ No newline at end of file +}