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}