1#[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#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct AcademicBenchmarkSuite {
19 pub id: String,
21 pub name: String,
23 pub description: String,
25 pub benchmarks: Vec<BenchmarkProblem>,
27 pub metrics: Vec<EvaluationMetric>,
29 pub reference_results: HashMap<String, BenchmarkResults>,
31 pub metadata: BenchmarkSuiteMetadata,
33 pub created_at: DateTime<Utc>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BenchmarkProblem {
40 pub id: String,
42 pub name: String,
44 pub description: String,
46 pub category: ProblemCategory,
48 pub difficulty: DifficultyLevel,
50 pub dimensions: Vec<usize>,
52 pub objective_function: ObjectiveFunction,
54 pub constraints: Vec<Constraint>,
56 pub optimal_solution: Option<OptimalSolution>,
58 pub parameters: HashMap<String, f64>,
60 pub references: Vec<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
66pub enum ProblemCategory {
67 Convex,
69 NonConvex,
71 MachineLearning,
73 DeepLearning,
75 ReinforcementLearning,
77 ComputerVision,
79 NaturalLanguageProcessing,
81 NumericalOptimization,
83 ConstrainedOptimization,
85 MultiObjective,
87 Stochastic,
89 Discrete,
91 Continuous,
93 Mixed,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
99pub enum DifficultyLevel {
100 Easy,
102 Medium,
104 Hard,
106 VeryHard,
108 Extreme,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ObjectiveFunction {
115 pub name: String,
117 pub function_type: FunctionType,
119 pub properties: FunctionProperties,
121 pub mathematical_form: String,
123 pub implementation_notes: String,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
129pub enum FunctionType {
130 Quadratic,
132 Rosenbrock,
134 Sphere,
136 Rastrigin,
138 Ackley,
140 Griewank,
142 Schwefel,
144 Himmelblau,
146 Booth,
148 Beale,
150 ThreeHumpCamel,
152 SixHumpCamel,
154 CrossInTray,
156 EggHolder,
158 HolderTable,
160 McCormick,
162 SchafferN2,
164 SchafferN4,
166 StyblinskiTang,
168 Custom(String),
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct FunctionProperties {
175 pub differentiable: bool,
177 pub continuous: bool,
179 pub convex: bool,
181 pub separable: bool,
183 pub multimodal: bool,
185 pub smoothness: SmoothnesLevel,
187 pub condition_number: Option<f64>,
189 pub lipschitz_constant: Option<f64>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
195pub enum SmoothnesLevel {
196 VerySmooth,
198 Smooth,
200 ModeratelySmooth,
202 Rough,
204 VeryRough,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct Constraint {
211 pub constraint_type: ConstraintType,
213 pub description: String,
215 pub mathematical_form: String,
217 pub parameters: HashMap<String, f64>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
223pub enum ConstraintType {
224 Equality,
226 Inequality,
228 Box,
230 Linear,
232 Nonlinear,
234 Integer,
236 Binary,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct OptimalSolution {
243 pub parameters: Array1<f64>,
245 pub objective_value: f64,
247 pub properties: SolutionProperties,
249 pub reference: Option<String>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct SolutionProperties {
256 pub global_optimum: bool,
258 pub local_optimum: bool,
260 pub unique: bool,
262 pub stable: bool,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct EvaluationMetric {
269 pub name: String,
271 pub description: String,
273 pub metric_type: MetricType,
275 pub aggregation: AggregationMethod,
277 pub better_direction: BetterDirection,
279 pub weight: f64,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
285pub enum MetricType {
286 FinalObjective,
288 IterationsToConvergence,
290 TimeToConvergence,
292 FunctionEvaluations,
294 GradientEvaluations,
296 SuccessRate,
298 SolutionQuality,
300 ConvergenceRate,
302 Robustness,
304 MemoryUsage,
306 EnergyConsumption,
308 Custom(String),
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
314pub enum AggregationMethod {
315 Mean,
317 Median,
319 Best,
321 Worst,
323 StandardDeviation,
325 Percentile(u8),
327 SuccessCount,
329 Custom(String),
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
335pub enum BetterDirection {
336 Higher,
338 Lower,
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct BenchmarkResults {
345 pub optimizer_name: String,
347 pub problem_results: HashMap<String, ProblemResults>,
349 pub overall_scores: HashMap<String, f64>,
351 pub statistical_tests: Vec<StatisticalTest>,
353 pub ranking: OptimizerRanking,
355 pub executed_at: DateTime<Utc>,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ProblemResults {
362 pub problem_id: String,
364 pub run_results: Vec<RunResult>,
366 pub aggregated_metrics: HashMap<String, f64>,
368 pub statistics: ResultStatistics,
370 pub convergence_analysis: ConvergenceAnalysis,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct RunResult {
377 pub run_id: String,
379 pub random_seed: u64,
381 pub final_objective: f64,
383 pub converged: bool,
385 pub iterations: usize,
387 pub execution_time: f64,
389 pub function_evaluations: usize,
391 pub gradient_evaluations: usize,
393 pub memory_usage: usize,
395 pub trajectory: Vec<f64>,
397 pub error_info: Option<String>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct ResultStatistics {
404 pub successful_runs: usize,
406 pub total_runs: usize,
408 pub success_rate: f64,
410 pub mean_objective: f64,
412 pub std_objective: f64,
414 pub best_objective: f64,
416 pub worst_objective: f64,
418 pub median_objective: f64,
420 pub quartiles: (f64, f64, f64), pub confidence_intervals: HashMap<String, (f64, f64)>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct ConvergenceAnalysis {
429 pub avg_convergence_rate: f64,
431 pub convergence_stability: f64,
433 pub early_convergence: bool,
435 pub plateau_detected: bool,
437 pub convergence_pattern: ConvergencePattern,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
443pub enum ConvergencePattern {
444 MonotonicDecrease,
446 ExponentialDecay,
448 LinearDecrease,
450 Oscillatory,
452 Stepwise,
454 PlateauThenDrop,
456 Irregular,
458}
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct StatisticalTest {
463 pub test_name: String,
465 pub optimizers: Vec<String>,
467 pub test_statistic: f64,
469 pub p_value: f64,
471 pub significance_level: f64,
473 pub significant: bool,
475 pub effect_size: Option<f64>,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct OptimizerRanking {
482 pub overall_rank: usize,
484 pub category_ranks: HashMap<String, usize>,
486 pub metric_ranks: HashMap<String, usize>,
488 pub ranking_score: f64,
490 pub ranking_method: RankingMethod,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
496pub enum RankingMethod {
497 AverageRank,
499 WeightedScore,
501 ParetoDominance,
503 WinLossTie,
505 Tournament,
507 Custom(String),
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct BenchmarkSuiteMetadata {
514 pub version: String,
516 pub authors: Vec<String>,
518 pub license: String,
520 pub references: Vec<String>,
522 pub target_audience: Vec<String>,
524 pub keywords: Vec<String>,
526 pub changelog: Vec<ChangelogEntry>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct ChangelogEntry {
533 pub version: String,
535 pub date: DateTime<Utc>,
537 pub changes: String,
539 pub author: String,
541}
542
543pub struct BenchmarkRunner {
545 suite: AcademicBenchmarkSuite,
547 settings: BenchmarkSettings,
549 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#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct BenchmarkSettings {
566 pub num_runs: usize,
568 pub random_seeds: Vec<u64>,
570 pub max_iterations: usize,
572 pub max_time_seconds: f64,
574 pub convergence_tolerance: f64,
576 pub parallel_execution: bool,
578 pub num_threads: Option<usize>,
580 pub save_detailed_results: bool,
582 pub output_directory: Option<String>,
584}
585
586impl AcademicBenchmarkSuite {
587 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 pub fn add_benchmark(&mut self, benchmark: BenchmarkProblem) {
603 self.benchmarks.push(benchmark);
604 }
605
606 pub fn add_metric(&mut self, metric: EvaluationMetric) {
608 self.metrics.push(metric);
609 }
610
611 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 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 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, 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], 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, 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
818enum 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
839fn 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
854fn 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
898fn 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
915fn student_t_critical_value(df: f64, quantile: f64) -> f64 {
921 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 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
949fn 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; 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 pub fn new(suite: AcademicBenchmarkSuite, settings: BenchmarkSettings) -> Self {
974 Self {
975 suite,
976 settings,
977 progress_callback: None,
978 }
979 }
980
981 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 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 self.calculate_overall_scores(&mut optimizer_results);
1032
1033 all_results.insert(optimizer_name.to_string(), optimizer_results);
1034 }
1035
1036 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 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 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 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 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 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 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 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 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; let early_convergence = avg_convergence_rate < self.settings.max_iterations as f64 * 0.5;
1287 let plateau_detected = false; let convergence_pattern = ConvergencePattern::MonotonicDecrease; 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 let mut total_score = 0.0;
1302 let mut total_weight = 0.0;
1303
1304 for metric in &self.suite.metrics {
1305 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 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 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
1428fn 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, 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 #[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 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 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 #[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 #[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 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 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 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}