Skip to main content

optirs_core/research/
benchmarks.rs

1// Academic benchmarking suite for research validation
2//
3// This module provides standardized benchmarks and evaluation protocols
4// for comparing optimization algorithms in academic research contexts.
5
6#[allow(unused_imports)]
7use crate::error::Result;
8use crate::unified_api::{OptimizerConfig, Parameter, UnifiedAdam, UnifiedOptimizer, UnifiedSGD};
9use chrono::{DateTime, Utc};
10use scirs2_core::ndarray::{Array1, Ix1, ScalarOperand};
11use scirs2_core::numeric::Float;
12use scirs2_core::random::Random;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16/// Academic benchmark suite
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AcademicBenchmarkSuite {
19    /// Suite identifier
20    pub id: String,
21    /// Suite name
22    pub name: String,
23    /// Suite description
24    pub description: String,
25    /// Benchmark problems
26    pub benchmarks: Vec<BenchmarkProblem>,
27    /// Evaluation metrics
28    pub metrics: Vec<EvaluationMetric>,
29    /// Reference results
30    pub reference_results: HashMap<String, BenchmarkResults>,
31    /// Suite metadata
32    pub metadata: BenchmarkSuiteMetadata,
33    /// Creation timestamp
34    pub created_at: DateTime<Utc>,
35}
36
37/// Individual benchmark problem
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BenchmarkProblem {
40    /// Problem identifier
41    pub id: String,
42    /// Problem name
43    pub name: String,
44    /// Problem description
45    pub description: String,
46    /// Problem category
47    pub category: ProblemCategory,
48    /// Problem difficulty
49    pub difficulty: DifficultyLevel,
50    /// Problem dimensions
51    pub dimensions: Vec<usize>,
52    /// Objective function
53    pub objective_function: ObjectiveFunction,
54    /// Problem constraints
55    pub constraints: Vec<Constraint>,
56    /// Known optimal solution
57    pub optimal_solution: Option<OptimalSolution>,
58    /// Problem parameters
59    pub parameters: HashMap<String, f64>,
60    /// Literature references
61    pub references: Vec<String>,
62}
63
64/// Problem categories
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub enum ProblemCategory {
67    /// Convex optimization
68    Convex,
69    /// Non-convex optimization
70    NonConvex,
71    /// Machine learning
72    MachineLearning,
73    /// Deep learning
74    DeepLearning,
75    /// Reinforcement learning
76    ReinforcementLearning,
77    /// Computer vision
78    ComputerVision,
79    /// Natural language processing
80    NaturalLanguageProcessing,
81    /// Numerical optimization
82    NumericalOptimization,
83    /// Constrained optimization
84    ConstrainedOptimization,
85    /// Multi-objective optimization
86    MultiObjective,
87    /// Stochastic optimization
88    Stochastic,
89    /// Discrete optimization
90    Discrete,
91    /// Continuous optimization
92    Continuous,
93    /// Mixed optimization
94    Mixed,
95}
96
97/// Difficulty levels
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
99pub enum DifficultyLevel {
100    /// Easy problems
101    Easy,
102    /// Medium problems
103    Medium,
104    /// Hard problems
105    Hard,
106    /// Very hard problems
107    VeryHard,
108    /// Extreme problems
109    Extreme,
110}
111
112/// Objective function definition
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ObjectiveFunction {
115    /// Function name
116    pub name: String,
117    /// Function type
118    pub function_type: FunctionType,
119    /// Function properties
120    pub properties: FunctionProperties,
121    /// Mathematical description
122    pub mathematical_form: String,
123    /// Implementation notes
124    pub implementation_notes: String,
125}
126
127/// Function types
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129pub enum FunctionType {
130    /// Quadratic function
131    Quadratic,
132    /// Rosenbrock function
133    Rosenbrock,
134    /// Sphere function
135    Sphere,
136    /// Rastrigin function
137    Rastrigin,
138    /// Ackley function
139    Ackley,
140    /// Griewank function
141    Griewank,
142    /// Schwefel function
143    Schwefel,
144    /// Himmelblau function
145    Himmelblau,
146    /// Booth function
147    Booth,
148    /// Beale function
149    Beale,
150    /// Three-hump camel function
151    ThreeHumpCamel,
152    /// Six-hump camel function
153    SixHumpCamel,
154    /// Cross-in-tray function
155    CrossInTray,
156    /// Egg holder function
157    EggHolder,
158    /// Holder table function
159    HolderTable,
160    /// McCormick function
161    McCormick,
162    /// Schaffer function N2
163    SchafferN2,
164    /// Schaffer function N4
165    SchafferN4,
166    /// StyblinskiTang function
167    StyblinskiTang,
168    /// Custom function
169    Custom(String),
170}
171
172/// Function properties
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct FunctionProperties {
175    /// Is the function differentiable
176    pub differentiable: bool,
177    /// Is the function continuous
178    pub continuous: bool,
179    /// Is the function convex
180    pub convex: bool,
181    /// Is the function separable
182    pub separable: bool,
183    /// Is the function multimodal
184    pub multimodal: bool,
185    /// Function smoothness
186    pub smoothness: SmoothnesLevel,
187    /// Condition number
188    pub condition_number: Option<f64>,
189    /// Lipschitz constant
190    pub lipschitz_constant: Option<f64>,
191}
192
193/// Smoothness levels
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
195pub enum SmoothnesLevel {
196    /// Very smooth
197    VerySmooth,
198    /// Smooth
199    Smooth,
200    /// Moderately smooth
201    ModeratelySmooth,
202    /// Rough
203    Rough,
204    /// Very rough
205    VeryRough,
206}
207
208/// Optimization constraint
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct Constraint {
211    /// Constraint type
212    pub constraint_type: ConstraintType,
213    /// Constraint description
214    pub description: String,
215    /// Mathematical form
216    pub mathematical_form: String,
217    /// Constraint parameters
218    pub parameters: HashMap<String, f64>,
219}
220
221/// Constraint types
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub enum ConstraintType {
224    /// Equality constraint
225    Equality,
226    /// Inequality constraint
227    Inequality,
228    /// Box constraint (bounds)
229    Box,
230    /// Linear constraint
231    Linear,
232    /// Nonlinear constraint
233    Nonlinear,
234    /// Integer constraint
235    Integer,
236    /// Binary constraint
237    Binary,
238}
239
240/// Known optimal solution
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct OptimalSolution {
243    /// Optimal parameter values
244    pub parameters: Array1<f64>,
245    /// Optimal objective value
246    pub objective_value: f64,
247    /// Solution properties
248    pub properties: SolutionProperties,
249    /// Literature reference
250    pub reference: Option<String>,
251}
252
253/// Solution properties
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SolutionProperties {
256    /// Is this a global optimum
257    pub global_optimum: bool,
258    /// Is this a local optimum
259    pub local_optimum: bool,
260    /// Solution uniqueness
261    pub unique: bool,
262    /// Solution stability
263    pub stable: bool,
264}
265
266/// Evaluation metric for benchmarks
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct EvaluationMetric {
269    /// Metric name
270    pub name: String,
271    /// Metric description
272    pub description: String,
273    /// Metric type
274    pub metric_type: MetricType,
275    /// Aggregation method
276    pub aggregation: AggregationMethod,
277    /// Better direction (higher or lower is better)
278    pub better_direction: BetterDirection,
279    /// Metric weight in overall score
280    pub weight: f64,
281}
282
283/// Metric types
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
285pub enum MetricType {
286    /// Objective value at convergence
287    FinalObjective,
288    /// Number of iterations to convergence
289    IterationsToConvergence,
290    /// Time to convergence
291    TimeToConvergence,
292    /// Function evaluations to convergence
293    FunctionEvaluations,
294    /// Gradient evaluations
295    GradientEvaluations,
296    /// Success rate (percentage of successful runs)
297    SuccessRate,
298    /// Solution quality
299    SolutionQuality,
300    /// Convergence rate
301    ConvergenceRate,
302    /// Robustness measure
303    Robustness,
304    /// Memory usage
305    MemoryUsage,
306    /// Energy consumption
307    EnergyConsumption,
308    /// Custom metric
309    Custom(String),
310}
311
312/// Aggregation methods for multiple runs
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
314pub enum AggregationMethod {
315    /// Mean value
316    Mean,
317    /// Median value
318    Median,
319    /// Best value
320    Best,
321    /// Worst value
322    Worst,
323    /// Standard deviation
324    StandardDeviation,
325    /// Percentile (specify which percentile)
326    Percentile(u8),
327    /// Success count
328    SuccessCount,
329    /// Custom aggregation
330    Custom(String),
331}
332
333/// Better direction for metrics
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
335pub enum BetterDirection {
336    /// Higher values are better
337    Higher,
338    /// Lower values are better
339    Lower,
340}
341
342/// Benchmark results for a specific optimizer
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct BenchmarkResults {
345    /// Optimizer name
346    pub optimizer_name: String,
347    /// Results per problem
348    pub problem_results: HashMap<String, ProblemResults>,
349    /// Overall scores
350    pub overall_scores: HashMap<String, f64>,
351    /// Statistical significance tests
352    pub statistical_tests: Vec<StatisticalTest>,
353    /// Performance ranking
354    pub ranking: OptimizerRanking,
355    /// Execution timestamp
356    pub executed_at: DateTime<Utc>,
357}
358
359/// Results for a single problem
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ProblemResults {
362    /// Problem identifier
363    pub problem_id: String,
364    /// Individual run results
365    pub run_results: Vec<RunResult>,
366    /// Aggregated metrics
367    pub aggregated_metrics: HashMap<String, f64>,
368    /// Statistical summaries
369    pub statistics: ResultStatistics,
370    /// Convergence analysis
371    pub convergence_analysis: ConvergenceAnalysis,
372}
373
374/// Result for a single run
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct RunResult {
377    /// Run identifier
378    pub run_id: String,
379    /// Random seed used
380    pub random_seed: u64,
381    /// Final objective value
382    pub final_objective: f64,
383    /// Convergence achieved
384    pub converged: bool,
385    /// Number of iterations
386    pub iterations: usize,
387    /// Execution time (seconds)
388    pub execution_time: f64,
389    /// Function evaluations
390    pub function_evaluations: usize,
391    /// Gradient evaluations
392    pub gradient_evaluations: usize,
393    /// Memory usage (bytes)
394    pub memory_usage: usize,
395    /// Convergence trajectory
396    pub trajectory: Vec<f64>,
397    /// Error information (if failed)
398    pub error_info: Option<String>,
399}
400
401/// Statistical summaries
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct ResultStatistics {
404    /// Number of successful runs
405    pub successful_runs: usize,
406    /// Total number of runs
407    pub total_runs: usize,
408    /// Success rate
409    pub success_rate: f64,
410    /// Mean objective value
411    pub mean_objective: f64,
412    /// Standard deviation of objective values
413    pub std_objective: f64,
414    /// Best objective value
415    pub best_objective: f64,
416    /// Worst objective value
417    pub worst_objective: f64,
418    /// Median objective value
419    pub median_objective: f64,
420    /// Quartiles
421    pub quartiles: (f64, f64, f64), // Q1, Q2, Q3
422    /// Confidence intervals
423    pub confidence_intervals: HashMap<String, (f64, f64)>,
424}
425
426/// Convergence analysis
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct ConvergenceAnalysis {
429    /// Average convergence rate
430    pub avg_convergence_rate: f64,
431    /// Convergence stability
432    pub convergence_stability: f64,
433    /// Early convergence indicator
434    pub early_convergence: bool,
435    /// Plateau detection
436    pub plateau_detected: bool,
437    /// Convergence pattern
438    pub convergence_pattern: ConvergencePattern,
439}
440
441/// Convergence patterns
442#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443pub enum ConvergencePattern {
444    /// Monotonic decrease
445    MonotonicDecrease,
446    /// Exponential decay
447    ExponentialDecay,
448    /// Linear decrease
449    LinearDecrease,
450    /// Oscillatory convergence
451    Oscillatory,
452    /// Stepwise convergence
453    Stepwise,
454    /// Plateau then drop
455    PlateauThenDrop,
456    /// No clear pattern
457    Irregular,
458}
459
460/// Statistical significance test
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct StatisticalTest {
463    /// Test name
464    pub test_name: String,
465    /// Compared optimizers
466    pub optimizers: Vec<String>,
467    /// Test statistic
468    pub test_statistic: f64,
469    /// P-value
470    pub p_value: f64,
471    /// Significance level
472    pub significance_level: f64,
473    /// Test result
474    pub significant: bool,
475    /// Effect size
476    pub effect_size: Option<f64>,
477}
478
479/// Optimizer ranking
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct OptimizerRanking {
482    /// Overall rank (1 is best)
483    pub overall_rank: usize,
484    /// Ranks per category
485    pub category_ranks: HashMap<String, usize>,
486    /// Ranks per metric
487    pub metric_ranks: HashMap<String, usize>,
488    /// Ranking score
489    pub ranking_score: f64,
490    /// Ranking method used
491    pub ranking_method: RankingMethod,
492}
493
494/// Ranking methods
495#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
496pub enum RankingMethod {
497    /// Average rank across all metrics
498    AverageRank,
499    /// Weighted score
500    WeightedScore,
501    /// Pareto dominance
502    ParetoDominance,
503    /// Win-loss-tie
504    WinLossTie,
505    /// Tournament ranking
506    Tournament,
507    /// Custom ranking method
508    Custom(String),
509}
510
511/// Benchmark suite metadata
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct BenchmarkSuiteMetadata {
514    /// Suite version
515    pub version: String,
516    /// Suite authors
517    pub authors: Vec<String>,
518    /// Suite license
519    pub license: String,
520    /// Literature references
521    pub references: Vec<String>,
522    /// Target audience
523    pub target_audience: Vec<String>,
524    /// Keywords
525    pub keywords: Vec<String>,
526    /// Changelog
527    pub changelog: Vec<ChangelogEntry>,
528}
529
530/// Changelog entry
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct ChangelogEntry {
533    /// Version number
534    pub version: String,
535    /// Release date
536    pub date: DateTime<Utc>,
537    /// Changes description
538    pub changes: String,
539    /// Author of changes
540    pub author: String,
541}
542
543/// Benchmark runner for executing benchmark suites
544pub struct BenchmarkRunner {
545    /// Benchmark suite
546    suite: AcademicBenchmarkSuite,
547    /// Execution settings
548    settings: BenchmarkSettings,
549    /// Progress callback
550    progress_callback: Option<Box<dyn Fn(f64) + Send + Sync>>,
551}
552
553impl std::fmt::Debug for BenchmarkRunner {
554    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555        f.debug_struct("BenchmarkRunner")
556            .field("suite", &self.suite)
557            .field("settings", &self.settings)
558            .field("progress_callback", &self.progress_callback.is_some())
559            .finish()
560    }
561}
562
563/// Benchmark execution settings
564#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct BenchmarkSettings {
566    /// Number of independent runs per problem
567    pub num_runs: usize,
568    /// Random seeds to use
569    pub random_seeds: Vec<u64>,
570    /// Maximum iterations per run
571    pub max_iterations: usize,
572    /// Maximum execution time per run (seconds)
573    pub max_time_seconds: f64,
574    /// Convergence tolerance
575    pub convergence_tolerance: f64,
576    /// Enable parallel execution
577    pub parallel_execution: bool,
578    /// Number of parallel threads
579    pub num_threads: Option<usize>,
580    /// Save detailed results
581    pub save_detailed_results: bool,
582    /// Output directory
583    pub output_directory: Option<String>,
584}
585
586impl AcademicBenchmarkSuite {
587    /// Create a new benchmark suite
588    pub fn new(name: &str) -> Self {
589        Self {
590            id: uuid::Uuid::new_v4().to_string(),
591            name: name.to_string(),
592            description: String::new(),
593            benchmarks: Vec::new(),
594            metrics: Vec::new(),
595            reference_results: HashMap::new(),
596            metadata: BenchmarkSuiteMetadata::default(),
597            created_at: Utc::now(),
598        }
599    }
600
601    /// Add a benchmark problem
602    pub fn add_benchmark(&mut self, benchmark: BenchmarkProblem) {
603        self.benchmarks.push(benchmark);
604    }
605
606    /// Add an evaluation metric
607    pub fn add_metric(&mut self, metric: EvaluationMetric) {
608        self.metrics.push(metric);
609    }
610
611    /// Create standard ML optimization benchmark suite
612    pub fn standard_ml_suite() -> Self {
613        let mut suite = Self::new("Standard ML Optimization Benchmark");
614        suite.description =
615            "Standard benchmark suite for machine learning optimization algorithms".to_string();
616
617        // Add standard problems
618        suite.add_benchmark(Self::create_quadratic_problem());
619        suite.add_benchmark(Self::create_rosenbrock_problem());
620        suite.add_benchmark(Self::create_logistic_regression_problem());
621        suite.add_benchmark(Self::create_neural_network_problem());
622
623        // Add standard metrics
624        suite.add_metric(Self::create_final_objective_metric());
625        suite.add_metric(Self::create_convergence_time_metric());
626        suite.add_metric(Self::create_success_rate_metric());
627
628        suite
629    }
630
631    fn create_quadratic_problem() -> BenchmarkProblem {
632        BenchmarkProblem {
633            id: "quadratic_10d".to_string(),
634            name: "10D Quadratic Function".to_string(),
635            description: "Simple quadratic function in 10 dimensions".to_string(),
636            category: ProblemCategory::Convex,
637            difficulty: DifficultyLevel::Easy,
638            dimensions: vec![10],
639            objective_function: ObjectiveFunction {
640                name: "Quadratic".to_string(),
641                function_type: FunctionType::Quadratic,
642                properties: FunctionProperties {
643                    differentiable: true,
644                    continuous: true,
645                    convex: true,
646                    separable: true,
647                    multimodal: false,
648                    smoothness: SmoothnesLevel::VerySmooth,
649                    condition_number: Some(1.0),
650                    lipschitz_constant: Some(2.0),
651                },
652                mathematical_form: "f(x) = 0.5 * x^T * x".to_string(),
653                implementation_notes: "Simple quadratic function with unit matrix".to_string(),
654            },
655            constraints: Vec::new(),
656            optimal_solution: Some(OptimalSolution {
657                parameters: Array1::zeros(10),
658                objective_value: 0.0,
659                properties: SolutionProperties {
660                    global_optimum: true,
661                    local_optimum: true,
662                    unique: true,
663                    stable: true,
664                },
665                reference: None,
666            }),
667            parameters: HashMap::new(),
668            references: vec!["Standard optimization textbooks".to_string()],
669        }
670    }
671
672    fn create_rosenbrock_problem() -> BenchmarkProblem {
673        BenchmarkProblem {
674            id: "rosenbrock_10d".to_string(),
675            name: "10D Rosenbrock Function".to_string(),
676            description: "Rosenbrock function in 10 dimensions".to_string(),
677            category: ProblemCategory::NonConvex,
678            difficulty: DifficultyLevel::Medium,
679            dimensions: vec![10],
680            objective_function: ObjectiveFunction {
681                name: "Rosenbrock".to_string(),
682                function_type: FunctionType::Rosenbrock,
683                properties: FunctionProperties {
684                    differentiable: true,
685                    continuous: true,
686                    convex: false,
687                    separable: false,
688                    multimodal: false,
689                    smoothness: SmoothnesLevel::Smooth,
690                    condition_number: None,
691                    lipschitz_constant: None},
692                mathematical_form: "f(x) = sum(100*(x[i+1] - x[i]^2)^2 + (1 - x[i])^2)".to_string(),
693                implementation_notes: "Classic Rosenbrock function, challenging for optimization".to_string()},
694            constraints: Vec::new(),
695            optimal_solution: Some(OptimalSolution {
696                parameters: Array1::ones(10),
697                objective_value: 0.0,
698                properties: SolutionProperties {
699                    global_optimum: true,
700                    local_optimum: true,
701                    unique: true,
702                    stable: true},
703                reference: Some("Rosenbrock, H.H. (1960)".to_string())}),
704            parameters: HashMap::new(),
705            references: vec!["Rosenbrock, H.H. (1960). An automatic method for finding the greatest or least value of a function.".to_string()]}
706    }
707
708    fn create_logistic_regression_problem() -> BenchmarkProblem {
709        BenchmarkProblem {
710            id: "logistic_regression_100d".to_string(),
711            name: "Logistic Regression (100D)".to_string(),
712            description: "Logistic regression on synthetic dataset".to_string(),
713            category: ProblemCategory::MachineLearning,
714            difficulty: DifficultyLevel::Medium,
715            dimensions: vec![100],
716            objective_function: ObjectiveFunction {
717                name: "Logistic Loss".to_string(),
718                function_type: FunctionType::Custom("LogisticLoss".to_string()),
719                properties: FunctionProperties {
720                    differentiable: true,
721                    continuous: true,
722                    convex: true,
723                    separable: false,
724                    multimodal: false,
725                    smoothness: SmoothnesLevel::Smooth,
726                    condition_number: None,
727                    lipschitz_constant: None,
728                },
729                mathematical_form: "f(w) = mean(log(1 + exp(-y * X * w))) + lambda * ||w||^2"
730                    .to_string(),
731                implementation_notes: "Binary classification with L2 regularization".to_string(),
732            },
733            constraints: Vec::new(),
734            optimal_solution: None, // Depends on dataset
735            parameters: {
736                let mut params = HashMap::new();
737                params.insert("lambda".to_string(), 0.01);
738                params.insert("num_samples".to_string(), 1000.0);
739                params
740            },
741            references: vec!["Standard machine learning references".to_string()],
742        }
743    }
744
745    fn create_neural_network_problem() -> BenchmarkProblem {
746        BenchmarkProblem {
747            id: "neural_network_mnist".to_string(),
748            name: "Neural Network MNIST".to_string(),
749            description: "Two-layer neural network on MNIST subset".to_string(),
750            category: ProblemCategory::DeepLearning,
751            difficulty: DifficultyLevel::Hard,
752            dimensions: vec![784, 128, 10], // Input, hidden, output
753            objective_function: ObjectiveFunction {
754                name: "Cross-entropy Loss".to_string(),
755                function_type: FunctionType::Custom("CrossEntropyLoss".to_string()),
756                properties: FunctionProperties {
757                    differentiable: true,
758                    continuous: true,
759                    convex: false,
760                    separable: false,
761                    multimodal: true,
762                    smoothness: SmoothnesLevel::Smooth,
763                    condition_number: None,
764                    lipschitz_constant: None,
765                },
766                mathematical_form: "f(θ) = mean(-log(softmax(NN(x; θ))[y]))".to_string(),
767                implementation_notes: "Two-layer ReLU network with softmax output".to_string(),
768            },
769            constraints: Vec::new(),
770            optimal_solution: None, // Unknown for neural networks
771            parameters: {
772                let mut params = HashMap::new();
773                params.insert("num_samples".to_string(), 10000.0);
774                params.insert("batch_size".to_string(), 64.0);
775                params
776            },
777            references: vec![
778                "LeCun et al. (1998). Gradient-based learning applied to document recognition."
779                    .to_string(),
780            ],
781        }
782    }
783
784    fn create_final_objective_metric() -> EvaluationMetric {
785        EvaluationMetric {
786            name: "Final Objective Value".to_string(),
787            description: "Final objective function value achieved".to_string(),
788            metric_type: MetricType::FinalObjective,
789            aggregation: AggregationMethod::Mean,
790            better_direction: BetterDirection::Lower,
791            weight: 1.0,
792        }
793    }
794
795    fn create_convergence_time_metric() -> EvaluationMetric {
796        EvaluationMetric {
797            name: "Time to Convergence".to_string(),
798            description: "Time required to reach convergence tolerance".to_string(),
799            metric_type: MetricType::TimeToConvergence,
800            aggregation: AggregationMethod::Median,
801            better_direction: BetterDirection::Lower,
802            weight: 0.5,
803        }
804    }
805
806    fn create_success_rate_metric() -> EvaluationMetric {
807        EvaluationMetric {
808            name: "Success Rate".to_string(),
809            description: "Percentage of runs that converged successfully".to_string(),
810            metric_type: MetricType::SuccessRate,
811            aggregation: AggregationMethod::Mean,
812            better_direction: BetterDirection::Higher,
813            weight: 0.8,
814        }
815    }
816}
817
818/// One real, runnable optimizer selected by name for a benchmark run.
819///
820/// [`UnifiedOptimizer::step_param`] is generic over the parameter's
821/// dimension type, which makes the trait itself object-unsafe (`dyn
822/// UnifiedOptimizer<A>` cannot exist); this small enum is the standard
823/// workaround, letting [`BenchmarkRunner`] pick an algorithm by name at
824/// runtime while still driving each one through its real implementation.
825enum ChosenOptimizer<A: Float + ScalarOperand + std::fmt::Debug + Send + Sync> {
826    Sgd(UnifiedSGD<A>),
827    Adam(UnifiedAdam<A>),
828}
829
830impl<A: Float + ScalarOperand + std::fmt::Debug + Send + Sync> ChosenOptimizer<A> {
831    fn step_param(&mut self, param: &mut Parameter<A, Ix1>) -> Result<()> {
832        match self {
833            ChosenOptimizer::Sgd(optimizer) => optimizer.step_param(param),
834            ChosenOptimizer::Adam(optimizer) => optimizer.step_param(param),
835        }
836    }
837}
838
839/// Select a concrete optimizer implementation by (case-insensitive)
840/// `optimizer_name`, defaulting to plain SGD for anything not recognized as
841/// Adam -- this only chooses *which* real optimizer, `optimizer_config`'s
842/// learning rate/weight decay/etc. are honored either way.
843fn select_optimizer<A: Float + ScalarOperand + std::fmt::Debug + Send + Sync>(
844    optimizer_name: &str,
845    config: OptimizerConfig<A>,
846) -> ChosenOptimizer<A> {
847    if optimizer_name.to_lowercase().contains("adam") {
848        ChosenOptimizer::Adam(UnifiedAdam::new(config))
849    } else {
850        ChosenOptimizer::Sgd(UnifiedSGD::new(config))
851    }
852}
853
854/// Evaluate a benchmark objective's value and analytic gradient at `x`.
855///
856/// [`FunctionType::Rosenbrock`] and [`FunctionType::Sphere`] get their real,
857/// classic closed-form definitions. Every other function type (including
858/// [`FunctionType::Custom`]) falls back to the convex quadratic bowl `f(x) =
859/// 0.5||x||^2` -- an honest, clearly-documented stand-in for "no dedicated
860/// implementation yet" rather than fabricated random noise presented as an
861/// optimization result.
862fn evaluate_objective<A: Float>(function_type: &FunctionType, x: &[A]) -> (A, Vec<A>) {
863    match function_type {
864        FunctionType::Rosenbrock => {
865            let mut value = A::zero();
866            let mut grad = vec![A::zero(); x.len()];
867            let hundred = A::from(100.0).unwrap_or_else(A::one);
868            let two = A::from(2.0).unwrap_or_else(A::one);
869            let four = A::from(4.0).unwrap_or_else(A::one);
870
871            for i in 0..x.len().saturating_sub(1) {
872                let xi = x[i];
873                let xi1 = x[i + 1];
874                let t1 = xi1 - xi * xi;
875                let t2 = A::one() - xi;
876                value = value + hundred * t1 * t1 + t2 * t2;
877                grad[i] = grad[i] + (-four * hundred * xi * t1) - two * t2;
878                grad[i + 1] = grad[i + 1] + two * hundred * t1;
879            }
880
881            (value, grad)
882        }
883        FunctionType::Sphere => {
884            let two = A::from(2.0).unwrap_or_else(A::one);
885            let value = x.iter().fold(A::zero(), |acc, &xi| acc + xi * xi);
886            let grad = x.iter().map(|&xi| two * xi).collect();
887            (value, grad)
888        }
889        _ => {
890            let half = A::from(0.5).unwrap_or_else(A::one);
891            let value = x.iter().fold(A::zero(), |acc, &xi| acc + xi * xi) * half;
892            let grad = x.to_vec();
893            (value, grad)
894        }
895    }
896}
897
898/// 95% two-sided confidence interval for a sample mean, using the
899/// Student-t distribution (appropriate for the typically small sample
900/// sizes -- a handful of independent benchmark runs -- these statistics are
901/// computed over) rather than a large-sample normal approximation. Returns
902/// `(mean, mean)` when there is no defined interval (fewer than 2 samples,
903/// or zero variance).
904fn confidence_interval_95(n: usize, mean: f64, sample_std: f64) -> (f64, f64) {
905    if n < 2 || sample_std <= 0.0 {
906        return (mean, mean);
907    }
908
909    let df = (n - 1) as f64;
910    let t_critical = student_t_critical_value(df, 0.975);
911    let margin = t_critical * sample_std / (n as f64).sqrt();
912    (mean - margin, mean + margin)
913}
914
915/// The critical value `t` such that `P(T <= t) = quantile` for a Student-t
916/// distribution with `df` degrees of freedom, found by bisecting the real
917/// CDF from `scirs2_stats` (the same technique
918/// `ContinuousDistribution::ppf`'s default implementation uses; `StudentT`
919/// does not implement that trait, so this reimplements just the bisection).
920fn student_t_critical_value(df: f64, quantile: f64) -> f64 {
921    // z_0.975, used as a fallback if the distribution can't be built.
922    const Z_975: f64 = 1.959963985;
923
924    let Ok(dist) = scirs2_stats::distributions::t(df, 0.0_f64, 1.0_f64) else {
925        return Z_975;
926    };
927
928    // Low-df Student-t is heavy-tailed (df=1 is the Cauchy distribution,
929    // whose 97.5th percentile is ~12.7), so a fixed bracket is not always
930    // wide enough: double the upper bound until it truly brackets the
931    // target quantile before bisecting.
932    let mut low = 0.0_f64;
933    let mut high = 2.0_f64;
934    while dist.cdf(high) < quantile && high < 1e12 {
935        high *= 2.0;
936    }
937
938    for _ in 0..200 {
939        let mid = 0.5 * (low + high);
940        if dist.cdf(mid) < quantile {
941            low = mid;
942        } else {
943            high = mid;
944        }
945    }
946    0.5 * (low + high)
947}
948
949/// Wilson score 95% confidence interval for a binomial proportion
950/// (`successes` out of `trials`) -- a standard closed-form interval that,
951/// unlike a normal approximation, stays within `[0, 1]` and remains
952/// well-behaved for the small sample counts and extreme (near 0 or 1)
953/// proportions typical of an optimizer's success rate.
954fn wilson_score_interval_95(successes: usize, trials: usize) -> (f64, f64) {
955    if trials == 0 {
956        return (0.0, 0.0);
957    }
958
959    const Z: f64 = 1.959963985; // z_0.975
960    let n = trials as f64;
961    let p_hat = successes as f64 / n;
962    let z_sq = Z * Z;
963
964    let denominator = 1.0 + z_sq / n;
965    let center = (p_hat + z_sq / (2.0 * n)) / denominator;
966    let margin = (Z * ((p_hat * (1.0 - p_hat) / n) + z_sq / (4.0 * n * n)).sqrt()) / denominator;
967
968    ((center - margin).max(0.0), (center + margin).min(1.0))
969}
970
971impl BenchmarkRunner {
972    /// Create a new benchmark runner
973    pub fn new(suite: AcademicBenchmarkSuite, settings: BenchmarkSettings) -> Self {
974        Self {
975            suite,
976            settings,
977            progress_callback: None,
978        }
979    }
980
981    /// Set progress callback
982    pub fn set_progress_callback<F>(&mut self, callback: F)
983    where
984        F: Fn(f64) + Send + Sync + 'static,
985    {
986        self.progress_callback = Some(Box::new(callback));
987    }
988
989    /// Run benchmark suite on multiple optimizers
990    pub fn run_benchmarks<
991        A: Float + std::fmt::Debug + Send + Sync + scirs2_core::ndarray::ScalarOperand + 'static,
992    >(
993        &self,
994        optimizers: &[(&str, OptimizerConfig<A>)],
995    ) -> Result<HashMap<String, BenchmarkResults>> {
996        let mut all_results = HashMap::new();
997
998        let total_work = optimizers.len() * self.suite.benchmarks.len() * self.settings.num_runs;
999        let mut completed_work = 0;
1000
1001        for (optimizer_name, optimizer_config) in optimizers {
1002            let mut optimizer_results = BenchmarkResults {
1003                optimizer_name: optimizer_name.to_string(),
1004                problem_results: HashMap::new(),
1005                overall_scores: HashMap::new(),
1006                statistical_tests: Vec::new(),
1007                ranking: OptimizerRanking {
1008                    overall_rank: 0,
1009                    category_ranks: HashMap::new(),
1010                    metric_ranks: HashMap::new(),
1011                    ranking_score: 0.0,
1012                    ranking_method: RankingMethod::WeightedScore,
1013                },
1014                executed_at: Utc::now(),
1015            };
1016
1017            for benchmark in &self.suite.benchmarks {
1018                let problem_results =
1019                    self.run_single_problem::<A>(benchmark, optimizer_name, optimizer_config)?;
1020                optimizer_results
1021                    .problem_results
1022                    .insert(benchmark.id.clone(), problem_results);
1023
1024                completed_work += self.settings.num_runs;
1025                if let Some(ref callback) = self.progress_callback {
1026                    callback(completed_work as f64 / total_work as f64);
1027                }
1028            }
1029
1030            // Calculate overall scores
1031            self.calculate_overall_scores(&mut optimizer_results);
1032
1033            all_results.insert(optimizer_name.to_string(), optimizer_results);
1034        }
1035
1036        // Calculate rankings and statistical tests
1037        self.calculate_rankings_and_tests(&mut all_results);
1038
1039        Ok(all_results)
1040    }
1041
1042    fn run_single_problem<
1043        A: Float + std::fmt::Debug + Send + Sync + scirs2_core::ndarray::ScalarOperand + 'static,
1044    >(
1045        &self,
1046        benchmark: &BenchmarkProblem,
1047        optimizer_name: &str,
1048        optimizer_config: &OptimizerConfig<A>,
1049    ) -> Result<ProblemResults> {
1050        let mut run_results = Vec::new();
1051
1052        for run_idx in 0..self.settings.num_runs {
1053            let seed = if run_idx < self.settings.random_seeds.len() {
1054                self.settings.random_seeds[run_idx]
1055            } else {
1056                42 + run_idx as u64
1057            };
1058
1059            let run_result =
1060                self.run_single_instance::<A>(benchmark, optimizer_name, optimizer_config, seed)?;
1061            run_results.push(run_result);
1062        }
1063
1064        // Calculate aggregated metrics and statistics
1065        let aggregated_metrics = self.calculate_aggregated_metrics(&run_results);
1066        let statistics = self.calculate_statistics(&run_results);
1067        let convergence_analysis = self.analyze_convergence(&run_results);
1068
1069        Ok(ProblemResults {
1070            problem_id: benchmark.id.clone(),
1071            run_results,
1072            aggregated_metrics,
1073            statistics,
1074            convergence_analysis,
1075        })
1076    }
1077
1078    /// Run `optimizer_name`/`optimizer_config` against `benchmark`'s real
1079    /// objective function for real: this seeds a deterministic starting
1080    /// point from `seed`, then repeatedly evaluates the objective's analytic
1081    /// gradient at the current point and applies one real optimizer step
1082    /// (via [`UnifiedSGD`]/[`UnifiedAdam`], selected by `optimizer_name`),
1083    /// recording the true trajectory of objective values.
1084    ///
1085    /// Previously this ignored both `optimizer_config` and `seed` entirely
1086    /// and returned a value drawn from a fixed, function-type-specific `Rng`
1087    /// range -- so every optimizer "converged" identically regardless of its
1088    /// hyperparameters, and repeated runs with different seeds were
1089    /// indistinguishable.
1090    fn run_single_instance<
1091        A: Float + std::fmt::Debug + Send + Sync + scirs2_core::ndarray::ScalarOperand + 'static,
1092    >(
1093        &self,
1094        benchmark: &BenchmarkProblem,
1095        optimizer_name: &str,
1096        optimizer_config: &OptimizerConfig<A>,
1097        seed: u64,
1098    ) -> Result<RunResult> {
1099        let run_id = uuid::Uuid::new_v4().to_string();
1100        let start_time = std::time::Instant::now();
1101
1102        let dim = benchmark.dimensions.first().copied().unwrap_or(10).max(1);
1103        let iterations = std::cmp::min(1000, self.settings.max_iterations).max(1);
1104
1105        let mut rng = Random::seed(seed);
1106        let initial: Vec<A> = (0..dim)
1107            .map(|_| A::from(rng.gen_range(-2.0_f64..2.0)).unwrap_or_else(A::zero))
1108            .collect();
1109
1110        let mut param = Parameter::new(Array1::from_vec(initial), "x".to_string());
1111        let mut optimizer = select_optimizer(optimizer_name, optimizer_config.clone());
1112
1113        let mut trajectory = Vec::with_capacity(iterations + 1);
1114
1115        for _ in 0..iterations {
1116            let x: Vec<A> = param.data.iter().copied().collect();
1117            let (value, grad) = evaluate_objective(&benchmark.objective_function.function_type, &x);
1118            trajectory.push(value.to_f64().unwrap_or(f64::NAN));
1119
1120            param.set_grad(Array1::from_vec(grad));
1121            optimizer.step_param(&mut param)?;
1122        }
1123
1124        // Score the point the optimizer actually finished on.
1125        let x: Vec<A> = param.data.iter().copied().collect();
1126        let (final_objective_a, _) =
1127            evaluate_objective(&benchmark.objective_function.function_type, &x);
1128        trajectory.push(final_objective_a.to_f64().unwrap_or(f64::NAN));
1129
1130        let final_objective = final_objective_a.to_f64().unwrap_or(f64::INFINITY);
1131        let execution_time = start_time.elapsed().as_secs_f64();
1132        let converged =
1133            final_objective.is_finite() && final_objective < self.settings.convergence_tolerance;
1134
1135        Ok(RunResult {
1136            run_id,
1137            random_seed: seed,
1138            final_objective,
1139            converged,
1140            iterations,
1141            execution_time,
1142            function_evaluations: iterations + 1,
1143            gradient_evaluations: iterations,
1144            memory_usage: dim * std::mem::size_of::<f64>() * 4,
1145            trajectory,
1146            error_info: None,
1147        })
1148    }
1149
1150    fn calculate_aggregated_metrics(&self, run_results: &[RunResult]) -> HashMap<String, f64> {
1151        let mut metrics = HashMap::new();
1152
1153        if !run_results.is_empty() {
1154            // Final objective metrics
1155            let final_objectives: Vec<f64> =
1156                run_results.iter().map(|r| r.final_objective).collect();
1157            metrics.insert(
1158                "mean_final_objective".to_string(),
1159                final_objectives.iter().sum::<f64>() / final_objectives.len() as f64,
1160            );
1161
1162            let mut sorted_objectives = final_objectives.clone();
1163            sorted_objectives.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1164            metrics.insert(
1165                "median_final_objective".to_string(),
1166                sorted_objectives[sorted_objectives.len() / 2],
1167            );
1168            metrics.insert("best_final_objective".to_string(), sorted_objectives[0]);
1169
1170            // Time metrics
1171            let execution_times: Vec<f64> = run_results.iter().map(|r| r.execution_time).collect();
1172            metrics.insert(
1173                "mean_execution_time".to_string(),
1174                execution_times.iter().sum::<f64>() / execution_times.len() as f64,
1175            );
1176
1177            // Success rate
1178            let successful_runs = run_results.iter().filter(|r| r.converged).count();
1179            metrics.insert(
1180                "success_rate".to_string(),
1181                successful_runs as f64 / run_results.len() as f64,
1182            );
1183        }
1184
1185        metrics
1186    }
1187
1188    fn calculate_statistics(&self, run_results: &[RunResult]) -> ResultStatistics {
1189        if run_results.is_empty() {
1190            return ResultStatistics {
1191                successful_runs: 0,
1192                total_runs: 0,
1193                success_rate: 0.0,
1194                mean_objective: 0.0,
1195                std_objective: 0.0,
1196                best_objective: 0.0,
1197                worst_objective: 0.0,
1198                median_objective: 0.0,
1199                quartiles: (0.0, 0.0, 0.0),
1200                confidence_intervals: HashMap::new(),
1201            };
1202        }
1203
1204        let successful_runs = run_results.iter().filter(|r| r.converged).count();
1205        let total_runs = run_results.len();
1206        let success_rate = successful_runs as f64 / total_runs as f64;
1207
1208        let objectives: Vec<f64> = run_results.iter().map(|r| r.final_objective).collect();
1209        let mean_objective = objectives.iter().sum::<f64>() / objectives.len() as f64;
1210
1211        // Sample variance (Bessel's correction, divide by n-1): these
1212        // `objectives` are a *sample* of independent runs used to infer the
1213        // variability of the underlying optimizer/problem, which is exactly
1214        // the setting the n-1 correction is for. n=1 has no defined sample
1215        // variance (would divide by zero), so it is reported as 0.
1216        let variance = if objectives.len() > 1 {
1217            objectives
1218                .iter()
1219                .map(|&x| (x - mean_objective).powi(2))
1220                .sum::<f64>()
1221                / (objectives.len() - 1) as f64
1222        } else {
1223            0.0
1224        };
1225        let std_objective = variance.sqrt();
1226
1227        let mut sorted_objectives = objectives.clone();
1228        sorted_objectives.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1229
1230        let best_objective = sorted_objectives[0];
1231        let worst_objective = sorted_objectives[sorted_objectives.len() - 1];
1232        let median_objective = sorted_objectives[sorted_objectives.len() / 2];
1233
1234        let q1_idx = sorted_objectives.len() / 4;
1235        let q3_idx = 3 * sorted_objectives.len() / 4;
1236        let quartiles = (
1237            sorted_objectives[q1_idx],
1238            median_objective,
1239            sorted_objectives[q3_idx],
1240        );
1241
1242        let mut confidence_intervals = HashMap::new();
1243        confidence_intervals.insert(
1244            "mean_objective_95".to_string(),
1245            confidence_interval_95(objectives.len(), mean_objective, std_objective),
1246        );
1247        confidence_intervals.insert(
1248            "success_rate_95".to_string(),
1249            wilson_score_interval_95(successful_runs, total_runs),
1250        );
1251
1252        ResultStatistics {
1253            successful_runs,
1254            total_runs,
1255            success_rate,
1256            mean_objective,
1257            std_objective,
1258            best_objective,
1259            worst_objective,
1260            median_objective,
1261            quartiles,
1262            confidence_intervals,
1263        }
1264    }
1265
1266    fn analyze_convergence(&self, run_results: &[RunResult]) -> ConvergenceAnalysis {
1267        if run_results.is_empty() {
1268            return ConvergenceAnalysis {
1269                avg_convergence_rate: 0.0,
1270                convergence_stability: 0.0,
1271                early_convergence: false,
1272                plateau_detected: false,
1273                convergence_pattern: ConvergencePattern::Irregular,
1274            };
1275        }
1276
1277        // Simplified convergence analysis
1278        let avg_convergence_rate = run_results
1279            .iter()
1280            .filter(|r| r.converged)
1281            .map(|r| r.iterations as f64)
1282            .sum::<f64>()
1283            / run_results.len() as f64;
1284
1285        let convergence_stability = 0.8; // Placeholder
1286        let early_convergence = avg_convergence_rate < self.settings.max_iterations as f64 * 0.5;
1287        let plateau_detected = false; // Would analyze trajectories
1288        let convergence_pattern = ConvergencePattern::MonotonicDecrease; // Simplified
1289
1290        ConvergenceAnalysis {
1291            avg_convergence_rate,
1292            convergence_stability,
1293            early_convergence,
1294            plateau_detected,
1295            convergence_pattern,
1296        }
1297    }
1298
1299    fn calculate_overall_scores(&self, results: &mut BenchmarkResults) {
1300        // Calculate weighted scores across all problems and metrics
1301        let mut total_score = 0.0;
1302        let mut total_weight = 0.0;
1303
1304        for metric in &self.suite.metrics {
1305            // `EvaluationMetric::name` is a free-form display string (e.g.
1306            // "Final Objective Value") that never matches the fixed keys
1307            // `calculate_aggregated_metrics` actually inserts (e.g.
1308            // "mean_final_objective") -- go through the metric *type*
1309            // instead, which is the field `calculate_aggregated_metrics`'s
1310            // keys were really chosen to represent.
1311            let Some(key) = aggregated_metric_key(&metric.metric_type) else {
1312                continue;
1313            };
1314
1315            let mut metric_score = 0.0;
1316            let mut metric_count = 0;
1317
1318            for problem_result in results.problem_results.values() {
1319                if let Some(&value) = problem_result.aggregated_metrics.get(key) {
1320                    let normalized_score = match metric.better_direction {
1321                        BetterDirection::Lower => 1.0 / (1.0 + value),
1322                        BetterDirection::Higher => value,
1323                    };
1324                    metric_score += normalized_score;
1325                    metric_count += 1;
1326                }
1327            }
1328
1329            if metric_count > 0 {
1330                metric_score /= metric_count as f64;
1331                total_score += metric_score * metric.weight;
1332                total_weight += metric.weight;
1333
1334                results
1335                    .overall_scores
1336                    .insert(metric.name.clone(), metric_score);
1337            }
1338        }
1339
1340        if total_weight > 0.0 {
1341            results
1342                .overall_scores
1343                .insert("overall_score".to_string(), total_score / total_weight);
1344        }
1345    }
1346
1347    fn calculate_rankings_and_tests(&self, all_results: &mut HashMap<String, BenchmarkResults>) {
1348        // Calculate rankings based on overall scores
1349        let mut optimizer_scores: Vec<(String, f64)> = all_results
1350            .iter()
1351            .filter_map(|(name, results)| {
1352                results
1353                    .overall_scores
1354                    .get("overall_score")
1355                    .map(|&score| (name.clone(), score))
1356            })
1357            .collect();
1358
1359        optimizer_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1360
1361        for (rank, (optimizer_name, score)) in optimizer_scores.iter().enumerate() {
1362            if let Some(results) = all_results.get_mut(optimizer_name) {
1363                results.ranking.overall_rank = rank + 1;
1364                results.ranking.ranking_score = *score;
1365            }
1366        }
1367
1368        // Pairwise two-sample Kolmogorov-Smirnov test between each pair of
1369        // optimizers' pooled final-objective values (across all
1370        // problems/runs), so `statistical_tests` reflects a real comparison
1371        // instead of never being populated.
1372        let pooled_objectives: HashMap<String, Vec<f64>> = all_results
1373            .iter()
1374            .map(|(name, results)| {
1375                let values: Vec<f64> = results
1376                    .problem_results
1377                    .values()
1378                    .flat_map(|p| p.run_results.iter().map(|r| r.final_objective))
1379                    .filter(|v| v.is_finite())
1380                    .collect();
1381                (name.clone(), values)
1382            })
1383            .collect();
1384
1385        let names: Vec<String> = optimizer_scores.into_iter().map(|(name, _)| name).collect();
1386        for i in 0..names.len() {
1387            for j in (i + 1)..names.len() {
1388                let (name_a, name_b) = (&names[i], &names[j]);
1389                let (Some(values_a), Some(values_b)) =
1390                    (pooled_objectives.get(name_a), pooled_objectives.get(name_b))
1391                else {
1392                    continue;
1393                };
1394                if values_a.len() < 2 || values_b.len() < 2 {
1395                    continue;
1396                }
1397
1398                let array_a = Array1::from_vec(values_a.clone());
1399                let array_b = Array1::from_vec(values_b.clone());
1400                let Ok((statistic, p_value)) =
1401                    scirs2_stats::ks_2samp(&array_a.view(), &array_b.view(), "two-sided")
1402                else {
1403                    continue;
1404                };
1405
1406                const SIGNIFICANCE_LEVEL: f64 = 0.05;
1407                let test = StatisticalTest {
1408                    test_name: "Kolmogorov-Smirnov (two-sample)".to_string(),
1409                    optimizers: vec![name_a.clone(), name_b.clone()],
1410                    test_statistic: statistic,
1411                    p_value,
1412                    significance_level: SIGNIFICANCE_LEVEL,
1413                    significant: p_value < SIGNIFICANCE_LEVEL,
1414                    effect_size: None,
1415                };
1416
1417                if let Some(results) = all_results.get_mut(name_a) {
1418                    results.statistical_tests.push(test.clone());
1419                }
1420                if let Some(results) = all_results.get_mut(name_b) {
1421                    results.statistical_tests.push(test);
1422                }
1423            }
1424        }
1425    }
1426}
1427
1428/// Map a benchmark metric's declared type to the key
1429/// [`BenchmarkRunner::calculate_aggregated_metrics`] actually stores it
1430/// under. `EvaluationMetric::name` is a free-form display string (e.g.
1431/// "Final Objective Value") that does not match those keys (e.g.
1432/// "mean_final_objective"); metric types not produced by
1433/// `calculate_aggregated_metrics` are not scored (`None`) rather than
1434/// silently, permanently failing to match anything.
1435fn aggregated_metric_key(metric_type: &MetricType) -> Option<&'static str> {
1436    match metric_type {
1437        MetricType::FinalObjective => Some("mean_final_objective"),
1438        MetricType::TimeToConvergence => Some("mean_execution_time"),
1439        MetricType::SuccessRate => Some("success_rate"),
1440        _ => None,
1441    }
1442}
1443
1444impl Default for BenchmarkSuiteMetadata {
1445    fn default() -> Self {
1446        Self {
1447            version: "1.0.0".to_string(),
1448            authors: Vec::new(),
1449            license: "MIT".to_string(),
1450            references: Vec::new(),
1451            target_audience: vec!["Researchers".to_string(), "Students".to_string()],
1452            keywords: Vec::new(),
1453            changelog: Vec::new(),
1454        }
1455    }
1456}
1457
1458impl Default for BenchmarkSettings {
1459    fn default() -> Self {
1460        Self {
1461            num_runs: 10,
1462            random_seeds: (0..10).map(|i| 42 + i).collect(),
1463            max_iterations: 1000,
1464            max_time_seconds: 300.0, // 5 minutes
1465            convergence_tolerance: 1e-6,
1466            parallel_execution: true,
1467            num_threads: None,
1468            save_detailed_results: true,
1469            output_directory: None,
1470        }
1471    }
1472}
1473
1474#[cfg(test)]
1475mod tests {
1476    use super::*;
1477
1478    #[test]
1479    fn test_benchmark_suite_creation() {
1480        let suite = AcademicBenchmarkSuite::standard_ml_suite();
1481
1482        assert_eq!(suite.name, "Standard ML Optimization Benchmark");
1483        assert!(!suite.benchmarks.is_empty());
1484        assert!(!suite.metrics.is_empty());
1485    }
1486
1487    #[test]
1488    fn test_benchmark_problem_creation() {
1489        let problem = AcademicBenchmarkSuite::create_quadratic_problem();
1490
1491        assert_eq!(problem.name, "10D Quadratic Function");
1492        assert_eq!(problem.category, ProblemCategory::Convex);
1493        assert_eq!(problem.difficulty, DifficultyLevel::Easy);
1494        assert!(problem.optimal_solution.is_some());
1495    }
1496
1497    #[test]
1498    fn test_benchmark_settings() {
1499        let settings = BenchmarkSettings::default();
1500
1501        assert_eq!(settings.num_runs, 10);
1502        assert_eq!(settings.max_iterations, 1000);
1503        assert!(settings.parallel_execution);
1504    }
1505
1506    fn make_runner(num_runs: usize, max_iterations: usize) -> BenchmarkRunner {
1507        let suite = AcademicBenchmarkSuite::new("Test Suite");
1508        let settings = BenchmarkSettings {
1509            num_runs,
1510            random_seeds: Vec::new(),
1511            max_iterations,
1512            max_time_seconds: 30.0,
1513            convergence_tolerance: 1e-6,
1514            parallel_execution: false,
1515            num_threads: None,
1516            save_detailed_results: false,
1517            output_directory: None,
1518        };
1519        BenchmarkRunner::new(suite, settings)
1520    }
1521
1522    // Regression tests for F19: `run_single_instance` used to ignore both
1523    // `optimizer_config` and `seed` entirely, drawing the "final objective"
1524    // from a fixed `Rng` range keyed only on the problem's `FunctionType`.
1525
1526    #[test]
1527    fn test_run_single_instance_is_deterministic_given_a_seed() {
1528        let runner = make_runner(1, 50);
1529        let problem = AcademicBenchmarkSuite::create_quadratic_problem();
1530        let config: OptimizerConfig<f64> = OptimizerConfig::new(0.1);
1531
1532        let run_a = runner
1533            .run_single_instance::<f64>(&problem, "sgd", &config, 7)
1534            .expect("run should succeed");
1535        let run_b = runner
1536            .run_single_instance::<f64>(&problem, "sgd", &config, 7)
1537            .expect("run should succeed");
1538
1539        assert_eq!(run_a.final_objective, run_b.final_objective);
1540        assert_eq!(run_a.trajectory, run_b.trajectory);
1541    }
1542
1543    #[test]
1544    fn test_run_single_instance_respects_optimizer_config() {
1545        let runner = make_runner(1, 30);
1546        let problem = AcademicBenchmarkSuite::create_quadratic_problem();
1547
1548        let stable_config: OptimizerConfig<f64> = OptimizerConfig::new(0.01);
1549        // Far past the stability limit for gradient descent on a unit
1550        // quadratic (which requires lr < 2.0): must diverge.
1551        let unstable_config: OptimizerConfig<f64> = OptimizerConfig::new(50.0);
1552
1553        let stable = runner
1554            .run_single_instance::<f64>(&problem, "sgd", &stable_config, 1)
1555            .expect("run should succeed");
1556        let unstable = runner
1557            .run_single_instance::<f64>(&problem, "sgd", &unstable_config, 1)
1558            .expect("run should succeed");
1559
1560        assert!(stable.final_objective.is_finite());
1561        assert!(
1562            unstable.final_objective > stable.final_objective,
1563            "an unstable learning rate must not converge as well as a stable one \
1564             (stable={}, unstable={}) -- the config must actually be used",
1565            stable.final_objective,
1566            unstable.final_objective
1567        );
1568    }
1569
1570    #[test]
1571    fn test_run_single_instance_uses_the_seed_for_the_initial_point() {
1572        let runner = make_runner(1, 5);
1573        let problem = AcademicBenchmarkSuite::create_quadratic_problem();
1574        let config: OptimizerConfig<f64> = OptimizerConfig::new(0.01);
1575
1576        let run_a = runner
1577            .run_single_instance::<f64>(&problem, "sgd", &config, 1)
1578            .expect("run should succeed");
1579        let run_b = runner
1580            .run_single_instance::<f64>(&problem, "sgd", &config, 2)
1581            .expect("run should succeed");
1582
1583        // trajectory[0] is the objective at the seed-derived initial point,
1584        // before any optimizer step.
1585        assert_ne!(
1586            run_a.trajectory[0], run_b.trajectory[0],
1587            "different seeds must produce different starting points"
1588        );
1589    }
1590
1591    #[test]
1592    fn test_select_optimizer_dispatches_by_name() {
1593        let config: OptimizerConfig<f64> = OptimizerConfig::new(0.1);
1594        assert!(matches!(
1595            select_optimizer("Adam", config.clone()),
1596            ChosenOptimizer::Adam(_)
1597        ));
1598        assert!(matches!(
1599            select_optimizer("adamw", config.clone()),
1600            ChosenOptimizer::Adam(_)
1601        ));
1602        assert!(matches!(
1603            select_optimizer("sgd", config.clone()),
1604            ChosenOptimizer::Sgd(_)
1605        ));
1606        assert!(matches!(
1607            select_optimizer("unknown", config),
1608            ChosenOptimizer::Sgd(_)
1609        ));
1610    }
1611
1612    // Regression test for F20: `EvaluationMetric::name` display strings
1613    // never matched `calculate_aggregated_metrics`'s fixed keys, so
1614    // `overall_score` was never populated and every optimizer's
1615    // `overall_rank` stayed at its default of 0.
1616    #[test]
1617    fn test_run_benchmarks_produces_nonzero_distinct_ranks() {
1618        let mut suite = AcademicBenchmarkSuite::new("Ranking Test Suite");
1619        suite.add_benchmark(AcademicBenchmarkSuite::create_quadratic_problem());
1620        suite.add_metric(AcademicBenchmarkSuite::create_final_objective_metric());
1621        suite.add_metric(AcademicBenchmarkSuite::create_convergence_time_metric());
1622        suite.add_metric(AcademicBenchmarkSuite::create_success_rate_metric());
1623
1624        let settings = BenchmarkSettings {
1625            num_runs: 3,
1626            random_seeds: Vec::new(),
1627            max_iterations: 20,
1628            max_time_seconds: 30.0,
1629            convergence_tolerance: 1e-6,
1630            parallel_execution: false,
1631            num_threads: None,
1632            save_detailed_results: false,
1633            output_directory: None,
1634        };
1635        let runner = BenchmarkRunner::new(suite, settings);
1636
1637        let optimizers: Vec<(&str, OptimizerConfig<f64>)> = vec![
1638            ("good_sgd", OptimizerConfig::new(0.1)),
1639            ("bad_sgd", OptimizerConfig::new(50.0)),
1640        ];
1641
1642        let results = runner
1643            .run_benchmarks::<f64>(&optimizers)
1644            .expect("benchmarks should run");
1645
1646        let good = &results["good_sgd"];
1647        let bad = &results["bad_sgd"];
1648
1649        assert_ne!(
1650            good.ranking.overall_rank, 0,
1651            "rank must not stay at the default 0"
1652        );
1653        assert_ne!(
1654            bad.ranking.overall_rank, 0,
1655            "rank must not stay at the default 0"
1656        );
1657        assert_ne!(good.ranking.overall_rank, bad.ranking.overall_rank);
1658        assert!(good.overall_scores.contains_key("overall_score"));
1659        assert_eq!(
1660            good.ranking.overall_rank, 1,
1661            "the well-tuned optimizer should outrank the divergent one"
1662        );
1663    }
1664
1665    fn make_run_result(final_objective: f64) -> RunResult {
1666        RunResult {
1667            run_id: uuid::Uuid::new_v4().to_string(),
1668            random_seed: 0,
1669            final_objective,
1670            converged: final_objective < 1.0,
1671            iterations: 10,
1672            execution_time: 0.001,
1673            function_evaluations: 10,
1674            gradient_evaluations: 10,
1675            memory_usage: 0,
1676            trajectory: vec![final_objective],
1677            error_info: None,
1678        }
1679    }
1680
1681    // Regression tests for F90: `confidence_intervals` was always an empty
1682    // map, and `std_objective` used the population-variance denominator `n`
1683    // instead of the sample-variance denominator `n-1` appropriate for a
1684    // sample of independent runs.
1685    #[test]
1686    fn test_calculate_statistics_reports_real_confidence_intervals_and_sample_std() {
1687        let runner = make_runner(1, 1);
1688        let run_results = vec![
1689            make_run_result(1.0),
1690            make_run_result(2.0),
1691            make_run_result(3.0),
1692            make_run_result(4.0),
1693            make_run_result(5.0),
1694        ];
1695
1696        let stats = runner.calculate_statistics(&run_results);
1697
1698        // Sample std (n-1 denominator) of [1,2,3,4,5] is sqrt(2.5); the
1699        // population std (n denominator) would be sqrt(2.0) instead.
1700        assert!(
1701            (stats.std_objective - 2.5_f64.sqrt()).abs() < 1e-9,
1702            "expected sample std sqrt(2.5) ~= {:.4}, got {}",
1703            2.5_f64.sqrt(),
1704            stats.std_objective
1705        );
1706
1707        assert!(!stats.confidence_intervals.is_empty());
1708        let (lower, upper) = stats.confidence_intervals["mean_objective_95"];
1709        assert!(lower < stats.mean_objective && stats.mean_objective < upper);
1710        let (rate_lower, rate_upper) = stats.confidence_intervals["success_rate_95"];
1711        assert!((0.0..=1.0).contains(&rate_lower));
1712        assert!((0.0..=1.0).contains(&rate_upper));
1713    }
1714
1715    #[test]
1716    fn test_student_t_critical_value_matches_known_table_value() {
1717        // t_{0.975, df=1} is a well-known tabulated constant (~12.706).
1718        let t = student_t_critical_value(1.0, 0.975);
1719        assert!((t - 12.706).abs() < 0.01, "expected ~12.706, got {t}");
1720    }
1721
1722    #[test]
1723    fn test_wilson_score_interval_stays_within_unit_bounds() {
1724        let (lower, upper) = wilson_score_interval_95(8, 10);
1725        assert!((0.0..=1.0).contains(&lower));
1726        assert!((0.0..=1.0).contains(&upper));
1727        assert!(lower < 0.8 && upper > 0.8);
1728    }
1729
1730    #[test]
1731    fn test_calculate_statistics_sort_does_not_panic_on_nan() {
1732        // Regression for the reachable NaN-panic half of F83/F90: a
1733        // divergent run can legitimately produce a non-finite objective.
1734        let runner = make_runner(1, 1);
1735        let run_results = vec![
1736            make_run_result(1.0),
1737            make_run_result(f64::NAN),
1738            make_run_result(2.0),
1739        ];
1740
1741        let stats = runner.calculate_statistics(&run_results);
1742        assert_eq!(stats.total_runs, 3);
1743    }
1744}