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