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
14/// A trait representing an evolutionary computation engine.
15//
16/// The [Engine] trait defines the fundamental interface for evolutionary algorithms.
17/// Implementors define how the algorithm progresses from one generation/epoch to the
18/// next, encapsulating the core evolutionary logic.
19///
20/// It is intentionally essentially an iterator.
21///
22/// # Generic Parameters
23///
24/// - `Epoch`: The type representing a single step or generation in the evolutionary process
25///
26/// # Examples
27///
28/// ```rust
29/// use radiate_core::engine::{Engine, EngineExt};
30/// use radiate_error::RadiateError;
31///
32/// #[derive(Default)]
33/// struct MyEngine {
34///     generation: usize,
35///     population: Vec<i32>,
36/// }
37///
38/// #[derive(Debug, Clone)]
39/// struct MyEpoch {
40///     generation: usize,
41///     population_size: usize,
42/// }
43///
44/// impl Engine for MyEngine {
45///     type Epoch = MyEpoch;
46///     type Ctx = ();
47///
48///     fn context(&self) -> &Self::Ctx {
49///        &()
50///     }
51///
52///     fn epoch(&self) -> Self::Epoch {
53///        MyEpoch {
54///           generation: self.generation,
55///           population_size: self.population.len(),
56///         }
57///     }
58///
59///     fn step(&mut self) -> Result<(), RadiateError> {
60///         // Perform one generation of evolution
61///         // ... evolve population ...  
62///         self.generation += 1;
63///         Ok(())
64///     }
65/// }
66///
67/// // Use the engine with a termination condition
68/// let mut engine = MyEngine::default();
69/// let final_epoch = engine.run(|epoch| epoch.generation >= 10);
70/// println!("Final generation: {}", final_epoch.generation);
71/// ```
72///
73/// # Design Philosophy
74///
75/// The [Engine] trait is intentionally minimal, focusing on the core concept of
76/// progression through evolutionary time. This allows for maximum flexibility in
77/// implementing different evolutionary algorithms while maintaining a small, consistent
78/// interface for execution control.
79pub trait Engine {
80    /// The type representing a single epoch or generation in the evolutionary process.
81    ///
82    /// The epoch type should contain all relevant information about the current
83    /// state of the evolutionary algorithm, such as:
84    /// - Generation number
85    /// - Population statistics
86    /// - Best fitness values
87    /// - Convergence metrics
88    /// - Any other state information needed for monitoring or decision-making
89    type Epoch;
90    type Ctx;
91
92    /// Advances the engine to the next epoch or generation.
93    ///
94    /// This method encapsulates one complete iteration of the evolutionary algorithm.
95    /// It should perform all necessary operations to progress the population from
96    /// the current state to the next generation, including:
97    /// - Fitness evaluation
98    /// - Selection
99    /// - Reproduction (crossover and mutation)
100    /// - Population replacement
101    /// - Any other evolutionary operators
102    ///
103    /// # Returns
104    ///
105    /// An instance of `Self::Epoch` representing the new state after the evolution step
106    ///
107    /// # Side Effects
108    ///
109    /// This method is mutable for allowance of modification of the internal state of the engine,
110    /// advancing the evolutionary process. The engine should maintain its state between calls
111    /// to allow for continuous evolution over multiple generations.
112    ///
113    /// # Performance
114    ///
115    /// This method is called repeatedly during execution, so it should be
116    /// optimized for performance.
117    fn context(&self) -> &Self::Ctx;
118
119    fn epoch(&self) -> Self::Epoch;
120
121    fn step(&mut self) -> Result<()>;
122
123    fn next(&mut self) -> Result<Self::Epoch> {
124        self.step().map(|_| self.epoch())
125    }
126}
127
128/// Extension trait providing convenient methods for running engines with custom logic.
129///
130/// `EngineExt` provides additional functionality for engines without requiring
131/// changes to the core [Engine] trait. This follows the Rust pattern of using
132/// extension traits to add functionality to existing types.
133///
134/// # Generic Parameters
135///
136/// - `E`: The engine type that this extension applies to
137///
138/// # Design Benefits
139///
140/// - **Separation of Concerns**: Core engine logic is separate from execution control
141/// - **Flexibility**: Different termination conditions can be easily implemented
142/// - **Reusability**: The same engine can be run with different stopping criteria
143/// - **Testability**: Termination logic can be tested independently of engine logic
144pub trait EngineExt<E: Engine> {
145    /// Runs the engine until the specified termination condition is met.
146    ///
147    /// This method continuously calls `engine.next()` until the provided closure
148    /// returns `true`, indicating that the termination condition has been satisfied.
149    /// The final epoch is returned, allowing you to inspect the final state of
150    /// the evolutionary process.
151    ///
152    /// # Arguments
153    ///
154    /// * `limit` - A closure that takes the current epoch and returns `true` when
155    ///   the engine should stop, `false` to continue
156    ///
157    /// # Returns
158    ///
159    /// The epoch that satisfied the termination condition
160    ///
161    /// # Termination Conditions
162    ///
163    /// Common termination conditions include:
164    /// - **Generation Limit**: Stop after a fixed number of generations
165    /// - **Fitness Threshold**: Stop when best fitness reaches a target value
166    /// - **Convergence**: Stop when population diversity or fitness improvement is minimal
167    /// - **Time Limit**: Stop after a certain amount of computation time
168    /// - **Solution Quality**: Stop when a satisfactory solution is found
169    ///
170    /// # Performance Considerations
171    ///
172    /// - The termination condition is checked after every epoch, so keep it lightweight
173    /// - Avoid expensive computations in the termination closure
174    /// - Consider using early termination for conditions that can be checked incrementally
175    ///
176    /// # Infinite Loops
177    ///
178    /// Be careful to ensure that your termination condition will eventually be met,
179    /// especially when using complex logic. An infinite loop will cause the program
180    /// to hang indefinitely.
181    fn run<F>(&mut self, limit: F) -> E::Epoch
182    where
183        F: Fn(&E::Epoch) -> bool;
184}
185
186/// Blanket implementation of [EngineExt] for all types that implement [Engine].
187///
188/// This implementation provides the `run` method to any type that implements
189/// the [Engine] trait, without requiring manual implementation.
190///
191/// # Implementation Details
192///
193/// The `run` method implements a simple loop that:
194/// 1. Calls `self.next()` to advance the engine
195/// 2. Checks the termination condition using the provided closure
196/// 3. Breaks and returns the final epoch when the condition is met
197impl<E> EngineExt<E> for E
198where
199    E: Engine,
200{
201    fn run<F>(&mut self, limit: F) -> E::Epoch
202    where
203        F: Fn(&E::Epoch) -> bool,
204    {
205        loop {
206            match self.next() {
207                Ok(epoch) => {
208                    if limit(&epoch) {
209                        return epoch;
210                    }
211                }
212                Err(e) => {
213                    panic!("{e}");
214                }
215            }
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    struct MockEpoch {
225        generation: usize,
226        fitness: f32,
227    }
228
229    #[derive(Default)]
230    struct MockEngine {
231        generation: usize,
232    }
233
234    impl Engine for MockEngine {
235        type Epoch = MockEpoch;
236        type Ctx = ();
237
238        fn context(&self) -> &Self::Ctx {
239            &()
240        }
241
242        fn epoch(&self) -> Self::Epoch {
243            MockEpoch {
244                generation: self.generation,
245                fitness: 1.0 / (self.generation as f32),
246            }
247        }
248
249        fn step(&mut self) -> Result<()> {
250            self.generation += 1;
251            Ok(())
252        }
253    }
254
255    #[test]
256    fn test_engine_next() {
257        let mut engine = MockEngine::default();
258
259        let epoch1 = engine.next().unwrap();
260        assert_eq!(epoch1.generation, 1);
261        assert_eq!(epoch1.fitness, 1.0);
262
263        let epoch2 = engine.next().unwrap();
264        assert_eq!(epoch2.generation, 2);
265        assert_eq!(epoch2.fitness, 0.5);
266    }
267
268    #[test]
269    fn test_engine_ext_run_generation_limit() {
270        let mut engine = MockEngine::default();
271
272        let final_epoch = engine.run(|epoch| epoch.generation >= 3);
273
274        assert_eq!(final_epoch.generation, 3);
275        assert_eq!(final_epoch.fitness, 1.0 / 3.0);
276    }
277
278    #[test]
279    fn test_engine_ext_run_fitness_limit() {
280        let mut engine = MockEngine::default();
281
282        let final_epoch = engine.run(|epoch| epoch.fitness < 0.3);
283
284        // Should stop when fitness drops below 0.3
285        // 1/4 = 0.25, so it should stop at generation 4
286        assert_eq!(final_epoch.generation, 4);
287        assert_eq!(final_epoch.fitness, 0.25);
288    }
289
290    #[test]
291    fn test_engine_ext_run_complex_condition() {
292        let mut engine = MockEngine::default();
293
294        let final_epoch = engine.run(|epoch| epoch.generation >= 5 || epoch.fitness < 0.2);
295
296        // Should stop at generation 5 due to generation limit
297        // (fitness at gen 5 is 0.2, which doesn't meet the fitness condition)
298        assert_eq!(final_epoch.generation, 5);
299        assert_eq!(final_epoch.fitness, 0.2);
300    }
301
302    #[test]
303    fn test_engine_ext_run_immediate_termination() {
304        let mut engine = MockEngine::default();
305
306        let final_epoch = engine.run(|_| true);
307
308        // Should stop immediately after first epoch
309        assert_eq!(final_epoch.generation, 1);
310        assert_eq!(final_epoch.fitness, 1.0);
311    }
312
313    #[test]
314    fn test_engine_ext_run_zero_generations() {
315        let mut engine = MockEngine::default();
316
317        let final_epoch = engine.run(|epoch| epoch.generation > 0);
318
319        // Should run at least one generation
320        assert_eq!(final_epoch.generation, 1);
321    }
322
323    #[test]
324    fn test_engine_ext_method_chaining() {
325        let mut engine = MockEngine::default();
326
327        // Test that we can call run multiple times on the same engine
328        let epoch1 = engine.run(|epoch| epoch.generation >= 2);
329        assert_eq!(epoch1.generation, 2);
330
331        let epoch2 = engine.run(|epoch| epoch.generation >= 4);
332        assert_eq!(epoch2.generation, 4);
333    }
334}