Skip to content

Commit a86f98f

Browse files
committed
Merge branch 'develop'
2 parents e4b0a56 + 3ca48e9 commit a86f98f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+3122
-638
lines changed

Cargo.toml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ exclude = [
5050
]
5151

5252
[workspace.package]
53-
version = "1.2.15"
53+
version = "1.2.16"
5454
edition = "2024"
5555
authors = ["pkalivas <peterkalivas@gmail.com>"]
5656
description = "A Rust library for genetic algorithms and artificial evolution."
@@ -65,17 +65,17 @@ homepage = "https://pkalivas.github.io/radiate/"
6565
rand = "0.9.2"
6666
pyo3 = "0.25.1"
6767
numpy ="0.25.0"
68-
rayon = "1.10.0"
68+
rayon = "1.11.0"
6969
serde = { version = "1.0.219", features = ["derive"] }
70-
serde_json = { version = "1.0.142" }
70+
serde_json = { version = "1.0.143" }
7171
tracing = "0.1"
7272
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
73-
radiate = { version = "1.2.15", path = "crates/radiate", default-features = false }
74-
radiate-core = { version = "1.2.15", path = "crates/radiate-core", default-features = false }
75-
radiate-alters = { version = "1.2.15", path = "crates/radiate-alters", default-features = false }
76-
radiate-selectors = { version = "1.2.15", path = "crates/radiate-selectors", default-features = false }
77-
radiate-engines = { version = "1.2.15", path = "crates/radiate-engines", default-features = false }
78-
radiate-gp = { version = "1.2.15", path = "crates/radiate-gp", default-features = false }
79-
radiate-error = { version = "1.2.15", path = "crates/radiate-error", default-features = false }
80-
radiate-python = { version = "1.2.15", path = "crates/radiate-python", default-features = false }
73+
radiate = { version = "1.2.16", path = "crates/radiate", default-features = false }
74+
radiate-core = { version = "1.2.16", path = "crates/radiate-core", default-features = false }
75+
radiate-alters = { version = "1.2.16", path = "crates/radiate-alters", default-features = false }
76+
radiate-selectors = { version = "1.2.16", path = "crates/radiate-selectors", default-features = false }
77+
radiate-engines = { version = "1.2.16", path = "crates/radiate-engines", default-features = false }
78+
radiate-gp = { version = "1.2.16", path = "crates/radiate-gp", default-features = false }
79+
radiate-error = { version = "1.2.16", path = "crates/radiate-error", default-features = false }
80+
radiate-python = { version = "1.2.16", path = "crates/radiate-python", default-features = false }
8181

