trellis_runner/engine/policy/
mod.rs1use super::EngineContext;
34
35use num_traits::float::FloatCore;
36use std::time::Duration;
37
38mod absolute_tolerance;
39mod cancellation;
40mod checkpoint;
41mod complete;
42mod max_iter;
43mod no_progress;
44mod relative_tolerance;
45mod stagnation;
46mod target_value;
47mod timeout;
48
49pub use absolute_tolerance::AbsoluteTolerancePolicy;
50pub use cancellation::CancellationPolicy;
51pub use checkpoint::CheckpointPolicy;
52pub use complete::CompletionPolicy;
53pub use max_iter::MaxIterationPolicy;
54pub use no_progress::NoProgressPolicy;
55pub use relative_tolerance::RelativeTolerancePolicy;
56pub use stagnation::StagnationPolicy;
57pub use target_value::TargetValuePolicy;
58pub use timeout::TimeoutPolicy;
59
60use crate::engine::{event::CheckpointReason, EngineAction, EventBatch};
61
62pub trait EnginePolicy<F> {
63 fn decide(&mut self, batch: &EventBatch<F>, context: &EngineContext) -> EngineAction;
64}
65
66pub trait PolicyExt<F>: EnginePolicy<F> + Sized + 'static {
67 fn boxed(self) -> Box<dyn EnginePolicy<F>> {
68 Box::new(self)
69 }
70}
71
72impl<F, T> PolicyExt<F> for T where T: EnginePolicy<F> + Sized + 'static {}
73
74pub struct PolicyStack<F> {
75 policies: Vec<Box<dyn EnginePolicy<F>>>,
76}
77
78impl<F> Default for PolicyStack<F> {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl<F> PolicyStack<F> {
85 pub fn new() -> Self {
86 Self { policies: vec![] }
87 }
88
89 pub fn is_empty(&self) -> bool {
90 self.policies.is_empty()
91 }
92
93 pub fn add<P>(mut self, p: P) -> Self
94 where
95 P: EnginePolicy<F> + 'static,
96 {
97 self.policies.push(Box::new(p));
98 self
99 }
100
101 pub fn merge(mut self, other: Self) -> Self {
102 for each in other.policies.into_iter() {
103 self.policies.push(each);
104 }
105 self
106 }
107}
108
109impl<F> EnginePolicy<F> for PolicyStack<F> {
110 fn decide(&mut self, batch: &EventBatch<F>, ctx: &EngineContext) -> EngineAction {
111 let mut checkpoint = false;
112 for p in &mut self.policies {
113 match p.decide(batch, ctx) {
114 EngineAction::Stop(t) => return EngineAction::Stop(t),
115 EngineAction::Continue => {}
116 EngineAction::EmitCheckpoint(_) => {
117 checkpoint = true;
118 }
119 }
120 }
121
122 if checkpoint {
123 return EngineAction::EmitCheckpoint(CheckpointReason::Scheduled);
124 }
125
126 EngineAction::Continue
127 }
128}
129
130impl<F> PolicyStack<F> {
131 pub fn standard(max_iter: usize, atol: F, window_size: usize) -> PolicyStack<F>
132 where
133 F: FloatCore + 'static,
134 {
135 PolicyStack::new()
136 .add(CancellationPolicy)
137 .add(MaxIterationPolicy::new(max_iter))
138 .add(AbsoluteTolerancePolicy::new(atol, window_size))
139 }
140
141 pub fn optimisation(max_iter: usize, atol: F, window_size: usize) -> PolicyStack<F>
142 where
143 F: FloatCore + 'static + num_traits::FromPrimitive + std::iter::Sum<F>,
144 {
145 PolicyStack::new()
146 .add(CancellationPolicy)
147 .add(MaxIterationPolicy::new(max_iter))
148 .add(AbsoluteTolerancePolicy::new(atol, window_size))
149 .add(StagnationPolicy::new(window_size))
150 }
151
152 pub fn global_optimisation(
153 max_iter: usize,
154 target: F,
155 tol: F,
156 window_size: usize,
157 ) -> PolicyStack<F>
158 where
159 F: FloatCore + 'static + num_traits::FromPrimitive + std::iter::Sum<F>,
160 {
161 PolicyStack::new()
162 .add(CancellationPolicy)
163 .add(MaxIterationPolicy::new(max_iter))
164 .add(TargetValuePolicy::new(target, tol, window_size))
165 .add(StagnationPolicy::new(window_size))
166 .add(NoProgressPolicy::new(F::epsilon(), window_size))
167 }
168
169 pub fn monte_carlo(max_iter: usize) -> PolicyStack<F>
170 where
171 F: 'static,
172 {
173 PolicyStack::new()
174 .add(CancellationPolicy)
175 .add(MaxIterationPolicy::new(max_iter))
176 }
177
178 pub fn timed(timeout: Duration) -> PolicyStack<F>
179 where
180 F: 'static,
181 {
182 PolicyStack::new()
183 .add(CancellationPolicy)
184 .add(TimeoutPolicy::new(timeout))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 use crate::progress::Progress;
193
194 #[test]
195 fn empty_stack_continues() {
196 let mut stack = PolicyStack::<f64>::new();
197
198 let batch: EventBatch<f64> = EventBatch::new();
199
200 let ctx = EngineContext::default();
201
202 assert!(matches!(stack.decide(&batch, &ctx), EngineAction::Continue));
203 }
204
205 #[test]
206 fn checkpoint_request_is_propagated() {
207 let mut stack = PolicyStack::<f64>::new()
208 .add(CheckpointPolicy::every(10))
209 .add(MaxIterationPolicy::new(500));
210
211 let batch: EventBatch<f64> = EventBatch::new().add(Progress::Complete);
212 let ctx = EngineContext {
213 iter: 10,
214 ..Default::default()
215 };
216
217 assert!(matches!(
218 stack.decide(&batch, &ctx),
219 EngineAction::EmitCheckpoint(_)
220 ));
221 }
222
223 #[test]
224 fn stop_takes_precedence_over_checkpoint() {
225 let mut stack = PolicyStack::<f64>::new()
226 .add(CheckpointPolicy::every(10))
227 .add(MaxIterationPolicy::new(0));
228
229 let batch: EventBatch<f64> = EventBatch::new().add(Progress::Complete);
230 let ctx = EngineContext {
231 iter: 10,
232 ..Default::default()
233 };
234
235 assert!(matches!(stack.decide(&batch, &ctx), EngineAction::Stop(_)));
236 }
237
238 #[test]
239 fn policy_stack_stop_overrides_all() {
240 let mut stack = PolicyStack::new()
241 .add(NoProgressPolicy::new(0.1, 10))
242 .add(MaxIterationPolicy::new(100));
243
244 let batch = EventBatch::new().add(Progress::Complete);
245 let ctx = EngineContext {
246 iter: 101,
247 ..Default::default()
248 };
249
250 let action = stack.decide(&batch, &ctx);
251
252 if let EngineAction::Stop(_) = action {
253 } else {
254 panic!("Stop must dominate all policies");
255 }
256 }
257
258 #[test]
259 fn integration_converges_via_tolerance() {
260 let mut stack = PolicyStack::<f64>::optimisation(100, 0.01, 5);
261
262 let ctx = EngineContext::default();
263
264 let batch = EventBatch::new().add(Progress::Report {
265 measure: 1.0,
266 diagnostics: crate::progress::ProgressDiagnostics {
267 absolute_error: Some(0.001),
268 ..Default::default()
269 },
270 });
271
272 for _ in 0..10 {
273 stack.decide(&batch, &ctx);
274 }
275
276 let action = stack.decide(&batch, &ctx);
277
278 assert!(matches!(
279 action,
280 EngineAction::Stop(crate::Termination::Converged)
281 ));
282 }
283
284 #[test]
285 fn integration_stagnation_overrides_no_progress() {
286 let mut stack = PolicyStack::<f64>::new()
287 .add(NoProgressPolicy::new(0.01, 3))
288 .add(StagnationPolicy::new(5));
289
290 let ctx = EngineContext::default();
291
292 for _ in 0..10 {
293 let batch = EventBatch::new().add(Progress::Measure(1.0));
294
295 let action = stack.decide(&batch, &ctx);
296
297 if let EngineAction::Stop(_) = action {
298 break;
299 }
300 }
301 }
302
303 #[test]
304 fn integration_timeout_trumps_all() {
305 let mut stack = PolicyStack::<f64>::timed(Duration::from_secs(1));
306
307 let batch = EventBatch::new().add(Progress::Complete);
308
309 let ctx = EngineContext {
310 elapsed: Duration::from_secs(10),
311 ..Default::default()
312 };
313
314 assert!(matches!(
315 stack.decide(&batch, &ctx),
316 EngineAction::Stop(crate::Termination::Timeout)
317 ));
318 }
319}