Skip to main content

symplex/domains/
optimize.rs

1//! Numerical optimisation and root bracketing.
2//!
3//! Plain `f64` routines that need nothing but a closure:
4//!
5//! | Task | Routines |
6//! |------|----------|
7//! | Bracketed root finding | [`brent_root`] (Brent–Dekker), [`bisect`] |
8//! | Finding a bracket | [`grow_bracket`] (doubling outward within [`Bounds`]) |
9//! | First integer with a monotone property | [`partition_point_by`] (galloping, then bisection) |
10//! | Root polishing from a point | [`newton_root`] |
11//! | Derivative-free local minimisation | [`nelder_mead`] |
12//! | Bracketed scalar minimisation | [`minimize_scalar`] (Brent), [`golden_section`] → [`ScalarMinimum`] |
13//! | Global minimisation in a box | [`differential_evolution`] (DE/rand/1/bin + Nelder–Mead polish) over `&[Interval<f64>]` |
14//! | Least-squares fitting | [`poly_fit`], [`poly_fit_exact`], [`linear_fit`] → [`LinearFit`] |
15//! | Helpers | [`trapezoid`], [`eval_poly`] |
16//!
17//! and convenience methods on [`Ex`] that compile an expression with
18//! [`Ex::compile`] and hand the resulting closure to the matching routine:
19//! [`Ex::find_root_bracket`], [`Ex::minimize_numeric`],
20//! [`Ex::minimize_scalar_numeric`], [`Ex::minimize_global_numeric`] and
21//! [`Ex::poly_fit_points`] (exact rational least squares).
22//!
23//! # Conventions
24//!
25//! * Every routine is deterministic — [`differential_evolution`] draws its
26//!   random numbers from a local SplitMix64 generator seeded by
27//!   [`DeOpts::seed`] — and bounded by an explicit iteration budget.
28//! * Nothing panics.  Bad input (empty vectors, a bracket without a sign
29//!   change, non-finite bounds, `degree ≥ len`, …) is reported as
30//!   [`SymplexError::InvalidArgument`]; running out of iterations or hitting
31//!   a non-finite function value is [`SymplexError::ComputationFailed`].
32//!   The minimisers that return a [`MinimizeResult`] report an exhausted
33//!   budget through [`MinimizeResult::converged`] instead of an error, so
34//!   the best point found is never thrown away.
35//! * Polynomial coefficients are always in **ascending** degree:
36//!   `[c₀, c₁, …, c_d]` represents `c₀ + c₁·x + … + c_d·x^d`.
37//!
38//! ```
39//! use symplex::optimize::{brent_root, nelder_mead, MinimizeOpts, RootOpts};
40//!
41//! // √2 as the root of x² − 2 on [0, 2].
42//! let r = brent_root(|x| x * x - 2.0, 0.0, 2.0, &RootOpts::default()).unwrap();
43//! assert!((r - 2f64.sqrt()).abs() < 1e-12);
44//!
45//! // Minimum of (x − 1)² + (y + 2)² by Nelder–Mead.
46//! let bowl = |p: &[f64]| (p[0] - 1.0).powi(2) + (p[1] + 2.0).powi(2);
47//! let m = nelder_mead(bowl, &[0.0, 0.0], &MinimizeOpts::default()).unwrap();
48//! assert!(m.converged);
49//! assert!((m.x[0] - 1.0).abs() < 1e-6 && (m.x[1] + 2.0).abs() < 1e-6);
50//! ```
51
52use crate::api::context::Context;
53use crate::api::expr::Ex;
54use crate::base::dense_f64;
55use crate::base::errors::SymplexError;
56use crate::base::interval::{Bounds, Interval, IntervalKind};
57use crate::base::node::ExprNode;
58use crate::base::rng::SplitMix64;
59use crate::output::lambdify::CompiledFn;
60use num_bigint::BigInt;
61use num_rational::Ratio;
62use num_traits::{One, Zero};
63
64// ═══════════════════════════════════════════════════════════════════════════
65// Shared helpers
66// ═══════════════════════════════════════════════════════════════════════════
67
68fn invalid(operation: &'static str, reason: String) -> SymplexError {
69    SymplexError::invalid_argument(operation, reason)
70}
71
72fn failed(operation: &'static str, reason: String) -> SymplexError {
73    SymplexError::computation_failed(operation, reason)
74}
75
76/// `NaN` objective values are treated as "worse than anything" so that a
77/// minimiser steps away from them instead of propagating the NaN.
78fn nan_to_inf(v: f64) -> f64 {
79    if v.is_nan() { f64::INFINITY } else { v }
80}
81
82fn check_endpoints(op: &'static str, a: f64, b: f64) -> Result<(), SymplexError> {
83    if !a.is_finite() || !b.is_finite() {
84        return Err(invalid(
85            op,
86            format!("interval endpoints must be finite, got [{a}, {b}]"),
87        ));
88    }
89    Ok(())
90}
91
92/// Validate an interval for the scalar minimisers: finite endpoints with
93/// positive width.  A reversed interval is accepted and returned ordered.
94fn check_interval(op: &'static str, a: f64, b: f64) -> Result<(f64, f64), SymplexError> {
95    check_endpoints(op, a, b)?;
96    if a == b {
97        return Err(invalid(
98            op,
99            format!("interval must have positive width, got [{a}, {b}]"),
100        ));
101    }
102    Ok(if a < b { (a, b) } else { (b, a) })
103}
104
105// ═══════════════════════════════════════════════════════════════════════════
106// Root finding
107// ═══════════════════════════════════════════════════════════════════════════
108
109/// Options for the scalar root finders [`brent_root`], [`bisect`] and
110/// [`newton_root`].
111///
112/// ```
113/// use symplex::optimize::RootOpts;
114///
115/// let opts = RootOpts::default();
116/// assert_eq!(opts.xtol, 2e-12);
117/// assert_eq!(opts.rtol, 4.0 * f64::EPSILON);
118/// assert_eq!(opts.max_iter, 100);
119/// ```
120#[derive(Clone, Debug, PartialEq)]
121pub struct RootOpts {
122    /// Absolute tolerance on the root location (default `2e-12`).
123    pub xtol: f64,
124    /// Relative tolerance on the root location (default `4·ε`).
125    ///
126    /// The bracketing methods stop once the bracket width is at most
127    /// `xtol + rtol·|x|`; Newton stops once the step is that small.
128    pub rtol: f64,
129    /// Maximum number of iterations (default `100`).  Each iteration costs
130    /// one function evaluation (plus one derivative evaluation for Newton);
131    /// the bracketing methods also evaluate the two endpoints up front.
132    pub max_iter: usize,
133}
134
135impl Default for RootOpts {
136    fn default() -> Self {
137        Self {
138            xtol: 2e-12,
139            rtol: 4.0 * f64::EPSILON,
140            max_iter: 100,
141        }
142    }
143}
144
145fn check_root_opts(op: &'static str, opts: &RootOpts) -> Result<(), SymplexError> {
146    let bad = |t: f64| t < 0.0 || !t.is_finite();
147    if bad(opts.xtol) || bad(opts.rtol) {
148        return Err(invalid(
149            op,
150            format!(
151                "tolerances must be finite and non-negative, got xtol = {}, rtol = {}",
152                opts.xtol, opts.rtol
153            ),
154        ));
155    }
156    Ok(())
157}
158
159/// Validate a root bracket.  `Ok(Some(x))` when an endpoint is an exact
160/// zero, `Ok(None)` for a proper sign change.
161fn check_bracket(
162    op: &'static str,
163    a: f64,
164    b: f64,
165    fa: f64,
166    fb: f64,
167) -> Result<Option<f64>, SymplexError> {
168    if !fa.is_finite() || !fb.is_finite() {
169        return Err(invalid(
170            op,
171            format!(
172                "function is not finite at the bracket endpoints: f({a}) = {fa}, f({b}) = {fb}"
173            ),
174        ));
175    }
176    if fa == 0.0 {
177        return Ok(Some(a));
178    }
179    if fb == 0.0 {
180        return Ok(Some(b));
181    }
182    if (fa > 0.0) == (fb > 0.0) {
183        return Err(invalid(
184            op,
185            format!("f(a) and f(b) must have opposite signs: f({a}) = {fa}, f({b}) = {fb}"),
186        ));
187    }
188    Ok(None)
189}
190
191/// Find a root of `f` in the bracket `[a, b]` by the Brent–Dekker method.
192///
193/// Each step chooses between inverse quadratic interpolation, the secant
194/// step and bisection, so convergence is superlinear on smooth functions
195/// while never being slower than bisection.  The bracket must satisfy
196/// `f(a)·f(b) < 0`; an exact zero at an endpoint is returned immediately.
197/// The result is within `xtol + rtol·|x|` of a sign change of `f`.
198///
199/// # Errors
200///
201/// * [`SymplexError::InvalidArgument`] if an endpoint or the function value
202///   there is not finite, if `f(a)` and `f(b)` have the same sign, or if the
203///   tolerances are negative.
204/// * [`SymplexError::ComputationFailed`] if `f` returns a non-finite value
205///   inside the bracket or the tolerance is not met within
206///   [`RootOpts::max_iter`] iterations.
207///
208/// # Examples
209///
210/// ```
211/// use symplex::optimize::{brent_root, RootOpts};
212///
213/// let root = brent_root(|x| x.cos() - x, 0.0, 1.0, &RootOpts::default()).unwrap();
214/// assert!((root.cos() - root).abs() < 1e-12);
215///
216/// // No sign change → error, not a bogus answer.
217/// assert!(brent_root(|x| x * x + 1.0, -1.0, 1.0, &RootOpts::default()).is_err());
218/// ```
219pub fn brent_root(
220    f: impl Fn(f64) -> f64,
221    a: f64,
222    b: f64,
223    opts: &RootOpts,
224) -> Result<f64, SymplexError> {
225    const OP: &str = "brent_root";
226    check_root_opts(OP, opts)?;
227    check_endpoints(OP, a, b)?;
228    let (mut a, mut b) = (a, b);
229    let (mut fa, mut fb) = (f(a), f(b));
230    if let Some(root) = check_bracket(OP, a, b, fa, fb)? {
231        return Ok(root);
232    }
233    // Invariant: `b` is the best iterate, `c` brackets the root with `b`,
234    // `a` is the previous iterate; `d` is the last step, `e` the one before.
235    let mut c = a;
236    let mut fc = fa;
237    let mut d = b - a;
238    let mut e = d;
239    for _ in 0..opts.max_iter {
240        if (fb > 0.0) == (fc > 0.0) {
241            c = a;
242            fc = fa;
243            d = b - a;
244            e = d;
245        }
246        if fc.abs() < fb.abs() {
247            a = b;
248            b = c;
249            c = a;
250            fa = fb;
251            fb = fc;
252            fc = fa;
253        }
254        let tol1 = 0.5 * (opts.xtol + opts.rtol * b.abs());
255        let xm = 0.5 * (c - b);
256        if xm.abs() <= tol1 || fb == 0.0 {
257            return Ok(b);
258        }
259        if e.abs() >= tol1 && fa.abs() > fb.abs() {
260            // Try interpolation: secant if only two points, else inverse
261            // quadratic through (a, fa), (b, fb), (c, fc).
262            let s = fb / fa;
263            let (mut p, mut q) = if a == c {
264                (2.0 * xm * s, 1.0 - s)
265            } else {
266                let q = fa / fc;
267                let r = fb / fc;
268                (
269                    s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0)),
270                    (q - 1.0) * (r - 1.0) * (s - 1.0),
271                )
272            };
273            if p > 0.0 {
274                q = -q;
275            }
276            p = p.abs();
277            let min1 = 3.0 * xm * q - (tol1 * q).abs();
278            let min2 = (e * q).abs();
279            if 2.0 * p < min1.min(min2) {
280                e = d;
281                d = p / q;
282            } else {
283                d = xm;
284                e = d;
285            }
286        } else {
287            d = xm;
288            e = d;
289        }
290        a = b;
291        fa = fb;
292        b += if d.abs() > tol1 { d } else { tol1.copysign(xm) };
293        fb = f(b);
294        if !fb.is_finite() {
295            return Err(failed(OP, format!("f({b}) = {fb} is not finite")));
296        }
297    }
298    Err(failed(
299        OP,
300        format!(
301            "did not converge within {} iterations; bracket [{}, {}] has width {:.3e}",
302            opts.max_iter,
303            b.min(c),
304            b.max(c),
305            (c - b).abs()
306        ),
307    ))
308}
309
310/// Find a root of `f` in the bracket `[a, b]` by bisection.
311///
312/// Linear convergence (one bit per iteration), but bullet-proof: only the
313/// sign of `f` is used.  The same bracket rules and error conditions as
314/// [`brent_root`] apply.  With the default `max_iter = 100` the method can
315/// resolve any bracket down to the default tolerance.
316///
317/// # Examples
318///
319/// ```
320/// use symplex::optimize::{bisect, RootOpts};
321///
322/// let r = bisect(|x| x * x - 2.0, 0.0, 2.0, &RootOpts::default()).unwrap();
323/// assert!((r - 2f64.sqrt()).abs() < 1e-11);
324/// ```
325pub fn bisect(
326    f: impl Fn(f64) -> f64,
327    a: f64,
328    b: f64,
329    opts: &RootOpts,
330) -> Result<f64, SymplexError> {
331    const OP: &str = "bisect";
332    check_root_opts(OP, opts)?;
333    check_endpoints(OP, a, b)?;
334    let (fa, fb) = (f(a), f(b));
335    if let Some(root) = check_bracket(OP, a, b, fa, fb)? {
336        return Ok(root);
337    }
338    let (mut lo, mut hi, mut flo) = (a, b, fa);
339    for _ in 0..opts.max_iter {
340        let mid = lo + 0.5 * (hi - lo);
341        let fm = f(mid);
342        if !fm.is_finite() {
343            return Err(failed(OP, format!("f({mid}) = {fm} is not finite")));
344        }
345        if fm == 0.0 {
346            return Ok(mid);
347        }
348        if (fm > 0.0) == (flo > 0.0) {
349            lo = mid;
350            flo = fm;
351        } else {
352            hi = mid;
353        }
354        let mid = lo + 0.5 * (hi - lo);
355        if (hi - lo).abs() <= opts.xtol + opts.rtol * mid.abs() {
356            return Ok(mid);
357        }
358    }
359    Err(failed(
360        OP,
361        format!(
362            "did not converge within {} iterations; bracket [{}, {}] has width {:.3e}",
363            opts.max_iter,
364            lo.min(hi),
365            lo.max(hi),
366            (hi - lo).abs()
367        ),
368    ))
369}
370
371/// Newton's method for a root of `f` starting from `x0`, using the
372/// derivative `df`.
373///
374/// Stops when the Newton step is smaller than `xtol + rtol·|x|` or `f(x)`
375/// is exactly zero.  Divergence is detected and reported instead of
376/// looping: a non-finite iterate or function value, a vanishing (or
377/// non-finite) derivative, and exhaustion of the iteration budget all
378/// yield [`SymplexError::ComputationFailed`].
379///
380/// # Errors
381///
382/// * [`SymplexError::InvalidArgument`] if `x0` is not finite or the
383///   tolerances are negative.
384/// * [`SymplexError::ComputationFailed`] on divergence or non-convergence.
385///
386/// # Examples
387///
388/// ```
389/// use symplex::optimize::{newton_root, RootOpts};
390///
391/// let r = newton_root(|x| x * x * x - 2.0, |x| 3.0 * x * x, 1.0, &RootOpts::default()).unwrap();
392/// assert!((r - 2f64.cbrt()).abs() < 1e-12);
393///
394/// // atan(x) from x₀ = 2 diverges: the iterates blow up and the call fails cleanly.
395/// let d = newton_root(f64::atan, |x| 1.0 / (1.0 + x * x), 2.0, &RootOpts::default());
396/// assert!(d.is_err());
397/// ```
398pub fn newton_root(
399    f: impl Fn(f64) -> f64,
400    df: impl Fn(f64) -> f64,
401    x0: f64,
402    opts: &RootOpts,
403) -> Result<f64, SymplexError> {
404    const OP: &str = "newton_root";
405    check_root_opts(OP, opts)?;
406    if !x0.is_finite() {
407        return Err(invalid(
408            OP,
409            format!("initial guess must be finite, got {x0}"),
410        ));
411    }
412    let mut x = x0;
413    for _ in 0..opts.max_iter {
414        let fx = f(x);
415        if !fx.is_finite() {
416            return Err(failed(
417                OP,
418                format!("f({x:e}) = {fx:e} is not finite; the iteration diverged"),
419            ));
420        }
421        if fx == 0.0 {
422            return Ok(x);
423        }
424        let dfx = df(x);
425        if !dfx.is_finite() || dfx == 0.0 {
426            return Err(failed(
427                OP,
428                format!("derivative f'({x:e}) = {dfx:e} vanishes or is not finite"),
429            ));
430        }
431        let step = fx / dfx;
432        let x_new = x - step;
433        if !x_new.is_finite() {
434            return Err(failed(
435                OP,
436                format!("iterate became non-finite after the step {step:e} from x = {x:e}"),
437            ));
438        }
439        if step.abs() <= opts.xtol + opts.rtol * x_new.abs() {
440            return Ok(x_new);
441        }
442        x = x_new;
443    }
444    Err(failed(
445        OP,
446        format!(
447            "did not converge within {} iterations; last iterate x = {x}, |f(x)| = {:.3e}",
448            opts.max_iter,
449            f(x).abs()
450        ),
451    ))
452}
453
454// ═══════════════════════════════════════════════════════════════════════════
455// Bracketing and integer search
456// ═══════════════════════════════════════════════════════════════════════════
457
458/// Grow the interval `[a, b]` outward until `f` changes sign across it,
459/// staying within `bounds`.
460///
461/// `f(a)` and `f(b)` are evaluated first; if they already differ in sign
462/// (or one is exactly zero) the interval is returned as it is.  Otherwise
463/// the end where `|f|` is smaller — for a monotone `f`, the side the root
464/// lies on — is moved away from the other end by the current width, so
465/// the width doubles at each step (both ends move when `|f(a)| = |f(b)|`).
466/// An end never passes its side of `bounds`; once it rests there the other
467/// end grows instead, and when both are pinned there is no sign change
468/// within `bounds`.  At most `max_doublings` growth steps are taken.
469///
470/// The result is a closed [`Interval`] ready for [`brent_root`] or
471/// [`bisect`].  A reversed `[a, b]` is accepted and ordered.
472///
473/// # Errors
474///
475/// * [`SymplexError::InvalidArgument`] if an endpoint is not finite,
476///   `a == b`, `[a, b]` is not inside `bounds`, or `f` is not finite at `a`
477///   or `b`.
478/// * [`SymplexError::ComputationFailed`] if `f` is not finite at a grown
479///   endpoint, or no sign change is found within `bounds` or within
480///   `max_doublings` steps.
481///
482/// # Examples
483///
484/// ```
485/// use symplex::optimize::{brent_root, grow_bracket, RootOpts};
486/// use symplex::Bounds;
487///
488/// // √500 lies well past the initial [0, 1]; with 0 as a bound only the
489/// // upper end grows: 1, 2, 4, 8, 16, 32.
490/// let f = |x: f64| x * x - 500.0;
491/// let iv = grow_bracket(f, 0.0, 1.0, Bounds::at_least(0.0), 20)?;
492/// assert_eq!((iv.lower, iv.upper), (0.0, 32.0));
493/// let r = brent_root(f, iv.lower, iv.upper, &RootOpts::default())?;
494/// assert!((r - 500f64.sqrt()).abs() < 1e-10);
495///
496/// // No sign change inside the bounds: an error, not a bogus bracket.
497/// assert!(grow_bracket(|x| x * x + 1.0, 0.0, 1.0, Bounds::closed(0.0, 8.0), 10).is_err());
498/// # Ok::<(), symplex::prelude::SymplexError>(())
499/// ```
500pub fn grow_bracket(
501    f: impl Fn(f64) -> f64,
502    a: f64,
503    b: f64,
504    bounds: Bounds<f64>,
505    max_doublings: usize,
506) -> Result<Interval<f64>, SymplexError> {
507    const OP: &str = "grow_bracket";
508    let (mut a, mut b) = check_interval(OP, a, b)?;
509    if !bounds.contains(&a) || !bounds.contains(&b) {
510        return Err(invalid(
511            OP,
512            format!("the initial interval [{a}, {b}] must lie within the bounds {bounds}"),
513        ));
514    }
515    let (mut fa, mut fb) = (f(a), f(b));
516    if !fa.is_finite() || !fb.is_finite() {
517        return Err(invalid(
518            OP,
519            format!(
520                "function is not finite at the initial endpoints: f({a}) = {fa}, f({b}) = {fb}"
521            ),
522        ));
523    }
524    let bracketed = |fa: f64, fb: f64| fa == 0.0 || fb == 0.0 || (fa > 0.0) != (fb > 0.0);
525    for _ in 0..max_doublings {
526        if bracketed(fa, fb) {
527            return Ok(Interval::closed(a, b));
528        }
529        let width = b - a;
530        let lower_free = bounds.lower.is_none_or(|lo| a > lo);
531        let upper_free = bounds.upper.is_none_or(|hi| b < hi);
532        let prefer_lower = fa.abs() <= fb.abs();
533        let prefer_upper = fb.abs() <= fa.abs();
534        let grow_lower = lower_free && (prefer_lower || !upper_free);
535        let grow_upper = upper_free && (prefer_upper || !lower_free);
536        if !grow_lower && !grow_upper {
537            return Err(failed(
538                OP,
539                format!("no sign change within the bounds {bounds}: f({a}) = {fa}, f({b}) = {fb}"),
540            ));
541        }
542        if grow_lower {
543            a = match bounds.lower {
544                Some(lo) => (a - width).max(lo),
545                None => a - width,
546            };
547            fa = eval_finite(OP, &f, a)?;
548        }
549        if grow_upper {
550            b = match bounds.upper {
551                Some(hi) => (b + width).min(hi),
552                None => b + width,
553            };
554            fb = eval_finite(OP, &f, b)?;
555        }
556        if !a.is_finite() || !b.is_finite() {
557            return Err(failed(
558                OP,
559                format!("the bracket [{a}, {b}] grew beyond the finite range"),
560            ));
561        }
562    }
563    if bracketed(fa, fb) {
564        return Ok(Interval::closed(a, b));
565    }
566    Err(failed(
567        OP,
568        format!("no sign change within {max_doublings} doublings: f({a}) = {fa}, f({b}) = {fb}"),
569    ))
570}
571
572/// The first `n` in `lo..hi` with `pred(n)`, for a `pred` that is `false`
573/// up to some point and `true` from there on; `hi` when there is none.
574///
575/// The search gallops from `lo` (`lo`, `lo + 1`, `lo + 2`, `lo + 4`, …)
576/// until `pred` holds or `hi` is passed, then bisects the last step, so
577/// the cost is `O(log(n − lo))` evaluations however large the cap `hi` is
578/// — the pattern behind "the smallest sample size with power ≥ 0.8".  For a
579/// slice, this is [`slice::partition_point`] with `lo` and `hi` as the
580/// index range.
581///
582/// # Examples
583///
584/// ```
585/// use symplex::optimize::partition_point_by;
586///
587/// // Smallest n with n² ≥ 1000.
588/// assert_eq!(partition_point_by(1, usize::MAX / 2, |n| n * n >= 1000), 32);
589/// // Nothing qualifies below the cap: the cap itself is returned.
590/// assert_eq!(partition_point_by(0, 100, |n| n >= 500), 100);
591/// assert_eq!(partition_point_by(7, 7, |_| true), 7);
592/// ```
593pub fn partition_point_by(lo: usize, hi: usize, mut pred: impl FnMut(usize) -> bool) -> usize {
594    if lo >= hi {
595        return hi;
596    }
597    // Gallop: `l` is the first untested index, `r` an index where `pred`
598    // holds (or `hi`).
599    let (mut l, mut r) = (lo, hi);
600    let mut step = 1usize;
601    let mut probe = lo;
602    while probe < hi {
603        if pred(probe) {
604            r = probe;
605            break;
606        }
607        l = probe + 1;
608        probe = lo.saturating_add(step);
609        step = step.saturating_mul(2);
610    }
611    // Bisect `l..r`: `pred` is false below `l` and true at `r`.
612    while l < r {
613        let mid = l + (r - l) / 2;
614        if pred(mid) {
615            r = mid;
616        } else {
617            l = mid + 1;
618        }
619    }
620    r
621}
622
623// ═══════════════════════════════════════════════════════════════════════════
624// Minimisation
625// ═══════════════════════════════════════════════════════════════════════════
626
627/// Options for [`nelder_mead`], [`minimize_scalar`] and [`golden_section`].
628///
629/// ```
630/// use symplex::optimize::MinimizeOpts;
631///
632/// let opts = MinimizeOpts::default();
633/// assert_eq!(opts.xtol, 1e-8);
634/// assert_eq!(opts.ftol, 1e-12);
635/// assert_eq!(opts.max_iter, 0);       // automatic: 200·n
636/// assert_eq!(opts.initial_step, 0.0); // automatic: 5 % of |x₀ᵢ|, or 0.00025
637/// ```
638#[derive(Clone, Debug, PartialEq)]
639pub struct MinimizeOpts {
640    /// Absolute tolerance on the location of the minimum (default `1e-8`).
641    ///
642    /// Nelder–Mead stops when every simplex vertex is within `xtol` of the
643    /// best one (in the max norm) *and* the `ftol` criterion holds.  The
644    /// bracketed scalar minimisers stop when the bracket has shrunk to
645    /// `xtol + √ε·|x|`; asking for more than `√ε·|x|` is pointless because
646    /// the objective is flat to rounding on that scale.
647    pub xtol: f64,
648    /// Absolute tolerance on the objective value (default `1e-12`).
649    ///
650    /// Nelder–Mead requires every vertex value to be within `ftol` of the
651    /// best one.  Not used by the bracketed scalar minimisers.
652    pub ftol: f64,
653    /// Maximum number of iterations.  `0` (the default) selects `200·n`,
654    /// where `n` is the number of variables.
655    ///
656    /// Reaching the budget is not an error for [`nelder_mead`] — the result
657    /// carries [`MinimizeResult::converged`]` == false` — but it is for the
658    /// scalar minimisers, which have no way to report partial success.
659    pub max_iter: usize,
660    /// Nelder–Mead initial simplex edge length.  `0.0` (the default) uses
661    /// the SciPy convention: vertex `i` perturbs coordinate `i` of `x0` by
662    /// 5 % of its value, or by `0.00025` when that coordinate is zero.  A
663    /// positive value is used as an absolute perturbation for every
664    /// coordinate.  Ignored by the scalar minimisers.
665    pub initial_step: f64,
666}
667
668impl Default for MinimizeOpts {
669    fn default() -> Self {
670        Self {
671            xtol: 1e-8,
672            ftol: 1e-12,
673            max_iter: 0,
674            initial_step: 0.0,
675        }
676    }
677}
678
679impl MinimizeOpts {
680    fn effective_max_iter(&self, n: usize) -> usize {
681        if self.max_iter == 0 {
682            200usize.saturating_mul(n)
683        } else {
684            self.max_iter
685        }
686    }
687}
688
689fn check_minimize_opts(op: &'static str, opts: &MinimizeOpts) -> Result<(), SymplexError> {
690    let bad = |t: f64| t < 0.0 || !t.is_finite();
691    if bad(opts.xtol) || bad(opts.ftol) || bad(opts.initial_step) {
692        return Err(invalid(
693            op,
694            format!(
695                "xtol, ftol and initial_step must be finite and non-negative, got {}, {}, {}",
696                opts.xtol, opts.ftol, opts.initial_step
697            ),
698        ));
699    }
700    Ok(())
701}
702
703/// Outcome of a multivariate minimisation ([`nelder_mead`],
704/// [`differential_evolution`] and the `Ex` wrappers).
705#[derive(Clone, Debug, PartialEq)]
706pub struct MinimizeResult {
707    /// Location of the best point found.
708    pub x: Vec<f64>,
709    /// Objective value at [`x`](Self::x).
710    pub fun: f64,
711    /// Iterations performed: Nelder–Mead steps, or generations for
712    /// differential evolution.
713    pub iterations: usize,
714    /// Total number of objective evaluations (including any polishing).
715    pub evaluations: usize,
716    /// `true` if the stopping criterion was met before the iteration budget
717    /// ran out.  When `false`, `x` is still the best point seen.
718    pub converged: bool,
719}
720
721/// Minimise `f` by the Nelder–Mead downhill-simplex method starting from `x0`.
722///
723/// Uses the standard reflect / expand / contract / shrink steps.  For `n ≤ 2`
724/// the classic coefficients `(1, 2, ½, ½)` are used; for `n > 2` the
725/// dimension-adaptive coefficients `(1, 1 + 2/n, ¾ − 1/(2n), 1 − 1/n)`
726/// are used, which markedly improve behaviour in higher dimensions.  The
727/// iteration stops when all vertices are within [`MinimizeOpts::xtol`] of
728/// the best vertex and all objective values within [`MinimizeOpts::ftol`]
729/// of the best value.  `NaN` objective values are treated as `+∞`, so the
730/// simplex simply moves away from regions where `f` is undefined.
731///
732/// Exhausting [`MinimizeOpts::max_iter`] is **not** an error: the best
733/// vertex is returned with [`MinimizeResult::converged`]` == false`.
734///
735/// # Errors
736///
737/// * [`SymplexError::InvalidArgument`] if `x0` is empty or contains a
738///   non-finite entry, if `f(x0)` is not finite, or if the options are
739///   negative.
740/// * [`SymplexError::ComputationFailed`] if `f` returns `−∞` (the objective
741///   is unbounded below).
742///
743/// # Examples
744///
745/// ```
746/// use symplex::optimize::{nelder_mead, MinimizeOpts};
747///
748/// // Rosenbrock's banana function; minimum f = 0 at (1, 1).
749/// let rosen = |p: &[f64]| (1.0 - p[0]).powi(2) + 100.0 * (p[1] - p[0] * p[0]).powi(2);
750/// let opts = MinimizeOpts { max_iter: 2000, ..MinimizeOpts::default() };
751/// let r = nelder_mead(rosen, &[-1.2, 1.0], &opts).unwrap();
752/// assert!(r.converged);
753/// assert!((r.x[0] - 1.0).abs() < 1e-4 && (r.x[1] - 1.0).abs() < 1e-4);
754/// assert!(r.fun < 1e-8);
755/// ```
756pub fn nelder_mead(
757    mut f: impl FnMut(&[f64]) -> f64,
758    x0: &[f64],
759    opts: &MinimizeOpts,
760) -> Result<MinimizeResult, SymplexError> {
761    const OP: &str = "nelder_mead";
762    let n = x0.len();
763    if n == 0 {
764        return Err(invalid(OP, "initial point must not be empty".into()));
765    }
766    if x0.iter().any(|v| !v.is_finite()) {
767        return Err(invalid(
768            OP,
769            format!("initial point must be finite, got {x0:?}"),
770        ));
771    }
772    check_minimize_opts(OP, opts)?;
773    let max_iter = opts.effective_max_iter(n);
774
775    let mut evaluations = 0usize;
776    let mut eval = |x: &[f64]| -> f64 {
777        evaluations += 1;
778        nan_to_inf(f(x))
779    };
780
781    let f0 = eval(x0);
782    if !f0.is_finite() {
783        return Err(invalid(
784            OP,
785            format!("f(x0) = {f0} is not finite at x0 = {x0:?}"),
786        ));
787    }
788
789    // Initial simplex: x0 plus one perturbed vertex per coordinate.
790    let mut vertices: Vec<(Vec<f64>, f64)> = Vec::with_capacity(n + 1);
791    vertices.push((x0.to_vec(), f0));
792    for i in 0..n {
793        let mut p = x0.to_vec();
794        p[i] = if opts.initial_step > 0.0 {
795            p[i] + opts.initial_step
796        } else if p[i] != 0.0 {
797            p[i] * 1.05
798        } else {
799            0.000_25
800        };
801        let fp = eval(&p);
802        vertices.push((p, fp));
803    }
804
805    let nf = n as f64;
806    let (rho, chi, psi, sigma) = if n > 2 {
807        (1.0, 1.0 + 2.0 / nf, 0.75 - 0.5 / nf, 1.0 - 1.0 / nf)
808    } else {
809        (1.0, 2.0, 0.5, 0.5)
810    };
811
812    // `(1 + t)·xbar − t·xw`: the point on the line through the centroid and
813    // the worst vertex, parameterised so that t = rho is the reflection.
814    let along = |xbar: &[f64], xw: &[f64], t: f64| -> Vec<f64> {
815        xbar.iter()
816            .zip(xw)
817            .map(|(c, w)| (1.0 + t) * c - t * w)
818            .collect()
819    };
820
821    let mut iterations = 0usize;
822    let mut converged = false;
823    loop {
824        vertices.sort_by(|a, b| a.1.total_cmp(&b.1));
825        if vertices[0].1 == f64::NEG_INFINITY {
826            return Err(failed(
827                OP,
828                format!(
829                    "objective is unbounded below: f = -inf at {:?}",
830                    vertices[0].0
831                ),
832            ));
833        }
834        if simplex_converged(&vertices, opts.xtol, opts.ftol) {
835            converged = true;
836            break;
837        }
838        if iterations >= max_iter {
839            break;
840        }
841        iterations += 1;
842
843        let mut xbar = vec![0.0; n];
844        for (v, _) in &vertices[..n] {
845            for (c, xi) in xbar.iter_mut().zip(v) {
846                *c += xi;
847            }
848        }
849        for c in &mut xbar {
850            *c /= nf;
851        }
852        let xw = vertices[n].0.clone();
853        let fw = vertices[n].1;
854        let f_best = vertices[0].1;
855        let f_second_worst = vertices[n - 1].1;
856
857        let xr = along(&xbar, &xw, rho);
858        let fr = eval(&xr);
859        if fr < f_best {
860            let xe = along(&xbar, &xw, rho * chi);
861            let fe = eval(&xe);
862            vertices[n] = if fe < fr { (xe, fe) } else { (xr, fr) };
863        } else if fr < f_second_worst {
864            vertices[n] = (xr, fr);
865        } else {
866            let mut shrink = false;
867            if fr < fw {
868                // Outside contraction.
869                let xc = along(&xbar, &xw, psi * rho);
870                let fc = eval(&xc);
871                if fc <= fr {
872                    vertices[n] = (xc, fc);
873                } else {
874                    shrink = true;
875                }
876            } else {
877                // Inside contraction.
878                let xcc = along(&xbar, &xw, -psi);
879                let fcc = eval(&xcc);
880                if fcc < fw {
881                    vertices[n] = (xcc, fcc);
882                } else {
883                    shrink = true;
884                }
885            }
886            if shrink {
887                let best = vertices[0].0.clone();
888                for (v, fv) in vertices.iter_mut().skip(1) {
889                    for (xj, bj) in v.iter_mut().zip(&best) {
890                        *xj = bj + sigma * (*xj - bj);
891                    }
892                    *fv = eval(v);
893                }
894            }
895        }
896    }
897
898    let (x, fun) = vertices.swap_remove(0);
899    Ok(MinimizeResult {
900        x,
901        fun,
902        iterations,
903        evaluations,
904        converged,
905    })
906}
907
908/// Nelder–Mead stopping test on a simplex sorted by objective value.
909fn simplex_converged(vertices: &[(Vec<f64>, f64)], xtol: f64, ftol: f64) -> bool {
910    let Some(((x0, f0), rest)) = vertices.split_first() else {
911        return true;
912    };
913    let dx = rest
914        .iter()
915        .flat_map(|(x, _)| x.iter().zip(x0).map(|(a, b)| (a - b).abs()))
916        .fold(0.0_f64, f64::max);
917    let df = rest
918        .iter()
919        .map(|(_, fv)| (fv - f0).abs())
920        .fold(0.0_f64, f64::max);
921    dx <= xtol && df <= ftol
922}
923
924/// Evaluate `f(x)` for a scalar minimiser, rejecting non-finite values.
925fn eval_finite(op: &'static str, f: &impl Fn(f64) -> f64, x: f64) -> Result<f64, SymplexError> {
926    let v = f(x);
927    if v.is_finite() {
928        Ok(v)
929    } else {
930        Err(failed(op, format!("f({x}) = {v} is not finite")))
931    }
932}
933
934/// Outcome of a bracketed scalar minimisation ([`minimize_scalar`],
935/// [`golden_section`], [`Ex::minimize_scalar_numeric`]): the minimiser and
936/// the objective value there.
937///
938/// ```
939/// use symplex::optimize::{minimize_scalar, MinimizeOpts, ScalarMinimum};
940///
941/// let m: ScalarMinimum = minimize_scalar(|x| (x - 2.0).powi(2), 0.0, 5.0, &MinimizeOpts::default()).unwrap();
942/// assert!((m.x - 2.0).abs() < 1e-6 && m.value < 1e-12);
943/// ```
944#[derive(Clone, Copy, Debug, PartialEq)]
945pub struct ScalarMinimum {
946    /// Location of the minimum.
947    pub x: f64,
948    /// Objective value at [`x`](Self::x).
949    pub value: f64,
950}
951
952/// Minimise a scalar function on `[a, b]` by Brent's method.
953///
954/// Combines golden-section steps with successive parabolic interpolation
955/// (Brent's `localmin`), giving superlinear convergence on smooth functions
956/// and golden-section behaviour otherwise.  Returns the minimiser and the
957/// value there as a [`ScalarMinimum`].  On a bracket containing several
958/// local minima the method converges to one of them; which one depends on
959/// the bracket.  A reversed interval is accepted.
960///
961/// # Errors
962///
963/// * [`SymplexError::InvalidArgument`] if an endpoint is not finite, the
964///   interval has zero width, or the options are negative.
965/// * [`SymplexError::ComputationFailed`] if `f` returns a non-finite value
966///   or the tolerance is not met within the iteration budget (default
967///   `200`).
968///
969/// # Examples
970///
971/// ```
972/// use symplex::optimize::{minimize_scalar, MinimizeOpts};
973///
974/// let m = minimize_scalar(|x| (x - 1.0).powi(2) + 3.0, -5.0, 5.0, &MinimizeOpts::default()).unwrap();
975/// assert!((m.x - 1.0).abs() < 1e-6);
976/// assert!((m.value - 3.0).abs() < 1e-12);
977/// ```
978pub fn minimize_scalar(
979    f: impl Fn(f64) -> f64,
980    a: f64,
981    b: f64,
982    opts: &MinimizeOpts,
983) -> Result<ScalarMinimum, SymplexError> {
984    const OP: &str = "minimize_scalar";
985    let (mut a, mut b) = check_interval(OP, a, b)?;
986    check_minimize_opts(OP, opts)?;
987    let max_iter = opts.effective_max_iter(1);
988    let cgold = 0.5 * (3.0 - 5.0_f64.sqrt());
989    let sqrt_eps = f64::EPSILON.sqrt();
990
991    // x: best point; w: second best; v: previous w.
992    let mut x = a + cgold * (b - a);
993    let mut w = x;
994    let mut v = x;
995    let mut fx = eval_finite(OP, &f, x)?;
996    let mut fw = fx;
997    let mut fv = fx;
998    let mut d = 0.0_f64; // last step
999    let mut e = 0.0_f64; // step before last
1000
1001    for _ in 0..max_iter {
1002        let xm = 0.5 * (a + b);
1003        let tol1 = sqrt_eps * x.abs() + opts.xtol / 3.0;
1004        let tol2 = 2.0 * tol1;
1005        if (x - xm).abs() <= tol2 - 0.5 * (b - a) {
1006            return Ok(ScalarMinimum { x, value: fx });
1007        }
1008        let golden = if e.abs() > tol1 {
1009            // Parabola through (x, fx), (v, fv), (w, fw).
1010            let r = (x - w) * (fx - fv);
1011            let mut q = (x - v) * (fx - fw);
1012            let mut p = (x - v) * q - (x - w) * r;
1013            q = 2.0 * (q - r);
1014            if q > 0.0 {
1015                p = -p;
1016            }
1017            q = q.abs();
1018            let e_prev = e;
1019            e = d;
1020            if p.abs() >= (0.5 * q * e_prev).abs() || p <= q * (a - x) || p >= q * (b - x) {
1021                true
1022            } else {
1023                d = p / q;
1024                let u = x + d;
1025                if u - a < tol2 || b - u < tol2 {
1026                    d = tol1.copysign(xm - x);
1027                }
1028                false
1029            }
1030        } else {
1031            true
1032        };
1033        if golden {
1034            e = if x >= xm { a - x } else { b - x };
1035            d = cgold * e;
1036        }
1037        let u = if d.abs() >= tol1 {
1038            x + d
1039        } else {
1040            x + tol1.copysign(d)
1041        };
1042        let fu = eval_finite(OP, &f, u)?;
1043        if fu <= fx {
1044            if u >= x {
1045                a = x;
1046            } else {
1047                b = x;
1048            }
1049            v = w;
1050            fv = fw;
1051            w = x;
1052            fw = fx;
1053            x = u;
1054            fx = fu;
1055        } else {
1056            if u < x {
1057                a = u;
1058            } else {
1059                b = u;
1060            }
1061            if fu <= fw || w == x {
1062                v = w;
1063                fv = fw;
1064                w = u;
1065                fw = fu;
1066            } else if fu <= fv || v == x || v == w {
1067                v = u;
1068                fv = fu;
1069            }
1070        }
1071    }
1072    Err(failed(
1073        OP,
1074        format!("did not converge within {max_iter} iterations; bracket [{a}, {b}], best x = {x}"),
1075    ))
1076}
1077
1078/// Minimise a scalar function on `[a, b]` by golden-section search.
1079///
1080/// Shrinks the bracket by the golden ratio each iteration using only
1081/// function comparisons; linear convergence, but immune to the parabolic
1082/// mis-steps of [`minimize_scalar`] on badly behaved functions.  Returns a
1083/// [`ScalarMinimum`].  Same argument rules and errors as
1084/// [`minimize_scalar`].
1085///
1086/// # Examples
1087///
1088/// ```
1089/// use symplex::optimize::{golden_section, MinimizeOpts};
1090///
1091/// // x·ln x has its minimum at x = 1/e.
1092/// let m = golden_section(|x| x * x.ln(), 0.1, 2.0, &MinimizeOpts::default()).unwrap();
1093/// assert!((m.x - (-1.0f64).exp()).abs() < 1e-6);
1094/// assert!((m.value + (-1.0f64).exp()).abs() < 1e-12);
1095/// ```
1096pub fn golden_section(
1097    f: impl Fn(f64) -> f64,
1098    a: f64,
1099    b: f64,
1100    opts: &MinimizeOpts,
1101) -> Result<ScalarMinimum, SymplexError> {
1102    const OP: &str = "golden_section";
1103    let (mut a, mut b) = check_interval(OP, a, b)?;
1104    check_minimize_opts(OP, opts)?;
1105    let max_iter = opts.effective_max_iter(1);
1106    let inv_phi = 0.5 * (5.0_f64.sqrt() - 1.0);
1107    let sqrt_eps = f64::EPSILON.sqrt();
1108
1109    let mut x1 = b - inv_phi * (b - a);
1110    let mut x2 = a + inv_phi * (b - a);
1111    let mut f1 = eval_finite(OP, &f, x1)?;
1112    let mut f2 = eval_finite(OP, &f, x2)?;
1113    for _ in 0..max_iter {
1114        let mid = 0.5 * (a + b);
1115        if (b - a).abs() <= opts.xtol + sqrt_eps * mid.abs() {
1116            return Ok(if f1 <= f2 {
1117                ScalarMinimum { x: x1, value: f1 }
1118            } else {
1119                ScalarMinimum { x: x2, value: f2 }
1120            });
1121        }
1122        if f1 < f2 {
1123            b = x2;
1124            x2 = x1;
1125            f2 = f1;
1126            x1 = b - inv_phi * (b - a);
1127            f1 = eval_finite(OP, &f, x1)?;
1128        } else {
1129            a = x1;
1130            x1 = x2;
1131            f1 = f2;
1132            x2 = a + inv_phi * (b - a);
1133            f2 = eval_finite(OP, &f, x2)?;
1134        }
1135    }
1136    Err(failed(
1137        OP,
1138        format!("did not converge within {max_iter} iterations; bracket [{a}, {b}]"),
1139    ))
1140}
1141
1142// ═══════════════════════════════════════════════════════════════════════════
1143// Differential evolution
1144// ═══════════════════════════════════════════════════════════════════════════
1145
1146/// Options for [`differential_evolution`].
1147///
1148/// ```
1149/// use symplex::optimize::DeOpts;
1150///
1151/// let opts = DeOpts::default();
1152/// assert_eq!(opts.population, 0); // automatic: max(15·n, 8)
1153/// assert_eq!(opts.max_generations, 300);
1154/// assert_eq!(opts.crossover, 0.7);
1155/// assert_eq!(opts.differential_weight, 0.8);
1156/// assert_eq!(opts.tol, 1e-8);
1157/// assert_eq!(opts.seed, 0);
1158/// ```
1159#[derive(Clone, Debug, PartialEq)]
1160pub struct DeOpts {
1161    /// Population size.  `0` (the default) selects `max(15·n, 8)` for `n`
1162    /// variables.  Explicit values must be at least `4`.
1163    pub population: usize,
1164    /// Maximum number of generations (default `300`).
1165    pub max_generations: usize,
1166    /// Crossover probability `CR ∈ [0, 1]` (default `0.7`): the chance that
1167    /// each coordinate of a trial vector is taken from the mutant rather
1168    /// than the parent.  One coordinate is always taken from the mutant.
1169    pub crossover: f64,
1170    /// Differential weight `F > 0` (default `0.8`) scaling the difference
1171    /// vector in `x_{r1} + F·(x_{r2} − x_{r3})`.
1172    pub differential_weight: f64,
1173    /// Convergence tolerance (default `1e-8`).  The run stops once the
1174    /// standard deviation of the population's objective values is at most
1175    /// `tol·(1 + |mean|)`.
1176    pub tol: f64,
1177    /// Seed of the internal SplitMix64 generator (default `0`).  Identical
1178    /// seeds and inputs give bit-identical results.
1179    pub seed: u64,
1180}
1181
1182impl Default for DeOpts {
1183    fn default() -> Self {
1184        Self {
1185            population: 0,
1186            max_generations: 300,
1187            crossover: 0.7,
1188            differential_weight: 0.8,
1189            tol: 1e-8,
1190            seed: 0,
1191        }
1192    }
1193}
1194
1195/// Population-convergence test for differential evolution.
1196fn population_converged(energies: &[f64], tol: f64) -> bool {
1197    let n = energies.len() as f64;
1198    if n == 0.0 {
1199        return true;
1200    }
1201    let mean = energies.iter().sum::<f64>() / n;
1202    let var = energies
1203        .iter()
1204        .map(|e| (e - mean) * (e - mean))
1205        .sum::<f64>()
1206        / n;
1207    var.sqrt() <= tol * (1.0 + mean.abs())
1208}
1209
1210/// Global minimisation of `f` over the box `bounds` by differential
1211/// evolution (strategy `DE/rand/1/bin`), followed by a Nelder–Mead polish
1212/// of the best member.
1213///
1214/// `bounds[j]` is the **closed** interval `[lo, hi]` of coordinate `j`
1215/// (build it with [`Interval::closed`] or `(lo..=hi).into()`); trial points
1216/// are clamped onto its endpoints, so an open or half-open kind would
1217/// misdescribe the search box and is rejected as [`SymplexError::InvalidArgument`].
1218///
1219/// The population is initialised by Latin-hypercube sampling; each
1220/// generation builds one trial vector per member from three other distinct
1221/// members (`x_{r1} + F·(x_{r2} − x_{r3})`, binomial crossover), clips it
1222/// to the box, and replaces the member when the trial is no worse.  Every
1223/// point at which `f` is evaluated — including during the polish — lies
1224/// inside `bounds`.  The run is deterministic for a given
1225/// [`DeOpts::seed`].
1226///
1227/// [`MinimizeResult::iterations`] is the number of generations,
1228/// [`MinimizeResult::evaluations`] counts all objective calls including the
1229/// polish, and [`MinimizeResult::converged`] reports whether the
1230/// population-spread criterion ([`DeOpts::tol`]) was met before
1231/// [`DeOpts::max_generations`] ran out.
1232///
1233/// # Errors
1234///
1235/// * [`SymplexError::InvalidArgument`] if `bounds` is empty, a bound is not
1236///   finite, reversed or not [`IntervalKind::Closed`], the population is
1237///   smaller than 4, `crossover` is outside `[0, 1]`, or
1238///   `differential_weight`/`tol` are not positive and finite.
1239/// * [`SymplexError::ComputationFailed`] if `f` has no finite value
1240///   anywhere the search looked.
1241///
1242/// # Examples
1243///
1244/// ```
1245/// use symplex::optimize::{differential_evolution, DeOpts};
1246/// use symplex::Interval;
1247///
1248/// // Rastrigin's function: many local minima, global minimum 0 at the origin.
1249/// let rastrigin = |p: &[f64]| {
1250///     10.0 * p.len() as f64
1251///         + p.iter()
1252///             .map(|x| x * x - 10.0 * (2.0 * std::f64::consts::PI * x).cos())
1253///             .sum::<f64>()
1254/// };
1255/// let bounds = [Interval::closed(-5.12, 5.12), Interval::closed(-5.12, 5.12)];
1256/// let r = differential_evolution(rastrigin, &bounds, &DeOpts::default()).unwrap();
1257/// assert!(r.fun < 1e-6, "f = {}", r.fun);
1258/// assert!(r.x.iter().all(|x| x.abs() < 1e-3));
1259/// ```
1260pub fn differential_evolution(
1261    mut f: impl FnMut(&[f64]) -> f64,
1262    bounds: &[Interval<f64>],
1263    opts: &DeOpts,
1264) -> Result<MinimizeResult, SymplexError> {
1265    const OP: &str = "differential_evolution";
1266    let n = bounds.len();
1267    if n == 0 {
1268        return Err(invalid(OP, "bounds must not be empty".into()));
1269    }
1270    for iv in bounds {
1271        if !iv.lower.is_finite() || !iv.upper.is_finite() || iv.lower > iv.upper {
1272            return Err(invalid(
1273                OP,
1274                format!("each bound must be a finite interval with lower <= upper, got {iv}"),
1275            ));
1276        }
1277        if iv.kind != IntervalKind::Closed {
1278            return Err(invalid(
1279                OP,
1280                format!("each bound must be a closed interval [lower, upper], got {iv}"),
1281            ));
1282        }
1283    }
1284    if !(0.0..=1.0).contains(&opts.crossover) {
1285        return Err(invalid(
1286            OP,
1287            format!("crossover must lie in [0, 1], got {}", opts.crossover),
1288        ));
1289    }
1290    if !opts.differential_weight.is_finite() || opts.differential_weight <= 0.0 {
1291        return Err(invalid(
1292            OP,
1293            format!(
1294                "differential_weight must be positive and finite, got {}",
1295                opts.differential_weight
1296            ),
1297        ));
1298    }
1299    if !opts.tol.is_finite() || opts.tol < 0.0 {
1300        return Err(invalid(
1301            OP,
1302            format!("tol must be finite and non-negative, got {}", opts.tol),
1303        ));
1304    }
1305    let np = if opts.population == 0 {
1306        (15 * n).max(8)
1307    } else {
1308        opts.population
1309    };
1310    if np < 4 {
1311        return Err(invalid(
1312            OP,
1313            format!("population must be at least 4, got {np}"),
1314        ));
1315    }
1316
1317    let mut rng = SplitMix64::new(opts.seed);
1318    let mut evaluations = 0usize;
1319    let mut eval = |x: &[f64]| -> f64 {
1320        evaluations += 1;
1321        nan_to_inf(f(x))
1322    };
1323
1324    // Latin-hypercube initialisation: every coordinate is stratified into
1325    // `np` equal slices, each used exactly once.
1326    let mut pop = vec![vec![0.0; n]; np];
1327    let mut perm: Vec<usize> = (0..np).collect();
1328    for (j, iv) in bounds.iter().enumerate() {
1329        rng.shuffle(&mut perm);
1330        for (member, &slice) in pop.iter_mut().zip(&perm) {
1331            let u = (slice as f64 + rng.next_f64()) / np as f64;
1332            member[j] = iv.lower + u * (iv.upper - iv.lower);
1333        }
1334    }
1335    let mut energies: Vec<f64> = pop.iter().map(|m| eval(m)).collect();
1336    let mut best = argmin(&energies);
1337
1338    let mut generations = 0usize;
1339    let mut converged = false;
1340    let mut trial = vec![0.0; n];
1341    loop {
1342        if population_converged(&energies, opts.tol) {
1343            converged = true;
1344            break;
1345        }
1346        if generations >= opts.max_generations {
1347            break;
1348        }
1349        generations += 1;
1350        for i in 0..np {
1351            let r1 = rng.below_excluding(np, &mut [i]);
1352            let r2 = rng.below_excluding(np, &mut [i, r1]);
1353            let r3 = rng.below_excluding(np, &mut [i, r1, r2]);
1354            let j_rand = rng.below(n);
1355            for (j, iv) in bounds.iter().enumerate() {
1356                let v = if j == j_rand || rng.next_f64() < opts.crossover {
1357                    pop[r1][j] + opts.differential_weight * (pop[r2][j] - pop[r3][j])
1358                } else {
1359                    pop[i][j]
1360                };
1361                // Bounds were validated finite with lower <= upper, so clamp cannot panic.
1362                trial[j] = v.clamp(iv.lower, iv.upper);
1363            }
1364            let ft = eval(&trial);
1365            if ft <= energies[i] {
1366                pop[i].copy_from_slice(&trial);
1367                energies[i] = ft;
1368                if ft < energies[best] {
1369                    best = i;
1370                }
1371            }
1372        }
1373    }
1374    let de_evaluations = evaluations;
1375
1376    let f_best = energies[best];
1377    if !f_best.is_finite() {
1378        return Err(failed(
1379            OP,
1380            format!("objective has no finite value in the box after {generations} generations"),
1381        ));
1382    }
1383
1384    // Local polish, evaluating only inside the box.
1385    let mut clipped = vec![0.0; n];
1386    let polish = nelder_mead(
1387        |x: &[f64]| {
1388            for ((c, &xi), iv) in clipped.iter_mut().zip(x).zip(bounds) {
1389                *c = xi.clamp(iv.lower, iv.upper);
1390            }
1391            f(&clipped)
1392        },
1393        &pop[best],
1394        &MinimizeOpts::default(),
1395    )
1396    .map_err(|e| failed(OP, format!("Nelder–Mead polish failed: {e}")))?;
1397
1398    let (x, fun) = if polish.fun < f_best {
1399        let x = polish
1400            .x
1401            .iter()
1402            .zip(bounds)
1403            .map(|(&xi, iv)| xi.clamp(iv.lower, iv.upper))
1404            .collect();
1405        (x, polish.fun)
1406    } else {
1407        (pop.swap_remove(best), f_best)
1408    };
1409    Ok(MinimizeResult {
1410        x,
1411        fun,
1412        iterations: generations,
1413        evaluations: de_evaluations + polish.evaluations,
1414        converged,
1415    })
1416}
1417
1418/// Index of the smallest value (`0` for an empty slice).
1419fn argmin(values: &[f64]) -> usize {
1420    values.iter().enumerate().fold(
1421        0usize,
1422        |best, (i, &v)| {
1423            if v < values[best] { i } else { best }
1424        },
1425    )
1426}
1427
1428// ═══════════════════════════════════════════════════════════════════════════
1429// Least-squares fitting
1430// ═══════════════════════════════════════════════════════════════════════════
1431
1432/// Least-squares polynomial fit of degree `degree` to the samples
1433/// `(xs[i], ys[i])`.
1434///
1435/// Returns the coefficients in **ascending** degree, `[c₀, c₁, …, c_d]`,
1436/// so that `ys[i] ≈ c₀ + c₁·xs[i] + … + c_d·xs[i]^d` (evaluate with
1437/// [`eval_poly`]).  The Vandermonde matrix is column-scaled and factored
1438/// by Householder QR, which is backward stable; the normal equations are
1439/// never formed.  With `degree + 1 == xs.len()` the fit interpolates.
1440///
1441/// # Errors
1442///
1443/// [`SymplexError::InvalidArgument`] if the slices differ in length,
1444/// `degree >= xs.len()`, or any sample is not finite;
1445/// [`SymplexError::ComputationFailed`] if the Vandermonde matrix is
1446/// numerically rank deficient (fewer than `degree + 1` distinct
1447/// abscissae).
1448///
1449/// # Examples
1450///
1451/// ```
1452/// use symplex::optimize::{eval_poly, poly_fit};
1453///
1454/// let xs: Vec<f64> = (0..6).map(f64::from).collect();
1455/// let ys: Vec<f64> = xs.iter().map(|x| 1.0 + 2.0 * x + 3.0 * x * x).collect();
1456/// let c = poly_fit(&xs, &ys, 2).unwrap();
1457/// assert!((c[0] - 1.0).abs() < 1e-9 && (c[1] - 2.0).abs() < 1e-9 && (c[2] - 3.0).abs() < 1e-9);
1458/// assert!((eval_poly(&c, 10.0) - 321.0).abs() < 1e-7);
1459///
1460/// // Not enough points for the requested degree.
1461/// assert!(poly_fit(&[0.0, 1.0], &[0.0, 1.0], 2).is_err());
1462/// ```
1463pub fn poly_fit(xs: &[f64], ys: &[f64], degree: usize) -> Result<Vec<f64>, SymplexError> {
1464    const OP: &str = "poly_fit";
1465    let m = xs.len();
1466    if m != ys.len() {
1467        return Err(invalid(
1468            OP,
1469            format!(
1470                "xs and ys must have the same length, got {m} and {}",
1471                ys.len()
1472            ),
1473        ));
1474    }
1475    if degree >= m {
1476        return Err(invalid(
1477            OP,
1478            format!(
1479                "degree {degree} needs at least {} points, got {m}",
1480                degree + 1
1481            ),
1482        ));
1483    }
1484    if xs.iter().chain(ys).any(|v| !v.is_finite()) {
1485        return Err(invalid(OP, "all samples must be finite".into()));
1486    }
1487    let ncols = degree + 1;
1488    let mut a: Vec<Vec<f64>> = xs
1489        .iter()
1490        .map(|&x| {
1491            let mut p = 1.0;
1492            (0..ncols)
1493                .map(|_| {
1494                    let v = p;
1495                    p *= x;
1496                    v
1497                })
1498                .collect()
1499        })
1500        .collect();
1501    // Equilibrate the columns: brings the condition number within a
1502    // modest factor of the best diagonal scaling.
1503    let mut scale = vec![1.0; ncols];
1504    for (j, s) in scale.iter_mut().enumerate() {
1505        let norm = a.iter().map(|row| row[j] * row[j]).sum::<f64>().sqrt();
1506        if norm > 0.0 && norm.is_finite() {
1507            *s = norm;
1508            for row in &mut a {
1509                row[j] /= norm;
1510            }
1511        }
1512    }
1513    let c =
1514        dense_f64::lstsq_householder(&dense_f64::flatten(&a), m, ncols, ys).ok_or_else(|| {
1515            failed(
1516                OP,
1517                format!(
1518                    "Vandermonde matrix is rank deficient: fewer than {ncols} distinct abscissae"
1519                ),
1520            )
1521        })?;
1522    Ok(c.iter().zip(&scale).map(|(c, s)| c / s).collect())
1523}
1524
1525/// Exact least-squares polynomial fit over ℚ.
1526///
1527/// Solves the normal equations `AᵀA·c = Aᵀy` for the Vandermonde matrix
1528/// `A` with exact rational Gaussian elimination, so the returned
1529/// coefficients (in **ascending** degree) are the exact least-squares
1530/// solution — for consistent data, the exact interpolating polynomial.
1531///
1532/// # Errors
1533///
1534/// [`SymplexError::InvalidArgument`] if `degree >= points.len()`;
1535/// [`SymplexError::ComputationFailed`] if the normal matrix is singular
1536/// (fewer than `degree + 1` distinct abscissae).
1537///
1538/// # Examples
1539///
1540/// ```
1541/// use num_bigint::BigInt;
1542/// use num_rational::Ratio;
1543/// use symplex::optimize::poly_fit_exact;
1544///
1545/// let q = |p: i64, d: i64| Ratio::new(BigInt::from(p), BigInt::from(d));
1546/// // y = x²/3 − x/2 + 1/7 sampled at x = 0, 1, 2, 3, 4 (five points, degree 2).
1547/// let pts = [
1548///     (q(0, 1), q(1, 7)),
1549///     (q(1, 1), q(-1, 42)),
1550///     (q(2, 1), q(10, 21)),
1551///     (q(3, 1), q(23, 14)),
1552///     (q(4, 1), q(73, 21)),
1553/// ];
1554/// let c = poly_fit_exact(&pts, 2).unwrap();
1555/// assert_eq!(c, vec![q(1, 7), q(-1, 2), q(1, 3)]);
1556/// ```
1557pub fn poly_fit_exact(
1558    points: &[(Ratio<BigInt>, Ratio<BigInt>)],
1559    degree: usize,
1560) -> Result<Vec<Ratio<BigInt>>, SymplexError> {
1561    const OP: &str = "poly_fit_exact";
1562    let m = points.len();
1563    if degree >= m {
1564        return Err(invalid(
1565            OP,
1566            format!(
1567                "degree {degree} needs at least {} points, got {m}",
1568                degree + 1
1569            ),
1570        ));
1571    }
1572    let ncols = degree + 1;
1573    // Power sums S_p = Σ xᵖ (p ≤ 2d) and moments T_j = Σ xʲ·y (j ≤ d).
1574    let mut power_sums = vec![Ratio::<BigInt>::zero(); 2 * degree + 1];
1575    let mut moments = vec![Ratio::<BigInt>::zero(); ncols];
1576    for (x, y) in points {
1577        let mut pow = Ratio::<BigInt>::one();
1578        for (p, s) in power_sums.iter_mut().enumerate() {
1579            *s += &pow;
1580            if let Some(t) = moments.get_mut(p) {
1581                *t += &pow * y;
1582            }
1583            if p + 1 < 2 * degree + 1 {
1584                pow *= x;
1585            }
1586        }
1587    }
1588    let normal: Vec<Vec<Ratio<BigInt>>> = (0..ncols)
1589        .map(|j| (0..ncols).map(|k| power_sums[j + k].clone()).collect())
1590        .collect();
1591    solve_exact(normal, moments).ok_or_else(|| {
1592        failed(
1593            OP,
1594            format!("normal equations are singular: fewer than {ncols} distinct abscissae"),
1595        )
1596    })
1597}
1598
1599/// Exact Gaussian elimination for the square system `a·x = b` over ℚ.
1600/// `None` if the matrix is singular.
1601fn solve_exact(
1602    mut a: Vec<Vec<Ratio<BigInt>>>,
1603    mut b: Vec<Ratio<BigInt>>,
1604) -> Option<Vec<Ratio<BigInt>>> {
1605    let n = b.len();
1606    if a.len() != n || a.iter().any(|row| row.len() != n) {
1607        return None;
1608    }
1609    for col in 0..n {
1610        let pivot = (col..n).find(|&r| !a[r][col].is_zero())?;
1611        a.swap(col, pivot);
1612        b.swap(col, pivot);
1613        let pivot_row = a[col].clone();
1614        let pivot_b = b[col].clone();
1615        for r in (col + 1)..n {
1616            if a[r][col].is_zero() {
1617                continue;
1618            }
1619            let factor = &a[r][col] / &pivot_row[col];
1620            for (entry, p) in a[r].iter_mut().zip(&pivot_row).skip(col) {
1621                *entry -= &factor * p;
1622            }
1623            b[r] -= &factor * &pivot_b;
1624        }
1625    }
1626    let mut x = vec![Ratio::<BigInt>::zero(); n];
1627    for r in (0..n).rev() {
1628        let mut s = b[r].clone();
1629        for c in (r + 1)..n {
1630            s -= &a[r][c] * &x[c];
1631        }
1632        x[r] = s / &a[r][r];
1633    }
1634    Some(x)
1635}
1636
1637/// The straight line `y = slope·x + intercept` fitted by [`linear_fit`].
1638///
1639/// ```
1640/// use symplex::optimize::{linear_fit, LinearFit};
1641///
1642/// let LinearFit { slope, intercept } = linear_fit(&[0.0, 1.0, 2.0], &[1.0, 3.0, 5.0]).unwrap();
1643/// assert!((slope - 2.0).abs() < 1e-12 && (intercept - 1.0).abs() < 1e-12);
1644/// ```
1645#[derive(Clone, Copy, Debug, PartialEq)]
1646pub struct LinearFit {
1647    /// Coefficient of `x`.
1648    pub slope: f64,
1649    /// Value at `x = 0`.
1650    pub intercept: f64,
1651}
1652
1653/// Least-squares straight line `y ≈ slope·x + intercept`.
1654///
1655/// Returns a [`LinearFit`].  Equivalent to [`poly_fit`] with `degree = 1`;
1656/// needs at least two samples with distinct abscissae.
1657///
1658/// # Examples
1659///
1660/// ```
1661/// use symplex::optimize::linear_fit;
1662///
1663/// let xs = [0.0, 1.0, 2.0, 3.0];
1664/// let ys = [1.0, 4.0, 7.0, 10.0]; // y = 3x + 1
1665/// let fit = linear_fit(&xs, &ys).unwrap();
1666/// assert!((fit.slope - 3.0).abs() < 1e-12 && (fit.intercept - 1.0).abs() < 1e-12);
1667/// ```
1668pub fn linear_fit(xs: &[f64], ys: &[f64]) -> Result<LinearFit, SymplexError> {
1669    let c = poly_fit(xs, ys, 1)?;
1670    match c.as_slice() {
1671        [intercept, slope] => Ok(LinearFit {
1672            slope: *slope,
1673            intercept: *intercept,
1674        }),
1675        _ => Err(failed(
1676            "linear_fit",
1677            format!("expected two coefficients, got {}", c.len()),
1678        )),
1679    }
1680}
1681
1682/// Trapezoidal-rule integral of the samples `ys` at abscissae `xs`.
1683///
1684/// `Σ ½·(xs[i+1] − xs[i])·(ys[i] + ys[i+1])`; fewer than two samples give
1685/// `0`.  The abscissae need not be evenly spaced.
1686///
1687/// # Errors
1688///
1689/// [`SymplexError::InvalidArgument`] if the slices differ in length.
1690///
1691/// # Examples
1692///
1693/// ```
1694/// use symplex::optimize::trapezoid;
1695///
1696/// let xs: Vec<f64> = (0..=1000).map(|i| i as f64 / 1000.0).collect();
1697/// let ys: Vec<f64> = xs.iter().map(|x| x * x).collect();
1698/// assert!((trapezoid(&ys, &xs).unwrap() - 1.0 / 3.0).abs() < 1e-6);
1699/// ```
1700pub fn trapezoid(ys: &[f64], xs: &[f64]) -> Result<f64, SymplexError> {
1701    if ys.len() != xs.len() {
1702        return Err(invalid(
1703            "trapezoid",
1704            format!(
1705                "ys and xs must have the same length, got {} and {}",
1706                ys.len(),
1707                xs.len()
1708            ),
1709        ));
1710    }
1711    Ok(xs
1712        .windows(2)
1713        .zip(ys.windows(2))
1714        .map(|(x, y)| 0.5 * (x[1] - x[0]) * (y[0] + y[1]))
1715        .sum())
1716}
1717
1718/// Evaluate a polynomial given by **ascending** coefficients at `x`
1719/// (Horner's rule).
1720///
1721/// ```
1722/// use symplex::optimize::eval_poly;
1723///
1724/// assert_eq!(eval_poly(&[1.0, 2.0, 3.0], 2.0), 17.0); // 1 + 2·2 + 3·4
1725/// assert_eq!(eval_poly(&[], 5.0), 0.0);
1726/// ```
1727#[must_use]
1728pub fn eval_poly(coeffs_ascending: &[f64], x: f64) -> f64 {
1729    coeffs_ascending
1730        .iter()
1731        .rev()
1732        .fold(0.0, |acc, &c| acc * x + c)
1733}
1734
1735// ═══════════════════════════════════════════════════════════════════════════
1736// Ex conveniences
1737// ═══════════════════════════════════════════════════════════════════════════
1738
1739/// Validate `vars` (same context, all symbols, covering every free symbol
1740/// of `expr`) and compile `expr` as a function of them, in order.
1741fn compile_in(
1742    expr: &Ex,
1743    vars: &[&Ex],
1744    operation: &'static str,
1745) -> Result<CompiledFn, SymplexError> {
1746    if vars.is_empty() {
1747        return Err(invalid(
1748            operation,
1749            "at least one variable is required".into(),
1750        ));
1751    }
1752    let ids: Vec<_> = vars.iter().map(|v| expr.checked_id(*v)).collect();
1753    let non_symbol = {
1754        let inner = expr.inner.read();
1755        ids.iter()
1756            .position(|&id| !matches!(inner.arena.node(id), ExprNode::Symbol(_)))
1757    };
1758    if let Some(i) = non_symbol {
1759        return Err(invalid(
1760            operation,
1761            format!("variables must be symbols, got `{}`", vars[i]),
1762        ));
1763    }
1764    if let Some(extra) = expr.free_symbols().into_iter().find(|s| !vars.contains(&s)) {
1765        return Err(SymplexError::FreeSymbol {
1766            name: format!("{extra}"),
1767        });
1768    }
1769    let names: Vec<String> = vars.iter().map(|v| format!("{v}")).collect();
1770    let name_refs: Vec<&str> = names.iter().map(String::as_str).collect();
1771    expr.compile(&name_refs)
1772}
1773
1774impl Ex {
1775    /// Numerically find a root of this expression in `var` inside the
1776    /// bracket `[a, b]` by [`brent_root`] with default [`RootOpts`].
1777    ///
1778    /// The expression is compiled with [`compile`](Self::compile) first, so
1779    /// evaluation is fast and the usual compile-time checks apply.
1780    ///
1781    /// # Errors
1782    ///
1783    /// * [`SymplexError::InvalidArgument`] if `var` is not a symbol, or the
1784    ///   bracket is invalid (non-finite, or no sign change).
1785    /// * [`SymplexError::FreeSymbol`] if the expression contains a symbol
1786    ///   other than `var`.
1787    /// * [`SymplexError::NotImplemented`] if the expression cannot be
1788    ///   compiled to `f64` arithmetic.
1789    /// * [`SymplexError::ComputationFailed`] if the iteration does not
1790    ///   converge or meets a non-finite value.
1791    ///
1792    /// # Examples
1793    ///
1794    /// ```
1795    /// use symplex::prelude::*;
1796    ///
1797    /// let ctx = Context::new();
1798    /// let x = ctx.symbol("x");
1799    /// let r = (&x.powi(2) - 2).find_root_bracket(&x, 0.0, 2.0).unwrap();
1800    /// assert!((r - 2f64.sqrt()).abs() < 1e-12);
1801    ///
1802    /// // A transcendental equation: cos x = x.
1803    /// let r = (x.cos() - &x).find_root_bracket(&x, 0.0, 1.0).unwrap();
1804    /// assert!((r - 0.739_085_133_215_160_6).abs() < 1e-12);
1805    ///
1806    /// // Another free symbol → FreeSymbol, not a silent NaN.
1807    /// let a = ctx.symbol("a");
1808    /// assert!(matches!(
1809    ///     (&x.powi(2) - &a).find_root_bracket(&x, 0.0, 2.0),
1810    ///     Err(SymplexError::FreeSymbol { .. })
1811    /// ));
1812    /// ```
1813    pub fn find_root_bracket(&self, var: &Ex, a: f64, b: f64) -> Result<f64, SymplexError> {
1814        self.find_root_bracket_with(var, a, b, &RootOpts::default())
1815    }
1816
1817    /// [`find_root_bracket`](Self::find_root_bracket) with explicit
1818    /// [`RootOpts`].
1819    ///
1820    /// # Examples
1821    ///
1822    /// ```
1823    /// use symplex::optimize::RootOpts;
1824    /// use symplex::prelude::*;
1825    ///
1826    /// let ctx = Context::new();
1827    /// let x = ctx.symbol("x");
1828    /// let opts = RootOpts { xtol: 1e-6, ..RootOpts::default() };
1829    /// let r = (x.exp() - 3).find_root_bracket_with(&x, 0.0, 2.0, &opts).unwrap();
1830    /// assert!((r - 3f64.ln()).abs() < 1e-6);
1831    /// ```
1832    pub fn find_root_bracket_with(
1833        &self,
1834        var: &Ex,
1835        a: f64,
1836        b: f64,
1837        opts: &RootOpts,
1838    ) -> Result<f64, SymplexError> {
1839        let f = compile_in(self, &[var], "find_root_bracket")?;
1840        brent_root(|x| f.call(&[x]), a, b, opts)
1841    }
1842
1843    /// Minimise this expression numerically over `vars` from the starting
1844    /// point `x0` by [`nelder_mead`] with default [`MinimizeOpts`].
1845    ///
1846    /// `x0[i]` is the initial value of `vars[i]`.
1847    ///
1848    /// # Errors
1849    ///
1850    /// * [`SymplexError::InvalidArgument`] if `vars` is empty, a variable is
1851    ///   not a symbol, or `x0.len() != vars.len()`.
1852    /// * [`SymplexError::FreeSymbol`] if the expression contains a symbol
1853    ///   not listed in `vars`.
1854    /// * [`SymplexError::NotImplemented`] if the expression cannot be
1855    ///   compiled to `f64` arithmetic.
1856    ///
1857    /// # Examples
1858    ///
1859    /// ```
1860    /// use symplex::prelude::*;
1861    ///
1862    /// let ctx = Context::new();
1863    /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
1864    /// let bowl = (&x - 1).powi(2) + (&y + 2).powi(2);
1865    /// let r = bowl.minimize_numeric(&[&x, &y], &[0.0, 0.0]).unwrap();
1866    /// assert!(r.converged);
1867    /// assert!((r.x[0] - 1.0).abs() < 1e-6 && (r.x[1] + 2.0).abs() < 1e-6);
1868    /// assert!(r.fun < 1e-12);
1869    /// ```
1870    pub fn minimize_numeric(
1871        &self,
1872        vars: &[&Ex],
1873        x0: &[f64],
1874    ) -> Result<MinimizeResult, SymplexError> {
1875        self.minimize_numeric_with(vars, x0, &MinimizeOpts::default())
1876    }
1877
1878    /// [`minimize_numeric`](Self::minimize_numeric) with explicit
1879    /// [`MinimizeOpts`].
1880    ///
1881    /// # Examples
1882    ///
1883    /// ```
1884    /// use symplex::optimize::MinimizeOpts;
1885    /// use symplex::prelude::*;
1886    ///
1887    /// let ctx = Context::new();
1888    /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
1889    /// let rosen = (1 - &x).powi(2) + 100 * (&y - &x.powi(2)).powi(2);
1890    /// let opts = MinimizeOpts { max_iter: 2000, ..MinimizeOpts::default() };
1891    /// let r = rosen.minimize_numeric_with(&[&x, &y], &[-1.2, 1.0], &opts).unwrap();
1892    /// assert!((r.x[0] - 1.0).abs() < 1e-4 && (r.x[1] - 1.0).abs() < 1e-4);
1893    /// ```
1894    pub fn minimize_numeric_with(
1895        &self,
1896        vars: &[&Ex],
1897        x0: &[f64],
1898        opts: &MinimizeOpts,
1899    ) -> Result<MinimizeResult, SymplexError> {
1900        const OP: &str = "minimize_numeric";
1901        if x0.len() != vars.len() {
1902            return Err(invalid(
1903                OP,
1904                format!(
1905                    "initial point has {} entries, expected {}",
1906                    x0.len(),
1907                    vars.len()
1908                ),
1909            ));
1910        }
1911        let f = compile_in(self, vars, OP)?;
1912        nelder_mead(|x| f.call(x), x0, opts)
1913    }
1914
1915    /// Minimise this expression in the single variable `var` over `[a, b]`
1916    /// by Brent's method ([`minimize_scalar`]) with default
1917    /// [`MinimizeOpts`].  Returns the minimiser and the value there as a
1918    /// [`ScalarMinimum`].
1919    ///
1920    /// # Errors
1921    ///
1922    /// As for [`find_root_bracket`](Self::find_root_bracket) plus the
1923    /// interval rules of [`minimize_scalar`].
1924    ///
1925    /// # Examples
1926    ///
1927    /// ```
1928    /// use symplex::prelude::*;
1929    ///
1930    /// let ctx = Context::new();
1931    /// let x = ctx.symbol("x");
1932    /// // x·ln x has its minimum −1/e at x = 1/e.
1933    /// let m = (&x * x.ln()).minimize_scalar_numeric(&x, 0.1, 2.0).unwrap();
1934    /// assert!((m.x - (-1.0f64).exp()).abs() < 1e-6);
1935    /// assert!((m.value + (-1.0f64).exp()).abs() < 1e-12);
1936    /// ```
1937    pub fn minimize_scalar_numeric(
1938        &self,
1939        var: &Ex,
1940        a: f64,
1941        b: f64,
1942    ) -> Result<ScalarMinimum, SymplexError> {
1943        let f = compile_in(self, &[var], "minimize_scalar_numeric")?;
1944        minimize_scalar(|x| f.call(&[x]), a, b, &MinimizeOpts::default())
1945    }
1946
1947    /// Globally minimise this expression over the box `bounds` (one closed
1948    /// [`Interval`] per entry of `vars`) by [`differential_evolution`].
1949    ///
1950    /// # Errors
1951    ///
1952    /// As for [`minimize_numeric`](Self::minimize_numeric), with
1953    /// `bounds.len()` playing the role of `x0.len()`, plus the option and
1954    /// bound rules of [`differential_evolution`].
1955    ///
1956    /// # Examples
1957    ///
1958    /// ```
1959    /// use symplex::optimize::DeOpts;
1960    /// use symplex::prelude::*;
1961    ///
1962    /// let ctx = Context::new();
1963    /// let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
1964    /// // Himmelblau's function has four global minima with f = 0.
1965    /// let h = (&x.powi(2) + &y - 11).powi(2) + (&x + &y.powi(2) - 7).powi(2);
1966    /// let box_ = [Interval::closed(-5.0, 5.0), Interval::closed(-5.0, 5.0)];
1967    /// let r = h.minimize_global_numeric(&[&x, &y], &box_, &DeOpts::default()).unwrap();
1968    /// assert!(r.fun < 1e-8, "f = {}", r.fun);
1969    /// ```
1970    pub fn minimize_global_numeric(
1971        &self,
1972        vars: &[&Ex],
1973        bounds: &[Interval<f64>],
1974        opts: &DeOpts,
1975    ) -> Result<MinimizeResult, SymplexError> {
1976        const OP: &str = "minimize_global_numeric";
1977        if bounds.len() != vars.len() {
1978            return Err(invalid(
1979                OP,
1980                format!("got {} bounds for {} variables", bounds.len(), vars.len()),
1981            ));
1982        }
1983        let f = compile_in(self, vars, OP)?;
1984        differential_evolution(|x| f.call(x), bounds, opts)
1985    }
1986
1987    /// Exact least-squares polynomial of degree `degree` in `var` through
1988    /// the rational points `(x, y)`.
1989    ///
1990    /// Each coordinate is constant-folded with [`eval`](Self::eval) and
1991    /// must then be a rational literal (`ctx.int`, `ctx.rational`,
1992    /// `sqrt(4)`, …).  The fit is computed by [`poly_fit_exact`], so the
1993    /// result is the exact least-squares polynomial — the interpolating
1994    /// polynomial when `degree + 1 == points.len()` or the data are
1995    /// consistent.
1996    ///
1997    /// # Errors
1998    ///
1999    /// * [`SymplexError::InvalidArgument`] if a coordinate is not a rational
2000    ///   literal after evaluation, or `degree >= points.len()`.
2001    /// * [`SymplexError::ComputationFailed`] if the normal equations are
2002    ///   singular (fewer than `degree + 1` distinct abscissae).
2003    ///
2004    /// # Panics
2005    ///
2006    /// Panics if `var` or a point belongs to a different context than
2007    /// `ctx` (the standard cross-context guard).
2008    ///
2009    /// # Examples
2010    ///
2011    /// ```
2012    /// use symplex::prelude::*;
2013    ///
2014    /// let ctx = Context::new();
2015    /// let x = ctx.symbol("x");
2016    /// // Five samples of x²/3 − x/2 + 1/7.
2017    /// let pts = [
2018    ///     (ctx.int(0), ctx.rational(1, 7)),
2019    ///     (ctx.int(1), ctx.rational(-1, 42)),
2020    ///     (ctx.int(2), ctx.rational(10, 21)),
2021    ///     (ctx.int(3), ctx.rational(23, 14)),
2022    ///     (ctx.int(4), ctx.rational(73, 21)),
2023    /// ];
2024    /// let p = Ex::poly_fit_points(&ctx, &pts, &x, 2).unwrap();
2025    /// let expected = &x.powi(2) * ctx.rational(1, 3) - &x * ctx.rational(1, 2) + ctx.rational(1, 7);
2026    /// assert!((&p - &expected).expand().is_zero_structural(), "{p}");
2027    ///
2028    /// // A symbolic coordinate is rejected.
2029    /// let a = ctx.symbol("a");
2030    /// assert!(Ex::poly_fit_points(&ctx, &[(ctx.int(0), a), (ctx.int(1), ctx.int(1))], &x, 1).is_err());
2031    /// ```
2032    pub fn poly_fit_points(
2033        ctx: &Context,
2034        points: &[(Ex, Ex)],
2035        var: &Ex,
2036        degree: usize,
2037    ) -> Result<Ex, SymplexError> {
2038        const OP: &str = "poly_fit_points";
2039        let _ = ctx.own_id(var);
2040        let to_ratio = |e: &Ex| -> Result<Ratio<BigInt>, SymplexError> {
2041            let _ = var.checked_id(e);
2042            e.eval().as_rational().ok_or_else(|| {
2043                invalid(
2044                    OP,
2045                    format!("point coordinate `{e}` is not a rational literal"),
2046                )
2047            })
2048        };
2049        let mut pts: Vec<(Ratio<BigInt>, Ratio<BigInt>)> = Vec::with_capacity(points.len());
2050        for (px, py) in points {
2051            pts.push((to_ratio(px)?, to_ratio(py)?));
2052        }
2053        let coeffs = poly_fit_exact(&pts, degree)?;
2054        let mut terms: Vec<Ex> = Vec::with_capacity(coeffs.len());
2055        for (i, c) in coeffs.into_iter().enumerate() {
2056            if c.is_zero() {
2057                continue;
2058            }
2059            let power =
2060                i64::try_from(i).map_err(|_| invalid(OP, format!("degree {i} is too large")))?;
2061            terms.push(ctx.from_ratio(c) * var.powi(power));
2062        }
2063        Ok(ctx.sum(&terms))
2064    }
2065}
2066
2067// ═══════════════════════════════════════════════════════════════════════════
2068// Tests
2069// ═══════════════════════════════════════════════════════════════════════════
2070
2071#[cfg(test)]
2072mod tests {
2073    use super::*;
2074
2075    #[test]
2076    fn householder_solves_square_system() {
2077        let a = [2.0, 1.0, 1.0, 3.0];
2078        let x = dense_f64::lstsq_householder(&a, 2, 2, &[3.0, 5.0]).unwrap();
2079        assert!((x[0] - 0.8).abs() < 1e-12 && (x[1] - 1.4).abs() < 1e-12);
2080    }
2081
2082    #[test]
2083    fn householder_detects_rank_deficiency() {
2084        let a = [1.0, 2.0, 2.0, 4.0, 3.0, 6.0];
2085        assert!(dense_f64::lstsq_householder(&a, 3, 2, &[1.0, 2.0, 3.0]).is_none());
2086    }
2087
2088    #[test]
2089    fn exact_solver_basic_and_singular() {
2090        let q = |n: i64| Ratio::from_integer(BigInt::from(n));
2091        let a = vec![vec![q(2), q(1)], vec![q(1), q(3)]];
2092        let x = solve_exact(a, vec![q(3), q(5)]).unwrap();
2093        assert_eq!(x[0], Ratio::new(BigInt::from(4), BigInt::from(5)));
2094        assert_eq!(x[1], Ratio::new(BigInt::from(7), BigInt::from(5)));
2095        let s = vec![vec![q(1), q(2)], vec![q(2), q(4)]];
2096        assert!(solve_exact(s, vec![q(1), q(2)]).is_none());
2097    }
2098
2099    #[test]
2100    fn argmin_picks_first_smallest() {
2101        assert_eq!(argmin(&[3.0, 1.0, 1.0, 2.0]), 1);
2102        assert_eq!(argmin(&[]), 0);
2103        assert_eq!(argmin(&[f64::INFINITY, f64::INFINITY]), 0);
2104    }
2105}