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}