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>,
allowed: Option<Vec<bool>>,
}
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,
pub declared_pattern: Option<Vec<(Index, Index)>>,
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,
declared_pattern: None,
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);
if let Some(pat) = self.declared_pattern.as_ref() {
let mut parent: Vec<usize> = (0..n).collect();
fn root(parent: &mut [usize], mut a: usize) -> usize {
while parent[a] != a {
parent[a] = parent[parent[a]];
a = parent[a];
}
a
}
let mut mentioned = vec![false; n];
let mut adj: std::collections::HashMap<Index, Vec<Index>> = Default::default();
for &(r, c) in pat {
let (ru, cu) = (r as usize, c as usize);
if ru >= n || cu >= n {
continue;
}
mentioned[ru] = true;
mentioned[cu] = true;
adj.entry(r).or_default().push(c);
if r != c {
adj.entry(c).or_default().push(r);
}
let (a, b) = (root(&mut parent, ru), root(&mut parent, cu));
if a != b {
parent[a.max(b)] = a.min(b);
}
}
let mut comp_of_root: std::collections::HashMap<usize, usize> = Default::default();
let mut comps: Vec<Vec<Index>> = Vec::new();
for (i, _) in mentioned.iter().enumerate().filter(|(_, m)| **m) {
let r = root(&mut parent, i);
let g = *comp_of_root.entry(r).or_insert_with(|| {
comps.push(Vec::new());
comps.len() - 1
});
comps[g].push(i as Index);
}
if comps.iter().all(|c| c.len() <= bs) {
for comp in comps {
let mut el = Self::make_element(
ElementSource::LagrangianBlock,
0,
comp,
Vec::new(),
usize::MAX,
);
el.allowed = pattern_mask(&el.support, &adj);
elements.push(el);
}
self.finish_structure(n, elements);
return;
}
}
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 adjacency: Option<std::collections::HashMap<Index, Vec<Index>>> =
self.declared_pattern.as_ref().map(|pat| {
let mut m: std::collections::HashMap<Index, Vec<Index>> = Default::default();
for &(r, c) in pat {
m.entry(r).or_default().push(c);
if r != c {
m.entry(c).or_default().push(r);
}
}
m
});
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() {
let mut sup = obj_support;
sup.sort_unstable();
sup.dedup();
self.push_split(
&mut elements,
adjacency.as_ref(),
ElementSource::Objective,
0,
sup,
Vec::new(),
);
}
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();
self.push_split(
&mut elements,
adjacency.as_ref(),
source,
(r - 1) as u32,
cols,
entries,
);
}
}
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 {
if e.allowed.as_ref().is_none_or(|m| m[a * (a + 1) / 2 + c]) {
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 {
let ok =
a == c || e.allowed.as_ref().is_none_or(|m| m[a * (a + 1) / 2 + c]);
map.push(if ok {
find(e.support[a], e.support[c])
} else {
u32::MAX
});
}
}
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 push_split(
&self,
elements: &mut Vec<Element>,
adjacency: Option<&std::collections::HashMap<Index, Vec<Index>>>,
source: ElementSource,
row: u32,
support: Vec<Index>,
entries: Vec<(u32, u32)>,
) {
let Some(adj) = adjacency else {
elements.push(Self::make_element(
source,
row,
support,
entries,
self.max_element,
));
return;
};
let k = support.len();
let mut parent: Vec<usize> = (0..k).collect();
fn find(parent: &mut [usize], mut a: usize) -> usize {
while parent[a] != a {
parent[a] = parent[parent[a]];
a = parent[a];
}
a
}
let mut mentioned = vec![false; k];
for a in 0..k {
if let Some(nbrs) = adj.get(&support[a]) {
for &c in nbrs {
if let Ok(b) = support.binary_search(&c) {
mentioned[a] = true;
mentioned[b] = true;
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra.max(rb)] = ra.min(rb);
}
}
}
}
}
let mut group_of: Vec<Option<usize>> = vec![None; k];
let mut groups: Vec<Vec<usize>> = Vec::new();
let mut root_to_group: std::collections::HashMap<usize, usize> = Default::default();
for a in 0..k {
if !mentioned[a] {
continue;
}
let r = find(&mut parent, a);
let g = *root_to_group.entry(r).or_insert_with(|| {
groups.push(Vec::new());
groups.len() - 1
});
groups[g].push(a);
group_of[a] = Some(g);
}
let mut local_in_group = vec![0u32; k];
for g in &groups {
for (pos, &a) in g.iter().enumerate() {
local_in_group[a] = pos as u32;
}
}
let mut group_entries: Vec<Vec<(u32, u32)>> = vec![Vec::new(); groups.len()];
for &(p, local) in &entries {
if let Some(g) = group_of[local as usize] {
group_entries[g].push((p, local_in_group[local as usize]));
}
}
for (g, members) in groups.iter().enumerate() {
let sub: Vec<Index> = members.iter().map(|&a| support[a]).collect();
let ent = std::mem::take(&mut group_entries[g]);
let mut el = Self::make_element(source, row, sub, ent, self.max_element);
if el.dense {
el.allowed = pattern_mask(&el.support, adj);
}
elements.push(el);
}
}
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(),
allowed: None,
}
}
}
fn pattern_mask(
support: &[Index],
adj: &std::collections::HashMap<Index, Vec<Index>>,
) -> Option<Vec<bool>> {
let k = support.len();
let mut allowed = vec![false; k * (k + 1) / 2];
let mut complete = true;
for a in 0..k {
let nbrs = adj.get(&support[a]);
for c in 0..=a {
let ok = nbrs.is_some_and(|v| v.contains(&support[c]));
allowed[a * (a + 1) / 2 + c] = ok;
complete &= ok;
}
}
if complete { None } else { Some(allowed) }
}
#[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 sparse_secant_update(
b: &mut [Number],
allowed: &[bool],
s: &[Number],
y: &[Number],
max_delta: Number,
) -> bool {
let k = s.len();
let mut bs = vec![0.0; k];
packed_mult(b, s, &mut bs);
let r: Vec<Number> = y.iter().zip(bs.iter()).map(|(a, c)| a - c).collect();
let r_norm = dot(&r, &r).sqrt();
let y_norm = dot(y, y).sqrt();
if !r_norm.is_finite() || r_norm <= 1e-14 * y_norm.max(Number::MIN_POSITIVE) || r_norm == 0.0 {
return false;
}
let mut q = vec![0.0; k * k];
for a in 0..k {
for c in 0..=a {
if !allowed[a * (a + 1) / 2 + c] {
continue;
}
if a == c {
q[a * k + a] += 2.0 * s[a] * s[a];
} else {
q[a * k + a] += s[c] * s[c];
q[c * k + c] += s[a] * s[a];
q[a * k + c] += s[a] * s[c];
q[c * k + a] += s[a] * s[c];
}
}
}
let Some(lam) = min_norm_solve(&mut q, &r, k) else {
return false;
};
let mut e = vec![0.0; b.len()];
let mut e_max = 0.0_f64;
for a in 0..k {
for c in 0..=a {
let p = a * (a + 1) / 2 + c;
if allowed[p] {
let v = lam[a] * s[c] + lam[c] * s[a];
e[p] = v;
e_max = e_max.max(v.abs());
}
}
}
if !e_max.is_finite() || e_max > max_delta {
return false;
}
for (bv, ev) in b.iter_mut().zip(e.iter()) {
*bv += ev;
}
true
}
fn min_norm_solve(q: &mut [Number], r: &[Number], k: usize) -> Option<Vec<Number>> {
let mut v = vec![0.0; k * k];
for i in 0..k {
v[i * k + i] = 1.0;
}
for _sweep in 0..60 {
let mut off = 0.0;
let mut diag = 0.0;
for i in 0..k {
diag += q[i * k + i] * q[i * k + i];
for j in 0..i {
off += q[i * k + j] * q[i * k + j];
}
}
if !(off.is_finite() && diag.is_finite()) {
return None;
}
if off <= 1e-30 * diag {
break;
}
for p in 0..k {
for qq in (p + 1)..k {
let apq = q[p * k + qq];
if apq == 0.0 {
continue;
}
let app = q[p * k + p];
let aqq = q[qq * k + qq];
let theta = (aqq - app) / (2.0 * apq);
let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
let t = if theta == 0.0 { 1.0 } else { t };
let c = 1.0 / (t * t + 1.0).sqrt();
let sn = t * c;
for i in 0..k {
let aip = q[i * k + p];
let aiq = q[i * k + qq];
q[i * k + p] = c * aip - sn * aiq;
q[i * k + qq] = sn * aip + c * aiq;
}
for i in 0..k {
let api = q[p * k + i];
let aqi = q[qq * k + i];
q[p * k + i] = c * api - sn * aqi;
q[qq * k + i] = sn * api + c * aqi;
}
for i in 0..k {
let vip = v[i * k + p];
let viq = v[i * k + qq];
v[i * k + p] = c * vip - sn * viq;
v[i * k + qq] = sn * vip + c * viq;
}
}
}
}
let lmax = (0..k).map(|i| q[i * k + i].abs()).fold(0.0_f64, f64::max);
if lmax <= 0.0 || !lmax.is_finite() {
return None;
}
let mut x = vec![0.0; k];
for j in 0..k {
let lj = q[j * k + j];
if lj.abs() <= 1e-12 * lmax {
continue;
}
let coef: Number = (0..k).map(|i| v[i * k + j] * r[i]).sum::<Number>() / lj;
for i in 0..k {
x[i] += coef * v[i * k + j];
}
}
Some(x)
}
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 let Some(allowed) = e.allowed.as_ref() {
let ok = sparse_secant_update(&mut e.b, allowed, s, y, max_delta);
e.seeded |= ok;
return ok;
}
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
}
}
}
fn element_update_type(requested: UpdateType, source: ElementSource) -> UpdateType {
match (requested, source) {
(UpdateType::Bfgs, ElementSource::EqRow | ElementSource::IneqRow) => UpdateType::Sr1,
(t, _) => t,
}
}
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()));
let element_update = element_update_type(self.update_type, e.source);
if update_element(
e,
&s_loc,
&y_loc,
element_update,
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() {
if m != u32::MAX {
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 declared_pattern_splits_an_element_into_its_independent_blocks() {
let u = PartitionedQuasiNewtonUpdater::new(UpdateType::Sr1);
let pat: Vec<(Index, Index)> = vec![(1, 0), (3, 2), (4, 4), (9, 9)];
let mut adj: std::collections::HashMap<Index, Vec<Index>> = Default::default();
for &(r, c) in &pat {
adj.entry(r).or_default().push(c);
if r != c {
adj.entry(c).or_default().push(r);
}
}
let mut els = Vec::new();
let entries: Vec<(u32, u32)> = (0..7).map(|i| (10 + i as u32, i as u32)).collect();
u.push_split(
&mut els,
Some(&adj),
ElementSource::EqRow,
3,
(0..7).collect(),
entries,
);
let sup: Vec<Vec<Index>> = els.iter().map(|e| e.support.clone()).collect();
assert_eq!(sup, vec![vec![0, 1], vec![2, 3], vec![4]]);
assert_eq!(els[1].entries, vec![(12, 0), (13, 1)]);
assert!(els.iter().all(|e| e.row == 3));
let mut one = Vec::new();
u.push_split(
&mut one,
None,
ElementSource::EqRow,
3,
(0..7).collect(),
Vec::new(),
);
assert_eq!(one.len(), 1);
assert_eq!(one[0].support.len(), 7);
}
#[test]
fn sparse_secant_recovers_a_star_hessian_from_one_pair() {
let (a, b1, b2) = (-3.0e-4, 2.0e-3, -7.0e-4);
let h = [a, b1, 0.0, b2, 0.0, 0.0];
let allowed = vec![true, true, false, true, false, false];
let s = vec![0.3, -1.2, 0.8];
let mut y = vec![0.0; 3];
packed_mult(&h, &s, &mut y);
let mut b = vec![0.0; 6];
assert!(sparse_secant_update(
&mut b,
&allowed,
&s,
&y,
Number::INFINITY
));
for p in 0..6 {
assert!(
(b[p] - h[p]).abs() < 1e-15,
"packed entry {p}: {} vs {}",
b[p],
h[p]
);
}
let s2 = vec![-0.5, 0.1, 0.4];
let mut y2 = vec![0.0; 3];
packed_mult(&h, &s2, &mut y2);
assert!(!sparse_secant_update(
&mut b,
&allowed,
&s2,
&y2,
Number::INFINITY
));
}
#[test]
fn sparse_secant_keeps_the_pattern_and_the_secant_equation() {
let k = 4;
let mut allowed = vec![false; k * (k + 1) / 2];
for (r, c) in [(0, 0), (1, 0), (2, 1), (3, 3), (2, 2)] {
allowed[r * (r + 1) / 2 + c] = true;
}
let mut b = vec![0.0; k * (k + 1) / 2];
b[0] = 1.0;
let s = vec![0.5, -0.25, 1.0, 0.0];
let y = vec![0.1, 0.7, -0.4, 0.0];
assert!(sparse_secant_update(
&mut b,
&allowed,
&s,
&y,
Number::INFINITY
));
let mut bs = vec![0.0; k];
packed_mult(&b, &s, &mut bs);
for a in 0..k {
assert!(
(bs[a] - y[a]).abs() < 1e-12,
"row {a}: {} vs {}",
bs[a],
y[a]
);
}
for (p, &ok) in allowed.iter().enumerate() {
if !ok {
assert_eq!(b[p], 0.0, "excluded packed entry {p} was written");
}
}
assert_eq!(b[9], 0.0, "(3,3) has no step component and must stay 0");
}
#[test]
fn min_norm_solve_handles_a_singular_system() {
let v = [1.0, 2.0, 0.0];
let mut q = vec![0.0; 9];
for i in 0..3 {
for j in 0..3 {
q[i * 3 + j] = v[i] * v[j];
}
}
let r = [5.0, 10.0, 0.0];
let x = min_norm_solve(&mut q, &r, 3).expect("solvable");
let want = [1.0, 2.0, 0.0];
for i in 0..3 {
assert!(
(x[i] - want[i]).abs() < 1e-12,
"{i}: {} vs {}",
x[i],
want[i]
);
}
}
#[test]
fn blocks_follow_the_declared_components_not_the_index_order() {
let mut u = PartitionedQuasiNewtonUpdater::new(UpdateType::Bfgs);
u.mode = ElementMode::PrimalBlock;
u.block_size = 64;
u.declared_pattern = Some(vec![(3, 0), (2, 1), (2, 2), (5, 2)]);
let empty = |rows| {
GenTMatrix::new(pounce_linalg::triplet::GenTMatrixSpace::new(
rows,
6,
Vec::new(),
Vec::new(),
))
};
u.build_structure(6, &[0.0; 6], &empty(0), &empty(0));
let sup: Vec<Vec<Index>> = u.elements.iter().map(|e| e.support.clone()).collect();
assert_eq!(sup, vec![vec![0, 3], vec![1, 2, 5]]);
assert_eq!(u.uncovered, vec![4]);
let star = &u.elements[1];
assert_eq!(
star.allowed.as_deref(),
Some(&[false, true, true, false, true, false][..])
);
}
#[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(),
allowed: None,
};
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(),
allowed: None,
};
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(),
allowed: None,
};
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(),
allowed: None,
};
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(),
allowed: None,
};
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(),
allowed: None,
};
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);
}
}