1use num_traits::Float;
2
3use crate::convergence::{dot, norm, ConvergenceParams};
4use crate::line_search::{backtracking_armijo, ArmijoParams};
5use crate::objective::Objective;
6use crate::result::{LbfgsDiagnostics, OptimResult, SolverDiagnostics, TerminationReason};
7
8#[derive(Debug, Clone)]
10pub struct LbfgsConfig<F> {
11 pub memory: usize,
13 pub convergence: ConvergenceParams<F>,
15 pub line_search: ArmijoParams<F>,
17}
18
19impl Default for LbfgsConfig<f64> {
20 fn default() -> Self {
21 LbfgsConfig {
22 memory: 10,
23 convergence: ConvergenceParams::default(),
24 line_search: ArmijoParams::default(),
25 }
26 }
27}
28
29impl Default for LbfgsConfig<f32> {
30 fn default() -> Self {
31 LbfgsConfig {
32 memory: 10,
33 convergence: ConvergenceParams::default(),
34 line_search: ArmijoParams::default(),
35 }
36 }
37}
38
39pub fn lbfgs<F: Float, O: Objective<F>>(
44 obj: &mut O,
45 x0: &[F],
46 config: &LbfgsConfig<F>,
47) -> OptimResult<F> {
48 let n = x0.len();
49 let mut diag = LbfgsDiagnostics::default();
50
51 if config.memory == 0 || config.convergence.max_iter == 0 {
53 return OptimResult {
54 x: x0.to_vec(),
55 value: F::nan(),
56 gradient: vec![F::nan(); n],
57 gradient_norm: F::nan(),
58 iterations: 0,
59 func_evals: 0,
60 termination: TerminationReason::NumericalError,
61 diagnostics: SolverDiagnostics::Lbfgs(diag),
62 };
63 }
64
65 let mut x = x0.to_vec();
66 let (mut f_val, mut grad) = obj.eval_grad(&x);
67 let mut func_evals = 1usize;
68 let mut grad_norm = norm(&grad);
69
70 if !grad_norm.is_finite() || !f_val.is_finite() {
72 return OptimResult {
73 x,
74 value: f_val,
75 gradient: grad,
76 gradient_norm: grad_norm,
77 iterations: 0,
78 func_evals,
79 termination: TerminationReason::NumericalError,
80 diagnostics: SolverDiagnostics::Lbfgs(diag),
81 };
82 }
83
84 if grad_norm < config.convergence.grad_tol {
86 return OptimResult {
87 x,
88 value: f_val,
89 gradient: grad,
90 gradient_norm: grad_norm,
91 iterations: 0,
92 func_evals,
93 termination: TerminationReason::GradientNorm,
94 diagnostics: SolverDiagnostics::Lbfgs(diag),
95 };
96 }
97
98 let m = config.memory;
100 let mut s_hist: Vec<Vec<F>> = Vec::with_capacity(m);
101 let mut y_hist: Vec<Vec<F>> = Vec::with_capacity(m);
102 let mut rho_hist: Vec<F> = Vec::with_capacity(m);
103
104 for iter in 0..config.convergence.max_iter {
105 let (d, gamma_clamped) = two_loop_recursion(&grad, &s_hist, &y_hist, &rho_hist);
108 if gamma_clamped {
109 diag.gamma_clamp_hits += 1;
110 }
111
112 let ls = match backtracking_armijo(obj, &x, &d, f_val, &grad, &config.line_search) {
114 Some(ls) => ls,
115 None => {
116 return OptimResult {
117 x,
118 value: f_val,
119 gradient: grad,
120 gradient_norm: grad_norm,
121 iterations: iter,
122 func_evals,
123 termination: TerminationReason::LineSearchFailed,
124 diagnostics: SolverDiagnostics::Lbfgs(diag),
125 };
126 }
127 };
128 func_evals += ls.evals;
129 diag.line_search_backtracks += ls.evals.saturating_sub(1);
132
133 let mut s = vec![F::zero(); n];
135 let mut y = vec![F::zero(); n];
136 for i in 0..n {
137 s[i] = ls.alpha * d[i];
140 y[i] = ls.gradient[i] - grad[i];
141 x[i] = x[i] + s[i];
142 }
143
144 let f_prev = f_val;
145 f_val = ls.value;
146 grad = ls.gradient;
147 grad_norm = norm(&grad);
148
149 let sy = dot(&s, &y);
157 let ss = dot(&s, &s);
158 let yy = dot(&y, &y);
159 if curvature_ok(sy, ss, yy) {
160 if s_hist.len() == m {
161 s_hist.remove(0);
162 y_hist.remove(0);
163 rho_hist.remove(0);
164 diag.pairs_evicted_by_memory += 1;
165 }
166 rho_hist.push(F::one() / sy);
167 s_hist.push(s);
168 y_hist.push(y);
169 diag.pairs_accepted += 1;
170 } else {
171 diag.pairs_curvature_rejected += 1;
172 }
173
174 if !grad_norm.is_finite() || !f_val.is_finite() {
176 return OptimResult {
177 x,
178 value: f_val,
179 gradient: grad,
180 gradient_norm: grad_norm,
181 iterations: iter + 1,
182 func_evals,
183 termination: TerminationReason::NumericalError,
184 diagnostics: SolverDiagnostics::Lbfgs(diag),
185 };
186 }
187
188 if grad_norm < config.convergence.grad_tol {
190 return OptimResult {
191 x,
192 value: f_val,
193 gradient: grad,
194 gradient_norm: grad_norm,
195 iterations: iter + 1,
196 func_evals,
197 termination: TerminationReason::GradientNorm,
198 diagnostics: SolverDiagnostics::Lbfgs(diag),
199 };
200 }
201
202 let step_norm = norm_step(ls.alpha, &d);
203 if step_norm < config.convergence.step_tol {
204 return OptimResult {
205 x,
206 value: f_val,
207 gradient: grad,
208 gradient_norm: grad_norm,
209 iterations: iter + 1,
210 func_evals,
211 termination: TerminationReason::StepSize,
212 diagnostics: SolverDiagnostics::Lbfgs(diag),
213 };
214 }
215
216 if config.convergence.func_tol > F::zero()
221 && (f_prev - f_val).abs() < config.convergence.func_tol * (F::one() + f_val.abs())
222 {
223 return OptimResult {
224 x,
225 value: f_val,
226 gradient: grad,
227 gradient_norm: grad_norm,
228 iterations: iter + 1,
229 func_evals,
230 termination: TerminationReason::FunctionChange,
231 diagnostics: SolverDiagnostics::Lbfgs(diag),
232 };
233 }
234 }
235
236 OptimResult {
237 x,
238 value: f_val,
239 gradient: grad,
240 gradient_norm: grad_norm,
241 iterations: config.convergence.max_iter,
242 func_evals,
243 termination: TerminationReason::MaxIterations,
244 diagnostics: SolverDiagnostics::Lbfgs(diag),
245 }
246}
247
248fn two_loop_recursion<F: Float>(
255 grad: &[F],
256 s_hist: &[Vec<F>],
257 y_hist: &[Vec<F>],
258 rho_hist: &[F],
259) -> (Vec<F>, bool) {
260 let k = s_hist.len();
261 let n = grad.len();
262 let mut gamma_clamp_hit = false;
263
264 let mut q: Vec<F> = grad.to_vec();
266
267 let mut alpha = vec![F::zero(); k];
269 for i in (0..k).rev() {
270 alpha[i] = rho_hist[i] * dot(&s_hist[i], &q);
271 for j in 0..n {
272 q[j] = q[j] - alpha[i] * y_hist[i][j];
273 }
274 }
275
276 let mut r = q;
279 if k > 0 {
280 let sy = dot(&s_hist[k - 1], &y_hist[k - 1]);
281 let yy = dot(&y_hist[k - 1], &y_hist[k - 1]);
282 if yy > F::epsilon() {
283 let raw_gamma = sy / yy;
284 let lo = F::from(1e-3).unwrap();
291 let hi = F::from(1e3).unwrap();
292 let gamma = if raw_gamma.is_finite() {
298 if raw_gamma < lo || raw_gamma > hi {
299 gamma_clamp_hit = true;
300 }
301 raw_gamma.max(lo).min(hi)
302 } else {
303 gamma_clamp_hit = true;
304 F::one()
305 };
306 for v in r.iter_mut() {
307 *v = *v * gamma;
308 }
309 }
310 }
311
312 for i in 0..k {
314 let beta = rho_hist[i] * dot(&y_hist[i], &r);
315 for j in 0..n {
316 r[j] = r[j] + (alpha[i] - beta) * s_hist[i][j];
317 }
318 }
319
320 for v in r.iter_mut() {
322 *v = F::zero() - *v;
323 }
324
325 (r, gamma_clamp_hit)
326}
327
328fn norm_step<F: Float>(alpha: F, d: &[F]) -> F {
329 let mut s = F::zero();
330 for &di in d {
331 let step = alpha * di;
332 s = s + step * step;
333 }
334 s.sqrt()
335}
336
337#[inline]
342fn curvature_ok<F: Float>(sy: F, ss: F, yy: F) -> bool {
343 sy > F::epsilon().sqrt() * (ss * yy).sqrt()
344}
345
346#[cfg(test)]
347mod tests {
348 use super::*;
349
350 #[test]
351 fn curvature_ok_rejects_near_orthogonal() {
352 assert!(!curvature_ok(1e-10_f64, 1.0, 1.0));
356 assert!(curvature_ok(0.5_f64, 1.0, 1.0));
358 }
359
360 struct Rosenbrock;
361
362 impl Objective<f64> for Rosenbrock {
363 fn dim(&self) -> usize {
364 2
365 }
366
367 fn eval_grad(&mut self, x: &[f64]) -> (f64, Vec<f64>) {
368 let a = 1.0 - x[0];
369 let b = x[1] - x[0] * x[0];
370 let f = a * a + 100.0 * b * b;
371 let g0 = -2.0 * a - 400.0 * x[0] * b;
372 let g1 = 200.0 * b;
373 (f, vec![g0, g1])
374 }
375 }
376
377 #[test]
378 fn lbfgs_rosenbrock() {
379 let mut obj = Rosenbrock;
380 let config = LbfgsConfig::default();
381 let result = lbfgs(&mut obj, &[0.0, 0.0], &config);
382
383 assert_eq!(result.termination, TerminationReason::GradientNorm);
384 assert!(
385 (result.x[0] - 1.0).abs() < 1e-6,
386 "x[0] = {}, expected 1.0",
387 result.x[0]
388 );
389 assert!(
390 (result.x[1] - 1.0).abs() < 1e-6,
391 "x[1] = {}, expected 1.0",
392 result.x[1]
393 );
394 assert!(result.gradient_norm < 1e-8);
395 }
396
397 #[test]
398 fn lbfgs_already_converged() {
399 let mut obj = Rosenbrock;
400 let config = LbfgsConfig::default();
401 let result = lbfgs(&mut obj, &[1.0, 1.0], &config);
402
403 assert_eq!(result.termination, TerminationReason::GradientNorm);
404 assert_eq!(result.iterations, 0);
405 }
406}