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