pub(crate) const HISTORY: usize = 8;
const ARMIJO_C1: f64 = 1e-4;
const WOLFE_C2: f64 = 0.9;
const BACKTRACK: f64 = 0.5;
const EXPAND: f64 = 2.1;
const MAX_LS_TRIALS: usize = 40;
#[derive(Clone, Copy, Debug)]
pub(crate) enum Converge {
GradRms(f64),
Fmax(f64),
}
pub type MinResult = (f64, f64, usize, bool);
#[inline]
pub(crate) fn fmax_from_grad(grad: &[f64]) -> f64 {
grad.chunks_exact(3)
.map(|g| (g[0] * g[0] + g[1] * g[1] + g[2] * g[2]).sqrt())
.fold(0.0_f64, f64::max)
}
pub(crate) fn minimize_core<F>(
coords: &mut [f64],
max_iters: usize,
converge: Converge,
max_step: f64,
memory: usize,
mut eval: F,
) -> (f64, Vec<f64>, usize, bool)
where
F: FnMut(&[f64]) -> (f64, Vec<f64>),
{
let n = coords.len();
if n == 0 {
return (0.0, Vec::new(), 0, true);
}
let memory = memory.max(1);
let trust = max_step.is_finite();
let inv_sqrt_n = 1.0 / (n as f64).sqrt();
let (mut energy, forces) = eval(coords);
let mut grad: Vec<f64> = forces.iter().map(|f| -f).collect();
let mut s_hist: Vec<Vec<f64>> = Vec::with_capacity(memory);
let mut y_hist: Vec<Vec<f64>> = Vec::with_capacity(memory);
let mut rho_hist: Vec<f64> = Vec::with_capacity(memory);
let mut converged = false;
let mut iters = 0;
for it in 0..max_iters {
iters = it + 1;
let gnorm2 = dot(&grad, &grad);
let metric_converged = match converge {
Converge::GradRms(tol) => gnorm2.sqrt() * inv_sqrt_n < tol,
Converge::Fmax(tol) => fmax_from_grad(&grad) < tol,
};
if metric_converged {
converged = true;
break;
}
let mut q = grad.clone();
let m = s_hist.len();
let mut alpha = vec![0.0; m];
for i in (0..m).rev() {
let a = rho_hist[i] * dot(&s_hist[i], &q);
alpha[i] = a;
axpy(&mut q, -a, &y_hist[i]);
}
let gamma = if m > 0 {
let last = m - 1;
let sy = dot(&s_hist[last], &y_hist[last]);
let yy = dot(&y_hist[last], &y_hist[last]);
if yy > 1e-30 { sy / yy } else { 1.0 }
} else {
let gn = gnorm2.sqrt();
if gn > 1e-12 { (1.0 / gn).min(0.1) } else { 1.0 }
};
scale(&mut q, gamma);
for i in 0..m {
let beta = rho_hist[i] * dot(&y_hist[i], &q);
axpy(&mut q, alpha[i] - beta, &s_hist[i]);
}
let mut dir = q;
scale(&mut dir, -1.0);
if dot(&grad, &dir) >= 0.0 {
dir = grad.clone();
scale(&mut dir, -1.0);
}
if trust {
let dmax = dir.iter().fold(0.0_f64, |m, &v| m.max(v.abs()));
if dmax > max_step {
scale(&mut dir, max_step / dmax);
}
}
let g_dot_dir = dot(&grad, &dir);
let x0 = coords.to_vec();
let e0 = energy;
let mut step = 1.0;
let mut lo = 0.0;
let mut hi = f64::INFINITY;
let mut accepted = false;
let mut new_grad = grad.clone();
for _ in 0..MAX_LS_TRIALS {
let mut trial = x0.clone();
axpy(&mut trial, step, &dir);
let (e_trial, f_trial) = eval(&trial);
if !e_trial.is_finite() {
hi = step;
step = 0.5 * (lo + hi);
continue;
}
if e_trial > e0 + ARMIJO_C1 * step * g_dot_dir {
hi = step;
step = 0.5 * (lo + hi);
continue;
}
let g_new: Vec<f64> = f_trial.iter().map(|f| -f).collect();
let g_new_dot_dir = dot(&g_new, &dir);
if g_new_dot_dir < WOLFE_C2 * g_dot_dir {
if trust {
break;
}
lo = step;
if hi.is_finite() {
step = 0.5 * (lo + hi);
} else {
step *= EXPAND;
}
continue;
}
coords.copy_from_slice(&trial);
energy = e_trial;
new_grad = g_new;
accepted = true;
break;
}
if !accepted {
let mut s = step.max(BACKTRACK);
let mut made = false;
for _ in 0..MAX_LS_TRIALS {
let mut trial = x0.clone();
axpy(&mut trial, s, &dir);
let (e_trial, f_trial) = eval(&trial);
if e_trial.is_finite() && e_trial < e0 + ARMIJO_C1 * s * g_dot_dir {
coords.copy_from_slice(&trial);
energy = e_trial;
new_grad = f_trial.iter().map(|f| -f).collect();
made = true;
break;
}
s *= BACKTRACK;
}
if !made {
converged = true;
break;
}
}
let mut s_k = coords.to_vec();
axpy(&mut s_k, -1.0, &x0);
let mut y_k = new_grad.clone();
axpy(&mut y_k, -1.0, &grad);
let sy = dot(&s_k, &y_k);
if sy > 1e-12 {
if s_hist.len() == memory {
s_hist.remove(0);
y_hist.remove(0);
rho_hist.remove(0);
}
rho_hist.push(1.0 / sy);
s_hist.push(s_k);
y_hist.push(y_k);
}
grad = new_grad;
}
(energy, grad, iters, converged)
}
pub fn minimize_lbfgs_rms<F>(
coords: &mut [f64],
max_iters: usize,
grad_rms_tol: f64,
eval: F,
) -> MinResult
where
F: FnMut(&[f64]) -> (f64, Vec<f64>),
{
let n = coords.len();
let (energy, grad, iters, converged) = minimize_core(
coords,
max_iters,
Converge::GradRms(grad_rms_tol),
f64::INFINITY,
HISTORY,
eval,
);
let grad_rms = if n == 0 {
0.0
} else {
dot(&grad, &grad).sqrt() / (n as f64).sqrt()
};
(energy, grad_rms, iters, converged)
}
#[inline]
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
#[inline]
fn axpy(a: &mut [f64], alpha: f64, b: &[f64]) {
for (x, y) in a.iter_mut().zip(b) {
*x += alpha * y;
}
}
#[inline]
fn scale(a: &mut [f64], factor: f64) {
for x in a.iter_mut() {
*x *= factor;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lbfgs_solves_stiff_quadratic() {
let k = [1.0, 10.0, 100.0, 1.0, 10.0, 100.0];
let eval = |x: &[f64]| -> (f64, Vec<f64>) {
let mut e = 0.0;
let mut forces = vec![0.0; x.len()];
for i in 0..x.len() {
e += 0.5 * k[i] * x[i] * x[i];
forces[i] = -k[i] * x[i]; }
(e, forces)
};
let mut x = vec![1.0, 1.0, 1.0, -1.0, -1.0, -1.0];
let (e, grms, steps, conv) = minimize_lbfgs_rms(&mut x, 200, 1e-6, eval);
assert!(conv, "should converge");
assert!(e < 1e-8, "energy should reach ~0, got {e}");
assert!(grms < 1e-6, "grad RMS should be tiny, got {grms}");
assert!(steps < 100, "should converge quickly, took {steps}");
for xi in &x {
assert!(xi.abs() < 1e-4, "coord should reach 0, got {xi}");
}
}
}