Skip to main content

radiate_core/
engine.rs

1//! # Engine Traits
2//!
3//! This module provides the core engine abstraction for genetic algorithms and evolutionary
4//! computation. The [Engine] trait defines the basic interface for evolutionary engines,
5//! while `EngineExt` provides convenient extension methods for running engines with
6//! custom termination conditions.
7//!
8//! The engine system is designed to be flexible and extensible, allowing different
9//! evolutionary algorithms to implement their own epoch types and progression logic
10//! while providing a common interface for execution control.
11
12use radiate_error::Result;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub enum EngineState {
19    PreStart,
20    Running,
21    Paused,
22    Stopped,
23}
24
25/// A trait representing an evolutionary computation engine.
26///
27/// The [Engine] trait defines the fundamental interface for evolutionary algorithms.
28/// Implementors define how the algorithm progresses from one generation/epoch to the
29/// next, encapsulating the core evolutionary logic.
30///
31/// It is intentionally essentially an iterator.
32///
33/// # Generic Parameters
34///
35/// - `Epoch`: The type representing a single step or generation in the evolutionary process
36///
37/// # Examples
38///
39/// ```rust
40/// use radiate_core::engine::{Engine, EngineExt, EngineState};
41/// use radiate_error::RadiateError;
42///
43/// #[derive(Default)]
44/// struct MyEngine {
45///     generation: usize,
46///     population: Vec<i32>,
47/// }
48///
49/// #[derive(Debug, Clone)]
50/// struct MyEpoch {
51///     generation: usize,
52///     population_size: usize,
53/// }
54///
55/// impl Engine for MyEngine {
56///     type Epoch = MyEpoch;
57///     type Ctx = ();
58///
59///     fn context(&self) -> &Self::Ctx {
60///        &()
61///     }
62///
63///     fn epoch(&self) -> Self::Epoch {
64///        MyEpoch {
65///           generation: self.generation,
66///           population_size: self.population.len(),
67///         }
68///     }
69///
70///     fn state(&self) -> EngineState {
71///         // Return the current state of the engine
72///         EngineState::Running
73///     }
74///
75///     fn step(&mut self) -> Result<(), RadiateError> {
76///         // Perform one generation of evolution
77///         // ... evolve population ...
78///         self.generation += 1;
79///         Ok(())
80///     }
81/// }
82///
83/// // Use the engine with a termination condition
84/// let mut engine = MyEngine::default();
85/// let final_epoch = engine.run(|epoch| epoch.generation >= 10);
86/// println!("Final generation: {}", final_epoch.generation);
87/// ```
88///
89/// # Design Philosophy
90///
91/// The [Engine] trait is intentionally minimal, focusing on the core concept of
92/// progression through evolutionary time. This allows for maximum flexibility in
93/// implementing different evolutionary algorithms while maintaining a small, consistent
94/// interface for execution control.
95pub trait Engine {
96    /// The type representing a single epoch or generation in the evolutionary process.
97    ///
98    /// The epoch type should contain all relevant information about the current
99    /// state of the evolutionary algorithm, such as:
100    /// - Generation number
101    /// - Population statistics
102    /// - Best fitness values
103    /// - Convergence metrics
104    /// - Any other state information needed for monitoring or decision-making
105    type Epoch;
106    /// The type representing the context of the engine, which may include configuration,
107    /// state, and other relevant information. This allows external systems to inspect
108    /// the engine's state without needing to clone or modify it.
109    type Ctx;
110
111    /// Returns a reference to the current context of the engine. This is meant to
112    /// provide a read-only view of the engine's internal state, to allow external systems
113    /// close to the engine level, to inspect the engine without cloning anything.
114    fn context(&self) -> &Self::Ctx;
115
116    /// Returns an epoch of the engine, which is intended to be a snapshot of the current state, or
117    /// the current context. The `Epoch` here can be given to iterators, callers, or anyone else who needs it.
118    /// Essentially to say, this shouldn't borrow anything from the engine, but instead be an owned snapshot
119    /// of the engine
120    fn epoch(&self) -> Self::Epoch;
121
122    /// Advances the engine by one step, performing the necessary computations to progress
123    /// the evolutionary algorithm. This may include evaluating fitness, selecting individuals,
124    /// applying genetic operators, and updating the population. This intentionally does not return anything,
125    /// that is left up to the `epoch()` method or the `next()` method. It's done this way so we
126    /// can advance the engine without having to clone or create any new data, and possibly perform operations
127    /// outside of the engine which don't require a snapshot of the engine state.
128    fn step(&mut self) -> Result<()>;
129
130    /// Starts the engine, initializing any necessary state or resources.
131    /// This is typically called before entering the main execution loop.
132    fn start(&mut self) {}
133
134    /// Stops the engine, performing any necessary cleanup or finalization.
135    /// This is typically called after exiting the main execution loop.
136    fn stop(&mut self) {}
137
138    /// Returns the current state of the engine.
139    /// This allows external systems to query the engine's status without modifying it.
140    /// This lets the [Engine] act as a state machine, while external systems can query
141    /// the current state of the engine.
142    fn state(&self) -> EngineState;
143}
144
145pub trait EngineStream: Engine {
146    type View<'a>
147    where
148        Self: 'a;
149
150    fn run<F>(self, limit: F) -> Result<Self::Epoch>
151    where
152        F: Fn(Self::View<'_>) -> bool + 'static;
153}
154
155/// Extension trait providing convenient methods for running engines with custom logic.
156///
157/// `EngineExt` provides additional functionality for engines without requiring
158/// changes to the core [Engine] trait. This follows the Rust pattern of using
159/// extension traits to add functionality to existing types.
160///
161/// # Generic Parameters
162///
163/// - `E`: The engine type that this extension applies to
164///
165/// # Design Benefits
166///
167/// - **Separation of Concerns**: Core engine logic is separate from execution control
168/// - **Flexibility**: Different termination conditions can be easily implemented
169/// - **Reusability**: The same engine can be run with different stopping criteria
170/// - **Testability**: Termination logic can be tested independently of engine logic
171pub trait EngineExt<E: Engine> {
172    /// Runs the engine until the specified termination condition is met.
173    ///
174    /// This method continuously calls `engine.next()` until the provided closure
175    /// returns `true`, indicating that the termination condition has been satisfied.
176    /// The final epoch is returned, allowing you to inspect the final state of
177    /// the evolutionary process.
178    ///
179    /// # Arguments
180    ///
181    /// * `limit` - A closure that takes the current epoch and returns `true` when
182    ///   the engine should stop, `false` to continue
183    ///
184    /// # Returns
185    ///
186    /// The epoch that satisfied the termination condition
187    ///
188    /// # Termination Conditions
189    ///
190    /// Common termination conditions include:
191    /// - **Generation Limit**: Stop after a fixed number of generations
192    /// - **Fitness Threshold**: Stop when best fitness reaches a target value
193    /// - **Convergence**: Stop when population diversity or fitness improvement is minimal
194    /// - **Time Limit**: Stop after a certain amount of computation time
195    /// - **Solution Quality**: Stop when a satisfactory solution is found
196    ///
197    /// # Performance Considerations
198    ///
199    /// - The termination condition is checked after every epoch, so keep it lightweight
200    /// - Avoid expensive computations in the termination closure
201    /// - Consider using early termination for conditions that can be checked incrementally
202    ///
203    /// # Infinite Loops
204    ///
205    /// Be careful to ensure that your termination condition will eventually be met,
206    /// especially when using complex logic. An infinite loop will cause the program
207    /// to hang indefinitely.
208    #[deprecated(
209        since = "1.3.1",
210        note = "Use the `EngineStream` trait impl instead, which provides a more flexible and \
211        efficient way to run engines with custom termination conditions. Instead of an `E::Epoch` being \
212        given to the fn, a `GenerationView<'a, C, T>` is provided instead which is much more efficient."
213    )]
214    fn run<F>(&mut self, limit: F) -> E::Epoch
215    where
216        F: Fn(&E::Epoch) -> bool;
217}
218
219/// Blanket implementation of [EngineExt] for all types that implement [Engine].
220///
221/// This implementation provides the `run` method to any type that implements
222/// the [Engine] trait, without requiring manual implementation.
223///
224/// # Implementation Details
225///
226/// The `run` method implements a simple loop that:
227/// 1. Calls `self.next()` to advance the engine
228/// 2. Checks the termination condition using the provided closure
229/// 3. Breaks and returns the final epoch when the condition is met
230impl<E> EngineExt<E> for E
231where
232    E: Engine,
233{
234    fn run<F>(&mut self, limit: F) -> E::Epoch
235    where
236        F: Fn(&E::Epoch) -> bool,
237    {
238        loop {
239            match self.step().map(|_| self.epoch()) {
240                Ok(epoch) => {
241                    if limit(&epoch) {
242                        return epoch;
243                    }
244                }
245                Err(e) => {
246                    panic!("{e}");
247                }
248            }
249        }
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    struct MockEpoch {
258        generation: usize,
259        fitness: f32,
260    }
261
262    #[derive(Default)]
263    struct MockEngine {
264        generation: usize,
265    }
266
267    impl Engine for MockEngine {
268        type Epoch = MockEpoch;
269        type Ctx = ();
270
271        fn context(&self) -> &Self::Ctx {
272            &()
273        }
274
275        fn epoch(&self) -> Self::Epoch {
276            MockEpoch {
277                generation: self.generation,
278                fitness: 1.0 / (self.generation as f32),
279            }
280        }
281
282        fn step(&mut self) -> Result<()> {
283            self.generation += 1;
284            Ok(())
285        }
286
287        fn state(&self) -> EngineState {
288            EngineState::Running
289        }
290    }
291
292    impl EngineStream for MockEngine {
293        type View<'a>
294            = &'a MockEpoch
295        where
296            Self: 'a;
297
298        fn run<F>(mut self, limit: F) -> Result<Self::Epoch>
299        where
300            F: Fn(Self::View<'_>) -> bool + 'static,
301        {
302            loop {
303                match self.step().map(|_| self.epoch()) {
304                    Ok(epoch) => {
305                        if limit(&epoch) {
306                            return Ok(epoch);
307                        }
308                    }
309                    Err(e) => {
310                        return Err(e);
311                    }
312                }
313            }
314        }
315    }
316
317    #[test]
318    fn test_engine_next() {
319        let mut engine = MockEngine::default();
320
321        let epoch1 = engine.step().map(|_| engine.epoch()).unwrap();
322        assert_eq!(epoch1.generation, 1);
323        assert_eq!(epoch1.fitness, 1.0);
324
325        let epoch2 = engine.step().map(|_| engine.epoch()).unwrap();
326        assert_eq!(epoch2.generation, 2);
327        assert_eq!(epoch2.fitness, 0.5);
328    }
329
330    #[test]
331    fn test_engine_ext_run_generation_limit() {
332        let engine = MockEngine::default();
333
334        let final_epoch = engine.run(|epoch| epoch.generation >= 3).unwrap();
335
336        assert_eq!(final_epoch.generation, 3);
337        assert_eq!(final_epoch.fitness, 1.0 / 3.0);
338    }
339
340    #[test]
341    fn test_engine_ext_run_fitness_limit() {
342        let engine = MockEngine::default();
343
344        let final_epoch = engine.run(|epoch| epoch.fitness < 0.3).unwrap();
345
346        // Should stop when fitness drops below 0.3
347        // 1/4 = 0.25, so it should stop at generation 4
348        assert_eq!(final_epoch.generation, 4);
349        assert_eq!(final_epoch.fitness, 0.25);
350    }
351
352    #[test]
353    fn test_engine_ext_run_complex_condition() {
354        let engine = MockEngine::default();
355
356        let final_epoch = engine
357            .run(|epoch| epoch.generation >= 5 || epoch.fitness < 0.2)
358            .unwrap();
359
360        // Should stop at generation 5 due to generation limit
361        // (fitness at gen 5 is 0.2, which doesn't meet the fitness condition)
362        assert_eq!(final_epoch.generation, 5);
363        assert_eq!(final_epoch.fitness, 0.2);
364    }
365
366    #[test]
367    fn test_engine_ext_run_immediate_termination() {
368        let engine = MockEngine::default();
369
370        let final_epoch = engine.run(|_| true).unwrap();
371
372        // Should stop immediately after first epoch
373        assert_eq!(final_epoch.generation, 1);
374        assert_eq!(final_epoch.fitness, 1.0);
375    }
376
377    #[test]
378    fn test_engine_ext_run_zero_generations() {
379        let engine = MockEngine::default();
380
381        let final_epoch = engine.run(|epoch| epoch.generation > 0).unwrap();
382
383        // Should run at least one generation
384        assert_eq!(final_epoch.generation, 1);
385    }
386}