Skip to main content

trellis_runner/engine/
builder.rs

1//! # Engine Builder API
2//!
3//! This module provides a fluent, consuming builder for constructing an [`Engine`] instance.
4//!
5//! The builder is responsible for assembling all runtime components required to execute a
6//! procedure:
7//!
8//! - numerical procedure (`FallibleProcedure`)
9//! - initial solver state (`State`)
10//! - policy stack (`PolicyStack`)
11//! - observers (`Observe` implementations)
12//! - cancellation support (`CancellationToken`)
13//! - execution extensions (`EngineSink`), including checkpointing
14//!
15//! ## Design philosophy
16//!
17//! The builder follows a *consuming accumulation model*:
18//!
19//! - Each method takes ownership of `self`
20//! - Each call returns a modified builder
21//! - No shared mutable setup state exists
22//!
23//! This ensures:
24//! - deterministic construction order
25//! - composable configuration layers
26//! - separation between configuration and execution
27//!
28//! ## Execution model
29//!
30//! The engine operates on three independent layers:
31//!
32//! ### 1. Policies
33//! Policies inspect solver progress and produce an [`EngineAction`]:
34//! - continue execution
35//! - request checkpointing
36//! - stop execution
37//!
38//! Policies are composed in a [`PolicyStack`].
39//!
40//! ### 2. Observers
41//! Observers receive structured state snapshots (`StateView`) and engine signals
42//! for logging, monitoring, or metrics.
43//!
44//! ### 3. Extensions
45//! Extensions react to high-level engine signals (`EngineSignal`) and perform
46//! side effects such as:
47//! - checkpoint persistence
48//! - external storage
49//! - asynchronous logging pipelines
50//!
51//! Extensions are decoupled from core execution logic.
52//!
53//! ## Checkpointing
54//!
55//! Checkpointing is implemented as an optional extension.
56//! It is only available when the state type supports snapshotting (`Snapshotable`).
57//!
58//! Checkpoints are triggered by policies and handled by an `EngineSink` extension.
59//!
60//! ## Minimal usage
61//!
62//! ```ignore
63//! let engine = MyFallibleProcedure::new()
64//!     .build_for(problem)
65//!     .finalise();
66//! ```
67//!
68//! ## Fully configured usage
69//!
70//! ```ignore
71//! let engine = MyFallibleProcedure::new()
72//!     .build_for(problem)
73//!     .time(true)
74//!     .with_default_policies(max_iter, tol)
75//!     .and_policy(my_policy)
76//!     .attach_observer(tracer, Frequency::Always)
77//!     .with_checkpoint_backend(store)
78//!     .finalise();
79//! ```
80//!
81//! ## Design note
82//!
83//! The builder does not enforce a single “correct” policy set.
84//! Policies are always composed explicitly by the user or via helpers.
85//!
86use num_traits::float::FloatCore;
87use std::sync::{Arc, Mutex};
88use tokio_util::sync::CancellationToken;
89
90use crate::engine::policy::{CancellationPolicy, CompletionPolicy, EnginePolicy, PolicyStack};
91use crate::{
92    engine::{
93        checkpoint::{CheckpointBackend, CheckpointExtension},
94        extensions::Extensions,
95        Engine,
96    },
97    state::{Snapshotable, State, StateRestorer},
98    watchers::{Frequency, Observe, Observers},
99    FallibleProcedure, Infallible, Procedure, UserState,
100};
101
102pub trait GenerateBuilderFallible: Sized {
103    fn build_for<P>(self, problem: P) -> Builder<Self, P, Uninitialised>
104    where
105        Self: FallibleProcedure<P>,
106        Self::State: UserState;
107}
108
109impl<Proc> GenerateBuilderFallible for Proc {
110    fn build_for<P>(self, problem: P) -> Builder<Self, P, Uninitialised>
111    where
112        Proc: FallibleProcedure<P>,
113        Proc::State: UserState,
114    {
115        Builder {
116            procedure: self,
117            problem,
118            state: None,
119            time: true,
120            cancellation_token: None,
121
122            observers: Observers::new(),
123
124            policies: PolicyStack::new()
125                .add(CancellationPolicy)
126                .add(CompletionPolicy),
127
128            extensions: Extensions::new(),
129
130            _initialised: std::marker::PhantomData,
131        }
132    }
133}
134
135pub trait GenerateBuilder: Sized {
136    fn build_for<P>(self, problem: P) -> Builder<Infallible<Self>, P, Uninitialised>
137    where
138        Self: Procedure<P>,
139        Self::State: UserState;
140}
141
142impl<Proc> GenerateBuilder for Proc {
143    fn build_for<P>(self, problem: P) -> Builder<Infallible<Self>, P, Uninitialised>
144    where
145        Proc: Procedure<P>,
146        Proc::State: UserState,
147    {
148        Builder {
149            procedure: Infallible(self),
150            problem,
151            state: None,
152            time: true,
153            cancellation_token: None,
154
155            observers: Observers::new(),
156
157            policies: PolicyStack::new()
158                .add(CancellationPolicy)
159                .add(CompletionPolicy),
160
161            extensions: Extensions::new(),
162
163            _initialised: std::marker::PhantomData,
164        }
165    }
166}
167
168pub struct Uninitialised;
169pub struct Initialised;
170
171pub struct Builder<Proc, P, I>
172where
173    Proc: FallibleProcedure<P>,
174    Proc::State: UserState,
175    <Proc::State as UserState>::Float: FloatCore,
176{
177    procedure: Proc,
178    problem: P,
179    state: Option<Proc::State>,
180    time: bool,
181    cancellation_token: Option<CancellationToken>,
182
183    observers: Observers<Proc::State>,
184
185    policies: PolicyStack<<Proc::State as UserState>::Float>,
186    extensions: Extensions<Proc::State>,
187
188    _initialised: std::marker::PhantomData<I>,
189}
190
191impl<Proc, P, I> Builder<Proc, P, I>
192where
193    Proc: FallibleProcedure<P>,
194    Proc::State: UserState,
195    <Proc::State as UserState>::Float: FloatCore + 'static,
196{
197    #[must_use]
198    pub fn time(mut self, time: bool) -> Self {
199        self.time = time;
200        self
201    }
202
203    /// Attach a state observer (full state + stage awareness)
204    #[must_use]
205    pub fn attach_observer<OBS>(mut self, observer: OBS, frequency: Frequency) -> Self
206    where
207        OBS: Observe<Proc::State> + 'static,
208    {
209        self.observers
210            .attach(Arc::new(Mutex::new(observer)), frequency);
211        self
212    }
213
214    #[must_use]
215    pub fn and_policy<Q>(mut self, policy: Q) -> Self
216    where
217        Q: EnginePolicy<<Proc::State as UserState>::Float> + 'static,
218    {
219        self.policies = self.policies.add(policy);
220        self
221    }
222
223    #[must_use]
224    pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
225        self.cancellation_token = Some(token);
226        self
227    }
228
229    #[must_use]
230    /// Appends a standard policy set to the existing policy stack.
231    ///
232    /// This does not replace existing policies; it merges them into the current stack.
233    pub fn with_default_policies(
234        mut self,
235        max_iter: usize,
236        absolute_tolerance: <Proc::State as UserState>::Float,
237        window_size: usize,
238    ) -> Self {
239        self.policies = self.policies.merge(PolicyStack::standard(
240            max_iter,
241            absolute_tolerance,
242            window_size,
243        ));
244        self
245    }
246
247    #[must_use]
248    /// Enables checkpointing support for this engine.
249    ///
250    /// This method is only available if the procedure state implements `Snapshotable`.
251    ///
252    /// When enabled, checkpoints are emitted via the engine extension system.
253    pub fn with_checkpoint_backend<C>(mut self, store: C) -> Self
254    where
255        C: CheckpointBackend<
256                <Proc::State as Snapshotable>::Snapshot,
257                <Proc::State as UserState>::Float,
258            > + 'static,
259        Proc::State: Snapshotable,
260    {
261        self.extensions = self.extensions.add(CheckpointExtension::new(store));
262        self
263    }
264}
265
266impl<Proc, P> Builder<Proc, P, Uninitialised>
267where
268    Proc: FallibleProcedure<P>,
269    Proc::State: UserState,
270    <Proc::State as UserState>::Float: FloatCore + 'static,
271{
272    // TODO: Possibly unneeded if a valid state is always constructed in the initialise method
273    #[must_use]
274    pub fn with_initial_state(self, user: Proc::State) -> Builder<Proc, P, Initialised> {
275        Builder {
276            procedure: self.procedure,
277            problem: self.problem,
278            state: Some(user),
279            time: self.time,
280            cancellation_token: self.cancellation_token,
281
282            observers: self.observers,
283
284            policies: self.policies,
285
286            extensions: self.extensions,
287
288            _initialised: std::marker::PhantomData,
289        }
290    }
291
292    #[must_use]
293    pub fn resume_from_checkpoint(
294        self,
295        snapshot: <Proc::State as Snapshotable>::Snapshot,
296    ) -> Builder<Proc, P, Initialised>
297    where
298        Proc: FallibleProcedure<P>,
299        Proc::State: Snapshotable + StateRestorer<Proc::State>,
300    {
301        let user = Proc::State::restore(snapshot);
302
303        Builder {
304            procedure: self.procedure,
305            problem: self.problem,
306            state: Some(user),
307            time: self.time,
308            cancellation_token: self.cancellation_token,
309
310            observers: self.observers,
311
312            policies: self.policies,
313
314            extensions: self.extensions,
315
316            _initialised: std::marker::PhantomData,
317        }
318    }
319}
320
321impl<Proc, P> Builder<Proc, P, Initialised>
322where
323    Proc: FallibleProcedure<P>,
324    Proc::State: UserState,
325    <Proc::State as UserState>::Float: FloatCore + 'static,
326{
327    /// Finalises the builder using the currently configured policy stack.
328    ///
329    /// If no policies were added, the engine will run with an empty policy stack
330    /// (i.e. no termination conditions beyond external cancellation).efault policy
331    pub fn finalise(mut self) -> Engine<Proc, P, PolicyStack<<Proc::State as UserState>::Float>>
332    where
333        <Proc::State as UserState>::Float: num_traits::FromPrimitive,
334    {
335        let user = self.state.take().expect("builder invariant: user is set");
336
337        let cancellation = self.cancellation_token.unwrap_or_default();
338
339        #[cfg(feature = "ctrlc")]
340        {
341            let token = cancellation.clone();
342            ctrlc::set_handler(move || {
343                token.cancel();
344            })
345            .unwrap();
346        }
347
348        Engine {
349            procedure: self.procedure,
350            problem: self.problem,
351            state: State::new(user),
352
353            time: self.time,
354            start_time: None,
355
356            cancellation,
357
358            policy: self.policies,
359
360            observers: self.observers,
361            extensions: self.extensions,
362        }
363    }
364
365    /// Finalises the engine with a custom policy stack.
366    ///
367    /// This replaces the builder’s internal policy stack but preserves:
368    /// - observers
369    /// - extensions
370    /// - cancellation token
371    /// - state configuration
372    pub fn finalise_with(
373        mut self,
374        policy: PolicyStack<<Proc::State as UserState>::Float>,
375    ) -> Engine<Proc, P, PolicyStack<<Proc::State as UserState>::Float>> {
376        let user = self.state.take().expect("builder invariant: user is set");
377        let cancellation = self.cancellation_token.unwrap_or_default();
378
379        #[cfg(feature = "ctrlc")]
380        {
381            let token = cancellation.clone();
382            ctrlc::set_handler(move || {
383                token.cancel();
384            })
385            .unwrap();
386        }
387
388        Engine {
389            procedure: self.procedure,
390            problem: self.problem,
391            state: State::new(user),
392
393            time: self.time,
394            start_time: None,
395
396            cancellation,
397
398            policy,
399
400            observers: self.observers,
401            extensions: self.extensions,
402        }
403    }
404}
405//     pub fn with_checkpoint_resumed(mut self) -> Result<Self, CheckpointError>
406//     where
407//         C: CheckpointBackend<Proc::State>,
408//     {
409//         if let Some(store) = &self.checkpoint_store {
410//             if let Some(checkpoint) = store.load()? {
411//                 self.state = checkpoint.into_state();
412//             }
413//         }
414
415//         Ok(self)
416//     }
417// }