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::{evaluate_within, expired, 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    /// Creates a runner that stops when `cancelled` is set — the caller's own
237    /// flag, observed directly, so no thread is needed to forward it.
238    pub fn with_cancellation(config: GaConfig, problem: P, cancelled: Arc<AtomicBool>) -> Self {
239        Self {
240            config,
241            problem,
242            cancelled,
243        }
244    }
245
246    /// Returns a handle to cancel the algorithm.
247    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
248        self.cancelled.clone()
249    }
250
251    /// Runs the genetic algorithm.
252    pub fn run(&self) -> GaResult<P::Individual> {
253        self.run_with_rng(&mut rand::rng())
254    }
255
256    /// Runs the genetic algorithm with a progress callback.
257    pub fn run_with_progress<F>(&self, progress_callback: F) -> GaResult<P::Individual>
258    where
259        F: Fn(GaProgress<<P::Individual as Individual>::Fitness>),
260    {
261        self.run_with_rng_and_progress(&mut rand::rng(), Some(progress_callback))
262    }
263
264    /// Runs the genetic algorithm with a specific RNG.
265    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> GaResult<P::Individual> {
266        self.run_with_rng_and_progress::<R, fn(GaProgress<<P::Individual as Individual>::Fitness>)>(
267            rng, None,
268        )
269    }
270
271    /// Runs the genetic algorithm with a specific RNG and optional progress callback.
272    pub fn run_with_rng_and_progress<R: Rng, F>(
273        &self,
274        rng: &mut R,
275        progress_callback: Option<F>,
276    ) -> GaResult<P::Individual>
277    where
278        F: Fn(GaProgress<<P::Individual as Individual>::Fitness>),
279    {
280        let start = Timer::now();
281        let mut history = Vec::new();
282
283        // Initialize population
284        let mut population = self
285            .problem
286            .initialize_population(self.config.population_size, rng);
287
288        // Evaluate the initial population — up to the time limit
289        evaluate_within(&mut population, &start, self.config.time_limit, |batch| {
290            self.problem.evaluate_parallel(batch)
291        });
292
293        // Sort by fitness (descending - higher is better in u-nesting convention)
294        population.sort_by(|a, b| {
295            b.fitness()
296                .partial_cmp(&a.fitness())
297                .unwrap_or(std::cmp::Ordering::Equal)
298        });
299
300        let mut best = population[0].clone();
301        let mut best_fitness: f64 = best.fitness().into();
302        let mut stagnation_count = 0u32;
303        let mut generation = 0u32;
304        let mut target_reached = false;
305
306        while generation < self.config.max_generations {
307            // Check cancellation
308            if self.cancelled.load(Ordering::Relaxed) {
309                break;
310            }
311
312            // Check time limit
313            if expired(&start, self.config.time_limit) {
314                break;
315            }
316
317            // Check target fitness
318            if let Some(target) = self.config.target_fitness {
319                if best_fitness >= target {
320                    target_reached = true;
321                    break;
322                }
323            }
324
325            // Record history
326            history.push(best_fitness);
327
328            // Create new generation
329            let mut new_population = Vec::with_capacity(self.config.population_size);
330
331            // Elitism: keep the best individuals
332            for individual in population
333                .iter()
334                .take(self.config.elite_count.min(population.len()))
335            {
336                new_population.push(individual.clone());
337            }
338
339            // Fill the rest with crossover and mutation
340            let mut children: Vec<P::Individual> =
341                Vec::with_capacity(self.config.population_size - new_population.len());
342
343            while children.len() < self.config.population_size - new_population.len() {
344                // Tournament selection
345                let parent1 = self.tournament_select(&population, rng);
346                let parent2 = self.tournament_select(&population, rng);
347
348                // Crossover (on Individual, u-nesting convention)
349                let mut child = if rng.random::<f64>() < self.config.crossover_rate {
350                    parent1.crossover(parent2, rng)
351                } else {
352                    parent1.clone()
353                };
354
355                // Mutation (on Individual, u-nesting convention)
356                if rng.random::<f64>() < self.config.mutation_rate {
357                    child.mutate(rng);
358                }
359
360                children.push(child);
361            }
362
363            // Evaluate the children — up to the time limit; the loop ends on the
364            // next check if it was reached
365            evaluate_within(&mut children, &start, self.config.time_limit, |batch| {
366                self.problem.evaluate_parallel(batch)
367            });
368
369            // Add evaluated children to new population
370            new_population.extend(children);
371
372            // Sort new population
373            new_population.sort_by(|a, b| {
374                b.fitness()
375                    .partial_cmp(&a.fitness())
376                    .unwrap_or(std::cmp::Ordering::Equal)
377            });
378
379            // Update best
380            let new_best_fitness: f64 = new_population[0].fitness().into();
381            if new_best_fitness > best_fitness {
382                best = new_population[0].clone();
383                best_fitness = new_best_fitness;
384                stagnation_count = 0;
385            } else {
386                stagnation_count += 1;
387            }
388
389            // Check stagnation
390            if let Some(limit) = self.config.stagnation_limit {
391                if stagnation_count >= limit {
392                    break;
393                }
394            }
395
396            // Callback to GaProblem
397            self.problem
398                .on_generation(generation, &best, &new_population);
399
400            // Progress callback
401            if let Some(ref callback) = progress_callback {
402                let avg_fitness = new_population
403                    .iter()
404                    .map(|ind| ind.fitness().into())
405                    .sum::<f64>()
406                    / new_population.len() as f64;
407
408                callback(GaProgress {
409                    generation,
410                    max_generations: self.config.max_generations,
411                    best_fitness: best.fitness(),
412                    avg_fitness,
413                    elapsed: start.elapsed(),
414                    running: true,
415                });
416            }
417
418            population = new_population;
419            generation += 1;
420        }
421
422        // Final history entry
423        history.push(best_fitness);
424
425        // Final progress callback indicating completion
426        if let Some(ref callback) = progress_callback {
427            let avg_fitness = population
428                .iter()
429                .map(|ind| ind.fitness().into())
430                .sum::<f64>()
431                / population.len().max(1) as f64;
432
433            callback(GaProgress {
434                generation,
435                max_generations: self.config.max_generations,
436                best_fitness: best.fitness(),
437                avg_fitness,
438                elapsed: start.elapsed(),
439                running: false,
440            });
441        }
442
443        GaResult {
444            best,
445            generations: generation,
446            elapsed: start.elapsed(),
447            target_reached,
448            history,
449        }
450    }
451
452    /// Tournament selection (maximization — higher fitness wins).
453    fn tournament_select<'a, R: Rng>(
454        &self,
455        population: &'a [P::Individual],
456        rng: &mut R,
457    ) -> &'a P::Individual {
458        let mut best_idx = rng.random_range(0..population.len());
459
460        for _ in 1..self.config.tournament_size {
461            let idx = rng.random_range(0..population.len());
462            if population[idx].fitness() > population[best_idx].fitness() {
463                best_idx = idx;
464            }
465        }
466
467        &population[best_idx]
468    }
469}
470
471/// Chromosome representation for permutation-based problems.
472#[derive(Debug, Clone)]
473pub struct PermutationChromosome {
474    /// The permutation (indices).
475    pub genes: Vec<usize>,
476    /// Additional rotation/orientation genes.
477    pub rotations: Vec<usize>,
478    /// Cached fitness value.
479    fitness: f64,
480}
481
482impl PermutationChromosome {
483    /// Creates a new chromosome with the given size.
484    pub fn new(size: usize, _rotation_options: usize) -> Self {
485        Self {
486            genes: (0..size).collect(),
487            rotations: vec![0; size],
488            fitness: f64::NEG_INFINITY,
489        }
490    }
491
492    /// Creates a random chromosome.
493    pub fn random_with_options<R: Rng>(size: usize, rotation_options: usize, rng: &mut R) -> Self {
494        let mut genes: Vec<usize> = (0..size).collect();
495        genes.shuffle(rng);
496
497        let rotations: Vec<usize> = (0..size)
498            .map(|_| rng.random_range(0..rotation_options.max(1)))
499            .collect();
500
501        Self {
502            genes,
503            rotations,
504            fitness: f64::NEG_INFINITY,
505        }
506    }
507
508    /// Sets the fitness value.
509    pub fn set_fitness(&mut self, fitness: f64) {
510        self.fitness = fitness;
511    }
512
513    /// Returns the number of genes.
514    pub fn len(&self) -> usize {
515        self.genes.len()
516    }
517
518    /// Returns true if empty.
519    pub fn is_empty(&self) -> bool {
520        self.genes.is_empty()
521    }
522
523    /// Order crossover (OX).
524    pub fn order_crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
525        let n = self.genes.len();
526        if n < 2 {
527            return self.clone();
528        }
529
530        // Select two crossover points
531        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
532        if p1 > p2 {
533            std::mem::swap(&mut p1, &mut p2);
534        }
535
536        // Copy segment from parent1
537        let mut child_genes = vec![usize::MAX; n];
538        let mut used = vec![false; n];
539
540        for i in p1..=p2 {
541            child_genes[i] = self.genes[i];
542            used[self.genes[i]] = true;
543        }
544
545        // Fill remaining from parent2
546        let mut j = (p2 + 1) % n;
547        for i in 0..n {
548            let idx = (p2 + 1 + i) % n;
549            if child_genes[idx] == usize::MAX {
550                while used[other.genes[j]] {
551                    j = (j + 1) % n;
552                }
553                child_genes[idx] = other.genes[j];
554                used[other.genes[j]] = true;
555                j = (j + 1) % n;
556            }
557        }
558
559        // Crossover rotations (uniform)
560        let rotations: Vec<usize> = self
561            .rotations
562            .iter()
563            .zip(&other.rotations)
564            .map(|(a, b)| if rng.random() { *a } else { *b })
565            .collect();
566
567        Self {
568            genes: child_genes,
569            rotations,
570            fitness: f64::NEG_INFINITY,
571        }
572    }
573
574    /// Swap mutation.
575    pub fn swap_mutate<R: Rng>(&mut self, rng: &mut R) {
576        if self.genes.len() < 2 {
577            return;
578        }
579
580        let i = rng.random_range(0..self.genes.len());
581        let j = rng.random_range(0..self.genes.len());
582        self.genes.swap(i, j);
583        self.fitness = f64::NEG_INFINITY;
584    }
585
586    /// Rotation mutation.
587    pub fn rotation_mutate<R: Rng>(&mut self, rotation_options: usize, rng: &mut R) {
588        if self.rotations.is_empty() || rotation_options <= 1 {
589            return;
590        }
591
592        let idx = rng.random_range(0..self.rotations.len());
593        self.rotations[idx] = rng.random_range(0..rotation_options);
594        self.fitness = f64::NEG_INFINITY;
595    }
596
597    /// Inversion mutation (reverses a segment).
598    pub fn inversion_mutate<R: Rng>(&mut self, rng: &mut R) {
599        let n = self.genes.len();
600        if n < 2 {
601            return;
602        }
603
604        let (mut p1, mut p2) = (rng.random_range(0..n), rng.random_range(0..n));
605        if p1 > p2 {
606            std::mem::swap(&mut p1, &mut p2);
607        }
608
609        self.genes[p1..=p2].reverse();
610        self.fitness = f64::NEG_INFINITY;
611    }
612}
613
614impl Individual for PermutationChromosome {
615    type Fitness = f64;
616
617    fn fitness(&self) -> f64 {
618        self.fitness
619    }
620
621    fn random<R: Rng>(rng: &mut R) -> Self {
622        // Default: empty, should be overridden by problem
623        Self::random_with_options(0, 1, rng)
624    }
625
626    fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
627        self.order_crossover(other, rng)
628    }
629
630    fn mutate<R: Rng>(&mut self, rng: &mut R) {
631        // 70% swap, 30% inversion
632        if rng.random::<f64>() < 0.7 {
633            self.swap_mutate(rng);
634        } else {
635            self.inversion_mutate(rng);
636        }
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643
644    #[derive(Clone)]
645    struct SimpleIndividual {
646        value: f64,
647    }
648
649    impl Individual for SimpleIndividual {
650        type Fitness = f64;
651
652        fn fitness(&self) -> f64 {
653            // Maximize: -(x^2), optimal at x=0
654            -self.value * self.value
655        }
656
657        fn random<R: Rng>(rng: &mut R) -> Self {
658            Self {
659                value: rng.random_range(-100.0..100.0),
660            }
661        }
662
663        fn crossover<R: Rng>(&self, other: &Self, rng: &mut R) -> Self {
664            Self {
665                value: if rng.random() {
666                    self.value
667                } else {
668                    other.value
669                },
670            }
671        }
672
673        fn mutate<R: Rng>(&mut self, rng: &mut R) {
674            self.value += rng.random_range(-10.0..10.0);
675        }
676    }
677
678    struct SimpleProblem;
679
680    impl GaProblem for SimpleProblem {
681        type Individual = SimpleIndividual;
682
683        fn evaluate(&self, _individual: &mut Self::Individual) {
684            // Fitness is computed in Individual::fitness()
685        }
686    }
687
688    #[test]
689    fn test_ga_basic() {
690        let config = GaConfig::default()
691            .with_population_size(50)
692            .with_max_generations(100)
693            .with_target_fitness(-0.01);
694
695        let runner = GaRunner::new(config, SimpleProblem);
696        let result = runner.run();
697
698        // Should find something close to 0
699        assert!(result.best.value.abs() < 5.0);
700    }
701
702    #[test]
703    fn test_permutation_crossover() {
704        let mut rng = rand::rng();
705        let parent1 = PermutationChromosome::random_with_options(10, 4, &mut rng);
706        let parent2 = PermutationChromosome::random_with_options(10, 4, &mut rng);
707
708        let child = parent1.order_crossover(&parent2, &mut rng);
709
710        // Child should be a valid permutation
711        assert_eq!(child.genes.len(), 10);
712        let mut sorted = child.genes.clone();
713        sorted.sort();
714        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
715    }
716
717    #[test]
718    fn test_permutation_mutation() {
719        let mut rng = rand::rng();
720        let mut chromosome = PermutationChromosome::random_with_options(10, 4, &mut rng);
721
722        chromosome.swap_mutate(&mut rng);
723
724        // Should still be a valid permutation
725        let mut sorted = chromosome.genes.clone();
726        sorted.sort();
727        assert_eq!(sorted, (0..10).collect::<Vec<_>>());
728    }
729}