use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::base::node::ExprNode;
use crate::output::lambdify::CompiledFn;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, Zero};
fn invalid(operation: &'static str, reason: String) -> SymplexError {
SymplexError::InvalidArgument { operation, reason }
}
fn failed(operation: &'static str, reason: String) -> SymplexError {
SymplexError::ComputationFailed { operation, reason }
}
fn nan_to_inf(v: f64) -> f64 {
if v.is_nan() { f64::INFINITY } else { v }
}
fn check_endpoints(op: &'static str, a: f64, b: f64) -> Result<(), SymplexError> {
if !a.is_finite() || !b.is_finite() {
return Err(invalid(
op,
format!("interval endpoints must be finite, got [{a}, {b}]"),
));
}
Ok(())
}
fn check_interval(op: &'static str, a: f64, b: f64) -> Result<(f64, f64), SymplexError> {
check_endpoints(op, a, b)?;
if a == b {
return Err(invalid(
op,
format!("interval must have positive width, got [{a}, {b}]"),
));
}
Ok(if a < b { (a, b) } else { (b, a) })
}
#[derive(Clone, Debug, PartialEq)]
pub struct RootOpts {
pub xtol: f64,
pub rtol: f64,
pub max_iter: usize,
}
impl Default for RootOpts {
fn default() -> Self {
Self {
xtol: 2e-12,
rtol: 4.0 * f64::EPSILON,
max_iter: 100,
}
}
}
fn check_root_opts(op: &'static str, opts: &RootOpts) -> Result<(), SymplexError> {
let bad = |t: f64| t < 0.0 || !t.is_finite();
if bad(opts.xtol) || bad(opts.rtol) {
return Err(invalid(
op,
format!(
"tolerances must be finite and non-negative, got xtol = {}, rtol = {}",
opts.xtol, opts.rtol
),
));
}
Ok(())
}
fn check_bracket(
op: &'static str,
a: f64,
b: f64,
fa: f64,
fb: f64,
) -> Result<Option<f64>, SymplexError> {
if !fa.is_finite() || !fb.is_finite() {
return Err(invalid(
op,
format!(
"function is not finite at the bracket endpoints: f({a}) = {fa}, f({b}) = {fb}"
),
));
}
if fa == 0.0 {
return Ok(Some(a));
}
if fb == 0.0 {
return Ok(Some(b));
}
if (fa > 0.0) == (fb > 0.0) {
return Err(invalid(
op,
format!("f(a) and f(b) must have opposite signs: f({a}) = {fa}, f({b}) = {fb}"),
));
}
Ok(None)
}
pub fn brent_root(
f: impl Fn(f64) -> f64,
a: f64,
b: f64,
opts: &RootOpts,
) -> Result<f64, SymplexError> {
const OP: &str = "brent_root";
check_root_opts(OP, opts)?;
check_endpoints(OP, a, b)?;
let (mut a, mut b) = (a, b);
let (mut fa, mut fb) = (f(a), f(b));
if let Some(root) = check_bracket(OP, a, b, fa, fb)? {
return Ok(root);
}
let mut c = a;
let mut fc = fa;
let mut d = b - a;
let mut e = d;
for _ in 0..opts.max_iter {
if (fb > 0.0) == (fc > 0.0) {
c = a;
fc = fa;
d = b - a;
e = d;
}
if fc.abs() < fb.abs() {
a = b;
b = c;
c = a;
fa = fb;
fb = fc;
fc = fa;
}
let tol1 = 0.5 * (opts.xtol + opts.rtol * b.abs());
let xm = 0.5 * (c - b);
if xm.abs() <= tol1 || fb == 0.0 {
return Ok(b);
}
if e.abs() >= tol1 && fa.abs() > fb.abs() {
let s = fb / fa;
let (mut p, mut q) = if a == c {
(2.0 * xm * s, 1.0 - s)
} else {
let q = fa / fc;
let r = fb / fc;
(
s * (2.0 * xm * q * (q - r) - (b - a) * (r - 1.0)),
(q - 1.0) * (r - 1.0) * (s - 1.0),
)
};
if p > 0.0 {
q = -q;
}
p = p.abs();
let min1 = 3.0 * xm * q - (tol1 * q).abs();
let min2 = (e * q).abs();
if 2.0 * p < min1.min(min2) {
e = d;
d = p / q;
} else {
d = xm;
e = d;
}
} else {
d = xm;
e = d;
}
a = b;
fa = fb;
b += if d.abs() > tol1 { d } else { tol1.copysign(xm) };
fb = f(b);
if !fb.is_finite() {
return Err(failed(OP, format!("f({b}) = {fb} is not finite")));
}
}
Err(failed(
OP,
format!(
"did not converge within {} iterations; bracket [{}, {}] has width {:.3e}",
opts.max_iter,
b.min(c),
b.max(c),
(c - b).abs()
),
))
}
pub fn bisect(
f: impl Fn(f64) -> f64,
a: f64,
b: f64,
opts: &RootOpts,
) -> Result<f64, SymplexError> {
const OP: &str = "bisect";
check_root_opts(OP, opts)?;
check_endpoints(OP, a, b)?;
let (fa, fb) = (f(a), f(b));
if let Some(root) = check_bracket(OP, a, b, fa, fb)? {
return Ok(root);
}
let (mut lo, mut hi, mut flo) = (a, b, fa);
for _ in 0..opts.max_iter {
let mid = lo + 0.5 * (hi - lo);
let fm = f(mid);
if !fm.is_finite() {
return Err(failed(OP, format!("f({mid}) = {fm} is not finite")));
}
if fm == 0.0 {
return Ok(mid);
}
if (fm > 0.0) == (flo > 0.0) {
lo = mid;
flo = fm;
} else {
hi = mid;
}
let mid = lo + 0.5 * (hi - lo);
if (hi - lo).abs() <= opts.xtol + opts.rtol * mid.abs() {
return Ok(mid);
}
}
Err(failed(
OP,
format!(
"did not converge within {} iterations; bracket [{}, {}] has width {:.3e}",
opts.max_iter,
lo.min(hi),
lo.max(hi),
(hi - lo).abs()
),
))
}
pub fn newton_root(
f: impl Fn(f64) -> f64,
df: impl Fn(f64) -> f64,
x0: f64,
opts: &RootOpts,
) -> Result<f64, SymplexError> {
const OP: &str = "newton_root";
check_root_opts(OP, opts)?;
if !x0.is_finite() {
return Err(invalid(
OP,
format!("initial guess must be finite, got {x0}"),
));
}
let mut x = x0;
for _ in 0..opts.max_iter {
let fx = f(x);
if !fx.is_finite() {
return Err(failed(
OP,
format!("f({x:e}) = {fx:e} is not finite; the iteration diverged"),
));
}
if fx == 0.0 {
return Ok(x);
}
let dfx = df(x);
if !dfx.is_finite() || dfx == 0.0 {
return Err(failed(
OP,
format!("derivative f'({x:e}) = {dfx:e} vanishes or is not finite"),
));
}
let step = fx / dfx;
let x_new = x - step;
if !x_new.is_finite() {
return Err(failed(
OP,
format!("iterate became non-finite after the step {step:e} from x = {x:e}"),
));
}
if step.abs() <= opts.xtol + opts.rtol * x_new.abs() {
return Ok(x_new);
}
x = x_new;
}
Err(failed(
OP,
format!(
"did not converge within {} iterations; last iterate x = {x}, |f(x)| = {:.3e}",
opts.max_iter,
f(x).abs()
),
))
}
#[derive(Clone, Debug, PartialEq)]
pub struct MinimizeOpts {
pub xtol: f64,
pub ftol: f64,
pub max_iter: usize,
pub initial_step: f64,
}
impl Default for MinimizeOpts {
fn default() -> Self {
Self {
xtol: 1e-8,
ftol: 1e-12,
max_iter: 0,
initial_step: 0.0,
}
}
}
impl MinimizeOpts {
fn effective_max_iter(&self, n: usize) -> usize {
if self.max_iter == 0 {
200usize.saturating_mul(n)
} else {
self.max_iter
}
}
}
fn check_minimize_opts(op: &'static str, opts: &MinimizeOpts) -> Result<(), SymplexError> {
let bad = |t: f64| t < 0.0 || !t.is_finite();
if bad(opts.xtol) || bad(opts.ftol) || bad(opts.initial_step) {
return Err(invalid(
op,
format!(
"xtol, ftol and initial_step must be finite and non-negative, got {}, {}, {}",
opts.xtol, opts.ftol, opts.initial_step
),
));
}
Ok(())
}
#[derive(Clone, Debug, PartialEq)]
pub struct MinimizeResult {
pub x: Vec<f64>,
pub fun: f64,
pub iterations: usize,
pub evaluations: usize,
pub converged: bool,
}
pub fn nelder_mead(
mut f: impl FnMut(&[f64]) -> f64,
x0: &[f64],
opts: &MinimizeOpts,
) -> Result<MinimizeResult, SymplexError> {
const OP: &str = "nelder_mead";
let n = x0.len();
if n == 0 {
return Err(invalid(OP, "initial point must not be empty".into()));
}
if x0.iter().any(|v| !v.is_finite()) {
return Err(invalid(
OP,
format!("initial point must be finite, got {x0:?}"),
));
}
check_minimize_opts(OP, opts)?;
let max_iter = opts.effective_max_iter(n);
let mut evaluations = 0usize;
let mut eval = |x: &[f64]| -> f64 {
evaluations += 1;
nan_to_inf(f(x))
};
let f0 = eval(x0);
if !f0.is_finite() {
return Err(invalid(
OP,
format!("f(x0) = {f0} is not finite at x0 = {x0:?}"),
));
}
let mut vertices: Vec<(Vec<f64>, f64)> = Vec::with_capacity(n + 1);
vertices.push((x0.to_vec(), f0));
for i in 0..n {
let mut p = x0.to_vec();
p[i] = if opts.initial_step > 0.0 {
p[i] + opts.initial_step
} else if p[i] != 0.0 {
p[i] * 1.05
} else {
0.000_25
};
let fp = eval(&p);
vertices.push((p, fp));
}
let nf = n as f64;
let (rho, chi, psi, sigma) = if n > 2 {
(1.0, 1.0 + 2.0 / nf, 0.75 - 0.5 / nf, 1.0 - 1.0 / nf)
} else {
(1.0, 2.0, 0.5, 0.5)
};
let along = |xbar: &[f64], xw: &[f64], t: f64| -> Vec<f64> {
xbar.iter()
.zip(xw)
.map(|(c, w)| (1.0 + t) * c - t * w)
.collect()
};
let mut iterations = 0usize;
let mut converged = false;
loop {
vertices.sort_by(|a, b| a.1.total_cmp(&b.1));
if vertices[0].1 == f64::NEG_INFINITY {
return Err(failed(
OP,
format!(
"objective is unbounded below: f = -inf at {:?}",
vertices[0].0
),
));
}
if simplex_converged(&vertices, opts.xtol, opts.ftol) {
converged = true;
break;
}
if iterations >= max_iter {
break;
}
iterations += 1;
let mut xbar = vec![0.0; n];
for (v, _) in &vertices[..n] {
for (c, xi) in xbar.iter_mut().zip(v) {
*c += xi;
}
}
for c in &mut xbar {
*c /= nf;
}
let xw = vertices[n].0.clone();
let fw = vertices[n].1;
let f_best = vertices[0].1;
let f_second_worst = vertices[n - 1].1;
let xr = along(&xbar, &xw, rho);
let fr = eval(&xr);
if fr < f_best {
let xe = along(&xbar, &xw, rho * chi);
let fe = eval(&xe);
vertices[n] = if fe < fr { (xe, fe) } else { (xr, fr) };
} else if fr < f_second_worst {
vertices[n] = (xr, fr);
} else {
let mut shrink = false;
if fr < fw {
let xc = along(&xbar, &xw, psi * rho);
let fc = eval(&xc);
if fc <= fr {
vertices[n] = (xc, fc);
} else {
shrink = true;
}
} else {
let xcc = along(&xbar, &xw, -psi);
let fcc = eval(&xcc);
if fcc < fw {
vertices[n] = (xcc, fcc);
} else {
shrink = true;
}
}
if shrink {
let best = vertices[0].0.clone();
for (v, fv) in vertices.iter_mut().skip(1) {
for (xj, bj) in v.iter_mut().zip(&best) {
*xj = bj + sigma * (*xj - bj);
}
*fv = eval(v);
}
}
}
}
let (x, fun) = vertices.swap_remove(0);
Ok(MinimizeResult {
x,
fun,
iterations,
evaluations,
converged,
})
}
fn simplex_converged(vertices: &[(Vec<f64>, f64)], xtol: f64, ftol: f64) -> bool {
let Some(((x0, f0), rest)) = vertices.split_first() else {
return true;
};
let dx = rest
.iter()
.flat_map(|(x, _)| x.iter().zip(x0).map(|(a, b)| (a - b).abs()))
.fold(0.0_f64, f64::max);
let df = rest
.iter()
.map(|(_, fv)| (fv - f0).abs())
.fold(0.0_f64, f64::max);
dx <= xtol && df <= ftol
}
fn eval_finite(op: &'static str, f: &impl Fn(f64) -> f64, x: f64) -> Result<f64, SymplexError> {
let v = f(x);
if v.is_finite() {
Ok(v)
} else {
Err(failed(op, format!("f({x}) = {v} is not finite")))
}
}
pub fn minimize_scalar(
f: impl Fn(f64) -> f64,
a: f64,
b: f64,
opts: &MinimizeOpts,
) -> Result<(f64, f64), SymplexError> {
const OP: &str = "minimize_scalar";
let (mut a, mut b) = check_interval(OP, a, b)?;
check_minimize_opts(OP, opts)?;
let max_iter = opts.effective_max_iter(1);
let cgold = 0.5 * (3.0 - 5.0_f64.sqrt());
let sqrt_eps = f64::EPSILON.sqrt();
let mut x = a + cgold * (b - a);
let mut w = x;
let mut v = x;
let mut fx = eval_finite(OP, &f, x)?;
let mut fw = fx;
let mut fv = fx;
let mut d = 0.0_f64; let mut e = 0.0_f64;
for _ in 0..max_iter {
let xm = 0.5 * (a + b);
let tol1 = sqrt_eps * x.abs() + opts.xtol / 3.0;
let tol2 = 2.0 * tol1;
if (x - xm).abs() <= tol2 - 0.5 * (b - a) {
return Ok((x, fx));
}
let golden = if e.abs() > tol1 {
let r = (x - w) * (fx - fv);
let mut q = (x - v) * (fx - fw);
let mut p = (x - v) * q - (x - w) * r;
q = 2.0 * (q - r);
if q > 0.0 {
p = -p;
}
q = q.abs();
let e_prev = e;
e = d;
if p.abs() >= (0.5 * q * e_prev).abs() || p <= q * (a - x) || p >= q * (b - x) {
true
} else {
d = p / q;
let u = x + d;
if u - a < tol2 || b - u < tol2 {
d = tol1.copysign(xm - x);
}
false
}
} else {
true
};
if golden {
e = if x >= xm { a - x } else { b - x };
d = cgold * e;
}
let u = if d.abs() >= tol1 {
x + d
} else {
x + tol1.copysign(d)
};
let fu = eval_finite(OP, &f, u)?;
if fu <= fx {
if u >= x {
a = x;
} else {
b = x;
}
v = w;
fv = fw;
w = x;
fw = fx;
x = u;
fx = fu;
} else {
if u < x {
a = u;
} else {
b = u;
}
if fu <= fw || w == x {
v = w;
fv = fw;
w = u;
fw = fu;
} else if fu <= fv || v == x || v == w {
v = u;
fv = fu;
}
}
}
Err(failed(
OP,
format!("did not converge within {max_iter} iterations; bracket [{a}, {b}], best x = {x}"),
))
}
pub fn golden_section(
f: impl Fn(f64) -> f64,
a: f64,
b: f64,
opts: &MinimizeOpts,
) -> Result<(f64, f64), SymplexError> {
const OP: &str = "golden_section";
let (mut a, mut b) = check_interval(OP, a, b)?;
check_minimize_opts(OP, opts)?;
let max_iter = opts.effective_max_iter(1);
let inv_phi = 0.5 * (5.0_f64.sqrt() - 1.0);
let sqrt_eps = f64::EPSILON.sqrt();
let mut x1 = b - inv_phi * (b - a);
let mut x2 = a + inv_phi * (b - a);
let mut f1 = eval_finite(OP, &f, x1)?;
let mut f2 = eval_finite(OP, &f, x2)?;
for _ in 0..max_iter {
let mid = 0.5 * (a + b);
if (b - a).abs() <= opts.xtol + sqrt_eps * mid.abs() {
return Ok(if f1 <= f2 { (x1, f1) } else { (x2, f2) });
}
if f1 < f2 {
b = x2;
x2 = x1;
f2 = f1;
x1 = b - inv_phi * (b - a);
f1 = eval_finite(OP, &f, x1)?;
} else {
a = x1;
x1 = x2;
f1 = f2;
x2 = a + inv_phi * (b - a);
f2 = eval_finite(OP, &f, x2)?;
}
}
Err(failed(
OP,
format!("did not converge within {max_iter} iterations; bracket [{a}, {b}]"),
))
}
struct SplitMix64(u64);
impl SplitMix64 {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn next_f64(&mut self) -> f64 {
const SCALE: f64 = 1.0 / (1u64 << 53) as f64;
(self.next_u64() >> 11) as f64 * SCALE
}
fn below(&mut self, n: usize) -> usize {
(self.next_u64() % (n as u64).max(1)) as usize
}
fn below_excluding(&mut self, n: usize, excluded: &mut [usize]) -> usize {
excluded.sort_unstable();
let mut r = self.below(n.saturating_sub(excluded.len()));
for &e in excluded.iter() {
if r >= e {
r += 1;
}
}
r
}
fn shuffle<T>(&mut self, items: &mut [T]) {
for i in (1..items.len()).rev() {
let j = self.below(i + 1);
items.swap(i, j);
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DeOpts {
pub population: usize,
pub max_generations: usize,
pub crossover: f64,
pub differential_weight: f64,
pub tol: f64,
pub seed: u64,
}
impl Default for DeOpts {
fn default() -> Self {
Self {
population: 0,
max_generations: 300,
crossover: 0.7,
differential_weight: 0.8,
tol: 1e-8,
seed: 0,
}
}
}
fn population_converged(energies: &[f64], tol: f64) -> bool {
let n = energies.len() as f64;
if n == 0.0 {
return true;
}
let mean = energies.iter().sum::<f64>() / n;
let var = energies
.iter()
.map(|e| (e - mean) * (e - mean))
.sum::<f64>()
/ n;
var.sqrt() <= tol * (1.0 + mean.abs())
}
pub fn differential_evolution(
mut f: impl FnMut(&[f64]) -> f64,
bounds: &[(f64, f64)],
opts: &DeOpts,
) -> Result<MinimizeResult, SymplexError> {
const OP: &str = "differential_evolution";
let n = bounds.len();
if n == 0 {
return Err(invalid(OP, "bounds must not be empty".into()));
}
for &(lo, hi) in bounds {
if !lo.is_finite() || !hi.is_finite() || lo > hi {
return Err(invalid(
OP,
format!(
"each bound must be a finite (lo, hi) pair with lo <= hi, got ({lo}, {hi})"
),
));
}
}
if !(0.0..=1.0).contains(&opts.crossover) {
return Err(invalid(
OP,
format!("crossover must lie in [0, 1], got {}", opts.crossover),
));
}
if !opts.differential_weight.is_finite() || opts.differential_weight <= 0.0 {
return Err(invalid(
OP,
format!(
"differential_weight must be positive and finite, got {}",
opts.differential_weight
),
));
}
if !opts.tol.is_finite() || opts.tol < 0.0 {
return Err(invalid(
OP,
format!("tol must be finite and non-negative, got {}", opts.tol),
));
}
let np = if opts.population == 0 {
(15 * n).max(8)
} else {
opts.population
};
if np < 4 {
return Err(invalid(
OP,
format!("population must be at least 4, got {np}"),
));
}
let mut rng = SplitMix64::new(opts.seed);
let mut evaluations = 0usize;
let mut eval = |x: &[f64]| -> f64 {
evaluations += 1;
nan_to_inf(f(x))
};
let mut pop = vec![vec![0.0; n]; np];
let mut perm: Vec<usize> = (0..np).collect();
for (j, &(lo, hi)) in bounds.iter().enumerate() {
rng.shuffle(&mut perm);
for (member, &slice) in pop.iter_mut().zip(&perm) {
let u = (slice as f64 + rng.next_f64()) / np as f64;
member[j] = lo + u * (hi - lo);
}
}
let mut energies: Vec<f64> = pop.iter().map(|m| eval(m)).collect();
let mut best = argmin(&energies);
let mut generations = 0usize;
let mut converged = false;
let mut trial = vec![0.0; n];
loop {
if population_converged(&energies, opts.tol) {
converged = true;
break;
}
if generations >= opts.max_generations {
break;
}
generations += 1;
for i in 0..np {
let r1 = rng.below_excluding(np, &mut [i]);
let r2 = rng.below_excluding(np, &mut [i, r1]);
let r3 = rng.below_excluding(np, &mut [i, r1, r2]);
let j_rand = rng.below(n);
for (j, &(lo, hi)) in bounds.iter().enumerate() {
let v = if j == j_rand || rng.next_f64() < opts.crossover {
pop[r1][j] + opts.differential_weight * (pop[r2][j] - pop[r3][j])
} else {
pop[i][j]
};
trial[j] = v.clamp(lo, hi);
}
let ft = eval(&trial);
if ft <= energies[i] {
pop[i].copy_from_slice(&trial);
energies[i] = ft;
if ft < energies[best] {
best = i;
}
}
}
}
let de_evaluations = evaluations;
let f_best = energies[best];
if !f_best.is_finite() {
return Err(failed(
OP,
format!("objective has no finite value in the box after {generations} generations"),
));
}
let mut clipped = vec![0.0; n];
let polish = nelder_mead(
|x: &[f64]| {
for ((c, &xi), &(lo, hi)) in clipped.iter_mut().zip(x).zip(bounds) {
*c = xi.clamp(lo, hi);
}
f(&clipped)
},
&pop[best],
&MinimizeOpts::default(),
)
.map_err(|e| failed(OP, format!("Nelder–Mead polish failed: {e}")))?;
let (x, fun) = if polish.fun < f_best {
let x = polish
.x
.iter()
.zip(bounds)
.map(|(&xi, &(lo, hi))| xi.clamp(lo, hi))
.collect();
(x, polish.fun)
} else {
(pop.swap_remove(best), f_best)
};
Ok(MinimizeResult {
x,
fun,
iterations: generations,
evaluations: de_evaluations + polish.evaluations,
converged,
})
}
fn argmin(values: &[f64]) -> usize {
values.iter().enumerate().fold(
0usize,
|best, (i, &v)| {
if v < values[best] { i } else { best }
},
)
}
pub fn poly_fit(xs: &[f64], ys: &[f64], degree: usize) -> Result<Vec<f64>, SymplexError> {
const OP: &str = "poly_fit";
let m = xs.len();
if m != ys.len() {
return Err(invalid(
OP,
format!(
"xs and ys must have the same length, got {m} and {}",
ys.len()
),
));
}
if degree >= m {
return Err(invalid(
OP,
format!(
"degree {degree} needs at least {} points, got {m}",
degree + 1
),
));
}
if xs.iter().chain(ys).any(|v| !v.is_finite()) {
return Err(invalid(OP, "all samples must be finite".into()));
}
let ncols = degree + 1;
let mut a: Vec<Vec<f64>> = xs
.iter()
.map(|&x| {
let mut p = 1.0;
(0..ncols)
.map(|_| {
let v = p;
p *= x;
v
})
.collect()
})
.collect();
let mut scale = vec![1.0; ncols];
for (j, s) in scale.iter_mut().enumerate() {
let norm = a.iter().map(|row| row[j] * row[j]).sum::<f64>().sqrt();
if norm > 0.0 && norm.is_finite() {
*s = norm;
for row in &mut a {
row[j] /= norm;
}
}
}
let c = lstsq_householder(a, ys.to_vec(), ncols).ok_or_else(|| {
failed(
OP,
format!("Vandermonde matrix is rank deficient: fewer than {ncols} distinct abscissae"),
)
})?;
Ok(c.iter().zip(&scale).map(|(c, s)| c / s).collect())
}
fn lstsq_householder(mut a: Vec<Vec<f64>>, mut b: Vec<f64>, n: usize) -> Option<Vec<f64>> {
let m = a.len();
if m < n || b.len() != m {
return None;
}
for k in 0..n {
let norm = (k..m).map(|i| a[i][k] * a[i][k]).sum::<f64>().sqrt();
if norm == 0.0 || !norm.is_finite() {
return None;
}
let alpha = if a[k][k] > 0.0 { -norm } else { norm };
let mut v: Vec<f64> = (k..m).map(|i| a[i][k]).collect();
v[0] -= alpha;
let vnorm2: f64 = v.iter().map(|x| x * x).sum();
if vnorm2 == 0.0 {
continue;
}
let scale = 2.0 / vnorm2;
let w: Vec<f64> = (k..n)
.map(|j| v.iter().zip(k..m).map(|(vi, i)| vi * a[i][j]).sum::<f64>())
.collect();
for (vi, i) in v.iter().zip(k..m) {
for (wj, entry) in w.iter().zip(a[i][k..].iter_mut()) {
*entry -= scale * vi * wj;
}
}
let s: f64 = v.iter().zip(k..m).map(|(vi, i)| vi * b[i]).sum();
let factor = scale * s;
for (vi, i) in v.iter().zip(k..m) {
b[i] -= factor * vi;
}
}
let r_max = (0..n).map(|k| a[k][k].abs()).fold(0.0_f64, f64::max);
let threshold = r_max * f64::EPSILON * m as f64;
let mut x = vec![0.0; n];
for r in (0..n).rev() {
let diag = a[r][r];
if !diag.is_finite() || diag.abs() <= threshold {
return None;
}
let s = b[r] - ((r + 1)..n).map(|c| a[r][c] * x[c]).sum::<f64>();
x[r] = s / diag;
}
Some(x)
}
pub fn poly_fit_exact(
points: &[(Ratio<BigInt>, Ratio<BigInt>)],
degree: usize,
) -> Result<Vec<Ratio<BigInt>>, SymplexError> {
const OP: &str = "poly_fit_exact";
let m = points.len();
if degree >= m {
return Err(invalid(
OP,
format!(
"degree {degree} needs at least {} points, got {m}",
degree + 1
),
));
}
let ncols = degree + 1;
let mut power_sums = vec![Ratio::<BigInt>::zero(); 2 * degree + 1];
let mut moments = vec![Ratio::<BigInt>::zero(); ncols];
for (x, y) in points {
let mut pow = Ratio::<BigInt>::one();
for (p, s) in power_sums.iter_mut().enumerate() {
*s += &pow;
if let Some(t) = moments.get_mut(p) {
*t += &pow * y;
}
if p + 1 < 2 * degree + 1 {
pow *= x;
}
}
}
let normal: Vec<Vec<Ratio<BigInt>>> = (0..ncols)
.map(|j| (0..ncols).map(|k| power_sums[j + k].clone()).collect())
.collect();
solve_exact(normal, moments).ok_or_else(|| {
failed(
OP,
format!("normal equations are singular: fewer than {ncols} distinct abscissae"),
)
})
}
fn solve_exact(
mut a: Vec<Vec<Ratio<BigInt>>>,
mut b: Vec<Ratio<BigInt>>,
) -> Option<Vec<Ratio<BigInt>>> {
let n = b.len();
if a.len() != n || a.iter().any(|row| row.len() != n) {
return None;
}
for col in 0..n {
let pivot = (col..n).find(|&r| !a[r][col].is_zero())?;
a.swap(col, pivot);
b.swap(col, pivot);
let pivot_row = a[col].clone();
let pivot_b = b[col].clone();
for r in (col + 1)..n {
if a[r][col].is_zero() {
continue;
}
let factor = &a[r][col] / &pivot_row[col];
for (entry, p) in a[r].iter_mut().zip(&pivot_row).skip(col) {
*entry -= &factor * p;
}
b[r] -= &factor * &pivot_b;
}
}
let mut x = vec![Ratio::<BigInt>::zero(); n];
for r in (0..n).rev() {
let mut s = b[r].clone();
for c in (r + 1)..n {
s -= &a[r][c] * &x[c];
}
x[r] = s / &a[r][r];
}
Some(x)
}
pub fn linear_fit(xs: &[f64], ys: &[f64]) -> Result<(f64, f64), SymplexError> {
let c = poly_fit(xs, ys, 1)?;
match c.as_slice() {
[intercept, slope] => Ok((*slope, *intercept)),
_ => Err(failed(
"linear_fit",
format!("expected two coefficients, got {}", c.len()),
)),
}
}
pub fn trapezoid(ys: &[f64], xs: &[f64]) -> Result<f64, SymplexError> {
if ys.len() != xs.len() {
return Err(invalid(
"trapezoid",
format!(
"ys and xs must have the same length, got {} and {}",
ys.len(),
xs.len()
),
));
}
Ok(xs
.windows(2)
.zip(ys.windows(2))
.map(|(x, y)| 0.5 * (x[1] - x[0]) * (y[0] + y[1]))
.sum())
}
#[must_use]
pub fn eval_poly(coeffs_ascending: &[f64], x: f64) -> f64 {
coeffs_ascending
.iter()
.rev()
.fold(0.0, |acc, &c| acc * x + c)
}
fn compile_in(
expr: &Ex,
vars: &[&Ex],
operation: &'static str,
) -> Result<CompiledFn, SymplexError> {
if vars.is_empty() {
return Err(invalid(
operation,
"at least one variable is required".into(),
));
}
let ids: Vec<_> = vars.iter().map(|v| expr.checked_id(*v)).collect();
let non_symbol = {
let inner = expr.inner.read();
ids.iter()
.position(|&id| !matches!(inner.arena.node(id), ExprNode::Symbol(_)))
};
if let Some(i) = non_symbol {
return Err(invalid(
operation,
format!("variables must be symbols, got `{}`", vars[i]),
));
}
if let Some(extra) = expr.free_symbols().into_iter().find(|s| !vars.contains(&s)) {
return Err(SymplexError::FreeSymbol {
name: format!("{extra}"),
});
}
let names: Vec<String> = vars.iter().map(|v| format!("{v}")).collect();
let name_refs: Vec<&str> = names.iter().map(String::as_str).collect();
expr.compile(&name_refs)
}
impl Ex {
pub fn find_root_bracket(&self, var: &Ex, a: f64, b: f64) -> Result<f64, SymplexError> {
self.find_root_bracket_with(var, a, b, &RootOpts::default())
}
pub fn find_root_bracket_with(
&self,
var: &Ex,
a: f64,
b: f64,
opts: &RootOpts,
) -> Result<f64, SymplexError> {
let f = compile_in(self, &[var], "find_root_bracket")?;
brent_root(|x| f.call(&[x]), a, b, opts)
}
pub fn minimize_numeric(
&self,
vars: &[&Ex],
x0: &[f64],
) -> Result<MinimizeResult, SymplexError> {
self.minimize_numeric_with(vars, x0, &MinimizeOpts::default())
}
pub fn minimize_numeric_with(
&self,
vars: &[&Ex],
x0: &[f64],
opts: &MinimizeOpts,
) -> Result<MinimizeResult, SymplexError> {
const OP: &str = "minimize_numeric";
if x0.len() != vars.len() {
return Err(invalid(
OP,
format!(
"initial point has {} entries, expected {}",
x0.len(),
vars.len()
),
));
}
let f = compile_in(self, vars, OP)?;
nelder_mead(|x| f.call(x), x0, opts)
}
pub fn minimize_scalar_numeric(
&self,
var: &Ex,
a: f64,
b: f64,
) -> Result<(f64, f64), SymplexError> {
let f = compile_in(self, &[var], "minimize_scalar_numeric")?;
minimize_scalar(|x| f.call(&[x]), a, b, &MinimizeOpts::default())
}
pub fn minimize_global_numeric(
&self,
vars: &[&Ex],
bounds: &[(f64, f64)],
opts: &DeOpts,
) -> Result<MinimizeResult, SymplexError> {
const OP: &str = "minimize_global_numeric";
if bounds.len() != vars.len() {
return Err(invalid(
OP,
format!("got {} bounds for {} variables", bounds.len(), vars.len()),
));
}
let f = compile_in(self, vars, OP)?;
differential_evolution(|x| f.call(x), bounds, opts)
}
pub fn poly_fit_points(
ctx: &Context,
points: &[(Ex, Ex)],
var: &Ex,
degree: usize,
) -> Result<Ex, SymplexError> {
const OP: &str = "poly_fit_points";
let _ = ctx.own_id(var);
let to_ratio = |e: &Ex| -> Result<Ratio<BigInt>, SymplexError> {
let _ = var.checked_id(e);
e.eval().as_rational().ok_or_else(|| {
invalid(
OP,
format!("point coordinate `{e}` is not a rational literal"),
)
})
};
let mut pts: Vec<(Ratio<BigInt>, Ratio<BigInt>)> = Vec::with_capacity(points.len());
for (px, py) in points {
pts.push((to_ratio(px)?, to_ratio(py)?));
}
let coeffs = poly_fit_exact(&pts, degree)?;
let mut terms: Vec<Ex> = Vec::with_capacity(coeffs.len());
for (i, c) in coeffs.into_iter().enumerate() {
if c.is_zero() {
continue;
}
let power =
i64::try_from(i).map_err(|_| invalid(OP, format!("degree {i} is too large")))?;
terms.push(ctx.from_ratio(c) * var.powi(power));
}
Ok(ctx.sum(&terms))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splitmix_is_deterministic_and_in_range() {
let mut a = SplitMix64::new(42);
let mut b = SplitMix64::new(42);
for _ in 0..100 {
let u = a.next_f64();
assert_eq!(u, b.next_f64());
assert!((0.0..1.0).contains(&u));
let k = a.below(7);
assert_eq!(k, b.below(7));
assert!(k < 7);
}
assert_eq!(SplitMix64::new(0).below(0), 0);
}
#[test]
fn below_excluding_never_returns_excluded() {
let mut rng = SplitMix64::new(7);
for _ in 0..1000 {
let i = rng.below(10);
let r1 = rng.below_excluding(10, &mut [i]);
assert_ne!(r1, i);
let r2 = rng.below_excluding(10, &mut [i, r1]);
assert!(r2 != i && r2 != r1);
let r3 = rng.below_excluding(10, &mut [i, r1, r2]);
assert!(r3 != i && r3 != r1 && r3 != r2 && r3 < 10);
}
}
#[test]
fn shuffle_is_a_permutation() {
let mut rng = SplitMix64::new(3);
let mut v: Vec<usize> = (0..20).collect();
rng.shuffle(&mut v);
let mut sorted = v.clone();
sorted.sort_unstable();
assert_eq!(sorted, (0..20).collect::<Vec<_>>());
assert_ne!(v, sorted, "20 elements should not stay in order");
}
#[test]
fn householder_solves_square_system() {
let a = vec![vec![2.0, 1.0], vec![1.0, 3.0]];
let x = lstsq_householder(a, vec![3.0, 5.0], 2).unwrap();
assert!((x[0] - 0.8).abs() < 1e-12 && (x[1] - 1.4).abs() < 1e-12);
}
#[test]
fn householder_detects_rank_deficiency() {
let a = vec![vec![1.0, 2.0], vec![2.0, 4.0], vec![3.0, 6.0]];
assert!(lstsq_householder(a, vec![1.0, 2.0, 3.0], 2).is_none());
}
#[test]
fn exact_solver_basic_and_singular() {
let q = |n: i64| Ratio::from_integer(BigInt::from(n));
let a = vec![vec![q(2), q(1)], vec![q(1), q(3)]];
let x = solve_exact(a, vec![q(3), q(5)]).unwrap();
assert_eq!(x[0], Ratio::new(BigInt::from(4), BigInt::from(5)));
assert_eq!(x[1], Ratio::new(BigInt::from(7), BigInt::from(5)));
let s = vec![vec![q(1), q(2)], vec![q(2), q(4)]];
assert!(solve_exact(s, vec![q(1), q(2)]).is_none());
}
#[test]
fn argmin_picks_first_smallest() {
assert_eq!(argmin(&[3.0, 1.0, 1.0, 2.0]), 1);
assert_eq!(argmin(&[]), 0);
assert_eq!(argmin(&[f64::INFINITY, f64::INFINITY]), 0);
}
}