1use crate::sqp::qp_assembly::Triplet;
32use pounce_common::types::{Index, Number};
33use std::collections::VecDeque;
34
35pub struct LBfgs {
38 n: usize,
39 m_history: usize,
40 pairs: VecDeque<(Vec<Number>, Vec<Number>)>,
42 prev_x: Option<Vec<Number>>,
45 prev_grad_lag: Option<Vec<Number>>,
46 seed_gamma: Option<Number>,
49}
50
51#[derive(Debug, Clone)]
60pub struct LowRankHessian {
61 pub sigma: Number,
62 pub vecs: Vec<Vec<Number>>,
64 pub signs: Vec<Number>,
66}
67
68impl LowRankHessian {
69 pub fn apply(&self, v: &[Number]) -> Vec<Number> {
71 let mut out: Vec<Number> = v.iter().map(|x| self.sigma * x).collect();
72 for (w, &sg) in self.vecs.iter().zip(self.signs.iter()) {
73 let c = sg * w.iter().zip(v.iter()).map(|(a, b)| a * b).sum::<Number>();
74 for (o, wi) in out.iter_mut().zip(w.iter()) {
75 *o += c * wi;
76 }
77 }
78 out
79 }
80
81 pub fn max_abs_entry(&self) -> Number {
86 let n = self.vecs.first().map_or(0, Vec::len);
87 let mut diag = vec![self.sigma; n];
88 for (w, &sg) in self.vecs.iter().zip(self.signs.iter()) {
89 for (d, wi) in diag.iter_mut().zip(w.iter()) {
90 *d += sg * wi * wi;
91 }
92 }
93 diag.iter()
94 .fold(self.sigma.abs(), |acc, d| acc.max(d.abs()))
95 }
96}
97
98impl LBfgs {
99 pub fn new(n: usize, m_history: usize) -> Self {
104 debug_assert!(m_history >= 1);
105 Self {
106 n,
107 m_history,
108 pairs: VecDeque::with_capacity(m_history),
109 prev_x: None,
110 prev_grad_lag: None,
111 seed_gamma: None,
112 }
113 }
114
115 pub fn seed_scale(&mut self, gamma: Number) {
122 if gamma.is_finite() && gamma > 0.0 {
123 self.seed_gamma = Some(gamma);
124 }
125 }
126
127 fn gamma(&self) -> Number {
130 self.pairs
131 .back()
132 .and_then(|(s, y)| {
133 let sy: Number = s.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
134 let yy: Number = y.iter().map(|v| v * v).sum();
135 if yy > 1e-30 && sy > 1e-30 {
136 Some(yy / sy)
137 } else {
138 None
139 }
140 })
141 .or(self.seed_gamma)
142 .unwrap_or(1.0)
143 }
144
145 pub fn low_rank(&self) -> LowRankHessian {
150 let mut b = LowRankHessian {
151 sigma: self.gamma(),
152 vecs: Vec::with_capacity(2 * self.pairs.len()),
153 signs: Vec::with_capacity(2 * self.pairs.len()),
154 };
155 for (s, y) in self.pairs.iter() {
156 let bs = b.apply(s);
157 let s_bs: Number = s.iter().zip(bs.iter()).map(|(a, c)| a * c).sum();
158 let s_y: Number = s.iter().zip(y.iter()).map(|(a, c)| a * c).sum();
159 let theta = powell_theta(s_bs, s_y);
160 let y_damp: Vec<Number> = y
161 .iter()
162 .zip(bs.iter())
163 .map(|(yi, bsi)| theta * yi + (1.0 - theta) * bsi)
164 .collect();
165 let s_y_damp: Number = s.iter().zip(y_damp.iter()).map(|(a, c)| a * c).sum();
166 if s_bs > 1e-14 && s_y_damp > 1e-14 {
167 let ra = 1.0 / s_bs.sqrt();
168 let rb = 1.0 / s_y_damp.sqrt();
169 b.vecs.push(bs.iter().map(|v| v * ra).collect());
170 b.signs.push(-1.0);
171 b.vecs.push(y_damp.iter().map(|v| v * rb).collect());
172 b.signs.push(1.0);
173 }
174 }
175 b
176 }
177
178 pub fn has_prev(&self) -> bool {
179 self.prev_x.is_some()
180 }
181
182 pub fn update(&mut self, x_new: &[Number], grad_lag_new: &[Number]) {
186 assert_eq!(x_new.len(), self.n, "LBFGS::update: x_new.len() != n");
188 assert_eq!(
189 grad_lag_new.len(),
190 self.n,
191 "LBFGS::update: grad_lag_new.len() != n"
192 );
193
194 if let (Some(prev_x), Some(prev_grad_lag)) =
195 (self.prev_x.as_ref(), self.prev_grad_lag.as_ref())
196 {
197 let s: Vec<Number> = x_new
198 .iter()
199 .zip(prev_x.iter())
200 .map(|(a, b)| a - b)
201 .collect();
202 let y: Vec<Number> = grad_lag_new
203 .iter()
204 .zip(prev_grad_lag.iter())
205 .map(|(a, b)| a - b)
206 .collect();
207 self.update_sy(&s, &y);
208 }
209
210 self.prev_x = Some(x_new.to_vec());
211 self.prev_grad_lag = Some(grad_lag_new.to_vec());
212 }
213
214 pub fn update_sy(&mut self, s: &[Number], y: &[Number]) {
222 assert_eq!(s.len(), self.n, "LBFGS::update_sy: s.len() != n");
223 assert_eq!(y.len(), self.n, "LBFGS::update_sy: y.len() != n");
224 let s_norm2: Number = s.iter().map(|v| v * v).sum();
226 if s_norm2 > 1e-30 {
227 if self.pairs.len() == self.m_history {
228 self.pairs.pop_front();
229 }
230 self.pairs.push_back((s.to_vec(), y.to_vec()));
231 }
232 }
233
234 pub fn as_triplet(&self) -> Triplet {
239 let n = self.n;
240 let nz = n * (n + 1) / 2;
241 let (mut irow, mut jcol) = (Vec::with_capacity(nz), Vec::with_capacity(nz));
242 for i in 0..n {
243 for j in 0..=i {
244 irow.push((i + 1) as Index);
245 jcol.push((j + 1) as Index);
246 }
247 }
248 Triplet {
249 n_rows: n,
250 n_cols: n,
251 irow,
252 jcol,
253 vals: lower_triangle(&self.materialize(), n),
254 }
255 }
256
257 fn materialize(&self) -> Vec<Number> {
262 let n = self.n;
263 let gamma = self.gamma();
266 let mut b = vec![0.0_f64; n * n];
267 for i in 0..n {
268 b[i * n + i] = gamma;
269 }
270
271 for (s, y) in self.pairs.iter() {
272 let mut bs = vec![0.0_f64; n];
274 for i in 0..n {
275 let mut acc = 0.0_f64;
276 let row = &b[i * n..i * n + n];
277 for j in 0..n {
278 acc += row[j] * s[j];
279 }
280 bs[i] = acc;
281 }
282 let s_bs: Number = s.iter().zip(bs.iter()).map(|(a, b)| a * b).sum();
283 let s_y: Number = s.iter().zip(y.iter()).map(|(a, b)| a * b).sum();
284
285 let theta = powell_theta(s_bs, s_y);
286 let y_damp: Vec<Number> = y
287 .iter()
288 .zip(bs.iter())
289 .map(|(yi, bsi)| theta * yi + (1.0 - theta) * bsi)
290 .collect();
291 let s_y_damp: Number = s.iter().zip(y_damp.iter()).map(|(a, b)| a * b).sum();
292
293 if s_bs > 1e-14 && s_y_damp > 1e-14 {
294 for i in 0..n {
295 for j in 0..n {
296 let delta = -(bs[i] * bs[j]) / s_bs + (y_damp[i] * y_damp[j]) / s_y_damp;
297 b[i * n + j] += delta;
298 }
299 }
300 }
301 }
302 b
303 }
304}
305
306fn lower_triangle(b: &[Number], n: usize) -> Vec<Number> {
308 let mut vals = Vec::with_capacity(n * (n + 1) / 2);
309 for i in 0..n {
310 for j in 0..=i {
311 vals.push(b[i * n + j]);
312 }
313 }
314 vals
315}
316
317fn powell_theta(s_bs: Number, s_y: Number) -> Number {
319 if s_y >= 0.2 * s_bs {
320 1.0
321 } else if s_bs - s_y > 1e-14 {
322 0.8 * s_bs / (s_bs - s_y)
323 } else {
324 1.0
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn lbfgs_seeds_identity_with_no_pairs() {
334 let lb = LBfgs::new(3, 5);
335 let t = lb.as_triplet();
336 for k in 0..t.vals.len() {
338 let i = (t.irow[k] - 1) as usize;
339 let j = (t.jcol[k] - 1) as usize;
340 let expected = if i == j { 1.0 } else { 0.0 };
341 assert!(
342 (t.vals[k] - expected).abs() < 1e-15,
343 "B[{i},{j}] = {} but expected {expected}",
344 t.vals[k]
345 );
346 }
347 }
348
349 #[test]
350 fn lbfgs_first_update_only_records_pair() {
351 let mut lb = LBfgs::new(2, 3);
352 lb.update(&[0.0, 0.0], &[1.0, 1.0]);
353 assert!(lb.has_prev());
354 assert!(lb.pairs.is_empty());
355 let t = lb.as_triplet();
357 let diag: Vec<_> = t
358 .vals
359 .iter()
360 .enumerate()
361 .filter(|(k, _)| t.irow[*k] == t.jcol[*k])
362 .map(|(_, v)| *v)
363 .collect();
364 assert!((diag[0] - 1.0).abs() < 1e-15);
365 assert!((diag[1] - 1.0).abs() < 1e-15);
366 }
367
368 #[test]
369 fn lbfgs_quadratic_recovers_exact_hessian_at_convergence() {
370 let mut lb = LBfgs::new(2, 5);
377 lb.update(&[0.0, 0.0], &[0.0, 0.0]); lb.update(&[1.0, 0.0], &[2.0, 0.0]); lb.update(&[1.0, 1.0], &[2.0, 4.0]); let t = lb.as_triplet();
382 let mut b = [[0.0_f64; 2]; 2];
384 for k in 0..t.vals.len() {
385 let i = (t.irow[k] - 1) as usize;
386 let j = (t.jcol[k] - 1) as usize;
387 b[i][j] = t.vals[k];
388 if i != j {
389 b[j][i] = t.vals[k];
390 }
391 }
392 assert!((b[0][0] - 2.0).abs() < 1e-9, "B[0,0] = {}", b[0][0]);
393 assert!((b[1][1] - 4.0).abs() < 1e-9, "B[1,1] = {}", b[1][1]);
394 assert!(b[0][1].abs() < 1e-9, "B[0,1] = {}", b[0][1]);
395 }
396
397 #[test]
398 fn lbfgs_history_cap_drops_oldest() {
399 let mut lb = LBfgs::new(2, 2);
400 lb.update(&[0.0, 0.0], &[0.0, 0.0]);
401 lb.update(&[1.0, 0.0], &[1.0, 0.0]);
402 lb.update(&[2.0, 0.0], &[2.0, 0.0]);
403 lb.update(&[2.0, 1.0], &[2.0, 1.0]);
404 assert_eq!(lb.pairs.len(), 2);
407 }
408
409 #[test]
413 fn low_rank_matches_the_dense_matrix() {
414 let n = 5;
415 let mut lb = LBfgs::new(n, 3);
416 lb.seed_scale(2.5);
417 let pairs: [([f64; 5], [f64; 5]); 4] = [
418 ([1.0, 0.0, 0.5, 0.0, 0.1], [3.0, 0.2, 1.0, 0.0, 0.4]),
419 ([0.0, 1.0, 0.0, -0.3, 0.0], [0.1, 2.0, 0.0, -0.5, 0.2]),
420 ([0.2, 0.2, 1.0, 0.0, 0.0], [-0.1, 0.0, -0.2, 0.0, 0.0]),
422 ([0.0, 0.0, 0.0, 1.0, 1.0], [0.0, 0.3, 0.0, 4.0, 3.5]),
423 ];
424 for (s, y) in &pairs {
425 lb.update_sy(s, y);
426 }
427 let dense = lb.materialize();
428 let lr = lb.low_rank();
429 assert_eq!(lr.vecs.len(), 6, "three stored pairs, two terms each");
430 for k in 0..n {
431 let mut e = vec![0.0; n];
432 e[k] = 1.0;
433 let col = lr.apply(&e);
434 for i in 0..n {
435 assert!(
436 (col[i] - dense[i * n + k]).abs() < 1e-12,
437 "B[{i},{k}]: product form {} vs dense {}",
438 col[i],
439 dense[i * n + k]
440 );
441 }
442 }
443 let max_dense = dense.iter().fold(0.0_f64, |a, v| a.max(v.abs()));
444 assert!((lr.max_abs_entry() - max_dense).abs() < 1e-12);
445 }
446
447 #[test]
448 fn seed_scale_sets_the_initial_matrix_until_a_pair_arrives() {
449 let mut lb = LBfgs::new(2, 3);
450 lb.seed_scale(7.0);
451 assert_eq!(lb.low_rank().sigma, 7.0);
452 lb.seed_scale(-1.0); assert_eq!(lb.low_rank().sigma, 7.0);
454 lb.update_sy(&[1.0, 0.0], &[3.0, 0.0]);
455 assert_eq!(lb.low_rank().sigma, 3.0, "yᵀy / sᵀy of the newest pair");
456 }
457}