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}