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#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub enum EngineState {
19 PreStart,
20 Running,
21 Paused,
22 Stopped,
23}
24
25/// A trait representing an evolutionary computation engine.
26///
27/// The [Engine] trait defines the fundamental interface for evolutionary algorithms.
28/// Implementors define how the algorithm progresses from one generation/epoch to the
29/// next, encapsulating the core evolutionary logic.
30///
31/// It is intentionally essentially an iterator.
32///
33/// # Generic Parameters
34///
35/// - `Epoch`: The type representing a single step or generation in the evolutionary process
36///
37/// # Examples
38///
39/// ```rust
40/// use radiate_core::engine::{Engine, EngineExt, EngineState};
41/// use radiate_error::RadiateError;
42///
43/// #[derive(Default)]
44/// struct MyEngine {
45/// generation: usize,
46/// population: Vec<i32>,
47/// }
48///
49/// #[derive(Debug, Clone)]
50/// struct MyEpoch {
51/// generation: usize,
52/// population_size: usize,
53/// }
54///
55/// impl Engine for MyEngine {
56/// type Epoch = MyEpoch;
57/// type Ctx = ();
58///
59/// fn context(&self) -> &Self::Ctx {
60/// &()
61/// }
62///
63/// fn epoch(&self) -> Self::Epoch {
64/// MyEpoch {
65/// generation: self.generation,
66/// population_size: self.population.len(),
67/// }
68/// }
69///
70/// fn state(&self) -> EngineState {
71/// // Return the current state of the engine
72/// EngineState::Running
73/// }
74///
75/// fn step(&mut self) -> Result<(), RadiateError> {
76/// // Perform one generation of evolution
77/// // ... evolve population ...
78/// self.generation += 1;
79/// Ok(())
80/// }
81/// }
82///
83/// // Use the engine with a termination condition
84/// let mut engine = MyEngine::default();
85/// let final_epoch = engine.run(|epoch| epoch.generation >= 10);
86/// println!("Final generation: {}", final_epoch.generation);
87/// ```
88///
89/// # Design Philosophy
90///
91/// The [Engine] trait is intentionally minimal, focusing on the core concept of
92/// progression through evolutionary time. This allows for maximum flexibility in
93/// implementing different evolutionary algorithms while maintaining a small, consistent
94/// interface for execution control.
95pub trait Engine {
96 /// The type representing a single epoch or generation in the evolutionary process.
97 ///
98 /// The epoch type should contain all relevant information about the current
99 /// state of the evolutionary algorithm, such as:
100 /// - Generation number
101 /// - Population statistics
102 /// - Best fitness values
103 /// - Convergence metrics
104 /// - Any other state information needed for monitoring or decision-making
105 type Epoch;
106 /// The type representing the context of the engine, which may include configuration,
107 /// state, and other relevant information. This allows external systems to inspect
108 /// the engine's state without needing to clone or modify it.
109 type Ctx;
110
111 /// Returns a reference to the current context of the engine. This is meant to
112 /// provide a read-only view of the engine's internal state, to allow external systems
113 /// close to the engine level, to inspect the engine without cloning anything.
114 fn context(&self) -> &Self::Ctx;
115
116 /// Returns an epoch of the engine, which is intended to be a snapshot of the current state, or
117 /// the current context. The `Epoch` here can be given to iterators, callers, or anyone else who needs it.
118 /// Essentially to say, this shouldn't borrow anything from the engine, but instead be an owned snapshot
119 /// of the engine
120 fn epoch(&self) -> Self::Epoch;
121
122 /// Advances the engine by one step, performing the necessary computations to progress
123 /// the evolutionary algorithm. This may include evaluating fitness, selecting individuals,
124 /// applying genetic operators, and updating the population. This intentionally does not return anything,
125 /// that is left up to the `epoch()` method or the `next()` method. It's done this way so we
126 /// can advance the engine without having to clone or create any new data, and possibly perform operations
127 /// outside of the engine which don't require a snapshot of the engine state.
128 fn step(&mut self) -> Result<()>;
129
130 /// Starts the engine, initializing any necessary state or resources.
131 /// This is typically called before entering the main execution loop.
132 fn start(&mut self) {}
133
134 /// Stops the engine, performing any necessary cleanup or finalization.
135 /// This is typically called after exiting the main execution loop.
136 fn stop(&mut self) {}
137
138 /// Returns the current state of the engine.
139 /// This allows external systems to query the engine's status without modifying it.
140 /// This lets the [Engine] act as a state machine, while external systems can query
141 /// the current state of the engine.
142 fn state(&self) -> EngineState;
143}
144
145pub trait EngineStream: Engine {
146 type View<'a>
147 where
148 Self: 'a;
149
150 fn run<F>(self, limit: F) -> Result<Self::Epoch>
151 where
152 F: Fn(Self::View<'_>) -> bool + 'static;
153}
154
155/// Extension trait providing convenient methods for running engines with custom logic.
156///
157/// `EngineExt` provides additional functionality for engines without requiring
158/// changes to the core [Engine] trait. This follows the Rust pattern of using
159/// extension traits to add functionality to existing types.
160///
161/// # Generic Parameters
162///
163/// - `E`: The engine type that this extension applies to
164///
165/// # Design Benefits
166///
167/// - **Separation of Concerns**: Core engine logic is separate from execution control
168/// - **Flexibility**: Different termination conditions can be easily implemented
169/// - **Reusability**: The same engine can be run with different stopping criteria
170/// - **Testability**: Termination logic can be tested independently of engine logic
171pub trait EngineExt<E: Engine> {
172 /// Runs the engine until the specified termination condition is met.
173 ///
174 /// This method continuously calls `engine.next()` until the provided closure
175 /// returns `true`, indicating that the termination condition has been satisfied.
176 /// The final epoch is returned, allowing you to inspect the final state of
177 /// the evolutionary process.
178 ///
179 /// # Arguments
180 ///
181 /// * `limit` - A closure that takes the current epoch and returns `true` when
182 /// the engine should stop, `false` to continue
183 ///
184 /// # Returns
185 ///
186 /// The epoch that satisfied the termination condition
187 ///
188 /// # Termination Conditions
189 ///
190 /// Common termination conditions include:
191 /// - **Generation Limit**: Stop after a fixed number of generations
192 /// - **Fitness Threshold**: Stop when best fitness reaches a target value
193 /// - **Convergence**: Stop when population diversity or fitness improvement is minimal
194 /// - **Time Limit**: Stop after a certain amount of computation time
195 /// - **Solution Quality**: Stop when a satisfactory solution is found
196 ///
197 /// # Performance Considerations
198 ///
199 /// - The termination condition is checked after every epoch, so keep it lightweight
200 /// - Avoid expensive computations in the termination closure
201 /// - Consider using early termination for conditions that can be checked incrementally
202 ///
203 /// # Infinite Loops
204 ///
205 /// Be careful to ensure that your termination condition will eventually be met,
206 /// especially when using complex logic. An infinite loop will cause the program
207 /// to hang indefinitely.
208 #[deprecated(
209 since = "1.3.1",
210 note = "Use the `EngineStream` trait impl instead, which provides a more flexible and \
211 efficient way to run engines with custom termination conditions. Instead of an `E::Epoch` being \
212 given to the fn, a `GenerationView<'a, C, T>` is provided instead which is much more efficient."
213 )]
214 fn run<F>(&mut self, limit: F) -> E::Epoch
215 where
216 F: Fn(&E::Epoch) -> bool;
217}
218
219/// Blanket implementation of [EngineExt] for all types that implement [Engine].
220///
221/// This implementation provides the `run` method to any type that implements
222/// the [Engine] trait, without requiring manual implementation.
223///
224/// # Implementation Details
225///
226/// The `run` method implements a simple loop that:
227/// 1. Calls `self.next()` to advance the engine
228/// 2. Checks the termination condition using the provided closure
229/// 3. Breaks and returns the final epoch when the condition is met
230impl<E> EngineExt<E> for E
231where
232 E: Engine,
233{
234 fn run<F>(&mut self, limit: F) -> E::Epoch
235 where
236 F: Fn(&E::Epoch) -> bool,
237 {
238 loop {
239 match self.step().map(|_| self.epoch()) {
240 Ok(epoch) => {
241 if limit(&epoch) {
242 return epoch;
243 }
244 }
245 Err(e) => {
246 panic!("{e}");
247 }
248 }
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 struct MockEpoch {
258 generation: usize,
259 fitness: f32,
260 }
261
262 #[derive(Default)]
263 struct MockEngine {
264 generation: usize,
265 }
266
267 impl Engine for MockEngine {
268 type Epoch = MockEpoch;
269 type Ctx = ();
270
271 fn context(&self) -> &Self::Ctx {
272 &()
273 }
274
275 fn epoch(&self) -> Self::Epoch {
276 MockEpoch {
277 generation: self.generation,
278 fitness: 1.0 / (self.generation as f32),
279 }
280 }
281
282 fn step(&mut self) -> Result<()> {
283 self.generation += 1;
284 Ok(())
285 }
286
287 fn state(&self) -> EngineState {
288 EngineState::Running
289 }
290 }
291
292 impl EngineStream for MockEngine {
293 type View<'a>
294 = &'a MockEpoch
295 where
296 Self: 'a;
297
298 fn run<F>(mut self, limit: F) -> Result<Self::Epoch>
299 where
300 F: Fn(Self::View<'_>) -> bool + 'static,
301 {
302 loop {
303 match self.step().map(|_| self.epoch()) {
304 Ok(epoch) => {
305 if limit(&epoch) {
306 return Ok(epoch);
307 }
308 }
309 Err(e) => {
310 return Err(e);
311 }
312 }
313 }
314 }
315 }
316
317 #[test]
318 fn test_engine_next() {
319 let mut engine = MockEngine::default();
320
321 let epoch1 = engine.step().map(|_| engine.epoch()).unwrap();
322 assert_eq!(epoch1.generation, 1);
323 assert_eq!(epoch1.fitness, 1.0);
324
325 let epoch2 = engine.step().map(|_| engine.epoch()).unwrap();
326 assert_eq!(epoch2.generation, 2);
327 assert_eq!(epoch2.fitness, 0.5);
328 }
329
330 #[test]
331 fn test_engine_ext_run_generation_limit() {
332 let engine = MockEngine::default();
333
334 let final_epoch = engine.run(|epoch| epoch.generation >= 3).unwrap();
335
336 assert_eq!(final_epoch.generation, 3);
337 assert_eq!(final_epoch.fitness, 1.0 / 3.0);
338 }
339
340 #[test]
341 fn test_engine_ext_run_fitness_limit() {
342 let engine = MockEngine::default();
343
344 let final_epoch = engine.run(|epoch| epoch.fitness < 0.3).unwrap();
345
346 // Should stop when fitness drops below 0.3
347 // 1/4 = 0.25, so it should stop at generation 4
348 assert_eq!(final_epoch.generation, 4);
349 assert_eq!(final_epoch.fitness, 0.25);
350 }
351
352 #[test]
353 fn test_engine_ext_run_complex_condition() {
354 let engine = MockEngine::default();
355
356 let final_epoch = engine
357 .run(|epoch| epoch.generation >= 5 || epoch.fitness < 0.2)
358 .unwrap();
359
360 // Should stop at generation 5 due to generation limit
361 // (fitness at gen 5 is 0.2, which doesn't meet the fitness condition)
362 assert_eq!(final_epoch.generation, 5);
363 assert_eq!(final_epoch.fitness, 0.2);
364 }
365
366 #[test]
367 fn test_engine_ext_run_immediate_termination() {
368 let engine = MockEngine::default();
369
370 let final_epoch = engine.run(|_| true).unwrap();
371
372 // Should stop immediately after first epoch
373 assert_eq!(final_epoch.generation, 1);
374 assert_eq!(final_epoch.fitness, 1.0);
375 }
376
377 #[test]
378 fn test_engine_ext_run_zero_generations() {
379 let engine = MockEngine::default();
380
381 let final_epoch = engine.run(|epoch| epoch.generation > 0).unwrap();
382
383 // Should run at least one generation
384 assert_eq!(final_epoch.generation, 1);
385 }
386}