Skip to main content

edge_rs/numeric/
optimise.rs

1//! Scalar minimisation, root finding and derivative-free simplex search.
2//!
3//! Covers the `scipy.optimize` calls in edgePython except the bounded
4//! quasi-Newton, which lives in [`crate::numeric::lbfgsb`]:
5//!
6//! * `brentq` is [`brentq`], used by `disp_pearson`, `disp_deviance` and
7//!   limma's `fitFDist`
8//! * `minimize(method = 'Nelder-Mead')` is [`nelder_mead`], used by the spline
9//!   and power trend fitters
10//!
11//! [`brent_fmin`] is R's `Brent_fmin` (not a `scipy.optimize` port), used by
12//! `disp_cox_reid` and limma's `squeezeVar`, where stopping at R's point
13//! matters.
14//!
15//! [`nelder_mead`] reproduces scipy's initial simplex exactly: the trend fits
16//! minimise a nearly flat objective and are sensitive to it.
17
18use std::cmp::Ordering;
19
20use crate::prelude::*;
21
22////////////
23// Consts //
24////////////
25
26/// Default `tol` of R's `optimize`, `.Machine$double.eps^0.25`, which is exactly
27/// `2^-13`.
28pub const OPTIMIZE_TOL: f64 = 1.220_703_125e-4;
29
30/// `sqrt(DBL_EPSILON)`, exactly `2^-26`, the relative half of `Brent_fmin`'s
31/// convergence test.
32const BRENT_SQRT_EPS: f64 = 1.490_116_119_384_765_6e-8;
33
34/// Golden section step `(3 - sqrt(5)) / 2`, as `Brent_fmin` spells it.
35const BRENT_GOLDEN: f64 = 0.381_966_011_250_105_15;
36
37/// Relative step used to build scipy's initial Nelder-Mead simplex.
38///
39/// scipy perturbs each coordinate by 5% of its value.
40const NELDER_MEAD_STEP: f64 = 0.05;
41
42/// Absolute perturbation used where a starting coordinate is exactly zero.
43///
44/// A relative step cannot move a zero.
45const NELDER_MEAD_ZERO_STEP: f64 = 0.000_25;
46
47/// Simplex extent, in units of `f64::EPSILON` times the best vertex's largest
48/// coordinate, below which Nelder-Mead stops whatever the objective says.
49///
50/// scipy needs both tolerances to pass, which a discontinuous objective can
51/// prevent: NEBULA's profile likelihood jumps by a few `1e-7` wherever an inner
52/// Newton loop changes its step count, and a simplex shrunk onto such a jump
53/// cycles until the iteration cap. Measured on one gene in 500: vertices two ulp
54/// apart, a `4.5e-7` jump against an `fatol` of `1e-7`, 1650 wasted evaluations.
55/// Stopping returns the same minimiser to the last few bits.
56const NELDER_MEAD_COLLAPSED_ULPS: f64 = 16.0;
57
58//////////////////
59// Root finding //
60//////////////////
61
62/// Finds a root of a continuous function on a bracketing interval.
63///
64/// Port of `scipy.optimize.brentq`: bisection, accelerated by inverse quadratic
65/// interpolation and the secant rule.
66///
67/// ### Params
68///
69/// * `f` - Function whose root is wanted
70/// * `lower` - Lower end of the bracket
71/// * `upper` - Upper end of the bracket
72/// * `xtol` - Absolute tolerance on the root
73/// * `rtol` - Relative tolerance on the root
74/// * `max_iter` - Iteration budget
75///
76/// ### Returns
77///
78/// The root, or [`EdgeErrors::RootNotBracketed`] if the endpoints share a sign,
79/// or [`EdgeErrors::NoConvergence`] if the budget runs out.
80///
81/// ### References
82///
83/// Brent, Algorithms for Minimization without Derivatives, 1973, chapter 4
84pub fn brentq<F>(
85    mut f: F,
86    lower: f64,
87    upper: f64,
88    xtol: f64,
89    rtol: f64,
90    max_iter: usize,
91) -> Result<f64, EdgeErrors>
92where
93    F: FnMut(f64) -> f64,
94{
95    if lower.partial_cmp(&upper) != Some(Ordering::Less) {
96        return Err(EdgeErrors::InvalidArgument(format!(
97            "brentq needs lower < upper, got [{lower}, {upper}]"
98        )));
99    }
100    if xtol <= 0.0 {
101        return Err(EdgeErrors::MustBePositive("xtol".to_string()));
102    }
103
104    let (mut xpre, mut xcur) = (lower, upper);
105    let (mut fpre, mut fcur) = (f(xpre), f(xcur));
106
107    if fpre == 0.0 {
108        return Ok(xpre);
109    }
110    if fcur == 0.0 {
111        return Ok(xcur);
112    }
113    if fpre.signum() == fcur.signum() {
114        return Err(EdgeErrors::RootNotBracketed {
115            lower,
116            upper,
117            f_lower: fpre,
118            f_upper: fcur,
119        });
120    }
121
122    let (mut xblk, mut fblk) = (0.0, 0.0);
123    let (mut spre, mut scur) = (0.0, 0.0);
124
125    for _ in 0..max_iter {
126        if fpre != 0.0 && fcur != 0.0 && fpre.signum() != fcur.signum() {
127            xblk = xpre;
128            fblk = fpre;
129            spre = xcur - xpre;
130            scur = spre;
131        }
132        if fblk.abs() < fcur.abs() {
133            xpre = xcur;
134            xcur = xblk;
135            xblk = xpre;
136            fpre = fcur;
137            fcur = fblk;
138            fblk = fpre;
139        }
140
141        let delta = (xtol + rtol * xcur.abs()) / 2.0;
142        let sbis = (xblk - xcur) / 2.0;
143        if fcur == 0.0 || sbis.abs() < delta {
144            return Ok(xcur);
145        }
146
147        if spre.abs() > delta && fcur.abs() < fpre.abs() {
148            let stry = if xpre == xblk {
149                // Secant.
150                -fcur * (xcur - xpre) / (fcur - fpre)
151            } else {
152                // Inverse quadratic interpolation.
153                let dpre = (fpre - fcur) / (xpre - xcur);
154                let dblk = (fblk - fcur) / (xblk - xcur);
155                -fcur * (fblk * dblk - fpre * dpre) / (dblk * dpre * (fblk - fpre))
156            };
157            if 2.0 * stry.abs() < (spre.abs()).min(3.0 * sbis.abs() - delta) {
158                spre = scur;
159                scur = stry;
160            } else {
161                spre = sbis;
162                scur = sbis;
163            }
164        } else {
165            spre = sbis;
166            scur = sbis;
167        }
168
169        xpre = xcur;
170        fpre = fcur;
171        if scur.abs() > delta {
172            xcur += scur;
173        } else {
174            xcur += if sbis > 0.0 { delta } else { -delta };
175        }
176        fcur = f(xcur);
177    }
178
179    Err(EdgeErrors::NoConvergence {
180        routine: "brentq",
181        iterations: max_iter,
182        last_delta: (xblk - xcur).abs(),
183    })
184}
185
186/// Minimises a scalar function on a closed interval, as R's `optimize` does.
187///
188/// Line-by-line port of R's `Brent_fmin`, including its convergence test
189/// (`sqrt(eps) * |x| + tol / 3`), so it stops where limma stops.
190///
191/// ### Params
192///
193/// * `ax` - Lower end of the interval
194/// * `bx` - Upper end of the interval
195/// * `f` - Objective
196/// * `tol` - Convergence tolerance, R's `optimize(tol = )`
197///
198/// ### Returns
199///
200/// The minimiser.
201///
202/// ### References
203///
204/// Brent, Algorithms for Minimization without Derivatives, 1973, chapter 5
205pub fn brent_fmin<F>(ax: f64, bx: f64, mut f: F, tol: f64) -> f64
206where
207    F: FnMut(f64) -> f64,
208{
209    let (mut a, mut b) = (ax, bx);
210    let mut v = a + BRENT_GOLDEN * (b - a);
211    let (mut w, mut x) = (v, v);
212    let mut d = 0.0_f64;
213    let mut e = 0.0_f64;
214    let mut fx = f(x);
215    let (mut fv, mut fw) = (fx, fx);
216    let tol3 = tol / 3.0;
217
218    loop {
219        let xm = 0.5 * (a + b);
220        let tol1 = BRENT_SQRT_EPS * x.abs() + tol3;
221        let t2 = 2.0 * tol1;
222        if (x - xm).abs() <= t2 - 0.5 * (b - a) {
223            return x;
224        }
225
226        let (mut p, mut q, mut r) = (0.0_f64, 0.0_f64, 0.0_f64);
227        if e.abs() > tol1 {
228            r = (x - w) * (fx - fv);
229            q = (x - v) * (fx - fw);
230            p = (x - v) * q - (x - w) * r;
231            q = 2.0 * (q - r);
232            if q > 0.0 {
233                p = -p;
234            } else {
235                q = -q;
236            }
237            r = e;
238            e = d;
239        }
240
241        if p.abs() >= (0.5 * q * r).abs() || p <= q * (a - x) || p >= q * (b - x) {
242            e = if x < xm { b - x } else { a - x };
243            d = BRENT_GOLDEN * e;
244        } else {
245            d = p / q;
246            let u = x + d;
247            // f must not be evaluated too close to either end of the interval.
248            if u - a < t2 || b - u < t2 {
249                d = if x >= xm { -tol1 } else { tol1 };
250            }
251        }
252
253        // Nor too close to the incumbent.
254        let u = if d.abs() >= tol1 {
255            x + d
256        } else if d > 0.0 {
257            x + tol1
258        } else {
259            x - tol1
260        };
261        let fu = f(u);
262
263        if fu <= fx {
264            if u < x {
265                b = x;
266            } else {
267                a = x;
268            }
269            v = w;
270            fv = fw;
271            w = x;
272            fw = fx;
273            x = u;
274            fx = fu;
275        } else {
276            if u < x {
277                a = u;
278            } else {
279                b = u;
280            }
281            if fu <= fw || w == x {
282                v = w;
283                fv = fw;
284                w = u;
285                fw = fu;
286            } else if fu <= fv || v == x || v == w {
287                v = u;
288                fv = fu;
289            }
290        }
291    }
292}
293
294/////////////////
295// Nelder-Mead //
296/////////////////
297
298/// Tuning knobs for [`nelder_mead`].
299#[derive(Clone, Copy, Debug)]
300pub struct NelderMeadParams {
301    /// Absolute tolerance on the spread of the simplex vertices.
302    pub xatol: f64,
303    /// Absolute tolerance on the spread of the objective across the simplex.
304    pub fatol: f64,
305    /// Iteration budget.
306    pub max_iter: usize,
307}
308
309impl Default for NelderMeadParams {
310    /// `xatol`/`fatol` match scipy's defaults. `max_iter` is a fixed fallback,
311    /// not scipy's rule (scipy defaults to `N * 200` for `N` parameters).
312    fn default() -> Self {
313        Self {
314            xatol: 1e-4,
315            fatol: 1e-4,
316            max_iter: 1000,
317        }
318    }
319}
320
321/// Outcome of a simplex search.
322#[derive(Clone, Debug)]
323pub struct NelderMeadResult {
324    /// Best vertex found.
325    pub x: Vec<f64>,
326    /// Objective at that vertex.
327    pub f: f64,
328    /// Iterations performed.
329    pub iterations: usize,
330    /// Whether both tolerances were met before the budget ran out.
331    pub converged: bool,
332}
333
334/// Derivative-free minimisation by the Nelder-Mead simplex method.
335///
336/// The initial simplex is scipy's: the start plus one vertex per coordinate,
337/// perturbed by 5% or a small absolute step at zero. Reflection, expansion,
338/// contraction and shrink use the coefficients 1, 2, 0.5 and 0.5.
339///
340/// ### Params
341///
342/// * `f` - Objective
343/// * `x0` - Starting point
344/// * `params` - Tuning knobs, or [`NelderMeadParams::default`]
345///
346/// ### Returns
347///
348/// The best vertex and its value, or [`EdgeErrors::MustBePositive`] if `x0` is
349/// empty.
350pub fn nelder_mead<F>(
351    mut f: F,
352    x0: &[f64],
353    params: Option<NelderMeadParams>,
354) -> Result<NelderMeadResult, EdgeErrors>
355where
356    F: FnMut(&[f64]) -> f64,
357{
358    let mut stepper = NelderMeadStepper::new(x0, params)?;
359    while let Some(x) = stepper.ask() {
360        let value = f(x);
361        stepper.tell(value);
362    }
363    Ok(stepper.result())
364}
365
366/// Where a [`NelderMeadStepper`] is waiting for a value.
367#[derive(Clone, Copy, Debug, PartialEq, Eq)]
368enum NelderMeadPhase {
369    /// Evaluating vertex `i` of the initial simplex.
370    Init(usize),
371    /// Evaluating the reflected point.
372    Reflect,
373    /// Evaluating the expanded point.
374    Expand,
375    /// Evaluating the contracted point.
376    Contract,
377    /// Evaluating the shrunk vertex at position `i` of the sorted order.
378    Shrink(usize),
379    /// Finished.
380    Done,
381}
382
383/// [`nelder_mead`] as a reverse-communication state machine.
384///
385/// The caller asks for the next point, evaluates it, and reports the value, so
386/// many independent searches can advance in lockstep with batched evaluations.
387/// Points and arithmetic are identical to the closure-driven form:
388/// [`nelder_mead`] is this stepper driven by a closure.
389#[derive(Clone, Debug)]
390pub struct NelderMeadStepper {
391    /// Tuning knobs.
392    params: NelderMeadParams,
393    /// Dimension.
394    n: usize,
395    /// Vertices, `n + 1` of them.
396    simplex: Vec<Vec<f64>>,
397    /// Objective at each vertex.
398    values: Vec<f64>,
399    /// Vertex indices, sorted by value at the top of every iteration. Kept
400    /// across iterations so the stable sort keeps the previous order on ties.
401    order: Vec<usize>,
402    /// Index of the next iteration to run.
403    next_iter: usize,
404    /// Iterations performed so far.
405    iterations: usize,
406    /// Whether both tolerances were met.
407    converged: bool,
408    /// What the pending value is for.
409    phase: NelderMeadPhase,
410    /// The point awaiting a value.
411    pending: Vec<f64>,
412    /// Centroid of all but the worst vertex, for the current iteration.
413    centroid: Vec<f64>,
414    /// The reflected point of the current iteration.
415    reflected: Vec<f64>,
416    /// Its value.
417    f_reflected: f64,
418    /// Whether the current contraction is the inside one.
419    inside: bool,
420    /// Best vertex of the current iteration.
421    best: usize,
422    /// Worst vertex of the current iteration.
423    worst: usize,
424    /// Second worst vertex of the current iteration.
425    second_worst: usize,
426}
427
428impl NelderMeadStepper {
429    /// Builds the initial simplex, scipy's, and asks for its first vertex.
430    ///
431    /// ### Params
432    ///
433    /// * `x0` - Starting point
434    /// * `params` - Tuning knobs, or [`NelderMeadParams::default`]
435    ///
436    /// ### Returns
437    ///
438    /// The stepper, or [`EdgeErrors::MustBePositive`] if `x0` is empty.
439    pub fn new(x0: &[f64], params: Option<NelderMeadParams>) -> Result<Self, EdgeErrors> {
440        let params = params.unwrap_or_default();
441        let n = x0.len();
442        if n == 0 {
443            return Err(EdgeErrors::MustBePositive("x0.len()".to_string()));
444        }
445        let mut simplex: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
446        simplex.push(x0.to_vec());
447        for i in 0..n {
448            let mut vertex = x0.to_vec();
449            if vertex[i] != 0.0 {
450                vertex[i] *= 1.0 + NELDER_MEAD_STEP;
451            } else {
452                vertex[i] = NELDER_MEAD_ZERO_STEP;
453            }
454            simplex.push(vertex);
455        }
456        let pending = simplex[0].clone();
457        Ok(Self {
458            params,
459            n,
460            simplex,
461            values: vec![0.0; n + 1],
462            order: (0..=n).collect(),
463            next_iter: 0,
464            iterations: 0,
465            converged: false,
466            phase: NelderMeadPhase::Init(0),
467            pending,
468            centroid: vec![0.0; n],
469            reflected: vec![0.0; n],
470            f_reflected: 0.0,
471            inside: false,
472            best: 0,
473            worst: 0,
474            second_worst: 0,
475        })
476    }
477
478    /// The point the search wants evaluated next.
479    ///
480    /// ### Returns
481    ///
482    /// The point, or `None` once the search has finished.
483    pub fn ask(&self) -> Option<&[f64]> {
484        if self.phase == NelderMeadPhase::Done {
485            None
486        } else {
487            Some(&self.pending)
488        }
489    }
490
491    /// Hands the stepper the value at the point [`Self::ask`] returned.
492    ///
493    /// ### Params
494    ///
495    /// * `value` - The objective at the pending point
496    pub fn tell(&mut self, value: f64) {
497        let n = self.n;
498        match self.phase {
499            NelderMeadPhase::Init(i) => {
500                self.values[i] = value;
501                if i < n {
502                    self.phase = NelderMeadPhase::Init(i + 1);
503                    self.pending.clone_from(&self.simplex[i + 1]);
504                } else {
505                    self.begin_iteration();
506                }
507            }
508            NelderMeadPhase::Reflect => {
509                self.f_reflected = value;
510                let worst = self.worst;
511                if value < self.values[self.best] {
512                    self.pending = (0..n)
513                        .map(|j| {
514                            self.centroid[j] + 2.0 * (self.centroid[j] - self.simplex[worst][j])
515                        })
516                        .collect();
517                    self.phase = NelderMeadPhase::Expand;
518                } else if value < self.values[self.second_worst] {
519                    self.simplex[worst].clone_from(&self.reflected);
520                    self.values[worst] = value;
521                    self.begin_iteration();
522                } else {
523                    self.inside = value >= self.values[worst];
524                    self.pending = if self.inside {
525                        (0..n)
526                            .map(|j| {
527                                self.centroid[j] - 0.5 * (self.centroid[j] - self.simplex[worst][j])
528                            })
529                            .collect()
530                    } else {
531                        (0..n)
532                            .map(|j| {
533                                self.centroid[j] + 0.5 * (self.centroid[j] - self.simplex[worst][j])
534                            })
535                            .collect()
536                    };
537                    self.phase = NelderMeadPhase::Contract;
538                }
539            }
540            NelderMeadPhase::Expand => {
541                let worst = self.worst;
542                if value < self.f_reflected {
543                    self.simplex[worst] = std::mem::take(&mut self.pending);
544                    self.values[worst] = value;
545                } else {
546                    self.simplex[worst].clone_from(&self.reflected);
547                    self.values[worst] = self.f_reflected;
548                }
549                self.begin_iteration();
550            }
551            NelderMeadPhase::Contract => {
552                let worst = self.worst;
553                let accept = if self.inside {
554                    value < self.values[worst]
555                } else {
556                    value <= self.f_reflected
557                };
558                if accept {
559                    self.simplex[worst] = std::mem::take(&mut self.pending);
560                    self.values[worst] = value;
561                    self.begin_iteration();
562                } else {
563                    // Shrink every vertex towards the best one, one at a time in
564                    // sorted order; each only touches itself.
565                    self.shrink_vertex(1);
566                }
567            }
568            NelderMeadPhase::Shrink(pos) => {
569                let i = self.order[pos];
570                self.values[i] = value;
571                if pos < n {
572                    self.shrink_vertex(pos + 1);
573                } else {
574                    self.begin_iteration();
575                }
576            }
577            NelderMeadPhase::Done => {}
578        }
579    }
580
581    /// Moves the vertex at sorted position `pos` halfway to the best one and
582    /// asks for its value.
583    ///
584    /// ### Params
585    ///
586    /// * `pos` - Position in the sorted order, from `1` to `n`
587    fn shrink_vertex(&mut self, pos: usize) {
588        let i = self.order[pos];
589        let best = self.best;
590        for j in 0..self.n {
591            let anchor = self.simplex[best][j];
592            self.simplex[i][j] = anchor + 0.5 * (self.simplex[i][j] - anchor);
593        }
594        self.pending.clone_from(&self.simplex[i]);
595        self.phase = NelderMeadPhase::Shrink(pos);
596    }
597
598    /// Runs the top of an iteration: sort, test convergence, reflect.
599    fn begin_iteration(&mut self) {
600        let n = self.n;
601        if self.next_iter >= self.params.max_iter {
602            self.finish();
603            return;
604        }
605        self.iterations = self.next_iter + 1;
606        self.next_iter += 1;
607
608        let values = &self.values;
609        self.order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
610        self.best = self.order[0];
611        self.worst = self.order[n];
612        self.second_worst = self.order[n - 1];
613        let best = self.best;
614
615        let spread_x = self.order[1..]
616            .iter()
617            .map(|&i| {
618                self.simplex[i]
619                    .iter()
620                    .zip(self.simplex[best].iter())
621                    .map(|(a, b)| (a - b).abs())
622                    .fold(0.0_f64, f64::max)
623            })
624            .fold(0.0_f64, f64::max);
625        let spread_f = self.order[1..]
626            .iter()
627            .map(|&i| (self.values[i] - self.values[best]).abs())
628            .fold(0.0_f64, f64::max);
629        let scale = self.simplex[best]
630            .iter()
631            .fold(f64::MIN_POSITIVE, |m, v| m.max(v.abs()));
632        let collapsed = spread_x <= NELDER_MEAD_COLLAPSED_ULPS * f64::EPSILON * scale;
633        if collapsed || (spread_x <= self.params.xatol && spread_f <= self.params.fatol) {
634            self.converged = true;
635            self.finish();
636            return;
637        }
638
639        self.centroid.iter_mut().for_each(|c| *c = 0.0);
640        for &i in &self.order[..n] {
641            for (c, v) in self.centroid.iter_mut().zip(self.simplex[i].iter()) {
642                *c += *v;
643            }
644        }
645        for c in self.centroid.iter_mut() {
646            *c /= n as f64;
647        }
648        let worst = self.worst;
649        self.reflected = (0..n)
650            .map(|j| self.centroid[j] + (self.centroid[j] - self.simplex[worst][j]))
651            .collect();
652        self.pending.clone_from(&self.reflected);
653        self.phase = NelderMeadPhase::Reflect;
654    }
655
656    /// Sorts once more and stops.
657    fn finish(&mut self) {
658        let values = &self.values;
659        self.order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
660        self.phase = NelderMeadPhase::Done;
661    }
662
663    /// The best vertex found so far.
664    ///
665    /// ### Returns
666    ///
667    /// The result; meaningful once [`Self::ask`] returns `None`.
668    pub fn result(&self) -> NelderMeadResult {
669        let best = self.order[0];
670        NelderMeadResult {
671            x: self.simplex[best].clone(),
672            f: self.values[best],
673            iterations: self.iterations,
674            converged: self.converged,
675        }
676    }
677}
678
679///////////
680// Tests //
681///////////
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686    use approx::assert_relative_eq;
687
688    #[test]
689    fn test_brentq_finds_a_polynomial_root() {
690        // x^3 - 2x - 5 has its real root near 2.0945514815423265.
691        let root = brentq(
692            |x: f64| x.powi(3) - 2.0 * x - 5.0,
693            1.0,
694            3.0,
695            1e-14,
696            1e-15,
697            200,
698        )
699        .unwrap();
700        assert_relative_eq!(root, 2.094_551_481_542_326_5, max_relative = 1e-12);
701    }
702
703    #[test]
704    fn test_brentq_handles_a_root_at_an_endpoint() {
705        let root = brentq(|x: f64| x - 1.0, 1.0, 3.0, 1e-14, 1e-15, 200).unwrap();
706        assert_relative_eq!(root, 1.0, max_relative = 1e-14);
707    }
708
709    #[test]
710    fn test_brentq_rejects_an_unbracketed_interval() {
711        let err = brentq(|x: f64| x * x + 1.0, -1.0, 1.0, 1e-12, 1e-14, 100).unwrap_err();
712        assert!(matches!(err, EdgeErrors::RootNotBracketed { .. }));
713    }
714
715    /// A steep function where bisection alone would be slow, so the
716    /// interpolation branches actually run.
717    #[test]
718    fn test_brentq_converges_on_a_steep_function() {
719        let root = brentq(|x: f64| x.exp() - 1e6, 0.0, 30.0, 1e-12, 1e-14, 200).unwrap();
720        assert_relative_eq!(root, 1e6_f64.ln(), max_relative = 1e-10);
721    }
722
723    #[test]
724    fn test_nelder_mead_minimises_rosenbrock() {
725        let res = nelder_mead(
726            |x: &[f64]| 100.0 * (x[1] - x[0] * x[0]).powi(2) + (1.0 - x[0]).powi(2),
727            &[-1.2, 1.0],
728            Some(NelderMeadParams {
729                xatol: 1e-10,
730                fatol: 1e-12,
731                max_iter: 5000,
732            }),
733        )
734        .unwrap();
735        assert!(res.converged);
736        assert_relative_eq!(res.x[0], 1.0, max_relative = 1e-4);
737        assert_relative_eq!(res.x[1], 1.0, max_relative = 1e-4);
738    }
739
740    #[test]
741    fn test_nelder_mead_handles_a_zero_starting_coordinate() {
742        // The zero coordinate exercises the absolute-step branch of the simplex.
743        let res = nelder_mead(
744            |x: &[f64]| (x[0] - 3.0).powi(2) + (x[1] + 2.0).powi(2),
745            &[0.0, 0.0],
746            Some(NelderMeadParams {
747                xatol: 1e-10,
748                fatol: 1e-12,
749                max_iter: 5000,
750            }),
751        )
752        .unwrap();
753        assert_relative_eq!(res.x[0], 3.0, max_relative = 1e-5);
754        assert_relative_eq!(res.x[1], -2.0, max_relative = 1e-5);
755    }
756
757    #[test]
758    fn test_nelder_mead_rejects_empty_start() {
759        let err = nelder_mead(|_: &[f64]| 0.0, &[], None).unwrap_err();
760        assert!(matches!(err, EdgeErrors::MustBePositive(_)));
761    }
762
763    /// Closure-driven Nelder-Mead, the specification [`NelderMeadStepper`] is
764    /// checked against bit for bit.
765    fn nelder_mead_reference<F>(
766        mut f: F,
767        x0: &[f64],
768        params: Option<NelderMeadParams>,
769    ) -> Result<NelderMeadResult, EdgeErrors>
770    where
771        F: FnMut(&[f64]) -> f64,
772    {
773        let params = params.unwrap_or_default();
774        let n = x0.len();
775        if n == 0 {
776            return Err(EdgeErrors::MustBePositive("x0.len()".to_string()));
777        }
778
779        // scipy's initial simplex.
780        let mut simplex: Vec<Vec<f64>> = Vec::with_capacity(n + 1);
781        simplex.push(x0.to_vec());
782        for i in 0..n {
783            let mut vertex = x0.to_vec();
784            if vertex[i] != 0.0 {
785                vertex[i] *= 1.0 + NELDER_MEAD_STEP;
786            } else {
787                vertex[i] = NELDER_MEAD_ZERO_STEP;
788            }
789            simplex.push(vertex);
790        }
791
792        let mut values: Vec<f64> = simplex.iter().map(|v| f(v)).collect();
793        let mut order: Vec<usize> = (0..=n).collect();
794        let mut iterations = 0;
795        let mut converged = false;
796
797        for iter in 0..params.max_iter {
798            iterations = iter + 1;
799
800            order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
801            let best = order[0];
802            let worst = order[n];
803            let second_worst = order[n - 1];
804
805            // Convergence: both the simplex and the objective values have collapsed.
806            let spread_x = order[1..]
807                .iter()
808                .map(|&i| {
809                    simplex[i]
810                        .iter()
811                        .zip(simplex[best].iter())
812                        .map(|(a, b)| (a - b).abs())
813                        .fold(0.0_f64, f64::max)
814                })
815                .fold(0.0_f64, f64::max);
816            let spread_f = order[1..]
817                .iter()
818                .map(|&i| (values[i] - values[best]).abs())
819                .fold(0.0_f64, f64::max);
820            if spread_x <= params.xatol && spread_f <= params.fatol {
821                converged = true;
822                break;
823            }
824
825            // Centroid of everything but the worst vertex.
826            let mut centroid = vec![0.0; n];
827            for &i in &order[..n] {
828                for (c, v) in centroid.iter_mut().zip(simplex[i].iter()) {
829                    *c += *v;
830                }
831            }
832            for c in centroid.iter_mut() {
833                *c /= n as f64;
834            }
835
836            let reflected: Vec<f64> = (0..n)
837                .map(|j| centroid[j] + (centroid[j] - simplex[worst][j]))
838                .collect();
839            let f_reflected = f(&reflected);
840
841            if f_reflected < values[best] {
842                let expanded: Vec<f64> = (0..n)
843                    .map(|j| centroid[j] + 2.0 * (centroid[j] - simplex[worst][j]))
844                    .collect();
845                let f_expanded = f(&expanded);
846                if f_expanded < f_reflected {
847                    simplex[worst] = expanded;
848                    values[worst] = f_expanded;
849                } else {
850                    simplex[worst] = reflected;
851                    values[worst] = f_reflected;
852                }
853            } else if f_reflected < values[second_worst] {
854                simplex[worst] = reflected;
855                values[worst] = f_reflected;
856            } else {
857                let inside = f_reflected >= values[worst];
858                let contracted: Vec<f64> = if inside {
859                    (0..n)
860                        .map(|j| centroid[j] - 0.5 * (centroid[j] - simplex[worst][j]))
861                        .collect()
862                } else {
863                    (0..n)
864                        .map(|j| centroid[j] + 0.5 * (centroid[j] - simplex[worst][j]))
865                        .collect()
866                };
867                let f_contracted = f(&contracted);
868                let accept = if inside {
869                    f_contracted < values[worst]
870                } else {
871                    f_contracted <= f_reflected
872                };
873
874                if accept {
875                    simplex[worst] = contracted;
876                    values[worst] = f_contracted;
877                } else {
878                    // Shrink every vertex towards the best one.
879                    let anchor = simplex[best].clone();
880                    for &i in &order[1..] {
881                        for j in 0..n {
882                            simplex[i][j] = anchor[j] + 0.5 * (simplex[i][j] - anchor[j]);
883                        }
884                        values[i] = f(&simplex[i]);
885                    }
886                }
887            }
888        }
889
890        order.sort_by(|&a, &b| values[a].total_cmp(&values[b]));
891        let best = order[0];
892        Ok(NelderMeadResult {
893            x: simplex[best].clone(),
894            f: values[best],
895            iterations,
896            converged,
897        })
898    }
899
900    /// Drives a [`NelderMeadStepper`] with a closure, recording every point.
901    fn drive_stepper(
902        f: &dyn Fn(&[f64]) -> f64,
903        x0: &[f64],
904        params: NelderMeadParams,
905    ) -> (NelderMeadResult, Vec<Vec<f64>>) {
906        let mut stepper = NelderMeadStepper::new(x0, Some(params)).unwrap();
907        let mut asked = Vec::new();
908        while let Some(x) = stepper.ask() {
909            asked.push(x.to_vec());
910            let v = f(x);
911            stepper.tell(v);
912        }
913        (stepper.result(), asked)
914    }
915
916    #[test]
917    fn test_nelder_mead_stepper_is_bit_identical_to_the_closure_form() {
918        let rosenbrock = |x: &[f64]| (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0] * x[0]).powi(2);
919        let quadratic_1d = |x: &[f64]| (x[0] - 0.3).powi(2) + 1.0;
920        // Plateaus force ties, so the stable sort's ordering has to match.
921        let staircase = |x: &[f64]| (x[0] * 4.0).floor().abs() + (x[1] * 4.0).floor().abs();
922        // Non-convex with a narrow valley: exercises shrink steps.
923        let bumpy =
924            |x: &[f64]| (x[0] * 3.0).sin() * (x[1] * 5.0).cos() + 0.1 * (x[0] * x[0] + x[1] * x[1]);
925        let three_d = |x: &[f64]| {
926            x.iter()
927                .enumerate()
928                .map(|(i, v)| (i as f64 + 1.0) * v * v)
929                .sum()
930        };
931
932        type Case<'a> = (&'a dyn Fn(&[f64]) -> f64, Vec<f64>);
933        let cases: Vec<Case<'_>> = vec![
934            (&rosenbrock, vec![-1.2, 1.0]),
935            (&quadratic_1d, vec![2.0]),
936            (&quadratic_1d, vec![0.0]),
937            (&staircase, vec![1.3, -0.7]),
938            (&bumpy, vec![0.4, 0.9]),
939            (&three_d, vec![1.0, -2.0, 0.5]),
940        ];
941
942        for max_iter in [3, 17, 1000] {
943            let params = NelderMeadParams {
944                xatol: 1e-9,
945                fatol: 1e-9,
946                max_iter,
947            };
948            for (f, x0) in &cases {
949                let mut expected_points = Vec::new();
950                let expected = nelder_mead_reference(
951                    |x: &[f64]| {
952                        expected_points.push(x.to_vec());
953                        f(x)
954                    },
955                    x0,
956                    Some(params),
957                )
958                .unwrap();
959                let (got, got_points) = drive_stepper(*f, x0, params);
960
961                assert_eq!(got_points, expected_points, "evaluation sequence differs");
962                assert_eq!(got.x, expected.x);
963                assert_eq!(got.f.to_bits(), expected.f.to_bits());
964                assert_eq!(got.iterations, expected.iterations);
965                assert_eq!(got.converged, expected.converged);
966            }
967        }
968    }
969}