1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum StopCriterion {
11 MinLoss,
13 MaxAccuracy,
15 MinMetric(String),
17 MaxMetric(String),
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum StopDecision {
24 Continue,
26 Stop(String),
28}
29
30#[derive(Debug, Clone)]
32pub struct EarlyStoppingConfig {
33 pub criterion: StopCriterion,
35 pub patience: usize,
37 pub min_delta: f64,
39 pub min_epochs: usize,
41 pub restore_best: bool,
43}
44
45#[derive(Debug, Clone)]
47pub struct EpochMetrics {
48 pub epoch: usize,
50 pub loss: f64,
52 pub accuracy: Option<f64>,
54 pub custom_metrics: HashMap<String, f64>,
56}
57
58#[derive(Debug, Clone, Default)]
60pub struct EarlyStoppingStats {
61 pub total_epochs: u64,
63 pub improvements: u64,
65 pub best_value: f64,
67 pub best_epoch: usize,
69 pub stopped_at: Option<usize>,
71}
72
73pub struct EarlyStoppingMonitor {
75 config: EarlyStoppingConfig,
76 history: Vec<EpochMetrics>,
77 best_value: f64,
78 best_epoch: usize,
79 epochs_without_improvement: usize,
80 stopped: bool,
81 stats: EarlyStoppingStats,
82}
83
84impl EarlyStoppingMonitor {
85 pub fn new(config: EarlyStoppingConfig) -> Self {
87 let initial_best = match &config.criterion {
88 StopCriterion::MinLoss | StopCriterion::MinMetric(_) => f64::INFINITY,
89 StopCriterion::MaxAccuracy | StopCriterion::MaxMetric(_) => f64::NEG_INFINITY,
90 };
91 Self {
92 config,
93 history: Vec::new(),
94 best_value: initial_best,
95 best_epoch: 0,
96 epochs_without_improvement: 0,
97 stopped: false,
98 stats: EarlyStoppingStats {
99 best_value: initial_best,
100 ..Default::default()
101 },
102 }
103 }
104
105 pub fn check(&mut self, metrics: EpochMetrics) -> StopDecision {
109 if self.stopped {
110 return StopDecision::Stop("Already stopped".to_string());
111 }
112
113 let epoch = metrics.epoch;
114 let value = self.extract_metric(&metrics);
115 self.history.push(metrics);
116 self.stats.total_epochs += 1;
117
118 let value = match value {
119 Some(v) => v,
120 None => {
121 self.epochs_without_improvement += 1;
123 return self.maybe_stop(epoch);
124 }
125 };
126
127 if value.is_nan() {
129 self.epochs_without_improvement += 1;
130 return self.maybe_stop(epoch);
131 }
132
133 if self.is_improvement(value) {
134 self.best_value = value;
135 self.best_epoch = epoch;
136 self.epochs_without_improvement = 0;
137 self.stats.improvements += 1;
138 self.stats.best_value = value;
139 self.stats.best_epoch = epoch;
140 } else {
141 self.epochs_without_improvement += 1;
142 }
143
144 self.maybe_stop(epoch)
145 }
146
147 pub fn is_improvement(&self, value: f64) -> bool {
150 if value.is_nan() {
151 return false;
152 }
153 match &self.config.criterion {
154 StopCriterion::MinLoss | StopCriterion::MinMetric(_) => {
155 value < self.best_value - self.config.min_delta
156 }
157 StopCriterion::MaxAccuracy | StopCriterion::MaxMetric(_) => {
158 value > self.best_value + self.config.min_delta
159 }
160 }
161 }
162
163 pub fn best_value(&self) -> f64 {
165 self.best_value
166 }
167
168 pub fn best_epoch(&self) -> usize {
170 self.best_epoch
171 }
172
173 pub fn epochs_without_improvement(&self) -> usize {
175 self.epochs_without_improvement
176 }
177
178 pub fn should_stop(&self) -> bool {
180 self.stopped
181 }
182
183 pub fn reset(&mut self) {
185 let initial_best = match &self.config.criterion {
186 StopCriterion::MinLoss | StopCriterion::MinMetric(_) => f64::INFINITY,
187 StopCriterion::MaxAccuracy | StopCriterion::MaxMetric(_) => f64::NEG_INFINITY,
188 };
189 self.history.clear();
190 self.best_value = initial_best;
191 self.best_epoch = 0;
192 self.epochs_without_improvement = 0;
193 self.stopped = false;
194 self.stats = EarlyStoppingStats {
195 best_value: initial_best,
196 ..Default::default()
197 };
198 }
199
200 pub fn history(&self) -> &[EpochMetrics] {
202 &self.history
203 }
204
205 pub fn stats(&self) -> &EarlyStoppingStats {
207 &self.stats
208 }
209
210 pub fn remaining_patience(&self) -> usize {
212 self.config
213 .patience
214 .saturating_sub(self.epochs_without_improvement)
215 }
216
217 pub fn extract_metric(&self, metrics: &EpochMetrics) -> Option<f64> {
219 match &self.config.criterion {
220 StopCriterion::MinLoss => Some(metrics.loss),
221 StopCriterion::MaxAccuracy => metrics.accuracy,
222 StopCriterion::MinMetric(name) => metrics.custom_metrics.get(name).copied(),
223 StopCriterion::MaxMetric(name) => metrics.custom_metrics.get(name).copied(),
224 }
225 }
226
227 fn maybe_stop(&mut self, epoch: usize) -> StopDecision {
230 if self.stats.total_epochs < self.config.min_epochs as u64 {
232 return StopDecision::Continue;
233 }
234 if self.epochs_without_improvement >= self.config.patience {
235 self.stopped = true;
236 self.stats.stopped_at = Some(epoch);
237 let reason = format!(
238 "No improvement for {} epochs (best={:.6} at epoch {})",
239 self.config.patience, self.best_value, self.best_epoch
240 );
241 StopDecision::Stop(reason)
242 } else {
243 StopDecision::Continue
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn default_config() -> EarlyStoppingConfig {
253 EarlyStoppingConfig {
254 criterion: StopCriterion::MinLoss,
255 patience: 3,
256 min_delta: 0.0,
257 min_epochs: 0,
258 restore_best: false,
259 }
260 }
261
262 fn epoch(epoch: usize, loss: f64) -> EpochMetrics {
263 EpochMetrics {
264 epoch,
265 loss,
266 accuracy: None,
267 custom_metrics: HashMap::new(),
268 }
269 }
270
271 fn epoch_with_acc(epoch: usize, loss: f64, acc: f64) -> EpochMetrics {
272 EpochMetrics {
273 epoch,
274 loss,
275 accuracy: Some(acc),
276 custom_metrics: HashMap::new(),
277 }
278 }
279
280 fn epoch_with_custom(epoch: usize, loss: f64, key: &str, val: f64) -> EpochMetrics {
281 let mut m = HashMap::new();
282 m.insert(key.to_string(), val);
283 EpochMetrics {
284 epoch,
285 loss,
286 accuracy: None,
287 custom_metrics: m,
288 }
289 }
290
291 #[test]
294 fn min_loss_stops_after_patience_exhausted() {
295 let mut mon = EarlyStoppingMonitor::new(default_config());
296 assert_eq!(mon.check(epoch(0, 1.0)), StopDecision::Continue);
297 assert_eq!(mon.check(epoch(1, 1.1)), StopDecision::Continue);
298 assert_eq!(mon.check(epoch(2, 1.2)), StopDecision::Continue);
299 match mon.check(epoch(3, 1.3)) {
300 StopDecision::Stop(_) => {}
301 StopDecision::Continue => panic!("expected Stop"),
302 }
303 }
304
305 #[test]
306 fn min_loss_continues_on_improvement() {
307 let mut mon = EarlyStoppingMonitor::new(default_config());
308 assert_eq!(mon.check(epoch(0, 1.0)), StopDecision::Continue);
309 assert_eq!(mon.check(epoch(1, 0.9)), StopDecision::Continue);
310 assert_eq!(mon.check(epoch(2, 0.8)), StopDecision::Continue);
311 assert_eq!(mon.check(epoch(3, 0.7)), StopDecision::Continue);
312 assert_eq!(mon.check(epoch(4, 0.6)), StopDecision::Continue);
313 }
314
315 #[test]
316 fn min_loss_resets_patience_on_improvement() {
317 let mut mon = EarlyStoppingMonitor::new(default_config());
318 assert_eq!(mon.check(epoch(0, 1.0)), StopDecision::Continue);
319 assert_eq!(mon.check(epoch(1, 1.1)), StopDecision::Continue); assert_eq!(mon.check(epoch(2, 1.2)), StopDecision::Continue); assert_eq!(mon.check(epoch(3, 0.5)), StopDecision::Continue); assert_eq!(mon.epochs_without_improvement(), 0);
323 assert_eq!(mon.check(epoch(4, 0.6)), StopDecision::Continue); assert_eq!(mon.check(epoch(5, 0.7)), StopDecision::Continue); match mon.check(epoch(6, 0.8)) {
326 StopDecision::Stop(_) => {}
327 StopDecision::Continue => panic!("expected Stop"),
328 }
329 }
330
331 #[test]
334 fn max_accuracy_stops_after_patience() {
335 let config = EarlyStoppingConfig {
336 criterion: StopCriterion::MaxAccuracy,
337 patience: 2,
338 min_delta: 0.0,
339 min_epochs: 0,
340 restore_best: false,
341 };
342 let mut mon = EarlyStoppingMonitor::new(config);
343 assert_eq!(
344 mon.check(epoch_with_acc(0, 1.0, 0.8)),
345 StopDecision::Continue
346 );
347 assert_eq!(
348 mon.check(epoch_with_acc(1, 0.9, 0.79)),
349 StopDecision::Continue
350 );
351 match mon.check(epoch_with_acc(2, 0.8, 0.78)) {
352 StopDecision::Stop(_) => {}
353 StopDecision::Continue => panic!("expected Stop"),
354 }
355 }
356
357 #[test]
358 fn max_accuracy_continues_on_improvement() {
359 let config = EarlyStoppingConfig {
360 criterion: StopCriterion::MaxAccuracy,
361 patience: 2,
362 min_delta: 0.0,
363 min_epochs: 0,
364 restore_best: false,
365 };
366 let mut mon = EarlyStoppingMonitor::new(config);
367 assert_eq!(
368 mon.check(epoch_with_acc(0, 1.0, 0.5)),
369 StopDecision::Continue
370 );
371 assert_eq!(
372 mon.check(epoch_with_acc(1, 0.9, 0.6)),
373 StopDecision::Continue
374 );
375 assert_eq!(
376 mon.check(epoch_with_acc(2, 0.8, 0.7)),
377 StopDecision::Continue
378 );
379 assert!(!mon.should_stop());
380 }
381
382 #[test]
385 fn min_delta_prevents_tiny_improvements() {
386 let config = EarlyStoppingConfig {
387 criterion: StopCriterion::MinLoss,
388 patience: 2,
389 min_delta: 0.1,
390 min_epochs: 0,
391 restore_best: false,
392 };
393 let mut mon = EarlyStoppingMonitor::new(config);
394 assert_eq!(mon.check(epoch(0, 1.0)), StopDecision::Continue);
395 assert_eq!(mon.check(epoch(1, 0.95)), StopDecision::Continue);
397 assert_eq!(mon.epochs_without_improvement(), 1);
398 match mon.check(epoch(2, 0.96)) {
399 StopDecision::Stop(_) => {}
400 StopDecision::Continue => panic!("expected Stop"),
401 }
402 }
403
404 #[test]
405 fn min_delta_allows_large_improvements() {
406 let config = EarlyStoppingConfig {
407 criterion: StopCriterion::MinLoss,
408 patience: 2,
409 min_delta: 0.1,
410 min_epochs: 0,
411 restore_best: false,
412 };
413 let mut mon = EarlyStoppingMonitor::new(config);
414 assert_eq!(mon.check(epoch(0, 1.0)), StopDecision::Continue);
415 assert_eq!(mon.check(epoch(1, 0.8)), StopDecision::Continue); assert_eq!(mon.epochs_without_improvement(), 0);
417 }
418
419 #[test]
420 fn min_delta_for_max_accuracy() {
421 let config = EarlyStoppingConfig {
422 criterion: StopCriterion::MaxAccuracy,
423 patience: 2,
424 min_delta: 0.05,
425 min_epochs: 0,
426 restore_best: false,
427 };
428 let mut mon = EarlyStoppingMonitor::new(config);
429 assert_eq!(
430 mon.check(epoch_with_acc(0, 1.0, 0.7)),
431 StopDecision::Continue
432 );
433 assert_eq!(
435 mon.check(epoch_with_acc(1, 0.9, 0.72)),
436 StopDecision::Continue
437 );
438 assert_eq!(mon.epochs_without_improvement(), 1);
439 assert_eq!(
441 mon.check(epoch_with_acc(2, 0.8, 0.81)),
442 StopDecision::Continue
443 );
444 assert_eq!(mon.epochs_without_improvement(), 0);
445 }
446
447 #[test]
450 fn min_epochs_prevents_early_stop() {
451 let config = EarlyStoppingConfig {
452 criterion: StopCriterion::MinLoss,
453 patience: 1,
454 min_delta: 0.0,
455 min_epochs: 5,
456 restore_best: false,
457 };
458 let mut mon = EarlyStoppingMonitor::new(config);
459 for i in 0..4 {
461 assert_eq!(mon.check(epoch(i, 1.0 + i as f64)), StopDecision::Continue);
462 }
463 match mon.check(epoch(4, 5.0)) {
465 StopDecision::Stop(_) => {}
466 StopDecision::Continue => panic!("expected Stop after min_epochs"),
467 }
468 }
469
470 #[test]
471 fn min_epochs_allows_stop_at_boundary() {
472 let config = EarlyStoppingConfig {
473 criterion: StopCriterion::MinLoss,
474 patience: 2,
475 min_delta: 0.0,
476 min_epochs: 3,
477 restore_best: false,
478 };
479 let mut mon = EarlyStoppingMonitor::new(config);
480 assert_eq!(mon.check(epoch(0, 1.0)), StopDecision::Continue);
481 assert_eq!(mon.check(epoch(1, 1.1)), StopDecision::Continue);
482 match mon.check(epoch(2, 1.2)) {
484 StopDecision::Stop(_) => {}
485 StopDecision::Continue => panic!("expected Stop"),
486 }
487 }
488
489 #[test]
492 fn custom_min_metric() {
493 let config = EarlyStoppingConfig {
494 criterion: StopCriterion::MinMetric("val_loss".to_string()),
495 patience: 2,
496 min_delta: 0.0,
497 min_epochs: 0,
498 restore_best: false,
499 };
500 let mut mon = EarlyStoppingMonitor::new(config);
501 assert_eq!(
502 mon.check(epoch_with_custom(0, 999.0, "val_loss", 1.0)),
503 StopDecision::Continue
504 );
505 assert_eq!(
506 mon.check(epoch_with_custom(1, 999.0, "val_loss", 0.9)),
507 StopDecision::Continue
508 );
509 assert_eq!(mon.epochs_without_improvement(), 0);
510 assert_eq!(
511 mon.check(epoch_with_custom(2, 999.0, "val_loss", 1.0)),
512 StopDecision::Continue
513 );
514 match mon.check(epoch_with_custom(3, 999.0, "val_loss", 1.1)) {
515 StopDecision::Stop(_) => {}
516 StopDecision::Continue => panic!("expected Stop"),
517 }
518 }
519
520 #[test]
521 fn custom_max_metric() {
522 let config = EarlyStoppingConfig {
523 criterion: StopCriterion::MaxMetric("f1_score".to_string()),
524 patience: 2,
525 min_delta: 0.0,
526 min_epochs: 0,
527 restore_best: false,
528 };
529 let mut mon = EarlyStoppingMonitor::new(config);
530 assert_eq!(
531 mon.check(epoch_with_custom(0, 999.0, "f1_score", 0.5)),
532 StopDecision::Continue
533 );
534 assert_eq!(
535 mon.check(epoch_with_custom(1, 999.0, "f1_score", 0.6)),
536 StopDecision::Continue
537 );
538 assert_eq!(
539 mon.check(epoch_with_custom(2, 999.0, "f1_score", 0.55)),
540 StopDecision::Continue
541 );
542 match mon.check(epoch_with_custom(3, 999.0, "f1_score", 0.54)) {
543 StopDecision::Stop(_) => {}
544 StopDecision::Continue => panic!("expected Stop"),
545 }
546 }
547
548 #[test]
549 fn missing_custom_metric_counts_as_no_improvement() {
550 let config = EarlyStoppingConfig {
551 criterion: StopCriterion::MinMetric("val_loss".to_string()),
552 patience: 2,
553 min_delta: 0.0,
554 min_epochs: 0,
555 restore_best: false,
556 };
557 let mut mon = EarlyStoppingMonitor::new(config);
558 assert_eq!(
560 mon.check(epoch_with_custom(0, 1.0, "val_loss", 0.5)),
561 StopDecision::Continue
562 );
563 assert_eq!(mon.check(epoch(1, 1.0)), StopDecision::Continue);
565 match mon.check(epoch(2, 1.0)) {
566 StopDecision::Stop(_) => {}
567 StopDecision::Continue => panic!("expected Stop"),
568 }
569 }
570
571 #[test]
574 fn reset_clears_all_state() {
575 let mut mon = EarlyStoppingMonitor::new(default_config());
576 mon.check(epoch(0, 1.0));
577 mon.check(epoch(1, 1.1));
578 mon.check(epoch(2, 1.2));
579 mon.check(epoch(3, 1.3)); assert!(mon.should_stop());
581 assert_eq!(mon.history().len(), 4);
582
583 mon.reset();
584 assert!(!mon.should_stop());
585 assert!(mon.history().is_empty());
586 assert_eq!(mon.epochs_without_improvement(), 0);
587 assert_eq!(mon.best_value(), f64::INFINITY);
588 assert_eq!(mon.stats().total_epochs, 0);
589 }
590
591 #[test]
594 fn is_improvement_min_loss() {
595 let mon = EarlyStoppingMonitor::new(default_config());
596 assert!(mon.is_improvement(1.0));
598 assert!(mon.is_improvement(-100.0));
599 }
600
601 #[test]
602 fn is_improvement_max_accuracy() {
603 let config = EarlyStoppingConfig {
604 criterion: StopCriterion::MaxAccuracy,
605 patience: 3,
606 min_delta: 0.0,
607 min_epochs: 0,
608 restore_best: false,
609 };
610 let mon = EarlyStoppingMonitor::new(config);
611 assert!(mon.is_improvement(0.0));
613 assert!(mon.is_improvement(0.5));
614 }
615
616 #[test]
617 fn is_improvement_nan_never_improves() {
618 let mon = EarlyStoppingMonitor::new(default_config());
619 assert!(!mon.is_improvement(f64::NAN));
620 }
621
622 #[test]
625 fn history_tracks_all_epochs() {
626 let mut mon = EarlyStoppingMonitor::new(default_config());
627 mon.check(epoch(0, 1.0));
628 mon.check(epoch(1, 0.9));
629 mon.check(epoch(2, 0.8));
630 assert_eq!(mon.history().len(), 3);
631 assert_eq!(mon.history()[0].epoch, 0);
632 assert_eq!(mon.history()[2].epoch, 2);
633 }
634
635 #[test]
638 fn stats_track_improvements() {
639 let mut mon = EarlyStoppingMonitor::new(default_config());
640 mon.check(epoch(0, 1.0)); mon.check(epoch(1, 0.9)); mon.check(epoch(2, 0.8)); mon.check(epoch(3, 0.9)); let s = mon.stats();
645 assert_eq!(s.total_epochs, 4);
646 assert_eq!(s.improvements, 3);
647 assert!((s.best_value - 0.8).abs() < 1e-10);
648 assert_eq!(s.best_epoch, 2);
649 assert!(s.stopped_at.is_none());
650 }
651
652 #[test]
653 fn stats_record_stopped_at() {
654 let mut mon = EarlyStoppingMonitor::new(default_config());
655 mon.check(epoch(0, 1.0));
656 mon.check(epoch(1, 1.1));
657 mon.check(epoch(2, 1.2));
658 mon.check(epoch(3, 1.3)); assert_eq!(mon.stats().stopped_at, Some(3));
660 }
661
662 #[test]
665 fn stop_reason_includes_details() {
666 let mut mon = EarlyStoppingMonitor::new(default_config());
667 mon.check(epoch(0, 0.5));
668 mon.check(epoch(1, 0.6));
669 mon.check(epoch(2, 0.7));
670 match mon.check(epoch(3, 0.8)) {
671 StopDecision::Stop(reason) => {
672 assert!(reason.contains("3 epochs"), "reason: {reason}");
673 assert!(reason.contains("0.5"), "reason: {reason}");
674 assert!(reason.contains("epoch 0"), "reason: {reason}");
675 }
676 StopDecision::Continue => panic!("expected Stop"),
677 }
678 }
679
680 #[test]
683 fn nan_loss_does_not_improve() {
684 let mut mon = EarlyStoppingMonitor::new(default_config());
685 mon.check(epoch(0, 1.0));
686 mon.check(epoch(1, f64::NAN));
687 assert_eq!(mon.epochs_without_improvement(), 1);
688 assert!((mon.best_value() - 1.0).abs() < 1e-10);
689 }
690
691 #[test]
692 fn identical_values_no_improvement() {
693 let mut mon = EarlyStoppingMonitor::new(default_config());
694 mon.check(epoch(0, 1.0));
695 mon.check(epoch(1, 1.0)); assert_eq!(mon.epochs_without_improvement(), 1);
697 }
698
699 #[test]
700 fn identical_values_with_zero_delta_still_no_improvement() {
701 let config = EarlyStoppingConfig {
702 criterion: StopCriterion::MinLoss,
703 patience: 3,
704 min_delta: 0.0,
705 min_epochs: 0,
706 restore_best: false,
707 };
708 let mut mon = EarlyStoppingMonitor::new(config);
709 mon.check(epoch(0, 1.0));
710 mon.check(epoch(1, 1.0));
711 assert_eq!(mon.epochs_without_improvement(), 1);
713 }
714
715 #[test]
718 fn remaining_patience_countdown() {
719 let mut mon = EarlyStoppingMonitor::new(default_config());
720 assert_eq!(mon.remaining_patience(), 3);
721 mon.check(epoch(0, 1.0)); assert_eq!(mon.remaining_patience(), 3);
723 mon.check(epoch(1, 1.1)); assert_eq!(mon.remaining_patience(), 2);
725 mon.check(epoch(2, 1.2));
726 assert_eq!(mon.remaining_patience(), 1);
727 mon.check(epoch(3, 1.3)); assert_eq!(mon.remaining_patience(), 0);
729 }
730
731 #[test]
732 fn remaining_patience_resets_on_improvement() {
733 let mut mon = EarlyStoppingMonitor::new(default_config());
734 mon.check(epoch(0, 1.0));
735 mon.check(epoch(1, 1.1)); mon.check(epoch(2, 1.2)); assert_eq!(mon.remaining_patience(), 1);
738 mon.check(epoch(3, 0.5)); assert_eq!(mon.remaining_patience(), 3);
740 }
741
742 #[test]
745 fn extract_metric_min_loss() {
746 let mon = EarlyStoppingMonitor::new(default_config());
747 let m = epoch(0, 0.42);
748 assert_eq!(mon.extract_metric(&m), Some(0.42));
749 }
750
751 #[test]
752 fn extract_metric_max_accuracy_present() {
753 let config = EarlyStoppingConfig {
754 criterion: StopCriterion::MaxAccuracy,
755 ..default_config()
756 };
757 let mon = EarlyStoppingMonitor::new(config);
758 let m = epoch_with_acc(0, 1.0, 0.95);
759 assert_eq!(mon.extract_metric(&m), Some(0.95));
760 }
761
762 #[test]
763 fn extract_metric_max_accuracy_absent() {
764 let config = EarlyStoppingConfig {
765 criterion: StopCriterion::MaxAccuracy,
766 ..default_config()
767 };
768 let mon = EarlyStoppingMonitor::new(config);
769 let m = epoch(0, 1.0);
770 assert_eq!(mon.extract_metric(&m), None);
771 }
772
773 #[test]
774 fn extract_metric_custom_present() {
775 let config = EarlyStoppingConfig {
776 criterion: StopCriterion::MinMetric("rmse".to_string()),
777 ..default_config()
778 };
779 let mon = EarlyStoppingMonitor::new(config);
780 let m = epoch_with_custom(0, 1.0, "rmse", std::f64::consts::PI);
781 assert_eq!(mon.extract_metric(&m), Some(std::f64::consts::PI));
782 }
783
784 #[test]
785 fn extract_metric_custom_absent() {
786 let config = EarlyStoppingConfig {
787 criterion: StopCriterion::MinMetric("rmse".to_string()),
788 ..default_config()
789 };
790 let mon = EarlyStoppingMonitor::new(config);
791 let m = epoch(0, 1.0);
792 assert_eq!(mon.extract_metric(&m), None);
793 }
794
795 #[test]
798 fn returns_stop_once_stopped() {
799 let mut mon = EarlyStoppingMonitor::new(default_config());
800 mon.check(epoch(0, 1.0));
801 mon.check(epoch(1, 1.1));
802 mon.check(epoch(2, 1.2));
803 mon.check(epoch(3, 1.3)); assert!(mon.should_stop());
805 match mon.check(epoch(4, 0.1)) {
806 StopDecision::Stop(reason) => assert!(reason.contains("Already")),
807 StopDecision::Continue => panic!("expected Stop"),
808 }
809 }
810
811 #[test]
814 fn best_epoch_tracks_correctly() {
815 let mut mon = EarlyStoppingMonitor::new(default_config());
816 mon.check(epoch(0, 1.0));
817 assert_eq!(mon.best_epoch(), 0);
818 mon.check(epoch(1, 0.8));
819 assert_eq!(mon.best_epoch(), 1);
820 mon.check(epoch(2, 0.9));
821 assert_eq!(mon.best_epoch(), 1); }
823
824 #[test]
825 fn best_value_tracks_correctly() {
826 let mut mon = EarlyStoppingMonitor::new(default_config());
827 mon.check(epoch(0, 1.0));
828 assert!((mon.best_value() - 1.0).abs() < 1e-10);
829 mon.check(epoch(1, 0.5));
830 assert!((mon.best_value() - 0.5).abs() < 1e-10);
831 mon.check(epoch(2, 0.7));
832 assert!((mon.best_value() - 0.5).abs() < 1e-10);
833 }
834}