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 intentially esentially 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 fn run<F>(&mut self, limit: F) -> E::Epoch
165 where
166 F: Fn(&E::Epoch) -> bool;
167}
168
169/// Blanket implementation of [EngineExt] for all types that implement [Engine].
170///
171/// This implementation provides the `run` method to any type that implements
172/// the [Engine] trait, without requiring manual implementation.
173///
174/// # Implementation Details
175///
176/// The `run` method implements a simple loop that:
177/// 1. Calls `self.next()` to advance the engine
178/// 2. Checks the termination condition using the provided closure
179/// 3. Breaks and returns the final epoch when the condition is met
180impl<E> EngineExt<E> for E
181where
182 E: Engine,
183{
184 fn run<F>(&mut self, limit: F) -> E::Epoch
185 where
186 F: Fn(&E::Epoch) -> bool,
187 {
188 loop {
189 match self.next() {
190 Ok(epoch) => {
191 if limit(&epoch) {
192 return epoch;
193 }
194 }
195 Err(e) => {
196 panic!("{e}");
197 }
198 }
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 struct MockEpoch {
208 generation: usize,
209 fitness: f32,
210 }
211
212 #[derive(Default)]
213 struct MockEngine {
214 generation: usize,
215 }
216
217 impl Engine for MockEngine {
218 type Epoch = MockEpoch;
219
220 fn next(&mut self) -> Result<Self::Epoch> {
221 self.generation += 1;
222 Ok(MockEpoch {
223 generation: self.generation,
224 fitness: 1.0 / (self.generation as f32),
225 })
226 }
227 }
228
229 #[test]
230 fn test_engine_next() {
231 let mut engine = MockEngine::default();
232
233 let epoch1 = engine.next().unwrap();
234 assert_eq!(epoch1.generation, 1);
235 assert_eq!(epoch1.fitness, 1.0);
236
237 let epoch2 = engine.next().unwrap();
238 assert_eq!(epoch2.generation, 2);
239 assert_eq!(epoch2.fitness, 0.5);
240 }
241
242 #[test]
243 fn test_engine_ext_run_generation_limit() {
244 let mut engine = MockEngine::default();
245
246 let final_epoch = engine.run(|epoch| epoch.generation >= 3);
247
248 assert_eq!(final_epoch.generation, 3);
249 assert_eq!(final_epoch.fitness, 1.0 / 3.0);
250 }
251
252 #[test]
253 fn test_engine_ext_run_fitness_limit() {
254 let mut engine = MockEngine::default();
255
256 let final_epoch = engine.run(|epoch| epoch.fitness < 0.3);
257
258 // Should stop when fitness drops below 0.3
259 // 1/4 = 0.25, so it should stop at generation 4
260 assert_eq!(final_epoch.generation, 4);
261 assert_eq!(final_epoch.fitness, 0.25);
262 }
263
264 #[test]
265 fn test_engine_ext_run_complex_condition() {
266 let mut engine = MockEngine::default();
267
268 let final_epoch = engine.run(|epoch| epoch.generation >= 5 || epoch.fitness < 0.2);
269
270 // Should stop at generation 5 due to generation limit
271 // (fitness at gen 5 is 0.2, which doesn't meet the fitness condition)
272 assert_eq!(final_epoch.generation, 5);
273 assert_eq!(final_epoch.fitness, 0.2);
274 }
275
276 #[test]
277 fn test_engine_ext_run_immediate_termination() {
278 let mut engine = MockEngine::default();
279
280 let final_epoch = engine.run(|_| true);
281
282 // Should stop immediately after first epoch
283 assert_eq!(final_epoch.generation, 1);
284 assert_eq!(final_epoch.fitness, 1.0);
285 }
286
287 #[test]
288 fn test_engine_ext_run_zero_generations() {
289 let mut engine = MockEngine::default();
290
291 let final_epoch = engine.run(|epoch| epoch.generation > 0);
292
293 // Should run at least one generation
294 assert_eq!(final_epoch.generation, 1);
295 }
296
297 #[test]
298 fn test_engine_ext_method_chaining() {
299 let mut engine = MockEngine::default();
300
301 // Test that we can call run multiple times on the same engine
302 let epoch1 = engine.run(|epoch| epoch.generation >= 2);
303 assert_eq!(epoch1.generation, 2);
304
305 let epoch2 = engine.run(|epoch| epoch.generation >= 4);
306 assert_eq!(epoch2.generation, 4);
307 }
308}