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