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::timing::{evaluate_within, expired, Timer};
28
29#[cfg(feature = "serde")]
30use serde::{Deserialize, Serialize};
31
32/// Configuration for the genetic algorithm.
33#[derive(Debug, Clone)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35pub struct GaConfig {
36    /// Population size.
37    pub population_size: usize,
38    /// Maximum number of generations.
39    pub max_generations: u32,
40    /// Crossover rate (0.0 - 1.0).
41    pub crossover_rate: f64,
42    /// Mutation rate (0.0 - 1.0).
43    pub mutation_rate: f64,
44    /// Number of elite individuals to preserve each generation.
45    pub elite_count: usize,
46    /// Tournament size for selection.
47    pub tournament_size: usize,
48    /// Maximum time limit (None = unlimited).
49    pub time_limit: Option<Duration>,
50    /// Target fitness to stop early (None = run all generations).
51    pub target_fitness: Option<f64>,
52    /// Stagnation generations before early stop.
53    pub stagnation_limit: Option<u32>,
54}
55
56impl Default for GaConfig {
57    fn default() -> Self {
58        Self {
59            population_size: 100,
60            max_generations: 500,
61            crossover_rate: 0.85,
62            mutation_rate: 0.05,
63            elite_count: 5,
64            tournament_size: 3,
65            time_limit: None,
66            target_fitness: None,
67            stagnation_limit: Some(50),
68        }
69    }
70}
71
72impl GaConfig {
73    /// Creates a new configuration with default values.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Sets the population size.
79    pub fn with_population_size(mut self, size: usize) -> Self {
80        self.population_size = size.max(2);
81        self
82    }
83
84    /// Sets the maximum generations.
85    pub fn with_max_generations(mut self, gen: u32) -> Self {
86        self.max_generations = gen;
87        self
88    }
89
90    /// Sets the crossover rate.
91    pub fn with_crossover_rate(mut self, rate: f64) -> Self {
92        self.crossover_rate = rate;
93        self
94    }
95
96    /// Sets the mutation rate.
97    pub fn with_mutation_rate(mut self, rate: f64) -> Self {
98        self.mutation_rate = rate;
99        self
100    }
101
102    /// Sets the elite count.
103    pub fn with_elite_count(mut self, count: usize) -> Self {
104        self.elite_count = count;
105        self
106    }
107
108    /// Sets the time limit.
109    pub fn with_time_limit(mut self, duration: Duration) -> Self {
110        self.time_limit = Some(duration);
111        self
112    }
113
114    /// Sets the target fitness.
115    pub fn with_target_fitness(mut self, fitness: f64) -> Self {
116        self.target_fitness = Some(fitness);
117        self
118    }
119
120    /// Checks every field against its range: the builders store what they are
121    /// given, and this is the one place a value is judged.
122    ///
123    /// # Errors
124    /// `ConfigError` naming the first field out of range: a crossover or
125    /// mutation rate outside `[0, 1]`, a population below 2 or a tournament of 0.
126    pub fn validate(&self) -> Result<()> {
127        check_range("crossover_rate", self.crossover_rate, 0.0, 1.0)?;
128        check_range("mutation_rate", self.mutation_rate, 0.0, 1.0)?;
129        check_at_least("population_size", self.population_size as f64, 2.0)?;
130        check_at_least("tournament_size", self.tournament_size as f64, 1.0)?;
131        Ok(())
132    }
133}
134
135/// Trait for individuals in the genetic algorithm.
136///
137/// In u-nesting's convention, crossover and mutation are defined on the
138/// individual itself (unlike u-metaheur which places them on GaProblem).
139pub trait Individual: Clone + Send + Sync {
140    /// The fitness type (usually f64).
141    type Fitness: PartialOrd + Copy + Send;
142
143    /// Returns the fitness of this individual.
144    fn fitness(&self) -> Self::Fitness;
145
146    /// Creates a random individual.
147    fn random<R: Rng>(rng: &mut R) -> Self;
148
149    /// Performs crossover with another individual.
150    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self;
151
152    /// Mutates this individual in place.
153    fn mutate<R: Rng>(&mut self, rng: &mut R);
154}
155
156/// Trait for problem-specific GA operations.
157///
158/// Uses mutable evaluation: `evaluate(&mut Individual)` can set both
159/// fitness and auxiliary state on the individual.
160pub trait GaProblem: Send + Sync {
161    /// The individual type for this problem.
162    type Individual: Individual;
163
164    /// Evaluates the fitness of an individual (mutable — can set auxiliary state).
165    fn evaluate(&self, individual: &mut Self::Individual);
166
167    /// Evaluates multiple individuals in parallel.
168    /// Default implementation uses rayon when the `parallel` feature is enabled.
169    fn evaluate_parallel(&self, individuals: &mut [Self::Individual]) {
170        #[cfg(feature = "parallel")]
171        individuals.par_iter_mut().for_each(|ind| {
172            self.evaluate(ind);
173        });
174        #[cfg(not(feature = "parallel"))]
175        for ind in individuals.iter_mut() {
176            self.evaluate(ind);
177        }
178    }
179
180    /// Creates an initial population.
181    fn initialize_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<Self::Individual> {
182        (0..size).map(|_| Self::Individual::random(rng)).collect()
183    }
184
185    /// Called after each generation (for progress reporting).
186    fn on_generation(
187        &self,
188        _generation: u32,
189        _best: &Self::Individual,
190        _population: &[Self::Individual],
191    ) {
192        // Default: do nothing
193    }
194}
195
196/// Progress information during GA execution.
197#[derive(Debug, Clone)]
198pub struct GaProgress<F> {
199    /// Current generation number.
200    pub generation: u32,
201    /// Maximum generations configured.
202    pub max_generations: u32,
203    /// Best fitness so far.
204    pub best_fitness: F,
205    /// Average fitness of current population.
206    pub avg_fitness: f64,
207    /// Elapsed time since start.
208    pub elapsed: Duration,
209    /// Whether the algorithm is still running.
210    pub running: bool,
211}
212
213/// Result of a GA run.
214#[derive(Debug, Clone)]
215pub struct GaResult<I: Individual> {
216    /// The best individual found.
217    pub best: I,
218    /// Final generation reached.
219    pub generations: u32,
220    /// Total elapsed time.
221    pub elapsed: Duration,
222    /// Whether the target fitness was reached.
223    pub target_reached: bool,
224    /// Fitness history (best fitness per generation).
225    pub history: Vec<f64>,
226}
227
228/// Genetic algorithm runner.
229///
230/// Runs the evolutionary loop with mutable evaluation, tournament selection,
231/// elitism, and configurable stopping conditions.
232pub struct GaRunner<P: GaProblem> {
233    config: GaConfig,
234    problem: P,
235    cancelled: Arc<AtomicBool>,
236}
237
238impl<P: GaProblem> GaRunner<P>
239where
240    <P::Individual as Individual>::Fitness: Into<f64>,
241{
242    /// Creates a new GA runner.
243    ///
244    /// # Errors
245    /// As [`GaConfig::validate`].
246    pub fn new(config: GaConfig, problem: P) -> Result<Self> {
247        Self::with_cancellation(config, problem, Arc::new(AtomicBool::new(false)))
248    }
249
250    /// Creates a runner that stops when `cancelled` is set — the caller's own
251    /// flag, observed directly, so no thread is needed to forward it.
252    ///
253    /// # Errors
254    /// As [`GaConfig::validate`].
255    pub fn with_cancellation(
256        config: GaConfig,
257        problem: P,
258        cancelled: Arc<AtomicBool>,
259    ) -> Result<Self> {
260        config.validate()?;
261        Ok(Self {
262            config,
263            problem,
264            cancelled,
265        })
266    }
267
268    /// Returns a handle to cancel the algorithm.
269    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
270        self.cancelled.clone()
271    }
272
273    /// Runs the genetic algorithm.
274    pub fn run(&self) -> GaResult<P::Individual> {
275        self.run_with_rng(&mut rand::rng())
276    }
277
278    /// Runs the genetic algorithm with a progress callback.
279    pub fn run_with_progress<F>(&self, progress_callback: F) -> GaResult<P::Individual>
280    where
281        F: Fn(GaProgress<<P::Individual as Individual>::Fitness>),
282    {
283        self.run_with_rng_and_progress(&mut rand::rng(), Some(progress_callback))
284    }
285
286    /// Runs the genetic algorithm with a specific RNG.
287    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> GaResult<P::Individual> {
288        self.run_with_rng_and_progress::<R, fn(GaProgress<<P::Individual as Individual>::Fitness>)>(
289            rng, None,
290        )
291    }
292
293    /// Runs the genetic algorithm with a specific RNG and optional progress callback.
294    pub fn run_with_rng_and_progress<R: Rng, F>(
295        &self,
296        rng: &mut R,
297        progress_callback: Option<F>,
298    ) -> GaResult<P::Individual>
299    where
300        F: Fn(GaProgress<<P::Individual as Individual>::Fitness>),
301    {
302        let start = Timer::now();
303        let mut history = Vec::new();
304
305        // Initialize population
306        let mut population = self
307            .problem
308            .initialize_population(self.config.population_size, rng);
309
310        // Evaluate the initial population — up to the time limit
311        evaluate_within(&mut population, &start, self.config.time_limit, |batch| {
312            self.problem.evaluate_parallel(batch)
313        });
314
315        // Sort by fitness (descending - higher is better in u-nesting convention)
316        population.sort_by(|a, b| {
317            b.fitness()
318                .partial_cmp(&a.fitness())
319                .unwrap_or(std::cmp::Ordering::Equal)
320        });
321
322        let mut best = population[0].clone();
323        let mut best_fitness: f64 = best.fitness().into();
324        let mut stagnation_count = 0u32;
325        let mut generation = 0u32;
326        let mut target_reached = false;
327
328        while generation < self.config.max_generations {
329            // Check cancellation
330            if self.cancelled.load(Ordering::Relaxed) {
331                break;
332            }
333
334            // Check time limit
335            if expired(&start, self.config.time_limit) {
336                break;
337            }
338
339            // Check target fitness
340            if let Some(target) = self.config.target_fitness {
341                if best_fitness >= target {
342                    target_reached = true;
343                    break;
344                }
345            }
346
347            // Record history
348            history.push(best_fitness);
349
350            // Create new generation
351            let mut new_population = Vec::with_capacity(self.config.population_size);
352
353            // Elitism: keep the best individuals
354            for individual in population
355                .iter()
356                .take(self.config.elite_count.min(population.len()))
357            {
358                new_population.push(individual.clone());
359            }
360
361            // Fill the rest with crossover and mutation
362            let mut children: Vec<P::Individual> =
363                Vec::with_capacity(self.config.population_size - new_population.len());
364
365            while children.len() < self.config.population_size - new_population.len() {
366                // Tournament selection
367                let parent1 = self.tournament_select(&population, rng);
368                let parent2 = self.tournament_select(&population, rng);
369
370                // Crossover (on Individual, u-nesting convention)
371                let mut child = if rng.random::<f64>() < self.config.crossover_rate {
372                    parent1.crossover(parent2, rng)
373                } else {
374                    parent1.clone()
375                };
376
377                // Mutation (on Individual, u-nesting convention)
378                if rng.random::<f64>() < self.config.mutation_rate {
379                    child.mutate(rng);
380                }
381
382                children.push(child);
383            }
384
385            // Evaluate the children — up to the time limit; the loop ends on the
386            // next check if it was reached
387            evaluate_within(&mut children, &start, self.config.time_limit, |batch| {
388                self.problem.evaluate_parallel(batch)
389            });
390
391            // Add evaluated children to new population
392            new_population.extend(children);
393
394            // Sort new population
395            new_population.sort_by(|a, b| {
396                b.fitness()
397                    .partial_cmp(&a.fitness())
398                    .unwrap_or(std::cmp::Ordering::Equal)
399            });
400
401            // Update best
402            let new_best_fitness: f64 = new_population[0].fitness().into();
403            if new_best_fitness > best_fitness {
404                best = new_population[0].clone();
405                best_fitness = new_best_fitness;
406                stagnation_count = 0;
407            } else {
408                stagnation_count += 1;
409            }
410
411            // Check stagnation
412            if let Some(limit) = self.config.stagnation_limit {
413                if stagnation_count >= limit {
414                    break;
415                }
416            }
417
418            // Callback to GaProblem
419            self.problem
420                .on_generation(generation, &best, &new_population);
421
422            // Progress callback
423            if let Some(ref callback) = progress_callback {
424                let avg_fitness = new_population
425                    .iter()
426                    .map(|ind| ind.fitness().into())
427                    .sum::<f64>()
428                    / new_population.len() as f64;
429
430                callback(GaProgress {
431                    generation,
432                    max_generations: self.config.max_generations,
433                    best_fitness: best.fitness(),
434                    avg_fitness,
435                    elapsed: start.elapsed(),
436                    running: true,
437                });
438            }
439
440            population = new_population;
441            generation += 1;
442        }
443
444        // Final history entry
445        history.push(best_fitness);
446
447        // Final progress callback indicating completion
448        if let Some(ref callback) = progress_callback {
449            let avg_fitness = population
450                .iter()
451                .map(|ind| ind.fitness().into())
452                .sum::<f64>()
453                / population.len().max(1) as f64;
454
455            callback(GaProgress {
456                generation,
457                max_generations: self.config.max_generations,
458                best_fitness: best.fitness(),
459                avg_fitness,
460                elapsed: start.elapsed(),
461                running: false,
462            });
463        }
464
465        GaResult {
466            best,
467            generations: generation,
468            elapsed: start.elapsed(),
469            target_reached,
470            history,
471        }
472    }
473
474    /// Tournament selection (maximization — higher fitness wins).
475    fn tournament_select<'a, R: Rng>(
476        &self,
477        population: &'a [P::Individual],
478        rng: &mut R,
479    ) -> &'a P::Individual {
480        let mut best_idx = rng.random_range(0..population.len());
481
482        for _ in 1..self.config.tournament_size {
483            let idx = rng.random_range(0..population.len());
484            if population[idx].fitness() > population[best_idx].fitness() {
485                best_idx = idx;
486            }
487        }
488
489        &population[best_idx]
490    }
491}
492
493/// Chromosome representation for permutation-based problems.
494#[derive(Debug, Clone)]
495pub struct PermutationChromosome {
496    /// The permutation (indices).
497    pub genes: Vec<usize>,
498    /// Additional rotation/orientation genes.
499    pub rotations: Vec<usize>,
500    /// Cached fitness value.
501    fitness: f64,
502}
503
504impl PermutationChromosome {
505    /// Creates a new chromosome with the given size.
506    pub fn new(size: usize, _rotation_options: usize) -> Self {
507        Self {
508            genes: (0..size).collect(),
509            rotations: vec![0; size],
510            fitness: f64::NEG_INFINITY,
511        }
512    }
513
514    /// Creates a random chromosome.
515    pub fn random_with_options<R: Rng>(size: usize, rotation_options: usize, rng: &mut R) -> Self {
516        let mut genes: Vec<usize> = (0..size).collect();
517        genes.shuffle(rng);
518
519        let rotations: Vec<usize> = (0..size)
520            .map(|_| rng.random_range(0..rotation_options.max(1)))
521            .collect();
522
523        Self {
524            genes,
525            rotations,
526            fitness: f64::NEG_INFINITY,
527        }
528    }
529
530    /// Sets the fitness value.
531    pub fn set_fitness(&mut self, fitness: f64) {
532        self.fitness = fitness;
533    }
534
535    /// Returns the number of genes.
536    pub fn len(&self) -> usize {
537        self.genes.len()
538    }
539
540    /// Returns true if empty.
541    pub fn is_empty(&self) -> bool {
542        self.genes.is_empty()
543    }
544
545    /// Order crossover (OX).
546    pub fn order_crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
547        let n = self.genes.len();
548        if n < 2 {
549            return self.clone();
550        }
551
552        // Select two crossover points
553        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
554        if p1 > p2 {
555            std::mem::swap(&mut p1, &mut p2);
556        }
557
558        // Copy segment from parent1
559        let mut child_genes = vec![usize::MAX; n];
560        let mut used = vec![false; n];
561
562        for i in p1..=p2 {
563            child_genes[i] = self.genes[i];
564            used[self.genes[i]] = true;
565        }
566
567        // Fill remaining from parent2
568        let mut j = (p2 + 1) % n;
569        for i in 0..n {
570            let idx = (p2 + 1 + i) % n;
571            if child_genes[idx] == usize::MAX {
572                while used[other.genes[j]] {
573                    j = (j + 1) % n;
574                }
575                child_genes[idx] = other.genes[j];
576                used[other.genes[j]] = true;
577                j = (j + 1) % n;
578            }
579        }
580
581        // Crossover rotations (uniform)
582        let rotations: Vec<usize> = self
583            .rotations
584            .iter()
585            .zip(&other.rotations)
586            .map(|(a, b)| if rng.random() { *a } else { *b })
587            .collect();
588
589        Self {
590            genes: child_genes,
591            rotations,
592            fitness: f64::NEG_INFINITY,
593        }
594    }
595
596    /// Swap mutation.
597    pub fn swap_mutate<R: Rng>(&mut self, rng: &mut R) {
598        if self.genes.len() < 2 {
599            return;
600        }
601
602        let i = rng.random_range(0..self.genes.len());
603        let j = rng.random_range(0..self.genes.len());
604        self.genes.swap(i, j);
605        self.fitness = f64::NEG_INFINITY;
606    }
607
608    /// Rotation mutation.
609    pub fn rotation_mutate<R: Rng>(&mut self, rotation_options: usize, rng: &mut R) {
610        if self.rotations.is_empty() || rotation_options <= 1 {
611            return;
612        }
613
614        let idx = rng.random_range(0..self.rotations.len());
615        self.rotations[idx] = rng.random_range(0..rotation_options);
616        self.fitness = f64::NEG_INFINITY;
617    }
618
619    /// Inversion mutation (reverses a segment).
620    pub fn inversion_mutate<R: Rng>(&mut self, rng: &mut R) {
621        let n = self.genes.len();
622        if n < 2 {
623            return;
624        }
625
626        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
627        if p1 > p2 {
628            std::mem::swap(&mut p1, &mut p2);
629        }
630
631        self.genes[p1..=p2].reverse();
632        self.fitness = f64::NEG_INFINITY;
633    }
634}
635
636impl Individual for PermutationChromosome {
637    type Fitness = f64;
638
639    fn fitness(&self) -> f64 {
640        self.fitness
641    }
642
643    fn random<R: Rng>(rng: &mut R) -> Self {
644        // Default: empty, should be overridden by problem
645        Self::random_with_options(0, 1, rng)
646    }
647
648    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
649        self.order_crossover(other, rng)
650    }
651
652    fn mutate<R: Rng>(&mut self, rng: &mut R) {
653        // 70% swap, 30% inversion
654        if rng.random::<f64>() < 0.7 {
655            self.swap_mutate(rng);
656        } else {
657            self.inversion_mutate(rng);
658        }
659    }
660}
661
662#[cfg(test)]
663mod tests {
664    use super::*;
665
666    #[derive(Clone)]
667    struct SimpleIndividual {
668        value: f64,
669    }
670
671    impl Individual for SimpleIndividual {
672        type Fitness = f64;
673
674        fn fitness(&self) -> f64 {
675            // Maximize: -(x^2), optimal at x=0
676            -self.value * self.value
677        }
678
679        fn random<R: Rng>(rng: &mut R) -> Self {
680            Self {
681                value: rng.random_range(-100.0..100.0),
682            }
683        }
684
685        fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
686            Self {
687                value: if rng.random() {
688                    self.value
689                } else {
690                    other.value
691                },
692            }
693        }
694
695        fn mutate<R: Rng>(&mut self, rng: &mut R) {
696            self.value += rng.random_range(-10.0..10.0);
697        }
698    }
699
700    struct SimpleProblem;
701
702    impl GaProblem for SimpleProblem {
703        type Individual = SimpleIndividual;
704
705        fn evaluate(&self, _individual: &mut Self::Individual) {
706            // Fitness is computed in Individual::fitness()
707        }
708    }
709
710    #[test]
711    fn test_ga_basic() {
712        let config = GaConfig::default()
713            .with_population_size(50)
714            .with_max_generations(100)
715            .with_target_fitness(-0.01);
716
717        let runner = GaRunner::new(config, SimpleProblem).expect("valid config");
718        let result = runner.run();
719
720        // Should find something close to 0
721        assert!(result.best.value.abs() < 5.0);
722    }
723
724    #[test]
725    fn test_permutation_crossover() {
726        let mut rng = rand::rng();
727        let parent1 = PermutationChromosome::random_with_options(10, 4, &mut rng);
728        let parent2 = PermutationChromosome::random_with_options(10, 4, &mut rng);
729
730        let child = parent1.order_crossover(&parent2, &mut rng);
731
732        // Child should be a valid permutation
733        assert_eq!(child.genes.len(), 10);
734        let mut sorted = child.genes.clone();
735        sorted.sort();
736        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
737    }
738
739    #[test]
740    fn test_permutation_mutation() {
741        let mut rng = rand::rng();
742        let mut chromosome = PermutationChromosome::random_with_options(10, 4, &mut rng);
743
744        chromosome.swap_mutate(&mut rng);
745
746        // Should still be a valid permutation
747        let mut sorted = chromosome.genes.clone();
748        sorted.sort();
749        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
750    }
751}