Skip to main content

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
123type FitnessFn<T> = dyn Fn(T) -> Score + Send + Sync;
124type RawFitnessFn<C> = dyn Fn(&Genotype<C>) -> Score + Send + Sync;
125
126/// A standard implementation of the [Problem] trait for general use.
127/// [EngineProblem] is a generic, base level concrete implementation of the [Problem] trait that is the
128/// default problem used by the engine if none other is specified during building.
129///
130/// We take the [Codec] and the fitness function from the user and simply wrap them into this struct.
131///
132///
133/// # Generic Parameters
134///
135/// - `C`: The [Chromosome] type that represents the genetic material
136/// - `T`: The phenotype type that represents the decoded individual
137///
138/// # Examples
139///
140/// ```rust
141/// use radiate_core::*;
142/// use std::sync::Arc;
143///
144/// // Create a simple fitness function
145/// let fitness_fn = Arc::new(|phenotype: Vec<f32>| {
146///     Score::from(phenotype.iter().cloned().fold(0.0, f32::max))
147/// });
148///
149/// let problem = EngineProblem {
150///     objective: Objective::Single(Optimize::Maximize),
151///     codec: Arc::new(FloatCodec::vector(5, 0.0..1.0)),
152///     fitness_fn: Some(fitness_fn),
153///     raw_fitness_fn: None,
154///};
155/// ```
156pub struct EngineProblem<C, T>
157where
158    C: Chromosome,
159{
160    pub objective: Objective,
161    pub codec: Arc<dyn Codec<C, T>>,
162    pub fitness_fn: Option<Arc<FitnessFn<T>>>,
163    pub raw_fitness_fn: Option<Arc<RawFitnessFn<C>>>,
164}
165
166/// Implementation of [Problem] for [EngineProblem].
167///
168/// This implementation delegates to the contained codec and fitness function,
169/// providing a clean separation of concerns between encoding/decoding logic
170/// and fitness evaluation.
171impl<C: Chromosome, T> Problem<C, T> for EngineProblem<C, T> {
172    fn encode(&self) -> Genotype<C> {
173        self.codec.encode()
174    }
175
176    fn decode(&self, genotype: &Genotype<C>) -> T {
177        self.codec.decode(genotype)
178    }
179
180    fn eval(&self, individual: &Genotype<C>) -> RadiateResult<Score> {
181        let score = if let Some(raw_fn) = &self.raw_fitness_fn {
182            raw_fn(individual)
183        } else if let Some(fitness_fn) = &self.fitness_fn {
184            let phenotype = self.decode(individual);
185            fitness_fn(phenotype)
186        } else {
187            return Err(radiate_err!(
188                Evaluation: "No fitness function defined for EngineProblem"
189            ));
190        };
191
192        if self.objective.validate(&score) {
193            return Ok(score);
194        }
195
196        Err(radiate_err!(
197            Evaluation: "Invalid fitness score {:?} for objective {:?}",
198            score,
199            self.objective
200        ))
201    }
202}
203
204/// Mark [EngineProblem] as thread-safe for parallel execution.
205///
206/// This implementation is safe because:
207/// - `Arc<dyn Codec<C, T>>` is `Send + Sync` when `C` and `T` are `Send + Sync`
208/// - `Arc<dyn Fn(T) -> Score + Send + Sync>` is `Send + Sync` by construction
209/// - The struct contains no interior mutability
210unsafe impl<C: Chromosome, T> Send for EngineProblem<C, T> {}
211unsafe impl<C: Chromosome, T> Sync for EngineProblem<C, T> {}
212
213type BatchFitnessFn<T> = dyn Fn(Vec<T>) -> Vec<Score> + Send + Sync;
214type RawBatchFitnessFn<C> = dyn Fn(Vec<&Genotype<C>>) -> Vec<Score> + Send + Sync;
215
216/// A specialized implementation of the [Problem] trait optimized for batch evaluation.
217///
218/// [BatchEngineProblem] is designed for problems where batch fitness evaluation
219/// is significantly more efficient than individual evaluation. It uses a batch
220/// fitness function that can process multiple phenotypes at once, potentially
221/// leveraging vectorization, shared computations, or parallel processing.
222///
223/// # Generic Parameters
224///
225/// - `C`: The [Chromosome] type that represents the genetic material
226/// - `T`: The phenotype type that represents the decoded individual
227///
228/// # Examples
229///
230/// ```rust
231/// use radiate_core::*;
232/// use std::sync::Arc;
233///
234/// // Create a simple fitness function
235/// let batch_fitness_fn = Arc::new(|phenotypes: Vec<Vec<f32>>| {
236///     phenotypes.iter().map(|p| {
237///         Score::from(p.iter().cloned().fold(0.0, f32::max))
238///     }).collect()
239/// });
240///
241/// let problem = BatchEngineProblem {
242///     objective: Objective::Single(Optimize::Maximize),
243///     codec: Arc::new(FloatCodec::vector(5, 0.0..1.0)),
244///     batch_fitness_fn: Some(batch_fitness_fn),
245///     raw_batch_fitness_fn: None,
246///};
247/// ```
248///
249/// # Use Cases
250///
251/// Use [BatchEngineProblem] when:
252/// - Your fitness function can benefit from vectorization
253/// - There are shared computations between multiple individuals
254/// - The problem domain supports efficient batch processing
255/// - You're evaluating large populations where batch overhead is amortized
256/// - You need parts or the whole of a population to be evaluated together
257pub struct BatchEngineProblem<C, T>
258where
259    C: Chromosome,
260{
261    pub objective: Objective,
262    pub codec: Arc<dyn Codec<C, T>>,
263    pub batch_fitness_fn: Option<Arc<BatchFitnessFn<T>>>,
264    pub raw_batch_fitness_fn: Option<Arc<RawBatchFitnessFn<C>>>,
265}
266
267/// Implementation of [Problem] for [BatchEngineProblem].
268///
269/// This implementation provides both individual and batch evaluation methods.
270/// The individual evaluation method (`eval`) is implemented by wrapping the
271/// phenotype in a single-element slice and calling the batch function, then
272/// extracting the first result. This ensures consistency between individual
273/// and batch evaluation while maintaining the benefits of batch processing.
274impl<C: Chromosome, T> Problem<C, T> for BatchEngineProblem<C, T> {
275    fn encode(&self) -> Genotype<C> {
276        self.codec.encode()
277    }
278
279    fn decode(&self, genotype: &Genotype<C>) -> T {
280        self.codec.decode(genotype)
281    }
282
283    fn eval(&self, individual: &Genotype<C>) -> RadiateResult<Score> {
284        let scores = if let Some(raw_batch_fn) = &self.raw_batch_fitness_fn {
285            raw_batch_fn(vec![individual])
286        } else if let Some(batch_fn) = &self.batch_fitness_fn {
287            let phenotypes = vec![self.decode(individual)];
288            batch_fn(phenotypes)
289        } else {
290            return Err(radiate_err!(
291                Evaluation: "No batch fitness function defined for BatchEngineProblem"
292            ));
293        };
294
295        // Cloning a score is a lightweight operation - the internal of a score is an Arc<[f32]>
296        // This function will likely never be called anyways as we expect `eval_batch` to be used.
297        Ok(scores[0].clone())
298    }
299
300    fn eval_batch(&self, individuals: &[Genotype<C>]) -> RadiateResult<Vec<Score>> {
301        let scores = if let Some(raw_batch_fn) = &self.raw_batch_fitness_fn {
302            raw_batch_fn(individuals.iter().collect())
303        } else if let Some(batch_fn) = &self.batch_fitness_fn {
304            let phenotypes = individuals
305                .iter()
306                .map(|genotype| self.decode(genotype))
307                .collect::<Vec<T>>();
308            batch_fn(phenotypes)
309        } else {
310            return Err(radiate_err!(
311                Evaluation: "No batch fitness function defined for BatchEngineProblem"
312            ));
313        };
314
315        for i in 0..scores.len() {
316            if !self.objective.validate(&scores[i]) {
317                return Err(radiate_err!(
318                    Evaluation: "Invalid fitness score {:?} for objective {:?}",
319                    scores[i],
320                    self.objective
321                ));
322            }
323        }
324
325        Ok(scores)
326    }
327}
328
329/// Mark [BatchEngineProblem] as thread-safe for parallel execution.
330///
331/// This implementation is safe because:
332/// - `Arc<dyn Codec<C, T>>` is `Send + Sync` when `C` and `T` are `Send + Sync`
333/// - `Arc<dyn Fn(&[T]) -> Vec<Score> + Send + Sync>` is `Send + Sync` by construction
334/// - The struct contains no interior mutability
335/// - Batch operations are designed to be thread-safe
336unsafe impl<C: Chromosome, T> Send for BatchEngineProblem<C, T> {}
337unsafe impl<C: Chromosome, T> Sync for BatchEngineProblem<C, T> {}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342    use crate::{Chromosome, Codec, FloatChromosome, FloatGene, Gene, Genotype, Score};
343
344    #[derive(Debug, Clone)]
345    struct MockPhenotype {
346        x: f32,
347        y: f32,
348    }
349
350    struct MockCodec;
351
352    impl Codec<FloatChromosome<f32>, MockPhenotype> for MockCodec {
353        fn encode(&self) -> Genotype<FloatChromosome<f32>> {
354            Genotype::new(vec![
355                FloatChromosome::from(FloatGene::from(1.0)),
356                FloatChromosome::from(FloatGene::from(2.0)),
357            ])
358        }
359
360        fn decode(&self, genotype: &Genotype<FloatChromosome<f32>>) -> MockPhenotype {
361            MockPhenotype {
362                x: *genotype[0].get(0).allele(),
363                y: *genotype[1].get(0).allele(),
364            }
365        }
366    }
367
368    #[test]
369    fn test_engine_problem_basic_functionality() {
370        let fitness_fn =
371            Arc::new(|phenotype: MockPhenotype| Score::from(phenotype.x + phenotype.y));
372
373        let problem = EngineProblem {
374            objective: Objective::default(),
375            codec: Arc::new(MockCodec),
376            fitness_fn: Some(fitness_fn),
377            raw_fitness_fn: None,
378        };
379
380        let genotype = problem.encode();
381        assert_eq!(genotype.len(), 2);
382
383        let phenotype = problem.decode(&genotype);
384        assert_eq!(phenotype.x, 1.0);
385        assert_eq!(phenotype.y, 2.0);
386
387        let fitness = problem.eval(&genotype).unwrap();
388        assert_eq!(fitness.as_f32(), 3.0);
389    }
390
391    #[test]
392    fn test_engine_problem_batch_evaluation() {
393        let fitness_fn =
394            Arc::new(|phenotype: MockPhenotype| Score::from(phenotype.x + phenotype.y));
395
396        let problem = EngineProblem {
397            objective: Objective::default(),
398            codec: Arc::new(MockCodec),
399            fitness_fn: Some(fitness_fn),
400            raw_fitness_fn: None,
401        };
402
403        let genotypes = vec![problem.encode(), problem.encode()];
404
405        let scores = problem.eval_batch(&genotypes).unwrap();
406        assert_eq!(scores.len(), 2);
407        assert_eq!(scores[0].as_f32(), 3.0);
408        assert_eq!(scores[1].as_f32(), 3.0);
409    }
410
411    #[test]
412    fn test_batch_engine_problem_basic_functionality() {
413        let batch_fitness_fn = Arc::new(|phenotypes: Vec<MockPhenotype>| {
414            phenotypes.iter().map(|p| Score::from(p.x * p.y)).collect()
415        });
416
417        let problem = BatchEngineProblem {
418            objective: Objective::default(),
419            codec: Arc::new(MockCodec),
420            batch_fitness_fn: Some(batch_fitness_fn),
421            raw_batch_fitness_fn: None,
422        };
423
424        let genotype = problem.encode();
425        assert_eq!(genotype.len(), 2);
426
427        let phenotype = problem.decode(&genotype);
428        assert_eq!(phenotype.x, 1.0);
429        assert_eq!(phenotype.y, 2.0);
430
431        let fitness = problem.eval(&genotype).unwrap();
432        assert_eq!(fitness.as_f32(), 2.0); // 1.0 * 2.0
433    }
434
435    #[test]
436    fn test_batch_engine_problem_batch_evaluation() {
437        let batch_fitness_fn = Arc::new(|phenotypes: Vec<MockPhenotype>| {
438            phenotypes.iter().map(|p| Score::from(p.x * p.y)).collect()
439        });
440
441        let problem = BatchEngineProblem {
442            objective: Objective::default(),
443            codec: Arc::new(MockCodec),
444            batch_fitness_fn: Some(batch_fitness_fn),
445            raw_batch_fitness_fn: None,
446        };
447
448        let genotypes = vec![problem.encode(), problem.encode()];
449
450        let scores = problem.eval_batch(&genotypes).unwrap();
451        assert_eq!(scores.len(), 2);
452        assert_eq!(scores[0].as_f32(), 2.0); // 1.0 * 2.0
453        assert_eq!(scores[1].as_f32(), 2.0); // 1.0 * 2.0
454    }
455
456    #[test]
457    fn test_consistency_between_eval_and_eval_batch() {
458        let batch_fitness_fn = Arc::new(|phenotypes: Vec<MockPhenotype>| {
459            phenotypes.iter().map(|p| Score::from(p.x * p.y)).collect()
460        });
461
462        let problem = BatchEngineProblem {
463            objective: Objective::default(),
464            codec: Arc::new(MockCodec),
465            batch_fitness_fn: Some(batch_fitness_fn),
466            raw_batch_fitness_fn: None,
467        };
468
469        let genotype = problem.encode();
470
471        let individual_fitness = problem.eval(&genotype).unwrap();
472
473        let batch_scores = problem.eval_batch(&[genotype.clone()]).unwrap();
474        let batch_fitness = &batch_scores[0];
475
476        assert_eq!(individual_fitness.as_f32(), batch_fitness.as_f32());
477    }
478}