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