1#[derive(Clone, Debug, PartialEq)]
11pub struct OptimizationStep {
12 pub step: u64,
14 pub loss: f64,
16 pub gradient_norm: f64,
18 pub learning_rate: f64,
20 pub timestamp_tick: u64,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum ConvergenceStatus {
31 NotConverged,
33 PossiblyConverged,
36 Converged,
39}
40
41#[derive(Clone, Debug, PartialEq)]
47pub struct OptimizationHistoryConfig {
48 pub max_steps: usize,
51 pub convergence_patience: usize,
54 pub convergence_threshold: f64,
56}
57
58impl Default for OptimizationHistoryConfig {
59 fn default() -> Self {
60 Self {
61 max_steps: 1000,
62 convergence_patience: 10,
63 convergence_threshold: 1e-6,
64 }
65 }
66}
67
68#[derive(Clone, Debug, PartialEq)]
74pub struct HistoryStats {
75 pub total_steps: usize,
77 pub best_loss: f64,
79 pub best_step: u64,
81 pub current_loss: f64,
83 pub avg_gradient_norm: f64,
85 pub convergence_status: ConvergenceStatus,
87}
88
89pub struct TensorOptimizationHistory {
108 pub steps: Vec<OptimizationStep>,
110 pub config: OptimizationHistoryConfig,
112 pub best_loss: f64,
114 pub best_step: u64,
116 pub consecutive_no_progress: usize,
119}
120
121impl TensorOptimizationHistory {
122 pub fn new(config: OptimizationHistoryConfig) -> Self {
124 Self {
125 steps: Vec::new(),
126 config,
127 best_loss: f64::MAX,
128 best_step: 0,
129 consecutive_no_progress: 0,
130 }
131 }
132
133 pub fn record(&mut self, step: OptimizationStep) {
138 if self.steps.len() >= self.config.max_steps {
140 self.steps.remove(0);
141 }
142
143 let new_loss = step.loss;
144 let prev_best = self.best_loss;
145
146 if prev_best == f64::MAX {
148 self.best_loss = new_loss;
149 self.best_step = step.step;
150 self.consecutive_no_progress = 0;
151 self.steps.push(step);
152 return;
153 }
154
155 let improvement = prev_best - new_loss;
157
158 if new_loss < self.best_loss {
159 self.best_loss = new_loss;
160 self.best_step = step.step;
161 }
162
163 if improvement < self.config.convergence_threshold {
164 self.consecutive_no_progress += 1;
165 } else {
166 self.consecutive_no_progress = 0;
167 }
168
169 self.steps.push(step);
170 }
171
172 pub fn convergence_status(&self) -> ConvergenceStatus {
175 if self.steps.is_empty() {
176 return ConvergenceStatus::NotConverged;
177 }
178 let patience = self.config.convergence_patience;
179 if self.consecutive_no_progress >= patience {
180 ConvergenceStatus::Converged
181 } else if self.consecutive_no_progress >= patience / 2 {
182 ConvergenceStatus::PossiblyConverged
183 } else {
184 ConvergenceStatus::NotConverged
185 }
186 }
187
188 pub fn recent_improvement(&self, n: usize) -> f64 {
194 if self.steps.len() < 2 {
195 return 0.0;
196 }
197 let window_n = n.min(self.steps.len());
198 let window_start = self.steps.len() - window_n;
199 let first_loss = self.steps[window_start].loss;
200 let last_loss = self.steps[self.steps.len() - 1].loss;
201 first_loss - last_loss
202 }
203
204 pub fn avg_gradient_norm(&self) -> f64 {
208 if self.steps.is_empty() {
209 return 0.0;
210 }
211 let sum: f64 = self.steps.iter().map(|s| s.gradient_norm).sum();
212 sum / self.steps.len() as f64
213 }
214
215 pub fn stats(&self) -> HistoryStats {
218 let current_loss = self.steps.last().map(|s| s.loss).unwrap_or(f64::MAX);
219
220 HistoryStats {
221 total_steps: self.steps.len(),
222 best_loss: self.best_loss,
223 best_step: self.best_step,
224 current_loss,
225 avg_gradient_norm: self.avg_gradient_norm(),
226 convergence_status: self.convergence_status(),
227 }
228 }
229
230 pub fn last_step(&self) -> Option<&OptimizationStep> {
233 self.steps.last()
234 }
235
236 pub fn reset(&mut self) {
238 self.steps.clear();
239 self.best_loss = f64::MAX;
240 self.best_step = 0;
241 self.consecutive_no_progress = 0;
242 }
243}
244
245#[cfg(test)]
250mod tests {
251 use super::*;
252
253 fn default_history() -> TensorOptimizationHistory {
254 TensorOptimizationHistory::new(OptimizationHistoryConfig::default())
255 }
256
257 fn make_step(step: u64, loss: f64) -> OptimizationStep {
258 OptimizationStep {
259 step,
260 loss,
261 gradient_norm: 0.5,
262 learning_rate: 0.01,
263 timestamp_tick: step,
264 }
265 }
266
267 fn make_step_full(step: u64, loss: f64, gradient_norm: f64) -> OptimizationStep {
268 OptimizationStep {
269 step,
270 loss,
271 gradient_norm,
272 learning_rate: 0.01,
273 timestamp_tick: step,
274 }
275 }
276
277 #[test]
282 fn test_record_adds_step() {
283 let mut h = default_history();
284 h.record(make_step(0, 1.0));
285 assert_eq!(h.steps.len(), 1);
286 }
287
288 #[test]
289 fn test_record_multiple_steps() {
290 let mut h = default_history();
291 for i in 0..5u64 {
292 h.record(make_step(i, 1.0 - i as f64 * 0.1));
293 }
294 assert_eq!(h.steps.len(), 5);
295 }
296
297 #[test]
302 fn test_max_steps_eviction() {
303 let config = OptimizationHistoryConfig {
304 max_steps: 3,
305 convergence_patience: 10,
306 convergence_threshold: 1e-6,
307 };
308 let mut h = TensorOptimizationHistory::new(config);
309 for i in 0..5u64 {
310 h.record(make_step(i, 1.0 - i as f64 * 0.01));
311 }
312 assert_eq!(h.steps.len(), 3);
313 assert_eq!(h.steps[0].step, 2);
315 }
316
317 #[test]
318 fn test_max_steps_boundary() {
319 let config = OptimizationHistoryConfig {
320 max_steps: 1,
321 convergence_patience: 5,
322 convergence_threshold: 1e-6,
323 };
324 let mut h = TensorOptimizationHistory::new(config);
325 h.record(make_step(0, 2.0));
326 h.record(make_step(1, 1.0));
327 assert_eq!(h.steps.len(), 1);
328 assert_eq!(h.steps[0].step, 1);
329 }
330
331 #[test]
336 fn test_best_loss_tracks_minimum() {
337 let mut h = default_history();
338 h.record(make_step(0, 3.0));
339 h.record(make_step(1, 1.0));
340 h.record(make_step(2, 2.0));
341 assert!((h.best_loss - 1.0).abs() < 1e-12);
342 assert_eq!(h.best_step, 1);
343 }
344
345 #[test]
346 fn test_best_loss_initialized_correctly() {
347 let mut h = default_history();
348 h.record(make_step(5, 42.0));
349 assert!((h.best_loss - 42.0).abs() < 1e-12);
350 assert_eq!(h.best_step, 5);
351 }
352
353 #[test]
354 fn test_best_step_updates_when_loss_decreases() {
355 let mut h = default_history();
356 h.record(make_step(0, 10.0));
357 h.record(make_step(1, 5.0));
358 h.record(make_step(2, 7.0));
359 h.record(make_step(3, 2.0));
360 assert!((h.best_loss - 2.0).abs() < 1e-12);
361 assert_eq!(h.best_step, 3);
362 }
363
364 #[test]
369 fn test_convergence_status_empty() {
370 let h = default_history();
371 assert_eq!(h.convergence_status(), ConvergenceStatus::NotConverged);
372 }
373
374 #[test]
375 fn test_convergence_status_not_converged() {
376 let mut h = default_history();
377 for i in 0..5u64 {
379 h.record(make_step(i, 100.0 - i as f64 * 10.0));
380 }
381 assert_eq!(h.convergence_status(), ConvergenceStatus::NotConverged);
382 }
383
384 #[test]
385 fn test_convergence_status_converged() {
386 let config = OptimizationHistoryConfig {
387 max_steps: 1000,
388 convergence_patience: 5,
389 convergence_threshold: 1e-3,
390 };
391 let mut h = TensorOptimizationHistory::new(config);
392 h.record(make_step(0, 1.0));
394 for i in 1..=5u64 {
396 h.record(make_step(i, 1.0 - i as f64 * 1e-9));
397 }
398 assert_eq!(h.convergence_status(), ConvergenceStatus::Converged);
399 }
400
401 #[test]
402 fn test_convergence_status_possibly_converged() {
403 let config = OptimizationHistoryConfig {
404 max_steps: 1000,
405 convergence_patience: 8,
406 convergence_threshold: 1e-3,
407 };
408 let mut h = TensorOptimizationHistory::new(config);
409 h.record(make_step(0, 1.0));
411 for i in 1..=4u64 {
413 h.record(make_step(i, 1.0 - i as f64 * 1e-9));
414 }
415 assert_eq!(h.convergence_status(), ConvergenceStatus::PossiblyConverged);
416 }
417
418 #[test]
423 fn test_possibly_converged_boundary_exact() {
424 let config = OptimizationHistoryConfig {
425 max_steps: 1000,
426 convergence_patience: 10,
427 convergence_threshold: 1e-3,
428 };
429 let mut h = TensorOptimizationHistory::new(config);
430 h.record(make_step(0, 1.0));
431 for i in 1..=5u64 {
433 h.record(make_step(i, 1.0 - i as f64 * 1e-9));
434 }
435 assert_eq!(h.convergence_status(), ConvergenceStatus::PossiblyConverged);
436 }
437
438 #[test]
439 fn test_not_converged_below_patience_half() {
440 let config = OptimizationHistoryConfig {
441 max_steps: 1000,
442 convergence_patience: 10,
443 convergence_threshold: 1e-3,
444 };
445 let mut h = TensorOptimizationHistory::new(config);
446 h.record(make_step(0, 1.0));
447 for i in 1..=4u64 {
449 h.record(make_step(i, 1.0 - i as f64 * 1e-9));
450 }
451 assert_eq!(h.convergence_status(), ConvergenceStatus::NotConverged);
452 }
453
454 #[test]
455 fn test_converged_at_full_patience() {
456 let config = OptimizationHistoryConfig {
457 max_steps: 1000,
458 convergence_patience: 6,
459 convergence_threshold: 1e-3,
460 };
461 let mut h = TensorOptimizationHistory::new(config);
462 h.record(make_step(0, 1.0));
463 for i in 1..=6u64 {
464 h.record(make_step(i, 1.0 - i as f64 * 1e-9));
465 }
466 assert_eq!(h.convergence_status(), ConvergenceStatus::Converged);
467 }
468
469 #[test]
474 fn test_no_progress_counter_resets() {
475 let config = OptimizationHistoryConfig {
476 max_steps: 1000,
477 convergence_patience: 3,
478 convergence_threshold: 1e-3,
479 };
480 let mut h = TensorOptimizationHistory::new(config);
481 h.record(make_step(0, 1.0));
482 h.record(make_step(1, 1.0 - 1e-9));
484 h.record(make_step(2, 1.0 - 2e-9));
485 h.record(make_step(3, 0.0));
487 assert_eq!(h.convergence_status(), ConvergenceStatus::NotConverged);
489 }
490
491 #[test]
496 fn test_recent_improvement_empty() {
497 let h = default_history();
498 assert!((h.recent_improvement(5) - 0.0).abs() < 1e-12);
499 }
500
501 #[test]
502 fn test_recent_improvement_one_step() {
503 let mut h = default_history();
504 h.record(make_step(0, 1.0));
505 assert!((h.recent_improvement(5) - 0.0).abs() < 1e-12);
506 }
507
508 #[test]
509 fn test_recent_improvement_full_window() {
510 let mut h = default_history();
511 for i in 0..5u64 {
512 h.record(make_step(i, 5.0 - i as f64));
513 }
514 assert!((h.recent_improvement(5) - 4.0).abs() < 1e-10);
516 }
517
518 #[test]
519 fn test_recent_improvement_partial_window() {
520 let mut h = default_history();
521 for i in 0..10u64 {
522 h.record(make_step(i, 10.0 - i as f64));
523 }
524 assert!((h.recent_improvement(3) - 2.0).abs() < 1e-10);
527 }
528
529 #[test]
530 fn test_recent_improvement_n_larger_than_history() {
531 let mut h = default_history();
532 h.record(make_step(0, 10.0));
533 h.record(make_step(1, 6.0));
534 assert!((h.recent_improvement(100) - 4.0).abs() < 1e-10);
536 }
537
538 #[test]
539 fn test_recent_improvement_negative_when_loss_increases() {
540 let mut h = default_history();
541 h.record(make_step(0, 1.0));
542 h.record(make_step(1, 2.0));
543 assert!((h.recent_improvement(2) - (-1.0)).abs() < 1e-10);
545 }
546
547 #[test]
552 fn test_avg_gradient_norm_empty() {
553 let h = default_history();
554 assert!((h.avg_gradient_norm() - 0.0).abs() < 1e-12);
555 }
556
557 #[test]
558 fn test_avg_gradient_norm_single() {
559 let mut h = default_history();
560 h.record(make_step_full(0, 1.0, 0.4));
561 assert!((h.avg_gradient_norm() - 0.4).abs() < 1e-12);
562 }
563
564 #[test]
565 fn test_avg_gradient_norm_multiple() {
566 let mut h = default_history();
567 h.record(make_step_full(0, 1.0, 1.0));
568 h.record(make_step_full(1, 0.9, 2.0));
569 h.record(make_step_full(2, 0.8, 3.0));
570 assert!((h.avg_gradient_norm() - 2.0).abs() < 1e-12);
572 }
573
574 #[test]
579 fn test_reset_clears_steps() {
580 let mut h = default_history();
581 for i in 0..5u64 {
582 h.record(make_step(i, 1.0));
583 }
584 h.reset();
585 assert!(h.steps.is_empty());
586 }
587
588 #[test]
589 fn test_reset_restores_best_loss() {
590 let mut h = default_history();
591 h.record(make_step(0, 0.5));
592 h.reset();
593 assert_eq!(h.best_loss, f64::MAX);
594 }
595
596 #[test]
597 fn test_reset_clears_best_step() {
598 let mut h = default_history();
599 h.record(make_step(7, 0.1));
600 h.reset();
601 assert_eq!(h.best_step, 0);
602 }
603
604 #[test]
605 fn test_reset_clears_consecutive_no_progress() {
606 let config = OptimizationHistoryConfig {
607 max_steps: 1000,
608 convergence_patience: 3,
609 convergence_threshold: 1e-3,
610 };
611 let mut h = TensorOptimizationHistory::new(config);
612 h.record(make_step(0, 1.0));
613 h.record(make_step(1, 1.0 - 1e-9));
614 h.record(make_step(2, 1.0 - 2e-9));
615 h.reset();
616 assert_eq!(h.consecutive_no_progress, 0);
617 }
618
619 #[test]
620 fn test_reset_allows_fresh_recording() {
621 let mut h = default_history();
622 h.record(make_step(0, 5.0));
623 h.reset();
624 h.record(make_step(0, 3.0));
625 assert_eq!(h.steps.len(), 1);
626 assert!((h.best_loss - 3.0).abs() < 1e-12);
627 }
628
629 #[test]
634 fn test_stats_empty() {
635 let h = default_history();
636 let s = h.stats();
637 assert_eq!(s.total_steps, 0);
638 assert_eq!(s.best_loss, f64::MAX);
639 assert_eq!(s.best_step, 0);
640 assert_eq!(s.current_loss, f64::MAX);
641 assert!((s.avg_gradient_norm - 0.0).abs() < 1e-12);
642 assert_eq!(s.convergence_status, ConvergenceStatus::NotConverged);
643 }
644
645 #[test]
646 fn test_stats_correct_values() {
647 let mut h = default_history();
648 h.record(make_step_full(0, 2.0, 1.0));
649 h.record(make_step_full(1, 1.0, 3.0));
650 let s = h.stats();
651 assert_eq!(s.total_steps, 2);
652 assert!((s.best_loss - 1.0).abs() < 1e-12);
653 assert_eq!(s.best_step, 1);
654 assert!((s.current_loss - 1.0).abs() < 1e-12);
655 assert!((s.avg_gradient_norm - 2.0).abs() < 1e-12);
656 }
657
658 #[test]
663 fn test_last_step_empty() {
664 let h = default_history();
665 assert!(h.last_step().is_none());
666 }
667
668 #[test]
669 fn test_last_step_returns_latest() {
670 let mut h = default_history();
671 h.record(make_step(0, 2.0));
672 h.record(make_step(1, 1.0));
673 let last = h.last_step().expect("should have last step");
674 assert_eq!(last.step, 1);
675 assert!((last.loss - 1.0).abs() < 1e-12);
676 }
677
678 #[test]
683 fn test_first_record_sets_best() {
684 let mut h = default_history();
685 h.record(make_step(42, 7.5));
686 assert!((h.best_loss - 7.5).abs() < 1e-12);
687 assert_eq!(h.best_step, 42);
688 assert_eq!(h.consecutive_no_progress, 0);
689 }
690
691 #[test]
696 fn test_convergence_status_copy() {
697 let s = ConvergenceStatus::Converged;
698 let t = s; assert_eq!(s, t);
700 }
701
702 #[test]
703 fn test_convergence_status_debug() {
704 let s = format!("{:?}", ConvergenceStatus::PossiblyConverged);
705 assert_eq!(s, "PossiblyConverged");
706 }
707}