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 FD_REL_STEP: Number = 1.4901161193847656e-8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FdColoring {
Cpr,
Star,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FdPatternSource {
Declared,
Jacobian,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FdStats {
pub pattern_used: Option<FdPatternSource>,
pub n: usize,
pub nnz: usize,
pub groups: usize,
pub rho_max: usize,
pub compression: f64,
pub coloring_fell_back: bool,
pub objective_clique_widened: bool,
}
pub struct FdHessianUpdater {
pub pattern_source: FdPatternSource,
pub coloring: FdColoring,
pub reuse_tol: Number,
pub objective_vars: Option<Vec<Index>>,
pub nonlinear_vars: Option<Vec<Index>>,
space: Option<Rc<SymTMatrixSpace>>,
groups: Vec<Vec<Index>>,
recovery: Vec<(u32, u32, u32)>,
by_group: Vec<Vec<u32>>,
stats: FdStats,
reported: bool,
prev_x: Option<Vec<Number>>,
prev_y: Option<Vec<Number>>,
prev_w: Option<Rc<SymTMatrix>>,
pub reused: u64,
pub rebuilt: u64,
}
impl FdHessianUpdater {
pub fn new(pattern_source: FdPatternSource) -> Self {
Self {
pattern_source,
coloring: FdColoring::Cpr,
reuse_tol: 0.0,
objective_vars: None,
nonlinear_vars: None,
space: None,
groups: Vec::new(),
recovery: Vec::new(),
by_group: Vec::new(),
stats: FdStats::default(),
reported: false,
prev_x: None,
prev_y: None,
prev_w: None,
reused: 0,
rebuilt: 0,
}
}
pub fn stats(&self) -> FdStats {
self.stats
}
fn color_cpr(n: usize, cols_of_row: &[Vec<Index>], rows_of_col: &[Vec<Index>]) -> Vec<usize> {
let mut order: Vec<Index> = (0..n as Index).collect();
order.sort_unstable_by_key(|&j| std::cmp::Reverse(rows_of_col[j as usize].len()));
let mut color = vec![usize::MAX; n];
let mut forbidden = vec![usize::MAX; n + 1];
let mut n_colors = 0usize;
for &j in &order {
let stamp = j as usize;
for &i in &rows_of_col[j as usize] {
for &k in &cols_of_row[i as usize] {
let c = color[k as usize];
if c != usize::MAX {
forbidden[c] = stamp;
}
}
}
let mut c = 0usize;
while c < n_colors && forbidden[c] == stamp {
c += 1;
}
if c == n_colors {
n_colors += 1;
}
color[j as usize] = c;
}
color
}
fn color_star(n: usize, adj: &[Vec<Index>]) -> Vec<usize> {
let mut order: Vec<Index> = (0..n as Index).collect();
order.sort_unstable_by_key(|&v| std::cmp::Reverse(adj[v as usize].len()));
let mut color = vec![usize::MAX; n];
let mut forbidden = vec![usize::MAX; n + 2];
let mut n_colors = 0usize;
for &v in &order {
let stamp = v as usize;
for &w in &adj[v as usize] {
let cw = color[w as usize];
if cw != usize::MAX {
forbidden[cw] = stamp;
for &x in &adj[w as usize] {
if x == v {
continue;
}
let cx = color[x as usize];
if cx == usize::MAX {
continue;
}
for &y in &adj[x as usize] {
if y != w && color[y as usize] == cw {
forbidden[cx] = stamp;
break;
}
}
}
} else {
for &x in &adj[w as usize] {
if x == v {
continue;
}
let cx = color[x as usize];
if cx != usize::MAX {
forbidden[cx] = stamp;
}
}
}
}
let mut c = 0usize;
while c < n_colors && forbidden[c] == stamp {
c += 1;
}
if c == n_colors {
n_colors += 1;
}
color[v as usize] = c;
}
color
}
fn build_structure(
&mut self,
n: usize,
declared: Option<&(Vec<Index>, Vec<Index>)>,
jac_c: &GenTMatrix,
jac_d: &GenTMatrix,
) {
let mut pairs: Vec<(Index, Index)> = Vec::new();
let pattern_used = match (self.pattern_source, declared) {
(FdPatternSource::Declared, Some(_)) => FdPatternSource::Declared,
_ => FdPatternSource::Jacobian,
};
match (self.pattern_source, declared) {
(FdPatternSource::Declared, Some((ir, jc))) => {
for (&i, &j) in ir.iter().zip(jc.iter()) {
let (a, b) = (i - 1, j - 1);
pairs.push(if a >= b { (a, b) } else { (b, a) });
}
}
_ => {
let (obj, widened) = objective_support(
self.objective_vars.as_deref(),
self.nonlinear_vars.as_deref(),
n,
);
if widened {
self.stats.objective_clique_widened = true;
}
for (a, &ca) in obj.iter().enumerate() {
for &cb in obj.iter().take(a + 1) {
pairs.push(if ca >= cb { (ca, cb) } else { (cb, ca) });
}
}
let mask: Option<Vec<bool>> = self.nonlinear_vars.as_ref().map(|v| {
let mut m = vec![false; n];
for &i in v {
m[i as usize] = true;
}
m
});
for jac in [jac_c, jac_d] {
let n_rows = jac.space().n_rows() as usize;
let mut by_row: Vec<Vec<Index>> = vec![Vec::new(); n_rows + 1];
for (&i, &j) in jac.irows().iter().zip(jac.jcols().iter()) {
by_row[i as usize].push(j - 1);
}
for row in by_row.iter_mut() {
if let Some(m) = mask.as_ref() {
row.retain(|&c| m[c as usize]);
}
row.sort_unstable();
row.dedup();
for (a, &ca) in row.iter().enumerate() {
for &cb in row.iter().take(a + 1) {
pairs.push((ca, cb));
}
}
}
}
}
}
for i in 0..n {
pairs.push((i as Index, i as Index));
}
pairs.sort_unstable();
pairs.dedup();
let mut rows_of_col: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut cols_of_row: Vec<Vec<Index>> = vec![Vec::new(); n];
for &(i, j) in &pairs {
rows_of_col[j as usize].push(i);
cols_of_row[i as usize].push(j);
if i != j {
rows_of_col[i as usize].push(j);
cols_of_row[j as usize].push(i);
}
}
let rho_max = cols_of_row.iter().map(|r| r.len()).max().unwrap_or(0);
let mut adj: Vec<Vec<Index>> = vec![Vec::new(); n];
for &(i, j) in &pairs {
if i != j {
adj[i as usize].push(j);
adj[j as usize].push(i);
}
}
let validate = |color: &[usize]| -> bool {
let count_in_color = |v: Index, c: usize| -> usize {
adj[v as usize]
.iter()
.filter(|&&w| color[w as usize] == c)
.count()
};
pairs.iter().all(|&(i, j)| {
i == j
|| count_in_color(i, color[j as usize]) == 1
|| count_in_color(j, color[i as usize]) == 1
})
};
let mut color = match self.coloring {
FdColoring::Cpr => Self::color_cpr(n, &cols_of_row, &rows_of_col),
FdColoring::Star => Self::color_star(n, &adj),
};
let mut fell_back = false;
if self.coloring == FdColoring::Star && !validate(&color) {
color = Self::color_cpr(n, &cols_of_row, &rows_of_col);
fell_back = true;
debug_assert!(validate(&color), "CPR colouring must always be recoverable");
}
let n_colors = color.iter().copied().max().map(|c| c + 1).unwrap_or(0);
let mut groups = vec![Vec::new(); n_colors];
for (j, &c) in color.iter().enumerate() {
groups[c].push(j as Index);
}
let count_in_color = |v: Index, c: usize| -> usize {
adj[v as usize]
.iter()
.filter(|&&w| color[w as usize] == c)
.count()
};
let mut recovery: Vec<(u32, u32, u32)> = Vec::with_capacity(pairs.len());
for &(i, j) in &pairs {
if i == j {
recovery.push((color[i as usize] as u32, i as u32, i as u32));
continue;
}
let (ci, cj) = (color[i as usize], color[j as usize]);
if count_in_color(i, cj) == 1 {
recovery.push((cj as u32, i as u32, j as u32));
} else {
recovery.push((ci as u32, j as u32, i as u32));
}
}
let mut by_group: Vec<Vec<u32>> = vec![Vec::new(); n_colors];
for (k, &(g, _, _)) in recovery.iter().enumerate() {
by_group[g as usize].push(k as u32);
}
self.stats = FdStats {
pattern_used: Some(pattern_used),
n,
nnz: pairs.len(),
groups: groups.len(),
rho_max,
compression: groups.len() as f64 / n.max(1) as f64,
coloring_fell_back: fell_back,
objective_clique_widened: self.stats.objective_clique_widened,
};
let irows: Vec<Index> = pairs.iter().map(|&(i, _)| i + 1).collect();
let jcols: Vec<Index> = pairs.iter().map(|&(_, j)| j + 1).collect();
self.space = Some(SymTMatrixSpace::new(n as Index, irows, jcols));
self.groups = groups;
self.recovery = recovery;
self.by_group = by_group;
}
}
impl HessianUpdater for FdHessianUpdater {
fn fd_hessian_stats(&self) -> Option<FdStats> {
self.stats.pattern_used.map(|_| self.stats)
}
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 nlp = Rc::clone(cq.borrow().nlp());
let base_grad_f = cq.borrow().curr_grad_f();
let base_jac_c = cq.borrow().curr_jac_c();
let base_jac_d = cq.borrow().curr_jac_d();
let (Some(jc), Some(jd)) = (
base_jac_c.as_any().downcast_ref::<GenTMatrix>(),
base_jac_d.as_any().downcast_ref::<GenTMatrix>(),
) else {
return false;
};
let x = flat(&*curr_x);
let n = x.len();
if self.space.is_none() {
let declared = nlp.borrow().uninitialized_h();
let declared_pat = declared
.as_any()
.downcast_ref::<SymTMatrix>()
.filter(|t| t.nonzeros() > 0)
.map(|t| (t.irows().to_vec(), t.jcols().to_vec()));
self.build_structure(n, declared_pat.as_ref(), jc, jd);
if !self.reported && std::env::var("POUNCE_FD_HESSIAN_DEBUG").is_ok() {
self.reported = true;
eprintln!("fd-hessian: {:?}", self.stats);
}
}
let mut base = curr_x.make_new();
base.copy(&*base_grad_f);
base_jac_c.trans_mult_vector(1.0, &*curr_y_c, 1.0, &mut *base);
base_jac_d.trans_mult_vector(1.0, &*curr_y_d, 1.0, &mut *base);
let base = flat(&*base);
let steps: Vec<Number> = (0..n).map(|j| FD_REL_STEP * x[j].abs().max(1.0)).collect();
if self.reuse_tol > 0.0 {
let y_now: Vec<Number> = flat(&*curr_y_c)
.into_iter()
.chain(flat(&*curr_y_d))
.collect();
if let (Some(px), Some(py), Some(pw)) = (
self.prev_x.as_ref(),
self.prev_y.as_ref(),
self.prev_w.as_ref(),
) {
let rel = |a: &[Number], b: &[Number]| -> Number {
let (mut d, mut m) = (0.0_f64, 1.0_f64);
for (u, v) in a.iter().zip(b.iter()) {
d = d.max((u - v).abs());
m = m.max(u.abs());
}
d / m
};
if px.len() == x.len()
&& py.len() == y_now.len()
&& rel(&x, px) <= self.reuse_tol
&& rel(&y_now, py) <= self.reuse_tol
{
self.reused += 1;
data.borrow_mut().w = Some(Rc::clone(pw) as Rc<dyn pounce_linalg::SymMatrix>);
return true;
}
}
self.prev_x = Some(x.clone());
self.prev_y = Some(y_now);
}
self.rebuilt += 1;
let space = Rc::clone(self.space.as_ref().expect("structure built above"));
let mut w = SymTMatrix::new(Rc::clone(&space));
{
let vals = w.values_mut();
vals.iter_mut().for_each(|v| *v = 0.0);
let mut probe = curr_x.make_new();
let mut gl = curr_x.make_new();
let mut xp = x.clone();
for (gi, group) in self.groups.iter().enumerate() {
let mut sign = 1.0;
let mut g1;
loop {
xp.copy_from_slice(&x);
for &j in group {
xp[j as usize] += sign * steps[j as usize];
}
set_expanded(probe.as_mut(), &xp);
nlp.borrow_mut().eval_grad_f(&*probe, &mut *gl);
let pj_c = nlp.borrow_mut().eval_jac_c(&*probe);
pj_c.trans_mult_vector(1.0, &*curr_y_c, 1.0, &mut *gl);
let pj_d = nlp.borrow_mut().eval_jac_d(&*probe);
pj_d.trans_mult_vector(1.0, &*curr_y_d, 1.0, &mut *gl);
g1 = flat(&*gl);
if g1.iter().all(|v| v.is_finite()) {
break;
}
if sign < 0.0 {
return false;
}
sign = -1.0;
}
for &k in &self.by_group[gi] {
let (_, read, col) = self.recovery[k as usize];
let hq = sign * steps[col as usize];
vals[k as usize] = (g1[read as usize] - base[read as usize]) / hq;
}
}
}
let w = Rc::new(w);
if self.reuse_tol > 0.0 {
self.prev_w = Some(Rc::clone(&w));
}
data.borrow_mut().w = Some(w as Rc<dyn pounce_linalg::SymMatrix>);
true
}
fn hessian_at_current(
&mut self,
data: &IpoptDataHandle,
cq: &IpoptCqHandle,
) -> Option<Rc<dyn pounce_linalg::SymMatrix>> {
let saved = data.borrow().w.clone();
let ok = self.update_hessian(data, cq);
let built = data.borrow().w.clone();
data.borrow_mut().w = saved;
if ok { built } else { None }
}
}
fn objective_support(
objective_vars: Option<&[Index]>,
nonlinear_vars: Option<&[Index]>,
n: usize,
) -> (Vec<Index>, bool) {
match objective_vars {
Some(v) => (v.to_vec(), false),
None => match nonlinear_vars {
Some(v) => (v.to_vec(), true),
None => ((0..n as Index).collect(), 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!("FdHessianUpdater: unsupported primal vector type");
}
fn set_expanded(dst: &mut dyn Vector, values: &[Number]) {
if let Some(dv) = dst.as_any_mut().downcast_mut::<DenseVector>() {
dv.set_values(values);
return;
}
if let Some(cv) = dst.as_any_mut().downcast_mut::<CompoundVector>() {
let dims: Vec<usize> = (0..cv.n_comps())
.map(|i| cv.comp(i).dim() as usize)
.collect();
let mut off = 0usize;
for (i, &d) in dims.iter().enumerate() {
set_expanded(cv.comp_mut(i as Index), &values[off..off + d]);
off += d;
}
return;
}
panic!("FdHessianUpdater: unsupported primal vector type");
}
#[cfg(test)]
mod tests {
use super::*;
fn banded(
n: usize,
half_band: usize,
) -> (
Vec<(Index, Index)>,
Vec<Vec<Index>>,
Vec<Vec<Index>>,
Vec<Vec<Index>>,
) {
let mut pairs = Vec::new();
for i in 0..n {
for j in i.saturating_sub(half_band)..=i {
pairs.push((i as Index, j as Index));
}
}
let mut rows_of_col: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut cols_of_row: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut adj: Vec<Vec<Index>> = vec![Vec::new(); n];
for &(i, j) in &pairs {
rows_of_col[j as usize].push(i);
cols_of_row[i as usize].push(j);
if i != j {
rows_of_col[i as usize].push(j);
cols_of_row[j as usize].push(i);
adj[i as usize].push(j);
adj[j as usize].push(i);
}
}
(pairs, rows_of_col, cols_of_row, adj)
}
fn recovers_exactly(coloring: FdColoring, n: usize, half_band: usize) -> usize {
let (pairs, rows_of_col, cols_of_row, adj) = banded(n, half_band);
let color = match coloring {
FdColoring::Cpr => FdHessianUpdater::color_cpr(n, &cols_of_row, &rows_of_col),
FdColoring::Star => FdHessianUpdater::color_star(n, &adj),
};
let n_colors = color.iter().copied().max().unwrap() + 1;
let val = |i: Index, j: Index| -> Number {
1.0 + (i as Number) * 0.5 - (j as Number) * 0.25 + ((i + j) as Number).sin()
};
let mut dense = vec![vec![0.0 as Number; n]; n];
for &(i, j) in &pairs {
let v = val(i.max(j), i.min(j));
dense[i as usize][j as usize] = v;
dense[j as usize][i as usize] = v;
}
let mut probes = vec![vec![0.0 as Number; n]; n_colors];
for (m, &c) in color.iter().enumerate() {
for i in 0..n {
probes[c][i] += dense[i][m];
}
}
let count_in_color = |v: Index, c: usize| -> usize {
adj[v as usize]
.iter()
.filter(|&&w| color[w as usize] == c)
.count()
};
for &(i, j) in &pairs {
let (g, read) = if i == j {
(color[i as usize], i)
} else {
let (ci, cj) = (color[i as usize], color[j as usize]);
if count_in_color(i, cj) == 1 {
(cj, i)
} else {
assert_eq!(
count_in_color(j, ci),
1,
"{coloring:?}: neither endpoint of ({i},{j}) is directly recoverable"
);
(ci, j)
}
};
let got = probes[g][read as usize];
let want = dense[i as usize][j as usize];
assert!(
(got - want).abs() < 1e-12,
"{coloring:?}: entry ({i},{j}) recovered {got}, want {want} — the probe component carried a sum, not one entry"
);
}
n_colors
}
#[test]
fn cpr_recovers_a_banded_matrix_exactly() {
for hb in 1..=4 {
recovers_exactly(FdColoring::Cpr, 40, hb);
}
}
#[test]
fn star_recovers_a_banded_matrix_exactly() {
for hb in 1..=4 {
recovers_exactly(FdColoring::Star, 40, hb);
}
}
#[test]
fn star_never_needs_more_groups_than_cpr() {
for hb in [1usize, 2, 4, 8] {
let star = recovers_exactly(FdColoring::Star, 60, hb);
let cpr = recovers_exactly(FdColoring::Cpr, 60, hb);
assert!(star <= cpr, "half-band {hb}: star {star} > cpr {cpr}");
}
}
#[test]
fn the_objective_clique_is_in_the_jacobian_derived_pattern() {
let n = 4usize;
let rows = [vec![0 as Index, 1], vec![2 as Index, 3]];
let obj = vec![0 as Index, 2];
let mut pairs: std::collections::BTreeSet<(Index, Index)> = Default::default();
for i in 0..n as Index {
pairs.insert((i, i));
}
for r in &rows {
for (a, &ca) in r.iter().enumerate() {
for &cb in r.iter().take(a + 1) {
pairs.insert(if ca >= cb { (ca, cb) } else { (cb, ca) });
}
}
}
assert!(
!pairs.contains(&(2, 0)),
"fixture is wrong: the constraint cliques already cover the objective pair"
);
for (a, &ca) in obj.iter().enumerate() {
for &cb in obj.iter().take(a + 1) {
pairs.insert(if ca >= cb { (ca, cb) } else { (cb, ca) });
}
}
assert!(
pairs.contains(&(2, 0)),
"the objective clique must contribute (2,0) — without it the \
Jacobian-derived pattern is a SUBSET of the true Hessian and \
`∂²f/∂x₀∂x₂` is dropped with no diagnostic"
);
}
#[test]
fn the_objective_fallback_is_structural_not_value_derived() {
let n = 2usize;
let (obj, widened) = objective_support(None, None, n);
assert_eq!(obj, vec![0 as Index, 1]);
assert!(widened, "a fallback this wide must be reported as widened");
assert!(
obj.contains(&0) && obj.contains(&1),
"`f = x₀x₁` has `∂²f/∂x₀∂x₁ ≠ 0`; both coordinates must be in \
the clique whatever the starting point"
);
let (obj, widened) = objective_support(None, Some(&[1 as Index]), n);
assert_eq!(obj, vec![1 as Index]);
assert!(widened);
let (obj, widened) = objective_support(Some(&[0 as Index]), Some(&[0, 1]), n);
assert_eq!(obj, vec![0 as Index]);
assert!(
!widened,
"a model that states its objective support must not be \
reported as widened — that flag is what tells a user why \
their probe count is large"
);
}
#[test]
fn the_objective_support_rule_is_a_function_of_structure_alone() {
let a = objective_support(None, Some(&[0 as Index, 2]), 4);
let b = objective_support(None, Some(&[0 as Index, 2]), 4);
assert_eq!(a, b);
assert_eq!(a.0, vec![0 as Index, 2]);
}
#[test]
fn overlapping_cliques_are_validated_not_assumed() {
let n = 18usize;
let mut set = std::collections::BTreeSet::new();
for start in (0..n - 5).step_by(3) {
let cols: Vec<Index> = (start..start + 6).map(|v| v as Index).collect();
for (a, &ca) in cols.iter().enumerate() {
for &cb in cols.iter().take(a + 1) {
set.insert((ca, cb));
}
}
}
for i in 0..n as Index {
set.insert((i, i));
}
let pairs: Vec<(Index, Index)> = set.into_iter().collect();
let mut rows_of_col: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut cols_of_row: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut adj: Vec<Vec<Index>> = vec![Vec::new(); n];
for &(i, j) in &pairs {
rows_of_col[j as usize].push(i);
cols_of_row[i as usize].push(j);
if i != j {
rows_of_col[i as usize].push(j);
cols_of_row[j as usize].push(i);
adj[i as usize].push(j);
adj[j as usize].push(i);
}
}
let recoverable = |color: &[usize]| -> bool {
let cnt = |v: Index, c: usize| {
adj[v as usize]
.iter()
.filter(|&&w| color[w as usize] == c)
.count()
};
pairs.iter().all(|&(i, j)| {
i == j || cnt(i, color[j as usize]) == 1 || cnt(j, color[i as usize]) == 1
})
};
let cpr = FdHessianUpdater::color_cpr(n, &cols_of_row, &rows_of_col);
assert!(recoverable(&cpr), "CPR must always be directly recoverable");
let val = |i: Index, j: Index| -> Number {
1.0 + (i as Number) * 0.5 - (j as Number) * 0.25 + ((i * 7 + j) as Number).sin()
};
let mut dense = vec![vec![0.0 as Number; n]; n];
for &(i, j) in &pairs {
let v = val(i.max(j), i.min(j));
dense[i as usize][j as usize] = v;
dense[j as usize][i as usize] = v;
}
let check = |color: &[usize], name: &str| -> Result<(), String> {
let n_colors = color.iter().copied().max().unwrap() + 1;
let mut probes = vec![vec![0.0 as Number; n]; n_colors];
for (m, &c) in color.iter().enumerate() {
for i in 0..n {
probes[c][i] += dense[i][m];
}
}
let cnt = |v: Index, c: usize| {
adj[v as usize]
.iter()
.filter(|&&w| color[w as usize] == c)
.count()
};
for &(i, j) in &pairs {
let (g, read) = if i == j {
(color[i as usize], i)
} else if cnt(i, color[j as usize]) == 1 {
(color[j as usize], i)
} else {
(color[i as usize], j)
};
let (got, want) = (probes[g][read as usize], dense[i as usize][j as usize]);
if (got - want).abs() > 1e-12 {
return Err(format!("{name}: ({i},{j}) recovered {got}, want {want}"));
}
}
Ok(())
};
check(&cpr, "cpr").expect("CPR recovery must be exact on overlapping cliques");
let star = FdHessianUpdater::color_star(n, &adj);
if check(&star, "star").is_err() {
assert!(
!recoverable(&star),
"star recovery is wrong on this pattern yet the validation \
predicate accepts it — the predicate is unsound, and \
`build_structure` would ship a silently wrong Hessian"
);
}
}
#[test]
fn a_clique_forces_its_size_in_groups_under_either_coloring() {
let n = 10usize;
let mut pairs = Vec::new();
for i in 0..n as Index {
for j in 0..=i {
pairs.push((i, j));
}
}
let mut rows_of_col: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut cols_of_row: Vec<Vec<Index>> = vec![Vec::new(); n];
let mut adj: Vec<Vec<Index>> = vec![Vec::new(); n];
for &(i, j) in &pairs {
rows_of_col[j as usize].push(i);
cols_of_row[i as usize].push(j);
if i != j {
rows_of_col[i as usize].push(j);
cols_of_row[j as usize].push(i);
adj[i as usize].push(j);
adj[j as usize].push(i);
}
}
let star = FdHessianUpdater::color_star(n, &adj);
let cpr = FdHessianUpdater::color_cpr(n, &cols_of_row, &rows_of_col);
assert_eq!(star.iter().copied().max().unwrap() + 1, n);
assert_eq!(cpr.iter().copied().max().unwrap() + 1, n);
}
}