1mod linalg;
8mod nuclear_norm;
9
10use crate::error::{OptimError, Result};
11use crate::optimizers::Optimizer;
12use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
13use scirs2_core::numeric::Float;
14use std::collections::HashMap;
15use std::fmt::Debug;
16use std::path::Path;
17
18use linalg::{
19 is_orthonormal, modified_gram_schmidt, power_iteration_spectral_norm,
20 project_positive_definite, to_matrix_2d, write_matrix_2d,
21};
22
23pub use nuclear_norm::{
24 nuclear_norm_of_matrix, nuclear_norm_prox, project_onto_nuclear_norm_ball,
25 truncated_svd_power_iteration, TruncatedSvd,
26};
27
28#[derive(Debug, Clone)]
30pub enum ParameterConstraint<A: Float> {
31 ValueClip {
33 min: A,
35 max: A,
37 },
38 L2NormConstraint {
40 maxnorm: A,
42 },
43 L1NormConstraint {
45 maxnorm: A,
47 },
48 NonNegative,
50 UnitSphere,
52 Simplex,
54 Orthogonal {
56 tolerance: A,
58 },
59 PositiveDefinite {
61 mineigenvalue: A,
63 },
64 SpectralNorm {
66 maxnorm: A,
68 },
69 NuclearNorm {
71 maxnorm: A,
73 },
74 Custom {
76 name: String,
78 },
79}
80
81impl<A: Float + Send + Sync> ParameterConstraint<A> {
82 pub fn apply<D: Dimension>(&self, params: &mut Array<A, D>) -> Result<()>
84 where
85 A: ScalarOperand,
86 {
87 match self {
88 ParameterConstraint::ValueClip { min, max } => {
89 params.mapv_inplace(|x| {
90 if x < *min {
91 *min
92 } else if x > *max {
93 *max
94 } else {
95 x
96 }
97 });
98 }
99 ParameterConstraint::L2NormConstraint { maxnorm } => {
100 let norm = params.mapv(|x| x * x).sum().sqrt();
101 if norm > *maxnorm {
102 let scale = *maxnorm / norm;
103 params.mapv_inplace(|x| x * scale);
104 }
105 }
106 ParameterConstraint::L1NormConstraint { maxnorm } => {
107 let norm = params.mapv(|x| x.abs()).sum();
108 if norm > *maxnorm {
109 let scale = *maxnorm / norm;
110 params.mapv_inplace(|x| x * scale);
111 }
112 }
113 ParameterConstraint::NonNegative => {
114 params.mapv_inplace(|x| if x < A::zero() { A::zero() } else { x });
115 }
116 ParameterConstraint::UnitSphere => {
117 let norm = params.mapv(|x| x * x).sum().sqrt();
118 if norm > A::zero() {
119 let scale = A::one() / norm;
120 params.mapv_inplace(|x| x * scale);
121 }
122 }
123 ParameterConstraint::Simplex => {
124 params.mapv_inplace(|x| if x < A::zero() { A::zero() } else { x });
126
127 let sum = params.sum();
129 if sum > A::zero() {
130 let scale = A::one() / sum;
131 params.mapv_inplace(|x| x * scale);
132 } else {
133 let uniform_val = A::one() / A::from(params.len()).unwrap_or(A::one());
135 params.fill(uniform_val);
136 }
137 }
138 ParameterConstraint::Orthogonal { tolerance } => {
139 if params.ndim() == 2 {
141 let matrix = to_matrix_2d(params)?;
142 let (rows, cols) = matrix.dim();
143
144 if rows > 0 && cols > 0 && is_orthonormal(&matrix, *tolerance) {
146 return Ok(());
147 }
148
149 let orthonormal = modified_gram_schmidt(&matrix);
150 write_matrix_2d(params, &orthonormal)?;
151 } else {
152 return Err(OptimError::InvalidConfig(
153 "Orthogonal constraint only applies to 2D arrays (matrices)".to_string(),
154 ));
155 }
156 }
157 ParameterConstraint::PositiveDefinite { mineigenvalue } => {
158 if params.ndim() != 2 {
160 return Err(OptimError::InvalidConfig(
161 "Positive definite constraint only applies to 2D arrays (matrices)"
162 .to_string(),
163 ));
164 }
165 let matrix = to_matrix_2d(params)?;
166 let (rows, cols) = matrix.dim();
167 if rows != cols {
168 return Err(OptimError::InvalidConfig(
169 "Positive definite constraint requires a square matrix".to_string(),
170 ));
171 }
172
173 let projected = project_positive_definite(&matrix, *mineigenvalue);
174 write_matrix_2d(params, &projected)?;
175 }
176 ParameterConstraint::SpectralNorm { maxnorm } => {
177 if params.ndim() != 2 {
179 return Err(OptimError::InvalidConfig(
180 "Spectral norm constraint only applies to 2D arrays (matrices)".to_string(),
181 ));
182 }
183 let matrix = to_matrix_2d(params)?;
184 let sigma_max = power_iteration_spectral_norm(&matrix);
185 if sigma_max > *maxnorm && sigma_max > A::zero() {
186 let scale = *maxnorm / sigma_max;
187 params.mapv_inplace(|x| x * scale);
188 }
189 }
190 ParameterConstraint::NuclearNorm { maxnorm } => {
191 if params.ndim() != 2 {
196 return Err(OptimError::InvalidConfig(
197 "Nuclear norm constraint only applies to 2D arrays (matrices)".to_string(),
198 ));
199 }
200 let matrix = to_matrix_2d(params)?;
201 let projected = project_onto_nuclear_norm_ball(&matrix, *maxnorm);
202 write_matrix_2d(params, &projected)?;
203 }
204 ParameterConstraint::Custom { name } => {
205 return Err(OptimError::InvalidConfig(format!(
206 "Custom constraint '{name}' not implemented"
207 )));
208 }
209 }
210 Ok(())
211 }
212}
213
214#[derive(Debug, Clone)]
216pub struct ParameterGroupConfig<A: Float> {
217 pub learning_rate: Option<A>,
219 pub weight_decay: Option<A>,
221 pub momentum: Option<A>,
223 pub constraints: Vec<ParameterConstraint<A>>,
225 pub custom_params: HashMap<String, A>,
227}
228
229impl<A: Float + Send + Sync> Default for ParameterGroupConfig<A> {
230 fn default() -> Self {
231 Self {
232 learning_rate: None,
233 weight_decay: None,
234 momentum: None,
235 constraints: Vec::new(),
236 custom_params: HashMap::new(),
237 }
238 }
239}
240
241impl<A: Float + Send + Sync> ParameterGroupConfig<A> {
242 pub fn new() -> Self {
244 Self::default()
245 }
246
247 pub fn with_learning_rate(mut self, lr: A) -> Self {
249 self.learning_rate = Some(lr);
250 self
251 }
252
253 pub fn with_weight_decay(mut self, wd: A) -> Self {
255 self.weight_decay = Some(wd);
256 self
257 }
258
259 pub fn with_momentum(mut self, momentum: A) -> Self {
261 self.momentum = Some(momentum);
262 self
263 }
264
265 pub fn with_custom_param(mut self, key: String, value: A) -> Self {
267 self.custom_params.insert(key, value);
268 self
269 }
270
271 pub fn with_constraint(mut self, constraint: ParameterConstraint<A>) -> Self {
273 self.constraints.push(constraint);
274 self
275 }
276
277 pub fn with_value_clip(mut self, min: A, max: A) -> Self {
279 self.constraints
280 .push(ParameterConstraint::ValueClip { min, max });
281 self
282 }
283
284 pub fn with_l2_norm_constraint(mut self, maxnorm: A) -> Self {
286 self.constraints
287 .push(ParameterConstraint::L2NormConstraint { maxnorm });
288 self
289 }
290
291 pub fn with_l1_norm_constraint(mut self, maxnorm: A) -> Self {
293 self.constraints
294 .push(ParameterConstraint::L1NormConstraint { maxnorm });
295 self
296 }
297
298 pub fn with_non_negative(mut self) -> Self {
300 self.constraints.push(ParameterConstraint::NonNegative);
301 self
302 }
303
304 pub fn with_unit_sphere(mut self) -> Self {
306 self.constraints.push(ParameterConstraint::UnitSphere);
307 self
308 }
309
310 pub fn with_simplex(mut self) -> Self {
312 self.constraints.push(ParameterConstraint::Simplex);
313 self
314 }
315
316 pub fn with_orthogonal(mut self, tolerance: A) -> Self {
318 self.constraints
319 .push(ParameterConstraint::Orthogonal { tolerance });
320 self
321 }
322
323 pub fn with_positive_definite(mut self, mineigenvalue: A) -> Self {
325 self.constraints
326 .push(ParameterConstraint::PositiveDefinite { mineigenvalue });
327 self
328 }
329
330 pub fn with_spectral_norm(mut self, maxnorm: A) -> Self {
332 self.constraints
333 .push(ParameterConstraint::SpectralNorm { maxnorm });
334 self
335 }
336
337 pub fn with_nuclear_norm(mut self, maxnorm: A) -> Self {
339 self.constraints
340 .push(ParameterConstraint::NuclearNorm { maxnorm });
341 self
342 }
343
344 pub fn with_custom_constraint(mut self, name: String) -> Self {
346 self.constraints.push(ParameterConstraint::Custom { name });
347 self
348 }
349}
350
351#[derive(Debug)]
353pub struct ParameterGroup<A: Float, D: Dimension> {
354 pub id: usize,
356 pub params: Vec<Array<A, D>>,
358 pub config: ParameterGroupConfig<A>,
360 pub state: HashMap<String, Vec<Array<A, D>>>,
362}
363
364impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> ParameterGroup<A, D> {
365 pub fn new(id: usize, params: Vec<Array<A, D>>, config: ParameterGroupConfig<A>) -> Self {
367 Self {
368 id,
369 params,
370 config,
371 state: HashMap::new(),
372 }
373 }
374
375 pub fn num_params(&self) -> usize {
377 self.params.len()
378 }
379
380 pub fn learning_rate(&self, default: A) -> A {
382 self.config.learning_rate.unwrap_or(default)
383 }
384
385 pub fn weight_decay(&self, default: A) -> A {
387 self.config.weight_decay.unwrap_or(default)
388 }
389
390 pub fn momentum(&self, default: A) -> A {
392 self.config.momentum.unwrap_or(default)
393 }
394
395 pub fn get_custom_param(&self, key: &str, default: A) -> A {
397 self.config
398 .custom_params
399 .get(key)
400 .copied()
401 .unwrap_or(default)
402 }
403
404 pub fn apply_constraints(&mut self) -> Result<()>
406 where
407 A: ScalarOperand + Send + Sync,
408 {
409 for constraint in &self.config.constraints {
410 for param in &mut self.params {
411 constraint.apply(param)?;
412 }
413 }
414 Ok(())
415 }
416
417 pub fn apply_constraints_to_param(&self, param: &mut Array<A, D>) -> Result<()>
419 where
420 A: ScalarOperand + Send + Sync,
421 {
422 for constraint in &self.config.constraints {
423 constraint.apply(param)?;
424 }
425 Ok(())
426 }
427
428 pub fn constraints(&self) -> &[ParameterConstraint<A>] {
430 &self.config.constraints
431 }
432}
433
434pub trait GroupedOptimizer<A: Float + ScalarOperand + Debug, D: Dimension>:
436 Optimizer<A, D>
437{
438 fn add_group(
440 &mut self,
441 params: Vec<Array<A, D>>,
442 config: ParameterGroupConfig<A>,
443 ) -> Result<usize>;
444
445 fn get_group(&self, groupid: usize) -> Result<&ParameterGroup<A, D>>;
447
448 fn get_group_mut(&mut self, groupid: usize) -> Result<&mut ParameterGroup<A, D>>;
450
451 fn groups(&self) -> &[ParameterGroup<A, D>];
453
454 fn groups_mut(&mut self) -> &mut [ParameterGroup<A, D>];
456
457 fn step_group(
459 &mut self,
460 group_id: usize,
461 gradients: &[Array<A, D>],
462 ) -> Result<Vec<Array<A, D>>>;
463
464 fn set_group_learning_rate(&mut self, groupid: usize, lr: A) -> Result<()>;
466
467 fn set_group_weight_decay(&mut self, groupid: usize, wd: A) -> Result<()>;
469}
470
471#[derive(Debug)]
473pub struct GroupManager<A: Float, D: Dimension> {
474 groups: Vec<ParameterGroup<A, D>>,
475 next_id: usize,
476}
477
478impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> Default for GroupManager<A, D> {
479 fn default() -> Self {
480 Self {
481 groups: Vec::new(),
482 next_id: 0,
483 }
484 }
485}
486
487impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> GroupManager<A, D> {
488 pub fn new() -> Self {
490 Self::default()
491 }
492
493 pub fn add_group(
495 &mut self,
496 params: Vec<Array<A, D>>,
497 config: ParameterGroupConfig<A>,
498 ) -> usize {
499 let id = self.next_id;
500 self.next_id += 1;
501 self.groups.push(ParameterGroup::new(id, params, config));
502 id
503 }
504
505 pub fn get_group(&self, id: usize) -> Result<&ParameterGroup<A, D>> {
507 self.groups
508 .iter()
509 .find(|g| g.id == id)
510 .ok_or_else(|| OptimError::InvalidConfig(format!("Group {id} not found")))
511 }
512
513 pub fn get_group_mut(&mut self, id: usize) -> Result<&mut ParameterGroup<A, D>> {
515 self.groups
516 .iter_mut()
517 .find(|g| g.id == id)
518 .ok_or_else(|| OptimError::InvalidConfig(format!("Group {id} not found")))
519 }
520
521 pub fn groups(&self) -> &[ParameterGroup<A, D>] {
523 &self.groups
524 }
525
526 pub fn groups_mut(&mut self) -> &mut [ParameterGroup<A, D>] {
528 &mut self.groups
529 }
530
531 pub fn total_params(&self) -> usize {
533 self.groups.iter().map(|g| g.num_params()).sum()
534 }
535}
536
537pub mod checkpointing {
539 use super::*;
540
541 #[derive(Debug, Clone)]
543 pub struct OptimizerCheckpoint<A: Float, D: Dimension> {
544 pub step: usize,
546 pub groups: Vec<ParameterGroupCheckpoint<A, D>>,
548 pub global_state: HashMap<String, String>,
550 pub metadata: CheckpointMetadata,
552 }
553
554 #[derive(Debug, Clone)]
556 pub struct ParameterGroupCheckpoint<A: Float, D: Dimension> {
557 pub id: usize,
559 pub params: Vec<Array<A, D>>,
561 pub config: ParameterGroupConfig<A>,
563 pub state: HashMap<String, Vec<Array<A, D>>>,
565 }
566
567 #[derive(Debug, Clone)]
569 pub struct CheckpointMetadata {
570 pub timestamp: String,
572 pub optimizerversion: String,
574 pub custom: HashMap<String, String>,
576 }
577
578 impl CheckpointMetadata {
579 pub fn new(optimizerversion: String) -> Self {
581 use std::time::{SystemTime, UNIX_EPOCH};
582
583 let timestamp = SystemTime::now()
584 .duration_since(UNIX_EPOCH)
585 .unwrap_or_default()
586 .as_secs()
587 .to_string();
588
589 Self {
590 timestamp,
591 optimizerversion,
592 custom: HashMap::new(),
593 }
594 }
595
596 pub fn with_custom(mut self, key: String, value: String) -> Self {
598 self.custom.insert(key, value);
599 self
600 }
601 }
602
603 pub trait Checkpointable<
605 A: Float + ToString + std::fmt::Display + std::str::FromStr,
606 D: Dimension,
607 >
608 {
609 fn create_checkpoint(&self) -> Result<OptimizerCheckpoint<A, D>>;
611
612 fn restore_checkpoint(&mut self, checkpoint: &OptimizerCheckpoint<A, D>) -> Result<()>;
614
615 fn save_checkpoint<P: AsRef<Path>>(&self, path: P) -> Result<()> {
617 use std::fs::File;
618 use std::io::{BufWriter, Write};
619
620 let checkpoint = self.create_checkpoint()?;
621 let path = path.as_ref();
622
623 let file = File::create(path).map_err(|e| {
625 OptimError::InvalidConfig(format!("Failed to create checkpoint file: {e}"))
626 })?;
627 let mut writer = BufWriter::new(file);
628
629 writeln!(writer, "# ScirS2 Optimizer Checkpoint v1.0").map_err(|e| {
631 OptimError::InvalidConfig(format!("Failed to write checkpoint header: {e}"))
632 })?;
633 writeln!(writer, "# Timestamp: {}", checkpoint.metadata.timestamp).map_err(|e| {
634 OptimError::InvalidConfig(format!("Failed to write timestamp: {e}"))
635 })?;
636 writeln!(
637 writer,
638 "# Optimizer Version: {}",
639 checkpoint.metadata.optimizerversion
640 )
641 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write version: {e}")))?;
642 writeln!(writer, "# Step: {}", checkpoint.step)
643 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write step: {e}")))?;
644 writeln!(writer)
645 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write newline: {e}")))?;
646
647 writeln!(writer, "[METADATA]").map_err(|e| {
649 OptimError::InvalidConfig(format!("Failed to write metadata section: {e}"))
650 })?;
651 for (key, value) in &checkpoint.metadata.custom {
652 writeln!(writer, "{}={}", key, value).map_err(|e| {
653 OptimError::InvalidConfig(format!("Failed to write metadata entry: {e}"))
654 })?;
655 }
656 writeln!(writer)
657 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write newline: {e}")))?;
658
659 writeln!(writer, "[GLOBAL_STATE]").map_err(|e| {
661 OptimError::InvalidConfig(format!("Failed to write global state section: {e}"))
662 })?;
663 for (key, value) in &checkpoint.global_state {
664 writeln!(writer, "{}={}", key, value).map_err(|e| {
665 OptimError::InvalidConfig(format!("Failed to write global state entry: {e}"))
666 })?;
667 }
668 writeln!(writer)
669 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write newline: {e}")))?;
670
671 writeln!(writer, "[GROUPS]").map_err(|e| {
673 OptimError::InvalidConfig(format!("Failed to write groups section: {e}"))
674 })?;
675 writeln!(writer, "count={}", checkpoint.groups.len()).map_err(|e| {
676 OptimError::InvalidConfig(format!("Failed to write group count: {e}"))
677 })?;
678 writeln!(writer)
679 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write newline: {e}")))?;
680
681 for group in &checkpoint.groups {
682 writeln!(writer, "[GROUP_{}]", group.id).map_err(|e| {
684 OptimError::InvalidConfig(format!("Failed to write group header: {e}"))
685 })?;
686
687 writeln!(
689 writer,
690 "learning_rate={}",
691 group
692 .config
693 .learning_rate
694 .map(|lr| lr.to_string())
695 .unwrap_or_else(|| "None".to_string())
696 )
697 .map_err(|e| {
698 OptimError::InvalidConfig(format!("Failed to write learning rate: {e}"))
699 })?;
700 writeln!(
701 writer,
702 "weight_decay={}",
703 group
704 .config
705 .weight_decay
706 .map(|wd| wd.to_string())
707 .unwrap_or_else(|| "None".to_string())
708 )
709 .map_err(|e| {
710 OptimError::InvalidConfig(format!("Failed to write weight decay: {e}"))
711 })?;
712 writeln!(
713 writer,
714 "momentum={}",
715 group
716 .config
717 .momentum
718 .map(|m| m.to_string())
719 .unwrap_or_else(|| "None".to_string())
720 )
721 .map_err(|e| OptimError::InvalidConfig(format!("Failed to write momentum: {e}")))?;
722
723 writeln!(
725 writer,
726 "custom_params_count={}",
727 group.config.custom_params.len()
728 )
729 .map_err(|e| {
730 OptimError::InvalidConfig(format!("Failed to write custom params count: {e}"))
731 })?;
732 for (key, value) in &group.config.custom_params {
733 writeln!(writer, "custom_{}={}", key, value).map_err(|e| {
734 OptimError::InvalidConfig(format!("Failed to write custom param: {e}"))
735 })?;
736 }
737
738 writeln!(writer, "param_count={}", group.params.len()).map_err(|e| {
740 OptimError::InvalidConfig(format!("Failed to write param count: {e}"))
741 })?;
742 for (i, param) in group.params.iter().enumerate() {
743 writeln!(writer, "param_{}shape={:?}", i, param.shape()).map_err(|e| {
744 OptimError::InvalidConfig(format!("Failed to write param shape: {e}"))
745 })?;
746 write!(writer, "param_{}_data=", i).map_err(|e| {
747 OptimError::InvalidConfig(format!("Failed to write param data label: {e}"))
748 })?;
749
750 for (j, &val) in param.iter().enumerate() {
752 if j > 0 {
753 write!(writer, " ").map_err(|e| {
754 OptimError::InvalidConfig(format!("Failed to write space: {e}"))
755 })?;
756 }
757 write!(writer, "{}", val).map_err(|e| {
758 OptimError::InvalidConfig(format!("Failed to write value: {e}"))
759 })?;
760 }
761 writeln!(writer).map_err(|e| {
762 OptimError::InvalidConfig(format!("Failed to write newline: {e}"))
763 })?;
764 }
765
766 writeln!(writer, "state_count={}", group.state.len()).map_err(|e| {
768 OptimError::InvalidConfig(format!("Failed to write state count: {e}"))
769 })?;
770 for (state_name, state_arrays) in &group.state {
771 writeln!(writer, "state_name={}", state_name).map_err(|e| {
772 OptimError::InvalidConfig(format!("Failed to write state name: {e}"))
773 })?;
774 writeln!(writer, "state_array_count={}", state_arrays.len()).map_err(|e| {
775 OptimError::InvalidConfig(format!("Failed to write state array count: {e}"))
776 })?;
777 for (i, array) in state_arrays.iter().enumerate() {
778 writeln!(writer, "state_{}shape={:?}", i, array.shape()).map_err(|e| {
779 OptimError::InvalidConfig(format!("Failed to write state shape: {e}"))
780 })?;
781 write!(writer, "state_{}_data=", i).map_err(|e| {
782 OptimError::InvalidConfig(format!(
783 "Failed to write state data label: {}",
784 e
785 ))
786 })?;
787
788 for (j, &val) in array.iter().enumerate() {
790 if j > 0 {
791 write!(writer, " ").map_err(|e| {
792 OptimError::InvalidConfig(format!(
793 "Failed to write space: {}",
794 e
795 ))
796 })?;
797 }
798 write!(writer, "{}", val).map_err(|e| {
799 OptimError::InvalidConfig(format!("Failed to write value: {e}"))
800 })?;
801 }
802 writeln!(writer).map_err(|e| {
803 OptimError::InvalidConfig(format!("Failed to write newline: {e}"))
804 })?;
805 }
806 }
807
808 writeln!(writer).map_err(|e| {
809 OptimError::InvalidConfig(format!("Failed to write newline: {e}"))
810 })?;
811 }
812
813 writer.flush().map_err(|e| {
814 OptimError::InvalidConfig(format!("Failed to flush checkpoint file: {e}"))
815 })?;
816
817 Ok(())
818 }
819
820 fn load_checkpoint<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
822 use std::fs::File;
823 use std::io::{BufRead, BufReader};
824
825 let path = path.as_ref();
826 let file = File::open(path).map_err(|e| {
827 OptimError::InvalidConfig(format!("Failed to open checkpoint file: {e}"))
828 })?;
829 let reader = BufReader::new(file);
830 let mut lines = reader.lines();
831
832 let mut step = 0;
834 let mut optimizerversion = String::new();
835 let mut timestamp = String::new();
836
837 while let Some(Ok(line)) = lines.next() {
838 if line.starts_with("# Step: ") {
839 step = line.trim_start_matches("# Step: ").parse().map_err(|_| {
840 OptimError::InvalidConfig("Invalid step format".to_string())
841 })?;
842 } else if line.starts_with("# Optimizer Version: ") {
843 optimizerversion = line.trim_start_matches("# Optimizer Version: ").to_string();
844 } else if line.starts_with("# Timestamp: ") {
845 timestamp = line.trim_start_matches("# Timestamp: ").to_string();
846 } else if line.starts_with("[METADATA]") {
847 break;
848 }
849 }
850
851 let mut custom_metadata = HashMap::new();
853 while let Some(Ok(line)) = lines.next() {
854 if line.is_empty() || line.starts_with("[") {
855 if line.starts_with("[GLOBAL_STATE]") {
856 break;
857 }
858 continue;
859 }
860 if let Some((key, value)) = line.split_once('=') {
861 custom_metadata.insert(key.to_string(), value.to_string());
862 }
863 }
864
865 let mut global_state = HashMap::new();
867 while let Some(Ok(line)) = lines.next() {
868 if line.is_empty() || line.starts_with("[") {
869 if line.starts_with("[GROUPS]") {
870 break;
871 }
872 continue;
873 }
874 if let Some((key, value)) = line.split_once('=') {
875 global_state.insert(key.to_string(), value.to_string());
876 }
877 }
878
879 let mut group_count = 0;
881 while let Some(Ok(line)) = lines.next() {
882 if line.starts_with("count=") {
883 group_count = line.trim_start_matches("count=").parse().map_err(|_| {
884 OptimError::InvalidConfig("Invalid group count".to_string())
885 })?;
886 break;
887 }
888 }
889
890 let mut groups = Vec::new();
892 for _ in 0..group_count {
893 let mut group_id = 0;
895 while let Some(Ok(line)) = lines.next() {
896 if line.starts_with("[GROUP_") {
897 let id_str = line.trim_start_matches("[GROUP_").trim_end_matches(']');
898 group_id = id_str.parse().map_err(|_| {
899 OptimError::InvalidConfig("Invalid group ID".to_string())
900 })?;
901 break;
902 }
903 }
904
905 let mut learning_rate = None;
907 let mut weight_decay = None;
908 let mut momentum = None;
909 let mut custom_params = HashMap::new();
910 let mut _custom_params_count = 0;
911
912 while let Some(Ok(line)) = lines.next() {
913 if line.starts_with("learning_rate=") {
914 let val_str = line.trim_start_matches("learning_rate=");
915 if val_str != "None" {
916 learning_rate = Some(A::from_str(val_str).map_err(|_| {
917 OptimError::InvalidConfig("Invalid learning rate".to_string())
918 })?);
919 }
920 } else if line.starts_with("weight_decay=") {
921 let val_str = line.trim_start_matches("weight_decay=");
922 if val_str != "None" {
923 weight_decay = Some(A::from_str(val_str).map_err(|_| {
924 OptimError::InvalidConfig("Invalid weight decay".to_string())
925 })?);
926 }
927 } else if line.starts_with("momentum=") {
928 let val_str = line.trim_start_matches("momentum=");
929 if val_str != "None" {
930 momentum = Some(A::from_str(val_str).map_err(|_| {
931 OptimError::InvalidConfig("Invalid momentum".to_string())
932 })?);
933 }
934 } else if line.starts_with("custom_params_count=") {
935 _custom_params_count = line
936 .trim_start_matches("custom_params_count=")
937 .parse()
938 .map_err(|_| {
939 OptimError::InvalidConfig("Invalid custom params count".to_string())
940 })?;
941 } else if line.starts_with("custom_") {
942 if let Some((key_with_prefix, value)) = line.split_once('=') {
943 let key = key_with_prefix.trim_start_matches("custom_");
944 custom_params.insert(
945 key.to_string(),
946 A::from_str(value).map_err(|_| {
947 OptimError::InvalidConfig(
948 "Invalid custom param value".to_string(),
949 )
950 })?,
951 );
952 }
953 } else if line.starts_with("param_count=") {
954 break;
955 }
956 }
957
958 let config = ParameterGroupConfig {
960 learning_rate,
961 weight_decay,
962 momentum,
963 constraints: Vec::new(), custom_params,
965 };
966
967 let param_count: usize = lines
969 .next()
970 .ok_or_else(|| OptimError::InvalidConfig("Missing param count".to_string()))?
971 .map_err(|e| OptimError::InvalidConfig(format!("Failed to read line: {e}")))?
972 .trim_start_matches("param_count=")
973 .parse()
974 .map_err(|_| OptimError::InvalidConfig("Invalid param count".to_string()))?;
975
976 let mut params = Vec::new();
977 for i in 0..param_count {
978 let shape_line = lines
980 .next()
981 .ok_or_else(|| {
982 OptimError::InvalidConfig("Missing param shape".to_string())
983 })?
984 .map_err(|e| {
985 OptimError::InvalidConfig(format!("Failed to read line: {e}"))
986 })?;
987
988 let shape_str = shape_line
989 .trim_start_matches(&format!("param_{}shape=", i))
990 .trim_start_matches('[')
991 .trim_end_matches(']');
992
993 let shape: Vec<usize> = shape_str
994 .split(", ")
995 .map(|s| {
996 s.parse()
997 .map_err(|_| OptimError::InvalidConfig("Invalid shape".to_string()))
998 })
999 .collect::<Result<Vec<_>>>()?;
1000
1001 let data_line = lines
1003 .next()
1004 .ok_or_else(|| OptimError::InvalidConfig("Missing param data".to_string()))?
1005 .map_err(|e| {
1006 OptimError::InvalidConfig(format!("Failed to read line: {e}"))
1007 })?;
1008
1009 let data_str = data_line.trim_start_matches(&format!("param_{}_data=", i));
1010 let data: Vec<A> = data_str
1011 .split(' ')
1012 .filter(|s| !s.is_empty())
1013 .map(|s| {
1014 A::from_str(s).map_err(|_| {
1015 OptimError::InvalidConfig("Invalid data value".to_string())
1016 })
1017 })
1018 .collect::<Result<Vec<_>>>()?;
1019
1020 let array: Array<A, scirs2_core::ndarray::IxDyn> =
1022 Array::from_shape_vec(shape, data).map_err(|e| {
1023 OptimError::InvalidConfig(format!("Failed to create array: {e}"))
1024 })?;
1025 params.push(array);
1026 }
1027
1028 let state_count: usize = lines
1030 .next()
1031 .ok_or_else(|| OptimError::InvalidConfig("Missing state count".to_string()))?
1032 .map_err(|e| OptimError::InvalidConfig(format!("Failed to read line: {e}")))?
1033 .trim_start_matches("state_count=")
1034 .parse()
1035 .map_err(|_| OptimError::InvalidConfig("Invalid state count".to_string()))?;
1036
1037 let mut state = HashMap::new();
1038 for _ in 0..state_count {
1039 let state_name = lines
1040 .next()
1041 .ok_or_else(|| OptimError::InvalidConfig("Missing state name".to_string()))?
1042 .map_err(|e| {
1043 OptimError::InvalidConfig(format!("Failed to read line: {e}"))
1044 })?
1045 .trim_start_matches("state_name=")
1046 .to_string();
1047
1048 let array_count: usize = lines
1049 .next()
1050 .ok_or_else(|| {
1051 OptimError::InvalidConfig("Missing state array count".to_string())
1052 })?
1053 .map_err(|e| {
1054 OptimError::InvalidConfig(format!("Failed to read line: {e}"))
1055 })?
1056 .trim_start_matches("state_array_count=")
1057 .parse()
1058 .map_err(|_| {
1059 OptimError::InvalidConfig("Invalid state array count".to_string())
1060 })?;
1061
1062 let mut state_arrays = Vec::new();
1063 for i in 0..array_count {
1064 let shape_line = lines
1066 .next()
1067 .ok_or_else(|| {
1068 OptimError::InvalidConfig("Missing state shape".to_string())
1069 })?
1070 .map_err(|e| {
1071 OptimError::InvalidConfig(format!("Failed to read line: {e}"))
1072 })?;
1073
1074 let shape_str = shape_line
1075 .trim_start_matches(&format!("state_{}shape=", i))
1076 .trim_start_matches('[')
1077 .trim_end_matches(']');
1078
1079 let shape: Vec<usize> = shape_str
1080 .split(", ")
1081 .map(|s| {
1082 s.parse().map_err(|_| {
1083 OptimError::InvalidConfig("Invalid state shape".to_string())
1084 })
1085 })
1086 .collect::<Result<Vec<_>>>()?;
1087
1088 let data_line = lines
1090 .next()
1091 .ok_or_else(|| {
1092 OptimError::InvalidConfig("Missing state data".to_string())
1093 })?
1094 .map_err(|e| {
1095 OptimError::InvalidConfig(format!("Failed to read line: {e}"))
1096 })?;
1097
1098 let data_str = data_line.trim_start_matches(&format!("state_{}_data=", i));
1099 let data: Vec<A> = data_str
1100 .split(' ')
1101 .filter(|s| !s.is_empty())
1102 .map(|s| {
1103 A::from_str(s).map_err(|_| {
1104 OptimError::InvalidConfig("Invalid state value".to_string())
1105 })
1106 })
1107 .collect::<Result<Vec<_>>>()?;
1108
1109 let array = Array::from_shape_vec(shape, data).map_err(|e| {
1111 OptimError::InvalidConfig(format!("Failed to create state array: {e}"))
1112 })?;
1113 state_arrays.push(array);
1114 }
1115
1116 state.insert(state_name, state_arrays);
1117 }
1118
1119 groups.push(ParameterGroupCheckpoint {
1121 id: group_id,
1122 params,
1123 config,
1124 state,
1125 });
1126 }
1127
1128 let mut metadata = CheckpointMetadata::new(optimizerversion);
1130 metadata.timestamp = timestamp;
1131 metadata.custom = custom_metadata;
1132
1133 let _dyn_checkpoint = OptimizerCheckpoint::<A, scirs2_core::ndarray::IxDyn> {
1135 step,
1136 groups,
1137 global_state,
1138 metadata,
1139 };
1140
1141 Err(OptimError::InvalidConfig(
1155 "Checkpoint loading from file with dimension type conversion is not supported in v1.0.0. \
1156 Use CheckpointManager for in-memory checkpoints, or save/load with consistent dimension types. \
1157 See documentation for checkpoint best practices.".to_string(),
1158 ))
1159 }
1160 }
1161
1162 #[derive(Debug)]
1164 pub struct CheckpointManager<A: Float, D: Dimension> {
1165 checkpoints: HashMap<String, OptimizerCheckpoint<A, D>>,
1166 _maxcheckpoints: usize,
1167 checkpoint_keys: Vec<String>, }
1169
1170 impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> CheckpointManager<A, D> {
1171 pub fn new() -> Self {
1173 Self {
1174 checkpoints: HashMap::new(),
1175 _maxcheckpoints: 10,
1176 checkpoint_keys: Vec::new(),
1177 }
1178 }
1179
1180 pub fn with_max_checkpoints(_maxcheckpoints: usize) -> Self {
1182 Self {
1183 checkpoints: HashMap::new(),
1184 _maxcheckpoints,
1185 checkpoint_keys: Vec::new(),
1186 }
1187 }
1188
1189 pub fn store_checkpoint(&mut self, key: String, checkpoint: OptimizerCheckpoint<A, D>) {
1191 if self.checkpoints.contains_key(&key) {
1193 self.checkpoints.insert(key.clone(), checkpoint);
1194 return;
1195 }
1196
1197 if self.checkpoints.len() >= self._maxcheckpoints {
1199 if let Some(oldest_key) = self.checkpoint_keys.first().cloned() {
1200 self.checkpoints.remove(&oldest_key);
1201 self.checkpoint_keys.retain(|k| k != &oldest_key);
1202 }
1203 }
1204
1205 self.checkpoints.insert(key.clone(), checkpoint);
1207 self.checkpoint_keys.push(key);
1208 }
1209
1210 pub fn get_checkpoint(&self, key: &str) -> Option<&OptimizerCheckpoint<A, D>> {
1212 self.checkpoints.get(key)
1213 }
1214
1215 pub fn remove_checkpoint(&mut self, key: &str) -> Option<OptimizerCheckpoint<A, D>> {
1217 self.checkpoint_keys.retain(|k| k != key);
1218 self.checkpoints.remove(key)
1219 }
1220
1221 pub fn list_checkpoints(&self) -> &[String] {
1223 &self.checkpoint_keys
1224 }
1225
1226 pub fn clear(&mut self) {
1228 self.checkpoints.clear();
1229 self.checkpoint_keys.clear();
1230 }
1231
1232 pub fn len(&self) -> usize {
1234 self.checkpoints.len()
1235 }
1236
1237 pub fn is_empty(&self) -> bool {
1239 self.checkpoints.is_empty()
1240 }
1241 }
1242
1243 impl<A: Float + ScalarOperand + Debug, D: Dimension + Send + Sync> Default
1244 for CheckpointManager<A, D>
1245 {
1246 fn default() -> Self {
1247 Self::new()
1248 }
1249 }
1250
1251 pub mod utils {
1253 use super::*;
1254
1255 pub fn create_checkpoint_from_groups<A: Float + ScalarOperand + Debug, D: Dimension>(
1257 step: usize,
1258 groups: &[ParameterGroup<A, D>],
1259 global_state: HashMap<String, String>,
1260 optimizerversion: String,
1261 ) -> OptimizerCheckpoint<A, D> {
1262 let group_checkpoints = groups
1263 .iter()
1264 .map(|group| ParameterGroupCheckpoint {
1265 id: group.id,
1266 params: group.params.clone(),
1267 config: group.config.clone(),
1268 state: group.state.clone(),
1269 })
1270 .collect();
1271
1272 OptimizerCheckpoint {
1273 step,
1274 groups: group_checkpoints,
1275 global_state,
1276 metadata: CheckpointMetadata::new(optimizerversion),
1277 }
1278 }
1279
1280 pub fn validate_checkpoint<A: Float, D: Dimension>(
1282 checkpoint: &OptimizerCheckpoint<A, D>,
1283 expected_groups: usize,
1284 ) -> Result<()> {
1285 if checkpoint.groups.len() != expected_groups {
1286 return Err(OptimError::InvalidConfig(format!(
1287 "Checkpoint has {} groups, expected {expected_groups}",
1288 checkpoint.groups.len()
1289 )));
1290 }
1291
1292 let mut ids = std::collections::HashSet::new();
1294 for group in &checkpoint.groups {
1295 if !ids.insert(group.id) {
1296 return Err(OptimError::InvalidConfig(format!(
1297 "Duplicate group ID {} in checkpoint",
1298 group.id
1299 )));
1300 }
1301 }
1302
1303 Ok(())
1304 }
1305
1306 pub fn checkpoint_summary<A: Float, D: Dimension>(
1308 checkpoint: &OptimizerCheckpoint<A, D>,
1309 ) -> String {
1310 let total_params: usize = checkpoint
1311 .groups
1312 .iter()
1313 .map(|g| g.params.iter().map(|p| p.len()).sum::<usize>())
1314 .sum();
1315
1316 format!(
1317 "Checkpoint at step {}: {} groups, {} total parameters, created at {}",
1318 checkpoint.step,
1319 checkpoint.groups.len(),
1320 total_params,
1321 checkpoint.metadata.timestamp
1322 )
1323 }
1324 }
1325}
1326
1327#[cfg(test)]
1328mod tests {
1329 use super::linalg::jacobi_eigen_symmetric;
1330 use super::*;
1331 use scirs2_core::ndarray::{Array1, Array2};
1332
1333 #[test]
1334 fn test_parameter_group_config() {
1335 let config = ParameterGroupConfig::new()
1336 .with_learning_rate(0.01)
1337 .with_weight_decay(0.0001)
1338 .with_momentum(0.9)
1339 .with_custom_param("beta1".to_string(), 0.9)
1340 .with_custom_param("beta2".to_string(), 0.999);
1341
1342 assert_eq!(config.learning_rate, Some(0.01));
1343 assert_eq!(config.weight_decay, Some(0.0001));
1344 assert_eq!(config.momentum, Some(0.9));
1345 assert_eq!(config.custom_params.get("beta1"), Some(&0.9));
1346 assert_eq!(config.custom_params.get("beta2"), Some(&0.999));
1347 }
1348
1349 #[test]
1350 fn test_parameter_group() {
1351 let params = vec![Array1::zeros(5), Array1::ones(3)];
1352 let config = ParameterGroupConfig::new().with_learning_rate(0.01);
1353
1354 let group = ParameterGroup::new(0, params, config);
1355
1356 assert_eq!(group.id, 0);
1357 assert_eq!(group.num_params(), 2);
1358 assert_eq!(group.learning_rate(0.001), 0.01);
1359 assert_eq!(group.weight_decay(0.0), 0.0);
1360 }
1361
1362 #[test]
1363 fn test_group_manager() {
1364 let mut manager: GroupManager<f64, scirs2_core::ndarray::Ix1> = GroupManager::new();
1365
1366 let params1 = vec![Array1::zeros(5)];
1368 let config1 = ParameterGroupConfig::new().with_learning_rate(0.01);
1369 let id1 = manager.add_group(params1, config1);
1370
1371 let params2 = vec![Array1::ones(3), Array1::zeros(4)];
1373 let config2 = ParameterGroupConfig::new().with_learning_rate(0.001);
1374 let id2 = manager.add_group(params2, config2);
1375
1376 assert_eq!(id1, 0);
1377 assert_eq!(id2, 1);
1378 assert_eq!(manager.groups().len(), 2);
1379 assert_eq!(manager.total_params(), 3);
1380
1381 let group1 = manager
1383 .get_group(id1)
1384 .expect("manager.get_group succeeds in test_group_manager");
1385 assert_eq!(group1.learning_rate(0.0), 0.01);
1386
1387 let group2 = manager
1388 .get_group(id2)
1389 .expect("manager.get_group succeeds in test_group_manager");
1390 assert_eq!(group2.learning_rate(0.0), 0.001);
1391 }
1392
1393 #[test]
1394 fn test_parameter_constraints() {
1395 use approx::assert_relative_eq;
1396
1397 let mut params = Array1::from_vec(vec![-2.0, 0.5, 3.0]);
1399 let clip_constraint = ParameterConstraint::ValueClip { min: 0.0, max: 1.0 };
1400 clip_constraint
1401 .apply(&mut params)
1402 .expect("clip_constraint.apply succeeds in test_parameter_constraints");
1403 assert_eq!(
1404 params
1405 .as_slice()
1406 .expect("params.as_slice succeeds in test_parameter_constraints"),
1407 &[0.0, 0.5, 1.0]
1408 );
1409
1410 let mut params = Array1::from_vec(vec![3.0, 4.0]); let l2_constraint = ParameterConstraint::L2NormConstraint { maxnorm: 2.0 };
1413 l2_constraint
1414 .apply(&mut params)
1415 .expect("l2_constraint.apply succeeds in test_parameter_constraints");
1416 let new_norm = params.mapv(|x| x * x).sum().sqrt();
1417 assert_relative_eq!(new_norm, 2.0, epsilon = 1e-6);
1418
1419 let mut params = Array1::from_vec(vec![-1.0, 2.0, -3.0]);
1421 let non_neg_constraint = ParameterConstraint::NonNegative;
1422 non_neg_constraint
1423 .apply(&mut params)
1424 .expect("apply succeeds in test_parameter_constraints");
1425 assert_eq!(
1426 params
1427 .as_slice()
1428 .expect("params.as_slice succeeds in test_parameter_constraints"),
1429 &[0.0, 2.0, 0.0]
1430 );
1431
1432 let mut params = Array1::from_vec(vec![3.0, 4.0]); let unit_sphere_constraint = ParameterConstraint::UnitSphere;
1435 unit_sphere_constraint
1436 .apply(&mut params)
1437 .expect("apply succeeds in test_parameter_constraints");
1438 let new_norm = params.mapv(|x| x * x).sum().sqrt();
1439 assert_relative_eq!(new_norm, 1.0, epsilon = 1e-6);
1440 }
1441
1442 #[test]
1443 fn test_parameter_group_with_constraints() {
1444 let params = vec![Array1::from_vec(vec![-2.0, 3.0])];
1445 let config = ParameterGroupConfig::new()
1446 .with_learning_rate(0.01)
1447 .with_value_clip(0.0, 1.0);
1448
1449 let mut group = ParameterGroup::new(0, params, config);
1450
1451 group
1453 .apply_constraints()
1454 .expect("group.apply_constraints succeeds in test_parameter_group_with_constraints");
1455
1456 assert_eq!(
1458 group.params[0]
1459 .as_slice()
1460 .expect("as_slice succeeds in test_parameter_group_with_constraints"),
1461 &[0.0, 1.0]
1462 );
1463 }
1464
1465 #[test]
1466 fn test_parameter_config_builder() {
1467 let config = ParameterGroupConfig::new()
1468 .with_learning_rate(0.01)
1469 .with_l2_norm_constraint(1.0)
1470 .with_non_negative()
1471 .with_custom_param("beta".to_string(), 0.9);
1472
1473 assert_eq!(config.learning_rate, Some(0.01));
1474 assert_eq!(config.constraints.len(), 2);
1475 assert_eq!(config.custom_params.get("beta"), Some(&0.9));
1476 }
1477
1478 #[test]
1479 fn test_simplex_constraint() {
1480 use approx::assert_relative_eq;
1481
1482 let mut params = Array1::from_vec(vec![2.0, 3.0, 5.0]);
1484 let simplex_constraint = ParameterConstraint::Simplex;
1485 simplex_constraint
1486 .apply(&mut params)
1487 .expect("apply succeeds in test_simplex_constraint");
1488
1489 let sum: f64 = params.sum();
1491 assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
1492 assert!(params.iter().all(|&x| x >= 0.0));
1493
1494 assert_relative_eq!(params[0], 0.2, epsilon = 1e-6); assert_relative_eq!(params[1], 0.3, epsilon = 1e-6); assert_relative_eq!(params[2], 0.5, epsilon = 1e-6); }
1499
1500 #[test]
1501 fn test_simplex_constraint_with_negatives() {
1502 use approx::assert_relative_eq;
1503
1504 let mut params = Array1::from_vec(vec![-1.0, 2.0, 3.0]);
1506 let simplex_constraint = ParameterConstraint::Simplex;
1507 simplex_constraint
1508 .apply(&mut params)
1509 .expect("apply succeeds in test_simplex_constraint_with_negatives");
1510
1511 let sum: f64 = params.sum();
1513 assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
1514 assert!(params.iter().all(|&x| x >= 0.0));
1515
1516 assert_relative_eq!(params[0], 0.0, epsilon = 1e-6);
1518 assert_relative_eq!(params[1], 0.4, epsilon = 1e-6); assert_relative_eq!(params[2], 0.6, epsilon = 1e-6); }
1521
1522 #[test]
1523 fn test_simplex_constraint_all_zeros() {
1524 use approx::assert_relative_eq;
1525
1526 let mut params = Array1::from_vec(vec![0.0, 0.0, 0.0]);
1528 let simplex_constraint = ParameterConstraint::Simplex;
1529 simplex_constraint
1530 .apply(&mut params)
1531 .expect("apply succeeds in test_simplex_constraint_all_zeros");
1532
1533 let sum: f64 = params.sum();
1535 assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
1536 for &val in params.iter() {
1537 assert_relative_eq!(val, 1.0 / 3.0, epsilon = 1e-6);
1538 }
1539 }
1540
1541 #[test]
1542 fn test_spectral_norm_constraint() {
1543 use approx::assert_relative_eq;
1544 use scirs2_core::ndarray::arr2;
1545
1546 let mut params = arr2(&[[3.0, 4.0]]);
1548 let spectral_constraint = ParameterConstraint::SpectralNorm { maxnorm: 2.0 };
1549 spectral_constraint
1550 .apply(&mut params)
1551 .expect("apply succeeds in test_spectral_norm_constraint");
1552
1553 let sigma = power_iteration_spectral_norm(¶ms);
1555 assert_relative_eq!(sigma, 2.0, epsilon = 1e-6);
1556 }
1557
1558 #[test]
1559 fn test_nuclear_norm_constraint() {
1560 use approx::assert_relative_eq;
1561 use scirs2_core::ndarray::arr2;
1562
1563 let mut params = arr2(&[[3.0, 0.0, 0.0], [0.0, -4.0, 0.0], [0.0, 0.0, 2.0]]);
1565 let nuclear_constraint = ParameterConstraint::NuclearNorm { maxnorm: 3.0 };
1566 nuclear_constraint
1567 .apply(&mut params)
1568 .expect("apply succeeds in test_nuclear_norm_constraint");
1569
1570 let new_nuclear_norm = nuclear_norm_of_matrix(¶ms);
1574 assert_relative_eq!(new_nuclear_norm, 3.0, epsilon = 1e-6);
1575 assert_relative_eq!(params[[0, 0]], 1.0, epsilon = 1e-6);
1576 assert_relative_eq!(params[[1, 1]], -2.0, epsilon = 1e-6);
1577 assert_relative_eq!(params[[2, 2]], 0.0, epsilon = 1e-6);
1578 }
1579
1580 #[test]
1581 fn test_nuclear_norm_constraint_rejects_non_matrix() {
1582 let mut params = Array1::from_vec(vec![3.0, -4.0, 2.0]);
1584 let nuclear_constraint = ParameterConstraint::NuclearNorm { maxnorm: 3.0 };
1585
1586 match nuclear_constraint.apply(&mut params) {
1587 Ok(()) => panic!("nuclear norm constraint must reject 1-D parameters"),
1588 Err(err) => assert!(err.to_string().contains("2D arrays")),
1589 }
1590 }
1591
1592 #[test]
1593 fn test_orthogonal_constraint_error() {
1594 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1596 let orthogonal_constraint = ParameterConstraint::Orthogonal { tolerance: 1e-6 };
1597 let result = orthogonal_constraint.apply(&mut params);
1598
1599 assert!(result.is_err());
1600 assert!(result.unwrap_err().to_string().contains("2D arrays"));
1601 }
1602
1603 #[test]
1604 fn test_positive_definite_constraint_error() {
1605 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1607 let pd_constraint = ParameterConstraint::PositiveDefinite {
1608 mineigenvalue: 0.01,
1609 };
1610 let result = pd_constraint.apply(&mut params);
1611
1612 assert!(result.is_err());
1613 assert!(result.unwrap_err().to_string().contains("2D arrays"));
1614 }
1615
1616 #[test]
1617 fn test_enhanced_config_builder() {
1618 let config = ParameterGroupConfig::new()
1619 .with_learning_rate(0.01)
1620 .with_simplex()
1621 .with_spectral_norm(2.0)
1622 .with_nuclear_norm(1.5)
1623 .with_custom_constraint("my_constraint".to_string());
1624
1625 assert_eq!(config.learning_rate, Some(0.01));
1626 assert_eq!(config.constraints.len(), 4);
1627
1628 match &config.constraints[0] {
1630 ParameterConstraint::Simplex => (),
1631 _ => panic!("Expected Simplex constraint"),
1632 }
1633
1634 match &config.constraints[1] {
1635 ParameterConstraint::SpectralNorm { maxnorm } => {
1636 assert_eq!(*maxnorm, 2.0);
1637 }
1638 _ => panic!("Expected SpectralNorm constraint"),
1639 }
1640 }
1641
1642 #[test]
1643 fn test_constraint_combination() {
1644 use approx::assert_relative_eq;
1645
1646 let params = vec![Array1::from_vec(vec![-1.0, 2.0, 3.0])];
1648 let config = ParameterGroupConfig::new()
1649 .with_learning_rate(0.01)
1650 .with_non_negative()
1651 .with_simplex();
1652
1653 let mut group = ParameterGroup::new(0, params, config);
1654
1655 group
1657 .apply_constraints()
1658 .expect("group.apply_constraints succeeds in test_constraint_combination");
1659
1660 let result = &group.params[0];
1662 let sum: f64 = result.sum();
1663 assert_relative_eq!(sum, 1.0, epsilon = 1e-6);
1664 assert!(result.iter().all(|&x| x >= 0.0));
1665
1666 assert_relative_eq!(result[0], 0.0, epsilon = 1e-6);
1668 assert_relative_eq!(result[1], 0.4, epsilon = 1e-6);
1669 assert_relative_eq!(result[2], 0.6, epsilon = 1e-6);
1670 }
1671
1672 fn gram_matrix(m: &Array2<f64>) -> Array2<f64> {
1678 let (rows, cols) = m.dim();
1679 let mut g = Array2::<f64>::zeros((cols, cols));
1680 for i in 0..cols {
1681 for j in 0..cols {
1682 let mut dot = 0.0;
1683 for k in 0..rows {
1684 dot += m[[k, i]] * m[[k, j]];
1685 }
1686 g[[i, j]] = dot;
1687 }
1688 }
1689 g
1690 }
1691
1692 #[test]
1693 fn test_orthogonal_constraint_square() {
1694 use approx::assert_abs_diff_eq;
1695 use scirs2_core::ndarray::arr2;
1696
1697 let mut params = arr2(&[[1.0, 2.0, 0.0], [0.0, 1.0, 1.0], [1.0, 0.0, 1.0]]);
1699 let constraint = ParameterConstraint::Orthogonal { tolerance: 1e-10 };
1700 constraint.apply(&mut params).expect("constraint failed");
1701
1702 let g = gram_matrix(¶ms);
1704 for i in 0..3 {
1705 for j in 0..3 {
1706 let target = if i == j { 1.0 } else { 0.0 };
1707 assert_abs_diff_eq!(g[[i, j]], target, epsilon = 1e-9);
1708 }
1709 }
1710 }
1711
1712 #[test]
1713 fn test_orthogonal_constraint_tall() {
1714 use approx::assert_abs_diff_eq;
1715 use scirs2_core::ndarray::arr2;
1716
1717 let mut params = arr2(&[[1.0, 1.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]);
1719 let constraint = ParameterConstraint::Orthogonal { tolerance: 1e-10 };
1720 constraint.apply(&mut params).expect("constraint failed");
1721
1722 let g = gram_matrix(¶ms);
1724 for i in 0..2 {
1725 for j in 0..2 {
1726 let target = if i == j { 1.0 } else { 0.0 };
1727 assert_abs_diff_eq!(g[[i, j]], target, epsilon = 1e-9);
1728 }
1729 }
1730 }
1731
1732 #[test]
1733 fn test_orthogonal_constraint_already_orthonormal_unchanged() {
1734 use approx::assert_abs_diff_eq;
1735 use scirs2_core::ndarray::arr2;
1736
1737 let mut params = arr2(&[[1.0, 0.0], [0.0, 1.0]]);
1739 let original = params.clone();
1740 let constraint = ParameterConstraint::Orthogonal { tolerance: 1e-8 };
1741 constraint.apply(&mut params).expect("constraint failed");
1742
1743 for (a, b) in params.iter().zip(original.iter()) {
1744 assert_abs_diff_eq!(*a, *b, epsilon = 1e-12);
1745 }
1746 }
1747
1748 #[test]
1749 fn test_orthogonal_constraint_1d_errors() {
1750 use scirs2_core::ndarray::Array1;
1751 let mut params = Array1::from_vec(vec![1.0, 2.0, 3.0]);
1752 let constraint = ParameterConstraint::Orthogonal { tolerance: 1e-6 };
1753 let result = constraint.apply(&mut params);
1754 assert!(result.is_err());
1755 assert!(result.unwrap_err().to_string().contains("2D arrays"));
1756 }
1757
1758 #[test]
1759 fn test_spectral_norm_constraint_matrix() {
1760 use scirs2_core::ndarray::arr2;
1761
1762 let mut params = arr2(&[[5.0, 0.0], [0.0, 1.0]]);
1764 let maxnorm = 2.0;
1765 let constraint = ParameterConstraint::SpectralNorm { maxnorm };
1766 constraint.apply(&mut params).expect("constraint failed");
1767
1768 let sigma = power_iteration_spectral_norm(¶ms);
1770 assert!(
1771 sigma <= maxnorm + 1e-6,
1772 "spectral norm {sigma} exceeds cap {maxnorm}"
1773 );
1774 assert!(
1776 sigma > maxnorm - 1e-3,
1777 "spectral norm {sigma} undershot cap"
1778 );
1779 }
1780
1781 #[test]
1782 fn test_spectral_norm_constraint_nondiagonal() {
1783 use scirs2_core::ndarray::arr2;
1784
1785 let mut params = arr2(&[[3.0, 1.0], [1.0, 3.0], [2.0, -2.0]]);
1787 let maxnorm = 1.5;
1788 let constraint = ParameterConstraint::SpectralNorm { maxnorm };
1789 constraint.apply(&mut params).expect("constraint failed");
1790
1791 let sigma = power_iteration_spectral_norm(¶ms);
1792 assert!(
1793 sigma <= maxnorm + 1e-5,
1794 "spectral norm {sigma} exceeds cap {maxnorm}"
1795 );
1796 }
1797
1798 #[test]
1799 fn test_spectral_norm_constraint_under_cap_unchanged() {
1800 use approx::assert_abs_diff_eq;
1801 use scirs2_core::ndarray::arr2;
1802
1803 let mut params = arr2(&[[1.0, 0.0], [0.0, 1.0]]);
1805 let original = params.clone();
1806 let constraint = ParameterConstraint::SpectralNorm { maxnorm: 10.0 };
1807 constraint.apply(&mut params).expect("constraint failed");
1808
1809 for (a, b) in params.iter().zip(original.iter()) {
1810 assert_abs_diff_eq!(*a, *b, epsilon = 1e-12);
1811 }
1812 }
1813
1814 #[test]
1815 fn test_positive_definite_constraint_indefinite() {
1816 use scirs2_core::ndarray::arr2;
1817
1818 let mut params = arr2(&[[1.0, 2.0], [2.0, 1.0]]);
1820 let min_eig = 0.0;
1821 let constraint = ParameterConstraint::PositiveDefinite {
1822 mineigenvalue: min_eig,
1823 };
1824 constraint.apply(&mut params).expect("constraint failed");
1825
1826 let (eigvals, _) = jacobi_eigen_symmetric(¶ms);
1828 for &lambda in eigvals.iter() {
1829 assert!(
1830 lambda >= min_eig - 1e-8,
1831 "eigenvalue {lambda} below floor {min_eig}"
1832 );
1833 }
1834
1835 let probes = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, -1.0], [2.0, -3.0]];
1837 for p in probes.iter() {
1838 let mut quad = 0.0;
1839 for i in 0..2 {
1840 for j in 0..2 {
1841 quad += p[i] * params[[i, j]] * p[j];
1842 }
1843 }
1844 assert!(quad >= -1e-8, "xᵀMx = {quad} is negative");
1845 }
1846 }
1847
1848 #[test]
1849 fn test_positive_definite_constraint_positive_floor() {
1850 use scirs2_core::ndarray::arr2;
1851
1852 let mut params = arr2(&[[0.0, 1.0], [1.0, 0.0]]); let min_eig = 0.5;
1855 let constraint = ParameterConstraint::PositiveDefinite {
1856 mineigenvalue: min_eig,
1857 };
1858 constraint.apply(&mut params).expect("constraint failed");
1859
1860 let (eigvals, _) = jacobi_eigen_symmetric(¶ms);
1861 for &lambda in eigvals.iter() {
1862 assert!(
1863 lambda >= min_eig - 1e-8,
1864 "eigenvalue {lambda} below floor {min_eig}"
1865 );
1866 }
1867 }
1868
1869 #[test]
1870 fn test_positive_definite_constraint_already_pd_unchanged() {
1871 use approx::assert_abs_diff_eq;
1872 use scirs2_core::ndarray::arr2;
1873
1874 let mut params = arr2(&[[2.0, 1.0], [1.0, 2.0]]);
1876 let original = params.clone();
1877 let constraint = ParameterConstraint::PositiveDefinite { mineigenvalue: 0.0 };
1878 constraint.apply(&mut params).expect("constraint failed");
1879
1880 for (a, b) in params.iter().zip(original.iter()) {
1881 assert_abs_diff_eq!(*a, *b, epsilon = 1e-8);
1882 }
1883 }
1884
1885 #[test]
1886 fn test_positive_definite_constraint_non_square_errors() {
1887 use scirs2_core::ndarray::arr2;
1888 let mut params = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
1889 let constraint = ParameterConstraint::PositiveDefinite { mineigenvalue: 0.0 };
1890 let result = constraint.apply(&mut params);
1891 assert!(result.is_err());
1892 assert!(result.unwrap_err().to_string().contains("square"));
1893 }
1894}