pub trait Objective {
fn value_and_grad(&self, x: &[f64], grad: &mut [f64]) -> f64;
}
#[derive(Debug, Clone, Copy)]
pub struct Options {
pub max_iter: usize,
pub grad_tol: f64,
pub memory: usize,
}
impl Default for Options {
fn default() -> Self {
Self {
max_iter: 400,
grad_tol: 1e-6,
memory: 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Report {
pub iterations: usize,
pub value: f64,
pub grad_norm: f64,
pub converged: bool,
pub discarded: usize,
pub backtracks: usize,
}
#[must_use]
pub fn max_grad_error(obj: &dyn Objective, x: &[f64], h: f64) -> f64 {
let n = x.len();
let mut g = vec![0.0; n];
obj.value_and_grad(x, &mut g);
let mut probe = x.to_vec();
let mut worst: f64 = 0.0;
let mut scratch = vec![0.0; n];
for i in 0..n {
let orig = probe[i];
probe[i] = orig + h;
let f1 = obj.value_and_grad(&probe, &mut scratch);
probe[i] = orig - h;
let f0 = obj.value_and_grad(&probe, &mut scratch);
probe[i] = orig;
let num = (f1 - f0) / (2.0 * h);
let denom = num.abs().max(g[i].abs()).max(1.0);
let rel = (num - g[i]).abs() / denom;
worst = crate::linalg::max_nan_wins(worst, rel);
}
worst
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
let mut s = 0.0;
for k in 0..a.len() {
s += a[k] * b[k];
}
s
}
fn inf_norm(v: &[f64]) -> f64 {
v.iter()
.fold(0.0_f64, |m, x| crate::linalg::max_nan_wins(m, x.abs()))
}
struct LineSearch {
ok: bool,
f: f64,
evals: usize,
}
fn wolfe_search(
obj: &dyn Objective,
x: &[f64],
dir: &[f64],
f0: f64,
slope0: f64,
x_out: &mut [f64],
g_out: &mut [f64],
) -> LineSearch {
const C1: f64 = 1e-4;
const C2: f64 = 0.9;
const MAX_EVAL: usize = 60;
let n = x.len();
let mut evals = 0;
let eval = |a: f64, xo: &mut [f64], go: &mut [f64]| -> (f64, f64) {
for j in 0..n {
xo[j] = x[j] + a * dir[j];
}
let fa = obj.value_and_grad(xo, go);
(fa, dot(go, dir))
};
let (mut a_prev, mut f_prev) = (0.0_f64, f0);
let mut a = 1.0_f64;
let (mut lo, mut hi) = (0.0_f64, -1.0_f64); for i in 0..MAX_EVAL {
let (fa, sa) = eval(a, x_out, g_out);
evals += 1;
if fa > f0 + C1 * a * slope0 || (i > 0 && fa >= f_prev) {
lo = a_prev;
hi = a;
break;
}
if sa.abs() <= -C2 * slope0 {
return LineSearch {
ok: true,
f: fa,
evals,
}; }
if sa >= 0.0 {
lo = a;
hi = a_prev;
break;
}
a_prev = a;
f_prev = fa;
a *= 2.0;
if a > 1e10 {
return LineSearch {
ok: false,
f: fa,
evals,
};
}
}
if hi < 0.0 {
return LineSearch {
ok: false,
f: f0,
evals,
};
}
let mut f_lo = {
let (fl, _) = eval(lo, x_out, g_out);
evals += 1;
fl
};
while evals < MAX_EVAL {
let am = 0.5 * (lo + hi);
let (fa, sa) = eval(am, x_out, g_out);
evals += 1;
if fa > f0 + C1 * am * slope0 || fa >= f_lo {
hi = am;
} else {
if sa.abs() <= -C2 * slope0 {
return LineSearch {
ok: true,
f: fa,
evals,
};
}
if sa * (hi - lo) >= 0.0 {
hi = lo;
}
lo = am;
f_lo = fa;
}
if (hi - lo).abs() < 1e-16 {
break;
}
}
let (fa, _) = eval(lo, x_out, g_out);
evals += 1;
LineSearch {
ok: lo > 0.0 && fa < f0,
f: fa,
evals,
}
}
pub fn minimize(obj: &dyn Objective, x: &mut [f64], opts: &Options) -> Report {
assert!(!x.is_empty(), "没有变量可优化");
let n = x.len();
let m = opts.memory.max(1);
let mut g = vec![0.0; n];
let mut f = obj.value_and_grad(x, &mut g);
let mut report = Report {
iterations: 0,
value: f,
grad_norm: inf_norm(&g),
converged: inf_norm(&g) <= opts.grad_tol,
discarded: 0,
backtracks: 0,
};
if report.converged {
return report;
}
let mut s_hist: Vec<Vec<f64>> = Vec::with_capacity(m);
let mut y_hist: Vec<Vec<f64>> = Vec::with_capacity(m);
let mut rho: Vec<f64> = Vec::with_capacity(m);
let mut dir = vec![0.0; n];
let mut x_new = vec![0.0; n];
let mut g_new = vec![0.0; n];
let mut alpha = vec![0.0; m];
for iter in 1..=opts.max_iter {
dir.copy_from_slice(&g);
let k = s_hist.len();
for i in (0..k).rev() {
let a = rho[i] * dot(&s_hist[i], &dir);
alpha[i] = a;
for j in 0..n {
dir[j] -= a * y_hist[i][j];
}
}
let gamma = if k > 0 {
let ys = dot(&s_hist[k - 1], &y_hist[k - 1]);
let yy = dot(&y_hist[k - 1], &y_hist[k - 1]);
if yy > 0.0 {
ys / yy
} else {
1.0
}
} else {
1.0
};
for d in dir.iter_mut() {
*d *= gamma;
}
for i in 0..k {
let beta = rho[i] * dot(&y_hist[i], &dir);
let coef = alpha[i] - beta;
for j in 0..n {
dir[j] += coef * s_hist[i][j];
}
}
for d in dir.iter_mut() {
*d = -*d;
}
let mut slope = dot(&g, &dir);
if slope >= 0.0 {
for (d, gi) in dir.iter_mut().zip(&g) {
*d = -gi;
}
slope = -dot(&g, &g);
s_hist.clear();
y_hist.clear();
rho.clear();
}
let ls = wolfe_search(obj, x, &dir, f, slope, &mut x_new, &mut g_new);
report.backtracks += ls.evals;
let ok = ls.ok;
if ok {
f = ls.f;
}
if !ok {
report.iterations = iter;
report.value = f;
report.grad_norm = inf_norm(&g);
report.converged = report.grad_norm <= opts.grad_tol;
return report;
}
let mut s_new = vec![0.0; n];
let mut y_new = vec![0.0; n];
for j in 0..n {
s_new[j] = x_new[j] - x[j];
y_new[j] = g_new[j] - g[j];
}
let ys = dot(&s_new, &y_new);
let ss = dot(&s_new, &s_new);
const CAUTION: f64 = 1e-8;
if ys > CAUTION * ss && ss > 0.0 {
if s_hist.len() == m {
s_hist.remove(0);
y_hist.remove(0);
rho.remove(0);
}
rho.push(1.0 / ys);
s_hist.push(s_new);
y_hist.push(y_new);
} else {
report.discarded += 1;
}
x.copy_from_slice(&x_new);
g.copy_from_slice(&g_new);
report.iterations = iter;
report.value = f;
report.grad_norm = inf_norm(&g);
if report.grad_norm <= opts.grad_tol {
report.converged = true;
return report;
}
}
report
}
#[cfg(test)]
mod tests {
use super::*;
struct Rosenbrock;
impl Objective for Rosenbrock {
fn value_and_grad(&self, x: &[f64], g: &mut [f64]) -> f64 {
let mut f = 0.0;
for v in g.iter_mut() {
*v = 0.0;
}
for i in 0..(x.len() - 1) {
let (a, b) = (x[i], x[i + 1]);
let t1 = b - a * a;
let t2 = 1.0 - a;
f += 100.0 * t1 * t1 + t2 * t2;
g[i] += -400.0 * a * t1 - 2.0 * t2;
g[i + 1] += 200.0 * t1;
}
f
}
}
struct Beale;
impl Objective for Beale {
fn value_and_grad(&self, x: &[f64], g: &mut [f64]) -> f64 {
let (a, b) = (x[0], x[1]);
let f1 = 1.5 - a + a * b;
let f2 = 2.25 - a + a * b * b;
let f3 = 2.625 - a + a * b * b * b;
g[0] = 2.0 * (f1 * (b - 1.0) + f2 * (b * b - 1.0) + f3 * (b * b * b - 1.0));
g[1] = 2.0 * (f1 * a + f2 * 2.0 * a * b + f3 * 3.0 * a * b * b);
f1 * f1 + f2 * f2 + f3 * f3
}
}
struct Powell;
impl Objective for Powell {
fn value_and_grad(&self, x: &[f64], g: &mut [f64]) -> f64 {
let (a, b, c, d) = (x[0], x[1], x[2], x[3]);
let t1 = a + 10.0 * b;
let t2 = c - d;
let t3 = b - 2.0 * c;
let t4 = a - d;
g[0] = 2.0 * t1 + 40.0 * t4 * t4 * t4;
g[1] = 20.0 * t1 + 4.0 * t3 * t3 * t3;
g[2] = 10.0 * t2 - 8.0 * t3 * t3 * t3;
g[3] = -10.0 * t2 - 40.0 * t4 * t4 * t4;
t1 * t1 + 5.0 * t2 * t2 + t3 * t3 * t3 * t3 + 10.0 * t4 * t4 * t4 * t4
}
}
struct FlatBottom {
center: Vec<f64>,
half_width: f64,
}
impl Objective for FlatBottom {
fn value_and_grad(&self, x: &[f64], g: &mut [f64]) -> f64 {
let mut f = 0.0;
for i in 0..x.len() {
let d = x[i] - self.center[i];
let over = d.abs() - self.half_width;
if over > 0.0 {
f += over * over;
g[i] = 2.0 * over * d.signum();
} else {
g[i] = 0.0;
}
}
f
}
}
#[test]
fn 解析梯度与数值梯度一致() {
let flat = FlatBottom {
center: vec![0.0, 0.0, 0.0],
half_width: 1.0,
};
let cases: Vec<(&dyn Objective, Vec<f64>)> = vec![
(&Rosenbrock, vec![-1.2, 1.0, 0.7, -0.3]),
(&Beale, vec![1.0, 0.5]),
(&Powell, vec![3.0, -1.0, 0.0, 1.0]),
(&flat, vec![2.5, -3.0, 0.25]),
];
for (obj, x) in cases {
let e = max_grad_error(obj, &x, 1e-6);
assert!(e < 1e-6, "梯度与能量对不上,最大相对偏差 {e:.3e}");
}
}
#[test]
fn rosenbrock_收敛到已知最优() {
for n in [2usize, 10] {
let mut x = vec![-1.2; n];
for (i, v) in x.iter_mut().enumerate() {
if i % 2 == 1 {
*v = 1.0;
}
}
let r = minimize(
&Rosenbrock,
&mut x,
&Options {
max_iter: 2000,
grad_tol: 1e-8,
memory: 8,
},
);
assert!(r.value < 1e-12, "n={n} 目标值 {:.3e}", r.value);
for (i, v) in x.iter().enumerate() {
assert!((v - 1.0).abs() < 1e-5, "n={n} x[{i}] = {v}");
}
}
}
#[test]
fn beale_与_powell_收敛到已知最优() {
let mut x = vec![1.0, 1.0];
let r = minimize(&Beale, &mut x, &Options::default());
assert!(r.value < 1e-12, "Beale 目标值 {:.3e}", r.value);
assert!(
(x[0] - 3.0).abs() < 1e-4 && (x[1] - 0.5).abs() < 1e-4,
"{x:?}"
);
let mut x = vec![3.0, -1.0, 0.0, 1.0];
let r = minimize(
&Powell,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-12,
memory: 8,
},
);
assert!(r.value < 1e-8, "Powell 目标值 {:.3e}", r.value);
}
struct DistanceBox {
n: usize,
lo: Vec<f64>,
hi: Vec<f64>,
}
impl DistanceBox {
fn idx(&self, i: usize, j: usize) -> usize {
i * self.n + j
}
}
impl Objective for DistanceBox {
fn value_and_grad(&self, x: &[f64], g: &mut [f64]) -> f64 {
for v in g.iter_mut() {
*v = 0.0;
}
let mut f = 0.0;
for i in 0..self.n {
for j in (i + 1)..self.n {
let d3 = [
x[3 * i] - x[3 * j],
x[3 * i + 1] - x[3 * j + 1],
x[3 * i + 2] - x[3 * j + 2],
];
let d = (d3[0] * d3[0] + d3[1] * d3[1] + d3[2] * d3[2]).sqrt();
if d < 1e-12 {
continue;
}
let k = self.idx(i, j);
let over = d - self.hi[k];
let under = self.lo[k] - d;
let (pen, sign) = if over > 0.0 {
(over, 1.0)
} else if under > 0.0 {
(under, -1.0)
} else {
continue;
};
f += pen * pen;
let c = 2.0 * pen * sign / d;
for t in 0..3 {
g[3 * i + t] += c * d3[t];
g[3 * j + t] -= c * d3[t];
}
}
}
f
}
}
fn distance_box(n: usize, half: f64, seed: u64) -> (DistanceBox, Vec<f64>) {
let mut st = seed;
let mut lcg = || {
st = st.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
((st >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let pts: Vec<f64> = (0..3 * n).map(|_| lcg() * 5.0).collect();
let mut lo = vec![0.0; n * n];
let mut hi = vec![0.0; n * n];
for i in 0..n {
for j in (i + 1)..n {
let d = ((pts[3 * i] - pts[3 * j]).powi(2)
+ (pts[3 * i + 1] - pts[3 * j + 1]).powi(2)
+ (pts[3 * i + 2] - pts[3 * j + 2]).powi(2))
.sqrt();
lo[i * n + j] = (d - half).max(0.1);
hi[i * n + j] = d + half;
}
}
(DistanceBox { n, lo, hi }, pts)
}
#[test]
fn 耦合平底罚项_梯度一致且线搜索健康() {
let (obj, pts) = distance_box(12, 0.25, 7);
let mut st = 99u64;
let mut lcg = || {
st = st.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
((st >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let start: Vec<f64> = (0..pts.len()).map(|_| lcg() * 0.3).collect();
let e = max_grad_error(&obj, &start, 1e-6);
assert!(e < 1e-6, "耦合平底罚项的梯度对不上:{e:.3e}");
let mut x = start.clone();
let r = minimize(
&obj,
&mut x,
&Options {
max_iter: 3000,
grad_tol: 1e-9,
memory: 8,
},
);
assert!(
r.value < 1e-12,
"没压下去:{:.3e}(迭代 {})",
r.value,
r.iterations
);
assert!(
r.discarded * 4 < r.iterations.max(4),
"丢了 {} 个曲率对 / 迭代 {} —— 线搜索可能退化了",
r.discarded,
r.iterations
);
}
#[test]
fn 平底罚项能压到零() {
let n = 20;
let obj = FlatBottom {
center: (0..n).map(|i| (i as f64) * 0.1).collect(),
half_width: 0.5,
};
let mut x: Vec<f64> = (0..n).map(|i| 10.0 + (i as f64)).collect();
let r = minimize(
&obj,
&mut x,
&Options {
max_iter: 2000,
grad_tol: 1e-10,
memory: 8,
},
);
assert!(r.value < 1e-16, "没压到零:{:.3e}", r.value);
for (i, v) in x.iter().enumerate() {
let d = (v - obj.center[i]).abs();
assert!(d <= obj.half_width + 1e-6, "x[{i}] 没进盒子:偏 {d}");
}
}
#[test]
#[ignore]
fn 探针_各目标丢弃了多少曲率对() {
let mut x = vec![-1.2, 1.0, 0.7, -0.3, 2.0, -1.5, 0.2, 3.0];
let r = minimize(
&Rosenbrock,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-10,
memory: 8,
},
);
println!(
"Rosenbrock(8维): 迭代 {} 丢弃 {} 回溯 {}",
r.iterations, r.discarded, r.backtracks
);
let mut x = vec![3.0, -1.0, 0.0, 1.0];
let r = minimize(
&Powell,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-12,
memory: 8,
},
);
println!(
"Powell: 迭代 {} 丢弃 {} 回溯 {}",
r.iterations, r.discarded, r.backtracks
);
for (n, half, seed) in [(12usize, 0.25, 7u64), (30, 0.1, 11), (40, 0.05, 3)] {
let (obj, _) = distance_box(n, half, seed);
let mut st = seed ^ 0xabcd;
let mut lcg = || {
st = st.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
((st >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let mut x: Vec<f64> = (0..3 * n).map(|_| lcg() * 0.3).collect();
let r = minimize(
&obj,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-10,
memory: 8,
},
);
println!(
"距离盒 n={n} 半宽{half}: 迭代 {} 丢弃 {} 回溯 {} 值 {:.3e}",
r.iterations, r.discarded, r.backtracks, r.value
);
}
}
#[test]
fn 迭代次数必须配得上_lbfgs() {
let mut x: Vec<f64> = (0..8)
.map(|i| if i % 2 == 0 { -1.2 } else { 1.0 })
.collect();
let r = minimize(
&Rosenbrock,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-10,
memory: 8,
},
);
assert!(r.value < 1e-14, "8 维 Rosenbrock 目标值 {:.3e}", r.value);
assert!(
r.iterations < 100,
"8 维 Rosenbrock 用了 {} 步(健康值 69)—— 拟牛顿那一套退化了",
r.iterations
);
let mut x = vec![3.0, -1.0, 0.0, 1.0];
let r = minimize(
&Powell,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-12,
memory: 8,
},
);
assert!(
r.iterations < 400,
"Powell 用了 {} 步(健康值 152;这一条只当回归护栏)",
r.iterations
);
let (obj, _) = distance_box(30, 0.1, 11);
let mut st = 11u64 ^ 0xabcd;
let mut lcg = || {
st = st.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
((st >> 11) as f64) / ((1u64 << 53) as f64) * 2.0 - 1.0
};
let mut x: Vec<f64> = (0..90).map(|_| lcg() * 0.3).collect();
let r = minimize(
&obj,
&mut x,
&Options {
max_iter: 5000,
grad_tol: 1e-10,
memory: 8,
},
);
assert!(r.value < 1e-12, "距离盒没压到零:{:.3e}", r.value);
assert!(
r.iterations < 60,
"距离盒用了 {} 步(健康值 24;这一条只当回归护栏)",
r.iterations
);
}
#[test]
fn 同样的输入两次给同样的答案() {
let run = || {
let mut x = vec![-1.2, 1.0, 0.7, -0.3, 2.0];
let r = minimize(&Rosenbrock, &mut x, &Options::default());
(x, r)
};
let (x1, r1) = run();
let (x2, r2) = run();
assert_eq!(x1, x2, "两次跑出来的坐标不逐位相同");
assert_eq!(r1, r2, "两次跑出来的账不一样");
}
#[test]
fn 起点就是最优时不乱动() {
let mut x = vec![1.0, 1.0, 1.0];
let r = minimize(&Rosenbrock, &mut x, &Options::default());
assert!(r.converged);
assert_eq!(r.iterations, 0, "起点已是最优,不该迭代");
for v in &x {
assert!((v - 1.0).abs() < 1e-15);
}
}
}