Skip to main content

fcmaes_core/
mode.rs

1// Numeric kernels index several parallel arrays by a shared loop counter, where
2// range loops read more clearly than zipped iterators.
3#![allow(clippy::needless_range_loop, clippy::manual_memcpy)]
4
5//! Multi-objective Differential Evolution (MODE).
6//!
7//! Multi-objective / constrained Differential Evolution (DE/all/1) with an
8//! optional NSGA-II-style population update. Features enhanced multiple
9//! constraint ranking, oscillating CR/F, SBX + polynomial variation, mixed
10//! integer handling, and normalized all-objective crowding distance.
11//!
12//! Ask/tell only (the caller evaluates objectives+constraints and feeds them
13//! back), so the optimizer can drive Rust threads, a GPU batch, or an external
14//! evaluator without embedding a callback.
15//!
16//! # References
17//!
18//! - R. Storn and K. Price, [Differential
19//!   Evolution](https://doi.org/10.1023/A:1008202821328) (1997).
20//! - K. Deb, A. Pratap, S. Agarwal, and T. Meyarivan, [“A Fast and Elitist
21//!   Multiobjective Genetic Algorithm:
22//!   NSGA-II”](https://doi.org/10.1109/4235.996017), *IEEE Transactions on
23//!   Evolutionary Computation* 6(2), 182–197 (2002).
24//!
25//! # Example
26//!
27//! ```
28//! use fcmaes_core::{Fitness, Mode, ModeParams};
29//!
30//! let fit = Fitness::bounded(2, 2, &[0.0; 2], &[2.0; 2]);
31//! let mut mode = Mode::new(fit, 2, 0, None, &ModeParams::default());
32//! for _ in 0..5 {
33//!     let xs = mode.ask();
34//!     let ys: Vec<Vec<f64>> = xs
35//!         .iter()
36//!         .map(|x| vec![
37//!             x.iter().map(|v| v * v).sum(),
38//!             x.iter().map(|v| (v - 2.0).powi(2)).sum(),
39//!         ])
40//!         .collect();
41//!     mode.tell(&ys);
42//! }
43//! assert_eq!(mode.population().len(), mode.popsize());
44//! ```
45
46use crate::fitness::Fitness;
47use crate::rng::Rng;
48
49const BIG: f64 = f64::MAX;
50
51/// Outcome/result snapshot of a MODE run.
52#[derive(Clone, Debug)]
53pub struct ModeResult {
54    /// Current population (rows = individuals, `dim` columns).
55    pub x: Vec<Vec<f64>>,
56    /// Objective+constraint values of the population.
57    pub y: Vec<Vec<f64>>,
58    /// Number of completed population updates.
59    pub iterations: i32,
60    /// Current termination code.
61    pub stop: i32,
62}
63
64/// Tunable inputs for [`Mode::new`].
65#[derive(Clone, Debug)]
66pub struct ModeParams {
67    /// Number of individuals in the population.
68    pub popsize: i32,
69    /// Differential mutation weight.
70    pub f: f64,
71    /// Differential crossover probability.
72    pub cr: f64,
73    /// Simulated-binary-crossover probability.
74    pub pro_c: f64,
75    /// Simulated-binary-crossover distribution index.
76    pub dis_c: f64,
77    /// Polynomial-mutation probability.
78    pub pro_m: f64,
79    /// Polynomial-mutation distribution index.
80    pub dis_m: f64,
81    /// Use the NSGA-II-style population update.
82    pub nsga_update: bool,
83    /// Probability of selecting the Pareto update when update modes are mixed.
84    pub pareto_update: f64,
85    /// Minimum mixed-integer mutation probability.
86    pub min_mutate: f64,
87    /// Maximum mixed-integer mutation probability.
88    pub max_mutate: f64,
89    /// Seed for the optimizer's independent random stream.
90    pub seed: u64,
91    /// Additional run identifier mixed into [`seed`](Self::seed).
92    pub runid: i64,
93}
94
95impl Default for ModeParams {
96    fn default() -> Self {
97        Self {
98            popsize: 64,
99            f: 0.5,
100            cr: 0.9,
101            pro_c: 0.5,
102            dis_c: 15.0,
103            pro_m: 0.9,
104            dis_m: 20.0,
105            nsga_update: true,
106            pareto_update: 0.0,
107            min_mutate: 0.1,
108            max_mutate: 0.5,
109            seed: 0,
110            runid: 0,
111        }
112    }
113}
114
115fn sort_index(v: &[f64]) -> Vec<usize> {
116    let mut idx: Vec<usize> = (0..v.len()).collect();
117    idx.sort_by(|&a, &b| v[a].total_cmp(&v[b]));
118    idx
119}
120
121fn validate_mode_inputs(
122    fitfun: &Fitness,
123    nobj: usize,
124    ncon: usize,
125    ints: Option<&[bool]>,
126    p: &ModeParams,
127) -> Result<(), &'static str> {
128    if fitfun.dim() == 0 || !fitfun.has_bounds() {
129        return Err("MODE requires a non-empty bounded decision space");
130    }
131    if fitfun
132        .lower()
133        .iter()
134        .zip(fitfun.upper())
135        .any(|(&lo, &hi)| !lo.is_finite() || !hi.is_finite() || lo >= hi)
136    {
137        return Err("MODE bounds must be finite and satisfy lower < upper");
138    }
139    if nobj == 0 || fitfun.nobj() != nobj + ncon {
140        return Err("MODE requires nobj > 0 and Fitness::nobj == nobj + ncon");
141    }
142    let popsize = if p.popsize > 0 { p.popsize } else { 128 };
143    if popsize < 4 {
144        return Err("MODE population size must be at least four");
145    }
146    if ints.is_some_and(|values| values.len() != fitfun.dim()) {
147        return Err("MODE integer mask length must equal the decision dimension");
148    }
149    if !p.f.is_finite()
150        || !p.cr.is_finite()
151        || !p.pro_c.is_finite()
152        || !p.dis_c.is_finite()
153        || !p.pro_m.is_finite()
154        || !p.dis_m.is_finite()
155        || !p.pareto_update.is_finite()
156        || !p.min_mutate.is_finite()
157        || !p.max_mutate.is_finite()
158    {
159        return Err("MODE parameters must be finite");
160    }
161    if !(0.0..=1.0).contains(&p.pro_c) || !(0.0..=1.0).contains(&p.pro_m) {
162        return Err("MODE crossover and mutation probabilities must be in [0, 1]");
163    }
164    if p.dis_c <= 0.0 || p.dis_m <= 0.0 {
165        return Err("MODE distribution indices must be positive");
166    }
167    if p.min_mutate > 0.0 && p.max_mutate > 0.0 && p.min_mutate > p.max_mutate {
168        return Err("MODE min_mutate must not exceed max_mutate");
169    }
170    Ok(())
171}
172
173/// Stateful constrained MODE optimizer.
174///
175/// Objective columns precede constraint columns in every row passed to
176/// [`tell`](Self::tell); constraints are feasible when they are non-positive.
177pub struct Mode {
178    fitfun: Fitness,
179    rng: Rng,
180    dim: usize,
181    nobj: usize,
182    ncon: usize,
183    nobj_ncon: usize,
184    popsize: usize,
185
186    f0: f64,
187    cr0: f64,
188    f: f64,
189    cr: f64,
190    pro_c: f64,
191    dis_c: f64,
192    pro_m: f64,
193    dis_m: f64,
194    nsga_update: bool,
195    pareto_update: f64,
196    min_mutate: f64,
197    max_mutate: f64,
198    is_int: Option<Vec<bool>>,
199
200    // population: [0..popsize] current, [popsize..2*popsize] offspring
201    pop_x: Vec<Vec<f64>>,
202    pop_y: Vec<Vec<f64>>,
203    v_x: Vec<Vec<f64>>, // NSGA variation buffer
204    vp: usize,
205
206    last_con: Option<Vec<Vec<f64>>>,
207    last_eps: Vec<f64>,
208
209    iterations: i32,
210    stop: i32,
211    pending: bool,
212}
213
214impl Mode {
215    /// Construct MODE after validating dimensions, bounds, population size,
216    /// probabilities, and the optional integer mask.
217    ///
218    /// # Errors
219    ///
220    /// Returns an error if the fitness dimension or bounds are inconsistent,
221    /// if `nobj` is zero, if the population size is below four, if any
222    /// probability parameter is outside its valid range, or if a supplied
223    /// integer mask does not have one entry per decision variable.
224    pub fn try_new(
225        fitfun: Fitness,
226        nobj: usize,
227        ncon: usize,
228        ints: Option<Vec<bool>>,
229        p: &ModeParams,
230    ) -> Result<Self, &'static str> {
231        validate_mode_inputs(&fitfun, nobj, ncon, ints.as_deref(), p)?;
232        Ok(Self::new_unchecked(fitfun, nobj, ncon, ints, p))
233    }
234
235    /// Construct MODE, panicking on invalid configuration. Applications that
236    /// accept user input should prefer [`Mode::try_new`].
237    ///
238    /// # Panics
239    ///
240    /// Panics on any configuration [`Mode::try_new`] rejects.
241    pub fn new(
242        fitfun: Fitness,
243        nobj: usize,
244        ncon: usize,
245        ints: Option<Vec<bool>>,
246        p: &ModeParams,
247    ) -> Self {
248        Self::try_new(fitfun, nobj, ncon, ints, p).expect("invalid MODE configuration")
249    }
250
251    fn new_unchecked(
252        fitfun: Fitness,
253        nobj: usize,
254        ncon: usize,
255        ints: Option<Vec<bool>>,
256        p: &ModeParams,
257    ) -> Self {
258        let dim = fitfun.dim();
259        let popsize = if p.popsize > 0 {
260            p.popsize as usize
261        } else {
262            128
263        };
264        let f0 = if p.f > 0.0 { p.f } else { 0.5 };
265        let cr0 = if p.cr > 0.0 { p.cr } else { 0.9 };
266        let mut m = Mode {
267            dim,
268            nobj,
269            ncon,
270            nobj_ncon: nobj + ncon,
271            popsize,
272            f0,
273            cr0,
274            f: f0,
275            cr: cr0,
276            pro_c: p.pro_c,
277            dis_c: p.dis_c,
278            pro_m: p.pro_m,
279            dis_m: p.dis_m,
280            nsga_update: p.nsga_update,
281            pareto_update: p.pareto_update,
282            min_mutate: if p.min_mutate > 0.0 {
283                p.min_mutate
284            } else {
285                0.1
286            },
287            max_mutate: if p.max_mutate > 0.0 {
288                p.max_mutate
289            } else {
290                0.5
291            },
292            is_int: ints,
293            pop_x: vec![],
294            pop_y: vec![],
295            v_x: vec![],
296            vp: 0,
297            last_con: None,
298            last_eps: vec![0.0; ncon],
299            iterations: 0,
300            stop: 0,
301            pending: false,
302            rng: Rng::new(p.seed.wrapping_add(p.runid as u64)),
303            fitfun,
304        };
305        m.init();
306        m
307    }
308
309    fn init(&mut self) {
310        let n = 2 * self.popsize;
311        self.pop_x = (0..n)
312            .map(|i| {
313                if i < self.popsize {
314                    self.fitfun.sample(&mut self.rng)
315                } else {
316                    vec![0.0; self.dim]
317                }
318            })
319            .collect();
320        self.pop_y = vec![vec![BIG; self.nobj_ncon]; n];
321        self.v_x = self.pop_x[0..self.popsize].to_vec();
322        self.vp = 0;
323        self.pending = false;
324    }
325
326    /// Number of decision variables.
327    pub fn dim(&self) -> usize {
328        self.dim
329    }
330    /// Number of objective columns.
331    pub fn nobj(&self) -> usize {
332        self.nobj
333    }
334    /// Number of constraint columns.
335    pub fn ncon(&self) -> usize {
336        self.ncon
337    }
338    /// Number of individuals evaluated per batch.
339    pub fn popsize(&self) -> usize {
340        self.popsize
341    }
342    /// Current termination code.
343    pub fn stop(&self) -> i32 {
344        self.stop
345    }
346
347    // ---- variation (SBX crossover + polynomial mutation) ----
348
349    fn variation(&mut self, pop: &[Vec<f64>]) -> Vec<Vec<f64>> {
350        let dim = self.dim;
351        let dis_c = (0.5 * self.rng.uniform01() + 0.5) * self.dis_c;
352        let dis_m = (0.5 * self.rng.uniform01() + 0.5) * self.dis_m;
353        let n2 = pop.len() / 2;
354        let n = 2 * n2;
355        // beta[p][i]
356        let mut beta = vec![vec![0.0; dim]; n2];
357        for pb in beta.iter_mut() {
358            let cross_pair = self.rng.uniform01() < self.pro_c;
359            for i in 0..dim {
360                if !cross_pair || self.rng.uniform01() < 0.5 {
361                    pb[i] = 1.0;
362                } else {
363                    let r = self.rng.uniform01();
364                    let mut b = if r <= 0.5 {
365                        (2.0 * r).powf(1.0 / (dis_c + 1.0))
366                    } else {
367                        (2.0 * r).powf(-1.0 / (dis_c + 1.0))
368                    };
369                    if self.rng.uniform01() > 0.5 {
370                        b = -b;
371                    }
372                    pb[i] = b;
373                }
374            }
375        }
376        let mut offspring: Vec<Vec<f64>> = Vec::with_capacity(n);
377        let mut off2: Vec<Vec<f64>> = Vec::with_capacity(n2);
378        for p in 0..n2 {
379            let p1 = &pop[p];
380            let p2 = &pop[n2 + p];
381            let mut o1 = vec![0.0; dim];
382            let mut o2 = vec![0.0; dim];
383            for i in 0..dim {
384                let base = (p1[i] + p2[i]) * 0.5;
385                let delta = beta[p][i] * (p1[i] - p2[i]) * 0.5;
386                o1[i] = base + delta;
387                o2[i] = base - delta;
388            }
389            offspring.push(o1);
390            off2.push(o2);
391        }
392        offspring.extend(off2);
393
394        // The Python implementation truncates odd populations, which leaves
395        // the ask/tell batch one candidate short. Preserve the final parent so
396        // polynomial mutation can still produce exactly `pop.len()` children.
397        if offspring.len() < pop.len() {
398            offspring.push(pop[pop.len() - 1].clone());
399        }
400
401        let limit = self.pro_m / dim as f64;
402        for op in offspring.iter_mut() {
403            for i in 0..dim {
404                if self.rng.uniform01() < limit {
405                    let mu = self.rng.uniform01();
406                    let norm = self.fitfun.norm_i(i, op[i]);
407                    let scale = self.fitfun.scale()[i];
408                    if mu <= 0.5 {
409                        op[i] += scale
410                            * ((2.0 * mu + (1.0 - 2.0 * mu) * (1.0 - norm).powf(dis_m + 1.0))
411                                .powf(1.0 / (dis_m + 1.0))
412                                - 1.0);
413                    } else {
414                        op[i] += scale
415                            * (1.0
416                                - (2.0 * (1.0 - mu)
417                                    + 2.0 * (mu - 0.5) * (1.0 - norm).powf(dis_m + 1.0))
418                                .powf(1.0 / (dis_m + 1.0)));
419                    }
420                }
421            }
422        }
423        for op in offspring.iter_mut() {
424            *op = self.fitfun.closest_feasible(op);
425        }
426        offspring
427    }
428
429    fn modify(&mut self, x: &mut [f64]) {
430        let Some(is_int) = self.is_int.clone() else {
431            return;
432        };
433        let n_ints = is_int.iter().filter(|&&b| b).count() as f64;
434        if n_ints == 0.0 {
435            return;
436        }
437        let to_mutate =
438            self.min_mutate + self.rng.uniform01() * (self.max_mutate - self.min_mutate);
439        for i in 0..self.dim {
440            if is_int[i] && self.rng.uniform01() < to_mutate / n_ints {
441                x[i] = self.fitfun.sample_i(i, &mut self.rng).trunc();
442            }
443        }
444    }
445
446    fn next_x(&mut self, p: usize) -> Vec<f64> {
447        if p == 0 {
448            self.iterations += 1;
449        }
450        if self.nsga_update {
451            let x = self.v_x[self.vp].clone();
452            self.vp = (self.vp + 1) % self.v_x.len();
453            return x;
454        }
455        if p == 0 {
456            self.cr = if self.iterations % 2 == 0 {
457                0.5 * self.cr0
458            } else {
459                self.cr0
460            };
461            self.f = if self.iterations % 2 == 0 {
462                0.5 * self.f0
463            } else {
464                self.f0
465            };
466        }
467        let ps = self.popsize;
468        let (mut r1, mut r2, mut r3);
469        loop {
470            r1 = self.rng.int_below(ps as i64) as usize;
471            r2 = self.rng.int_below(ps as i64) as usize;
472            r3 = if self.pareto_update > 0.0 {
473                (self.rng.uniform01().powf(1.0 + self.pareto_update) * ps as f64) as usize
474            } else {
475                self.rng.int_below(ps as i64) as usize
476            };
477            if r3 != p && r3 != r1 && r3 != r2 && r2 != p && r2 != r1 && r1 != p {
478                break;
479            }
480        }
481        let xp = self.pop_x[p].clone();
482        let x1 = &self.pop_x[r1];
483        let x2 = &self.pop_x[r2];
484        let x3 = &self.pop_x[r3];
485        let mut x: Vec<f64> = (0..self.dim)
486            .map(|j| x3[j] + (x1[j] - x2[j]) * self.f)
487            .collect();
488        let r = self.rng.int_below(self.dim as i64) as usize;
489        for j in 0..self.dim {
490            if j != r && self.rng.uniform01() > self.cr {
491                x[j] = xp[j];
492            }
493        }
494        self.modify(&mut x);
495        self.fitfun.closest_feasible(&x)
496    }
497
498    // ---- pareto ranking ----
499
500    /// `true` if individual `i` is dominated by `index` (index is <= i in all
501    /// objectives). `objs[k]` is the length-`nobj` objective vector of k.
502    fn is_dominated(objs: &[Vec<f64>], i: usize, index: usize) -> bool {
503        for j in 0..objs[i].len() {
504            if objs[i][j] < objs[index][j] {
505                return false;
506            }
507        }
508        true
509    }
510
511    fn pareto_levels(objs: &[Vec<f64>]) -> Vec<f64> {
512        let n = objs.len();
513        let mut domination = vec![0.0; n];
514        let mut mask = vec![true; n];
515        let mut index = 0;
516        while index < n {
517            for i in 0..n {
518                if i != index && mask[i] && Self::is_dominated(objs, i, index) {
519                    mask[i] = false;
520                }
521            }
522            for i in 0..n {
523                if mask[i] {
524                    domination[i] += 1.0;
525                }
526            }
527            index += 1;
528            while index < n && !mask[index] {
529                index += 1;
530            }
531        }
532        domination
533    }
534
535    fn objranks(objs: &[Vec<f64>]) -> Vec<f64> {
536        let n = objs.len();
537        let nobj = objs[0].len();
538        let mut rank_sum = vec![0.0; n];
539        for j in 0..nobj {
540            let col: Vec<f64> = objs.iter().map(|o| o[j]).collect();
541            let order = sort_index(&col);
542            for (pos, &idx) in order.iter().enumerate() {
543                rank_sum[idx] += pos as f64;
544            }
545        }
546        rank_sum
547    }
548
549    fn ranks(cons: &[Vec<f64>], eps: &[f64]) -> Vec<f64> {
550        let n = cons.len();
551        let ncon = eps.len();
552        let mut rank = vec![vec![0.0; ncon]; n];
553        let mut alpha = vec![0.0; n];
554        for j in 0..ncon {
555            let col: Vec<f64> = cons.iter().map(|c| c[j]).collect();
556            let order = sort_index(&col);
557            for (pos, &idx) in order.iter().enumerate() {
558                if cons[idx][j] <= eps[j] {
559                    rank[idx][j] = 0.0;
560                } else {
561                    rank[idx][j] = pos as f64;
562                    alpha[idx] += 1.0;
563                }
564            }
565        }
566        let mut csum = vec![0.0; n];
567        for i in 0..n {
568            for j in 0..ncon {
569                csum[i] += rank[i][j] * alpha[i] / ncon as f64;
570            }
571        }
572        csum
573    }
574
575    fn pareto(&mut self, ys: &[Vec<f64>]) -> Vec<f64> {
576        if self.ncon == 0 {
577            return Self::pareto_levels(ys);
578        }
579        let popn = ys.len();
580        let objs: Vec<Vec<f64>> = ys.iter().map(|y| y[0..self.nobj].to_vec()).collect();
581        let cons: Vec<Vec<f64>> = ys
582            .iter()
583            .map(|y| {
584                y[self.nobj..self.nobj_ncon]
585                    .iter()
586                    .map(|&c| c.max(0.0))
587                    .collect()
588            })
589            .collect();
590
591        let mut eps = vec![0.0; self.ncon];
592        if self.iterations > 1
593            && let Some(last) = &self.last_con
594        {
595            let last_max = last
596                .iter()
597                .flat_map(|c| c.iter().cloned())
598                .fold(f64::MIN, f64::max);
599            if last_max < 1e90 {
600                let mut eps_mean = vec![0.0; self.ncon];
601                for j in 0..self.ncon {
602                    let mean_j = last.iter().map(|c| c[j]).sum::<f64>() / last.len() as f64;
603                    eps_mean[j] = 0.5 * (self.last_eps[j] + 0.5 * mean_j);
604                }
605                if eps_mean.iter().cloned().fold(f64::MIN, f64::max) > 1e-8 {
606                    eps = eps_mean;
607                }
608            }
609        }
610        self.last_con = Some(cons.clone());
611        self.last_eps = eps.clone();
612
613        let feasible: Vec<bool> = cons
614            .iter()
615            .map(|c| c.iter().zip(&eps).all(|(&cv, &ev)| cv <= ev))
616            .collect();
617        let has_feasible = feasible.iter().any(|&f| f);
618        let has_infeasible = feasible.iter().any(|&f| !f);
619
620        let mut csum = Self::ranks(&cons, &eps);
621        if has_feasible {
622            let orank = Self::objranks(&objs);
623            for i in 0..popn {
624                csum[i] += orank[i];
625            }
626        }
627        let ci = sort_index(&csum);
628        let mut fiv = vec![];
629        let mut viv = vec![];
630        for &i in &ci {
631            if feasible[i] {
632                fiv.push(i);
633            } else {
634                viv.push(i);
635            }
636        }
637        let mut domination = vec![0.0; popn];
638        if has_feasible {
639            let feas_objs: Vec<Vec<f64>> = fiv.iter().map(|&i| objs[i].clone()).collect();
640            let ypar = Self::pareto_levels(&feas_objs);
641            for (k, &i) in fiv.iter().enumerate() {
642                domination[i] += ypar[k];
643            }
644        }
645        if has_infeasible {
646            for (i, &vi) in viv.iter().enumerate() {
647                domination[vi] += (viv.len() - i) as f64;
648            }
649            for &fi in &fiv {
650                domination[fi] += (viv.len() + 1) as f64;
651            }
652        }
653        domination
654    }
655
656    fn crowd_dist(sub: &[Vec<f64>], nobj: usize) -> Vec<f64> {
657        let n = sub.len();
658        if n == 0 {
659            return Vec::new();
660        }
661        if n <= 2 {
662            return vec![BIG; n];
663        }
664        let mut distance = vec![0.0; n];
665        for objective in 0..nobj {
666            let values: Vec<f64> = sub.iter().map(|y| y[objective]).collect();
667            let order = sort_index(&values);
668            let lo = values[order[0]];
669            let hi = values[order[n - 1]];
670            let span = hi - lo;
671            if !span.is_finite() || span <= 0.0 {
672                continue;
673            }
674            distance[order[0]] = BIG;
675            distance[order[n - 1]] = BIG;
676            for position in 1..n - 1 {
677                let index = order[position];
678                if distance[index] != BIG {
679                    distance[index] +=
680                        (values[order[position + 1]] - values[order[position - 1]]) / span;
681                }
682            }
683        }
684        if distance.iter().all(|&value| value == 0.0) {
685            return vec![0.0; n];
686        }
687        distance
688    }
689
690    fn pop_update(&mut self) {
691        let n = 2 * self.popsize;
692        let mut x0 = self.pop_x[0..n].to_vec();
693        let mut y0 = self.pop_y[0..n].to_vec();
694        if self.nobj == 1 {
695            let col: Vec<f64> = y0.iter().map(|y| y[0]).collect();
696            let mut yi = sort_index(&col);
697            yi.reverse();
698            x0 = yi.iter().map(|&i| x0[i].clone()).collect();
699            y0 = yi.iter().map(|&i| y0[i].clone()).collect();
700        }
701        let domination = self.pareto(&y0);
702        let maxdom = domination.iter().cloned().fold(f64::MIN, f64::max) as i32;
703        let mut newx: Vec<Vec<f64>> = Vec::with_capacity(self.popsize);
704        let mut newy: Vec<Vec<f64>> = Vec::with_capacity(self.popsize);
705        for dom in (0..=maxdom).rev() {
706            let level: Vec<usize> = (0..n).filter(|&i| domination[i] as i32 == dom).collect();
707            if level.is_empty() {
708                continue;
709            }
710            if newx.len() + level.len() <= self.popsize {
711                for &i in &level {
712                    newx.push(x0[i].clone());
713                    newy.push(y0[i].clone());
714                }
715            } else {
716                if level.len() > 1 {
717                    let domy: Vec<Vec<f64>> = level.iter().map(|&i| y0[i].clone()).collect();
718                    let cd = Self::crowd_dist(&domy, self.nobj);
719                    let mut si = sort_index(&cd);
720                    si.reverse();
721                    for &k in &si {
722                        if newx.len() >= self.popsize {
723                            break;
724                        }
725                        let i = level[k];
726                        newx.push(x0[i].clone());
727                        newy.push(y0[i].clone());
728                    }
729                } else {
730                    newx.push(x0[level[0]].clone());
731                    newy.push(y0[level[0]].clone());
732                }
733                break;
734            }
735        }
736        for i in 0..self.popsize {
737            self.pop_x[i] = newx[i].clone();
738            self.pop_y[i] = newy[i].clone();
739        }
740        if self.nsga_update {
741            let cur = self.pop_x[0..self.popsize].to_vec();
742            self.v_x = self.variation(&cur);
743        }
744    }
745
746    // ---- ask/tell interface ----
747
748    /// Ask for `popsize` offspring rows.
749    ///
750    /// # Panics
751    ///
752    /// Panics if a previous batch is still pending, that is if `ask` is called
753    /// twice without an intervening `tell`. Use [`Mode::try_ask`] to receive
754    /// that as an error instead.
755    pub fn ask(&mut self) -> Vec<Vec<f64>> {
756        self.try_ask().expect("invalid MODE ask call")
757    }
758
759    /// Fallible ask variant for interfaces that need to report call-order
760    /// errors rather than panic.
761    ///
762    /// # Errors
763    ///
764    /// Returns an error if the previously asked batch has not been told yet.
765    pub fn try_ask(&mut self) -> Result<Vec<Vec<f64>>, &'static str> {
766        if self.pending {
767            return Err("MODE ask called before telling the pending batch");
768        }
769        for p in 0..self.popsize {
770            let x = self.next_x(p);
771            self.pop_x[self.popsize + p] = x;
772        }
773        self.pending = true;
774        Ok(self.pop_x[self.popsize..2 * self.popsize].to_vec())
775    }
776
777    fn set_x(&mut self, xs: &[Vec<f64>]) {
778        for (p, row) in xs.iter().enumerate().take(self.popsize) {
779            self.pop_x[self.popsize + p] = row.clone();
780        }
781    }
782
783    /// Tell objective+constraint values for the offspring from [`ask`](Mode::ask).
784    ///
785    /// # Panics
786    ///
787    /// Panics if no batch is pending, if `ys` does not have `popsize` rows, or
788    /// if a row width differs from `nobj + ncon`. Use [`Mode::try_tell`] to
789    /// receive these as errors instead.
790    pub fn tell(&mut self, ys: &[Vec<f64>]) -> i32 {
791        self.try_tell(ys).expect("invalid MODE tell call")
792    }
793
794    /// Fallible tell variant validating call order and matrix shape.
795    ///
796    /// # Errors
797    ///
798    /// Returns an error if no batch is pending, if `ys` does not have
799    /// `popsize` rows, or if a row width differs from `nobj + ncon`.
800    pub fn try_tell(&mut self, ys: &[Vec<f64>]) -> Result<i32, &'static str> {
801        if !self.pending {
802            return Err("MODE tell called without a pending ask batch");
803        }
804        if ys.len() != self.popsize {
805            return Err("MODE tell batch length must equal popsize");
806        }
807        for (p, row) in ys.iter().enumerate() {
808            if row.len() != self.nobj_ncon {
809                return Err("MODE tell row width must equal nobj + ncon");
810            }
811            self.pop_y[self.popsize + p] = row
812                .iter()
813                .map(|&value| if value.is_finite() { value } else { BIG })
814                .collect();
815        }
816        self.pop_update();
817        self.pending = false;
818        Ok(self.stop)
819    }
820
821    /// Tell values while switching the population-update mode.
822    ///
823    /// # Panics
824    ///
825    /// Panics on everything [`Mode::tell`] panics on, and additionally if
826    /// `pareto_update` is not finite. Use [`Mode::try_tell_switch`] for the
827    /// fallible form.
828    pub fn tell_switch(&mut self, ys: &[Vec<f64>], nsga_update: bool, pareto_update: f64) -> i32 {
829        self.try_tell_switch(ys, nsga_update, pareto_update)
830            .expect("invalid MODE tell_switch call")
831    }
832
833    /// Fallible [`tell_switch`](Self::tell_switch) variant validating the
834    /// update probability and pending batch.
835    ///
836    /// # Errors
837    ///
838    /// Returns an error if `pareto_update` is not finite, or for any condition
839    /// [`Mode::try_tell`] rejects.
840    pub fn try_tell_switch(
841        &mut self,
842        ys: &[Vec<f64>],
843        nsga_update: bool,
844        pareto_update: f64,
845    ) -> Result<i32, &'static str> {
846        if !pareto_update.is_finite() {
847            return Err("MODE pareto_update must be finite");
848        }
849        self.nsga_update = nsga_update;
850        self.pareto_update = pareto_update;
851        self.try_tell(ys)
852    }
853
854    /// Replace the candidate population and tell its values.
855    ///
856    /// # Panics
857    ///
858    /// Panics if the population size is below four, or if `xs`/`ys` shapes do
859    /// not match the configured dimension and value width. Use
860    /// [`Mode::try_set_population`] for the fallible form.
861    pub fn set_population(&mut self, xs: &[Vec<f64>], ys: &[Vec<f64>]) -> i32 {
862        self.try_set_population(xs, ys)
863            .expect("invalid MODE set_population call")
864    }
865
866    /// Fallible [`set_population`](Self::set_population) variant validating
867    /// dimensions and population sizes.
868    ///
869    /// # Errors
870    ///
871    /// Returns an error if the population size is below four, if `xs` and `ys`
872    /// have different lengths, if a decision row width differs from the
873    /// configured dimension, or if a value row width differs from
874    /// `nobj + ncon`.
875    pub fn try_set_population(
876        &mut self,
877        xs: &[Vec<f64>],
878        ys: &[Vec<f64>],
879    ) -> Result<i32, &'static str> {
880        if xs.len() < 4 {
881            return Err("MODE population size must be at least four");
882        }
883        if xs.len() != ys.len() {
884            return Err("MODE population x/y length mismatch");
885        }
886        if xs.iter().any(|row| row.len() != self.dim) {
887            return Err("MODE population row width must equal dim");
888        }
889        if xs.len() != self.popsize {
890            self.popsize = xs.len();
891            self.init();
892        }
893        self.set_x(xs);
894        self.pending = true;
895        self.try_tell(ys)
896    }
897
898    /// Current population (rows = individuals).
899    pub fn population(&self) -> Vec<Vec<f64>> {
900        self.pop_x[0..self.popsize].to_vec()
901    }
902
903    /// Return a snapshot of the current population and its values.
904    pub fn result(&self) -> ModeResult {
905        ModeResult {
906            x: self.population(),
907            y: self.pop_y[0..self.popsize].to_vec(),
908            iterations: self.iterations,
909            stop: self.stop,
910        }
911    }
912}
913
914#[cfg(test)]
915mod tests {
916    use super::*;
917
918    // Two-objective test: minimize (sum x^2, sum (x-2)^2) — a convex Pareto
919    // front between 0 and 2 in each coordinate.
920    fn eval(x: &[f64]) -> Vec<f64> {
921        let o1: f64 = x.iter().map(|v| v * v).sum();
922        let o2: f64 = x.iter().map(|v| (v - 2.0) * (v - 2.0)).sum();
923        vec![o1, o2]
924    }
925
926    fn run(nsga: bool) -> Mode {
927        let fit = Fitness::bounded(3, 2, &[-5.0; 3], &[5.0; 3]);
928        let params = ModeParams {
929            popsize: 32,
930            nsga_update: nsga,
931            seed: 1,
932            ..Default::default()
933        };
934        let mut opt = Mode::new(fit, 2, 0, None, &params);
935        for _ in 0..80 {
936            let xs = opt.ask();
937            let ys: Vec<Vec<f64>> = xs.iter().map(|x| eval(x)).collect();
938            opt.tell(&ys);
939        }
940        opt
941    }
942
943    #[test]
944    fn nsga_finds_pareto_front() {
945        let opt = run(true);
946        let r = opt.result();
947        // Front should contain points near both extremes (o1~0 and o2~0).
948        let min_o1 = r.y.iter().map(|y| y[0]).fold(f64::MAX, f64::min);
949        let min_o2 = r.y.iter().map(|y| y[1]).fold(f64::MAX, f64::min);
950        assert!(min_o1 < 0.1, "no low-o1 solution: {min_o1}");
951        assert!(min_o2 < 0.1, "no low-o2 solution: {min_o2}");
952    }
953
954    #[test]
955    fn de_update_finds_pareto_front() {
956        let opt = run(false);
957        let r = opt.result();
958        let min_o1 = r.y.iter().map(|y| y[0]).fold(f64::MAX, f64::min);
959        let min_o2 = r.y.iter().map(|y| y[1]).fold(f64::MAX, f64::min);
960        assert!(min_o1 < 0.2, "no low-o1 solution: {min_o1}");
961        assert!(min_o2 < 0.2, "no low-o2 solution: {min_o2}");
962    }
963
964    #[test]
965    fn constrained_run_progresses() {
966        // 1 objective, 1 constraint: minimize sum x^2 s.t. sum x >= 1
967        // (constraint value = 1 - sum x, feasible when <= 0).
968        let fit = Fitness::bounded(3, 2, &[-5.0; 3], &[5.0; 3]);
969        let params = ModeParams {
970            popsize: 24,
971            nsga_update: false,
972            seed: 2,
973            ..Default::default()
974        };
975        let mut opt = Mode::new(fit, 1, 1, None, &params);
976        for _ in 0..100 {
977            let xs = opt.ask();
978            let ys: Vec<Vec<f64>> = xs
979                .iter()
980                .map(|x| {
981                    let o: f64 = x.iter().map(|v| v * v).sum();
982                    let c: f64 = 1.0 - x.iter().sum::<f64>();
983                    vec![o, c]
984                })
985                .collect();
986            opt.tell(&ys);
987        }
988        let r = opt.result();
989        // Some feasible solution (sum x >= 1) should exist with small objective.
990        let best =
991            r.y.iter()
992                .filter(|y| y[1] <= 0.0)
993                .map(|y| y[0])
994                .fold(f64::MAX, f64::min);
995        assert!(best < 2.0, "constrained best too large: {best}");
996    }
997
998    #[test]
999    fn rejects_invalid_configuration() {
1000        let fit = Fitness::bounded(2, 2, &[-1.0; 2], &[1.0; 2]);
1001        let mut params = ModeParams {
1002            popsize: 3,
1003            ..Default::default()
1004        };
1005        assert!(Mode::try_new(fit.clone(), 2, 0, None, &params).is_err());
1006        params.popsize = 5;
1007        assert!(Mode::try_new(fit.clone(), 0, 2, None, &params).is_err());
1008        assert!(Mode::try_new(fit.clone(), 2, 0, Some(vec![true]), &params).is_err());
1009        params.pro_m = 1.5;
1010        assert!(Mode::try_new(fit, 2, 0, None, &params).is_err());
1011    }
1012
1013    #[test]
1014    fn odd_population_preserves_batch_size() {
1015        let fit = Fitness::bounded(2, 2, &[-1.0; 2], &[1.0; 2]);
1016        let params = ModeParams {
1017            popsize: 5,
1018            nsga_update: true,
1019            seed: 7,
1020            ..Default::default()
1021        };
1022        let mut mode = Mode::try_new(fit, 2, 0, None, &params).unwrap();
1023        for _ in 0..3 {
1024            let xs = mode.ask();
1025            assert_eq!(xs.len(), 5);
1026            let ys: Vec<Vec<f64>> = xs.iter().map(|x| vec![x[0], x[1]]).collect();
1027            mode.tell(&ys);
1028        }
1029    }
1030
1031    #[test]
1032    fn crowding_uses_every_objective() {
1033        let values = vec![
1034            vec![0.0, 0.5],
1035            vec![0.25, 0.0],
1036            vec![0.5, 0.5],
1037            vec![0.75, 1.0],
1038            vec![1.0, 0.5],
1039        ];
1040        let distance = Mode::crowd_dist(&values, 2);
1041        assert_eq!(distance.iter().filter(|&&d| d == BIG).count(), 4);
1042        assert!(distance[2].is_finite() && distance[2] > 0.0);
1043        assert_eq!(Mode::crowd_dist(&vec![vec![1.0, 1.0]; 4], 2), vec![0.0; 4]);
1044    }
1045
1046    #[test]
1047    fn zero_crossover_and_mutation_preserve_parents() {
1048        let fit = Fitness::bounded(2, 2, &[-1.0; 2], &[1.0; 2]);
1049        let params = ModeParams {
1050            popsize: 5,
1051            pro_c: 0.0,
1052            pro_m: 0.0,
1053            seed: 9,
1054            ..Default::default()
1055        };
1056        let mut mode = Mode::try_new(fit, 2, 0, None, &params).unwrap();
1057        let parents = vec![
1058            vec![-0.8, -0.7],
1059            vec![-0.4, -0.3],
1060            vec![0.1, 0.2],
1061            vec![0.5, 0.6],
1062            vec![0.8, 0.9],
1063        ];
1064        let offspring = mode.variation(&parents);
1065        for (child, parent) in offspring.iter().zip(&parents) {
1066            for (&actual, &expected) in child.iter().zip(parent) {
1067                assert!((actual - expected).abs() < 1e-14);
1068            }
1069        }
1070    }
1071
1072    #[test]
1073    fn tell_sanitizes_non_finite_values() {
1074        let fit = Fitness::bounded(2, 1, &[-1.0; 2], &[1.0; 2]);
1075        let params = ModeParams {
1076            popsize: 4,
1077            nsga_update: false,
1078            ..Default::default()
1079        };
1080        let mut mode = Mode::try_new(fit, 1, 0, None, &params).unwrap();
1081        mode.ask();
1082        mode.tell(&[vec![f64::NAN], vec![1.0], vec![2.0], vec![3.0]]);
1083        assert!(
1084            mode.result()
1085                .y
1086                .iter()
1087                .flatten()
1088                .all(|value| !value.is_nan())
1089        );
1090    }
1091
1092    #[test]
1093    fn ask_tell_enforces_call_order_and_shapes() {
1094        let fit = Fitness::bounded(2, 1, &[-1.0; 2], &[1.0; 2]);
1095        let params = ModeParams {
1096            popsize: 4,
1097            ..Default::default()
1098        };
1099        let mut mode = Mode::try_new(fit, 1, 0, None, &params).unwrap();
1100        assert!(mode.try_tell(&vec![vec![0.0]; 4]).is_err());
1101        mode.try_ask().unwrap();
1102        assert!(mode.try_ask().is_err());
1103        assert!(mode.try_tell(&vec![vec![0.0]; 3]).is_err());
1104        assert!(mode.try_tell(&vec![vec![0.0, 1.0]; 4]).is_err());
1105        assert_eq!(mode.try_tell(&vec![vec![0.0]; 4]).unwrap(), 0);
1106    }
1107}