radiate_core/
evaluator.rs

1//! # Fitness Evaluators
2//!
3//! This module provides fitness evaluation strategies for genetic algorithms, offering
4//! both individual and batch evaluation approaches. Evaluators are responsible for
5//! computing fitness scores for individuals in the population using the provided problem.
6//!
7//! The module provides two main evaluation strategies:
8//! - **FitnessEvaluator**: Evaluates individuals one at a time
9//! - **BatchFitnessEvaluator**: Evaluates individuals in batches for better performance
10//!
11//! Both evaluators support parallel execution through the executor system and integrate
12//! seamlessly with the ecosystem and problem abstractions.
13
14use crate::Result;
15use crate::{Chromosome, Ecosystem, Executor, Problem};
16use std::sync::Arc;
17
18/// A trait for evaluating fitness of individuals in the ecosystem.
19///
20/// The [Evaluator] trait defines the interface for fitness evaluation strategies.
21/// Implementors can define different approaches to computing fitness scores,
22/// such as individual evaluation, batch evaluation, or specialized evaluation
23/// strategies for specific problem domains.
24///
25/// The two main implementations provided are:
26/// - [FitnessEvaluator]: Evaluates individuals one at a time
27/// - [BatchFitnessEvaluator]: Evaluates individuals in batches
28/// Custom evaluators can be created and used, however, take special note on how the
29/// members of the ecosystem are accessed and modified, (taken out of the phenotype then restored).
30/// This is important to ensure memory safety and avoid unnecessary cloning of genotypes.
31///
32/// # Generic Parameters
33/// - `C`: The chromosome type that represents the genetic material
34/// - `T`: The target type that the problem operates on
35pub trait Evaluator<C: Chromosome, T>: Send + Sync {
36    /// Evaluates the fitness of unevaluated individuals in the ecosystem.
37    ///
38    /// This method processes individuals that don't have fitness scores yet,
39    /// evaluates them using the provided problem, and updates the ecosystem
40    /// with the computed scores.
41    ///
42    /// # Arguments
43    ///
44    /// * `ecosystem` - The ecosystem containing the population
45    /// * `problem` - The problem instance used to evaluate fitness
46    ///
47    /// # Returns
48    ///
49    /// The number of individuals that were evaluated during this call
50    fn eval(&self, ecosystem: &mut Ecosystem<C>, problem: Arc<dyn Problem<C, T>>) -> Result<usize>;
51}
52
53/// A fitness evaluator that evaluates individuals one at a time.
54///
55/// `FitnessEvaluator` processes individuals individually, which is suitable for
56/// problems where individual evaluation is the most efficient approach or when
57/// you need fine-grained control over the evaluation process.
58///
59/// # Performance Characteristics
60///
61/// - **Individual Processing**: Each individual is evaluated separately
62/// - **Parallelization**: Can utilize multiple threads through the executor
63/// - **Flexibility**: Easy to implement custom evaluation logic
64pub struct FitnessEvaluator {
65    executor: Arc<Executor>,
66}
67
68impl FitnessEvaluator {
69    /// Creates a new fitness evaluator with the specified executor.
70    ///
71    /// # Arguments
72    ///
73    /// * `executor` - The executor to use for running fitness evaluations
74    ///
75    /// # Returns
76    ///
77    /// A new `FitnessEvaluator` instance
78    ///
79    /// # Note
80    ///
81    /// Choose an executor that matches your performance requirements:
82    /// - [Executor::Serial] for single-threaded execution
83    /// - [Executor::ThreadPool(n)] for parallel execution with n worker threads
84    pub fn new(executor: Arc<Executor>) -> Self {
85        Self { executor }
86    }
87}
88
89/// Implementation of [Evaluator] for [FitnessEvaluator].
90///
91/// This implementation evaluates individuals one at a time, collecting unevaluated
92/// individuals and processing them through the executor system.
93///
94/// # Algorithm
95///
96/// 1. **Collect Unevaluated Individuals**: Find individuals without fitness scores
97/// 2. **Create Evaluation Jobs**: Package genotypes and indices into jobs
98/// 3. **Execute Jobs**: Run evaluations through the executor
99/// 4. **Update Ecosystem**: Store computed scores and restore genotypes
100///
101/// # Performance Optimizations
102///
103/// - **Efficient Job Creation**: Jobs are created with minimal allocation
104/// - **Batch Execution**: Multiple jobs are submitted to the executor at once
105/// - **Memory Reuse**: Genotypes are restored to avoid unnecessary cloning
106impl<C: Chromosome, T> Evaluator<C, T> for FitnessEvaluator
107where
108    C: Chromosome + 'static,
109    T: 'static,
110{
111    #[inline]
112    fn eval(&self, ecosystem: &mut Ecosystem<C>, problem: Arc<dyn Problem<C, T>>) -> Result<usize> {
113        let mut jobs = Vec::new();
114        let len = ecosystem.population.len();
115        for idx in 0..len {
116            if ecosystem.population[idx].score().is_none() {
117                let geno = ecosystem.population[idx].take_genotype()?;
118                jobs.push((idx, geno));
119            }
120        }
121
122        let results = self.executor.execute_batch(
123            jobs.into_iter()
124                .map(|(idx, geno)| {
125                    let problem = Arc::clone(&problem);
126                    move || {
127                        let score = problem.eval(&geno);
128                        (idx, score, geno)
129                    }
130                })
131                .collect::<Vec<_>>(),
132        );
133
134        let count = results.len();
135        for result in results {
136            let (idx, score, genotype) = result;
137            ecosystem.population[idx].set_score(Some(score?));
138            ecosystem.population[idx].set_genotype(genotype);
139        }
140
141        Ok(count)
142    }
143}
144
145/// Default implementation for [FitnessEvaluator].
146///
147/// Creates a fitness evaluator with a serial executor, which is suitable for
148/// single-threaded applications or when parallel execution is not needed.
149///
150/// # Note
151///
152/// The default executor is [Executor::Serial], which processes evaluations
153/// sequentially. For better performance with large populations, consider using
154/// [variant@Executor::ThreadPool(n)] with an appropriate number of worker threads
155/// or [Executor::WorkerPool] (rayon feature required).
156impl Default for FitnessEvaluator {
157    fn default() -> Self {
158        Self {
159            executor: Arc::new(Executor::Serial),
160        }
161    }
162}
163
164/// A fitness evaluator that evaluates individuals in batches for improved performance or
165/// for when you need access to parts or the whole of an unevaluated population in order
166/// to compute fitness.
167///
168/// [BatchFitnessEvaluator] groups individuals into batches and evaluates them
169/// together, which can be more efficient than individual evaluation, especially
170/// when the problem supports batch evaluation or when there are significant
171/// overhead costs per evaluation.
172///
173/// # Performance Characteristics
174///
175/// - **Batch Processing**: Multiple individuals evaluated together
176/// - **Reduced Overhead**: Sometimes lower per-individual evaluation cost
177///
178/// # Use Cases
179///
180/// Use [BatchFitnessEvaluator] when:
181/// - The problem supports efficient batch evaluation
182/// - You have a large population that benefits from parallelization
183/// - Individual evaluation overhead is significant
184/// - You need access to parts or the whole of an unevaluated population in order to compute fitness.
185///
186/// # Batch Size Calculation
187///
188/// In order to ensure the optimal distribution of work across available threads,
189/// batch size is automatically calculated based on the number of workers
190/// in the executor and the total number of individuals to evaluate:
191///
192/// ```text
193/// batch_size = (total_individuals + num_workers - 1) / num_workers
194/// ```
195/// So, for example, if you have 100 individuals and 4 workers, the batch size would be:
196///
197/// ```text
198/// batch_size = (100 + 4 - 1) / 4 = 25
199/// ```
200///
201/// But, if your executor is [Executor::Serial], then the batch size will simply be the total number of
202/// individuals to evaluate, resulting in a single batch.
203pub struct BatchFitnessEvaluator {
204    executor: Arc<Executor>,
205}
206
207impl BatchFitnessEvaluator {
208    /// Creates a new batch fitness evaluator with the specified executor.
209    ///
210    /// # Arguments
211    ///
212    /// * `executor` - The executor to use for running batch evaluations
213    ///
214    /// # Returns
215    ///
216    /// A new [BatchFitnessEvaluator] instance
217    ///
218    /// # Note
219    ///
220    /// Batch evaluation works best with parallel executors that have multiple
221    /// worker threads. The evaluator will automatically divide work into optimal
222    /// batch sizes based on the number of available workers.
223    pub fn new(executor: Arc<Executor>) -> Self {
224        Self { executor }
225    }
226}
227
228/// Implementation of [Evaluator] for [BatchFitnessEvaluator].
229///
230/// This implementation groups individuals into batches and evaluates them
231/// together, which can significantly improve performance for large populations
232/// or when the problem supports efficient batch evaluation.
233///
234/// # Algorithm
235///
236/// The algorithm largely follows the same steps as `FitnessEvaluator`:
237/// 1. **Collect Unevaluated Individuals**: Find individuals without fitness scores
238/// 2. **Calculate Batch Size**: Determine optimal batch size based on worker count
239/// 3. **Create Batches**: Group individuals into batches for parallel processing
240/// 4. **Execute Batches**: Run batch evaluations through the executor
241/// 5. **Update Ecosystem**: Store computed scores and restore genotypes
242///
243/// # Batch Size Optimization
244///
245/// The batch size is calculated to ensure optimal work distribution:
246/// - **Small Batches**: Ensure all workers have work to do
247/// - **Large Batches**: Minimize overhead from job creation and distribution
248/// - **Balanced Distribution**: Work is evenly distributed across available threads
249///
250/// # Performance Optimizations
251///
252/// - **Efficient Batching**: Batches are created with minimal memory allocation
253/// - **Parallel Execution**: Multiple batches are processed simultaneously
254/// - **Memory Management**: Genotypes are efficiently restored after evaluation
255impl<C: Chromosome, T> Evaluator<C, T> for BatchFitnessEvaluator
256where
257    C: Chromosome + 'static,
258    T: 'static,
259{
260    #[inline]
261    fn eval(&self, ecosystem: &mut Ecosystem<C>, problem: Arc<dyn Problem<C, T>>) -> Result<usize> {
262        let mut pairs = Vec::new();
263        let len = ecosystem.population.len();
264        for idx in 0..len {
265            if ecosystem.population[idx].score().is_none() {
266                let geno = ecosystem.population[idx].take_genotype()?;
267                pairs.push((idx, geno));
268            }
269        }
270
271        let num_workers = self.executor.num_workers();
272        let batch_size = (pairs.len() + num_workers - 1) / num_workers;
273
274        if pairs.is_empty() || batch_size == 0 {
275            return Ok(0);
276        }
277
278        let mut batches = Vec::with_capacity(num_workers);
279
280        while !pairs.is_empty() {
281            let take = pairs.len().min(batch_size);
282
283            let mut batch_indices = Vec::with_capacity(take);
284            let mut batch_genotypes = Vec::with_capacity(take);
285
286            // drain from the end of pairs vector to avoid O(n^2) complexity
287            for (idx, geno) in pairs.drain(pairs.len() - take..) {
288                batch_indices.push(idx);
289                batch_genotypes.push(geno);
290            }
291
292            batches.push((batch_indices, batch_genotypes));
293        }
294
295        let results = self.executor.execute_batch(
296            batches
297                .into_iter()
298                .map(|batch| {
299                    let problem = Arc::clone(&problem);
300                    move || {
301                        let scores = problem.eval_batch(&batch.1);
302                        (batch.0, scores, batch.1)
303                    }
304                })
305                .collect(),
306        );
307
308        let mut count = 0;
309        for (indices, scores, genotypes) in results {
310            count += indices.len();
311            let score_genotype_iter = scores?.into_iter().zip(genotypes.into_iter());
312            for (i, (score, genotype)) in score_genotype_iter.enumerate() {
313                let idx = indices[i];
314                ecosystem.population[idx].set_score(Some(score));
315                ecosystem.population[idx].set_genotype(genotype);
316            }
317        }
318
319        Ok(count)
320    }
321}