1use crate::error::{OptimError, Result};
8use crate::schedulers::LearningRateScheduler;
9use scirs2_core::ndarray::ScalarOperand;
10use scirs2_core::numeric::Float;
11use std::fmt::{self, Debug};
12
13fn from_f64<A: Float>(v: f64) -> A {
15 A::from(v).unwrap_or_else(A::zero)
16}
17
18fn from_usize<A: Float>(v: usize) -> A {
20 A::from(v).unwrap_or_else(A::zero)
21}
22
23fn denom_from_usize<A: Float>(v: usize) -> A {
27 match A::from(v) {
28 Some(x) if x != A::zero() => x,
29 _ => A::one(),
30 }
31}
32
33pub struct OneCycle<A: Float> {
65 initial_lr: A,
66 max_lr: A,
67 final_lr: Option<A>,
68 total_steps: usize,
70 warmup_steps: usize,
72 current_step: usize,
73 max_momentum: Option<A>,
74 min_momentum: Option<A>,
75 base_momentum: Option<A>,
76 anneal_strategy: AnnealStrategy,
77 final_div_factor: A,
78}
79
80#[derive(Debug, Clone, Copy)]
82pub enum AnnealStrategy {
83 Linear,
85 Cosine,
87}
88
89impl<A: Float + ScalarOperand + std::fmt::Debug + Send + Sync> OneCycle<A> {
90 pub fn new(initial_lr: A, max_lr: A, total_steps: usize, warmup_frac: f64) -> Self {
102 let total_steps = total_steps.max(1);
103 let frac = if warmup_frac.is_finite() {
104 warmup_frac.clamp(0.0, 1.0)
105 } else {
106 0.0
107 };
108 let warmup_steps = ((total_steps as f64) * frac) as usize;
110 let warmup_steps = warmup_steps.min(total_steps.saturating_sub(1));
112
113 let final_div_factor = from_f64::<A>(10000.0); Self {
116 initial_lr,
117 max_lr,
118 final_lr: None,
119 total_steps,
120 warmup_steps,
121 current_step: 0,
122 max_momentum: None,
123 min_momentum: None,
124 base_momentum: None,
125 anneal_strategy: AnnealStrategy::Cosine,
126 final_div_factor,
127 }
128 }
129
130 pub fn try_new(initial_lr: A, max_lr: A, total_steps: usize, warmup_frac: f64) -> Result<Self> {
140 if total_steps == 0 {
141 return Err(OptimError::InvalidConfig(
142 "OneCycle requires total_steps > 0".to_string(),
143 ));
144 }
145 if !warmup_frac.is_finite() || warmup_frac <= 0.0 || warmup_frac >= 1.0 {
146 return Err(OptimError::InvalidConfig(format!(
147 "OneCycle requires pct_start (warmup_frac) in the open interval (0, 1), got {warmup_frac}"
148 )));
149 }
150 if !initial_lr.is_finite() || !max_lr.is_finite() {
151 return Err(OptimError::InvalidConfig(
152 "OneCycle requires finite initial_lr and max_lr".to_string(),
153 ));
154 }
155 if initial_lr <= A::zero() {
156 return Err(OptimError::InvalidConfig(
157 "OneCycle requires initial_lr > 0".to_string(),
158 ));
159 }
160 if max_lr < initial_lr {
161 return Err(OptimError::InvalidConfig(
162 "OneCycle requires max_lr >= initial_lr".to_string(),
163 ));
164 }
165
166 Ok(Self::new(initial_lr, max_lr, total_steps, warmup_frac))
167 }
168
169 pub fn with_final_lr(mut self, final_lr: A) -> Self {
171 self.final_lr = Some(final_lr);
172 self.final_div_factor = if final_lr == A::zero() {
173 from_f64::<A>(10000.0)
174 } else {
175 self.initial_lr / final_lr
176 };
177 self
178 }
179
180 pub fn with_momentum(mut self, min_momentum: A, max_momentum: A, base_momentum: A) -> Self {
182 self.min_momentum = Some(min_momentum);
183 self.max_momentum = Some(max_momentum);
184 self.base_momentum = Some(base_momentum);
185 self
186 }
187
188 pub fn with_anneal_strategy(mut self, strategy: AnnealStrategy) -> Self {
190 self.anneal_strategy = strategy;
191 self
192 }
193
194 pub fn total_steps(&self) -> usize {
196 self.total_steps
197 }
198
199 pub fn warmup_steps(&self) -> usize {
201 self.warmup_steps
202 }
203
204 fn warmup_progress(&self) -> Option<A> {
209 let step = self.current_step.min(self.total_steps);
210 if self.warmup_steps == 0 || step >= self.warmup_steps {
211 return None;
212 }
213 Some(from_usize::<A>(step) / denom_from_usize::<A>(self.warmup_steps))
214 }
215
216 fn cooldown_progress(&self) -> A {
218 let step = self.current_step.min(self.total_steps);
219 let remaining_steps = self.total_steps.saturating_sub(self.warmup_steps);
221 let cooled = step.saturating_sub(self.warmup_steps).min(remaining_steps);
222 from_usize::<A>(cooled) / denom_from_usize::<A>(remaining_steps)
223 }
224
225 pub fn get_momentum(&self) -> Option<A> {
227 match (self.min_momentum, self.max_momentum) {
228 (Some(min_mom), Some(max_mom)) => match self.warmup_progress() {
229 Some(progress) => {
230 Some(max_mom - (max_mom - min_mom) * progress)
232 }
233 None => {
234 let cool_progress = self.cooldown_progress();
236 match self.anneal_strategy {
237 AnnealStrategy::Linear => {
238 Some(min_mom + (max_mom - min_mom) * cool_progress)
239 }
240 AnnealStrategy::Cosine => {
241 let cos_out = ((cool_progress * from_f64::<A>(std::f64::consts::PI))
242 .cos()
243 + A::one())
244 / from_f64::<A>(2.0);
245 Some(min_mom + (max_mom - min_mom) * (A::one() - cos_out))
246 }
247 }
248 }
249 },
250 _ => self.base_momentum,
251 }
252 }
253
254 pub fn get_percentage_complete(&self) -> A {
256 let step = self.current_step.min(self.total_steps);
257 from_usize::<A>(step) / denom_from_usize::<A>(self.total_steps)
258 }
259}
260
261impl<A: Float + ScalarOperand + Debug + Send + Sync> LearningRateScheduler<A> for OneCycle<A> {
262 fn get_learning_rate(&self) -> A {
263 match self.warmup_progress() {
264 Some(progress) => {
265 self.initial_lr + (self.max_lr - self.initial_lr) * progress
267 }
268 None => {
269 let cool_progress = self.cooldown_progress();
271 let final_lr = match self.final_lr {
272 Some(lr) => lr,
273 None => self.initial_lr / self.final_div_factor,
274 };
275
276 match self.anneal_strategy {
277 AnnealStrategy::Linear => {
278 self.max_lr - (self.max_lr - final_lr) * cool_progress
279 }
280 AnnealStrategy::Cosine => {
281 let cos_out = ((cool_progress * from_f64::<A>(std::f64::consts::PI)).cos()
282 + A::one())
283 / from_f64::<A>(2.0);
284 final_lr + (self.max_lr - final_lr) * cos_out
285 }
286 }
287 }
288 }
289 }
290
291 fn step(&mut self) -> A {
292 self.current_step = self.current_step.saturating_add(1).min(self.total_steps);
294 self.get_learning_rate()
295 }
296
297 fn reset(&mut self) {
298 self.current_step = 0;
299 }
300}
301
302impl<A: Float + Debug + Send + Sync> fmt::Debug for OneCycle<A> {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 f.debug_struct("OneCycle")
305 .field("initial_lr", &self.initial_lr)
306 .field("max_lr", &self.max_lr)
307 .field("final_lr", &self.final_lr)
308 .field("total_steps", &self.total_steps)
309 .field("warmup_steps", &self.warmup_steps)
310 .field("current_step", &self.current_step)
311 .field("anneal_strategy", &self.anneal_strategy)
312 .finish()
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use approx::assert_relative_eq;
320
321 #[test]
322 fn test_one_cycle_basic() {
323 let mut scheduler = OneCycle::new(0.0001, 0.001, 100, 0.25);
324
325 assert_relative_eq!(scheduler.get_learning_rate(), 0.0001, epsilon = 1e-6);
327
328 for _ in 0..25 {
330 scheduler.step();
331 }
332 assert_relative_eq!(scheduler.get_learning_rate(), 0.001, epsilon = 1e-6);
333
334 for _ in 25..100 {
336 scheduler.step();
337 }
338 assert!(scheduler.get_learning_rate() < 0.0001);
339 }
340
341 #[test]
342 fn test_one_cycle_momentum() {
343 let mut scheduler = OneCycle::new(0.0001, 0.001, 100, 0.25).with_momentum(0.85, 0.95, 0.9);
344
345 assert_relative_eq!(
347 scheduler.get_momentum().unwrap_or(f64::NAN),
348 0.95,
349 epsilon = 1e-6
350 );
351
352 for _ in 0..25 {
354 scheduler.step();
355 }
356 assert_relative_eq!(
357 scheduler.get_momentum().unwrap_or(f64::NAN),
358 0.85,
359 epsilon = 1e-6
360 );
361
362 for _ in 25..100 {
364 scheduler.step();
365 }
366 let final_momentum = scheduler.get_momentum().unwrap_or(f64::NAN);
367 assert!(final_momentum > 0.94); }
369
370 #[test]
371 fn test_one_cycle_linear_anneal() {
372 let mut scheduler = OneCycle::new(0.0001, 0.001, 100, 0.25)
373 .with_anneal_strategy(AnnealStrategy::Linear)
374 .with_final_lr(0.00001);
375
376 for _ in 0..25 {
378 scheduler.step();
379 }
380
381 let lr_at_warmup = scheduler.get_learning_rate();
382 assert_relative_eq!(lr_at_warmup, 0.001, epsilon = 1e-6);
383
384 for _ in 0..37 {
386 scheduler.step();
388 }
389
390 let lr_halfway = scheduler.get_learning_rate();
391 assert!(lr_halfway < 0.001);
392 assert!(lr_halfway > 0.00001);
393
394 let expected = 0.001 - (0.001 - 0.00001) * 0.5;
396 assert_relative_eq!(lr_halfway, expected, epsilon = 1e-4);
397 }
398
399 #[test]
400 fn test_percentage_complete() {
401 let mut scheduler = OneCycle::new(0.0001, 0.001, 100, 0.25);
402
403 assert_relative_eq!(scheduler.get_percentage_complete(), 0.0, epsilon = 1e-6);
404
405 for _ in 0..50 {
406 scheduler.step();
407 }
408 assert_relative_eq!(scheduler.get_percentage_complete(), 0.5, epsilon = 1e-6);
409
410 for _ in 50..100 {
411 scheduler.step();
412 }
413 assert_relative_eq!(scheduler.get_percentage_complete(), 1.0, epsilon = 1e-6);
414
415 for _ in 0..50 {
417 scheduler.step();
418 }
419 assert_relative_eq!(scheduler.get_percentage_complete(), 1.0, epsilon = 1e-6);
420 }
421
422 #[test]
423 fn test_reset() {
424 let mut scheduler = OneCycle::new(0.0001, 0.001, 100, 0.25);
425
426 for _ in 0..50 {
428 scheduler.step();
429 }
430
431 let lr_mid = scheduler.get_learning_rate();
432 assert!(lr_mid != 0.0001);
433
434 scheduler.reset();
436 assert_eq!(scheduler.current_step, 0);
437 assert_relative_eq!(scheduler.get_learning_rate(), 0.0001, epsilon = 1e-6);
438 }
439
440 #[test]
441 fn test_degenerate_configs_are_clamped() {
442 let mut zero = OneCycle::new(0.0001, 0.001, 0, 0.25);
444 assert_eq!(zero.total_steps(), 1);
445 for _ in 0..5 {
446 assert!(zero.step().is_finite());
447 }
448
449 let mut over = OneCycle::new(0.0001, 0.001, 100, 1.5);
451 assert!(over.warmup_steps() < over.total_steps());
452 for _ in 0..200 {
453 let lr = over.step();
454 assert!(lr.is_finite() && lr >= 0.0);
455 }
456
457 let nan = OneCycle::new(0.0001, 0.001, 100, f64::NAN);
459 assert_eq!(nan.warmup_steps(), 0);
460 }
461
462 #[test]
463 fn test_try_new_validates() {
464 assert!(OneCycle::try_new(0.0001f64, 0.001, 0, 0.25).is_err());
465 assert!(OneCycle::try_new(0.0001f64, 0.001, 100, 0.0).is_err());
466 assert!(OneCycle::try_new(0.0001f64, 0.001, 100, 1.0).is_err());
467 assert!(OneCycle::try_new(0.0001f64, 0.001, 100, f64::NAN).is_err());
468 assert!(OneCycle::try_new(0.0f64, 0.001, 100, 0.25).is_err());
469 assert!(OneCycle::try_new(0.001f64, 0.0001, 100, 0.25).is_err());
470 assert!(OneCycle::try_new(0.0001f64, 0.001, 100, 0.25).is_ok());
471 }
472}