1use std::collections::HashMap;
18use thiserror::Error;
19
20#[derive(Debug, Error, Clone, PartialEq)]
24pub enum OptimizerError {
25 #[error("dimension mismatch in group '{name}': params len={params}, grad len={grad}")]
27 DimensionMismatch {
28 name: String,
30 params: usize,
32 grad: usize,
34 },
35
36 #[error("parameter group '{0}' is empty")]
38 EmptyGroup(String),
39}
40
41#[derive(Debug, Clone, PartialEq)]
45pub enum OptimizerAlgorithm {
46 Adam {
48 lr: f64,
50 beta1: f64,
52 beta2: f64,
54 epsilon: f64,
56 },
57
58 AdaGrad {
60 lr: f64,
62 epsilon: f64,
64 },
65
66 RmsProp {
68 lr: f64,
70 alpha: f64,
72 epsilon: f64,
74 momentum: f64,
76 },
77
78 AdamW {
80 lr: f64,
82 beta1: f64,
84 beta2: f64,
86 epsilon: f64,
88 weight_decay: f64,
90 },
91}
92
93impl OptimizerAlgorithm {
94 #[must_use]
96 pub fn adam_default() -> Self {
97 Self::Adam {
98 lr: 0.001,
99 beta1: 0.9,
100 beta2: 0.999,
101 epsilon: 1e-8,
102 }
103 }
104
105 #[must_use]
107 pub fn adagrad_default() -> Self {
108 Self::AdaGrad {
109 lr: 0.01,
110 epsilon: 1e-8,
111 }
112 }
113
114 #[must_use]
116 pub fn rmsprop_default() -> Self {
117 Self::RmsProp {
118 lr: 0.01,
119 alpha: 0.99,
120 epsilon: 1e-8,
121 momentum: 0.0,
122 }
123 }
124
125 #[must_use]
127 pub fn adamw_default() -> Self {
128 Self::AdamW {
129 lr: 0.001,
130 beta1: 0.9,
131 beta2: 0.999,
132 epsilon: 1e-8,
133 weight_decay: 0.01,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
145pub struct ParameterGroup {
146 pub name: String,
148 pub params: Vec<f64>,
150 pub grad: Vec<f64>,
152}
153
154impl ParameterGroup {
155 #[must_use]
157 pub fn new(name: impl Into<String>, params: Vec<f64>) -> Self {
158 let n = params.len();
159 Self {
160 name: name.into(),
161 params,
162 grad: vec![0.0; n],
163 }
164 }
165
166 #[must_use]
168 pub fn with_grad(name: impl Into<String>, params: Vec<f64>, grad: Vec<f64>) -> Self {
169 Self {
170 name: name.into(),
171 params,
172 grad,
173 }
174 }
175}
176
177#[derive(Debug, Clone)]
184pub struct OptimizerState {
185 pub m: Vec<f64>,
187 pub v: Vec<f64>,
189 pub step: u64,
191}
192
193impl OptimizerState {
194 #[must_use]
196 pub fn zeros(n: usize) -> Self {
197 Self {
198 m: vec![0.0; n],
199 v: vec![0.0; n],
200 step: 0,
201 }
202 }
203
204 pub fn reset(&mut self) {
206 self.m.iter_mut().for_each(|x| *x = 0.0);
207 self.v.iter_mut().for_each(|x| *x = 0.0);
208 self.step = 0;
209 }
210}
211
212#[derive(Debug, Clone, PartialEq)]
216pub struct OptimizerStats {
217 pub total_steps: u64,
219 pub parameter_groups: usize,
221 pub total_parameters: usize,
223 pub last_grad_norm: f64,
225}
226
227#[derive(Debug, Clone)]
246pub struct AdaptiveOptimizer {
247 pub algorithm: OptimizerAlgorithm,
249 pub states: HashMap<String, OptimizerState>,
251 pub global_step: u64,
253 last_grad_norm: f64,
255}
256
257impl AdaptiveOptimizer {
258 #[must_use]
260 pub fn new(algorithm: OptimizerAlgorithm) -> Self {
261 Self {
262 algorithm,
263 states: HashMap::new(),
264 global_step: 0,
265 last_grad_norm: 0.0,
266 }
267 }
268
269 pub fn step(&mut self, groups: &mut [ParameterGroup]) -> Result<f64, OptimizerError> {
280 for g in groups.iter() {
282 Self::validate_group(g)?;
283 }
284
285 let norm = Self::global_grad_norm(groups);
287 self.last_grad_norm = norm;
288 self.global_step += 1;
289
290 for g in groups.iter_mut() {
292 self.step_group(g)?;
293 }
294
295 Ok(norm)
296 }
297
298 pub fn step_group(&mut self, group: &mut ParameterGroup) -> Result<(), OptimizerError> {
306 Self::validate_group(group)?;
307 let n = group.params.len();
308 let key = group.name.clone();
309
310 let state = self
312 .states
313 .entry(key)
314 .or_insert_with(|| OptimizerState::zeros(n));
315
316 if state.m.len() != n {
318 *state = OptimizerState::zeros(n);
319 }
320
321 match &self.algorithm.clone() {
322 OptimizerAlgorithm::Adam {
323 lr,
324 beta1,
325 beta2,
326 epsilon,
327 } => Self::apply_adam(group, state, *lr, *beta1, *beta2, *epsilon),
328 OptimizerAlgorithm::AdaGrad { lr, epsilon } => {
329 Self::apply_adagrad(group, state, *lr, *epsilon);
330 }
331 OptimizerAlgorithm::RmsProp {
332 lr,
333 alpha,
334 epsilon,
335 momentum,
336 } => Self::apply_rmsprop(group, state, *lr, *alpha, *epsilon, *momentum),
337 OptimizerAlgorithm::AdamW {
338 lr,
339 beta1,
340 beta2,
341 epsilon,
342 weight_decay,
343 } => Self::apply_adamw(group, state, *lr, *beta1, *beta2, *epsilon, *weight_decay),
344 }
345
346 Ok(())
347 }
348
349 pub fn zero_grad(groups: &mut [ParameterGroup]) {
351 for g in groups.iter_mut() {
352 g.grad.iter_mut().for_each(|x| *x = 0.0);
353 }
354 }
355
356 #[must_use]
358 pub fn global_grad_norm(groups: &[ParameterGroup]) -> f64 {
359 let sum_sq: f64 = groups
360 .iter()
361 .flat_map(|g| g.grad.iter())
362 .map(|&x| x * x)
363 .sum();
364 sum_sq.sqrt()
365 }
366
367 pub fn clip_grad_norm(groups: &mut [ParameterGroup], max_norm: f64) {
372 let norm = Self::global_grad_norm(groups);
373 if norm > max_norm && norm.is_finite() && max_norm > 0.0 {
374 let scale = max_norm / norm;
375 for g in groups.iter_mut() {
376 g.grad.iter_mut().for_each(|x| *x *= scale);
377 }
378 }
379 }
380
381 pub fn reset_state(&mut self, group_name: &str) {
383 self.states.remove(group_name);
384 }
385
386 pub fn reset_all(&mut self) {
388 self.states.clear();
389 self.global_step = 0;
390 self.last_grad_norm = 0.0;
391 }
392
393 #[must_use]
395 pub fn stats(&self, groups: &[ParameterGroup]) -> OptimizerStats {
396 let total_parameters = groups.iter().map(|g| g.params.len()).sum();
397 OptimizerStats {
398 total_steps: self.global_step,
399 parameter_groups: groups.len(),
400 total_parameters,
401 last_grad_norm: self.last_grad_norm,
402 }
403 }
404
405 fn validate_group(g: &ParameterGroup) -> Result<(), OptimizerError> {
409 if g.params.is_empty() {
410 return Err(OptimizerError::EmptyGroup(g.name.clone()));
411 }
412 if g.params.len() != g.grad.len() {
413 return Err(OptimizerError::DimensionMismatch {
414 name: g.name.clone(),
415 params: g.params.len(),
416 grad: g.grad.len(),
417 });
418 }
419 Ok(())
420 }
421
422 fn apply_adam(
434 group: &mut ParameterGroup,
435 state: &mut OptimizerState,
436 lr: f64,
437 beta1: f64,
438 beta2: f64,
439 epsilon: f64,
440 ) {
441 state.step += 1;
442 let t = state.step as f64;
443 let bc1 = 1.0 - beta1.powf(t);
444 let bc2 = 1.0 - beta2.powf(t);
445
446 for i in 0..group.params.len() {
447 let g = group.grad[i];
448 state.m[i] = beta1 * state.m[i] + (1.0 - beta1) * g;
449 state.v[i] = beta2 * state.v[i] + (1.0 - beta2) * g * g;
450 let m_hat = state.m[i] / bc1;
451 let v_hat = state.v[i] / bc2;
452 group.params[i] -= lr * m_hat / (v_hat.sqrt() + epsilon);
453 }
454 }
455
456 fn apply_adagrad(
464 group: &mut ParameterGroup,
465 state: &mut OptimizerState,
466 lr: f64,
467 epsilon: f64,
468 ) {
469 state.step += 1;
470 for i in 0..group.params.len() {
471 let g = group.grad[i];
472 state.v[i] += g * g;
473 group.params[i] -= lr * g / (state.v[i].sqrt() + epsilon);
474 }
475 }
476
477 fn apply_rmsprop(
485 group: &mut ParameterGroup,
486 state: &mut OptimizerState,
487 lr: f64,
488 alpha: f64,
489 epsilon: f64,
490 momentum: f64,
491 ) {
492 state.step += 1;
493 for i in 0..group.params.len() {
494 let g = group.grad[i];
495 state.v[i] = alpha * state.v[i] + (1.0 - alpha) * g * g;
496 let delta = lr * g / (state.v[i] + epsilon).sqrt();
497 state.m[i] = momentum * state.m[i] + delta;
498 group.params[i] -= state.m[i];
499 }
500 }
501
502 fn apply_adamw(
509 group: &mut ParameterGroup,
510 state: &mut OptimizerState,
511 lr: f64,
512 beta1: f64,
513 beta2: f64,
514 epsilon: f64,
515 weight_decay: f64,
516 ) {
517 state.step += 1;
518 let t = state.step as f64;
519 let bc1 = 1.0 - beta1.powf(t);
520 let bc2 = 1.0 - beta2.powf(t);
521
522 for i in 0..group.params.len() {
523 group.params[i] -= lr * weight_decay * group.params[i];
525
526 let g = group.grad[i];
527 state.m[i] = beta1 * state.m[i] + (1.0 - beta1) * g;
528 state.v[i] = beta2 * state.v[i] + (1.0 - beta2) * g * g;
529 let m_hat = state.m[i] / bc1;
530 let v_hat = state.v[i] / bc2;
531 group.params[i] -= lr * m_hat / (v_hat.sqrt() + epsilon);
532 }
533 }
534}
535
536#[cfg(test)]
539mod tests {
540 use super::{
541 AdaptiveOptimizer, OptimizerAlgorithm, OptimizerError, OptimizerState, ParameterGroup,
542 };
543
544 fn adam_opt() -> AdaptiveOptimizer {
547 AdaptiveOptimizer::new(OptimizerAlgorithm::adam_default())
548 }
549
550 fn adagrad_opt() -> AdaptiveOptimizer {
551 AdaptiveOptimizer::new(OptimizerAlgorithm::adagrad_default())
552 }
553
554 fn rmsprop_opt() -> AdaptiveOptimizer {
555 AdaptiveOptimizer::new(OptimizerAlgorithm::rmsprop_default())
556 }
557
558 fn adamw_opt() -> AdaptiveOptimizer {
559 AdaptiveOptimizer::new(OptimizerAlgorithm::adamw_default())
560 }
561
562 fn simple_group(name: &str, p: f64, g: f64) -> ParameterGroup {
563 ParameterGroup::with_grad(name, vec![p], vec![g])
564 }
565
566 #[test]
569 fn test_new_optimizer_initial_state() {
570 let opt = adam_opt();
571 assert_eq!(opt.global_step, 0);
572 assert!(opt.states.is_empty());
573 }
574
575 #[test]
576 fn test_parameter_group_new_zeros_grad() {
577 let g = ParameterGroup::new("layer", vec![1.0, 2.0, 3.0]);
578 assert_eq!(g.grad, vec![0.0, 0.0, 0.0]);
579 assert_eq!(g.params.len(), 3);
580 }
581
582 #[test]
583 fn test_parameter_group_with_grad() {
584 let g = ParameterGroup::with_grad("w", vec![1.0], vec![0.5]);
585 assert_eq!(g.params[0], 1.0);
586 assert_eq!(g.grad[0], 0.5);
587 }
588
589 #[test]
590 fn test_optimizer_state_zeros() {
591 let s = OptimizerState::zeros(4);
592 assert_eq!(s.m, vec![0.0; 4]);
593 assert_eq!(s.v, vec![0.0; 4]);
594 assert_eq!(s.step, 0);
595 }
596
597 #[test]
598 fn test_optimizer_state_reset() {
599 let mut s = OptimizerState {
600 m: vec![1.0, 2.0],
601 v: vec![3.0, 4.0],
602 step: 10,
603 };
604 s.reset();
605 assert_eq!(s.m, vec![0.0, 0.0]);
606 assert_eq!(s.v, vec![0.0, 0.0]);
607 assert_eq!(s.step, 0);
608 }
609
610 #[test]
613 fn test_step_dimension_mismatch_error() {
614 let mut opt = adam_opt();
615 let mut groups = vec![ParameterGroup {
616 name: "bad".to_string(),
617 params: vec![1.0, 2.0],
618 grad: vec![0.1],
619 }];
620 let err = opt.step(&mut groups).unwrap_err();
621 assert!(matches!(err, OptimizerError::DimensionMismatch { .. }));
622 }
623
624 #[test]
625 fn test_step_empty_group_error() {
626 let mut opt = adam_opt();
627 let mut groups = vec![ParameterGroup {
628 name: "empty".to_string(),
629 params: vec![],
630 grad: vec![],
631 }];
632 let err = opt.step(&mut groups).unwrap_err();
633 assert!(matches!(err, OptimizerError::EmptyGroup(_)));
634 }
635
636 #[test]
637 fn test_step_group_dimension_mismatch() {
638 let mut opt = adam_opt();
639 let mut g = ParameterGroup {
640 name: "x".to_string(),
641 params: vec![1.0],
642 grad: vec![0.1, 0.2],
643 };
644 assert!(opt.step_group(&mut g).is_err());
645 }
646
647 #[test]
650 fn test_adam_step_reduces_param_toward_zero() {
651 let mut opt = adam_opt();
652 let mut groups = vec![simple_group("w", 1.0, 1.0)];
653 opt.step(&mut groups).expect("test: should succeed");
654 assert!(groups[0].params[0] < 1.0);
656 }
657
658 #[test]
659 fn test_adam_global_step_increments() {
660 let mut opt = adam_opt();
661 let mut groups = vec![simple_group("w", 1.0, 0.1)];
662 opt.step(&mut groups).expect("test: should succeed");
663 opt.step(&mut groups).expect("test: should succeed");
664 assert_eq!(opt.global_step, 2);
665 }
666
667 #[test]
668 fn test_adam_state_step_increments_per_group() {
669 let mut opt = adam_opt();
670 let mut groups = vec![simple_group("a", 0.5, 0.2)];
671 opt.step(&mut groups).expect("test: should succeed");
672 opt.step(&mut groups).expect("test: should succeed");
673 assert_eq!(opt.states["a"].step, 2);
674 }
675
676 #[test]
677 fn test_adam_moment_vectors_are_nonzero_after_step() {
678 let mut opt = adam_opt();
679 let mut groups = vec![simple_group("m", 0.0, 1.0)];
680 opt.step(&mut groups).expect("test: should succeed");
681 let s = &opt.states["m"];
682 assert_ne!(s.m[0], 0.0);
683 assert_ne!(s.v[0], 0.0);
684 }
685
686 #[test]
687 fn test_adam_multiple_params() {
688 let mut opt = adam_opt();
689 let mut groups = vec![ParameterGroup::with_grad(
690 "layer",
691 vec![1.0, -1.0, 0.0],
692 vec![0.5, -0.5, 1.0],
693 )];
694 let norm = opt.step(&mut groups).expect("test: should succeed");
695 assert!(norm > 0.0);
696 assert_eq!(groups[0].params.len(), 3);
697 }
698
699 #[test]
700 fn test_adam_zero_gradient_leaves_param_almost_unchanged() {
701 let mut opt = AdaptiveOptimizer::new(OptimizerAlgorithm::Adam {
702 lr: 0.001,
703 beta1: 0.9,
704 beta2: 0.999,
705 epsilon: 1e-8,
706 });
707 let initial = 5.0_f64;
708 let mut groups = vec![simple_group("p", initial, 0.0)];
709 opt.step(&mut groups).expect("test: should succeed");
710 assert!((groups[0].params[0] - initial).abs() < 1e-12);
713 }
714
715 #[test]
718 fn test_adagrad_step_lowers_param_for_positive_grad() {
719 let mut opt = adagrad_opt();
720 let mut groups = vec![simple_group("w", 2.0, 1.0)];
721 opt.step(&mut groups).expect("test: should succeed");
722 assert!(groups[0].params[0] < 2.0);
723 }
724
725 #[test]
726 fn test_adagrad_accumulates_squared_grad_in_v() {
727 let mut opt = adagrad_opt();
728 let mut groups = vec![simple_group("a", 0.0, 3.0)];
729 opt.step(&mut groups).expect("test: should succeed");
730 assert!((opt.states["a"].v[0] - 9.0).abs() < 1e-10);
732 }
733
734 #[test]
735 fn test_adagrad_large_gradient_decays_lr() {
736 let mut opt = adagrad_opt();
738 let mut groups = vec![simple_group("w", 1.0, 100.0)];
739 let p_after_1 = {
740 opt.step(&mut groups).expect("test: should succeed");
741 groups[0].params[0]
742 };
743 groups[0].params[0] = 1.0;
745 groups[0].grad[0] = 100.0;
746 opt.step(&mut groups).expect("test: should succeed");
747 let p_after_2 = groups[0].params[0];
748 let delta1 = (1.0 - p_after_1).abs();
751 let delta2 = (1.0 - p_after_2).abs();
752 assert!(delta2 < delta1);
753 }
754
755 #[test]
758 fn test_rmsprop_step_moves_param() {
759 let mut opt = rmsprop_opt();
760 let mut groups = vec![simple_group("w", 1.0, 1.0)];
761 let before = groups[0].params[0];
762 opt.step(&mut groups).expect("test: should succeed");
763 assert_ne!(groups[0].params[0], before);
764 }
765
766 #[test]
767 fn test_rmsprop_with_momentum() {
768 let mut opt = AdaptiveOptimizer::new(OptimizerAlgorithm::RmsProp {
769 lr: 0.01,
770 alpha: 0.99,
771 epsilon: 1e-8,
772 momentum: 0.9,
773 });
774 let mut groups = vec![simple_group("w", 1.0, 1.0)];
775 opt.step(&mut groups).expect("test: should succeed");
776 assert_ne!(opt.states["w"].m[0], 0.0);
778 }
779
780 #[test]
781 fn test_rmsprop_v_decays_toward_zero_on_zero_grad() {
782 let mut opt = AdaptiveOptimizer::new(OptimizerAlgorithm::RmsProp {
783 lr: 0.01,
784 alpha: 0.9,
785 epsilon: 1e-8,
786 momentum: 0.0,
787 });
788 let mut groups = vec![simple_group("w", 1.0, 1.0)];
790 opt.step(&mut groups).expect("test: should succeed");
791 let v_after_1 = opt.states["w"].v[0];
792
793 groups[0].grad[0] = 0.0;
795 opt.step(&mut groups).expect("test: should succeed");
796 let v_after_2 = opt.states["w"].v[0];
797 assert!(v_after_2 < v_after_1);
798 }
799
800 #[test]
803 fn test_adamw_applies_weight_decay() {
804 let mut opt_adamw = adamw_opt(); let mut opt_adam = AdaptiveOptimizer::new(OptimizerAlgorithm::Adam {
807 lr: 0.001,
808 beta1: 0.9,
809 beta2: 0.999,
810 epsilon: 1e-8,
811 });
812
813 let init_param = 2.0_f64;
814 let grad_val = 0.1_f64;
815
816 let mut groups_wd = vec![simple_group("p", init_param, grad_val)];
817 let mut groups_no_wd = vec![simple_group("p", init_param, grad_val)];
818
819 opt_adamw
820 .step(&mut groups_wd)
821 .expect("test: should succeed");
822 opt_adam
823 .step(&mut groups_no_wd)
824 .expect("test: should succeed");
825
826 assert!(groups_wd[0].params[0] < groups_no_wd[0].params[0]);
828 }
829
830 #[test]
831 fn test_adamw_zero_weight_decay_equals_adam() {
832 let mut opt_adamw = AdaptiveOptimizer::new(OptimizerAlgorithm::AdamW {
833 lr: 0.001,
834 beta1: 0.9,
835 beta2: 0.999,
836 epsilon: 1e-8,
837 weight_decay: 0.0,
838 });
839 let mut opt_adam = AdaptiveOptimizer::new(OptimizerAlgorithm::Adam {
840 lr: 0.001,
841 beta1: 0.9,
842 beta2: 0.999,
843 epsilon: 1e-8,
844 });
845
846 let mut g1 = vec![simple_group("w", 1.0, 0.5)];
847 let mut g2 = vec![simple_group("w", 1.0, 0.5)];
848
849 opt_adamw.step(&mut g1).expect("test: should succeed");
850 opt_adam.step(&mut g2).expect("test: should succeed");
851
852 let diff = (g1[0].params[0] - g2[0].params[0]).abs();
853 assert!(diff < 1e-14, "expected Adam≈AdamW(wd=0), diff={diff}");
854 }
855
856 #[test]
859 fn test_global_grad_norm_single_value() {
860 let groups = vec![simple_group("w", 0.0, 3.0)];
861 let norm = AdaptiveOptimizer::global_grad_norm(&groups);
862 assert!((norm - 3.0).abs() < 1e-10);
863 }
864
865 #[test]
866 fn test_global_grad_norm_two_groups() {
867 let groups = vec![simple_group("a", 0.0, 3.0), simple_group("b", 0.0, 4.0)];
868 let norm = AdaptiveOptimizer::global_grad_norm(&groups);
869 assert!((norm - 5.0).abs() < 1e-10);
871 }
872
873 #[test]
874 fn test_global_grad_norm_zero_gradients() {
875 let groups = vec![ParameterGroup::new("w", vec![1.0, 2.0])];
876 let norm = AdaptiveOptimizer::global_grad_norm(&groups);
877 assert_eq!(norm, 0.0);
878 }
879
880 #[test]
881 fn test_clip_grad_norm_scales_down() {
882 let mut groups = vec![simple_group("a", 0.0, 3.0), simple_group("b", 0.0, 4.0)];
883 AdaptiveOptimizer::clip_grad_norm(&mut groups, 1.0);
884 let new_norm = AdaptiveOptimizer::global_grad_norm(&groups);
885 assert!((new_norm - 1.0).abs() < 1e-10);
886 }
887
888 #[test]
889 fn test_clip_grad_norm_no_op_when_below_max() {
890 let mut groups = vec![simple_group("w", 0.0, 0.3)];
891 AdaptiveOptimizer::clip_grad_norm(&mut groups, 5.0);
892 assert!((groups[0].grad[0] - 0.3).abs() < 1e-14);
893 }
894
895 #[test]
896 fn test_clip_grad_norm_preserves_direction() {
897 let mut groups = vec![ParameterGroup::with_grad(
898 "w",
899 vec![0.0, 0.0],
900 vec![3.0, 4.0],
901 )];
902 AdaptiveOptimizer::clip_grad_norm(&mut groups, 1.0);
903 let ratio = groups[0].grad[0] / groups[0].grad[1];
905 assert!((ratio - 0.75).abs() < 1e-10, "ratio={ratio}");
906 }
907
908 #[test]
909 fn test_zero_grad_clears_all() {
910 let mut groups = vec![simple_group("a", 1.0, 2.0), simple_group("b", 3.0, 4.0)];
911 AdaptiveOptimizer::zero_grad(&mut groups);
912 for g in &groups {
913 for &v in &g.grad {
914 assert_eq!(v, 0.0);
915 }
916 }
917 }
918
919 #[test]
922 fn test_reset_state_clears_single_group() {
923 let mut opt = adam_opt();
924 let mut groups = vec![simple_group("w", 1.0, 0.5)];
925 opt.step(&mut groups).expect("test: should succeed");
926 assert!(opt.states.contains_key("w"));
927 opt.reset_state("w");
928 assert!(!opt.states.contains_key("w"));
929 }
930
931 #[test]
932 fn test_reset_all_clears_everything() {
933 let mut opt = adam_opt();
934 let mut groups = vec![simple_group("a", 1.0, 0.1), simple_group("b", 2.0, 0.2)];
935 opt.step(&mut groups).expect("test: should succeed");
936 opt.reset_all();
937 assert!(opt.states.is_empty());
938 assert_eq!(opt.global_step, 0);
939 }
940
941 #[test]
942 fn test_reset_state_nonexistent_key_is_noop() {
943 let mut opt = adam_opt();
944 opt.reset_state("nonexistent"); assert!(opt.states.is_empty());
946 }
947
948 #[test]
951 fn test_stats_initial() {
952 let opt = adam_opt();
953 let groups = vec![
954 ParameterGroup::new("a", vec![1.0, 2.0]),
955 ParameterGroup::new("b", vec![3.0]),
956 ];
957 let s = opt.stats(&groups);
958 assert_eq!(s.total_steps, 0);
959 assert_eq!(s.parameter_groups, 2);
960 assert_eq!(s.total_parameters, 3);
961 assert_eq!(s.last_grad_norm, 0.0);
962 }
963
964 #[test]
965 fn test_stats_after_step() {
966 let mut opt = adam_opt();
967 let mut groups = vec![ParameterGroup::with_grad(
968 "w",
969 vec![1.0, 2.0],
970 vec![3.0, 4.0],
971 )];
972 opt.step(&mut groups).expect("test: should succeed");
973 let s = opt.stats(&groups);
974 assert_eq!(s.total_steps, 1);
975 assert!((s.last_grad_norm - 5.0).abs() < 1e-10);
976 }
977
978 #[test]
981 fn test_step_returns_correct_grad_norm() {
982 let mut opt = adam_opt();
983 let mut groups = vec![ParameterGroup::with_grad(
984 "w",
985 vec![0.0, 0.0],
986 vec![3.0, 4.0],
987 )];
988 let norm = opt.step(&mut groups).expect("test: should succeed");
989 assert!((norm - 5.0).abs() < 1e-10);
990 }
991
992 #[test]
993 fn test_step_returns_zero_norm_for_zero_grads() {
994 let mut opt = adam_opt();
995 let mut groups = vec![ParameterGroup::new("w", vec![1.0, 2.0])];
996 let norm = opt.step(&mut groups).expect("test: should succeed");
997 assert_eq!(norm, 0.0);
998 }
999
1000 #[test]
1003 fn test_multiple_groups_each_have_independent_state() {
1004 let mut opt = adam_opt();
1005 let mut groups = vec![
1006 simple_group("layer1", 1.0, 0.1),
1007 simple_group("layer2", -1.0, -0.1),
1008 ];
1009 opt.step(&mut groups).expect("test: should succeed");
1010 assert!(opt.states.contains_key("layer1"));
1012 assert!(opt.states.contains_key("layer2"));
1013 }
1014
1015 #[test]
1016 fn test_step_group_individually_matches_bulk_step() {
1017 let mut opt_bulk = adam_opt();
1019 let mut opt_single = adam_opt();
1020
1021 let mut groups_bulk = vec![simple_group("w1", 1.0, 0.5), simple_group("w2", -0.5, -0.3)];
1022 let mut groups_single = groups_bulk.clone();
1023
1024 opt_bulk
1025 .step(&mut groups_bulk)
1026 .expect("test: should succeed");
1027 for g in groups_single.iter_mut() {
1028 opt_single.step_group(g).expect("test: should succeed");
1029 }
1030
1031 for (gb, gs) in groups_bulk.iter().zip(groups_single.iter()) {
1032 let diff = (gb.params[0] - gs.params[0]).abs();
1033 assert!(diff < 1e-14, "param mismatch for {}: {diff}", gb.name);
1034 }
1035 }
1036
1037 #[test]
1040 fn test_adam_converges_simple_quadratic() {
1041 let mut opt = AdaptiveOptimizer::new(OptimizerAlgorithm::Adam {
1044 lr: 0.1,
1045 beta1: 0.9,
1046 beta2: 0.999,
1047 epsilon: 1e-8,
1048 });
1049 let mut groups = vec![ParameterGroup::new("x", vec![5.0])];
1050 for _ in 0..2000 {
1051 groups[0].grad[0] = groups[0].params[0]; opt.step(&mut groups).expect("test: should succeed");
1053 }
1054 assert!(
1055 groups[0].params[0].abs() < 0.01,
1056 "did not converge: x={}",
1057 groups[0].params[0]
1058 );
1059 }
1060
1061 #[test]
1062 fn test_adagrad_converges_simple_quadratic() {
1063 let mut opt = AdaptiveOptimizer::new(OptimizerAlgorithm::AdaGrad {
1065 lr: 1.0,
1066 epsilon: 1e-8,
1067 });
1068 let mut groups = vec![ParameterGroup::new("x", vec![3.0])];
1069 for _ in 0..500 {
1070 groups[0].grad[0] = groups[0].params[0];
1071 opt.step(&mut groups).expect("test: should succeed");
1072 }
1073 assert!(
1074 groups[0].params[0].abs() < 0.1,
1075 "did not converge: x={}",
1076 groups[0].params[0]
1077 );
1078 }
1079
1080 #[test]
1081 fn test_rmsprop_converges_simple_quadratic() {
1082 let mut opt = rmsprop_opt();
1083 let mut groups = vec![ParameterGroup::new("x", vec![3.0])];
1084 for _ in 0..3000 {
1085 groups[0].grad[0] = groups[0].params[0];
1086 opt.step(&mut groups).expect("test: should succeed");
1087 }
1088 assert!(
1089 groups[0].params[0].abs() < 0.1,
1090 "did not converge: x={}",
1091 groups[0].params[0]
1092 );
1093 }
1094
1095 #[test]
1096 fn test_adamw_converges_simple_quadratic() {
1097 let mut opt = AdaptiveOptimizer::new(OptimizerAlgorithm::AdamW {
1098 lr: 0.01,
1099 beta1: 0.9,
1100 beta2: 0.999,
1101 epsilon: 1e-8,
1102 weight_decay: 0.001,
1103 });
1104 let mut groups = vec![ParameterGroup::new("x", vec![3.0])];
1105 for _ in 0..5000 {
1106 groups[0].grad[0] = groups[0].params[0];
1107 opt.step(&mut groups).expect("test: should succeed");
1108 }
1109 assert!(
1110 groups[0].params[0].abs() < 0.1,
1111 "did not converge: x={}",
1112 groups[0].params[0]
1113 );
1114 }
1115
1116 #[test]
1119 fn test_algorithm_default_constructors() {
1120 let adam = OptimizerAlgorithm::adam_default();
1121 assert!(matches!(adam, OptimizerAlgorithm::Adam { lr, .. } if (lr - 0.001).abs() < 1e-15));
1122
1123 let adagrad = OptimizerAlgorithm::adagrad_default();
1124 assert!(
1125 matches!(adagrad, OptimizerAlgorithm::AdaGrad { lr, .. } if (lr - 0.01).abs() < 1e-15)
1126 );
1127
1128 let rmsprop = OptimizerAlgorithm::rmsprop_default();
1129 assert!(
1130 matches!(rmsprop, OptimizerAlgorithm::RmsProp { lr, .. } if (lr - 0.01).abs() < 1e-15)
1131 );
1132
1133 let adamw = OptimizerAlgorithm::adamw_default();
1134 assert!(
1135 matches!(adamw, OptimizerAlgorithm::AdamW { weight_decay, .. } if (weight_decay - 0.01).abs() < 1e-15)
1136 );
1137 }
1138
1139 #[test]
1142 fn test_state_lazily_initialised_on_first_step() {
1143 let mut opt = adam_opt();
1144 assert!(opt.states.is_empty());
1145 let mut groups = vec![simple_group("w", 0.0, 1.0)];
1146 opt.step(&mut groups).expect("test: should succeed");
1147 assert!(opt.states.contains_key("w"));
1148 }
1149
1150 #[test]
1153 fn test_dimension_mismatch_error_contains_name() {
1154 let mut opt = adam_opt();
1155 let mut groups = vec![ParameterGroup {
1156 name: "my_layer".to_string(),
1157 params: vec![1.0],
1158 grad: vec![0.1, 0.2],
1159 }];
1160 let err = opt.step(&mut groups).unwrap_err();
1161 let msg = err.to_string();
1162 assert!(msg.contains("my_layer"), "error message: {msg}");
1163 }
1164
1165 #[test]
1166 fn test_empty_group_error_contains_name() {
1167 let mut opt = adam_opt();
1168 let mut groups = vec![ParameterGroup {
1169 name: "empty_layer".to_string(),
1170 params: vec![],
1171 grad: vec![],
1172 }];
1173 let err = opt.step(&mut groups).unwrap_err();
1174 let msg = err.to_string();
1175 assert!(msg.contains("empty_layer"), "error message: {msg}");
1176 }
1177
1178 #[test]
1181 fn test_optimizer_clone_is_independent() {
1182 let mut opt = adam_opt();
1183 let mut groups = vec![simple_group("w", 1.0, 0.5)];
1184 opt.step(&mut groups).expect("test: should succeed");
1185 let mut opt2 = opt.clone();
1186 opt2.reset_all();
1187 assert_eq!(opt.global_step, 1);
1189 }
1190
1191 #[test]
1192 fn test_optimizer_debug_does_not_panic() {
1193 let opt = adam_opt();
1194 let _ = format!("{opt:?}");
1195 }
1196}