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