use crate::hess::lim_mem_quasi_newton::UpdateType;
use crate::hess::r#trait::HessianUpdater;
use crate::ipopt_cq::IpoptCqHandle;
use crate::ipopt_data::IpoptDataHandle;
use pounce_common::types::{Index, Number};
use pounce_linalg::Vector;
use pounce_linalg::compound_vector::CompoundVector;
use pounce_linalg::dense_vector::DenseVector;
use pounce_linalg::triplet::{GenTMatrix, SymTMatrix, SymTMatrixSpace};
use std::rc::Rc;
const SR1_SAFEGUARD: Number = 1e-8;
const POWELL_THETA: Number = 0.2;
const BFGS_DENOM_FLOOR: Number = 1e-8;
const DEFAULT_CURVATURE_CAP: Number = Number::INFINITY;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ElementSource {
Objective,
EqRow,
IneqRow,
LagrangianBlock,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ElementMode {
PerConstraint,
PrimalBlock,
}
#[derive(Debug)]
struct Element {
source: ElementSource,
row: u32,
support: Vec<Index>,
dense: bool,
b: Vec<Number>,
prev_g: Vec<Number>,
has_prev: bool,
seeded: bool,
entries: Vec<(u32, u32)>,
map: Vec<u32>,
}
impl Element {
fn k(&self) -> usize {
self.support.len()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PartitionStats {
pub elements: usize,
pub dense_elements: usize,
pub diagonal_elements: usize,
pub max_support: usize,
pub total_support: usize,
pub assembled_nnz: usize,
pub stored_reals: usize,
}
pub struct PartitionedQuasiNewtonUpdater {
pub update_type: UpdateType,
pub mode: ElementMode,
pub block_size: usize,
pub max_element: usize,
pub init_val_min: Number,
pub init_val_max: Number,
pub init_val: Number,
pub objective_vars: Option<Vec<Index>>,
pub curvature_cap: Number,
elements: Vec<Element>,
space: Option<Rc<SymTMatrixSpace>>,
diag_pos: Vec<u32>,
uncovered: Vec<Index>,
prev_x: Option<Vec<Number>>,
prev_grad_f: Option<Vec<Number>>,
prev_jac_c: Option<Vec<Number>>,
prev_jac_d: Option<Vec<Number>>,
stats: PartitionStats,
census_done: bool,
pub accepted_updates: u64,
pub skipped_updates: u64,
dbg: DebugPeaks,
}
impl PartitionedQuasiNewtonUpdater {
pub fn new(update_type: UpdateType) -> Self {
Self {
update_type,
mode: ElementMode::PerConstraint,
block_size: 64,
max_element: 64,
init_val_min: 1e-8,
init_val_max: 1e8,
init_val: 1.0,
objective_vars: None,
curvature_cap: DEFAULT_CURVATURE_CAP,
elements: Vec::new(),
space: None,
diag_pos: Vec::new(),
uncovered: Vec::new(),
prev_x: None,
prev_grad_f: None,
prev_jac_c: None,
prev_jac_d: None,
stats: PartitionStats::default(),
census_done: false,
accepted_updates: 0,
skipped_updates: 0,
dbg: DebugPeaks::default(),
}
}
pub fn stats(&self) -> PartitionStats {
self.stats
}
fn build_structure(
&mut self,
n: usize,
grad_f: &[Number],
jac_c: &GenTMatrix,
jac_d: &GenTMatrix,
) {
let mut elements: Vec<Element> = Vec::new();
if self.mode == ElementMode::PrimalBlock {
let bs = self.block_size.max(1);
let mut start = 0usize;
while start < n {
let end = (start + bs).min(n);
let support: Vec<Index> = (start..end).map(|i| i as Index).collect();
elements.push(Self::make_element(
ElementSource::LagrangianBlock,
0,
support,
Vec::new(),
usize::MAX,
));
start = end;
}
self.finish_structure(n, elements);
return;
}
let obj_support: Vec<Index> = match self.objective_vars.clone() {
Some(v) => v,
None => (0..n)
.filter(|&i| grad_f[i] != 0.0)
.map(|i| i as Index)
.collect(),
};
if !obj_support.is_empty() {
elements.push(Self::make_element(
ElementSource::Objective,
0,
obj_support,
Vec::new(),
self.max_element,
));
}
for (source, jac) in [
(ElementSource::EqRow, jac_c),
(ElementSource::IneqRow, jac_d),
] {
let n_rows = jac.space().n_rows() as usize;
let irows = jac.irows();
let jcols = jac.jcols();
let mut row_counts = vec![0u32; n_rows + 1];
for &i in irows {
row_counts[i as usize] += 1;
}
let mut row_start = vec![0u32; n_rows + 2];
for r in 0..=n_rows {
row_start[r + 1] = row_start[r] + row_counts[r];
}
let mut cursor = row_start.clone();
let mut by_row = vec![0u32; irows.len()];
for (pos, &i) in irows.iter().enumerate() {
let r = i as usize;
by_row[cursor[r] as usize] = pos as u32;
cursor[r] += 1;
}
for r in 1..=n_rows {
let slice = &by_row[row_start[r] as usize..row_start[r + 1] as usize];
if slice.is_empty() {
continue;
}
let mut cols: Vec<Index> = slice.iter().map(|&p| jcols[p as usize] - 1).collect();
cols.sort_unstable();
cols.dedup();
let entries: Vec<(u32, u32)> = slice
.iter()
.map(|&p| {
let c = jcols[p as usize] - 1;
let local = cols.partition_point(|&x| x < c) as u32;
(p, local)
})
.collect();
elements.push(Self::make_element(
source,
(r - 1) as u32,
cols,
entries,
self.max_element,
));
}
}
self.finish_structure(n, elements);
}
fn finish_structure(&mut self, n: usize, mut elements: Vec<Element>) {
let mut pairs: Vec<(Index, Index)> = Vec::new();
for i in 0..n {
pairs.push((i as Index, i as Index));
}
for e in &elements {
if e.dense {
for a in 0..e.k() {
for c in 0..=a {
pairs.push((e.support[a], e.support[c]));
}
}
} else {
for a in 0..e.k() {
pairs.push((e.support[a], e.support[a]));
}
}
}
pairs.sort_unstable();
pairs.dedup();
let find =
|row: Index, col: Index| -> u32 { pairs.partition_point(|&p| p < (row, col)) as u32 };
for e in &mut elements {
if e.dense {
let mut map = Vec::with_capacity(e.k() * (e.k() + 1) / 2);
for a in 0..e.k() {
for c in 0..=a {
map.push(find(e.support[a], e.support[c]));
}
}
e.map = map;
} else {
e.map = (0..e.k())
.map(|a| find(e.support[a], e.support[a]))
.collect();
}
}
self.diag_pos = (0..n).map(|i| find(i as Index, i as Index)).collect();
let mut covered = vec![false; n];
for e in &elements {
for &i in &e.support {
covered[i as usize] = true;
}
}
self.uncovered = (0..n)
.filter(|&i| !covered[i])
.map(|i| i as Index)
.collect();
let irows: Vec<Index> = pairs.iter().map(|&(r, _)| r + 1).collect();
let jcols: Vec<Index> = pairs.iter().map(|&(_, c)| c + 1).collect();
self.stats = PartitionStats {
elements: elements.len(),
dense_elements: elements.iter().filter(|e| e.dense).count(),
diagonal_elements: elements.iter().filter(|e| !e.dense).count(),
max_support: elements.iter().map(|e| e.k()).max().unwrap_or(0),
total_support: elements.iter().map(|e| e.k()).sum(),
assembled_nnz: pairs.len(),
stored_reals: elements.iter().map(|e| e.b.len()).sum(),
};
self.space = Some(SymTMatrixSpace::new(n as Index, irows, jcols));
self.elements = elements;
if std::env::var("POUNCE_PARTITIONED_DEBUG").is_ok() {
eprintln!("partitioned-qn: {:?}", self.stats);
}
}
fn make_element(
source: ElementSource,
row: u32,
support: Vec<Index>,
entries: Vec<(u32, u32)>,
max_element: usize,
) -> Element {
let k = support.len();
let dense = k <= max_element;
let b_len = if dense { k * (k + 1) / 2 } else { k };
Element {
source,
row,
support,
dense,
b: vec![0.0; b_len],
prev_g: vec![0.0; k],
has_prev: false,
seeded: false,
entries,
map: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct DebugPeaks {
ratio: Number,
ratio_s: Number,
ratio_y: Number,
ratio_k: usize,
delta: Number,
step_norm: Number,
}
fn packed_mult(b: &[Number], s: &[Number], out: &mut [Number]) {
out.iter_mut().for_each(|v| *v = 0.0);
let mut p = 0usize;
for a in 0..s.len() {
for c in 0..=a {
let v = b[p];
p += 1;
if v == 0.0 {
continue;
}
out[a] += v * s[c];
if c != a {
out[c] += v * s[a];
}
}
}
}
fn dot(a: &[Number], b: &[Number]) -> Number {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn update_element(
e: &mut Element,
s: &[Number],
y: &[Number],
update_type: UpdateType,
init_val_min: Number,
init_val_max: Number,
curvature_cap: Number,
) -> bool {
let sts = dot(s, s);
if !(sts > 0.0) || !sts.is_finite() {
return false;
}
let sty = dot(s, y);
if !sty.is_finite() {
return false;
}
let s_norm = sts.sqrt();
let implied = dot(y, y).sqrt() / s_norm;
let max_delta = curvature_cap * implied;
if !e.seeded {
let mut gamma = sty / sts;
if !gamma.is_finite() || gamma == 0.0 {
gamma = if update_type == UpdateType::Bfgs {
1.0
} else {
0.0
};
}
if update_type == UpdateType::Bfgs && gamma <= 0.0 {
gamma = 1.0;
}
if gamma != 0.0 {
let mag = gamma.abs().clamp(init_val_min, init_val_max);
gamma = gamma.signum() * mag;
}
if e.dense {
for a in 0..e.k() {
e.b[a * (a + 1) / 2 + a] = gamma;
}
} else {
e.b.iter_mut().for_each(|v| *v = gamma);
}
e.seeded = true;
}
if !e.dense {
let s_bs: Number = (0..e.k()).map(|a| e.b[a] * s[a] * s[a]).sum();
let denom: Number = s.iter().map(|v| v * v * v * v).sum();
if !(denom > 0.0) || !denom.is_finite() {
return false;
}
let scale = (sty - s_bs) / denom;
if !scale.is_finite() {
return false;
}
for a in 0..e.k() {
e.b[a] += scale * s[a] * s[a];
}
return true;
}
let mut bs = vec![0.0; e.k()];
packed_mult(&e.b, s, &mut bs);
match update_type {
UpdateType::Sr1 => {
let w: Vec<Number> = y.iter().zip(bs.iter()).map(|(a, b)| a - b).collect();
let den = dot(&w, s);
let w_norm = dot(&w, &w).sqrt();
let s_norm = sts.sqrt();
if !den.is_finite() || w_norm == 0.0 || den.abs() <= SR1_SAFEGUARD * s_norm * w_norm {
return false;
}
let w_max = w.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
if w_max * w_max / den.abs() > max_delta {
return false;
}
let mut p = 0usize;
for a in 0..e.k() {
for c in 0..=a {
e.b[p] += w[a] * w[c] / den;
p += 1;
}
}
true
}
UpdateType::Bfgs => {
let s_bs = dot(s, &bs);
let bs_norm = dot(&bs, &bs).sqrt();
if !(s_bs > 0.0) || !s_bs.is_finite() || s_bs <= BFGS_DENOM_FLOOR * s_norm * bs_norm {
return false;
}
let theta = if sty >= POWELL_THETA * s_bs {
1.0
} else {
(1.0 - POWELL_THETA) * s_bs / (s_bs - sty)
};
if !theta.is_finite() {
return false;
}
let r: Vec<Number> = y
.iter()
.zip(bs.iter())
.map(|(yy, bb)| theta * yy + (1.0 - theta) * bb)
.collect();
let sr = dot(s, &r);
let r_norm = dot(&r, &r).sqrt();
if !(sr > 0.0) || !sr.is_finite() || sr <= BFGS_DENOM_FLOOR * s_norm * r_norm {
return false;
}
let r_max = r.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
let bs_max = bs.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
if r_max * r_max / sr + bs_max * bs_max / s_bs > max_delta {
return false;
}
let mut p = 0usize;
for a in 0..e.k() {
for c in 0..=a {
e.b[p] += r[a] * r[c] / sr - bs[a] * bs[c] / s_bs;
p += 1;
}
}
true
}
}
}
impl HessianUpdater for PartitionedQuasiNewtonUpdater {
fn update_hessian(&mut self, data: &IpoptDataHandle, cq: &IpoptCqHandle) -> bool {
let (curr_x, curr_y_c, curr_y_d) = match data.borrow().curr.as_ref() {
Some(c) => (c.x.clone(), c.y_c.clone(), c.y_d.clone()),
None => return true,
};
let curr_grad_f = cq.borrow().curr_grad_f();
let curr_jac_c = cq.borrow().curr_jac_c();
let curr_jac_d = cq.borrow().curr_jac_d();
let (Some(jac_c), Some(jac_d)) = (
curr_jac_c.as_any().downcast_ref::<GenTMatrix>(),
curr_jac_d.as_any().downcast_ref::<GenTMatrix>(),
) else {
return false;
};
let x = flat(&*curr_x);
let grad_f = flat(&*curr_grad_f);
let n = x.len();
if self.space.is_none() {
self.build_structure(n, &grad_f, jac_c, jac_d);
}
let y_c_now = flat(&*curr_y_c);
let y_d_now = flat(&*curr_y_d);
let s_full: Option<Vec<Number>> = self
.prev_x
.as_ref()
.map(|p| x.iter().zip(p.iter()).map(|(a, b)| a - b).collect());
let lagrangian_dy: Option<Vec<Number>> = if self.mode == ElementMode::PrimalBlock {
match (
self.prev_grad_f.as_ref(),
self.prev_jac_c.as_ref(),
self.prev_jac_d.as_ref(),
) {
(Some(pg), Some(pc), Some(pd)) => {
let mut dy = vec![0.0; n];
for i in 0..n {
dy[i] = grad_f[i] - pg[i];
}
for (jac, prev, mult) in [(jac_c, pc, &y_c_now), (jac_d, pd, &y_d_now)] {
let (ir, jc, cur) = (jac.irows(), jac.jcols(), jac.values());
for k in 0..ir.len() {
let row = (ir[k] - 1) as usize;
let col = (jc[k] - 1) as usize;
dy[col] += (cur[k] - prev[k]) * mult[row];
}
}
Some(dy)
}
_ => None,
}
} else {
None
};
let oracle = std::env::var("POUNCE_PARTITIONED_ORACLE").is_ok();
if oracle {
self.dbg = DebugPeaks {
step_norm: s_full.as_ref().map(|v| dot(v, v).sqrt()).unwrap_or(0.0),
..DebugPeaks::default()
};
}
let mut s_loc: Vec<Number> = Vec::new();
let mut y_loc: Vec<Number> = Vec::new();
let mut g_loc: Vec<Number> = Vec::new();
for e in &mut self.elements {
let k = e.k();
g_loc.clear();
g_loc.resize(k, 0.0);
match e.source {
ElementSource::Objective => {
for (a, &i) in e.support.iter().enumerate() {
g_loc[a] = grad_f[i as usize];
}
}
ElementSource::EqRow => {
let v = jac_c.values();
for &(pos, local) in &e.entries {
g_loc[local as usize] += v[pos as usize];
}
}
ElementSource::IneqRow => {
let v = jac_d.values();
for &(pos, local) in &e.entries {
g_loc[local as usize] += v[pos as usize];
}
}
ElementSource::LagrangianBlock => {}
}
let pair_ready = match e.source {
ElementSource::LagrangianBlock => lagrangian_dy.is_some(),
_ => e.has_prev,
};
if let (Some(s_full), true) = (s_full.as_ref(), pair_ready) {
s_loc.clear();
y_loc.clear();
for (a, &i) in e.support.iter().enumerate() {
s_loc.push(s_full[i as usize]);
y_loc.push(match e.source {
ElementSource::LagrangianBlock => {
lagrangian_dy.as_ref().expect("checked above")[i as usize]
}
_ => g_loc[a] - e.prev_g[a],
});
}
let before = e.b.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
if update_element(
e,
&s_loc,
&y_loc,
self.update_type,
self.init_val_min,
self.init_val_max,
self.curvature_cap,
) {
self.accepted_updates += 1;
} else {
self.skipped_updates += 1;
}
if oracle {
let sn = dot(&s_loc, &s_loc).sqrt();
let yn = dot(&y_loc, &y_loc).sqrt();
let r = if sn > 0.0 { yn / sn } else { 0.0 };
if r > self.dbg.ratio {
self.dbg = DebugPeaks {
ratio: r,
ratio_s: sn,
ratio_y: yn,
ratio_k: e.k(),
..self.dbg
};
}
let after = e.b.iter().fold(0.0_f64, |m, v| m.max(v.abs()));
self.dbg.delta = self.dbg.delta.max((after - before).abs());
}
}
e.prev_g.copy_from_slice(&g_loc);
e.has_prev = true;
}
self.prev_x = Some(x);
if self.mode == ElementMode::PrimalBlock {
self.prev_grad_f = Some(grad_f.clone());
self.prev_jac_c = Some(jac_c.values().to_vec());
self.prev_jac_d = Some(jac_d.values().to_vec());
}
let y_c = flat(&*curr_y_c);
let y_d = flat(&*curr_y_d);
let space = Rc::clone(self.space.as_ref().expect("structure built above"));
let mut w = SymTMatrix::new(Rc::clone(&space));
let any_seeded = self.elements.iter().any(|e| e.seeded);
{
let vals = w.values_mut();
vals.iter_mut().for_each(|v| *v = 0.0);
if !any_seeded {
for &p in &self.diag_pos {
vals[p as usize] = self.init_val;
}
}
for e in self.elements.iter().filter(|_| any_seeded) {
let weight = match e.source {
ElementSource::Objective => 1.0,
ElementSource::EqRow => y_c[e.row as usize],
ElementSource::IneqRow => y_d[e.row as usize],
ElementSource::LagrangianBlock => 1.0,
};
if weight == 0.0 || !weight.is_finite() {
continue;
}
for (p, &m) in e.map.iter().enumerate() {
vals[m as usize] += weight * e.b[p];
}
}
if any_seeded {
for &i in &self.uncovered {
vals[self.diag_pos[i as usize] as usize] = self.init_val_min;
}
}
}
if std::env::var("POUNCE_PARTITIONED_ORACLE").is_ok() {
let exact = cq.borrow().curr_exact_hessian();
if let Some(t) = exact.as_any().downcast_ref::<SymTMatrix>() {
if std::env::var("POUNCE_HESS_PATTERN_CENSUS").is_ok() && !self.census_done {
self.census_done = true;
let n_h = t.space().dim() as usize;
let mut deg = vec![0usize; n_h];
for (&i, &j) in t.irows().iter().zip(t.jcols().iter()) {
let (a, b) = ((i - 1) as usize, (j - 1) as usize);
deg[a] += 1;
if a != b {
deg[b] += 1;
}
}
let rho_max = deg.iter().copied().max().unwrap_or(0);
let mean = deg.iter().sum::<usize>() as f64 / n_h as f64;
let mut hist = [0usize; 8];
for &d in ° {
let b = (d.saturating_sub(1) / 8).min(7);
hist[b] += 1;
}
eprintln!(
"hess-pattern: n={n_h} nnz={} rho_max={rho_max} mean_row={mean:.2} \
hist(1-8,9-16,...)={hist:?}",
t.nonzeros()
);
}
use std::collections::HashMap;
let mut mine: HashMap<(Index, Index), Number> = HashMap::new();
for ((&i, &j), &v) in space
.irows()
.iter()
.zip(space.jcols().iter())
.zip(w.values().iter())
{
*mine.entry((i, j)).or_insert(0.0) += v;
}
let (mut max_exact, mut max_err) = (0.0_f64, 0.0_f64);
let (mut num, mut den) = (0.0_f64, 0.0_f64);
let mut captured = 0.0_f64;
let mut worst = ((0, 0), 0.0, 0.0);
let mut seen: HashMap<(Index, Index), bool> = HashMap::new();
for ((&i, &j), &v) in t
.irows()
.iter()
.zip(t.jcols().iter())
.zip(t.values().iter())
{
seen.insert((i, j), true);
let m = mine.get(&(i, j)).copied().unwrap_or(0.0);
if mine.contains_key(&(i, j)) {
captured += v * v;
}
let e = (m - v).abs();
max_exact = max_exact.max(v.abs());
if e > max_err {
max_err = e;
worst = ((i, j), v, m);
}
num += e * e;
den += v * v;
}
let mut extra = 0.0_f64;
for (&k, &v) in mine.iter() {
if !seen.contains_key(&k) {
extra = extra.max(v.abs());
}
}
eprintln!(
"partitioned-qn oracle: rel_fro={:.3e} max_abs_err={:.3e} max|exact|={:.3e} worst={:?} exact={:.6e} mine={:.6e} max|extra-pattern|={:.3e} pattern_captures={:.4}",
(num / den.max(1e-300)).sqrt(),
max_err,
max_exact,
worst.0,
worst.1,
worst.2,
extra,
(captured / den.max(1e-300)).sqrt()
);
eprintln!(
" peaks: max|y_e|/|s_e|={:.3e} (|s_e|={:.3e} |y_e|={:.3e} k={}) \
max_block_delta={:.3e} |s|={:.3e} accepted={} skipped={}",
self.dbg.ratio,
self.dbg.ratio_s,
self.dbg.ratio_y,
self.dbg.ratio_k,
self.dbg.delta,
self.dbg.step_norm,
self.accepted_updates,
self.skipped_updates
);
}
}
if std::env::var("POUNCE_PARTITIONED_DUMP").is_ok() && space.nonzeros() <= 32 {
eprintln!(
"partitioned-qn W: seeded={any_seeded} irows={:?} jcols={:?} vals={:?}",
space.irows(),
space.jcols(),
w.values()
);
eprintln!(" y_c={y_c:?} y_d={y_d:?}");
}
data.borrow_mut().w = Some(Rc::new(w));
true
}
}
fn flat(v: &dyn Vector) -> Vec<Number> {
if let Some(dv) = v.as_any().downcast_ref::<DenseVector>() {
return dv.expanded_values();
}
if let Some(cv) = v.as_any().downcast_ref::<CompoundVector>() {
let mut out = Vec::with_capacity(cv.dim() as usize);
for i in 0..cv.n_comps() {
out.extend(flat(cv.comp(i)));
}
return out;
}
panic!("PartitionedQuasiNewtonUpdater: unsupported primal vector type");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn packed_mult_matches_dense() {
let b = vec![1.0, 2.0, 4.0, 3.0, 5.0, 6.0];
let s = vec![1.0, -2.0, 0.5];
let mut out = vec![0.0; 3];
packed_mult(&b, &s, &mut out);
let dense = [[1.0, 2.0, 3.0], [2.0, 4.0, 5.0], [3.0, 5.0, 6.0]];
for a in 0..3 {
let want: Number = (0..3).map(|c| dense[a][c] * s[c]).sum();
assert!(
(out[a] - want).abs() < 1e-12,
"row {a}: {} vs {want}",
out[a]
);
}
}
#[test]
fn sr1_satisfies_the_secant_equation() {
let mut e = Element {
source: ElementSource::EqRow,
row: 0,
support: vec![0, 1, 2],
dense: true,
b: vec![0.0; 6],
prev_g: vec![0.0; 3],
has_prev: true,
seeded: true,
entries: Vec::new(),
map: Vec::new(),
};
let s = vec![1.0, 0.5, -0.25];
let y = vec![-2.0, 1.0, 3.0];
assert!(update_element(
&mut e,
&s,
&y,
UpdateType::Sr1,
1e-8,
1e8,
1e12
));
let mut bs = vec![0.0; 3];
packed_mult(&e.b, &s, &mut bs);
for a in 0..3 {
assert!(
(bs[a] - y[a]).abs() < 1e-10,
"component {a}: {} vs {}",
bs[a],
y[a]
);
}
}
#[test]
fn sr1_preserves_negative_curvature_where_bfgs_would_not() {
let make = || Element {
source: ElementSource::EqRow,
row: 0,
support: vec![0],
dense: true,
b: vec![0.0],
prev_g: vec![0.0],
has_prev: true,
seeded: false,
entries: Vec::new(),
map: Vec::new(),
};
let s = vec![1.0];
let y = vec![-3.0];
let mut sr1 = make();
update_element(&mut sr1, &s, &y, UpdateType::Sr1, 1e-8, 1e8, 1e12);
assert!(sr1.b[0] < 0.0, "SR1 kept curvature {}", sr1.b[0]);
assert!((sr1.b[0] + 3.0).abs() < 1e-12, "{}", sr1.b[0]);
let mut bfgs = make();
update_element(&mut bfgs, &s, &y, UpdateType::Bfgs, 1e-8, 1e8, 1e12);
assert!(bfgs.b[0] > 0.0, "damped BFGS kept curvature {}", bfgs.b[0]);
}
#[test]
fn scalar_seeding_leaves_the_first_sr1_update_with_nothing_to_do() {
let mut e = Element {
source: ElementSource::EqRow,
row: 0,
support: vec![0, 1],
dense: true,
b: vec![0.0; 3],
prev_g: vec![0.0; 2],
has_prev: true,
seeded: false,
entries: Vec::new(),
map: Vec::new(),
};
let s = vec![1.0, 0.5];
let y = vec![2.0, -4.0];
assert!(!update_element(
&mut e,
&s,
&y,
UpdateType::Sr1,
1e-8,
1e8,
1e12
));
assert!(e.seeded);
assert_eq!(e.b[1], 0.0, "off-diagonal must still be zero");
assert!(
(e.b[0] - e.b[2]).abs() < 1e-15,
"block must be a multiple of I"
);
}
#[test]
fn sr1_reaches_an_indefinite_block_where_bfgs_stays_definite() {
let make = || Element {
source: ElementSource::EqRow,
row: 0,
support: vec![0, 1],
dense: true,
b: vec![0.0; 3],
prev_g: vec![0.0; 2],
has_prev: true,
seeded: false,
entries: Vec::new(),
map: Vec::new(),
};
let (s1, y1) = (vec![1.0, 0.0], vec![2.0, 0.0]);
let (s2, y2) = (vec![0.0, 1.0], vec![0.0, -4.0]);
let mut sr1 = make();
update_element(&mut sr1, &s1, &y1, UpdateType::Sr1, 1e-8, 1e8, 1e12);
assert!(update_element(
&mut sr1,
&s2,
&y2,
UpdateType::Sr1,
1e-8,
1e8,
1e12
));
let mut bs = vec![0.0; 2];
packed_mult(&sr1.b, &s2, &mut bs);
for a in 0..2 {
assert!(
(bs[a] - y2[a]).abs() < 1e-10,
"component {a}: {} vs {}",
bs[a],
y2[a]
);
}
let det = sr1.b[0] * sr1.b[2] - sr1.b[1] * sr1.b[1];
assert!(det < 0.0, "SR1 block determinant {det}");
let mut bfgs = make();
update_element(&mut bfgs, &s1, &y1, UpdateType::Bfgs, 1e-8, 1e8, 1e12);
assert!(update_element(
&mut bfgs,
&s2,
&y2,
UpdateType::Bfgs,
1e-8,
1e8,
1e12
));
let det_b = bfgs.b[0] * bfgs.b[2] - bfgs.b[1] * bfgs.b[1];
assert!(
bfgs.b[0] > 0.0 && det_b > 0.0,
"damped BFGS block is positive definite: diag {} det {det_b}",
bfgs.b[0]
);
}
#[test]
fn diagonal_element_satisfies_the_weak_secant_condition() {
let mut e = Element {
source: ElementSource::Objective,
row: 0,
support: vec![0, 1, 2],
dense: false,
b: vec![0.0; 3],
prev_g: vec![0.0; 3],
has_prev: true,
seeded: true,
entries: Vec::new(),
map: Vec::new(),
};
let s = vec![1.0, -2.0, 0.5];
let y = vec![0.5, 1.0, -3.0];
assert!(update_element(
&mut e,
&s,
&y,
UpdateType::Sr1,
1e-8,
1e8,
1e12
));
let s_bs: Number = (0..3).map(|a| e.b[a] * s[a] * s[a]).sum();
assert!(
(s_bs - dot(&s, &y)).abs() < 1e-10,
"{s_bs} vs {}",
dot(&s, &y)
);
}
#[test]
fn sr1_skips_a_degenerate_denominator() {
let mut e = Element {
source: ElementSource::EqRow,
row: 0,
support: vec![0, 1],
dense: true,
b: vec![2.0, 0.0, 2.0],
prev_g: vec![0.0; 2],
has_prev: true,
seeded: true,
entries: Vec::new(),
map: Vec::new(),
};
let before = e.b.clone();
let s = vec![1.0, 1.0];
let y = vec![2.0, 2.0];
assert!(!update_element(
&mut e,
&s,
&y,
UpdateType::Sr1,
1e-8,
1e8,
1e12
));
assert_eq!(e.b, before);
}
}