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