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