use crate::errors::EdgeErrors;
const WOLFE_C1: f64 = 1e-4;
const WOLFE_C2: f64 = 0.9;
const CURVATURE_EPS: f64 = 2.2e-16;
const MAX_STEP: f64 = 1e3;
#[derive(Clone, Copy, Debug)]
pub struct LbfgsbParams {
pub memory: usize,
pub ftol: f64,
pub pgtol: f64,
pub max_iter: usize,
pub max_line_search: usize,
}
impl Default for LbfgsbParams {
fn default() -> Self {
Self {
memory: 10,
ftol: 1e-6,
pgtol: 1e-5,
max_iter: 200,
max_line_search: 20,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LbfgsbStatus {
GradientTolerance,
ObjectiveTolerance,
MaxIterations,
LineSearchFailed,
}
#[derive(Clone, Debug)]
pub struct LbfgsbResult {
pub x: Vec<f64>,
pub f: f64,
pub grad: Vec<f64>,
pub iterations: usize,
pub evaluations: usize,
pub status: LbfgsbStatus,
}
struct Memory {
s: Vec<Vec<f64>>,
y: Vec<Vec<f64>>,
theta: f64,
limit: usize,
}
impl Memory {
fn new(limit: usize) -> Self {
Self {
s: Vec::with_capacity(limit),
y: Vec::with_capacity(limit),
theta: 1.0,
limit,
}
}
fn k(&self) -> usize {
self.s.len()
}
fn reset(&mut self) {
self.s.clear();
self.y.clear();
self.theta = 1.0;
}
fn push(&mut self, s: Vec<f64>, y: Vec<f64>) -> bool {
let sy: f64 = s.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
let yy: f64 = y.iter().map(|b| b * b).sum();
if sy <= CURVATURE_EPS * yy || yy <= 0.0 {
return false;
}
if self.s.len() == self.limit {
self.s.remove(0);
self.y.remove(0);
}
self.theta = yy / sy;
self.s.push(s);
self.y.push(y);
true
}
#[inline]
fn w(&self, i: usize, j: usize) -> f64 {
let k = self.k();
if j < k {
self.y[j][i]
} else {
self.theta * self.s[j - k][i]
}
}
fn wt_dot(&self, v: &[f64]) -> Vec<f64> {
let k = self.k();
let mut out = vec![0.0; 2 * k];
for (j, out_j) in out.iter_mut().enumerate() {
let mut acc = 0.0;
for (i, &vi) in v.iter().enumerate() {
acc += self.w(i, j) * vi;
}
*out_j = acc;
}
out
}
fn middle(&self) -> Vec<f64> {
let k = self.k();
let dim = 2 * k;
let mut m = vec![0.0; dim * dim];
for i in 0..k {
let sy: f64 = self.s[i]
.iter()
.zip(self.y[i].iter())
.map(|(a, b)| a * b)
.sum();
m[i * dim + i] = -sy;
}
for i in 0..k {
for j in 0..k {
if i > j {
let sy: f64 = self.s[i]
.iter()
.zip(self.y[j].iter())
.map(|(a, b)| a * b)
.sum();
m[(k + i) * dim + j] = sy;
m[j * dim + (k + i)] = sy;
}
}
}
for i in 0..k {
for j in 0..k {
let ss: f64 = self.s[i]
.iter()
.zip(self.s[j].iter())
.map(|(a, b)| a * b)
.sum();
m[(k + i) * dim + (k + j)] = self.theta * ss;
}
}
m
}
fn apply_m(&self, v: &[f64]) -> Option<Vec<f64>> {
let dim = 2 * self.k();
if dim == 0 {
return Some(Vec::new());
}
solve_dense(&self.middle(), v, dim)
}
}
fn solve_dense(a: &[f64], b: &[f64], dim: usize) -> Option<Vec<f64>> {
let mut m = a.to_vec();
let mut x = b.to_vec();
for col in 0..dim {
let mut pivot = col;
let mut best = m[col * dim + col].abs();
for row in (col + 1)..dim {
let v = m[row * dim + col].abs();
if v > best {
best = v;
pivot = row;
}
}
if best < 1e-300 {
return None;
}
if pivot != col {
for j in 0..dim {
m.swap(col * dim + j, pivot * dim + j);
}
x.swap(col, pivot);
}
let diag = m[col * dim + col];
for row in (col + 1)..dim {
let factor = m[row * dim + col] / diag;
if factor == 0.0 {
continue;
}
for j in col..dim {
m[row * dim + j] -= factor * m[col * dim + j];
}
x[row] -= factor * x[col];
}
}
for col in (0..dim).rev() {
let mut acc = x[col];
for j in (col + 1)..dim {
acc -= m[col * dim + j] * x[j];
}
x[col] = acc / m[col * dim + col];
}
Some(x)
}
struct CauchyPoint {
x: Vec<f64>,
free: Vec<bool>,
c: Vec<f64>,
}
fn cauchy_point(
x: &[f64],
g: &[f64],
lower: &[f64],
upper: &[f64],
mem: &Memory,
) -> Option<CauchyPoint> {
let n = x.len();
let k = mem.k();
let mut t = vec![f64::INFINITY; n];
let mut d = vec![0.0; n];
for i in 0..n {
if g[i] < 0.0 && upper[i].is_finite() {
t[i] = (x[i] - upper[i]) / g[i];
} else if g[i] > 0.0 && lower[i].is_finite() {
t[i] = (x[i] - lower[i]) / g[i];
}
d[i] = if t[i] == 0.0 { 0.0 } else { -g[i] };
}
let mut xcp = x.to_vec();
let mut free: Vec<bool> = (0..n).map(|i| t[i] > 0.0).collect();
let mut order: Vec<usize> = (0..n).filter(|&i| t[i] > 0.0 && t[i].is_finite()).collect();
order.sort_by(|&a, &b| t[a].total_cmp(&t[b]));
let mut c = vec![0.0; 2 * k];
let mut p = mem.wt_dot(&d);
let mut f_prime: f64 = -d.iter().map(|v| v * v).sum::<f64>();
let mp = mem.apply_m(&p)?;
let mut f_double = -mem.theta * f_prime - dot(&p, &mp);
let f_double_initial = f_double.max(f64::EPSILON);
f_double = f_double.max(f64::EPSILON);
let mut dt_min = -f_prime / f_double;
let mut t_old = 0.0;
let mut cursor = 0;
while cursor < order.len() {
let b = order[cursor];
let t_b = t[b];
let dt = t_b - t_old;
if dt_min < dt {
break;
}
xcp[b] = if d[b] > 0.0 { upper[b] } else { lower[b] };
let zb = xcp[b] - x[b];
free[b] = false;
for (ci, pi) in c.iter_mut().zip(p.iter()) {
*ci += dt * pi;
}
let gb = g[b];
let wb: Vec<f64> = (0..2 * k).map(|j| mem.w(b, j)).collect();
let mc = mem.apply_m(&c)?;
let mp = mem.apply_m(&p)?;
let mwb = mem.apply_m(&wb)?;
f_prime += dt * f_double + gb * gb + mem.theta * gb * zb - gb * dot(&wb, &mc);
f_double += -mem.theta * gb * gb - 2.0 * gb * dot(&wb, &mp) - gb * gb * dot(&wb, &mwb);
f_double = f_double.max(f64::EPSILON * f_double_initial);
for (pi, wbi) in p.iter_mut().zip(wb.iter()) {
*pi += gb * wbi;
}
d[b] = 0.0;
dt_min = -f_prime / f_double;
t_old = t_b;
cursor += 1;
}
let dt_min = dt_min.max(0.0);
let t_final = t_old + dt_min;
for i in 0..n {
if free[i] && t[i] >= t_final {
xcp[i] = x[i] + t_final * d[i];
}
}
for (ci, pi) in c.iter_mut().zip(p.iter()) {
*ci += dt_min * pi;
}
Some(CauchyPoint { x: xcp, free, c })
}
#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn subspace_minimise(
x: &[f64],
g: &[f64],
lower: &[f64],
upper: &[f64],
cp: &CauchyPoint,
mem: &Memory,
) -> Option<Vec<f64>> {
let n = x.len();
let k = mem.k();
if k == 0 {
return Some(cp.x.clone());
}
let free: Vec<usize> = (0..n).filter(|&i| cp.free[i]).collect();
if free.is_empty() {
return Some(cp.x.clone());
}
let mc = mem.apply_m(&cp.c)?;
let mut r = vec![0.0; free.len()];
for (slot, &i) in free.iter().enumerate() {
let wi: Vec<f64> = (0..2 * k).map(|j| mem.w(i, j)).collect();
r[slot] = g[i] + mem.theta * (cp.x[i] - x[i]) - dot(&wi, &mc);
}
let mut v = vec![0.0; 2 * k];
for (j, v_j) in v.iter_mut().enumerate() {
let mut acc = 0.0;
for (slot, &i) in free.iter().enumerate() {
acc += mem.w(i, j) * r[slot];
}
*v_j = acc;
}
let mut v = mem.apply_m(&v)?;
let dim = 2 * k;
let mut wtw = vec![0.0; dim * dim];
for a in 0..dim {
for b in 0..dim {
let mut acc = 0.0;
for &i in &free {
acc += mem.w(i, a) * mem.w(i, b);
}
wtw[a * dim + b] = acc;
}
}
let mut n_mat = vec![0.0; dim * dim];
for b in 0..dim {
let col: Vec<f64> = (0..dim).map(|a| wtw[a * dim + b]).collect();
let m_col = mem.apply_m(&col)?;
for a in 0..dim {
n_mat[a * dim + b] = -m_col[a] / mem.theta;
}
}
for a in 0..dim {
n_mat[a * dim + a] += 1.0;
}
v = solve_dense(&n_mat, &v, dim)?;
let mut d = vec![0.0; free.len()];
for (slot, &i) in free.iter().enumerate() {
let acc: f64 = v
.iter()
.enumerate()
.map(|(j, &v_j)| mem.w(i, j) * v_j)
.sum();
d[slot] = -(r[slot] / mem.theta + acc / (mem.theta * mem.theta));
}
let mut alpha = 1.0_f64;
for (slot, &i) in free.iter().enumerate() {
if d[slot] > 0.0 && upper[i].is_finite() {
alpha = alpha.min((upper[i] - cp.x[i]) / d[slot]);
} else if d[slot] < 0.0 && lower[i].is_finite() {
alpha = alpha.min((lower[i] - cp.x[i]) / d[slot]);
}
}
let alpha = alpha.clamp(0.0, 1.0);
let mut out = cp.x.clone();
for (slot, &i) in free.iter().enumerate() {
out[i] = (cp.x[i] + alpha * d[slot]).clamp(lower[i], upper[i]);
}
Some(out)
}
fn max_feasible_step(x: &[f64], direction: &[f64], lower: &[f64], upper: &[f64]) -> f64 {
let mut step = f64::INFINITY;
for i in 0..x.len() {
if direction[i] > 0.0 && upper[i].is_finite() {
step = step.min((upper[i] - x[i]) / direction[i]);
} else if direction[i] < 0.0 && lower[i].is_finite() {
step = step.min((lower[i] - x[i]) / direction[i]);
}
}
step.max(0.0)
}
#[allow(clippy::too_many_arguments)]
fn line_search<F>(
f_and_grad: &mut F,
x: &[f64],
direction: &[f64],
lower: &[f64],
upper: &[f64],
f0: f64,
g0: &[f64],
max_evals: usize,
) -> Option<(Vec<f64>, f64, Vec<f64>, usize)>
where
F: FnMut(&[f64], &mut [f64]) -> f64,
{
let n = x.len();
let slope0 = dot(g0, direction);
if slope0 >= 0.0 {
return None;
}
let step_max = max_feasible_step(x, direction, lower, upper).min(MAX_STEP);
if step_max <= 0.0 {
return None;
}
let mut evals = 0;
let mut grad = vec![0.0; n];
let trial_point = |alpha: f64| -> Vec<f64> {
(0..n)
.map(|i| (x[i] + alpha * direction[i]).clamp(lower[i], upper[i]))
.collect()
};
let mut alpha_prev = 0.0;
let mut f_prev = f0;
let mut alpha = 1.0_f64.min(step_max);
let mut best: Option<(Vec<f64>, f64, Vec<f64>)> = None;
for iteration in 0..max_evals {
let point = trial_point(alpha);
let f_new = f_and_grad(&point, &mut grad);
evals += 1;
if !f_new.is_finite() {
alpha *= 0.5;
if alpha < 1e-20 {
break;
}
continue;
}
let sufficient = f_new <= f0 + WOLFE_C1 * alpha * slope0;
if !sufficient || (iteration > 0 && f_new >= f_prev) {
return zoom(
f_and_grad,
x,
direction,
lower,
upper,
f0,
slope0,
alpha_prev,
f_prev,
alpha,
max_evals - evals,
evals,
);
}
let slope = dot(&grad, direction);
if slope.abs() <= -WOLFE_C2 * slope0 {
return Some((point, f_new, grad, evals));
}
if slope >= 0.0 {
return zoom(
f_and_grad,
x,
direction,
lower,
upper,
f0,
slope0,
alpha,
f_new,
alpha_prev,
max_evals - evals,
evals,
);
}
if alpha >= step_max {
return Some((point, f_new, grad, evals));
}
best = Some((point, f_new, grad.clone()));
alpha_prev = alpha;
f_prev = f_new;
alpha = (alpha * 2.0).min(step_max);
}
best.map(|(p, f, g)| (p, f, g, evals))
}
#[allow(clippy::too_many_arguments)]
fn zoom<F>(
f_and_grad: &mut F,
x: &[f64],
direction: &[f64],
lower: &[f64],
upper: &[f64],
f0: f64,
slope0: f64,
mut alpha_lo: f64,
mut f_lo: f64,
mut alpha_hi: f64,
budget: usize,
evals_so_far: usize,
) -> Option<(Vec<f64>, f64, Vec<f64>, usize)>
where
F: FnMut(&[f64], &mut [f64]) -> f64,
{
let n = x.len();
let mut grad = vec![0.0; n];
let mut evals = evals_so_far;
for _ in 0..budget {
let alpha = 0.5 * (alpha_lo + alpha_hi);
if (alpha_hi - alpha_lo).abs() < 1e-16 {
break;
}
let point: Vec<f64> = (0..n)
.map(|i| (x[i] + alpha * direction[i]).clamp(lower[i], upper[i]))
.collect();
let f_new = f_and_grad(&point, &mut grad);
evals += 1;
if !f_new.is_finite() || f_new > f0 + WOLFE_C1 * alpha * slope0 || f_new >= f_lo {
alpha_hi = alpha;
continue;
}
let slope = dot(&grad, direction);
if slope.abs() <= -WOLFE_C2 * slope0 {
return Some((point, f_new, grad, evals));
}
if slope * (alpha_hi - alpha_lo) >= 0.0 {
alpha_hi = alpha_lo;
}
alpha_lo = alpha;
f_lo = f_new;
}
None
}
pub fn minimise<F>(
mut f_and_grad: F,
x0: &[f64],
lower: &[f64],
upper: &[f64],
params: Option<LbfgsbParams>,
) -> Result<LbfgsbResult, EdgeErrors>
where
F: FnMut(&[f64], &mut [f64]) -> f64,
{
let params = params.unwrap_or_default();
let n = x0.len();
if n == 0 {
return Err(EdgeErrors::MustBePositive("x0.len()".to_string()));
}
if lower.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "lower",
expected: n,
got: lower.len(),
});
}
if upper.len() != n {
return Err(EdgeErrors::LengthMismatch {
name: "upper",
expected: n,
got: upper.len(),
});
}
for i in 0..n {
if lower[i] > upper[i] {
return Err(EdgeErrors::InvalidBounds {
index: i,
lower: lower[i],
upper: upper[i],
});
}
}
if params.memory == 0 {
return Err(EdgeErrors::MustBePositive("memory".to_string()));
}
let mut x: Vec<f64> = (0..n).map(|i| x0[i].clamp(lower[i], upper[i])).collect();
let mut grad = vec![0.0; n];
let mut f = f_and_grad(&x, &mut grad);
let mut evaluations = 1;
let mut mem = Memory::new(params.memory);
let mut status = LbfgsbStatus::MaxIterations;
let mut iterations = 0;
for iter in 0..params.max_iter {
iterations = iter + 1;
if projected_gradient_norm(&x, &grad, lower, upper) <= params.pgtol {
status = LbfgsbStatus::GradientTolerance;
break;
}
let candidate = cauchy_point(&x, &grad, lower, upper, &mem)
.and_then(|cp| subspace_minimise(&x, &grad, lower, upper, &cp, &mem));
let direction: Vec<f64> = match candidate {
Some(target) => (0..n).map(|i| target[i] - x[i]).collect(),
None => (0..n)
.map(|i| (x[i] - grad[i]).clamp(lower[i], upper[i]) - x[i])
.collect(),
};
if direction.iter().all(|v| *v == 0.0) {
status = LbfgsbStatus::GradientTolerance;
break;
}
let searched = line_search(
&mut f_and_grad,
&x,
&direction,
lower,
upper,
f,
&grad,
params.max_line_search,
);
let (x_new, f_new, grad_new, used) = match searched {
Some(v) => v,
None => {
if mem.k() > 0 {
mem.reset();
continue;
}
status = LbfgsbStatus::LineSearchFailed;
break;
}
};
evaluations += used;
let s: Vec<f64> = (0..n).map(|i| x_new[i] - x[i]).collect();
let y: Vec<f64> = (0..n).map(|i| grad_new[i] - grad[i]).collect();
mem.push(s, y);
let reduction = f - f_new;
let scale = f.abs().max(f_new.abs()).max(1.0);
x = x_new;
f = f_new;
grad = grad_new;
if reduction <= params.ftol * scale {
status = LbfgsbStatus::ObjectiveTolerance;
break;
}
}
Ok(LbfgsbResult {
x,
f,
grad,
iterations,
evaluations,
status,
})
}
fn projected_gradient_norm(x: &[f64], g: &[f64], lower: &[f64], upper: &[f64]) -> f64 {
let mut norm: f64 = 0.0;
for i in 0..x.len() {
let projected = (x[i] - g[i]).clamp(lower[i], upper[i]) - x[i];
norm = norm.max(projected.abs());
}
norm
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn rosenbrock(x: &[f64], g: &mut [f64]) -> f64 {
let (a, b) = (x[0], x[1]);
g[0] = -400.0 * a * (b - a * a) - 2.0 * (1.0 - a);
g[1] = 200.0 * (b - a * a);
100.0 * (b - a * a).powi(2) + (1.0 - a).powi(2)
}
fn quadratic(x: &[f64], g: &mut [f64]) -> f64 {
let target = [1.0, 2.0, 3.0];
let mut f = 0.0;
for i in 0..3 {
let d = x[i] - target[i];
f += d * d;
g[i] = 2.0 * d;
}
f
}
fn tight(n: usize) -> (Vec<f64>, Vec<f64>) {
(vec![f64::NEG_INFINITY; n], vec![f64::INFINITY; n])
}
#[test]
fn test_unbounded_rosenbrock_finds_the_optimum() {
let (lo, hi) = tight(2);
let params = LbfgsbParams {
ftol: 1e-14,
pgtol: 1e-10,
max_iter: 500,
..Default::default()
};
let res = minimise(rosenbrock, &[-1.2, 1.0], &lo, &hi, Some(params)).unwrap();
assert_relative_eq!(res.x[0], 1.0, max_relative = 1e-5);
assert_relative_eq!(res.x[1], 1.0, max_relative = 1e-5);
assert!(res.f < 1e-10, "objective {} not near zero", res.f);
}
#[test]
fn test_quadratic_converges_in_few_iterations() {
let (lo, hi) = tight(3);
let res = minimise(
quadratic,
&[0.0, 0.0, 0.0],
&lo,
&hi,
Some(LbfgsbParams {
ftol: 1e-14,
pgtol: 1e-12,
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(res.x[0], 1.0, max_relative = 1e-8);
assert_relative_eq!(res.x[1], 2.0, max_relative = 1e-8);
assert_relative_eq!(res.x[2], 3.0, max_relative = 1e-8);
}
#[test]
fn test_active_bound_is_respected() {
let lower = vec![2.0, f64::NEG_INFINITY, f64::NEG_INFINITY];
let upper = vec![f64::INFINITY; 3];
let res = minimise(
quadratic,
&[5.0, 0.0, 0.0],
&lower,
&upper,
Some(LbfgsbParams {
ftol: 1e-14,
pgtol: 1e-12,
..Default::default()
}),
)
.unwrap();
assert_relative_eq!(res.x[0], 2.0, max_relative = 1e-10);
assert_relative_eq!(res.x[1], 2.0, max_relative = 1e-8);
assert_relative_eq!(res.x[2], 3.0, max_relative = 1e-8);
}
#[test]
fn test_degenerate_box_returns_the_only_feasible_point() {
let lower = vec![1.5, 1.5, 1.5];
let upper = vec![1.5, 1.5, 1.5];
let res = minimise(quadratic, &[0.0, 0.0, 0.0], &lower, &upper, None).unwrap();
for v in &res.x {
assert_relative_eq!(v, &1.5, max_relative = 1e-12);
}
}
#[test]
fn test_bounded_rosenbrock_stops_on_the_boundary() {
let lower = vec![f64::NEG_INFINITY, f64::NEG_INFINITY];
let upper = vec![0.5, f64::INFINITY];
let params = LbfgsbParams {
ftol: 1e-14,
pgtol: 1e-10,
max_iter: 500,
..Default::default()
};
let res = minimise(rosenbrock, &[-1.2, 1.0], &lower, &upper, Some(params)).unwrap();
assert_relative_eq!(res.x[0], 0.5, max_relative = 1e-6);
assert_relative_eq!(res.x[1], 0.25, max_relative = 1e-4);
}
#[test]
fn test_infeasible_start_is_clamped() {
let lower = vec![0.0, 0.0, 0.0];
let upper = vec![10.0, 10.0, 10.0];
let res = minimise(quadratic, &[-50.0, 100.0, 3.0], &lower, &upper, None).unwrap();
for (i, v) in res.x.iter().enumerate() {
assert!(*v >= lower[i] - 1e-12 && *v <= upper[i] + 1e-12);
}
}
#[test]
fn test_rejects_inverted_bounds() {
let err = minimise(
quadratic,
&[0.0; 3],
&[1.0, 0.0, 0.0],
&[0.0, 1.0, 1.0],
None,
)
.unwrap_err();
assert!(matches!(err, EdgeErrors::InvalidBounds { index: 0, .. }));
}
#[test]
fn test_rejects_mismatched_bound_lengths() {
let err = minimise(quadratic, &[0.0; 3], &[0.0; 2], &[1.0; 3], None).unwrap_err();
assert!(matches!(
err,
EdgeErrors::LengthMismatch { name: "lower", .. }
));
}
#[test]
fn test_rejects_empty_start() {
let err = minimise(quadratic, &[], &[], &[], None).unwrap_err();
assert!(matches!(err, EdgeErrors::MustBePositive(_)));
}
fn nebula_shaped(p: &[f64], g: &mut [f64]) -> f64 {
const N: usize = 60;
let counts: Vec<f64> = (0..N)
.map(|i| [0.0, 3.0, 1.0, 2.0, 0.0, 5.0, 2.0, 1.0, 4.0, 2.0][i % 10])
.collect();
let offset = 800.0_f64.ln();
let (b0, b1, sigma, phi) = (p[0], p[1], p[2], p[3]);
let r = 1.0 / phi;
let mut f = 0.5 * sigma * sigma;
let mut gb0 = 0.0;
let mut gb1 = 0.0;
let mut dr = 0.0;
for (i, &y) in counts.iter().enumerate() {
let xi = -1.0 + 2.0 * (i as f64) / ((N - 1) as f64);
let mu = (offset + b0 + b1 * xi).exp();
f += (y + r) * (mu + r).ln() - y * mu.ln() - r * r.ln();
let d_mu = (y + r) / (mu + r) - y / mu;
gb0 += d_mu * mu;
gb1 += xi * d_mu * mu;
dr += (mu + r).ln() + (y + r) / (mu + r) - r.ln() - 1.0;
}
g[0] = gb0;
g[1] = gb1;
g[2] = sigma;
g[3] = dr * (-1.0 / (phi * phi));
f
}
#[test]
fn test_nebula_shaped_problem_matches_scipy() {
let lower = vec![-100.0, -100.0, 1e-4, 1e-4];
let upper = vec![100.0, 100.0, 10.0, 1000.0];
let params = LbfgsbParams {
ftol: 1e-12,
pgtol: 1e-10,
max_iter: 500,
..Default::default()
};
let res = minimise(
nebula_shaped,
&[0.0, 0.0, 1.0, 1.0],
&lower,
&upper,
Some(params),
)
.unwrap();
assert_relative_eq!(res.x[0], -5.992406750800, max_relative = 1e-6);
assert_relative_eq!(res.x[1], 0.074133661253, max_relative = 1e-5);
assert_relative_eq!(res.x[2], 1e-4, max_relative = 1e-12);
assert_relative_eq!(res.x[3], 1000.0, max_relative = 1e-12);
assert_relative_eq!(res.f, 0.516012646087, max_relative = 1e-8);
}
fn decaying(x: &[f64], g: &mut [f64]) -> f64 {
const RATE: [f64; 4] = [1.0, 30.0, 300.0, 3000.0];
let mut f = 0.0;
for i in 0..4 {
let e = (-RATE[i] * x[i]).exp();
f += e;
g[i] = -RATE[i] * e;
}
f
}
#[test]
fn test_search_stops_at_the_boundary_rather_than_past_it() {
let lower = vec![0.0; 4];
let upper = vec![1.0; 4];
let params = LbfgsbParams {
ftol: 0.0,
pgtol: 1e-10,
max_iter: 200,
..Default::default()
};
let res = minimise(decaying, &[0.0; 4], &lower, &upper, Some(params)).unwrap();
for i in 0..4 {
assert_relative_eq!(res.x[i], 1.0, max_relative = 1e-12);
}
assert_eq!(res.status, LbfgsbStatus::GradientTolerance);
assert!(
res.evaluations < 100,
"took {} evaluations on a four-variable box problem",
res.evaluations
);
}
#[test]
fn test_solve_dense_matches_a_known_solution() {
let a = vec![2.0, 1.0, 1.0, 3.0];
let b = vec![3.0, 5.0];
let x = solve_dense(&a, &b, 2).unwrap();
assert_relative_eq!(x[0], 0.8, max_relative = 1e-12);
assert_relative_eq!(x[1], 1.4, max_relative = 1e-12);
}
#[test]
fn test_solve_dense_reports_singularity() {
let a = vec![1.0, 2.0, 2.0, 4.0];
assert!(solve_dense(&a, &[1.0, 2.0], 2).is_none());
}
}