Skip to main content

rlevo_evolution/algorithms/
ga_binary.rs

1//! Binary-coded Genetic Algorithm.
2//!
3//! A canonical GA over `Tensor<B, 2, Int>` populations where every gene is
4//! restricted to `{0, 1}`:
5//!
6//! 1. Evaluate the current population (done externally by the harness).
7//! 2. Select two independent sets of parents via
8//!    [`crate::ops::selection::tournament_indices_host`] (k-tournament).
9//! 3. Recombine via [`crate::ops::crossover::binary_uniform_crossover`]
10//!    (per-gene coin flip with probability `crossover_p`).
11//! 4. Mutate via [`crate::ops::mutation::bit_flip_mutation`]
12//!    (per-gene flip with probability `mutation_rate`).
13//! 5. Replace via fixed elitist policy: the `elitism_k` best parents
14//!    survive; the remaining `pop_size − elitism_k` slots are filled by
15//!    the best offspring.
16//!
17//! Unlike [`crate::algorithms::ga::GeneticAlgorithm`], there is no
18//! enum-selectable replacement policy — only elitist replacement is
19//! supported. Extend [`BinaryGaConfig`] and the `tell` impl if a
20//! generational variant is needed.
21//!
22//! All random draws go through [`crate::rng::seed_stream`] — never
23//! `B::seed` + `Tensor::random` — so per-run results are reproducible
24//! across thread schedules.
25//!
26//! # Fitness convention
27//!
28//! Fitness is canonical (higher is better), matching all other strategies in
29//! this crate. A maximisation benchmark like `OneMax` (`count_ones(genome)`)
30//! plugs in directly; a cost objective is reconciled into canonical space by
31//! the harness/adapter chokepoint rather than hand-negated here.
32//!
33//! # References
34//!
35//! - Holland (1975), *Adaptation in Natural and Artificial Systems*.
36//! - Goldberg (1989), *Genetic Algorithms in Search, Optimization, and
37//!   Machine Learning*.
38
39use std::marker::PhantomData;
40
41use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
42use rand::Rng;
43use rand::RngExt;
44
45use crate::ops::crossover::binary_uniform_crossover;
46use crate::ops::mutation::bit_flip_mutation;
47use crate::ops::selection::{tournament_indices_host, truncation_indices_host};
48use crate::rng::{SeedPurpose, seed_stream};
49use crate::strategy::{Strategy, StrategyMetrics};
50use rlevo_core::config::{self, ConfigError, ConstraintKind, Validate};
51use rlevo_core::probability::Probability;
52
53/// Static configuration for a [`BinaryGeneticAlgorithm`] run.
54#[derive(Debug, Clone)]
55pub struct BinaryGaConfig {
56    /// Population size.
57    pub pop_size: usize,
58    /// Genome dimensionality.
59    pub genome_dim: usize,
60    /// Probability of bit flip per gene. Valid by construction (`[0, 1]`).
61    pub mutation_rate: Probability,
62    /// Probability of taking parent A's bit in uniform crossover. Valid by
63    /// construction (`[0, 1]`).
64    pub crossover_p: Probability,
65    /// Tournament size for parent selection.
66    pub tournament_size: usize,
67    /// Number of elites carried over to the next generation.
68    pub elitism_k: usize,
69}
70
71impl BinaryGaConfig {
72    /// Sensible defaults for small-scale binary optimization.
73    ///
74    /// Mutation rate defaults to `1 / D` (the standard "one expected
75    /// flip per genome" rule from the binary-GA literature).
76    ///
77    /// # Panics
78    ///
79    /// Panics when `genome_dim == 0`: the `1 / D` mutation rate becomes
80    /// non-finite (`1.0 / 0.0 == +∞`), which [`Probability::new`] rejects as
81    /// outside `[0, 1]`. This is the sanctioned builder-time panic on an
82    /// unusable config (ADR 0026); pass a non-zero `genome_dim`.
83    #[must_use]
84    pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
85        Self {
86            pop_size,
87            genome_dim,
88            #[allow(clippy::cast_precision_loss)]
89            mutation_rate: Probability::new(1.0 / genome_dim as f32),
90            crossover_p: Probability::new(0.5),
91            tournament_size: 2,
92            elitism_k: 1,
93        }
94    }
95}
96
97impl Validate for BinaryGaConfig {
98    fn validate(&self) -> Result<(), ConfigError> {
99        const C: &str = "BinaryGaConfig";
100        config::at_least(C, "pop_size", self.pop_size, 1)?;
101        config::nonzero(C, "genome_dim", self.genome_dim)?;
102        // `mutation_rate` / `crossover_p` are `Probability`: valid by
103        // construction (`[0, 1]`, NaN/Inf rejected), so no `in_range` check
104        // here — see ADR 0031.
105        config::at_least(C, "tournament_size", self.tournament_size, 1)?;
106        if self.tournament_size > self.pop_size {
107            return Err(ConfigError {
108                config: C,
109                field: "tournament_size",
110                kind: ConstraintKind::Custom("tournament_size must not exceed pop_size"),
111            });
112        }
113        if self.elitism_k > self.pop_size {
114            return Err(ConfigError {
115                config: C,
116                field: "elitism_k",
117                kind: ConstraintKind::Custom("elitism_k must not exceed pop_size"),
118            });
119        }
120        Ok(())
121    }
122}
123
124/// State for [`BinaryGeneticAlgorithm`].
125#[derive(Debug, Clone)]
126pub struct BinaryGaState<B: Backend> {
127    /// Current population, shape `(pop_size, D)`.
128    pub population: Tensor<B, 2, Int>,
129    /// Host-side fitness cache for the current population. Empty on
130    /// init until the first `tell` call populates it.
131    pub fitness: Vec<f32>,
132    /// Best-so-far genome.
133    pub best_genome: Option<Tensor<B, 2, Int>>,
134    /// Best-so-far fitness.
135    pub best_fitness: f32,
136    /// Generation counter.
137    pub generation: usize,
138}
139
140/// Binary-coded canonical Genetic Algorithm.
141///
142/// # Example
143///
144/// ```no_run
145/// use burn::backend::Flex;
146/// use rlevo_evolution::algorithms::ga_binary::{BinaryGaConfig, BinaryGeneticAlgorithm};
147///
148/// let strategy = BinaryGeneticAlgorithm::<Flex>::new();
149/// let params = BinaryGaConfig::default_for(32, 16);
150/// let _ = (strategy, params);
151/// ```
152#[derive(Debug, Clone, Copy, Default)]
153pub struct BinaryGeneticAlgorithm<B: Backend> {
154    _backend: PhantomData<fn() -> B>,
155}
156
157impl<B: Backend> BinaryGeneticAlgorithm<B> {
158    /// Builds a new (stateless) strategy object.
159    #[must_use]
160    pub fn new() -> Self {
161        Self {
162            _backend: PhantomData,
163        }
164    }
165
166    fn sample_initial_population(
167        params: &BinaryGaConfig,
168        rng: &mut dyn Rng,
169        device: &<B as burn::tensor::backend::BackendTypes>::Device,
170    ) -> Tensor<B, 2, Int> {
171        // Host-sample U[0,1) from a deterministic `seed_stream` rather than
172        // the process-wide Flex RNG (`B::seed` + `Tensor::random`), whose
173        // draws interleave with sibling tests under the parallel runner and
174        // are not reproducible across thread schedules.
175        let pop = params.pop_size;
176        let genome_dim = params.genome_dim;
177        let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
178        let mut rows = Vec::with_capacity(pop * genome_dim);
179        for _ in 0..pop * genome_dim {
180            rows.push(stream.random::<f32>());
181        }
182        let u = Tensor::<B, 2>::from_data(TensorData::new(rows, [pop, genome_dim]), device);
183        u.lower_elem(0.5).int()
184    }
185}
186
187impl<B: Backend> Strategy<B> for BinaryGeneticAlgorithm<B>
188where
189    B::Device: Clone,
190{
191    type Params = BinaryGaConfig;
192    type State = BinaryGaState<B>;
193    type Genome = Tensor<B, 2, Int>;
194
195    /// Build the initial state.
196    ///
197    /// Samples an `(pop_size, D)` binary population uniformly at random
198    /// (each gene independently `Bernoulli(0.5)`) using a host RNG derived
199    /// from `rng`. Sets `fitness` to empty and `best_fitness` to
200    /// `f32::NEG_INFINITY` (the worst value under the maximise convention);
201    /// the first [`tell`](Self::tell) call populates both.
202    fn init(
203        &self,
204        params: &BinaryGaConfig,
205        rng: &mut dyn Rng,
206        device: &<B as burn::tensor::backend::BackendTypes>::Device,
207    ) -> BinaryGaState<B> {
208        debug_assert!(
209            params.validate().is_ok(),
210            "invalid BinaryGaConfig reached init: {params:?}"
211        );
212        BinaryGaState {
213            population: Self::sample_initial_population(params, rng, device),
214            fitness: Vec::new(),
215            best_genome: None,
216            best_fitness: f32::NEG_INFINITY,
217            generation: 0,
218        }
219    }
220
221    /// Propose the next offspring population.
222    ///
223    /// On the very first call (before any [`tell`](Self::tell)), `state.fitness`
224    /// is empty — the harness has not evaluated the seed population yet. In
225    /// that case the unchanged seed population is returned so the harness can
226    /// evaluate and pass it back to `tell`.
227    ///
228    /// On subsequent calls the method runs one full selection → crossover →
229    /// mutation pipeline, deriving three independent host sub-streams from
230    /// `rng` via [`crate::rng::seed_stream`]:
231    ///
232    /// - `SeedPurpose::Selection` — two independent tournament draws
233    ///   (parents A and parents B);
234    /// - `SeedPurpose::Crossover` — per-gene coin flip
235    ///   (probability `crossover_p`);
236    /// - `SeedPurpose::Mutation` — per-gene bit-flip
237    ///   (probability `mutation_rate`).
238    ///
239    /// # Panics
240    ///
241    /// Propagates the programming-error panics of the delegated host operators
242    /// once `state.fitness` is non-empty:
243    /// [`tournament_indices_host`]
244    /// panics if the fitness slice is empty or `tournament_size == 0`, and the
245    /// crossover / mutation operators assume shape-consistent inputs.
246    /// `validate()` rules
247    /// these out for a well-formed config, but `BinaryGaState` / `BinaryGaConfig`
248    /// fields are public and can be mutated into an invalid state after
249    /// validation.
250    fn ask(
251        &self,
252        params: &BinaryGaConfig,
253        state: &BinaryGaState<B>,
254        rng: &mut dyn Rng,
255        device: &<B as burn::tensor::backend::BackendTypes>::Device,
256    ) -> (Tensor<B, 2, Int>, BinaryGaState<B>) {
257        if state.fitness.is_empty() {
258            return (state.population.clone(), state.clone());
259        }
260
261        let mut selection_rng = seed_stream(
262            rng.next_u64(),
263            state.generation as u64,
264            SeedPurpose::Selection,
265        );
266        let mut crossover_rng = seed_stream(
267            rng.next_u64(),
268            state.generation as u64,
269            SeedPurpose::Crossover,
270        );
271        let mut mutation_rng = seed_stream(
272            rng.next_u64(),
273            state.generation as u64,
274            SeedPurpose::Mutation,
275        );
276
277        let idx_a = tournament_indices_host(
278            &state.fitness,
279            params.tournament_size,
280            params.pop_size,
281            &mut selection_rng,
282        );
283        let idx_b = tournament_indices_host(
284            &state.fitness,
285            params.tournament_size,
286            params.pop_size,
287            &mut selection_rng,
288        );
289        let parents_a = state.population.clone().select(
290            0,
291            Tensor::<B, 1, Int>::from_data(TensorData::new(idx_a, [params.pop_size]), device),
292        );
293        let parents_b = state.population.clone().select(
294            0,
295            Tensor::<B, 1, Int>::from_data(TensorData::new(idx_b, [params.pop_size]), device),
296        );
297
298        let offspring = binary_uniform_crossover(
299            parents_a,
300            parents_b,
301            params.crossover_p,
302            &mut crossover_rng,
303            device,
304        );
305
306        let offspring =
307            bit_flip_mutation(offspring, params.mutation_rate, &mut mutation_rng, device);
308
309        (offspring, state.clone())
310    }
311
312    /// Consume offspring fitness and produce the next generation's state.
313    ///
314    /// The first call (when `state.fitness` is empty) caches the seed
315    /// population's fitness and increments the generation counter; no
316    /// replacement is performed.
317    ///
318    /// On subsequent calls the method performs elitist replacement: the
319    /// `elitism_k` highest-fitness parents survive directly, and the remaining
320    /// `pop_size − elitism_k` slots are filled with the best offspring.
321    /// Both selections use [`crate::ops::selection::truncation_indices_host`].
322    ///
323    /// `fitness` must have shape `(pop_size,)` in the canonical maximise
324    /// convention — higher is better. The harness canonicalises a `Minimize`
325    /// objective (negation) before this call, so the strategy stays
326    /// sense-unaware per ADR 0023.
327    ///
328    /// # Panics
329    ///
330    /// Propagates the programming-error panics of the delegated host path on the
331    /// elitist-replacement branch:
332    /// [`truncation_indices_host`]
333    /// and the bare `state.fitness[i]` indexing assume a `pop_size`-consistent
334    /// fitness buffer. `validate()` rules this out for a well-formed config, but
335    /// `BinaryGaState` / `BinaryGaConfig` fields are public and can be mutated
336    /// into an inconsistent state after validation. Also panics if `fitness`
337    /// cannot be read back as `f32`.
338    fn tell(
339        &self,
340        params: &BinaryGaConfig,
341        offspring: Tensor<B, 2, Int>,
342        fitness: Tensor<B, 1>,
343        mut state: BinaryGaState<B>,
344        _rng: &mut dyn Rng,
345    ) -> (BinaryGaState<B>, StrategyMetrics) {
346        let fitness_host = fitness
347            .into_data()
348            .into_vec::<f32>()
349            .expect("fitness tensor must be readable as f32");
350        let device = offspring.device();
351
352        // First `tell`: initial population just evaluated.
353        if state.fitness.is_empty() {
354            state.fitness.clone_from(&fitness_host);
355            state.generation += 1;
356            update_best(&mut state, &offspring, &fitness_host);
357            let m = StrategyMetrics::from_host_fitness(
358                state.generation,
359                &fitness_host,
360                state.best_fitness,
361            );
362            state.best_fitness = m.best_fitness_ever();
363            state.population = offspring;
364            return (state, m);
365        }
366
367        // Elitist replacement on (pop, fitness) × (offspring, fitness).
368        let pop_size = params.pop_size;
369        let k = params.elitism_k.min(pop_size);
370
371        let elite_idx = truncation_indices_host(&state.fitness, k);
372        let elites = state.population.clone().select(
373            0,
374            Tensor::<B, 1, Int>::from_data(TensorData::new(elite_idx.clone(), [k]), &device),
375        );
376        let n_off_keep = pop_size - k;
377        let off_keep_idx = truncation_indices_host(&fitness_host, n_off_keep);
378        let kept_off = offspring.clone().select(
379            0,
380            Tensor::<B, 1, Int>::from_data(
381                TensorData::new(off_keep_idx.clone(), [n_off_keep]),
382                &device,
383            ),
384        );
385        let next_pop = Tensor::cat(vec![elites, kept_off], 0);
386        let mut next_fit = Vec::with_capacity(pop_size);
387        for i in elite_idx {
388            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
389            next_fit.push(state.fitness[i as usize]);
390        }
391        for i in off_keep_idx {
392            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
393            next_fit.push(fitness_host[i as usize]);
394        }
395
396        update_best(&mut state, &next_pop, &next_fit);
397        state.population = next_pop;
398        state.fitness.clone_from(&next_fit);
399        state.generation += 1;
400        let m = StrategyMetrics::from_host_fitness(state.generation, &next_fit, state.best_fitness);
401        state.best_fitness = m.best_fitness_ever();
402        (state, m)
403    }
404
405    /// Return the best-so-far genome and its fitness.
406    ///
407    /// Returns `None` before the first [`tell`](Self::tell) call.
408    /// The fitness value uses the canonical maximise convention (higher is better).
409    fn best(&self, state: &BinaryGaState<B>) -> Option<(Tensor<B, 2, Int>, f32)> {
410        state
411            .best_genome
412            .as_ref()
413            .map(|g| (g.clone(), state.best_fitness))
414    }
415}
416
417fn update_best<B: Backend>(state: &mut BinaryGaState<B>, pop: &Tensor<B, 2, Int>, fitness: &[f32]) {
418    if fitness.is_empty() {
419        return;
420    }
421    let mut best_idx = 0usize;
422    let mut best_f = fitness[0];
423    for (i, &f) in fitness.iter().enumerate().skip(1) {
424        if f > best_f {
425            best_f = f;
426            best_idx = i;
427        }
428    }
429    if best_f > state.best_fitness {
430        let device = pop.device();
431        #[allow(clippy::cast_possible_wrap)]
432        let idx =
433            Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
434        state.best_genome = Some(pop.clone().select(0, idx));
435        state.best_fitness = best_f;
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use crate::fitness::BatchFitnessFn;
443    use crate::strategy::EvolutionaryHarness;
444    use burn::backend::Flex;
445    type TestBackend = Flex;
446
447    #[test]
448    fn default_config_validates() {
449        assert!(BinaryGaConfig::default_for(32, 16).validate().is_ok());
450    }
451
452    #[test]
453    fn rejects_elitism_larger_than_pop() {
454        let mut cfg = BinaryGaConfig::default_for(8, 16);
455        cfg.elitism_k = 16;
456        assert_eq!(cfg.validate().unwrap_err().field, "elitism_k");
457    }
458
459    /// `OneMax` as a native maximisation: `fitness = count_ones`, optimum at
460    /// `D` (all ones).
461    struct OneMax {
462        dim: usize,
463    }
464
465    impl<B: Backend> BatchFitnessFn<B, Tensor<B, 2, Int>> for OneMax {
466        fn evaluate_batch(
467            &mut self,
468            population: &Tensor<B, 2, Int>,
469            device: &<B as burn::tensor::backend::BackendTypes>::Device,
470        ) -> Tensor<B, 1> {
471            let dims = population.dims();
472            let pop_size = dims[0];
473            let data = population
474                .clone()
475                .into_data()
476                .into_vec::<i32>()
477                .expect("genome host-read of a tensor this test just built");
478            let mut fitness = Vec::with_capacity(pop_size);
479            for row in 0..pop_size {
480                let mut ones = 0_u32;
481                for col in 0..self.dim {
482                    if data[row * self.dim + col] != 0 {
483                        ones += 1;
484                    }
485                }
486                #[allow(clippy::cast_precision_loss)]
487                fitness.push(ones as f32);
488            }
489            Tensor::<B, 1>::from_data(TensorData::new(fitness, [pop_size]), device)
490        }
491
492        fn sense(&self) -> rlevo_core::objective::ObjectiveSense {
493            rlevo_core::objective::ObjectiveSense::Maximize
494        }
495    }
496
497    #[test]
498    fn binary_ga_solves_onemax() {
499        let device = Default::default();
500        let dim = 16;
501        let params = BinaryGaConfig::default_for(32, dim);
502        let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
503            BinaryGeneticAlgorithm::<TestBackend>::new(),
504            params,
505            OneMax { dim },
506            7,
507            device,
508            200,
509        )
510        .expect("valid params");
511        harness.reset();
512        loop {
513            if harness.step(()).done {
514                break;
515            }
516        }
517        let best = harness.latest_metrics().unwrap().best_fitness_ever();
518        // OneMax optimum: all ones → fitness == D.
519        #[allow(clippy::cast_precision_loss)]
520        let optimum = dim as f32;
521        approx::assert_relative_eq!(best, optimum, epsilon = 1e-6);
522    }
523
524    /// Runs one `ask` pipeline over a *uniform* population (every parent is the
525    /// same constant bit) and returns the flat offspring bits. With every
526    /// parent identical, uniform crossover is a no-op (both parents agree at
527    /// every gene), so the only source of variation is bit-flip mutation — the
528    /// offspring is therefore a pure function of `mutation_rate`, RNG-invariant.
529    fn ask_over_uniform_population(rate: f32, bit: i32) -> Vec<i32> {
530        use rand::SeedableRng;
531        use rand::rngs::StdRng;
532
533        let device = Default::default();
534        let (pop, dim) = (4usize, 8usize);
535        let mut params = BinaryGaConfig::default_for(pop, dim);
536        params.mutation_rate = Probability::new(rate);
537        let strategy = BinaryGeneticAlgorithm::<TestBackend>::new();
538
539        let population = Tensor::<TestBackend, 2, Int>::from_data(
540            TensorData::new(vec![bit; pop * dim], [pop, dim]),
541            &device,
542        );
543        // Non-empty `fitness` drives the real selection→crossover→mutation path
544        // (an empty cache would short-circuit to returning the seed population).
545        let state = BinaryGaState {
546            population,
547            fitness: vec![1.0; pop],
548            best_genome: None,
549            best_fitness: f32::NEG_INFINITY,
550            generation: 1,
551        };
552        let mut rng = StdRng::seed_from_u64(0);
553        let (offspring, _) = strategy.ask(&params, &state, &mut rng, &device);
554        offspring
555            .into_data()
556            .into_vec::<i32>()
557            .expect("offspring host-read of a tensor this test just built")
558    }
559
560    /// `mutation_rate == 0.0` performs no bit flips: over a uniform all-zero
561    /// population (where crossover is a no-op) the offspring must be all zeros,
562    /// i.e. a subset of the parents' bit values.
563    #[test]
564    fn mutation_rate_zero_flips_no_bits() {
565        let bits = ask_over_uniform_population(0.0, 0);
566        assert!(
567            bits.iter().all(|&b| b == 0),
568            "rate 0.0 must leave every offspring bit at the parent value, got {bits:?}"
569        );
570    }
571
572    /// `mutation_rate == 1.0` flips every bit deterministically: an all-zero
573    /// uniform population becomes all ones after mutation.
574    #[test]
575    fn mutation_rate_one_flips_every_bit() {
576        let bits = ask_over_uniform_population(1.0, 0);
577        assert!(
578            bits.iter().all(|&b| b == 1),
579            "rate 1.0 must flip every 0→1, got {bits:?}"
580        );
581    }
582
583    /// Elitism boundary `elitism_k == pop_size - 1`: all but one slot are the
584    /// top parents, leaving exactly one offspring slot. The `pop_size − 1`
585    /// fittest parents survive (in descending-fitness order) followed by the
586    /// single best offspring, and the champion tracks the best offspring.
587    #[test]
588    fn elitism_k_equals_pop_minus_one_keeps_top_parents_and_one_offspring() {
589        use rand::SeedableRng;
590        use rand::rngs::StdRng;
591
592        let device = Default::default();
593        let (pop, dim) = (4usize, 4usize);
594        let mut params = BinaryGaConfig::default_for(pop, dim);
595        params.elitism_k = pop - 1; // boundary: one offspring slot.
596        let strategy = BinaryGeneticAlgorithm::<TestBackend>::new();
597
598        // Four distinct parents with strictly increasing fitness.
599        let parent_pop = Tensor::<TestBackend, 2, Int>::from_data(
600            TensorData::new(vec![0i32; pop * dim], [pop, dim]),
601            &device,
602        );
603        let state = BinaryGaState {
604            population: parent_pop,
605            fitness: vec![1.0, 2.0, 3.0, 4.0],
606            best_genome: None,
607            best_fitness: 4.0,
608            generation: 1,
609        };
610
611        // Offspring: only row 0 beats every parent; the rest are worst.
612        let offspring = Tensor::<TestBackend, 2, Int>::from_data(
613            TensorData::new(vec![1i32; pop * dim], [pop, dim]),
614            &device,
615        );
616        let off_fitness = Tensor::<TestBackend, 1>::from_data(
617            TensorData::new(vec![10.0f32, 0.0, 0.0, 0.0], [pop]),
618            &device,
619        );
620
621        let mut rng = StdRng::seed_from_u64(0);
622        let (next, m) = strategy.tell(&params, offspring, off_fitness, state, &mut rng);
623
624        // Top `pop-1` parents (fitness 4,3,2 in descending order) then the best
625        // offspring (fitness 10).
626        assert_eq!(next.population.dims(), [pop, dim]);
627        assert_eq!(next.fitness, vec![4.0, 3.0, 2.0, 10.0]);
628        approx::assert_relative_eq!(m.best_fitness_ever(), 10.0, epsilon = 1e-6);
629    }
630}