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}