trellis_runner/engine/policy/
no_progress.rs1use super::EnginePolicy;
28
29use crate::{
30 engine::{EngineAction, EngineContext, EventBatch},
31 progress::Progress,
32 Termination,
33};
34
35use num_traits::float::FloatCore;
36
37pub struct NoProgressPolicy<F> {
38 tolerance: F,
39 patience: usize,
40
41 best_so_far: Option<F>,
42 counter: usize,
43}
44
45impl<F> NoProgressPolicy<F> {
46 pub fn new(tolerance: F, patience: usize) -> Self {
47 Self {
48 tolerance,
49 patience,
50 best_so_far: None,
51 counter: 0,
52 }
53 }
54}
55
56impl<F> EnginePolicy<F> for NoProgressPolicy<F>
57where
58 F: FloatCore,
59{
60 fn decide(&mut self, batch: &EventBatch<F>, _ctx: &EngineContext) -> EngineAction {
61 let mut batch_best: Option<F> = None;
62
63 for e in &batch.events {
64 let value = match e {
65 Progress::Measure(v) => *v,
66 Progress::Report { measure, .. } => *measure,
67 _ => continue,
68 };
69
70 batch_best = Some(match batch_best {
71 Some(v) => v.min(value),
72 None => value,
73 });
74 }
75
76 let Some(batch_best) = batch_best else {
77 return EngineAction::Continue;
78 };
79
80 match self.best_so_far {
81 None => {
82 self.best_so_far = Some(batch_best);
83 self.counter = 0;
84 return EngineAction::Continue;
85 }
86
87 Some(prev_best) => {
88 let denom = prev_best.abs().max(F::one());
89 let improvement = (prev_best - batch_best) / denom;
90
91 if improvement > self.tolerance {
92 self.best_so_far = Some(batch_best);
94 self.counter = 0;
95 } else {
96 self.counter += 1;
98 }
99 }
100 }
101
102 if self.counter >= self.patience {
103 return EngineAction::Stop(Termination::NoProgress);
104 }
105
106 EngineAction::Continue
107 }
108}
109
110#[cfg(test)]
111mod test {
112 use super::*;
113 use crate::engine::policy::PolicyStack;
114 use crate::engine::{EngineAction, EngineContext};
115 use crate::progress::Progress;
116
117 fn batch(v: f64) -> EventBatch<f64> {
118 EventBatch::new().add(Progress::Measure(v))
119 }
120
121 #[test]
122 fn no_progress_resets_with_improvement() {
123 let mut stack = PolicyStack::<f64>::new().add(NoProgressPolicy::new(0.1, 3));
124
125 let batch1 = EventBatch::new().add(Progress::Measure(10.0));
127
128 let ctx = EngineContext::default();
129 let _ = stack.decide(&batch1, &ctx);
130
131 for _ in 0..2 {
133 let batch = EventBatch::new().add(Progress::Measure(9.95)); let res = stack.decide(&batch, &ctx);
136 assert!(matches!(res, crate::engine::EngineAction::Continue));
137 }
138
139 let reset_batch = EventBatch::new().add(Progress::Measure(8.0));
141
142 let res = stack.decide(&reset_batch, &ctx);
143 assert!(matches!(res, crate::engine::EngineAction::Continue));
144 }
145
146 #[test]
147 fn no_progress_triggers_stagnation() {
148 let mut p = NoProgressPolicy::new(0.1, 2);
149
150 let ctx = EngineContext::default();
151
152 let _ = p.decide(&batch(10.0), &ctx);
153 let _ = p.decide(&batch(10.01), &ctx);
154 let _ = p.decide(&batch(10.02), &ctx);
155
156 assert!(matches!(
157 p.decide(&batch(10.02), &ctx),
158 EngineAction::Stop(_)
159 ));
160 }
161
162 #[test]
163 fn ignores_non_numeric_events() {
164 let mut p = NoProgressPolicy::new(0.1, 2);
165
166 let mut b = EventBatch::new();
167 b.events.push(Progress::Complete);
168
169 let ctx = EngineContext::default();
170
171 assert!(matches!(p.decide(&b, &ctx), EngineAction::Continue));
172 }
173}