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}