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    /// The order of `n` items held in the first `n` keys: item indices
248    /// `0..n`, ranked by their keys (ties keep index order).
249    ///
250    /// Only the first `n` keys are ranked. A chromosome that also carries
251    /// rotation or orientation keys after them must not rank those: ranking
252    /// every key and taking the first `n` indices picks whichever indices hold
253    /// the smallest keys, rotation keys included, and drops the items they
254    /// displace -- about half of them with uniform keys.
255    pub fn decode_as_permutation(&self, n: usize) -> Vec<usize> {
256        let n = n.min(self.keys.len());
257        let mut indices: Vec<usize> = (0..n).collect();
258        indices.sort_by(|&a, &b| {
259            self.keys[a]
260                .partial_cmp(&self.keys[b])
261                .unwrap_or(std::cmp::Ordering::Equal)
262        });
263        indices
264    }
265
266    /// Decodes a subset of keys as discrete values in a range.
267    ///
268    /// For example, to decode orientation (0-5), use:
269    /// `decode_as_discrete(key_idx, 6)` which maps [0, 1) to {0, 1, 2, 3, 4, 5}.
270    pub fn decode_as_discrete(&self, key_idx: usize, num_options: usize) -> usize {
271        if key_idx >= self.keys.len() || num_options == 0 {
272            return 0;
273        }
274        let key = self.keys[key_idx].clamp(0.0, 0.9999999);
275        (key * num_options as f64) as usize
276    }
277
278    /// The key [`Self::decode_as_discrete`] reads as `choice` of `num_options`
279    /// -- how a known solution is written into a chromosome.
280    ///
281    /// `decode_as_discrete` maps the interval
282    /// `[choice / num_options, (choice + 1) / num_options)` to `choice`; this
283    /// is its midpoint, the furthest a key can be from either edge, so a
284    /// crossover that nudges it still decodes as the same choice.
285    pub fn discrete_key(choice: usize, num_options: usize) -> f64 {
286        if num_options == 0 {
287            return 0.0;
288        }
289        (((choice as f64) + 0.5) / num_options as f64).clamp(0.0, 0.9999999)
290    }
291
292    /// The keys [`Self::decode_as_permutation`] reads back as `order` --
293    /// `order[rank]` is the item placed `rank`-th. Items `order` leaves out
294    /// keep key 0.
295    pub fn permutation_keys(order: &[usize], num_items: usize) -> Vec<f64> {
296        let mut keys = vec![0.0; num_items];
297        let span = order.len().max(1) as f64;
298        for (rank, &item) in order.iter().enumerate() {
299            if let Some(key) = keys.get_mut(item) {
300                *key = ((rank as f64 + 0.5) / span).clamp(0.0, 0.9999999);
301            }
302        }
303        keys
304    }
305}
306
307/// Trait for BRKGA problem-specific operations.
308pub trait BrkgaProblem: Send + Sync {
309    /// Returns the number of random keys needed for one solution.
310    fn num_keys(&self) -> usize;
311
312    /// Evaluates the fitness of a chromosome and updates its fitness value.
313    fn evaluate(&self, chromosome: &mut RandomKeyChromosome);
314
315    /// Evaluates multiple chromosomes in parallel.
316    /// Default implementation uses rayon when the `parallel` feature is enabled.
317    fn evaluate_parallel(&self, chromosomes: &mut [RandomKeyChromosome]) {
318        #[cfg(feature = "parallel")]
319        chromosomes.par_iter_mut().for_each(|c| {
320            self.evaluate(c);
321        });
322        #[cfg(not(feature = "parallel"))]
323        for c in chromosomes.iter_mut() {
324            self.evaluate(c);
325        }
326    }
327
328    /// The population the run starts from.
329    ///
330    /// The default is what BRKGA assumes: `size` chromosomes of random keys.
331    /// A problem that already has a solution -- one a cheap heuristic found,
332    /// or the answer to a previous run -- overrides this to put it in the
333    /// population, so the search improves on it instead of having to rediscover
334    /// it. This is the usual warm start; the rest of the algorithm is
335    /// unchanged, since a seeded chromosome competes on fitness like any other.
336    fn initial_population<R: Rng>(&self, size: usize, rng: &mut R) -> Vec<RandomKeyChromosome> {
337        (0..size)
338            .map(|_| RandomKeyChromosome::random(self.num_keys(), rng))
339            .collect()
340    }
341
342    /// Called after each generation (for progress reporting).
343    fn on_generation(
344        &self,
345        _generation: u32,
346        _best: &RandomKeyChromosome,
347        _population: &[RandomKeyChromosome],
348    ) {
349        // Default: do nothing
350    }
351}
352
353/// Result of a BRKGA run.
354#[derive(Debug, Clone)]
355pub struct BrkgaResult {
356    /// The best chromosome found.
357    pub best: RandomKeyChromosome,
358    /// Final generation reached.
359    pub generations: u32,
360    /// Total elapsed time.
361    pub elapsed: Duration,
362    /// Why the run stopped.
363    pub stop_reason: StopReason,
364    /// Fitness history (best fitness per generation).
365    pub history: Vec<f64>,
366}
367
368/// Progress information during BRKGA execution.
369#[derive(Debug, Clone)]
370pub struct BrkgaProgress {
371    /// Current generation number.
372    pub generation: u32,
373    /// Maximum generations configured.
374    pub max_generations: u32,
375    /// Best fitness so far.
376    pub best_fitness: f64,
377    /// Average fitness of current population.
378    pub avg_fitness: f64,
379    /// Elapsed time since start.
380    pub elapsed: Duration,
381    /// Whether the algorithm is still running.
382    pub running: bool,
383}
384
385/// BRKGA runner.
386pub struct BrkgaRunner<P: BrkgaProblem> {
387    config: BrkgaConfig,
388    problem: P,
389    cancelled: Arc<AtomicBool>,
390}
391
392impl<P: BrkgaProblem> BrkgaRunner<P> {
393    /// Creates a new BRKGA runner.
394    ///
395    /// # Errors
396    /// As [`BrkgaConfig::validate`].
397    pub fn new(config: BrkgaConfig, problem: P) -> Result<Self> {
398        Self::with_cancellation(config, problem, Arc::new(AtomicBool::new(false)))
399    }
400
401    /// Creates a runner with a pre-existing cancellation handle.
402    ///
403    /// # Errors
404    /// As [`BrkgaConfig::validate`].
405    pub fn with_cancellation(
406        config: BrkgaConfig,
407        problem: P,
408        cancelled: Arc<AtomicBool>,
409    ) -> Result<Self> {
410        config.validate()?;
411        Ok(Self {
412            config,
413            problem,
414            cancelled,
415        })
416    }
417
418    /// Returns a handle to cancel the algorithm.
419    pub fn cancel_handle(&self) -> Arc<AtomicBool> {
420        self.cancelled.clone()
421    }
422
423    /// Runs the BRKGA algorithm.
424    pub fn run(&self) -> BrkgaResult {
425        self.run_with_rng(&mut rand::rng())
426    }
427
428    /// Runs the BRKGA algorithm with a progress callback.
429    pub fn run_with_progress<F>(&self, progress_callback: F) -> BrkgaResult
430    where
431        F: Fn(BrkgaProgress),
432    {
433        self.run_with_rng_and_progress(&mut rand::rng(), Some(progress_callback))
434    }
435
436    /// Runs the BRKGA algorithm with a specific RNG.
437    pub fn run_with_rng<R: Rng>(&self, rng: &mut R) -> BrkgaResult {
438        self.run_with_rng_and_progress::<R, fn(BrkgaProgress)>(rng, None)
439    }
440
441    /// Runs the BRKGA algorithm with a specific RNG and optional progress callback.
442    pub fn run_with_rng_and_progress<R: Rng, F>(
443        &self,
444        rng: &mut R,
445        progress_callback: Option<F>,
446    ) -> BrkgaResult
447    where
448        F: Fn(BrkgaProgress),
449    {
450        let start = Timer::now();
451        let mut history = Vec::new();
452        let num_keys = self.problem.num_keys();
453
454        // The problem decides what the run starts from; the default is random.
455        let mut population = self
456            .problem
457            .initial_population(self.config.population_size, rng);
458        // A problem that returns too few (or too many) does not get to change
459        // the population size the caller configured.
460        population.truncate(self.config.population_size);
461        while population.len() < self.config.population_size {
462            population.push(RandomKeyChromosome::random(num_keys, rng));
463        }
464
465        // A report after every evaluated batch, not only every generation: one
466        // generation of a large problem can take seconds, and a caller that
467        // cancels from its callback has to be heard within it. Returns the
468        // best fitness seen, counting the batch.
469        let heartbeat = |generation: u32, best: f64, batch: &[RandomKeyChromosome]| {
470            let best = batch.iter().map(|c| c.fitness()).fold(best, f64::max);
471            if let Some(ref callback) = progress_callback {
472                callback(BrkgaProgress {
473                    generation,
474                    max_generations: self.config.max_generations,
475                    best_fitness: best,
476                    avg_fitness: batch.iter().map(|c| c.fitness()).sum::<f64>()
477                        / batch.len().max(1) as f64,
478                    elapsed: start.elapsed(),
479                    running: true,
480                });
481            }
482            best
483        };
484
485        // Evaluate the initial population — up to the time limit
486        let mut best_so_far = f64::NEG_INFINITY;
487        evaluate_within(&mut population, &start, self.config.time_limit, |batch| {
488            self.problem.evaluate_parallel(batch);
489            best_so_far = heartbeat(0, best_so_far, batch);
490        });
491
492        // Sort by fitness (descending - higher is better)
493        population.sort_by(|a, b| {
494            b.fitness()
495                .partial_cmp(&a.fitness())
496                .unwrap_or(std::cmp::Ordering::Equal)
497        });
498
499        let mut best = population[0].clone();
500        let mut best_fitness = best.fitness();
501        let mut stagnation_count = 0u32;
502        let mut generation = 0u32;
503        let reached = |fitness: f64| self.config.target_fitness.is_some_and(|t| fitness >= t);
504
505        let elite_count = self.config.elite_count();
506        let mutant_count = self.config.mutant_count();
507
508        // Each exit names its reason; the target is judged before the
509        // generation budget, so a target met by the last generation is
510        // reported as met.
511        let stop_reason = loop {
512            if reached(best_fitness) {
513                break StopReason::TargetReached;
514            }
515            if generation >= self.config.max_generations {
516                break StopReason::IterationLimit;
517            }
518            if self.cancelled.load(Ordering::Relaxed) {
519                break StopReason::Cancelled;
520            }
521            if expired(&start, self.config.time_limit) {
522                break StopReason::TimeLimit;
523            }
524
525            // Record history
526            history.push(best_fitness);
527
528            // Create new generation
529            let mut new_population = Vec::with_capacity(self.config.population_size);
530
531            // 1. Copy elite individuals directly
532            for elite in population.iter().take(elite_count) {
533                new_population.push(elite.clone());
534            }
535
536            // 2. Generate mutants (completely random new individuals)
537            let mut mutants: Vec<RandomKeyChromosome> = (0..mutant_count)
538                .map(|_| RandomKeyChromosome::random(num_keys, rng))
539                .collect();
540
541            // 3. Fill the rest with biased crossover
542            let crossover_count = self.config.population_size - elite_count - mutant_count;
543            let mut children: Vec<RandomKeyChromosome> = (0..crossover_count)
544                .map(|_| {
545                    // Select one elite parent
546                    let elite_idx = rng.random_range(0..elite_count);
547                    let elite_parent = &population[elite_idx];
548
549                    // Select one non-elite parent
550                    let non_elite_idx = rng.random_range(elite_count..population.len());
551                    let non_elite_parent = &population[non_elite_idx];
552
553                    // Biased crossover
554                    elite_parent.biased_crossover(non_elite_parent, self.config.elite_bias, rng)
555                })
556                .collect();
557
558            // Evaluate the new individuals — up to the time limit; the loop ends on
559            // the next check if it was reached
560            evaluate_within(&mut mutants, &start, self.config.time_limit, |batch| {
561                self.problem.evaluate_parallel(batch);
562                heartbeat(generation, best_fitness, batch);
563            });
564            if expired(&start, self.config.time_limit) {
565                children.clear();
566            }
567            evaluate_within(&mut children, &start, self.config.time_limit, |batch| {
568                self.problem.evaluate_parallel(batch);
569                heartbeat(generation, best_fitness, batch);
570            });
571
572            // Add to new population
573            new_population.extend(mutants);
574            new_population.extend(children);
575
576            // Sort new population
577            new_population.sort_by(|a, b| {
578                b.fitness()
579                    .partial_cmp(&a.fitness())
580                    .unwrap_or(std::cmp::Ordering::Equal)
581            });
582
583            // Update best
584            let new_best_fitness = new_population[0].fitness();
585            if new_best_fitness > best_fitness {
586                best = new_population[0].clone();
587                best_fitness = new_best_fitness;
588                stagnation_count = 0;
589            } else {
590                stagnation_count += 1;
591            }
592
593            // Check stagnation
594            if let Some(limit) = self.config.stagnation_limit {
595                if stagnation_count >= limit {
596                    break StopReason::NoImprovement;
597                }
598            }
599
600            // Callback to BrkgaProblem
601            self.problem
602                .on_generation(generation, &best, &new_population);
603
604            // Progress callback
605            if let Some(ref callback) = progress_callback {
606                let avg_fitness = new_population.iter().map(|c| c.fitness()).sum::<f64>()
607                    / new_population.len() as f64;
608
609                callback(BrkgaProgress {
610                    generation,
611                    max_generations: self.config.max_generations,
612                    best_fitness,
613                    avg_fitness,
614                    elapsed: start.elapsed(),
615                    running: true,
616                });
617            }
618
619            population = new_population;
620            generation += 1;
621        };
622
623        // Final history entry
624        history.push(best_fitness);
625
626        // Final progress callback indicating completion
627        if let Some(ref callback) = progress_callback {
628            let avg_fitness = population.iter().map(|c| c.fitness()).sum::<f64>()
629                / population.len().max(1) as f64;
630
631            callback(BrkgaProgress {
632                generation,
633                max_generations: self.config.max_generations,
634                best_fitness,
635                avg_fitness,
636                elapsed: start.elapsed(),
637                running: false,
638            });
639        }
640
641        BrkgaResult {
642            best,
643            generations: generation,
644            elapsed: start.elapsed(),
645            stop_reason,
646            history,
647        }
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    /// Simple problem: maximize sum of keys (trivially, all keys should be close to 1)
656    struct MaxSumProblem {
657        num_keys: usize,
658    }
659
660    impl BrkgaProblem for MaxSumProblem {
661        fn num_keys(&self) -> usize {
662            self.num_keys
663        }
664
665        fn evaluate(&self, chromosome: &mut RandomKeyChromosome) {
666            let sum: f64 = chromosome.keys.iter().sum();
667            chromosome.set_fitness(sum);
668        }
669    }
670
671    #[test]
672    fn test_brkga_basic() {
673        let config = BrkgaConfig::default()
674            .with_population_size(50)
675            .with_max_generations(50);
676
677        let problem = MaxSumProblem { num_keys: 10 };
678        let runner = BrkgaRunner::new(config, problem).expect("valid config");
679        let result = runner.run();
680
681        // Best should have keys close to 1.0, sum close to 10.0
682        assert!(result.best.fitness() > 5.0);
683    }
684
685    #[test]
686    fn test_random_key_chromosome() {
687        let mut rng = rand::rng();
688        let chromosome = RandomKeyChromosome::random(10, &mut rng);
689
690        assert_eq!(chromosome.len(), 10);
691        for &key in &chromosome.keys {
692            assert!((0.0..1.0).contains(&key));
693        }
694    }
695
696    #[test]
697    fn test_biased_crossover() {
698        let mut rng = rand::rng();
699        let elite = RandomKeyChromosome::random(10, &mut rng);
700        let non_elite = RandomKeyChromosome::random(10, &mut rng);
701
702        let child = elite.biased_crossover(&non_elite, 0.7, &mut rng);
703
704        assert_eq!(child.len(), 10);
705        for &key in &child.keys {
706            assert!((0.0..1.0).contains(&key));
707        }
708    }
709
710    #[test]
711    fn test_decode_as_permutation() {
712        let mut chromosome = RandomKeyChromosome::new(5);
713        chromosome.keys = vec![0.3, 0.1, 0.9, 0.5, 0.2];
714
715        let perm = chromosome.decode_as_permutation(5);
716
717        // Sorted order: 0.1(1), 0.2(4), 0.3(0), 0.5(3), 0.9(2)
718        assert_eq!(perm, vec![1, 4, 0, 3, 2]);
719        // Only the first n keys are items: the smaller keys after them are not.
720        assert_eq!(chromosome.decode_as_permutation(3), vec![1, 0, 2]);
721    }
722
723    #[test]
724    fn test_decode_as_discrete() {
725        let mut chromosome = RandomKeyChromosome::new(3);
726        chromosome.keys = vec![0.0, 0.5, 0.99];
727
728        // 6 options: 0.0 -> 0, 0.5 -> 3, 0.99 -> 5
729        assert_eq!(chromosome.decode_as_discrete(0, 6), 0);
730        assert_eq!(chromosome.decode_as_discrete(1, 6), 3);
731        assert_eq!(chromosome.decode_as_discrete(2, 6), 5);
732    }
733}