Skip to main content

rlevo_evolution/algorithms/
de.rs

1//! Differential Evolution.
2//!
3//! Classical DE over `Tensor<B, 2>` populations with all common
4//! mutation/crossover variants enumerated in [`DeVariant`].
5//!
6//! # Variants
7//!
8//! | Variant | Mutation formula |
9//! |---|---|
10//! | [`DeVariant::Rand1Bin`], [`DeVariant::Rand1Exp`] | `v = x_{r1} + F · (x_{r2} − x_{r3})` |
11//! | [`DeVariant::Best1Bin`] | `v = x_{best} + F · (x_{r2} − x_{r3})` |
12//! | [`DeVariant::CurrentToBest1Bin`] | `v = x_i + F · (x_{best} − x_i) + F · (x_{r1} − x_{r2})` |
13//! | [`DeVariant::Rand2Bin`] | `v = x_{r1} + F · (x_{r2} − x_{r3}) + F · (x_{r4} − x_{r5})` |
14//!
15//! The suffix `Bin`/`Exp` selects between binomial and exponential
16//! crossover. All index draws reject repeated and self-referential
17//! indices.
18//!
19//! # Hot path
20//!
21//! A fused `CubeCL` kernel for trial-vector construction is tracked as
22//! follow-up work (see [`crate::ops::kernels`]). Until then this module
23//! uses host-sampled indices and composes the update from primitive
24//! tensor ops.
25//!
26//! # Reference
27//!
28//! - Storn & Price (1997), *Differential Evolution — A Simple and
29//!   Efficient Heuristic for Global Optimization over Continuous
30//!   Spaces*.
31
32use std::marker::PhantomData;
33
34use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
35use rand::{Rng, RngExt};
36
37use crate::rng::{SeedPurpose, seed_stream};
38use crate::strategy::{Strategy, StrategyMetrics};
39
40/// Mutation + crossover variant for differential evolution.
41///
42/// # Convergence caveats
43///
44/// Not every variant converges to machine precision on every landscape
45/// within the same budget. On unimodal landscapes like Sphere,
46/// [`Best1Bin`](DeVariant::Best1Bin) and
47/// [`CurrentToBest1Bin`](DeVariant::CurrentToBest1Bin) tend to
48/// **converge prematurely**: the population collapses around the
49/// current best before the differential search has fully explored, and
50/// the per-generation variance `F · (x_{r2} − x_{r3})` shrinks to zero.
51/// Classical DE literature documents this as the core trade-off of
52/// best-biased variants. The crate's integration tests therefore only
53/// require strong *reduction* from the random baseline for those
54/// variants, not optimality — see
55/// `algorithms::de::tests::all_variants_converge_on_sphere_d10` for the
56/// per-variant tolerance choice.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum DeVariant {
59    /// `x_{r1} + F · (x_{r2} − x_{r3})`, binomial crossover. Balanced
60    /// exploration / exploitation; reaches machine precision on Sphere
61    /// within a few hundred generations.
62    Rand1Bin,
63    /// `x_{best} + F · (x_{r2} − x_{r3})`, binomial crossover.
64    ///
65    /// Strong exploitation — the mutation base is always the current
66    /// best, so the population concentrates quickly. Prone to
67    /// **premature convergence** on landscapes where the current best
68    /// is far from the global optimum; on Sphere-D10 with 500 gens this
69    /// variant stalls around `best_fitness ≈ 1` while `Rand1Bin` reaches
70    /// `< 1e-20`.
71    Best1Bin,
72    /// `x_i + F · (x_{best} − x_i) + F · (x_{r1} − x_{r2})`, binomial.
73    ///
74    /// Hybrid of the current individual and the best-so-far. Still
75    /// **prone to premature convergence** because the
76    /// `F · (x_{best} − x_i)` term dominates once the population is
77    /// near the best. Useful on multimodal landscapes where pure-best
78    /// variants get stuck in local basins, less useful on Sphere.
79    CurrentToBest1Bin,
80    /// `x_{r1} + F · (x_{r2} − x_{r3}) + F · (x_{r4} − x_{r5})`,
81    /// binomial. Higher variance than `Rand1Bin` thanks to two
82    /// difference vectors; converges on Sphere but more slowly.
83    Rand2Bin,
84    /// `x_{r1} + F · (x_{r2} − x_{r3})`, exponential crossover.
85    /// Identical mutation to `Rand1Bin`, different crossover mask shape.
86    /// Performance comparable to `Rand1Bin` in practice.
87    Rand1Exp,
88}
89
90impl DeVariant {
91    /// Number of distinct random indices the variant needs (in
92    /// addition to the current individual `i`).
93    const fn random_indices(self) -> usize {
94        match self {
95            DeVariant::Rand1Bin | DeVariant::Rand1Exp => 3,
96            DeVariant::Best1Bin | DeVariant::CurrentToBest1Bin => 2,
97            DeVariant::Rand2Bin => 5,
98        }
99    }
100
101    /// Whether this variant uses exponential crossover.
102    const fn is_exponential(self) -> bool {
103        matches!(self, DeVariant::Rand1Exp)
104    }
105}
106
107/// Static configuration for a [`DifferentialEvolution`] run.
108#[derive(Debug, Clone)]
109pub struct DeConfig {
110    /// Population size (≥ 5 for `Rand2Bin`, ≥ 4 otherwise).
111    pub pop_size: usize,
112    /// Genome dimensionality.
113    pub genome_dim: usize,
114    /// Search-space bounds (initialization and clamping).
115    pub bounds: (f32, f32),
116    /// Differential weight (F). Typical range [0.4, 0.9].
117    pub f: f32,
118    /// Crossover probability (CR). Typical range [0.1, 0.9].
119    pub cr: f32,
120    /// Variant.
121    pub variant: DeVariant,
122}
123
124impl DeConfig {
125    /// Default configuration (`Rand1Bin`, F = 0.5, CR = 0.9) for a given
126    /// dimensionality.
127    #[must_use]
128    pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
129        Self {
130            pop_size,
131            genome_dim,
132            bounds: (-5.12, 5.12),
133            f: 0.5,
134            cr: 0.9,
135            variant: DeVariant::Rand1Bin,
136        }
137    }
138}
139
140/// Generation state for [`DifferentialEvolution`].
141///
142/// The two-phase ask/tell handshake uses `fitness.is_empty()` as a
143/// sentinel: on the very first [`Strategy::ask`] call the initial
144/// population is returned unchanged; on the very first
145/// [`Strategy::tell`] call `fitness` is populated and
146/// `best_genome`/`best_fitness` are initialized. Subsequent
147/// ask/tell cycles produce and evaluate trial vectors.
148#[derive(Debug, Clone)]
149pub struct DeState<B: Backend> {
150    /// Current population, shape `(pop_size, D)`.
151    pub population: Tensor<B, 2>,
152    /// Host-side fitness cache for the current population.
153    ///
154    /// Empty before the first [`Strategy::tell`] call; length `pop_size`
155    /// thereafter. The `is_empty()` check is the sentinel that
156    /// distinguishes the initial evaluation phase from subsequent
157    /// trial-vector generations.
158    pub fitness: Vec<f32>,
159    /// Index of the current best individual within `population`.
160    pub best_index: usize,
161    /// Best-so-far genome, shape `(1, D)`.
162    ///
163    /// `None` before the first [`Strategy::tell`] call.
164    pub best_genome: Option<Tensor<B, 2>>,
165    /// Best-so-far fitness across all completed generations.
166    ///
167    /// `f32::INFINITY` before the first [`Strategy::tell`] call.
168    pub best_fitness: f32,
169    /// Number of completed `tell` calls (zero-based generation index + 1).
170    pub generation: usize,
171}
172
173/// Classical DE/rand/1/bin (and friends).
174///
175/// # Example
176///
177/// ```no_run
178/// use burn::backend::Flex;
179/// use rlevo_evolution::algorithms::de::{DeConfig, DeVariant, DifferentialEvolution};
180///
181/// let strategy = DifferentialEvolution::<Flex>::new();
182/// let mut params = DeConfig::default_for(30, 10);
183/// params.variant = DeVariant::Rand1Bin;
184/// let _ = (strategy, params);
185/// ```
186#[derive(Debug, Clone, Copy, Default)]
187pub struct DifferentialEvolution<B: Backend> {
188    _backend: PhantomData<fn() -> B>,
189}
190
191impl<B: Backend> DifferentialEvolution<B> {
192    /// Builds a new (stateless) strategy object.
193    #[must_use]
194    pub fn new() -> Self {
195        Self {
196            _backend: PhantomData,
197        }
198    }
199
200    fn sample_initial_population(
201        params: &DeConfig,
202        rng: &mut dyn Rng,
203        device: &<B as burn::tensor::backend::BackendTypes>::Device,
204    ) -> Tensor<B, 2> {
205        let (lo, hi) = params.bounds;
206        // Host-sample the initial population from a deterministic
207        // `seed_stream` rather than the process-wide Flex RNG (`B::seed` +
208        // `Tensor::random`), whose draws interleave with sibling tests under
209        // the parallel runner and are not reproducible across schedules.
210        let pop = params.pop_size;
211        let genome_dim = params.genome_dim;
212        let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
213        let mut rows = Vec::with_capacity(pop * genome_dim);
214        for _ in 0..pop * genome_dim {
215            rows.push(lo + (hi - lo) * stream.random::<f32>());
216        }
217        Tensor::<B, 2>::from_data(TensorData::new(rows, [pop, genome_dim]), device)
218    }
219
220    /// Samples `k` indices from `0..pop_size`, all distinct and all
221    /// different from `self_idx`.
222    ///
223    /// # Panics
224    ///
225    /// Panics if `pop_size <= k`, since the rejection loop cannot make
226    /// progress without enough candidates outside `self_idx`.
227    fn sample_distinct_excluding(
228        self_idx: usize,
229        pop_size: usize,
230        k: usize,
231        rng: &mut dyn Rng,
232    ) -> Vec<usize> {
233        assert!(
234            pop_size > k,
235            "DE: pop_size must exceed the number of distinct indices required"
236        );
237        let mut chosen = Vec::with_capacity(k);
238        while chosen.len() < k {
239            let candidate = rng.random_range(0..pop_size);
240            if candidate != self_idx && !chosen.contains(&candidate) {
241                chosen.push(candidate);
242            }
243        }
244        chosen
245    }
246}
247
248impl<B: Backend> Strategy<B> for DifferentialEvolution<B>
249where
250    B::Device: Clone,
251{
252    type Params = DeConfig;
253    type State = DeState<B>;
254    type Genome = Tensor<B, 2>;
255
256    /// Samples the initial population uniformly within `params.bounds`
257    /// and returns a [`DeState`] with an empty fitness cache, signalling
258    /// that the first ask/tell cycle should evaluate the initial
259    /// population rather than generate trial vectors.
260    ///
261    /// Initial sampling goes through [`seed_stream`] rather than
262    /// `B::seed + Tensor::random` to keep results reproducible across
263    /// parallel test threads.
264    fn init(&self, params: &DeConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> DeState<B> {
265        let population = Self::sample_initial_population(params, rng, device);
266        DeState {
267            population,
268            fitness: Vec::new(),
269            best_index: 0,
270            best_genome: None,
271            best_fitness: f32::INFINITY,
272            generation: 0,
273        }
274    }
275
276    /// Proposes the next population of candidate solutions.
277    ///
278    /// **First call (fitness cache empty):** returns the initial
279    /// population from [`DeState::population`] unchanged so the caller
280    /// can evaluate it before any mutation/crossover step.
281    ///
282    /// **Subsequent calls:** for each individual `i` in `0..pop_size`:
283    ///
284    /// 1. Sample the required number of distinct random indices
285    ///    (excluding `i`) via [`seed_stream`] with [`SeedPurpose::Trial`].
286    /// 2. Compute the mutant vector `v_i` according to
287    ///    [`DeConfig::variant`].
288    /// 3. Apply binomial or exponential crossover (also seeded through
289    ///    [`seed_stream`] with [`SeedPurpose::Crossover`]) to blend `v_i`
290    ///    with the current individual, ensuring at least one gene comes
291    ///    from `v_i` (`j_rand` guarantee).
292    /// 4. Clamp the trial genome to `params.bounds`.
293    ///
294    /// The returned state is a clone of the input state; no fitness
295    /// update occurs here — that happens in [`Strategy::tell`].
296    #[allow(clippy::too_many_lines, clippy::many_single_char_names)]
297    fn ask(
298        &self,
299        params: &DeConfig,
300        state: &DeState<B>,
301        rng: &mut dyn Rng,
302        device: &<B as burn::tensor::backend::BackendTypes>::Device,
303    ) -> (Tensor<B, 2>, DeState<B>) {
304        // First call: evaluate the initial population.
305        if state.fitness.is_empty() {
306            return (state.population.clone(), state.clone());
307        }
308
309        let DeConfig {
310            pop_size,
311            genome_dim,
312            f,
313            cr,
314            variant,
315            ..
316        } = *params;
317
318        let mut trial_rng =
319            seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Trial);
320
321        // ------------------------------------------------------------------
322        // 1. Build the mutant vector v_i for every i, host-side gathers.
323        //    We assemble three index tensors (a, b, c [and d, e for rand2])
324        //    and do the arithmetic on-device in one sweep.
325        // ------------------------------------------------------------------
326        let k = variant.random_indices();
327        let mut rand_indices: Vec<Vec<usize>> =
328            (0..k).map(|_| Vec::with_capacity(pop_size)).collect();
329        for i in 0..pop_size {
330            let chosen = Self::sample_distinct_excluding(i, pop_size, k, &mut trial_rng);
331            for (j, idx) in chosen.into_iter().enumerate() {
332                rand_indices[j].push(idx);
333            }
334        }
335
336        let gather = |idxs: &[usize]| -> Tensor<B, 2> {
337            #[allow(clippy::cast_possible_wrap)]
338            let v: Vec<i64> = idxs.iter().map(|&i| i as i64).collect();
339            let t = Tensor::<B, 1, Int>::from_data(TensorData::new(v, [pop_size]), device);
340            state.population.clone().select(0, t)
341        };
342
343        let v = match variant {
344            DeVariant::Rand1Bin | DeVariant::Rand1Exp => {
345                let a = gather(&rand_indices[0]);
346                let b = gather(&rand_indices[1]);
347                let c = gather(&rand_indices[2]);
348                a + (b - c).mul_scalar(f)
349            }
350            DeVariant::Best1Bin => {
351                #[allow(clippy::single_range_in_vec_init)]
352                let best = state
353                    .population
354                    .clone()
355                    .slice([state.best_index..state.best_index + 1])
356                    .expand([pop_size, genome_dim]);
357                let b = gather(&rand_indices[0]);
358                let c = gather(&rand_indices[1]);
359                best + (b - c).mul_scalar(f)
360            }
361            DeVariant::CurrentToBest1Bin => {
362                #[allow(clippy::single_range_in_vec_init)]
363                let best = state
364                    .population
365                    .clone()
366                    .slice([state.best_index..state.best_index + 1])
367                    .expand([pop_size, genome_dim]);
368                let current = state.population.clone();
369                let a = gather(&rand_indices[0]);
370                let b = gather(&rand_indices[1]);
371                current.clone() + (best - current).mul_scalar(f) + (a - b).mul_scalar(f)
372            }
373            DeVariant::Rand2Bin => {
374                let a = gather(&rand_indices[0]);
375                let b = gather(&rand_indices[1]);
376                let c = gather(&rand_indices[2]);
377                let d = gather(&rand_indices[3]);
378                let e = gather(&rand_indices[4]);
379                a + (b - c).mul_scalar(f) + (d - e).mul_scalar(f)
380            }
381        };
382
383        // ------------------------------------------------------------------
384        // 2. Crossover: binomial or exponential. Always preserve at
385        //    least one mutant gene per row (j_rand).
386        // ------------------------------------------------------------------
387        let mut cross_rng = seed_stream(
388            rng.next_u64(),
389            state.generation as u64,
390            SeedPurpose::Crossover,
391        );
392        let mut cross_mask = vec![false; pop_size * genome_dim];
393        if variant.is_exponential() {
394            for row in 0..pop_size {
395                let start = cross_rng.random_range(0..genome_dim);
396                let mut len = 1;
397                while len < genome_dim && cross_rng.random::<f32>() < cr {
398                    len += 1;
399                }
400                for k in 0..len {
401                    let j = (start + k) % genome_dim;
402                    cross_mask[row * genome_dim + j] = true;
403                }
404            }
405        } else {
406            for row in 0..pop_size {
407                let j_rand = cross_rng.random_range(0..genome_dim);
408                for j in 0..genome_dim {
409                    if j == j_rand || cross_rng.random::<f32>() < cr {
410                        cross_mask[row * genome_dim + j] = true;
411                    }
412                }
413            }
414        }
415        #[allow(clippy::cast_possible_wrap)]
416        let mask_int: Vec<i64> = cross_mask.iter().map(|&b| i64::from(b)).collect();
417        let mask_tensor = Tensor::<B, 2, Int>::from_data(
418            TensorData::new(mask_int, [pop_size, genome_dim]),
419            device,
420        );
421        let mask_bool = mask_tensor.equal_elem(1);
422
423        // Where cross_mask == 1, take from v; otherwise from state.population.
424        let trial = state.population.clone().mask_where(mask_bool, v);
425        let (lo, hi) = params.bounds;
426        let trial = trial.clamp(lo, hi);
427
428        (trial, state.clone())
429    }
430
431    /// Consumes the evaluated trial population and advances the state.
432    ///
433    /// **First call (fitness cache empty):** stores the initial
434    /// population's fitness, initializes `best_genome`/`best_fitness`,
435    /// and increments the generation counter. No replacement occurs
436    /// because there are no previous individuals to compare against.
437    ///
438    /// **Subsequent calls:** applies greedy per-slot replacement — each
439    /// trial individual replaces its corresponding current individual if
440    /// and only if `trial_fitness[i] <= state.fitness[i]`. The best-ever
441    /// genome and fitness are updated if the new generation improves on
442    /// `state.best_fitness`.
443    ///
444    /// Returns the updated [`DeState`] and a [`StrategyMetrics`] snapshot
445    /// covering the current generation's fitness distribution.
446    fn tell(
447        &self,
448        _params: &DeConfig,
449        trial: Tensor<B, 2>,
450        fitness: Tensor<B, 1>,
451        mut state: DeState<B>,
452        _rng: &mut dyn Rng,
453    ) -> (DeState<B>, StrategyMetrics) {
454        let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
455
456        // First `tell`: stash fitness for the initial population.
457        if state.fitness.is_empty() {
458            state.fitness.clone_from(&fitness_host);
459            state.best_index = argmin(&fitness_host);
460            state.generation += 1;
461            update_best(&mut state, &trial, &fitness_host);
462            let m = StrategyMetrics::from_host_fitness(
463                state.generation,
464                &fitness_host,
465                state.best_fitness,
466            );
467            state.best_fitness = m.best_fitness_ever;
468            state.population = trial;
469            return (state, m);
470        }
471
472        // Greedy per-slot replacement: trial replaces current iff
473        // trial is at least as good.
474        let device = trial.device();
475        let pop_size = state.fitness.len();
476        let mut replace_mask = vec![0i64; pop_size];
477        let mut new_fit = state.fitness.clone();
478        for i in 0..pop_size {
479            if fitness_host[i] <= state.fitness[i] {
480                replace_mask[i] = 1;
481                new_fit[i] = fitness_host[i];
482            }
483        }
484
485        let mask_int =
486            Tensor::<B, 1, Int>::from_data(TensorData::new(replace_mask, [pop_size]), &device);
487        let mask_bool_row = mask_int.equal_elem(1);
488        let genome_dim = state.population.dims()[1];
489        let mask_bool = mask_bool_row
490            .unsqueeze_dim::<2>(1)
491            .expand([pop_size, genome_dim]);
492        let next_pop = state
493            .population
494            .clone()
495            .mask_where(mask_bool, trial.clone());
496
497        state.population = next_pop;
498        state.fitness.clone_from(&new_fit);
499        state.best_index = argmin(&new_fit);
500        state.generation += 1;
501        update_best(&mut state, &trial, &fitness_host);
502        let m = StrategyMetrics::from_host_fitness(state.generation, &new_fit, state.best_fitness);
503        state.best_fitness = m.best_fitness_ever;
504        (state, m)
505    }
506
507    /// Returns the best-so-far genome and its raw (minimization) fitness.
508    ///
509    /// Returns `None` before the first [`Strategy::tell`] call, when
510    /// `DeState::best_genome` is still `None`.
511    fn best(&self, state: &DeState<B>) -> Option<(Tensor<B, 2>, f32)> {
512        state
513            .best_genome
514            .as_ref()
515            .map(|g| (g.clone(), state.best_fitness))
516    }
517}
518
519fn argmin(xs: &[f32]) -> usize {
520    let mut best_idx = 0usize;
521    let mut best = f32::INFINITY;
522    for (i, &v) in xs.iter().enumerate() {
523        if v < best {
524            best = v;
525            best_idx = i;
526        }
527    }
528    best_idx
529}
530
531fn update_best<B: Backend>(state: &mut DeState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
532    if fitness.is_empty() {
533        return;
534    }
535    let best_idx = argmin(fitness);
536    let best_f = fitness[best_idx];
537    if best_f < state.best_fitness {
538        let device = pop.device();
539        #[allow(clippy::cast_possible_wrap)]
540        let idx =
541            Tensor::<B, 1, Int>::from_data(TensorData::new(vec![best_idx as i64], [1]), &device);
542        state.best_genome = Some(pop.clone().select(0, idx));
543        state.best_fitness = best_f;
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use crate::fitness::FromFitnessEvaluable;
551    use crate::strategy::EvolutionaryHarness;
552    use burn::backend::Flex;
553    use rlevo_core::fitness::FitnessEvaluable;
554    type TestBackend = Flex;
555
556    struct Sphere;
557    struct SphereFit;
558    impl FitnessEvaluable for SphereFit {
559        type Individual = Vec<f64>;
560        type Landscape = Sphere;
561        fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
562            x.iter().map(|v| v * v).sum()
563        }
564    }
565
566    fn run_de(variant: DeVariant, dim: usize, gens: usize) -> f32 {
567        let device = Default::default();
568        let mut params = DeConfig::default_for(30, dim);
569        params.variant = variant;
570        let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
571        let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
572            DifferentialEvolution::<TestBackend>::new(),
573            params,
574            fitness_fn,
575            11,
576            device,
577            gens,
578        );
579        harness.reset();
580        loop {
581            if harness.step(()).done {
582                break;
583            }
584        }
585        harness.latest_metrics().unwrap().best_fitness_ever
586    }
587
588    /// All five DE variants converge on Sphere-D10 within budget.
589    ///
590    /// The Burn Flex backend seeds its RNG through a process-wide
591    /// mutex, so separate `#[test]` functions that call `Tensor::random`
592    /// race on seeding and produce non-deterministic trajectories. This
593    /// single test runs the variants sequentially inside one function
594    /// so their seed state is not contended.
595    ///
596    /// Per-variant tolerance reflects classical characterizations:
597    /// `rand1`/`rand2` converge to optimum, `best1` / current-to-best
598    /// suffer from premature convergence on unimodal landscapes.
599    #[test]
600    fn all_variants_converge_on_sphere_d10() {
601        let rand1bin = run_de(DeVariant::Rand1Bin, 10, 500);
602        assert!(rand1bin < 1e-6, "DE/rand/1/bin best={rand1bin}");
603
604        let rand2bin = run_de(DeVariant::Rand2Bin, 10, 800);
605        assert!(rand2bin < 1e-6, "DE/rand/2/bin best={rand2bin}");
606
607        let rand1exp = run_de(DeVariant::Rand1Exp, 10, 500);
608        assert!(rand1exp < 1e-6, "DE/rand/1/exp best={rand1exp}");
609
610        let best1bin = run_de(DeVariant::Best1Bin, 10, 500);
611        assert!(best1bin < 1.0, "DE/best/1/bin best={best1bin}");
612
613        let c2b = run_de(DeVariant::CurrentToBest1Bin, 10, 500);
614        assert!(c2b < 2.0, "DE/current-to-best/1/bin best={c2b}");
615    }
616}