Skip to main content

u_nesting_core/
ga.rs

1//! Genetic Algorithm framework for optimization.
2//!
3//! This module provides the GA abstraction layer for u-nesting. It defines
4//! domain-specific traits ([`Individual`], [`GaProblem`]) that support
5//! mutable evaluation — the key difference from u-metaheur's immutable pattern.
6//!
7//! # Architecture
8//!
9//! u-nesting uses a **mutable evaluation** pattern: `evaluate(&mut Individual)`
10//! sets both fitness and auxiliary state (e.g., `placed_count`, `total_count`).
11//! This differs from u-metaheur's `evaluate(&Individual) -> Fitness` pattern,
12//! which only returns a fitness value without modifying the individual.
13//!
14//! Because of this fundamental difference, u-nesting maintains its own
15//! evolutionary loop while sharing the rand/rayon ecosystem with u-metaheur.
16//! Crossover and mutation operators are defined on [`Individual`] (u-nesting
17//! convention), not on [`GaProblem`] (u-metaheur convention).
18
19use rand::prelude::*;
20#[cfg(feature = "parallel")]
21use rayon::prelude::*;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::Arc;
24use std::time::Duration;
25
26use crate::error::{check_at_least, check_range, Result};
27use crate::result::StopReason;
28use crate::timing::{evaluate_within, expired, Timer};
29
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32
33/// Configuration for the genetic algorithm.
34#[derive(Debug, Clone)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36pub struct GaConfig {
37    /// Population size.
38    pub population_size: usize,
39    /// Maximum number of generations.
40    pub max_generations: u32,
41    /// Crossover rate (0.0 - 1.0).
42    pub crossover_rate: f64,
43    /// Mutation rate (0.0 - 1.0).
44    pub mutation_rate: f64,
45    /// Number of elite individuals to preserve each generation.
46    pub elite_count: usize,
47    /// Tournament size for selection.
48    pub tournament_size: usize,
49    /// Maximum time limit (None = unlimited).
50    pub time_limit: Option<Duration>,
51    /// Target fitness to stop early (None = run all generations).
52    pub target_fitness: Option<f64>,
53    /// Stagnation generations before early stop.
54    pub stagnation_limit: Option<u32>,
55}
56
57impl Default for GaConfig {
58    fn default() -> Self {
59        Self {
60            population_size: 100,
61            max_generations: 500,
62            crossover_rate: 0.85,
63            mutation_rate: 0.05,
64            elite_count: 5,
65            tournament_size: 3,
66            time_limit: None,
67            target_fitness: None,
68            stagnation_limit: Some(50),
69        }
70    }
71}
72
73impl GaConfig {
74    /// Creates a new configuration with default values.
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Sets the population size.
80    pub fn with_population_size(mut self, size: usize) -> Self {
81        self.population_size = size;
82        self
83    }
84
85    /// Sets the maximum generations.
86    pub fn with_max_generations(mut self, gen: u32) -> Self {
87        self.max_generations = gen;
88        self
89    }
90
91    /// Sets the crossover rate.
92    pub fn with_crossover_rate(mut self, rate: f64) -> Self {
93        self.crossover_rate = rate;
94        self
95    }
96
97    /// Sets the mutation rate.
98    pub fn with_mutation_rate(mut self, rate: f64) -> Self {
99        self.mutation_rate = rate;
100        self
101    }
102
103    /// Sets the elite count.
104    pub fn with_elite_count(mut self, count: usize) -> Self {
105        self.elite_count = count;
106        self
107    }
108
109    /// Sets the time limit.
110    pub fn with_time_limit(mut self, duration: Duration) -> Self {
111        self.time_limit = Some(duration);
112        self
113    }
114
115    /// Sets the target fitness.
116    pub fn with_target_fitness(mut self, fitness: f64) -> Self {
117        self.target_fitness = Some(fitness);
118        self
119    }
120
121    /// Checks every field against its range: the builders store what they are
122    /// given, and this is the one place a value is judged.
123    ///
124    /// # Errors
125    /// `ConfigError` naming the first field out of range: a crossover or
126    /// mutation rate outside `[0, 1]`, a population below 2 or a tournament of 0.
127    pub fn validate(&self) -> Result<()> {
128        check_range("crossover_rate", self.crossover_rate, 0.0, 1.0)?;
129        check_range("mutation_rate", self.mutation_rate, 0.0, 1.0)?;
130        check_at_least("population_size", self.population_size as f64, 2.0)?;
131        check_at_least("tournament_size", self.tournament_size as f64, 1.0)?;
132        Ok(())
133    }
134}
135
136/// Trait for individuals in the genetic algorithm.
137///
138/// In u-nesting's convention, crossover and mutation are defined on the
139/// individual itself (unlike u-metaheur which places them on GaProblem).
140pub trait Individual: Clone + Send + Sync {
141    /// The fitness type (usually f64).
142    type Fitness: PartialOrd + Copy + Send;
143
144    /// Returns the fitness of this individual.
145    fn fitness(&self) -> Self::Fitness;
146
147    /// Creates a random individual.
148    fn random<R: Rng>(rng: &mut R) -> Self;
149
150    /// Performs crossover with another individual.
151    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self;
152
153    /// Mutates this individual in place.
154    fn mutate<R: Rng>(&mut self, rng: &mut R);
155}
156
157/// Trait for problem-specific GA operations.
158///
159/// Uses mutable evaluation: `evaluate(&mut Individual)` can set both
160/// fitness and auxiliary state on the individual.
161pub trait GaProblem: Send + Sync {
162    /// The individual type for this problem.
163    type Individual: Individual;
164
165    /// Evaluates the fitness of an individual (mutable — can set auxiliary state).
166    fn evaluate(&self, individual: &mut Self::Individual);
167
168    /// Evaluates multiple individuals in parallel.
169    /// Default implementation uses rayon when the `parallel` feature is enabled.
170    fn evaluate_parallel(&self, individuals: &mut [Self::Individual]) {
171        #[cfg(feature = "parallel")]
172        individuals.par_iter_mut().for_each(|ind| {
173            self.evaluate(ind);
174        });
175        #[cfg(not(feature = "parallel"))]
176        for ind in individuals.iter_mut() {
177            self.evaluate(ind);
178        }
179    }
180
181    /// Creates an initial population.
182    fn initialize_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<Self::Individual> {
183        (0..size).map(|_| Self::Individual::random(rng)).collect()
184    }
185
186    /// Called after each generation (for progress reporting).
187    fn on_generation(
188        &self,
189        _generation: u32,
190        _best: &Self::Individual,
191        _population: &[Self::Individual],
192    ) {
193        // Default: do nothing
194    }
195}
196
197/// Progress information during GA execution.
198#[derive(Debug, Clone)]
199pub struct GaProgress<F> {
200    /// Current generation number.
201    pub generation: u32,
202    /// Maximum generations configured.
203    pub max_generations: u32,
204    /// Best fitness so far.
205    pub best_fitness: F,
206    /// Average fitness of current population.
207    pub avg_fitness: f64,
208    /// Elapsed time since start.
209    pub elapsed: Duration,
210    /// Whether the algorithm is still running.
211    pub running: bool,
212}
213
214/// Result of a GA run.
215#[derive(Debug, Clone)]
216pub struct GaResult<I: Individual> {
217    /// The best individual found.
218    pub best: I,
219    /// Final generation reached.
220    pub generations: u32,
221    /// Total elapsed time.
222    pub elapsed: Duration,
223    /// Why the run stopped.
224    pub stop_reason: StopReason,
225    /// Fitness history (best fitness per generation).
226    pub history: Vec<f64>,
227}
228
229/// Genetic algorithm runner.
230///
231/// Runs the evolutionary loop with mutable evaluation, tournament selection,
232/// elitism, and configurable stopping conditions.
233pub struct GaRunner<P: GaProblem> {
234    config: GaConfig,
235    problem: P,
236    cancelled: Arc<AtomicBool>,
237}
238
239impl<P: GaProblem> GaRunner<P>
240where
241    <P::Individual as Individual>::Fitness: Into<f64>,
242{
243    /// Creates a new GA runner.
244    ///
245    /// # Errors
246    /// As [`GaConfig::validate`].
247    pub fn new(config: GaConfig, problem: P) -> Result<Self> {
248        Self::with_cancellation(config, problem, Arc::new(AtomicBool::new(false)))
249    }
250
251    /// Creates a runner that stops when `cancelled` is set — the caller's own
252    /// flag, observed directly, so no thread is needed to forward it.
253    ///
254    /// # Errors
255    /// As [`GaConfig::validate`].
256    pub fn with_cancellation(
257        config: GaConfig,
258        problem: P,
259        cancelled: Arc<AtomicBool>,
260    ) -> Result<Self> {
261        config.validate()?;
262        Ok(Self {
263            config,
264            problem,
265            cancelled,
266        })
267    }
268
269    /// Returns a handle to cancel the algorithm.
270    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
271        self.cancelled.clone()
272    }
273
274    /// Runs the genetic algorithm.
275    pub fn run(&self) -> GaResult<P::Individual> {
276        self.run_with_rng(&mut rand::rng())
277    }
278
279    /// Runs the genetic algorithm with a progress callback.
280    pub fn run_with_progress<F>(&self, progress_callback: F) -> GaResult<P::Individual>
281    where
282        F: Fn(GaProgress<<P::Individual as Individual>::Fitness>),
283    {
284        self.run_with_rng_and_progress(&mut rand::rng(), Some(progress_callback))
285    }
286
287    /// Runs the genetic algorithm with a specific RNG.
288    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> GaResult<P::Individual> {
289        self.run_with_rng_and_progress::<R, fn(GaProgress<<P::Individual as Individual>::Fitness>)>(
290            rng, None,
291        )
292    }
293
294    /// Runs the genetic algorithm with a specific RNG and optional progress callback.
295    pub fn run_with_rng_and_progress<R: Rng, F>(
296        &self,
297        rng: &mut R,
298        progress_callback: Option<F>,
299    ) -> GaResult<P::Individual>
300    where
301        F: Fn(GaProgress<<P::Individual as Individual>::Fitness>),
302    {
303        let start = Timer::now();
304        let mut history = Vec::new();
305
306        // A report after every evaluated batch, not only every generation: one
307        // generation of a large problem can take seconds, and a caller that
308        // cancels from its callback has to be heard within it. Returns the
309        // best fitness seen, counting the batch.
310        let heartbeat = |generation: u32,
311                         best: Option<<P::Individual as Individual>::Fitness>,
312                         batch: &[P::Individual]| {
313            let best = batch
314                .iter()
315                .map(Individual::fitness)
316                .fold(best, |b, f| match b {
317                    Some(b) if b >= f => Some(b),
318                    _ => Some(f),
319                });
320            if let (Some(callback), Some(best_fitness)) = (&progress_callback, best) {
321                let avg_fitness = batch.iter().map(|i| i.fitness().into()).sum::<f64>()
322                    / batch.len().max(1) as f64;
323                callback(GaProgress {
324                    generation,
325                    max_generations: self.config.max_generations,
326                    best_fitness,
327                    avg_fitness,
328                    elapsed: start.elapsed(),
329                    running: true,
330                });
331            }
332            best
333        };
334
335        // Initialize population
336        let mut population = self
337            .problem
338            .initialize_population(self.config.population_size, rng);
339
340        // Evaluate the initial population — up to the time limit
341        let mut best_so_far = None;
342        evaluate_within(&mut population, &start, self.config.time_limit, |batch| {
343            self.problem.evaluate_parallel(batch);
344            best_so_far = heartbeat(0, best_so_far, batch);
345        });
346
347        // Sort by fitness (descending - higher is better in u-nesting convention)
348        population.sort_by(|a, b| {
349            b.fitness()
350                .partial_cmp(&a.fitness())
351                .unwrap_or(std::cmp::Ordering::Equal)
352        });
353
354        let mut best = population[0].clone();
355        let mut best_fitness: f64 = best.fitness().into();
356        let mut stagnation_count = 0u32;
357        let mut generation = 0u32;
358        let reached = |fitness: f64| self.config.target_fitness.is_some_and(|t| fitness >= t);
359
360        // Each exit names its reason. The target is judged before the
361        // generation budget, so a target met by the last generation is
362        // reported as met.
363        let stop_reason = loop {
364            if reached(best_fitness) {
365                break StopReason::TargetReached;
366            }
367            if generation >= self.config.max_generations {
368                break StopReason::IterationLimit;
369            }
370            if self.cancelled.load(Ordering::Relaxed) {
371                break StopReason::Cancelled;
372            }
373            if expired(&start, self.config.time_limit) {
374                break StopReason::TimeLimit;
375            }
376
377            // Record history
378            history.push(best_fitness);
379
380            // Create new generation
381            let mut new_population = Vec::with_capacity(self.config.population_size);
382
383            // Elitism: keep the best individuals
384            for individual in population
385                .iter()
386                .take(self.config.elite_count.min(population.len()))
387            {
388                new_population.push(individual.clone());
389            }
390
391            // Fill the rest with crossover and mutation
392            let mut children: Vec<P::Individual> =
393                Vec::with_capacity(self.config.population_size - new_population.len());
394
395            while children.len() < self.config.population_size - new_population.len() {
396                // Tournament selection
397                let parent1 = self.tournament_select(&population, rng);
398                let parent2 = self.tournament_select(&population, rng);
399
400                // Crossover (on Individual, u-nesting convention)
401                let mut child = if rng.random::<f64>() < self.config.crossover_rate {
402                    parent1.crossover(parent2, rng)
403                } else {
404                    parent1.clone()
405                };
406
407                // Mutation (on Individual, u-nesting convention)
408                if rng.random::<f64>() < self.config.mutation_rate {
409                    child.mutate(rng);
410                }
411
412                children.push(child);
413            }
414
415            // Evaluate the children — up to the time limit; the loop ends on the
416            // next check if it was reached
417            let best_now = best.fitness();
418            evaluate_within(&mut children, &start, self.config.time_limit, |batch| {
419                self.problem.evaluate_parallel(batch);
420                heartbeat(generation, Some(best_now), batch);
421            });
422
423            // Add evaluated children to new population
424            new_population.extend(children);
425
426            // Sort new population
427            new_population.sort_by(|a, b| {
428                b.fitness()
429                    .partial_cmp(&a.fitness())
430                    .unwrap_or(std::cmp::Ordering::Equal)
431            });
432
433            // Update best
434            let new_best_fitness: f64 = new_population[0].fitness().into();
435            if new_best_fitness > best_fitness {
436                best = new_population[0].clone();
437                best_fitness = new_best_fitness;
438                stagnation_count = 0;
439            } else {
440                stagnation_count += 1;
441            }
442
443            // Check stagnation
444            if let Some(limit) = self.config.stagnation_limit {
445                if stagnation_count >= limit {
446                    break StopReason::NoImprovement;
447                }
448            }
449
450            // Callback to GaProblem
451            self.problem
452                .on_generation(generation, &best, &new_population);
453
454            // Progress callback
455            if let Some(ref callback) = progress_callback {
456                let avg_fitness = new_population
457                    .iter()
458                    .map(|ind| ind.fitness().into())
459                    .sum::<f64>()
460                    / new_population.len() as f64;
461
462                callback(GaProgress {
463                    generation,
464                    max_generations: self.config.max_generations,
465                    best_fitness: best.fitness(),
466                    avg_fitness,
467                    elapsed: start.elapsed(),
468                    running: true,
469                });
470            }
471
472            population = new_population;
473            generation += 1;
474        };
475
476        // Final history entry
477        history.push(best_fitness);
478
479        // Final progress callback indicating completion
480        if let Some(ref callback) = progress_callback {
481            let avg_fitness = population
482                .iter()
483                .map(|ind| ind.fitness().into())
484                .sum::<f64>()
485                / population.len().max(1) as f64;
486
487            callback(GaProgress {
488                generation,
489                max_generations: self.config.max_generations,
490                best_fitness: best.fitness(),
491                avg_fitness,
492                elapsed: start.elapsed(),
493                running: false,
494            });
495        }
496
497        GaResult {
498            best,
499            generations: generation,
500            elapsed: start.elapsed(),
501            stop_reason,
502            history,
503        }
504    }
505
506    /// Tournament selection (maximization — higher fitness wins).
507    fn tournament_select<'a, R: Rng>(
508        &self,
509        population: &'a [P::Individual],
510        rng: &mut R,
511    ) -> &'a P::Individual {
512        let mut best_idx = rng.random_range(0..population.len());
513
514        for _ in 1..self.config.tournament_size {
515            let idx = rng.random_range(0..population.len());
516            if population[idx].fitness() > population[best_idx].fitness() {
517                best_idx = idx;
518            }
519        }
520
521        &population[best_idx]
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[derive(Clone)]
530    struct SimpleIndividual {
531        value: f64,
532    }
533
534    impl Individual for SimpleIndividual {
535        type Fitness = f64;
536
537        fn fitness(&self) -> f64 {
538            // Maximize: -(x^2), optimal at x=0
539            -self.value * self.value
540        }
541
542        fn random<R: Rng>(rng: &mut R) -> Self {
543            Self {
544                value: rng.random_range(-100.0..100.0),
545            }
546        }
547
548        fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
549            Self {
550                value: if rng.random() {
551                    self.value
552                } else {
553                    other.value
554                },
555            }
556        }
557
558        fn mutate<R: Rng>(&mut self, rng: &mut R) {
559            self.value += rng.random_range(-10.0..10.0);
560        }
561    }
562
563    struct SimpleProblem;
564
565    impl GaProblem for SimpleProblem {
566        type Individual = SimpleIndividual;
567
568        fn evaluate(&self, _individual: &mut Self::Individual) {
569            // Fitness is computed in Individual::fitness()
570        }
571    }
572
573    #[test]
574    fn test_ga_basic() {
575        let config = GaConfig::default()
576            .with_population_size(50)
577            .with_max_generations(100)
578            .with_target_fitness(-0.01);
579
580        let runner = GaRunner::new(config, SimpleProblem).expect("valid config");
581        let result = runner.run();
582
583        // Should find something close to 0
584        assert!(result.best.value.abs() < 5.0);
585    }
586}