1use scirs2_core::numeric::Float;
22use std::collections::HashMap;
23use std::fmt::Debug;
24use std::time::{Duration, Instant, SystemTime};
25
26use super::types::*;
27use crate::error::{OptimError, Result};
28use crate::{EvaluationConfig, OptimizerArchitecture, ResourceUsage};
29
30#[derive(Debug, Clone)]
32pub struct TestFunction<T: Float + Debug + Send + Sync + 'static> {
33 pub function_type: TestFunctionType,
35
36 pub parameters: HashMap<String, T>,
38
39 pub dimensions: usize,
41
42 pub max_evaluations: usize,
44
45 pub target_performance: Option<T>,
47}
48
49#[derive(Debug, Clone)]
51pub struct StandardBenchmark<T: Float + Debug + Send + Sync + 'static> {
52 pub name: String,
54
55 pub benchmark_type: BenchmarkType,
57
58 pub test_function: TestFunction<T>,
60
61 pub expected_range: (T, T),
63
64 pub difficulty: DifficultyLevel,
66
67 pub resource_requirements: ResourceRequirements,
69}
70
71#[derive(Debug, Clone)]
73pub struct BenchmarkMetadata {
74 pub name: String,
76
77 pub version: String,
79
80 pub description: String,
82
83 pub created_at: SystemTime,
85
86 pub updated_at: SystemTime,
88
89 pub author: String,
91
92 pub license: String,
94}
95
96#[derive(Debug, Clone)]
98pub struct BenchmarkResults<T: Float + Debug + Send + Sync + 'static> {
99 pub test_results: Vec<TestResult<T>>,
101
102 pub overall_score: T,
104
105 pub ranking: PerformanceRanking,
107
108 pub statistical_summary: StatisticalSummary<T>,
110
111 pub resource_summary: ResourceSummary<T>,
113}
114
115#[derive(Debug, Clone)]
117pub struct TestResult<T: Float + Debug + Send + Sync + 'static> {
118 pub test_name: String,
120
121 pub score: T,
123
124 pub normalized_score: T,
126
127 pub percentile_rank: T,
129
130 pub execution_time: Duration,
132
133 pub resource_usage: ResourceUsage<T>,
135
136 pub metrics: HashMap<String, T>,
138}
139
140#[derive(Debug)]
142pub struct BenchmarkSuite<T: Float + Debug + Send + Sync + 'static> {
143 standard_benchmarks: Vec<StandardBenchmark<T>>,
145
146 metadata: BenchmarkMetadata,
149
150 score_history: HashMap<String, Vec<f64>>,
156
157 steps_per_benchmark: usize,
160}
161
162const DEFAULT_STEPS_PER_BENCHMARK: usize = 60;
165
166const MAX_STEPS_PER_BENCHMARK: usize = 400;
172
173impl<T: Float + Debug + Default + Send + Sync> BenchmarkSuite<T> {
174 pub(crate) fn new() -> Result<Self> {
175 Ok(Self {
176 standard_benchmarks: Vec::new(),
177 metadata: BenchmarkMetadata {
178 name: "Standard Benchmark Suite".to_string(),
179 version: env!("CARGO_PKG_VERSION").to_string(),
180 description: "Optimizer evaluation suite over classical \
181 continuous-optimization test functions"
182 .to_string(),
183 created_at: SystemTime::now(),
184 updated_at: SystemTime::now(),
185 author: "COOLJAPAN OU (Team KitaSan)".to_string(),
186 license: "Apache-2.0".to_string(),
187 },
188 score_history: HashMap::new(),
189 steps_per_benchmark: DEFAULT_STEPS_PER_BENCHMARK,
190 })
191 }
192
193 pub(crate) fn initialize(&mut self, config: &EvaluationConfig) -> Result<()> {
194 self.steps_per_benchmark = (config.epochs as usize).clamp(1, MAX_STEPS_PER_BENCHMARK);
197
198 self.standard_benchmarks.clear();
200 self.add_standard_benchmarks()?;
201 Ok(())
202 }
203
204 pub fn steps_per_benchmark(&self) -> usize {
206 self.steps_per_benchmark
207 }
208
209 pub fn metadata(&self) -> &BenchmarkMetadata {
212 &self.metadata
213 }
214
215 pub fn standard_benchmarks(&self) -> &[StandardBenchmark<T>] {
217 &self.standard_benchmarks
218 }
219
220 fn add_standard_benchmarks(&mut self) -> Result<()> {
221 self.standard_benchmarks.push(StandardBenchmark {
223 name: "Rosenbrock".to_string(),
224 benchmark_type: BenchmarkType::NonConvexOptimization,
225 test_function: TestFunction {
226 function_type: TestFunctionType::Rosenbrock,
227 parameters: HashMap::new(),
228 dimensions: 10,
229 max_evaluations: 1000,
230 target_performance: Some(
231 scirs2_core::numeric::NumCast::from(1e-6).unwrap_or_else(|| T::zero()),
232 ),
233 },
234 expected_range: (
235 scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero()),
236 scirs2_core::numeric::NumCast::from(1e-2).unwrap_or_else(|| T::zero()),
237 ),
238 difficulty: DifficultyLevel::Medium,
239 resource_requirements: ResourceRequirements {
240 memory_mb: 100,
241 cpu_cores: 1,
242 gpu_memory_mb: None,
243 max_runtime_seconds: 300,
244 storage_mb: 10,
245 },
246 });
247
248 self.standard_benchmarks.push(StandardBenchmark {
250 name: "Quadratic".to_string(),
251 benchmark_type: BenchmarkType::ConvergenceSpeed,
252 test_function: TestFunction {
253 function_type: TestFunctionType::Quadratic,
254 parameters: HashMap::new(),
255 dimensions: 20,
256 max_evaluations: 500,
257 target_performance: Some(
258 scirs2_core::numeric::NumCast::from(1e-8).unwrap_or_else(|| T::zero()),
259 ),
260 },
261 expected_range: (
262 scirs2_core::numeric::NumCast::from(1e-10).unwrap_or_else(|| T::zero()),
263 scirs2_core::numeric::NumCast::from(1e-4).unwrap_or_else(|| T::zero()),
264 ),
265 difficulty: DifficultyLevel::Easy,
266 resource_requirements: ResourceRequirements {
267 memory_mb: 50,
268 cpu_cores: 1,
269 gpu_memory_mb: None,
270 max_runtime_seconds: 120,
271 storage_mb: 5,
272 },
273 });
274
275 self.standard_benchmarks.push(StandardBenchmark {
277 name: "Rastrigin".to_string(),
278 benchmark_type: BenchmarkType::NonConvexOptimization,
279 test_function: TestFunction {
280 function_type: TestFunctionType::Rastrigin,
281 parameters: HashMap::new(),
282 dimensions: 10,
283 max_evaluations: 1000,
284 target_performance: Some(
285 scirs2_core::numeric::NumCast::from(1e-4).unwrap_or_else(|| T::zero()),
286 ),
287 },
288 expected_range: (
289 scirs2_core::numeric::NumCast::from(1e-6).unwrap_or_else(|| T::zero()),
290 scirs2_core::numeric::NumCast::from(1e1).unwrap_or_else(|| T::zero()),
291 ),
292 difficulty: DifficultyLevel::Hard,
293 resource_requirements: ResourceRequirements {
294 memory_mb: 100,
295 cpu_cores: 1,
296 gpu_memory_mb: None,
297 max_runtime_seconds: 300,
298 storage_mb: 10,
299 },
300 });
301
302 self.standard_benchmarks.push(StandardBenchmark {
304 name: "Sphere".to_string(),
305 benchmark_type: BenchmarkType::ConvergenceSpeed,
306 test_function: TestFunction {
307 function_type: TestFunctionType::Sphere,
308 parameters: HashMap::new(),
309 dimensions: 20,
310 max_evaluations: 500,
311 target_performance: Some(
312 scirs2_core::numeric::NumCast::from(1e-10).unwrap_or_else(|| T::zero()),
313 ),
314 },
315 expected_range: (
316 scirs2_core::numeric::NumCast::from(1e-12).unwrap_or_else(|| T::zero()),
317 scirs2_core::numeric::NumCast::from(1e-2).unwrap_or_else(|| T::zero()),
318 ),
319 difficulty: DifficultyLevel::Easy,
320 resource_requirements: ResourceRequirements {
321 memory_mb: 50,
322 cpu_cores: 1,
323 gpu_memory_mb: None,
324 max_runtime_seconds: 120,
325 storage_mb: 5,
326 },
327 });
328
329 Ok(())
330 }
331
332 pub(crate) fn run_benchmarks(
339 &mut self,
340 architecture: &OptimizerArchitecture<T>,
341 ) -> Result<Vec<TestResult<T>>> {
342 let mut results = Vec::with_capacity(self.standard_benchmarks.len());
343
344 for idx in 0..self.standard_benchmarks.len() {
347 let benchmark = self.standard_benchmarks[idx].clone();
348 let result = self.run_single_benchmark(&benchmark, architecture)?;
349 results.push(result);
350 }
351
352 Ok(results)
353 }
354
355 fn run_single_benchmark(
360 &mut self,
361 benchmark: &StandardBenchmark<T>,
362 architecture: &OptimizerArchitecture<T>,
363 ) -> Result<TestResult<T>> {
364 let function_type = &benchmark.test_function.function_type;
365 let dimensions = benchmark.test_function.dimensions.max(1);
366
367 let mut params = initial_point(function_type, dimensions);
371 let initial_objective = objective_value(function_type, ¶ms)?;
372
373 let spec = OptimizerSpec::from_architecture(architecture);
374 let mut optimizer = CandidateOptimizer::new(spec.clone(), params.len());
375
376 let start_time = Instant::now();
377 let mut best_objective = initial_objective;
378
379 for _ in 0..self.steps_per_benchmark {
380 let gradient = objective_gradient(function_type, ¶ms)?;
381 optimizer.step(&mut params, &gradient);
382
383 if params.iter().any(|v| !v.is_finite()) {
387 break;
388 }
389
390 let value = objective_value(function_type, ¶ms)?;
391 if value < best_objective {
392 best_objective = value;
393 }
394 }
395
396 let execution_time = start_time.elapsed();
397
398 let normalized = if initial_objective > 0.0 && initial_objective.is_finite() {
402 ((initial_objective - best_objective) / initial_objective).clamp(0.0, 1.0)
403 } else {
404 0.0
405 };
406
407 let history = self
410 .score_history
411 .entry(benchmark.name.clone())
412 .or_default();
413 history.push(normalized);
414 let total = history.len();
415 let at_or_below = history.iter().filter(|&&s| s <= normalized).count();
416 let percentile = at_or_below as f64 / total as f64;
417
418 let working_set_bytes =
423 (dimensions * spec.state_slots_per_parameter() * std::mem::size_of::<f64>()) as f64;
424 let memory_gb = working_set_bytes / 1.0e9;
425 let elapsed_secs = execution_time.as_secs_f64();
426
427 let mut metrics = HashMap::new();
428 metrics.insert(
429 "initial_objective".to_string(),
430 to_t::<T>(initial_objective),
431 );
432 metrics.insert("final_objective".to_string(), to_t::<T>(best_objective));
433 metrics.insert(
434 "steps".to_string(),
435 to_t::<T>(self.steps_per_benchmark as f64),
436 );
437 metrics.insert("learning_rate".to_string(), to_t::<T>(spec.learning_rate));
438
439 Ok(TestResult {
440 test_name: benchmark.name.clone(),
441 score: to_t::<T>(best_objective),
442 normalized_score: to_t::<T>(normalized),
443 percentile_rank: to_t::<T>(percentile),
444 execution_time,
445 resource_usage: ResourceUsage {
446 memory_gb: to_t::<T>(memory_gb),
447 cpu_time_seconds: to_t::<T>(elapsed_secs),
448 gpu_time_seconds: T::zero(),
449 energy_kwh: T::zero(),
450 network_io_gb: T::zero(),
451 disk_io_gb: T::zero(),
452 peak_memory_gb: to_t::<T>(memory_gb),
453 efficiency_score: to_t::<T>(normalized / (1.0 + elapsed_secs)),
454 cost_usd: T::zero(),
455 network_gb: T::zero(),
456 },
457 metrics,
458 })
459 }
460}
461
462fn to_t<T: Float>(value: f64) -> T {
464 scirs2_core::numeric::NumCast::from(value).unwrap_or_else(T::zero)
465}
466
467fn initial_point(function_type: &TestFunctionType, dimensions: usize) -> Vec<f64> {
477 match function_type {
478 TestFunctionType::Rosenbrock => (0..dimensions)
479 .map(|i| if i % 2 == 0 { -1.2 } else { 1.0 })
480 .collect(),
481 TestFunctionType::Rastrigin => (0..dimensions)
482 .map(|i| 2.5 - 0.1 * ((i % 7) as f64))
483 .collect(),
484 TestFunctionType::Ackley => (0..dimensions)
485 .map(|i| 3.0 - 0.2 * ((i % 5) as f64))
486 .collect(),
487 TestFunctionType::Beale => vec![1.0, 1.0],
488 _ => (0..dimensions)
490 .map(|i| 1.0 + 0.5 * ((i % 4) as f64))
491 .collect(),
492 }
493}
494
495fn objective_value(function_type: &TestFunctionType, x: &[f64]) -> Result<f64> {
501 if x.is_empty() {
502 return Err(OptimError::EvaluationError(
503 "test function evaluated on an empty parameter vector".to_string(),
504 ));
505 }
506
507 let value = match function_type {
508 TestFunctionType::Quadratic => x
510 .iter()
511 .enumerate()
512 .map(|(i, &xi)| (1.0 + i as f64) * xi * xi)
513 .sum(),
514
515 TestFunctionType::Sphere => x.iter().map(|&xi| xi * xi).sum(),
517
518 TestFunctionType::Rosenbrock => {
520 let mut acc = 0.0;
521 for i in 0..x.len().saturating_sub(1) {
522 let t = x[i + 1] - x[i] * x[i];
523 let u = 1.0 - x[i];
524 acc += 100.0 * t * t + u * u;
525 }
526 acc
527 }
528
529 TestFunctionType::Rastrigin => {
531 let two_pi = 2.0 * std::f64::consts::PI;
532 10.0 * x.len() as f64
533 + x.iter()
534 .map(|&xi| xi * xi - 10.0 * (two_pi * xi).cos())
535 .sum::<f64>()
536 }
537
538 TestFunctionType::Ackley => {
540 let n = x.len() as f64;
541 let two_pi = 2.0 * std::f64::consts::PI;
542 let sum_sq = x.iter().map(|&xi| xi * xi).sum::<f64>() / n;
543 let sum_cos = x.iter().map(|&xi| (two_pi * xi).cos()).sum::<f64>() / n;
544 -20.0 * (-0.2 * sum_sq.sqrt()).exp() - sum_cos.exp() + 20.0 + std::f64::consts::E
545 }
546
547 TestFunctionType::Beale => {
549 if x.len() < 2 {
550 return Err(OptimError::EvaluationError(
551 "Beale test function requires two dimensions".to_string(),
552 ));
553 }
554 let (a, b) = (x[0], x[1]);
555 let t1 = 1.5 - a + a * b;
556 let t2 = 2.25 - a + a * b * b;
557 let t3 = 2.625 - a + a * b * b * b;
558 t1 * t1 + t2 * t2 + t3 * t3
559 }
560
561 other => {
562 return Err(OptimError::NotImplemented(format!(
563 "test function {:?} has no analytic implementation",
564 other
565 )))
566 }
567 };
568
569 Ok(value)
570}
571
572fn objective_gradient(function_type: &TestFunctionType, x: &[f64]) -> Result<Vec<f64>> {
574 if x.is_empty() {
575 return Err(OptimError::EvaluationError(
576 "test function differentiated on an empty parameter vector".to_string(),
577 ));
578 }
579
580 let gradient = match function_type {
581 TestFunctionType::Quadratic => x
582 .iter()
583 .enumerate()
584 .map(|(i, &xi)| 2.0 * (1.0 + i as f64) * xi)
585 .collect(),
586
587 TestFunctionType::Sphere => x.iter().map(|&xi| 2.0 * xi).collect(),
588
589 TestFunctionType::Rosenbrock => {
590 let n = x.len();
591 let mut g = vec![0.0; n];
592 for i in 0..n.saturating_sub(1) {
593 let t = x[i + 1] - x[i] * x[i];
594 g[i] += -400.0 * x[i] * t - 2.0 * (1.0 - x[i]);
595 g[i + 1] += 200.0 * t;
596 }
597 g
598 }
599
600 TestFunctionType::Rastrigin => {
601 let two_pi = 2.0 * std::f64::consts::PI;
602 x.iter()
603 .map(|&xi| 2.0 * xi + 10.0 * two_pi * (two_pi * xi).sin())
604 .collect()
605 }
606
607 TestFunctionType::Ackley => {
608 let n = x.len() as f64;
609 let two_pi = 2.0 * std::f64::consts::PI;
610 let mean_sq = x.iter().map(|&xi| xi * xi).sum::<f64>() / n;
611 let root = mean_sq.sqrt();
612 let mean_cos = x.iter().map(|&xi| (two_pi * xi).cos()).sum::<f64>() / n;
613 let exp1 = (-0.2 * root).exp();
614 let exp2 = mean_cos.exp();
615
616 x.iter()
617 .map(|&xi| {
618 let first = if root > 1e-12 {
620 4.0 * exp1 * xi / (n * root)
621 } else {
622 0.0
623 };
624 let second = exp2 * two_pi * (two_pi * xi).sin() / n;
626 first + second
627 })
628 .collect()
629 }
630
631 TestFunctionType::Beale => {
632 if x.len() < 2 {
633 return Err(OptimError::EvaluationError(
634 "Beale test function requires two dimensions".to_string(),
635 ));
636 }
637 let (a, b) = (x[0], x[1]);
638 let t1 = 1.5 - a + a * b;
639 let t2 = 2.25 - a + a * b * b;
640 let t3 = 2.625 - a + a * b * b * b;
641 let da = 2.0 * t1 * (b - 1.0) + 2.0 * t2 * (b * b - 1.0) + 2.0 * t3 * (b * b * b - 1.0);
642 let db = 2.0 * t1 * a + 2.0 * t2 * (2.0 * a * b) + 2.0 * t3 * (3.0 * a * b * b);
643 vec![da, db]
644 }
645
646 other => {
647 return Err(OptimError::NotImplemented(format!(
648 "test function {:?} has no analytic gradient",
649 other
650 )))
651 }
652 };
653
654 Ok(gradient)
655}
656
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
663enum OptimizerFamily {
664 Sgd,
665 Adam,
666 AdamW,
667 RmsProp,
668 AdaGrad,
669 Lion,
670 RAdam,
671}
672
673#[derive(Debug, Clone)]
675struct OptimizerSpec {
676 family: OptimizerFamily,
677 learning_rate: f64,
678 beta1: f64,
679 beta2: f64,
680 epsilon: f64,
681 weight_decay: f64,
682 momentum: f64,
683}
684
685impl OptimizerSpec {
686 fn from_architecture<T: Float + Debug + Send + Sync + 'static>(
695 architecture: &OptimizerArchitecture<T>,
696 ) -> Self {
697 let family = architecture
698 .components
699 .iter()
700 .find_map(|c| family_from_name(c))
701 .unwrap_or(OptimizerFamily::Sgd);
702
703 let lookup = |keys: &[&str], default: f64| -> f64 {
704 for key in keys {
705 if let Some(value) = architecture
706 .hyperparameters
707 .get(*key)
708 .or_else(|| architecture.parameters.get(*key))
709 {
710 if let Some(v) = value.to_f64() {
711 if v.is_finite() {
712 return v;
713 }
714 }
715 }
716 }
717 default
718 };
719
720 let default_lr = match family {
721 OptimizerFamily::Sgd => 1e-2,
722 OptimizerFamily::Lion => 1e-4,
723 _ => 1e-3,
724 };
725
726 Self {
727 family,
728 learning_rate: lookup(&["learning_rate", "lr"], default_lr).clamp(1e-8, 1.0),
729 beta1: lookup(&["beta1", "momentum"], 0.9).clamp(0.0, 0.9999),
730 beta2: lookup(&["beta2", "decay", "rho"], 0.999).clamp(0.0, 0.999999),
731 epsilon: lookup(&["epsilon", "eps"], 1e-8).clamp(1e-16, 1e-1),
732 weight_decay: lookup(&["weight_decay", "l2"], 0.0).clamp(0.0, 1.0),
733 momentum: lookup(&["momentum", "beta1"], 0.9).clamp(0.0, 0.9999),
734 }
735 }
736
737 fn state_slots_per_parameter(&self) -> usize {
741 match self.family {
742 OptimizerFamily::AdaGrad => 2, OptimizerFamily::RmsProp => 2, OptimizerFamily::Lion => 2, OptimizerFamily::Sgd => {
746 if self.momentum > 0.0 {
747 2
748 } else {
749 1
750 }
751 }
752 OptimizerFamily::Adam | OptimizerFamily::AdamW | OptimizerFamily::RAdam => 3,
754 }
755 }
756}
757
758fn family_from_name(name: &str) -> Option<OptimizerFamily> {
762 let lower = name.to_ascii_lowercase();
763 if lower.contains("adamw") {
764 Some(OptimizerFamily::AdamW)
765 } else if lower.contains("radam") {
766 Some(OptimizerFamily::RAdam)
767 } else if lower.contains("adagrad") {
768 Some(OptimizerFamily::AdaGrad)
769 } else if lower.contains("rmsprop") {
770 Some(OptimizerFamily::RmsProp)
771 } else if lower.contains("lion") {
772 Some(OptimizerFamily::Lion)
773 } else if lower.contains("adam") {
774 Some(OptimizerFamily::Adam)
775 } else if lower.contains("sgd") || lower.contains("momentum") || lower.contains("nesterov") {
776 Some(OptimizerFamily::Sgd)
777 } else {
778 None
779 }
780}
781
782#[derive(Debug, Clone)]
798struct CandidateOptimizer {
799 spec: OptimizerSpec,
800 m: Vec<f64>,
802 v: Vec<f64>,
804 t: u32,
806}
807
808impl CandidateOptimizer {
809 fn new(spec: OptimizerSpec, dimensions: usize) -> Self {
810 Self {
811 spec,
812 m: vec![0.0; dimensions],
813 v: vec![0.0; dimensions],
814 t: 0,
815 }
816 }
817
818 fn step(&mut self, params: &mut [f64], gradients: &[f64]) {
820 self.t = self.t.saturating_add(1);
821 let t = self.t as f64;
822
823 let lr = self.spec.learning_rate;
824 let b1 = self.spec.beta1;
825 let b2 = self.spec.beta2;
826 let eps = self.spec.epsilon;
827 let wd = self.spec.weight_decay;
828
829 match self.spec.family {
830 OptimizerFamily::Sgd => {
831 let mu = self.spec.momentum;
832 for i in 0..params.len() {
833 let g = gradients[i] + wd * params[i];
834 self.m[i] = mu * self.m[i] + g;
835 params[i] -= lr * self.m[i];
836 }
837 }
838
839 OptimizerFamily::Adam => {
840 let bc1 = 1.0 - b1.powf(t);
841 let bc2 = 1.0 - b2.powf(t);
842 for i in 0..params.len() {
843 let g = gradients[i] + wd * params[i];
844 self.m[i] = b1 * self.m[i] + (1.0 - b1) * g;
845 self.v[i] = b2 * self.v[i] + (1.0 - b2) * g * g;
846 let m_hat = self.m[i] / bc1.max(f64::MIN_POSITIVE);
847 let v_hat = self.v[i] / bc2.max(f64::MIN_POSITIVE);
848 params[i] -= lr * m_hat / (v_hat.max(0.0).sqrt() + eps);
849 }
850 }
851
852 OptimizerFamily::AdamW => {
853 let bc1 = 1.0 - b1.powf(t);
854 let bc2 = 1.0 - b2.powf(t);
855 for i in 0..params.len() {
856 params[i] -= lr * wd * params[i];
859
860 let g = gradients[i];
861 self.m[i] = b1 * self.m[i] + (1.0 - b1) * g;
862 self.v[i] = b2 * self.v[i] + (1.0 - b2) * g * g;
863 let m_hat = self.m[i] / bc1.max(f64::MIN_POSITIVE);
864 let v_hat = self.v[i] / bc2.max(f64::MIN_POSITIVE);
865 params[i] -= lr * m_hat / (v_hat.max(0.0).sqrt() + eps);
866 }
867 }
868
869 OptimizerFamily::RmsProp => {
870 let rho = b2.clamp(0.5, 0.9999);
872 for i in 0..params.len() {
873 let g = gradients[i] + wd * params[i];
874 self.v[i] = rho * self.v[i] + (1.0 - rho) * g * g;
875 params[i] -= lr * g / (self.v[i].max(0.0).sqrt() + eps);
876 }
877 }
878
879 OptimizerFamily::AdaGrad => {
880 for i in 0..params.len() {
881 let g = gradients[i] + wd * params[i];
882 self.v[i] += g * g;
883 params[i] -= lr * g / (self.v[i].max(0.0).sqrt() + eps);
884 }
885 }
886
887 OptimizerFamily::Lion => {
888 for i in 0..params.len() {
889 let g = gradients[i];
890 let interpolated = b1 * self.m[i] + (1.0 - b1) * g;
892 let direction = if interpolated > 0.0 {
893 1.0
894 } else if interpolated < 0.0 {
895 -1.0
896 } else {
897 0.0
898 };
899 params[i] -= lr * (direction + wd * params[i]);
900 self.m[i] = b2 * self.m[i] + (1.0 - b2) * g;
902 }
903 }
904
905 OptimizerFamily::RAdam => {
906 let bc1 = 1.0 - b1.powf(t);
907 let b2_t = b2.powf(t);
908 let bc2 = 1.0 - b2_t;
909 let rho_inf = 2.0 / (1.0 - b2).max(f64::MIN_POSITIVE) - 1.0;
911 let rho_t = rho_inf - 2.0 * t * b2_t / bc2.max(f64::MIN_POSITIVE);
912
913 let rectification = if rho_t > 4.0 {
917 let numerator = (rho_t - 4.0) * (rho_t - 2.0) * rho_inf;
918 let denominator = (rho_inf - 4.0) * (rho_inf - 2.0) * rho_t;
919 if denominator > 0.0 {
920 Some((numerator / denominator).max(0.0).sqrt())
921 } else {
922 None
923 }
924 } else {
925 None
926 };
927
928 for i in 0..params.len() {
929 let g = gradients[i] + wd * params[i];
930 self.m[i] = b1 * self.m[i] + (1.0 - b1) * g;
931 self.v[i] = b2 * self.v[i] + (1.0 - b2) * g * g;
932 let m_hat = self.m[i] / bc1.max(f64::MIN_POSITIVE);
933
934 match rectification {
935 Some(r) => {
936 let v_hat = (self.v[i] / bc2.max(f64::MIN_POSITIVE)).max(0.0).sqrt();
937 params[i] -= lr * r * m_hat / (v_hat + eps);
938 }
939 None => params[i] -= lr * m_hat,
940 }
941 }
942 }
943 }
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 fn architecture(component: &str, hyper: &[(&str, f64)]) -> OptimizerArchitecture<f64> {
952 let mut hyperparameters = HashMap::new();
953 for (k, v) in hyper {
954 hyperparameters.insert((*k).to_string(), *v);
955 }
956 OptimizerArchitecture {
957 components: vec![component.to_string()],
958 parameters: HashMap::new(),
959 connections: Vec::new(),
960 metadata: HashMap::new(),
961 hyperparameters,
962 architecture_id: format!("{}_test", component),
963 }
964 }
965
966 fn suite() -> BenchmarkSuite<f64> {
967 let mut suite = BenchmarkSuite::<f64>::new().expect("suite");
968 let config = EvaluationConfig {
969 epochs: 40,
970 ..EvaluationConfig::default()
971 };
972 suite.initialize(&config).expect("initialize");
973 suite
974 }
975
976 #[test]
977 fn test_benchmark_suite_creation() {
978 let suite = BenchmarkSuite::<f64>::new();
979 assert!(suite.is_ok());
980 }
981
982 #[test]
983 fn test_standard_benchmarks_registered() {
984 let suite = suite();
985 let names: Vec<&str> = suite
986 .standard_benchmarks()
987 .iter()
988 .map(|b| b.name.as_str())
989 .collect();
990 assert!(names.contains(&"Rosenbrock"));
991 assert!(names.contains(&"Quadratic"));
992 assert!(names.contains(&"Rastrigin"));
993 assert!(names.contains(&"Sphere"));
994 assert_eq!(suite.steps_per_benchmark(), 40);
995 }
996
997 #[test]
998 fn test_objective_values_at_known_optima() {
999 assert!(
1001 objective_value(&TestFunctionType::Sphere, &[0.0; 5])
1002 .expect("sphere")
1003 .abs()
1004 < 1e-12
1005 );
1006 assert!(
1007 objective_value(&TestFunctionType::Quadratic, &[0.0; 5])
1008 .expect("quadratic")
1009 .abs()
1010 < 1e-12
1011 );
1012 assert!(
1014 objective_value(&TestFunctionType::Rosenbrock, &[1.0; 6])
1015 .expect("rosenbrock")
1016 .abs()
1017 < 1e-12
1018 );
1019 assert!(
1021 objective_value(&TestFunctionType::Rastrigin, &[0.0; 4])
1022 .expect("rastrigin")
1023 .abs()
1024 < 1e-12
1025 );
1026 assert!(
1028 objective_value(&TestFunctionType::Beale, &[3.0, 0.5])
1029 .expect("beale")
1030 .abs()
1031 < 1e-12
1032 );
1033 }
1034
1035 #[test]
1036 fn test_gradients_match_finite_differences() {
1037 let point = [0.7_f64, -0.4, 1.3, 0.2];
1038 for function_type in [
1039 TestFunctionType::Sphere,
1040 TestFunctionType::Quadratic,
1041 TestFunctionType::Rosenbrock,
1042 TestFunctionType::Rastrigin,
1043 TestFunctionType::Ackley,
1044 ] {
1045 let analytic = objective_gradient(&function_type, &point).expect("gradient");
1046 let h = 1e-6;
1047 for i in 0..point.len() {
1048 let mut plus = point;
1049 let mut minus = point;
1050 plus[i] += h;
1051 minus[i] -= h;
1052 let numeric = (objective_value(&function_type, &plus).expect("f+")
1053 - objective_value(&function_type, &minus).expect("f-"))
1054 / (2.0 * h);
1055 assert!(
1056 (analytic[i] - numeric).abs() < 1e-3,
1057 "{:?} gradient[{}]: analytic {} vs numeric {}",
1058 function_type,
1059 i,
1060 analytic[i],
1061 numeric
1062 );
1063 }
1064 }
1065 }
1066
1067 #[test]
1068 fn test_unimplemented_test_function_is_reported() {
1069 let err = objective_value(&TestFunctionType::NeuralNetworkTraining, &[1.0]);
1070 assert!(matches!(err, Err(OptimError::NotImplemented(_))));
1071 }
1072
1073 #[test]
1074 fn test_benchmarks_actually_optimize() {
1075 let mut suite = suite();
1076 let results = suite
1077 .run_benchmarks(&architecture("Adam", &[("learning_rate", 0.1)]))
1078 .expect("run benchmarks");
1079
1080 assert_eq!(results.len(), 4);
1081 for result in &results {
1082 assert!(
1084 result.normalized_score > 0.0,
1085 "{} made no progress",
1086 result.test_name
1087 );
1088 assert!(result.normalized_score <= 1.0);
1089 assert!(result.score.is_finite());
1090 assert!(result.percentile_rank > 0.0 && result.percentile_rank <= 1.0);
1091 }
1092 }
1093
1094 #[test]
1095 fn test_different_architectures_produce_different_scores() {
1096 let mut suite = suite();
1097 let fast = suite
1098 .run_benchmarks(&architecture("Adam", &[("learning_rate", 0.2)]))
1099 .expect("fast");
1100 let slow = suite
1101 .run_benchmarks(&architecture("SGD", &[("learning_rate", 1e-6)]))
1102 .expect("slow");
1103
1104 let fast_sphere = fast
1108 .iter()
1109 .find(|r| r.test_name == "Sphere")
1110 .expect("sphere")
1111 .normalized_score;
1112 let slow_sphere = slow
1113 .iter()
1114 .find(|r| r.test_name == "Sphere")
1115 .expect("sphere")
1116 .normalized_score;
1117 assert!(
1118 fast_sphere > slow_sphere,
1119 "expected {} > {}",
1120 fast_sphere,
1121 slow_sphere
1122 );
1123 }
1124
1125 #[test]
1126 fn test_component_type_alone_changes_the_score() {
1127 let mut suite = suite();
1132 let adam = suite
1133 .run_benchmarks(&architecture("Adam", &[]))
1134 .expect("adam");
1135 let sgd = suite
1136 .run_benchmarks(&architecture("SGD", &[]))
1137 .expect("sgd");
1138 let rmsprop = suite
1139 .run_benchmarks(&architecture("RMSprop", &[]))
1140 .expect("rmsprop");
1141
1142 let score_of = |results: &[TestResult<f64>], name: &str| -> f64 {
1143 results
1144 .iter()
1145 .find(|r| r.test_name == name)
1146 .map(|r| r.normalized_score)
1147 .unwrap_or(f64::NAN)
1148 };
1149
1150 for benchmark in ["Sphere", "Rosenbrock"] {
1151 let a = score_of(&adam, benchmark);
1152 let s = score_of(&sgd, benchmark);
1153 let r = score_of(&rmsprop, benchmark);
1154 assert!(
1155 (a - s).abs() > 1e-9,
1156 "{}: Adam and SGD must differ ({} vs {})",
1157 benchmark,
1158 a,
1159 s
1160 );
1161 assert!(
1162 (a - r).abs() > 1e-9,
1163 "{}: Adam and RMSprop must differ ({} vs {})",
1164 benchmark,
1165 a,
1166 r
1167 );
1168 }
1169 }
1170
1171 #[test]
1172 fn test_unrecognised_component_falls_back_to_sgd() {
1173 assert_eq!(family_from_name("Custom"), None);
1177 let spec = OptimizerSpec::from_architecture(&architecture("Custom", &[]));
1178 assert_eq!(spec.family, OptimizerFamily::Sgd);
1179 }
1180
1181 #[test]
1182 fn test_identical_architecture_reproduces() {
1183 let mut suite_a = suite();
1184 let mut suite_b = suite();
1185 let arch = architecture("AdamW", &[("learning_rate", 0.05), ("beta1", 0.85)]);
1186
1187 let a = suite_a.run_benchmarks(&arch).expect("a");
1188 let b = suite_b.run_benchmarks(&arch).expect("b");
1189
1190 assert_eq!(a.len(), b.len());
1191 for (ra, rb) in a.iter().zip(b.iter()) {
1192 assert_eq!(ra.test_name, rb.test_name);
1193 assert!(
1194 (ra.score - rb.score).abs() < 1e-12,
1195 "{} not reproducible: {} vs {}",
1196 ra.test_name,
1197 ra.score,
1198 rb.score
1199 );
1200 }
1201 }
1202
1203 #[test]
1204 fn test_optimizer_family_resolution() {
1205 assert_eq!(family_from_name("AdamW"), Some(OptimizerFamily::AdamW));
1206 assert_eq!(family_from_name("Adam"), Some(OptimizerFamily::Adam));
1207 assert_eq!(family_from_name("RAdam"), Some(OptimizerFamily::RAdam));
1208 assert_eq!(family_from_name("SGD"), Some(OptimizerFamily::Sgd));
1209 assert_eq!(family_from_name("RMSprop"), Some(OptimizerFamily::RmsProp));
1210 assert_eq!(family_from_name("Unrelated"), None);
1211 }
1212}