Skip to main content

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    /// # Note
72    ///
73    /// Choose an executor that matches your performance requirements:
74    /// - [Executor::Serial] for single-threaded execution
75    /// - [Executor::ThreadPool(n)] for parallel execution with n worker threads
76    pub fn new(executor: Arc<Executor>) -> Self {
77        Self { executor }
78    }
79}
80
81/// Implementation of [Evaluator] for [FitnessEvaluator].
82///
83/// This implementation evaluates individuals one at a time, collecting unevaluated
84/// individuals and processing them through the executor system.
85///
86/// # Algorithm
87///
88/// 1. **Collect Unevaluated Individuals**: Find individuals without fitness scores
89/// 2. **Create Evaluation Jobs**: Package genotypes and indices into jobs
90/// 3. **Execute Jobs**: Run evaluations through the executor
91/// 4. **Update Ecosystem**: Store computed scores and restore genotypes
92///
93/// # Performance Optimizations
94///
95/// - **Efficient Job Creation**: Jobs are created with minimal allocation
96/// - **Batch Execution**: Multiple jobs are submitted to the executor at once
97/// - **Memory Reuse**: Genotypes are restored to avoid unnecessary cloning
98impl<C: Chromosome, T> Evaluator<C, T> for FitnessEvaluator
99where
100    C: Chromosome + 'static,
101    T: 'static,
102{
103    #[inline]
104    fn eval(&self, ecosystem: &mut Ecosystem<C>, problem: Arc<dyn Problem<C, T>>) -> Result<usize> {
105        let mut jobs = Vec::new();
106        let len = ecosystem.population.len();
107        for idx in 0..len {
108            if ecosystem.population[idx].score().is_none() {
109                let geno = ecosystem.population[idx].take_genotype()?;
110                jobs.push((idx, geno));
111            }
112        }
113
114        let results = self.executor.execute_batch(
115            jobs.into_iter()
116                .map(|(idx, geno)| {
117                    let problem = Arc::clone(&problem);
118                    move || {
119                        let score = problem.eval(&geno);
120                        (idx, score, geno)
121                    }
122                })
123                .collect::<Vec<_>>(),
124        );
125
126        let count = results.len();
127        for result in results {
128            let (idx, score, genotype) = result;
129            ecosystem.population[idx].set_score(Some(score?));
130            ecosystem.population[idx].set_genotype(genotype);
131        }
132
133        Ok(count)
134    }
135}
136
137/// Default implementation for [FitnessEvaluator].
138///
139/// Creates a fitness evaluator with a serial executor, which is suitable for
140/// single-threaded applications or when parallel execution is not needed.
141///
142/// # Note
143///
144/// The default executor is [Executor::Serial], which processes evaluations
145/// sequentially. For better performance with large populations, consider using
146/// [variant@Executor::ThreadPool(n)] with an appropriate number of worker threads
147/// or [Executor::WorkerPool] (rayon feature required).
148impl Default for FitnessEvaluator {
149    fn default() -> Self {
150        Self {
151            executor: Arc::new(Executor::Serial),
152        }
153    }
154}
155
156/// A fitness evaluator that evaluates individuals in batches for improved performance or
157/// for when you need access to parts or the whole of an unevaluated population in order
158/// to compute fitness.
159///
160/// [BatchFitnessEvaluator] groups individuals into batches and evaluates them
161/// together, which can be more efficient than individual evaluation, especially
162/// when the problem supports batch evaluation or when there are significant
163/// overhead costs per evaluation.
164///
165/// # Performance Characteristics
166///
167/// - **Batch Processing**: Multiple individuals evaluated together
168/// - **Reduced Overhead**: Sometimes lower per-individual evaluation cost
169///
170/// # Use Cases
171///
172/// Use [BatchFitnessEvaluator] when:
173/// - The problem supports efficient batch evaluation
174/// - You have a large population that benefits from parallelization
175/// - Individual evaluation overhead is significant
176/// - You need access to parts or the whole of an unevaluated population in order to compute fitness.
177///
178/// # Batch Size Calculation
179///
180/// In order to ensure the optimal distribution of work across available threads,
181/// batch size is automatically calculated based on the number of workers
182/// in the executor and the total number of individuals to evaluate:
183///
184/// ```text
185/// batch_size = (total_individuals + num_workers - 1) / num_workers
186/// ```
187/// So, for example, if you have 100 individuals and 4 workers, the batch size would be:
188///
189/// ```text
190/// batch_size = (100 + 4 - 1) / 4 = 25
191/// ```
192///
193/// But, if your executor is [Executor::Serial], then the batch size will simply be the total number of
194/// individuals to evaluate, resulting in a single batch.
195pub struct BatchFitnessEvaluator {
196    executor: Arc<Executor>,
197}
198
199impl BatchFitnessEvaluator {
200    /// Creates a new batch fitness evaluator with the specified executor.
201    ///
202    /// # Note
203    ///
204    /// Batch evaluation works best with parallel executors that have multiple
205    /// worker threads. The evaluator will automatically divide work into optimal
206    /// batch sizes based on the number of available workers.
207    pub fn new(executor: Arc<Executor>) -> Self {
208        Self { executor }
209    }
210}
211
212/// Implementation of [Evaluator] for [BatchFitnessEvaluator].
213///
214/// This implementation groups individuals into batches and evaluates them
215/// together, which can significantly improve performance for large populations
216/// or when the problem supports efficient batch evaluation.
217///
218/// # Algorithm
219///
220/// The algorithm largely follows the same steps as `FitnessEvaluator`:
221/// 1. **Collect Unevaluated Individuals**: Find individuals without fitness scores
222/// 2. **Calculate Batch Size**: Determine optimal batch size based on worker count
223/// 3. **Create Batches**: Group individuals into batches for parallel processing
224/// 4. **Execute Batches**: Run batch evaluations through the executor
225/// 5. **Update Ecosystem**: Store computed scores and restore genotypes
226///
227/// # Batch Size Optimization
228///
229/// The batch size is calculated to ensure optimal work distribution:
230/// - **Small Batches**: Ensure all workers have work to do
231/// - **Large Batches**: Minimize overhead from job creation and distribution
232/// - **Balanced Distribution**: Work is evenly distributed across available threads
233///
234/// # Performance Optimizations
235///
236/// - **Efficient Batching**: Batches are created with minimal memory allocation
237/// - **Parallel Execution**: Multiple batches are processed simultaneously
238/// - **Memory Management**: Genotypes are efficiently restored after evaluation
239impl<C: Chromosome, T> Evaluator<C, T> for BatchFitnessEvaluator
240where
241    C: Chromosome + 'static,
242    T: 'static,
243{
244    #[inline]
245    fn eval(&self, ecosystem: &mut Ecosystem<C>, problem: Arc<dyn Problem<C, T>>) -> Result<usize> {
246        let mut pairs = Vec::new();
247        let len = ecosystem.population.len();
248        for idx in 0..len {
249            if ecosystem.population[idx].score().is_none() {
250                let geno = ecosystem.population[idx].take_genotype()?;
251                pairs.push((idx, geno));
252            }
253        }
254
255        let num_workers = self.executor.num_workers();
256        let batch_size = (pairs.len() + num_workers - 1) / num_workers;
257
258        if pairs.is_empty() || batch_size == 0 {
259            return Ok(0);
260        }
261
262        let mut batches = Vec::with_capacity(num_workers);
263
264        while !pairs.is_empty() {
265            let take = pairs.len().min(batch_size);
266
267            let mut batch_indices = Vec::with_capacity(take);
268            let mut batch_genotypes = Vec::with_capacity(take);
269
270            // drain from the end of pairs vector to avoid O(n^2) complexity
271            for (idx, geno) in pairs.drain(pairs.len() - take..) {
272                batch_indices.push(idx);
273                batch_genotypes.push(geno);
274            }
275
276            batches.push((batch_indices, batch_genotypes));
277        }
278
279        let results = self.executor.execute_batch(
280            batches
281                .into_iter()
282                .map(|batch| {
283                    let problem = Arc::clone(&problem);
284                    move || {
285                        let scores = problem.eval_batch(&batch.1);
286                        (batch.0, scores, batch.1)
287                    }
288                })
289                .collect(),
290        );
291
292        let mut count = 0;
293        for (indices, scores, genotypes) in results {
294            count += indices.len();
295            let score_genotype_iter = scores?.into_iter().zip(genotypes.into_iter());
296            for (i, (score, genotype)) in score_genotype_iter.enumerate() {
297                let idx = indices[i];
298                ecosystem.population[idx].set_score(Some(score));
299                ecosystem.population[idx].set_genotype(genotype);
300            }
301        }
302
303        Ok(count)
304    }
305}