radiate_core/
problem.rs

1//! # Problem Abstraction
2//!
3//! This module provides the core problem abstraction for genetic algorithms, defining
4//! how genotypes are encoded, decoded, and evaluated. The [Problem] trait serves as
5//! the central interface that combines encoding/decoding logic with fitness evaluation,
6//! making it easy to implement custom problems while maintaining a consistent API.
7//!
8//! The module provides both the trait definition and concrete implementations:
9//! - **Problem trait**: Core interface for genetic algorithm problems
10//! - **EngineProblem**: Standard implementation using individual fitness functions
11//! - **BatchEngineProblem**: Optimized implementation for batch fitness evaluation
12
13use super::{Chromosome, Codec, Genotype, Score};
14use std::sync::Arc;
15
16/// The core interface for genetic algorithm problems.
17///
18/// The [Problem] trait encapsulates the three essential components of a genetic
19/// algorithm: encoding, decoding, and fitness evaluation. It provides a unified
20/// interface that allows the engine to work with any problem implementation
21/// without needing to understand the specific details of how genotypes are
22/// represented or how fitness is calculated.
23///
24/// # Generic Parameters
25///
26/// - `C`: The chromosome type that represents the genetic material
27/// - `T`: The phenotype type that represents the decoded individual
28///
29/// # Thread Safety
30///
31/// All problems must be `Send + Sync` to support parallel execution across
32/// multiple threads. This ensures safe sharing of problems between worker threads
33/// during fitness evaluation.
34///
35/// # Performance Considerations
36///
37/// - **Encoding**: Called sparingly, so performance is less critical
38/// - **Decoding**: Called frequently during evaluation, should be optimized
39/// - **Fitness Evaluation**: Debatably the most computationally expensive operation across all problem types
40pub trait Problem<C: Chromosome, T>: Send + Sync {
41    /// Creates a new [Genotype] representing the initial state of the problem.
42    /// The returned [Genotype] should represent a valid starting point for evolution.
43    ///
44    /// # Returns
45    ///
46    /// A [Genotype] that can be used as a starting point for evolution
47    ///
48    /// # Note
49    ///
50    /// The encoding should produce diverse genotypes to ensure the initial
51    /// population has sufficient genetic diversity for effective evolution.
52    fn encode(&self) -> Genotype<C>;
53
54    /// Converts a [Genotype] into its corresponding phenotype.
55    ///
56    /// This method transforms the genetic representation into a form that
57    /// can be evaluated by the fitness function. The decoding process should
58    /// be deterministic and efficient, as it's called frequently during
59    /// fitness evaluation.
60    ///
61    /// # Arguments
62    ///
63    /// * [Genotype] - The genotype to decode
64    ///
65    /// # Returns
66    ///
67    /// The decoded phenotype that can be evaluated
68    ///
69    /// # Performance
70    ///
71    /// This method is called for every individual during fitness evaluation,
72    /// so it should be optimized for speed.
73    fn decode(&self, genotype: &Genotype<C>) -> T;
74
75    /// Evaluates the fitness of a single individual.
76    ///
77    /// This method computes the fitness score for a given [Genotype] by first
78    /// decoding it to a phenotype and then applying the fitness function.
79    /// The fitness score indicates how well the individual solves the problem.
80    ///
81    /// # Arguments
82    ///
83    /// * `individual` - The [Genotype] to evaluate
84    ///
85    /// # Returns
86    ///
87    /// A fitness score representing the quality of the individual
88    fn eval(&self, individual: &Genotype<C>) -> Score;
89
90    /// Evaluates the fitness of multiple individuals in a batch.
91    ///
92    /// This method provides an efficient way to evaluate multiple individuals
93    /// at once, which can be more efficient than calling `eval` multiple times,
94    /// especially when the fitness function can benefit from vectorization,
95    /// when there are significant overhead costs per evaluation, or when you need
96    /// access to parts, or the whole, of the population to make informed decisions.
97    ///
98    /// # Arguments
99    ///
100    /// * `individuals` - A slice of genotypes to evaluate
101    ///
102    /// # Returns
103    ///
104    /// A vector of fitness scores, one for each individual
105    ///
106    /// # Default Implementation
107    ///
108    /// The default implementation simply calls `eval` for each individual.
109    /// Override this method if you can provide a more efficient batch
110    /// evaluation strategy.
111    ///
112    /// # Note
113    ///
114    /// The order in which the scores are returned must match the order in which
115    /// the genotypes are provided.
116    fn eval_batch(&self, individuals: &[Genotype<C>]) -> Vec<Score> {
117        individuals.iter().map(|ind| self.eval(ind)).collect()
118    }
119}
120
121/// [EngineProblem] is a generic, base level concrete implementation of the [Problem] trait that is the
122/// default problem used by the engine if none other is specified during building.
123///
124/// We take the [Codec] and the fitness function from the user and simply wrap them into this struct.
125///
126/// # Generic Parameters
127///
128/// - `C`: The [Chromosome] type that represents the genetic material
129/// - `T`: The phenotype type that represents the decoded individual
130///
131/// # Examples
132///
133/// ```rust
134/// use radiate_core::*;
135/// use std::sync::Arc;
136///
137/// // Create a simple fitness function
138/// let fitness_fn = Arc::new(|phenotype: Vec<f32>| {
139///     Score::from(phenotype.iter().cloned().fold(0.0, f32::max))
140/// });
141///
142/// let problem = EngineProblem {
143///     codec: Arc::new(FloatCodec::vector(5, 0.0..1.0)),
144///     fitness_fn,
145/// };
146/// ```
147///
148/// # Thread Safety
149///
150/// This struct is marked as `Send + Sync` to ensure it can be safely shared
151/// across multiple threads during parallel fitness evaluation.
152///
153/// # Performance Characteristics
154///
155/// - **Individual Evaluation**: Each individual is evaluated separately
156/// - **Memory Usage**: Lower memory overhead per evaluation
157/// - **Flexibility**: Easy to implement custom fitness logic
158/// - **Parallelization**: Can utilize multiple threads through the executor
159pub struct EngineProblem<C, T>
160where
161    C: Chromosome,
162{
163    pub codec: Arc<dyn Codec<C, T>>,
164    pub fitness_fn: Arc<dyn Fn(T) -> Score + Send + Sync>,
165}
166
167/// Implementation of [Problem] for [EngineProblem].
168///
169/// This implementation delegates to the contained codec and fitness function,
170/// providing a clean separation of concerns between encoding/decoding logic
171/// and fitness evaluation.
172impl<C: Chromosome, T> Problem<C, T> for EngineProblem<C, T> {
173    fn encode(&self) -> Genotype<C> {
174        self.codec.encode()
175    }
176
177    fn decode(&self, genotype: &Genotype<C>) -> T {
178        self.codec.decode(genotype)
179    }
180
181    fn eval(&self, individual: &Genotype<C>) -> Score {
182        let phenotype = self.decode(individual);
183        (self.fitness_fn)(phenotype)
184    }
185}
186
187/// Mark [EngineProblem] as thread-safe for parallel execution.
188///
189/// This implementation is safe because:
190/// - `Arc<dyn Codec<C, T>>` is `Send + Sync` when `C` and `T` are `Send + Sync`
191/// - `Arc<dyn Fn(T) -> Score + Send + Sync>` is `Send + Sync` by construction
192/// - The struct contains no interior mutability
193unsafe impl<C: Chromosome, T> Send for EngineProblem<C, T> {}
194unsafe impl<C: Chromosome, T> Sync for EngineProblem<C, T> {}
195
196/// A specialized implementation of the [Problem] trait optimized for batch evaluation.
197///
198/// [BatchEngineProblem] is designed for problems where batch fitness evaluation
199/// is significantly more efficient than individual evaluation. It uses a batch
200/// fitness function that can process multiple phenotypes at once, potentially
201/// leveraging vectorization, shared computations, or parallel processing.
202///
203/// # Generic Parameters
204///
205/// - `C`: The [Chromosome] type that represents the genetic material
206/// - `T`: The phenotype type that represents the decoded individual
207///
208/// # Examples
209///
210/// ```rust
211/// use radiate_core::*;
212/// use std::sync::Arc;
213///
214/// // Create a simple fitness function
215/// let batch_fitness_fn = Arc::new(|phenotypes: &[Vec<f32>]| {
216///     phenotypes.iter().map(|p| {
217///         Score::from(p.iter().cloned().fold(0.0, f32::max))
218///     }).collect()
219/// });
220///
221/// let problem = BatchEngineProblem {
222///     codec: Arc::new(FloatCodec::vector(5, 0.0..1.0)),
223///     batch_fitness_fn,
224/// };
225/// ```
226///
227/// # Use Cases
228///
229/// Use [BatchEngineProblem] when:
230/// - Your fitness function can benefit from vectorization
231/// - There are shared computations between multiple individuals
232/// - The problem domain supports efficient batch processing
233/// - You're evaluating large populations where batch overhead is amortized
234/// - You need parts or the whole of a population to be evaluated together
235pub struct BatchEngineProblem<C, T>
236where
237    C: Chromosome,
238{
239    pub codec: Arc<dyn Codec<C, T>>,
240    pub batch_fitness_fn: Arc<dyn Fn(&[T]) -> Vec<Score> + Send + Sync>,
241}
242
243/// Implementation of [Problem] for [BatchEngineProblem].
244///
245/// This implementation provides both individual and batch evaluation methods.
246/// The individual evaluation method (`eval`) is implemented by wrapping the
247/// phenotype in a single-element slice and calling the batch function, then
248/// extracting the first result. This ensures consistency between individual
249/// and batch evaluation while maintaining the benefits of batch processing.
250impl<C: Chromosome, T> Problem<C, T> for BatchEngineProblem<C, T> {
251    fn encode(&self) -> Genotype<C> {
252        self.codec.encode()
253    }
254
255    fn decode(&self, genotype: &Genotype<C>) -> T {
256        self.codec.decode(genotype)
257    }
258
259    fn eval(&self, individual: &Genotype<C>) -> Score {
260        let phenotype = self.decode(individual);
261        let scores = (self.batch_fitness_fn)(&[phenotype]);
262
263        // Cloning a score is a lightweight operation - the internal of a score is a Arc<[f32]>
264        // This function will likely never be called anyways as we expect `eval_batch` to be used.
265        scores[0].clone()
266    }
267
268    fn eval_batch(&self, individuals: &[Genotype<C>]) -> Vec<Score> {
269        let phenotypes = individuals
270            .iter()
271            .map(|genotype| self.decode(genotype))
272            .collect::<Vec<T>>();
273        (self.batch_fitness_fn)(&phenotypes)
274    }
275}
276
277/// Mark [BatchEngineProblem] as thread-safe for parallel execution.
278///
279/// This implementation is safe because:
280/// - `Arc<dyn Codec<C, T>>` is `Send + Sync` when `C` and `T` are `Send + Sync`
281/// - `Arc<dyn Fn(&[T]) -> Vec<Score> + Send + Sync>` is `Send + Sync` by construction
282/// - The struct contains no interior mutability
283/// - Batch operations are designed to be thread-safe
284unsafe impl<C: Chromosome, T> Send for BatchEngineProblem<C, T> {}
285unsafe impl<C: Chromosome, T> Sync for BatchEngineProblem<C, T> {}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use crate::{Chromosome, Codec, FloatChromosome, FloatGene, Gene, Genotype, Score};
291
292    // Mock phenotype for testing
293    #[derive(Debug, Clone)]
294    struct MockPhenotype {
295        x: f32,
296        y: f32,
297    }
298
299    // Mock codec for testing
300    struct MockCodec;
301
302    impl Codec<FloatChromosome, MockPhenotype> for MockCodec {
303        fn encode(&self) -> Genotype<FloatChromosome> {
304            Genotype::new(vec![
305                FloatChromosome::from(FloatGene::from(1.0)),
306                FloatChromosome::from(FloatGene::from(2.0)),
307            ])
308        }
309
310        fn decode(&self, genotype: &Genotype<FloatChromosome>) -> MockPhenotype {
311            MockPhenotype {
312                x: *genotype[0].get(0).allele(),
313                y: *genotype[1].get(0).allele(),
314            }
315        }
316    }
317
318    #[test]
319    fn test_engine_problem_basic_functionality() {
320        let fitness_fn =
321            Arc::new(|phenotype: MockPhenotype| Score::from(phenotype.x + phenotype.y));
322
323        let problem = EngineProblem {
324            codec: Arc::new(MockCodec),
325            fitness_fn,
326        };
327
328        // Test encoding
329        let genotype = problem.encode();
330        assert_eq!(genotype.len(), 2);
331
332        // Test decoding
333        let phenotype = problem.decode(&genotype);
334        assert_eq!(phenotype.x, 1.0);
335        assert_eq!(phenotype.y, 2.0);
336
337        // Test evaluation
338        let fitness = problem.eval(&genotype);
339        assert_eq!(fitness.as_f32(), 3.0);
340    }
341
342    #[test]
343    fn test_engine_problem_batch_evaluation() {
344        let fitness_fn =
345            Arc::new(|phenotype: MockPhenotype| Score::from(phenotype.x + phenotype.y));
346
347        let problem = EngineProblem {
348            codec: Arc::new(MockCodec),
349            fitness_fn,
350        };
351
352        let genotypes = vec![problem.encode(), problem.encode()];
353
354        let scores = problem.eval_batch(&genotypes);
355        assert_eq!(scores.len(), 2);
356        assert_eq!(scores[0].as_f32(), 3.0);
357        assert_eq!(scores[1].as_f32(), 3.0);
358    }
359
360    #[test]
361    fn test_batch_engine_problem_basic_functionality() {
362        let batch_fitness_fn = Arc::new(|phenotypes: &[MockPhenotype]| {
363            phenotypes.iter().map(|p| Score::from(p.x * p.y)).collect()
364        });
365
366        let problem = BatchEngineProblem {
367            codec: Arc::new(MockCodec),
368            batch_fitness_fn,
369        };
370
371        // Test encoding
372        let genotype = problem.encode();
373        assert_eq!(genotype.len(), 2);
374
375        // Test decoding
376        let phenotype = problem.decode(&genotype);
377        assert_eq!(phenotype.x, 1.0);
378        assert_eq!(phenotype.y, 2.0);
379
380        // Test individual evaluation
381        let fitness = problem.eval(&genotype);
382        assert_eq!(fitness.as_f32(), 2.0); // 1.0 * 2.0
383    }
384
385    #[test]
386    fn test_batch_engine_problem_batch_evaluation() {
387        let batch_fitness_fn = Arc::new(|phenotypes: &[MockPhenotype]| {
388            phenotypes.iter().map(|p| Score::from(p.x * p.y)).collect()
389        });
390
391        let problem = BatchEngineProblem {
392            codec: Arc::new(MockCodec),
393            batch_fitness_fn,
394        };
395
396        let genotypes = vec![problem.encode(), problem.encode()];
397
398        let scores = problem.eval_batch(&genotypes);
399        assert_eq!(scores.len(), 2);
400        assert_eq!(scores[0].as_f32(), 2.0); // 1.0 * 2.0
401        assert_eq!(scores[1].as_f32(), 2.0); // 1.0 * 2.0
402    }
403
404    #[test]
405    fn test_consistency_between_eval_and_eval_batch() {
406        let batch_fitness_fn = Arc::new(|phenotypes: &[MockPhenotype]| {
407            phenotypes.iter().map(|p| Score::from(p.x * p.y)).collect()
408        });
409
410        let problem = BatchEngineProblem {
411            codec: Arc::new(MockCodec),
412            batch_fitness_fn,
413        };
414
415        let genotype = problem.encode();
416
417        // Individual evaluation
418        let individual_fitness = problem.eval(&genotype);
419
420        // Batch evaluation with single individual
421        let batch_scores = problem.eval_batch(&[genotype.clone()]);
422        let batch_fitness = &batch_scores[0];
423
424        // Results should be consistent
425        assert_eq!(individual_fitness.as_f32(), batch_fitness.as_f32());
426    }
427}