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