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 p    /// Propagate free-coefficient covariance to response variance at `stimulus`.ersistence
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::checkpoint::CheckpointError;
91use crate::engine::policy::{CancellationPolicy, CompletionPolicy, EnginePolicy, PolicyStack};
92use crate::state::{ConvergenceState, RuntimeState};
93use crate::{
94    engine::{
95        checkpoint::{CheckpointBackend, CheckpointExtension},
96        extensions::Extensions,
97        Engine,
98    },
99    state::{Snapshotable, State, StateRestorer},
100    watchers::{Frequency, Observe, Observers},
101    FallibleProcedure, Infallible, Procedure, UserState,
102};
103
104pub trait GenerateBuilderFallible: Sized {
105    fn build_for<P>(self, problem: P) -> Builder<Self, P, Uninitialised>
106    where
107        Self: FallibleProcedure<P>,
108        Self::State: UserState;
109}
110
111impl<Proc> GenerateBuilderFallible for Proc {
112    fn build_for<P>(self, problem: P) -> Builder<Self, P, Uninitialised>
113    where
114        Proc: FallibleProcedure<P>,
115        Proc::State: UserState,
116    {
117        Builder {
118            procedure: self,
119            problem,
120            state: None,
121            runtime: None,
122            convergence: None,
123            time: true,
124            cancellation_token: None,
125
126            observers: Observers::new(),
127
128            policies: PolicyStack::new()
129                .add(CancellationPolicy)
130                .add(CompletionPolicy),
131
132            extensions: Extensions::new(),
133
134            _initialised: std::marker::PhantomData,
135        }
136    }
137}
138
139pub trait GenerateBuilder: Sized {
140    fn build_for<P>(self, problem: P) -> Builder<Infallible<Self>, P, Uninitialised>
141    where
142        Self: Procedure<P>,
143        Self::State: UserState;
144}
145
146impl<Proc> GenerateBuilder for Proc {
147    fn build_for<P>(self, problem: P) -> Builder<Infallible<Self>, P, Uninitialised>
148    where
149        Proc: Procedure<P>,
150        Proc::State: UserState,
151    {
152        Builder {
153            procedure: Infallible(self),
154            problem,
155            state: None,
156            runtime: None,
157            convergence: None,
158            time: true,
159            cancellation_token: None,
160
161            observers: Observers::new(),
162
163            policies: PolicyStack::new()
164                .add(CancellationPolicy)
165                .add(CompletionPolicy),
166
167            extensions: Extensions::new(),
168
169            _initialised: std::marker::PhantomData,
170        }
171    }
172}
173
174pub struct Uninitialised;
175pub struct Initialised;
176
177pub struct Builder<Proc, P, I>
178where
179    Proc: FallibleProcedure<P>,
180    Proc::State: UserState,
181    <Proc::State as UserState>::Float: FloatCore,
182{
183    procedure: Proc,
184    problem: P,
185    state: Option<Proc::State>,
186    runtime: Option<RuntimeState>,
187    convergence: Option<ConvergenceState<<Proc::State as UserState>::Float>>,
188    time: bool,
189    cancellation_token: Option<CancellationToken>,
190
191    observers: Observers<Proc::State>,
192
193    policies: PolicyStack<<Proc::State as UserState>::Float>,
194    extensions: Extensions<Proc::State>,
195
196    _initialised: std::marker::PhantomData<I>,
197}
198
199impl<Proc, P, I> Builder<Proc, P, I>
200where
201    Proc: FallibleProcedure<P>,
202    Proc::State: UserState,
203    <Proc::State as UserState>::Float: FloatCore + 'static,
204{
205    #[must_use]
206    pub fn time(mut self, time: bool) -> Self {
207        self.time = time;
208        self
209    }
210
211    /// Attach a state observer (full state + stage awareness)
212    ///
213    /// TODO: pub(crate) until stabilised
214    #[must_use]
215    pub(crate) fn attach_observer<OBS>(mut self, observer: OBS, frequency: Frequency) -> Self
216    where
217        OBS: Observe<Proc::State> + 'static,
218    {
219        self.observers
220            .attach(Arc::new(Mutex::new(observer)), frequency);
221        self
222    }
223
224    #[must_use]
225    pub fn and_policy<Q>(mut self, policy: Q) -> Self
226    where
227        Q: EnginePolicy<<Proc::State as UserState>::Float> + 'static,
228    {
229        self.policies = self.policies.add(policy);
230        self
231    }
232
233    #[must_use]
234    pub fn cancellation_token(mut self, token: CancellationToken) -> Self {
235        self.cancellation_token = Some(token);
236        self
237    }
238
239    #[must_use]
240    /// Appends a standard policy set to the existing policy stack.
241    ///
242    /// This does not replace existing policies; it merges them into the current stack.
243    pub fn with_default_policies(
244        mut self,
245        max_iter: usize,
246        absolute_tolerance: <Proc::State as UserState>::Float,
247        window_size: usize,
248    ) -> Self {
249        self.policies = self.policies.merge(PolicyStack::standard(
250            max_iter,
251            absolute_tolerance,
252            window_size,
253        ));
254        self
255    }
256
257    #[must_use]
258    /// Enables checkpointing support for this engine.
259    ///
260    /// This method is only available if the procedure state implements `Snapshotable`.
261    ///
262    /// When enabled, checkpoints are emitted via the engine extension system.
263    pub fn with_checkpoint_backend<C>(mut self, store: C) -> Self
264    where
265        C: CheckpointBackend<
266                <Proc::State as Snapshotable>::Snapshot,
267                <Proc::State as UserState>::Float,
268            > + 'static,
269        Proc::State: Snapshotable,
270    {
271        self.extensions = self.extensions.add(CheckpointExtension::new(store));
272        self
273    }
274}
275
276impl<Proc, P> Builder<Proc, P, Uninitialised>
277where
278    Proc: FallibleProcedure<P>,
279    Proc::State: UserState,
280    <Proc::State as UserState>::Float: FloatCore + 'static,
281{
282    // TODO: Possibly unneeded if a valid state is always constructed in the initialise method
283    #[must_use]
284    pub fn with_initial_state(self, user: Proc::State) -> Builder<Proc, P, Initialised> {
285        Builder {
286            procedure: self.procedure,
287            problem: self.problem,
288            state: Some(user),
289            runtime: self.runtime,
290            convergence: self.convergence,
291            time: self.time,
292            cancellation_token: self.cancellation_token,
293
294            observers: self.observers,
295
296            policies: self.policies,
297
298            extensions: self.extensions,
299
300            _initialised: std::marker::PhantomData,
301        }
302    }
303
304    #[must_use]
305    pub fn resume_from_snapshot(
306        self,
307        snapshot: <Proc::State as Snapshotable>::Snapshot,
308    ) -> Builder<Proc, P, Initialised>
309    where
310        Proc: FallibleProcedure<P>,
311        Proc::State: Snapshotable + StateRestorer<Proc::State>,
312    {
313        let user = Proc::State::restore(snapshot);
314
315        Builder {
316            procedure: self.procedure,
317            problem: self.problem,
318            state: Some(user),
319            runtime: self.runtime,
320            convergence: self.convergence,
321            time: self.time,
322            cancellation_token: self.cancellation_token,
323
324            observers: self.observers,
325
326            policies: self.policies,
327
328            extensions: self.extensions,
329
330            _initialised: std::marker::PhantomData,
331        }
332    }
333
334    pub fn resume_from_checkpoint<C>(
335        self,
336        store: C,
337    ) -> Result<Builder<Proc, P, Initialised>, CheckpointError>
338    where
339        C: CheckpointBackend<
340                <Proc::State as Snapshotable>::Snapshot,
341                <Proc::State as UserState>::Float,
342            > + 'static,
343        Proc: FallibleProcedure<P>,
344        Proc::State: Snapshotable + StateRestorer<Proc::State>,
345    {
346        if let Some(state) = store.load()? {
347            return Ok(Builder {
348                procedure: self.procedure,
349                problem: self.problem,
350                state: Some(Proc::State::restore(state.user)),
351                runtime: Some(state.runtime),
352                convergence: Some(state.convergence),
353                time: self.time,
354                cancellation_token: self.cancellation_token,
355
356                observers: self.observers,
357
358                policies: self.policies,
359
360                extensions: self.extensions,
361
362                _initialised: std::marker::PhantomData,
363            });
364        }
365        Err(CheckpointError::NoCheckpoint)
366    }
367}
368
369impl<Proc, P> Builder<Proc, P, Initialised>
370where
371    Proc: FallibleProcedure<P>,
372    Proc::State: UserState,
373    <Proc::State as UserState>::Float: FloatCore + 'static,
374{
375    /// Finalises the builder using the currently configured policy stack.
376    ///
377    /// If no policies were added, the engine will run with an empty policy stack
378    /// (i.e. no termination conditions beyond external cancellation).efault policy
379    pub fn finalise(mut self) -> Engine<Proc, P, PolicyStack<<Proc::State as UserState>::Float>>
380    where
381        <Proc::State as UserState>::Float: num_traits::FromPrimitive,
382    {
383        let user = self.state.take().expect("builder invariant: user is set");
384
385        let cancellation = self.cancellation_token.unwrap_or_default();
386
387        #[cfg(all(feature = "ctrlc", not(test)))]
388        {
389            let token = cancellation.clone();
390            ctrlc::set_handler(move || {
391                token.cancel();
392            })
393            .unwrap();
394        }
395
396        Engine {
397            procedure: self.procedure,
398            problem: self.problem,
399            state: {
400                match (self.convergence, self.runtime) {
401                    (Some(convergence), Some(runtime)) => {
402                        State::from_parts(user, runtime, convergence)
403                    }
404                    _ => State::new(user),
405                }
406            },
407
408            time: self.time,
409            start_time: None,
410
411            cancellation,
412
413            policy: self.policies,
414
415            observers: self.observers,
416            extensions: self.extensions,
417        }
418    }
419
420    /// Finalises the engine with a custom policy stack.
421    ///
422    /// This replaces the builder’s internal policy stack but preserves:
423    /// - observers
424    /// - extensions
425    /// - cancellation token
426    /// - state configuration
427    pub fn finalise_with(
428        mut self,
429        policy: PolicyStack<<Proc::State as UserState>::Float>,
430    ) -> Engine<Proc, P, PolicyStack<<Proc::State as UserState>::Float>> {
431        let user = self.state.take().expect("builder invariant: user is set");
432        let cancellation = self.cancellation_token.unwrap_or_default();
433
434        #[cfg(all(feature = "ctrlc", not(test)))]
435        {
436            let token = cancellation.clone();
437            ctrlc::set_handler(move || {
438                token.cancel();
439            })
440            .unwrap();
441        }
442
443        Engine {
444            procedure: self.procedure,
445            problem: self.problem,
446            state: {
447                match (self.convergence, self.runtime) {
448                    (Some(convergence), Some(runtime)) => {
449                        State::from_parts(user, runtime, convergence)
450                    }
451                    _ => State::new(user),
452                }
453            },
454
455            time: self.time,
456            start_time: None,
457
458            cancellation,
459
460            policy,
461
462            observers: self.observers,
463            extensions: self.extensions,
464        }
465    }
466}
467//     pub fn with_checkpoint_resumed(mut self) -> Result<Self, CheckpointError>
468//     where
469//         C: CheckpointBackend<Proc::State>,
470//     {
471//         if let Some(store) = &self.checkpoint_store {
472//             if let Some(checkpoint) = store.load()? {
473//                 self.state = checkpoint.into_state();
474//             }
475//         }
476
477//         Ok(self)
478//     }
479// }