Skip to main content

u_nesting_core/
brkga.rs

1//! Biased Random-Key Genetic Algorithm (BRKGA) framework.
2//!
3//! BRKGA is a variant of genetic algorithms that uses random-key encoding
4//! and biased crossover to favor elite parents.
5//!
6//! # Architecture
7//!
8//! This module maintains its own BRKGA loop rather than delegating to
9//! u-metaheur's `BrkgaDecoder`. u-metaheur uses `decode(&self, &[f64]) -> f64`
10//! (immutable, returns cost), while u-nesting uses `evaluate(&self, &mut
11//! RandomKeyChromosome)` (mutable) to update cached fitness on the chromosome.
12//! This allows the framework to track generation callbacks via `on_generation()`.
13//!
14//! The rand 0.9 API is shared with u-metaheur for ecosystem compatibility.
15//!
16//! # Key Features
17//!
18//! - **Random-key encoding**: Each gene is a float in [0, 1)
19//! - **Biased crossover**: Elite parent is favored during crossover
20//! - **Population partitioning**: Elite, non-elite, and mutant subpopulations
21//!
22//! # References
23//!
24//! Gonçalves, J. F., & Resende, M. G. (2011). Biased random-key genetic algorithms
25//! for combinatorial optimization. Journal of Heuristics, 17(5), 487-525.
26
27use rand::prelude::*;
28#[cfg(feature = "parallel")]
29use rayon::prelude::*;
30use std::sync::atomic::{AtomicBool, Ordering};
31use std::sync::Arc;
32use std::time::Duration;
33
34use crate::error::{check_at_least, check_range, Result};
35use crate::timing::{evaluate_within, expired, Timer};
36
37#[cfg(feature = "serde")]
38use serde::{Deserialize, Serialize};
39
40/// Configuration for BRKGA.
41#[derive(Debug, Clone)]
42#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
43pub struct BrkgaConfig {
44    /// Population size.
45    pub population_size: usize,
46    /// Maximum number of generations.
47    pub max_generations: u32,
48    /// Fraction of population that is elite (0.0 - 1.0).
49    pub elite_fraction: f64,
50    /// Fraction of population that are mutants (0.0 - 1.0).
51    pub mutant_fraction: f64,
52    /// Probability of inheriting gene from elite parent during crossover.
53    pub elite_bias: f64,
54    /// Maximum time limit (None = unlimited).
55    pub time_limit: Option<Duration>,
56    /// Target fitness to stop early (None = run all generations).
57    pub target_fitness: Option<f64>,
58    /// Stagnation generations before early stop.
59    pub stagnation_limit: Option<u32>,
60}
61
62impl Default for BrkgaConfig {
63    fn default() -> Self {
64        Self {
65            population_size: 100,
66            max_generations: 500,
67            elite_fraction: 0.2,   // 20% elite
68            mutant_fraction: 0.15, // 15% mutants
69            elite_bias: 0.7,       // 70% chance to inherit from elite
70            time_limit: None,
71            target_fitness: None,
72            stagnation_limit: Some(50),
73        }
74    }
75}
76
77impl BrkgaConfig {
78    /// Creates a new configuration with default values.
79    pub fn new() -> Self {
80        Self::default()
81    }
82
83    /// Sets the population size.
84    pub fn with_population_size(mut self, size: usize) -> Self {
85        self.population_size = size.max(4);
86        self
87    }
88
89    /// Sets the maximum generations.
90    pub fn with_max_generations(mut self, gen: u32) -> Self {
91        self.max_generations = gen;
92        self
93    }
94
95    /// Sets the elite fraction.
96    pub fn with_elite_fraction(mut self, fraction: f64) -> Self {
97        self.elite_fraction = fraction;
98        self
99    }
100
101    /// Sets the mutant fraction.
102    pub fn with_mutant_fraction(mut self, fraction: f64) -> Self {
103        self.mutant_fraction = fraction;
104        self
105    }
106
107    /// Sets the elite bias for crossover.
108    pub fn with_elite_bias(mut self, bias: f64) -> Self {
109        self.elite_bias = bias;
110        self
111    }
112
113    /// Checks every field against its range; the builders store what they are
114    /// given.
115    ///
116    /// # Errors
117    /// `ConfigError` naming the first field out of range: `elite_fraction`
118    /// outside `[0.01, 0.5]`, `mutant_fraction` outside `[0, 0.5]`, the two
119    /// together above 1, `elite_bias` outside `[0.5, 1]`, a population below 2.
120    pub fn validate(&self) -> Result<()> {
121        check_range("elite_fraction", self.elite_fraction, 0.01, 0.5)?;
122        check_range("mutant_fraction", self.mutant_fraction, 0.0, 0.5)?;
123        check_range("elite_bias", self.elite_bias, 0.5, 1.0)?;
124        check_at_least("population_size", self.population_size as f64, 2.0)?;
125        Ok(())
126    }
127
128    /// Sets the time limit.
129    pub fn with_time_limit(mut self, duration: Duration) -> Self {
130        self.time_limit = Some(duration);
131        self
132    }
133
134    /// Sets the target fitness.
135    pub fn with_target_fitness(mut self, fitness: f64) -> Self {
136        self.target_fitness = Some(fitness);
137        self
138    }
139
140    /// Sets the stagnation limit.
141    pub fn with_stagnation_limit(mut self, limit: u32) -> Self {
142        self.stagnation_limit = Some(limit);
143        self
144    }
145
146    /// Returns the number of elite individuals.
147    pub fn elite_count(&self) -> usize {
148        ((self.population_size as f64) * self.elite_fraction).ceil() as usize
149    }
150
151    /// Returns the number of mutant individuals.
152    pub fn mutant_count(&self) -> usize {
153        ((self.population_size as f64) * self.mutant_fraction).ceil() as usize
154    }
155}
156
157/// Random-key chromosome for BRKGA.
158///
159/// Each gene is a floating-point value in [0, 1) that represents a random key.
160/// The decoder interprets these keys to construct a solution.
161#[derive(Debug, Clone)]
162pub struct RandomKeyChromosome {
163    /// Random keys in [0, 1).
164    pub keys: Vec<f64>,
165    /// Cached fitness value.
166    fitness: f64,
167}
168
169impl RandomKeyChromosome {
170    /// Creates a new chromosome with the given number of keys.
171    pub fn new(num_keys: usize) -> Self {
172        Self {
173            keys: vec![0.0; num_keys],
174            fitness: f64::NEG_INFINITY,
175        }
176    }
177
178    /// Creates a random chromosome.
179    pub fn random<R: Rng>(num_keys: usize, rng: &mut R) -> Self {
180        let keys: Vec<f64> = (0..num_keys).map(|_| rng.random::<f64>()).collect();
181        Self {
182            keys,
183            fitness: f64::NEG_INFINITY,
184        }
185    }
186
187    /// Returns the number of keys.
188    pub fn len(&self) -> usize {
189        self.keys.len()
190    }
191
192    /// Returns true if empty.
193    pub fn is_empty(&self) -> bool {
194        self.keys.is_empty()
195    }
196
197    /// Returns the fitness value.
198    pub fn fitness(&self) -> f64 {
199        self.fitness
200    }
201
202    /// Sets the fitness value.
203    pub fn set_fitness(&mut self, fitness: f64) {
204        self.fitness = fitness;
205    }
206
207    /// Biased crossover with another chromosome.
208    ///
209    /// For each gene, there's an `elite_bias` probability of inheriting
210    /// from `self` (the elite parent) and `1 - elite_bias` from `other`.
211    pub fn biased_crossover<R: Rng>(&self, other: &Self, elite_bias: f64, rng: &mut R) -> Self {
212        let keys: Vec<f64> = self
213            .keys
214            .iter()
215            .zip(&other.keys)
216            .map(|(&elite_key, &non_elite_key)| {
217                if rng.random::<f64>() < elite_bias {
218                    elite_key
219                } else {
220                    non_elite_key
221                }
222            })
223            .collect();
224
225        Self {
226            keys,
227            fitness: f64::NEG_INFINITY,
228        }
229    }
230
231    /// Decodes the random keys into a permutation (sorted indices by key value).
232    ///
233    /// This is the most common decoding: sort items by their random keys
234    /// to get a placement order.
235    pub fn decode_as_permutation(&self) -> Vec<usize> {
236        let mut indices: Vec<usize> = (0..self.keys.len()).collect();
237        indices.sort_by(|&a, &b| {
238            self.keys[a]
239                .partial_cmp(&self.keys[b])
240                .unwrap_or(std::cmp::Ordering::Equal)
241        });
242        indices
243    }
244
245    /// Decodes a subset of keys as discrete values in a range.
246    ///
247    /// For example, to decode orientation (0-5), use:
248    /// `decode_as_discrete(key_idx, 6)` which maps [0, 1) to {0, 1, 2, 3, 4, 5}.
249    pub fn decode_as_discrete(&self, key_idx: usize, num_options: usize) -> usize {
250        if key_idx >= self.keys.len() || num_options == 0 {
251            return 0;
252        }
253        let key = self.keys[key_idx].clamp(0.0, 0.9999999);
254        (key * num_options as f64) as usize
255    }
256}
257
258/// Trait for BRKGA problem-specific operations.
259pub trait BrkgaProblem: Send + Sync {
260    /// Returns the number of random keys needed for one solution.
261    fn num_keys(&self) -> usize;
262
263    /// Evaluates the fitness of a chromosome and updates its fitness value.
264    fn evaluate(&self, chromosome: &mut RandomKeyChromosome);
265
266    /// Evaluates multiple chromosomes in parallel.
267    /// Default implementation uses rayon when the `parallel` feature is enabled.
268    fn evaluate_parallel(&self, chromosomes: &mut [RandomKeyChromosome]) {
269        #[cfg(feature = "parallel")]
270        chromosomes.par_iter_mut().for_each(|c| {
271            self.evaluate(c);
272        });
273        #[cfg(not(feature = "parallel"))]
274        for c in chromosomes.iter_mut() {
275            self.evaluate(c);
276        }
277    }
278
279    /// The population the run starts from.
280    ///
281    /// The default is what BRKGA assumes: `size` chromosomes of random keys.
282    /// A problem that already has a solution -- one a cheap heuristic found,
283    /// or the answer to a previous run -- overrides this to put it in the
284    /// population, so the search improves on it instead of having to rediscover
285    /// it. This is the usual warm start; the rest of the algorithm is
286    /// unchanged, since a seeded chromosome competes on fitness like any other.
287    fn initial_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<RandomKeyChromosome> {
288        (0..size)
289            .map(|_| RandomKeyChromosome::random(self.num_keys(), rng))
290            .collect()
291    }
292
293    /// Called after each generation (for progress reporting).
294    fn on_generation(
295        &self,
296        _generation: u32,
297        _best: &RandomKeyChromosome,
298        _population: &[RandomKeyChromosome],
299    ) {
300        // Default: do nothing
301    }
302}
303
304/// Result of a BRKGA run.
305#[derive(Debug, Clone)]
306pub struct BrkgaResult {
307    /// The best chromosome found.
308    pub best: RandomKeyChromosome,
309    /// Final generation reached.
310    pub generations: u32,
311    /// Total elapsed time.
312    pub elapsed: Duration,
313    /// Whether the target fitness was reached.
314    pub target_reached: bool,
315    /// Fitness history (best fitness per generation).
316    pub history: Vec<f64>,
317}
318
319/// Progress information during BRKGA execution.
320#[derive(Debug, Clone)]
321pub struct BrkgaProgress {
322    /// Current generation number.
323    pub generation: u32,
324    /// Maximum generations configured.
325    pub max_generations: u32,
326    /// Best fitness so far.
327    pub best_fitness: f64,
328    /// Average fitness of current population.
329    pub avg_fitness: f64,
330    /// Elapsed time since start.
331    pub elapsed: Duration,
332    /// Whether the algorithm is still running.
333    pub running: bool,
334}
335
336/// BRKGA runner.
337pub struct BrkgaRunner<P: BrkgaProblem> {
338    config: BrkgaConfig,
339    problem: P,
340    cancelled: Arc<AtomicBool>,
341}
342
343impl<P: BrkgaProblem> BrkgaRunner<P> {
344    /// Creates a new BRKGA runner.
345    ///
346    /// # Errors
347    /// As [`BrkgaConfig::validate`].
348    pub fn new(config: BrkgaConfig, problem: P) -> Result<Self> {
349        Self::with_cancellation(config, problem, Arc::new(AtomicBool::new(false)))
350    }
351
352    /// Creates a runner with a pre-existing cancellation handle.
353    ///
354    /// # Errors
355    /// As [`BrkgaConfig::validate`].
356    pub fn with_cancellation(
357        config: BrkgaConfig,
358        problem: P,
359        cancelled: Arc<AtomicBool>,
360    ) -> Result<Self> {
361        config.validate()?;
362        Ok(Self {
363            config,
364            problem,
365            cancelled,
366        })
367    }
368
369    /// Returns a handle to cancel the algorithm.
370    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
371        self.cancelled.clone()
372    }
373
374    /// Runs the BRKGA algorithm.
375    pub fn run(&self) -> BrkgaResult {
376        self.run_with_rng(&mut rand::rng())
377    }
378
379    /// Runs the BRKGA algorithm with a progress callback.
380    pub fn run_with_progress<F>(&self, progress_callback: F) -> BrkgaResult
381    where
382        F: Fn(BrkgaProgress),
383    {
384        self.run_with_rng_and_progress(&mut rand::rng(), Some(progress_callback))
385    }
386
387    /// Runs the BRKGA algorithm with a specific RNG.
388    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> BrkgaResult {
389        self.run_with_rng_and_progress::<R, fn(BrkgaProgress)>(rng, None)
390    }
391
392    /// Runs the BRKGA algorithm with a specific RNG and optional progress callback.
393    pub fn run_with_rng_and_progress<R: Rng, F>(
394        &self,
395        rng: &mut R,
396        progress_callback: Option<F>,
397    ) -> BrkgaResult
398    where
399        F: Fn(BrkgaProgress),
400    {
401        let start = Timer::now();
402        let mut history = Vec::new();
403        let num_keys = self.problem.num_keys();
404
405        // The problem decides what the run starts from; the default is random.
406        let mut population = self
407            .problem
408            .initial_population(self.config.population_size, rng);
409        // A problem that returns too few (or too many) does not get to change
410        // the population size the caller configured.
411        population.truncate(self.config.population_size);
412        while population.len() < self.config.population_size {
413            population.push(RandomKeyChromosome::random(num_keys, rng));
414        }
415
416        // Evaluate the initial population — up to the time limit
417        evaluate_within(&mut population, &start, self.config.time_limit, |batch| {
418            self.problem.evaluate_parallel(batch)
419        });
420
421        // Sort by fitness (descending - higher is better)
422        population.sort_by(|a, b| {
423            b.fitness()
424                .partial_cmp(&a.fitness())
425                .unwrap_or(std::cmp::Ordering::Equal)
426        });
427
428        let mut best = population[0].clone();
429        let mut best_fitness = best.fitness();
430        let mut stagnation_count = 0u32;
431        let mut generation = 0u32;
432        let mut target_reached = false;
433
434        let elite_count = self.config.elite_count();
435        let mutant_count = self.config.mutant_count();
436
437        while generation < self.config.max_generations {
438            // Check cancellation
439            if self.cancelled.load(Ordering::Relaxed) {
440                break;
441            }
442
443            // Check time limit
444            if expired(&start, self.config.time_limit) {
445                break;
446            }
447
448            // Check target fitness
449            if let Some(target) = self.config.target_fitness {
450                if best_fitness >= target {
451                    target_reached = true;
452                    break;
453                }
454            }
455
456            // Record history
457            history.push(best_fitness);
458
459            // Create new generation
460            let mut new_population = Vec::with_capacity(self.config.population_size);
461
462            // 1. Copy elite individuals directly
463            for elite in population.iter().take(elite_count) {
464                new_population.push(elite.clone());
465            }
466
467            // 2. Generate mutants (completely random new individuals)
468            let mut mutants: Vec<RandomKeyChromosome> = (0..mutant_count)
469                .map(|_| RandomKeyChromosome::random(num_keys, rng))
470                .collect();
471
472            // 3. Fill the rest with biased crossover
473            let crossover_count = self.config.population_size - elite_count - mutant_count;
474            let mut children: Vec<RandomKeyChromosome> = (0..crossover_count)
475                .map(|_| {
476                    // Select one elite parent
477                    let elite_idx = rng.random_range(0..elite_count);
478                    let elite_parent = &population[elite_idx];
479
480                    // Select one non-elite parent
481                    let non_elite_idx = rng.random_range(elite_count..population.len());
482                    let non_elite_parent = &population[non_elite_idx];
483
484                    // Biased crossover
485                    elite_parent.biased_crossover(non_elite_parent, self.config.elite_bias, rng)
486                })
487                .collect();
488
489            // Evaluate the new individuals — up to the time limit; the loop ends on
490            // the next check if it was reached
491            evaluate_within(&mut mutants, &start, self.config.time_limit, |batch| {
492                self.problem.evaluate_parallel(batch)
493            });
494            if expired(&start, self.config.time_limit) {
495                children.clear();
496            }
497            evaluate_within(&mut children, &start, self.config.time_limit, |batch| {
498                self.problem.evaluate_parallel(batch)
499            });
500
501            // Add to new population
502            new_population.extend(mutants);
503            new_population.extend(children);
504
505            // Sort new population
506            new_population.sort_by(|a, b| {
507                b.fitness()
508                    .partial_cmp(&a.fitness())
509                    .unwrap_or(std::cmp::Ordering::Equal)
510            });
511
512            // Update best
513            let new_best_fitness = new_population[0].fitness();
514            if new_best_fitness > best_fitness {
515                best = new_population[0].clone();
516                best_fitness = new_best_fitness;
517                stagnation_count = 0;
518            } else {
519                stagnation_count += 1;
520            }
521
522            // Check stagnation
523            if let Some(limit) = self.config.stagnation_limit {
524                if stagnation_count >= limit {
525                    break;
526                }
527            }
528
529            // Callback to BrkgaProblem
530            self.problem
531                .on_generation(generation, &best, &new_population);
532
533            // Progress callback
534            if let Some(ref callback) = progress_callback {
535                let avg_fitness = new_population.iter().map(|c| c.fitness()).sum::<f64>()
536                    / new_population.len() as f64;
537
538                callback(BrkgaProgress {
539                    generation,
540                    max_generations: self.config.max_generations,
541                    best_fitness,
542                    avg_fitness,
543                    elapsed: start.elapsed(),
544                    running: true,
545                });
546            }
547
548            population = new_population;
549            generation += 1;
550        }
551
552        // Final history entry
553        history.push(best_fitness);
554
555        // Final progress callback indicating completion
556        if let Some(ref callback) = progress_callback {
557            let avg_fitness = population.iter().map(|c| c.fitness()).sum::<f64>()
558                / population.len().max(1) as f64;
559
560            callback(BrkgaProgress {
561                generation,
562                max_generations: self.config.max_generations,
563                best_fitness,
564                avg_fitness,
565                elapsed: start.elapsed(),
566                running: false,
567            });
568        }
569
570        BrkgaResult {
571            best,
572            generations: generation,
573            elapsed: start.elapsed(),
574            target_reached,
575            history,
576        }
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    /// Simple problem: maximize sum of keys (trivially, all keys should be close to 1)
585    struct MaxSumProblem {
586        num_keys: usize,
587    }
588
589    impl BrkgaProblem for MaxSumProblem {
590        fn num_keys(&self) -> usize {
591            self.num_keys
592        }
593
594        fn evaluate(&self, chromosome: &mut RandomKeyChromosome) {
595            let sum: f64 = chromosome.keys.iter().sum();
596            chromosome.set_fitness(sum);
597        }
598    }
599
600    #[test]
601    fn test_brkga_basic() {
602        let config = BrkgaConfig::default()
603            .with_population_size(50)
604            .with_max_generations(50);
605
606        let problem = MaxSumProblem { num_keys: 10 };
607        let runner = BrkgaRunner::new(config, problem).expect("valid config");
608        let result = runner.run();
609
610        // Best should have keys close to 1.0, sum close to 10.0
611        assert!(result.best.fitness() > 5.0);
612    }
613
614    #[test]
615    fn test_random_key_chromosome() {
616        let mut rng = rand::rng();
617        let chromosome = RandomKeyChromosome::random(10, &mut rng);
618
619        assert_eq!(chromosome.len(), 10);
620        for &key in &chromosome.keys {
621            assert!((0.0..1.0).contains(&key));
622        }
623    }
624
625    #[test]
626    fn test_biased_crossover() {
627        let mut rng = rand::rng();
628        let elite = RandomKeyChromosome::random(10, &mut rng);
629        let non_elite = RandomKeyChromosome::random(10, &mut rng);
630
631        let child = elite.biased_crossover(&non_elite, 0.7, &mut rng);
632
633        assert_eq!(child.len(), 10);
634        for &key in &child.keys {
635            assert!((0.0..1.0).contains(&key));
636        }
637    }
638
639    #[test]
640    fn test_decode_as_permutation() {
641        let mut chromosome = RandomKeyChromosome::new(5);
642        chromosome.keys = vec![0.3, 0.1, 0.9, 0.5, 0.2];
643
644        let perm = chromosome.decode_as_permutation();
645
646        // Sorted order: 0.1(1), 0.2(4), 0.3(0), 0.5(3), 0.9(2)
647        assert_eq!(perm, vec![1, 4, 0, 3, 2]);
648    }
649
650    #[test]
651    fn test_decode_as_discrete() {
652        let mut chromosome = RandomKeyChromosome::new(3);
653        chromosome.keys = vec![0.0, 0.5, 0.99];
654
655        // 6 options: 0.0 -> 0, 0.5 -> 3, 0.99 -> 5
656        assert_eq!(chromosome.decode_as_discrete(0, 6), 0);
657        assert_eq!(chromosome.decode_as_discrete(1, 6), 3);
658        assert_eq!(chromosome.decode_as_discrete(2, 6), 5);
659    }
660}