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