crates/radiate-core/src/engine.rs

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,179 @@
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+
12+
/// A trait representing an evolutionary computation engine.
13+
//
14+
/// The [Engine] trait defines the fundamental interface for evolutionary algorithms.
15+
/// Implementors define how the algorithm progresses from one generation/epoch to the
16+
/// next, encapsulating the core evolutionary logic.
17+
///
18+
/// It is intentially esentially an iterator.
19+
///
20+
/// # Generic Parameters
21+
///
22+
/// - `Epoch`: The type representing a single step or generation in the evolutionary process
23+
///
24+
/// # Examples
25+
///
26+
/// ```rust
27+
/// use radiate_core::engine::{Engine, EngineExt};
28+
///
29+
/// #[derive(Default)]
30+
/// struct MyEngine {
31+
/// generation: usize,
32+
/// population: Vec<i32>,
33+
/// }
34+
///
35+
/// #[derive(Debug, Clone)]
36+
/// struct MyEpoch {
37+
/// generation: usize,
38+
/// population_size: usize,
39+
/// }
40+
///
41+
/// impl Engine for MyEngine {
42+
/// type Epoch = MyEpoch;
43+
///
44+
/// fn next(&mut self) -> Self::Epoch {
45+
/// // Perform one generation of evolution
46+
/// // ... evolve population ...
47+
/// self.generation += 1;
48+
///
49+
/// MyEpoch {
50+
/// generation: self.generation,
51+
/// population_size: self.population.len()
52+
/// }
53+
/// }
54+
/// }
55+
///
56+
/// // Use the engine with a termination condition
57+
/// let mut engine = MyEngine::default();
58+
/// let final_epoch = engine.run(|epoch| epoch.generation >= 10);
59+
/// println!("Final generation: {}", final_epoch.generation);
60+
/// ```
61+
///
62+
/// # Design Philosophy
63+
///
64+
/// The [Engine] trait is intentionally minimal, focusing on the core concept of
65+
/// progression through evolutionary time. This allows for maximum flexibility in
66+
/// implementing different evolutionary algorithms while maintaining a consistent
67+
/// interface for execution control.
168
pub trait Engine {
69+
/// The type representing a single epoch or generation in the evolutionary process.
70+
///
71+
/// The epoch type should contain all relevant information about the current
72+
/// state of the evolutionary algorithm, such as:
73+
/// - Generation number
74+
/// - Population statistics
75+
/// - Best fitness values
76+
/// - Convergence metrics
77+
/// - Any other state information needed for monitoring or decision making
278
type Epoch;
79+
80+
/// Advances the engine to the next epoch or generation.
81+
///
82+
/// This method encapsulates one complete iteration of the evolutionary algorithm.
83+
/// It should perform all necessary operations to progress the population from
84+
/// the current state to the next generation, including:
85+
/// - Fitness evaluation
86+
/// - Selection
87+
/// - Reproduction (crossover and mutation)
88+
/// - Population replacement
89+
/// - Any other evolutionary operators
90+
///
91+
/// # Returns
92+
///
93+
/// An instance of `Self::Epoch` representing the new state after the evolution step
94+
///
95+
/// # Side Effects
96+
///
97+
/// This method is mutable for allowance of modification of the internal state of the engine,
98+
/// advancing the evolutionary process. The engine should maintain its state between calls
99+
/// to allow for continuous evolution over multiple generations.
100+
///
101+
/// # Performance
102+
///
103+
/// This method is called repeatedly during execution, so it should be
104+
/// optimized for performance.
3105
fn next(&mut self) -> Self::Epoch;
4106
}
5107

108+
/// Extension trait providing convenient methods for running engines with custom logic.
109+
///
110+
/// `EngineExt` provides additional functionality for engines without requiring
111+
/// changes to the core [Engine] trait. This follows the Rust pattern of using
112+
/// extension traits to add functionality to existing types.
113+
///
114+
/// # Generic Parameters
115+
///
116+
/// - `E`: The engine type that this extension applies to
117+
///
118+
/// # Design Benefits
119+
///
120+
/// - **Separation of Concerns**: Core engine logic is separate from execution control
121+
/// - **Flexibility**: Different termination conditions can be easily implemented
122+
/// - **Reusability**: The same engine can be run with different stopping criteria
123+
/// - **Testability**: Termination logic can be tested independently of engine logic
6124
pub trait EngineExt<E: Engine> {
125+
/// Runs the engine until the specified termination condition is met.
126+
///
127+
/// This method continuously calls `engine.next()` until the provided closure
128+
/// returns `true`, indicating that the termination condition has been satisfied.
129+
/// The final epoch is returned, allowing you to inspect the final state of
130+
/// the evolutionary process.
131+
///
132+
/// # Arguments
133+
///
134+
/// * `limit` - A closure that takes the current epoch and returns `true` when
135+
/// the engine should stop, `false` to continue
136+
///
137+
/// # Returns
138+
///
139+
/// The epoch that satisfied the termination condition
140+
///
141+
/// # Termination Conditions
142+
///
143+
/// Common termination conditions include:
144+
/// - **Generation Limit**: Stop after a fixed number of generations
145+
/// - **Fitness Threshold**: Stop when best fitness reaches a target value
146+
/// - **Convergence**: Stop when population diversity or fitness improvement is minimal
147+
/// - **Time Limit**: Stop after a certain amount of computation time
148+
/// - **Solution Quality**: Stop when a satisfactory solution is found
149+
///
150+
/// # Performance Considerations
151+
///
152+
/// - The termination condition is checked after every epoch, so keep it lightweight
153+
/// - Avoid expensive computations in the termination closure
154+
/// - Consider using early termination for conditions that can be checked incrementally
155+
///
156+
/// # Infinite Loops
157+
///
158+
/// Be careful to ensure that your termination condition will eventually be met,
159+
/// especially when using complex logic. An infinite loop will cause the program
160+
/// to hang indefinitely.
7161
fn run<F>(&mut self, limit: F) -> E::Epoch
8162
where
9163
F: Fn(&E::Epoch) -> bool;
10164
}
11165

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

0 commit comments

Comments
 (0)