use pounce_common::types::{Index, Number};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Quantity {
StartingPoint,
Objective,
ObjectiveGradient,
Constraint,
Jacobian,
}
impl Quantity {
pub fn label(self) -> &'static str {
match self {
Self::StartingPoint => "starting point x",
Self::Objective => "objective f(x)",
Self::ObjectiveGradient => "objective gradient grad f(x)",
Self::Constraint => "constraint value g(x)",
Self::Jacobian => "constraint Jacobian",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NonFinite {
pub quantity: Quantity,
pub index: usize,
pub column: Option<usize>,
pub value: Number,
}
impl NonFinite {
pub fn describe(&self) -> String {
let v = if self.value.is_nan() {
"NaN".to_string()
} else if self.value > 0.0 {
"+inf".to_string()
} else {
"-inf".to_string()
};
match (self.quantity, self.column) {
(Quantity::Objective, _) => format!("{} = {v}", self.quantity.label()),
(Quantity::Jacobian, Some(col)) => format!(
"{}[row {}, column {}] = {v}",
self.quantity.label(),
self.index,
col
),
_ => format!("{}[{}] = {v}", self.quantity.label(), self.index),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PointAudit {
pub non_finite: Vec<NonFinite>,
pub suppressed: usize,
}
impl PointAudit {
pub fn is_clean(&self) -> bool {
self.non_finite.is_empty() && self.suppressed == 0
}
pub fn total(&self) -> usize {
self.non_finite.len() + self.suppressed
}
pub fn describe(&self) -> Option<String> {
if self.is_clean() {
return None;
}
let mut parts: Vec<String> = self.non_finite.iter().map(|f| f.describe()).collect();
if self.suppressed > 0 {
parts.push(format!("and {} more", self.suppressed));
}
Some(parts.join("; "))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct PointValues<'a> {
pub x: Option<&'a [Number]>,
pub f: Option<Number>,
pub grad_f: Option<&'a [Number]>,
pub g: Option<&'a [Number]>,
pub jac: Option<(&'a [Number], &'a [Index], &'a [Index])>,
}
pub fn audit_point(values: PointValues<'_>, limit: usize) -> PointAudit {
let mut audit = PointAudit::default();
let mut push = |quantity: Quantity, index: usize, column: Option<usize>, value: Number| {
if audit.non_finite.len() < limit {
audit.non_finite.push(NonFinite {
quantity,
index,
column,
value,
});
} else {
audit.suppressed += 1;
}
};
if let Some(x) = values.x {
for (i, v) in x.iter().enumerate() {
if !v.is_finite() {
push(Quantity::StartingPoint, i, None, *v);
}
}
}
if let Some(f) = values.f
&& !f.is_finite()
{
push(Quantity::Objective, 0, None, f);
}
if let Some(grad) = values.grad_f {
for (i, v) in grad.iter().enumerate() {
if !v.is_finite() {
push(Quantity::ObjectiveGradient, i, None, *v);
}
}
}
if let Some(g) = values.g {
for (i, v) in g.iter().enumerate() {
if !v.is_finite() {
push(Quantity::Constraint, i, None, *v);
}
}
}
if let Some((vals, rows, cols)) = values.jac {
for (k, v) in vals.iter().enumerate() {
if !v.is_finite() {
let row = rows.get(k).copied().unwrap_or(0).max(0) as usize;
let col = cols.get(k).copied().unwrap_or(0).max(0) as usize;
push(Quantity::Jacobian, row, Some(col), *v);
}
}
}
audit
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct JacobianDegeneracy {
pub zero_rows: Vec<usize>,
pub zero_cols: Vec<usize>,
pub structural_rows: usize,
pub structural_cols: usize,
}
impl JacobianDegeneracy {
pub fn is_degenerate(&self) -> bool {
!self.zero_rows.is_empty() || !self.zero_cols.is_empty()
}
pub fn describe(&self, limit: usize) -> Option<String> {
if !self.is_degenerate() {
return None;
}
fn list(v: &[usize], limit: usize) -> String {
let shown: Vec<String> = v.iter().take(limit).map(|i| i.to_string()).collect();
if v.len() > limit {
format!("{}, … ({} total)", shown.join(", "), v.len())
} else {
shown.join(", ")
}
}
let mut parts: Vec<String> = Vec::new();
if !self.zero_rows.is_empty() {
parts.push(format!(
"{} of {} constraint rows have an identically zero gradient here (rows {})",
self.zero_rows.len(),
self.structural_rows,
list(&self.zero_rows, limit)
));
}
if !self.zero_cols.is_empty() {
parts.push(format!(
"{} of {} variable columns are identically zero here (variables {})",
self.zero_cols.len(),
self.structural_cols,
list(&self.zero_cols, limit)
));
}
Some(parts.join("; "))
}
}
pub fn jacobian_degeneracy(
m: usize,
n: usize,
values: &[Number],
rows: &[Index],
cols: &[Index],
tol: Number,
) -> JacobianDegeneracy {
let mut row_present = vec![false; m];
let mut col_present = vec![false; n];
let mut row_nonzero = vec![false; m];
let mut col_nonzero = vec![false; n];
for (k, v) in values.iter().enumerate() {
let (Some(&r), Some(&c)) = (rows.get(k), cols.get(k)) else {
continue;
};
if r < 0 || c < 0 {
continue;
}
let (r, c) = (r as usize, c as usize);
if r >= m || c >= n {
continue;
}
row_present[r] = true;
col_present[c] = true;
if !v.is_finite() || v.abs() > tol {
row_nonzero[r] = true;
col_nonzero[c] = true;
}
}
JacobianDegeneracy {
zero_rows: (0..m)
.filter(|&i| row_present[i] && !row_nonzero[i])
.collect(),
zero_cols: (0..n)
.filter(|&j| col_present[j] && !col_nonzero[j])
.collect(),
structural_rows: row_present.iter().filter(|p| **p).count(),
structural_cols: col_present.iter().filter(|p| **p).count(),
}
}
#[derive(Debug, Clone)]
pub struct StartPointDiagnosis {
pub audit: PointAudit,
pub jacobian: Option<JacobianDegeneracy>,
}
impl StartPointDiagnosis {
pub fn is_clean(&self) -> bool {
self.audit.is_clean() && !self.jacobian.as_ref().is_some_and(|j| j.is_degenerate())
}
}
pub fn diagnose_start_point(
tnlp: &std::rc::Rc<std::cell::RefCell<dyn crate::tnlp::TNLP>>,
limit: usize,
) -> Option<StartPointDiagnosis> {
use crate::tnlp::{IndexStyle, SparsityRequest, StartingPoint};
let info = tnlp.borrow_mut().get_nlp_info()?;
let n = info.n.max(0) as usize;
let m = info.m.max(0) as usize;
let nnz = info.nnz_jac_g.max(0) as usize;
let mut x = vec![0.0; n];
let (mut z_l, mut z_u, mut lambda) = (vec![0.0; n], vec![0.0; n], vec![0.0; m]);
if !tnlp.borrow_mut().get_starting_point(StartingPoint {
init_x: true,
x: &mut x,
init_z: false,
z_l: &mut z_l,
z_u: &mut z_u,
init_lambda: false,
lambda: &mut lambda,
}) {
return None;
}
let f = tnlp.borrow_mut().eval_f(&x, true);
let mut grad_f = vec![0.0; n];
let have_grad = tnlp.borrow_mut().eval_grad_f(&x, false, &mut grad_f);
let mut g = vec![0.0; m];
let have_g = m > 0 && tnlp.borrow_mut().eval_g(&x, false, &mut g);
let (mut rows, mut cols, mut vals) =
(vec![0 as Index; nnz], vec![0 as Index; nnz], vec![0.0; nnz]);
let have_jac = m > 0
&& tnlp.borrow_mut().eval_jac_g(
None,
false,
SparsityRequest::Structure {
irow: &mut rows,
jcol: &mut cols,
},
)
&& tnlp.borrow_mut().eval_jac_g(
Some(&x),
false,
SparsityRequest::Values { values: &mut vals },
);
if have_jac && info.index_style == IndexStyle::Fortran {
for idx in rows.iter_mut().chain(cols.iter_mut()) {
*idx -= 1;
}
}
let audit = audit_point(
PointValues {
x: Some(&x),
f,
grad_f: have_grad.then_some(&grad_f[..]),
g: have_g.then_some(&g[..]),
jac: have_jac.then_some((&vals[..], &rows[..], &cols[..])),
},
limit,
);
let jacobian = have_jac.then(|| jacobian_degeneracy(m, n, &vals, &rows, &cols, 0.0));
Some(StartPointDiagnosis { audit, jacobian })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_the_variable_carrying_a_nan_in_the_starting_point() {
let x = [f64::NAN, 1.0, f64::NAN, 0.0];
let audit = audit_point(
PointValues {
x: Some(&x),
..Default::default()
},
8,
);
assert_eq!(audit.total(), 2);
assert_eq!(audit.non_finite[0].index, 0);
assert_eq!(audit.non_finite[1].index, 2);
assert_eq!(audit.non_finite[0].quantity, Quantity::StartingPoint);
assert_eq!(audit.non_finite[0].describe(), "starting point x[0] = NaN");
}
#[test]
fn distinguishes_a_bad_start_from_a_bad_objective_at_a_good_start() {
let x = [0.0, 0.0];
let bad_obj = audit_point(
PointValues {
x: Some(&x),
f: Some(f64::NAN),
..Default::default()
},
8,
);
assert_eq!(bad_obj.non_finite.len(), 1);
assert_eq!(bad_obj.non_finite[0].quantity, Quantity::Objective);
assert_eq!(bad_obj.non_finite[0].describe(), "objective f(x) = NaN");
}
#[test]
fn reports_infinities_with_their_sign() {
let g = [f64::INFINITY, f64::NEG_INFINITY];
let audit = audit_point(
PointValues {
g: Some(&g),
..Default::default()
},
8,
);
assert!(audit.non_finite[0].describe().ends_with("= +inf"));
assert!(audit.non_finite[1].describe().ends_with("= -inf"));
}
#[test]
fn locates_a_jacobian_nonzero_by_row_and_column() {
let vals = [1.0, f64::NAN];
let rows = [0, 1];
let cols = [0, 2];
let audit = audit_point(
PointValues {
jac: Some((&vals, &rows, &cols)),
..Default::default()
},
8,
);
assert_eq!(audit.non_finite.len(), 1);
assert_eq!(audit.non_finite[0].index, 1);
assert_eq!(audit.non_finite[0].column, Some(2));
assert_eq!(
audit.non_finite[0].describe(),
"constraint Jacobian[row 1, column 2] = NaN"
);
}
#[test]
fn caps_the_report_but_counts_the_rest_exactly() {
let x = vec![f64::NAN; 1200];
let audit = audit_point(
PointValues {
x: Some(&x),
..Default::default()
},
2,
);
assert_eq!(audit.non_finite.len(), 2);
assert_eq!(audit.suppressed, 1198);
assert_eq!(audit.total(), 1200);
assert!(audit.describe().unwrap().contains("and 1198 more"));
}
#[test]
fn a_finite_point_is_clean_and_describes_as_nothing() {
let x = [1.0, 2.0];
let grad = [0.5, -0.5];
let audit = audit_point(
PointValues {
x: Some(&x),
f: Some(3.0),
grad_f: Some(&grad),
..Default::default()
},
8,
);
assert!(audit.is_clean());
assert_eq!(audit.describe(), None);
}
#[test]
fn an_identically_zero_jacobian_reports_every_row_and_column() {
let vals = [0.0, 0.0, 0.0, 0.0];
let rows = [0, 0, 1, 1];
let cols = [0, 1, 0, 1];
let d = jacobian_degeneracy(2, 2, &vals, &rows, &cols, 0.0);
assert!(d.is_degenerate());
assert_eq!(d.zero_rows, vec![0, 1]);
assert_eq!(d.zero_cols, vec![0, 1]);
assert_eq!(d.structural_rows, 2);
assert_eq!(d.structural_cols, 2);
}
#[test]
fn a_squared_slack_at_zero_shows_up_as_a_zero_column() {
let vals = [1.0, 0.0];
let rows = [0, 0];
let cols = [0, 1];
let d = jacobian_degeneracy(1, 2, &vals, &rows, &cols, 0.0);
assert!(d.is_degenerate());
assert!(d.zero_rows.is_empty());
assert_eq!(d.zero_cols, vec![1]);
}
#[test]
fn a_structurally_absent_column_is_not_a_finding() {
let vals = [1.0, 1.0];
let rows = [0, 1];
let cols = [0, 1];
let d = jacobian_degeneracy(2, 3, &vals, &rows, &cols, 0.0);
assert!(!d.is_degenerate());
assert_eq!(d.structural_cols, 2);
}
#[test]
fn a_non_finite_entry_is_not_counted_as_a_zero() {
let vals = [f64::NAN];
let rows = [0];
let cols = [0];
let d = jacobian_degeneracy(1, 1, &vals, &rows, &cols, 0.0);
assert!(!d.is_degenerate());
}
#[test]
fn out_of_range_and_negative_indices_are_skipped_not_panicked_on() {
let vals = [1.0, 2.0, 3.0];
let rows = [0, 9, -1];
let cols = [0, 0, 0];
let d = jacobian_degeneracy(1, 1, &vals, &rows, &cols, 0.0);
assert!(!d.is_degenerate());
assert_eq!(d.structural_rows, 1);
}
#[test]
fn the_tolerance_is_absolute_and_zero_by_default_semantics() {
let vals = [1e-14];
let rows = [0];
let cols = [0];
assert!(!jacobian_degeneracy(1, 1, &vals, &rows, &cols, 0.0).is_degenerate());
assert!(jacobian_degeneracy(1, 1, &vals, &rows, &cols, 1e-12).is_degenerate());
}
#[test]
fn the_row_and_column_lists_are_truncated_with_a_total() {
let m = 50;
let vals = vec![0.0; m];
let rows: Vec<Index> = (0..m as Index).collect();
let cols = vec![0; m];
let d = jacobian_degeneracy(m, 1, &vals, &rows, &cols, 0.0);
let text = d.describe(3).unwrap();
assert!(text.contains("(50 total)"), "{text}");
assert!(text.contains("50 of 50 constraint rows"), "{text}");
}
}