use crate::ipopt_cq::IpoptCqHandle;
use crate::ipopt_data::IpoptDataHandle;
use pounce_common::types::Number;
use pounce_linalg::{Matrix, Vector};
use pounce_nlp::ipopt_nlp::SplitNames;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Checkpoint {
IterStart,
AfterBarrierUpdate,
AfterSearchDirection,
AfterStep,
StepRejected,
PreRestoration,
PostRestoration,
Terminated,
}
impl Checkpoint {
pub fn as_str(self) -> &'static str {
match self {
Checkpoint::IterStart => "iter_start",
Checkpoint::AfterBarrierUpdate => "after_mu",
Checkpoint::AfterSearchDirection => "after_search_dir",
Checkpoint::AfterStep => "after_step",
Checkpoint::StepRejected => "step_rejected",
Checkpoint::PreRestoration => "pre_restoration_entry",
Checkpoint::PostRestoration => "post_restoration_exit",
Checkpoint::Terminated => "terminated",
}
}
pub fn is_sub_iteration(self) -> bool {
matches!(
self,
Checkpoint::AfterBarrierUpdate
| Checkpoint::AfterSearchDirection
| Checkpoint::AfterStep
| Checkpoint::StepRejected
| Checkpoint::PreRestoration
| Checkpoint::PostRestoration
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DebugAction {
Resume,
Stop,
}
pub const BLOCK_NAMES: [&str; 8] = ["x", "s", "y_c", "y_d", "z_l", "z_u", "v_l", "v_u"];
#[derive(Clone, Debug)]
pub struct KktReport {
pub iter: i32,
pub dim: i32,
pub n_neg: i32,
pub n_pos: i32,
pub expected_neg: i32,
pub provides_inertia: bool,
pub inertia_correct: bool,
pub delta_w: Number,
pub delta_c: Number,
pub status: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResidKind {
Eq,
Ineq,
DualX,
DualS,
}
impl ResidKind {
pub fn tag(self) -> &'static str {
match self {
ResidKind::Eq => "c",
ResidKind::Ineq => "d-s",
ResidKind::DualX => "grad_x_L",
ResidKind::DualS => "grad_s_L",
}
}
pub fn is_primal(self) -> bool {
matches!(self, ResidKind::Eq | ResidKind::Ineq)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Residual {
pub kind: ResidKind,
pub index: usize,
pub value: Number,
}
pub struct DebugCtx {
data: IpoptDataHandle,
cq: Option<IpoptCqHandle>,
cp: Checkpoint,
status: Option<String>,
pending_tol: Vec<(String, Number)>,
}
pub const LIVE_TOLERANCE_OPTS: &[&str] = &[
"tol",
"dual_inf_tol",
"constr_viol_tol",
"compl_inf_tol",
"acceptable_tol",
"acceptable_dual_inf_tol",
"acceptable_constr_viol_tol",
"acceptable_compl_inf_tol",
"acceptable_obj_change_tol",
];
pub fn is_live_tolerance(name: &str) -> bool {
LIVE_TOLERANCE_OPTS.contains(&name)
}
#[derive(Clone)]
pub struct IterateSnapshot {
pub iter: i32,
pub mu: Number,
pub tau: Number,
curr: crate::iterates_vector::IteratesVector,
}
impl IterateSnapshot {
pub fn iter(&self) -> i32 {
self.iter
}
pub fn mu(&self) -> Number {
self.mu
}
pub fn tau(&self) -> Number {
self.tau
}
pub(crate) fn iterates(&self) -> &crate::iterates_vector::IteratesVector {
&self.curr
}
pub fn block(&self, name: &str) -> Option<Vec<Number>> {
let v = block_ref(&self.curr, name)?;
Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
}
}
impl DebugCtx {
pub fn new(data: IpoptDataHandle, cq: IpoptCqHandle, cp: Checkpoint) -> Self {
Self {
data,
cq: Some(cq),
cp,
status: None,
pending_tol: Vec::new(),
}
}
pub fn set_live_tolerance(&mut self, name: &str, value: Number) {
self.pending_tol.push((name.to_string(), value));
}
pub fn take_live_tolerances(&mut self) -> Vec<(String, Number)> {
std::mem::take(&mut self.pending_tol)
}
pub fn with_status(mut self, status: String) -> Self {
self.status = Some(status);
self
}
pub fn status(&self) -> Option<&str> {
self.status.as_deref()
}
#[cfg(test)]
fn new_data_only(data: IpoptDataHandle, cp: Checkpoint) -> Self {
Self {
data,
cq: None,
cp,
status: None,
pending_tol: Vec::new(),
}
}
pub fn snapshot(&self) -> Option<IterateSnapshot> {
let d = self.data.borrow();
let curr = d.curr.as_ref()?.clone();
Some(IterateSnapshot {
iter: d.iter_count,
mu: d.curr_mu,
tau: d.curr_tau,
curr,
})
}
pub fn restore(&mut self, snap: &IterateSnapshot) {
let mut d = self.data.borrow_mut();
d.set_curr(snap.curr.clone());
d.curr_mu = snap.mu;
d.curr_tau = snap.tau;
d.iter_count = snap.iter;
}
fn cq_scalar(
&self,
f: impl FnOnce(&crate::ipopt_cq::IpoptCalculatedQuantities) -> Number,
) -> Number {
match self.cq.as_ref() {
Some(cq) => f(&cq.borrow()),
None => Number::NAN,
}
}
pub fn checkpoint(&self) -> Checkpoint {
self.cp
}
pub fn iter(&self) -> i32 {
self.data.borrow().iter_count
}
pub fn mu(&self) -> Number {
self.data.borrow().curr_mu
}
pub fn objective(&self) -> Number {
self.cq_scalar(|c| c.unscaled_curr_f())
}
pub fn inf_pr(&self) -> Number {
self.cq_scalar(|c| c.curr_primal_infeasibility_max())
}
pub fn inf_du(&self) -> Number {
self.cq_scalar(|c| c.curr_dual_infeasibility_max())
}
pub fn nlp_error(&self) -> Number {
self.cq_scalar(|c| c.curr_nlp_error())
}
pub fn complementarity(&self) -> Number {
self.cq_scalar(|c| c.curr_avrg_compl())
}
pub fn bound_slack(&self, which: &str) -> Option<Vec<Number>> {
let c = self.cq.as_ref()?.borrow();
let v = match which {
"x_l" => c.curr_slack_x_l(),
"x_u" => c.curr_slack_x_u(),
"s_l" => c.curr_slack_s_l(),
"s_u" => c.curr_slack_s_u(),
_ => return None,
};
Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
}
pub fn var_bounds(&self) -> Option<(Vec<Number>, Vec<Number>)> {
let cq = self.cq.as_ref()?.borrow();
let nlp = cq.nlp().borrow();
let d = self.data.borrow();
let x = &d.curr.as_ref()?.x; let lower = expand_bound(&*nlp.px_l(), nlp.x_l(), &**x, Number::NEG_INFINITY);
let upper = expand_bound(&*nlp.px_u(), nlp.x_u(), &**x, Number::INFINITY);
Some((lower, upper))
}
pub fn constraint_residuals(&self) -> Option<Vec<Residual>> {
let cq = self.cq.as_ref()?.borrow();
let c = crate::ipopt_alg::flat_read_owned(cq.curr_c().as_ref());
let dms = crate::ipopt_alg::flat_read_owned(cq.curr_d_minus_s().as_ref());
let mut out = Vec::with_capacity(c.len() + dms.len());
out.extend(c.iter().enumerate().map(|(index, &value)| Residual {
kind: ResidKind::Eq,
index,
value,
}));
out.extend(dms.iter().enumerate().map(|(index, &value)| Residual {
kind: ResidKind::Ineq,
index,
value,
}));
Some(out)
}
pub fn dual_residuals(&self) -> Option<Vec<Residual>> {
let cq = self.cq.as_ref()?.borrow();
let gx = crate::ipopt_alg::flat_read_owned(cq.curr_grad_lag_x().as_ref());
let gs = crate::ipopt_alg::flat_read_owned(cq.curr_grad_lag_s().as_ref());
let mut out = Vec::with_capacity(gx.len() + gs.len());
out.extend(gx.iter().enumerate().map(|(index, &value)| Residual {
kind: ResidKind::DualX,
index,
value,
}));
out.extend(gs.iter().enumerate().map(|(index, &value)| Residual {
kind: ResidKind::DualS,
index,
value,
}));
Some(out)
}
pub fn split_names(&self) -> Option<SplitNames> {
let cq = self.cq.as_ref()?.borrow();
let names = cq.nlp().borrow().split_space_names();
names
}
pub fn regularization(&self) -> Number {
self.data.borrow().info_regu_x
}
pub fn ls_count(&self) -> i32 {
self.data.borrow().info_ls_count
}
pub fn alpha(&self) -> (Number, Number) {
let d = self.data.borrow();
(d.info_alpha_primal, d.info_alpha_dual)
}
pub fn kkt(&self) -> Option<KktReport> {
let d = self.data.borrow();
let k = d.kkt_debug.as_ref()?;
let curr = d.curr.as_ref();
let expected_neg = curr.map(|c| c.y_c.dim() + c.y_d.dim()).unwrap_or(0);
let n_pos = if k.n_neg >= 0 { k.dim - k.n_neg } else { -1 };
let inertia_correct = k.provides_inertia && k.n_neg == expected_neg;
Some(KktReport {
iter: k.iter,
dim: k.dim,
n_neg: k.n_neg,
n_pos,
expected_neg,
provides_inertia: k.provides_inertia,
inertia_correct,
delta_w: d.perturbations.delta_x,
delta_c: d.perturbations.delta_c,
status: k.status.clone(),
})
}
pub fn kkt_matrix(&self) -> Option<(i32, Vec<i32>, Vec<i32>, Vec<Number>)> {
self.data.borrow().kkt_debug.as_ref()?.matrix.clone()
}
pub fn rank_report(&self) -> Option<crate::debug_rank::RankReport> {
use crate::debug_rank::RankRow;
use pounce_linalg::triplet::GenTMatrix;
let cq = self.cq.as_ref()?.borrow();
let jac = cq.curr_jac_c();
let g = jac.as_any().downcast_ref::<GenTMatrix>()?;
let m = g.n_rows() as usize;
let n = g.n_cols() as usize;
if m == 0 || n == 0 {
return None;
}
let mut dense = vec![0.0; m * n];
for ((&ir, &jc), &v) in g.irows().iter().zip(g.jcols()).zip(g.values()) {
dense[(ir - 1) as usize * n + (jc - 1) as usize] += v;
}
let rows: Vec<RankRow> = (0..m)
.map(|index| RankRow {
kind: ResidKind::Eq,
index,
})
.collect();
crate::debug_rank::svd_rank(m, n, &dense, rows)
}
pub fn kkt_captured_iter(&self) -> Option<i32> {
Some(self.data.borrow().kkt_debug.as_ref()?.iter)
}
#[allow(clippy::type_complexity)]
pub fn kkt_l_factor(
&self,
) -> Option<(usize, Vec<usize>, Vec<i32>, Vec<i32>, Option<Vec<Number>>)> {
let d = self.data.borrow();
let f = d.kkt_debug.as_ref()?.l_factor.as_ref()?;
Some((
f.n,
f.perm.clone(),
f.l_irn.clone(),
f.l_jcn.clone(),
f.l_vals.clone(),
))
}
pub fn block_dims(&self) -> Vec<(&'static str, usize)> {
let d = self.data.borrow();
let Some(curr) = d.curr.as_ref() else {
return BLOCK_NAMES.iter().map(|&n| (n, 0)).collect();
};
BLOCK_NAMES
.iter()
.map(|&n| (n, block_ref(curr, n).map(|v| v.dim() as usize).unwrap_or(0)))
.collect()
}
pub fn block(&self, name: &str) -> Option<Vec<Number>> {
let d = self.data.borrow();
let curr = d.curr.as_ref()?;
let v = block_ref(curr, name)?;
Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
}
pub fn delta_block(&self, name: &str) -> Option<Vec<Number>> {
let d = self.data.borrow();
let delta = d.delta.as_ref()?;
let v = block_ref(delta, name)?;
Some(crate::ipopt_alg::flat_read_owned(v.as_ref()))
}
pub fn set_mu(&mut self, mu: Number) -> Result<(), String> {
if !mu.is_finite() || mu <= 0.0 {
return Err(format!("mu must be finite and positive, got {mu}"));
}
self.data.borrow_mut().curr_mu = mu;
Ok(())
}
pub fn set_block(&mut self, name: &str, vals: &[Number]) -> Result<(), String> {
if !BLOCK_NAMES.contains(&name) {
return Err(format!(
"unknown block `{name}` (expected one of {BLOCK_NAMES:?})"
));
}
let mut d = self.data.borrow_mut();
let curr = d.curr.as_ref().ok_or("no current iterate yet")?;
let mut m = curr.deep_copy();
let blk = block_ref_mut(&mut m, name).expect("name checked above");
let dim = blk.dim() as usize;
if vals.len() != dim {
return Err(format!(
"block `{name}` has dimension {dim}, got {} value(s)",
vals.len()
));
}
crate::ipopt_alg::flat_write_into(blk.as_mut(), vals);
let frozen = m.freeze();
d.set_curr(frozen);
Ok(())
}
pub fn set_component(&mut self, name: &str, idx: usize, val: Number) -> Result<(), String> {
let mut vals = self
.block(name)
.ok_or_else(|| format!("unknown block `{name}` or no iterate yet"))?;
if idx >= vals.len() {
return Err(format!(
"index {idx} out of range for block `{name}` (dimension {})",
vals.len()
));
}
vals[idx] = val;
self.set_block(name, &vals)
}
}
fn expand_bound(
p: &dyn Matrix,
reduced: &dyn Vector,
template: &dyn Vector,
absent: Number,
) -> Vec<Number> {
let mut ones = reduced.make_new();
ones.set(1.0);
let mut mask = template.make_new();
p.mult_vector(1.0, &*ones, 0.0, &mut *mask);
let mut vals = template.make_new();
p.mult_vector(1.0, reduced, 0.0, &mut *vals);
let mask = crate::ipopt_alg::flat_read_owned(&*mask);
let vals = crate::ipopt_alg::flat_read_owned(&*vals);
mask.iter()
.zip(vals)
.map(|(&m, v)| if m > 0.5 { v } else { absent })
.collect()
}
fn block_ref<'a>(
iv: &'a crate::iterates_vector::IteratesVector,
name: &str,
) -> Option<&'a std::rc::Rc<dyn pounce_linalg::Vector>> {
Some(match name {
"x" => &iv.x,
"s" => &iv.s,
"y_c" => &iv.y_c,
"y_d" => &iv.y_d,
"z_l" => &iv.z_l,
"z_u" => &iv.z_u,
"v_l" => &iv.v_l,
"v_u" => &iv.v_u,
_ => return None,
})
}
fn block_ref_mut<'a>(
iv: &'a mut crate::iterates_vector::IteratesVectorMut,
name: &str,
) -> Option<&'a mut Box<dyn pounce_linalg::Vector>> {
Some(match name {
"x" => &mut iv.x,
"s" => &mut iv.s,
"y_c" => &mut iv.y_c,
"y_d" => &mut iv.y_d,
"z_l" => &mut iv.z_l,
"z_u" => &mut iv.z_u,
"v_l" => &mut iv.v_l,
"v_u" => &mut iv.v_u,
_ => return None,
})
}
pub trait DebugHook {
fn at_checkpoint(&mut self, ctx: &mut DebugCtx) -> DebugAction;
fn wants_kkt_capture(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ipopt_data::IpoptData;
use crate::iterates_vector::IteratesVector;
use pounce_linalg::dense_vector::DenseVectorSpace;
use pounce_linalg::Vector;
use std::cell::RefCell;
use std::rc::Rc;
fn iv(xvals: &[f64]) -> IteratesVector {
let dense = |vals: &[f64]| {
let mut v = DenseVectorSpace::new(vals.len() as i32).make_new_dense();
v.set_values(vals);
Rc::new(v) as Rc<dyn Vector>
};
let z = |n| dense(&vec![0.0; n]);
IteratesVector::new(dense(xvals), z(1), z(1), z(1), z(2), z(2), z(1), z(1))
}
fn ctx_with(xvals: &[f64]) -> DebugCtx {
let mut data = IpoptData::new();
data.set_curr(iv(xvals));
data.curr_mu = 0.1;
let data = Rc::new(RefCell::new(data));
DebugCtx::new_data_only(data, Checkpoint::IterStart)
}
#[test]
fn reads_block_and_mu() {
let ctx = ctx_with(&[1.0, 2.0]);
assert_eq!(ctx.mu(), 0.1);
assert_eq!(ctx.block("x"), Some(vec![1.0, 2.0]));
assert_eq!(ctx.block("nope"), None);
}
#[test]
fn set_component_rebuilds_iterate_with_fresh_tag() {
let mut ctx = ctx_with(&[1.0, 2.0]);
let before = ctx
.data
.borrow()
.curr
.as_ref()
.unwrap()
.x
.as_tagged()
.get_tag();
ctx.set_component("x", 1, 9.0).unwrap();
let after = ctx
.data
.borrow()
.curr
.as_ref()
.unwrap()
.x
.as_tagged()
.get_tag();
assert_eq!(ctx.block("x"), Some(vec![1.0, 9.0]));
assert_ne!(before, after, "mutating the iterate must mint a new tag");
}
#[test]
fn set_block_dim_mismatch_is_rejected() {
let mut ctx = ctx_with(&[1.0, 2.0]);
assert!(ctx.set_block("x", &[1.0]).is_err());
assert!(ctx.set_block("x", &[1.0, 2.0, 3.0]).is_err());
assert!(ctx.set_block("x", &[3.0, 4.0]).is_ok());
assert_eq!(ctx.block("x"), Some(vec![3.0, 4.0]));
}
#[test]
fn block_names_all_resolve_in_block_ref() {
let mut ctx = ctx_with(&[1.0, 2.0]);
for name in BLOCK_NAMES {
let cur = ctx
.block(name)
.unwrap_or_else(|| panic!("block_ref does not resolve `{name}`"));
ctx.set_block(name, &cur)
.unwrap_or_else(|e| panic!("block_ref_mut does not resolve `{name}`: {e}"));
}
}
#[test]
fn residuals_are_none_without_cq() {
let ctx = ctx_with(&[1.0, 2.0]);
assert!(ctx.constraint_residuals().is_none());
assert!(ctx.dual_residuals().is_none());
}
#[test]
fn resid_kind_tags_and_primal_classification_are_stable() {
assert_eq!(ResidKind::Eq.tag(), "c");
assert_eq!(ResidKind::Ineq.tag(), "d-s");
assert_eq!(ResidKind::DualX.tag(), "grad_x_L");
assert_eq!(ResidKind::DualS.tag(), "grad_s_L");
assert!(ResidKind::Eq.is_primal());
assert!(ResidKind::Ineq.is_primal());
assert!(!ResidKind::DualX.is_primal());
assert!(!ResidKind::DualS.is_primal());
}
#[test]
fn checkpoint_as_str_is_stable() {
assert_eq!(Checkpoint::IterStart.as_str(), "iter_start");
assert_eq!(Checkpoint::AfterBarrierUpdate.as_str(), "after_mu");
assert_eq!(
Checkpoint::AfterSearchDirection.as_str(),
"after_search_dir"
);
assert_eq!(Checkpoint::AfterStep.as_str(), "after_step");
assert_eq!(Checkpoint::StepRejected.as_str(), "step_rejected");
assert_eq!(Checkpoint::PreRestoration.as_str(), "pre_restoration_entry");
assert_eq!(
Checkpoint::PostRestoration.as_str(),
"post_restoration_exit"
);
assert_eq!(Checkpoint::Terminated.as_str(), "terminated");
}
#[test]
fn snapshot_then_restore_round_trips_iterate_and_mu() {
let mut ctx = ctx_with(&[1.0, 2.0]);
let snap = ctx.snapshot().expect("snapshot");
assert_eq!(snap.iter(), 0);
ctx.set_component("x", 0, 99.0).unwrap();
ctx.set_mu(0.5).unwrap();
assert_eq!(ctx.block("x"), Some(vec![99.0, 2.0]));
assert_eq!(ctx.mu(), 0.5);
ctx.restore(&snap);
assert_eq!(ctx.block("x"), Some(vec![1.0, 2.0]));
assert_eq!(ctx.mu(), 0.1);
assert_eq!(ctx.iter(), 0);
}
#[test]
fn set_mu_rejects_nonpositive() {
let mut ctx = ctx_with(&[1.0]);
assert!(ctx.set_mu(-1.0).is_err());
assert!(ctx.set_mu(0.0).is_err());
assert!(ctx.set_mu(1e-3).is_ok());
assert_eq!(ctx.mu(), 1e-3);
}
}