use std::rc::Rc;
use pounce_common::types::{Index, Number};
use pounce_linalg::Matrix;
use pounce_linalg::dense_vector::{DenseVector, DenseVectorSpace};
use pounce_linalg::expansion_matrix::ExpansionMatrix;
use pounce_linalg::triplet::{GenTMatrix, SymTMatrix};
use crate::PdSensBacksolver;
use crate::vec_util::dense_to_vec;
pub const UNBOUNDED: i8 = -1;
pub const INACTIVE: i8 = 0;
pub const WEAKLY_ACTIVE: i8 = 1;
pub const STRONGLY_ACTIVE: i8 = 2;
pub const AMBIGUOUS: i8 = 3;
pub const UNIDENTIFIED: i8 = 4;
pub const FIXED: i8 = 5;
pub const EQUALITY: i8 = 6;
pub struct ActivityReport {
pub mu: Number,
pub var_status: Vec<i8>,
pub var_ratio: Vec<Number>,
pub var_q_sign: Vec<i8>,
pub var_off_central_path: Vec<bool>,
pub var_contaminated: Vec<bool>,
pub var_sigma: Vec<Number>,
pub row_status: Vec<i8>,
pub row_ratio: Vec<Number>,
pub row_q_sign: Vec<i8>,
pub row_off_central_path: Vec<bool>,
pub row_contaminated: Vec<bool>,
pub row_sigma: Vec<Number>,
}
fn classify(r: Number, mu: Number) -> i8 {
if mu > 1e-4 {
if r < 1e-1 {
INACTIVE
} else if r > 1e1 {
STRONGLY_ACTIVE
} else {
AMBIGUOUS
}
} else if r < mu.sqrt() {
INACTIVE
} else if r > 1.0 / mu.sqrt() {
STRONGLY_ACTIVE
} else if (1e-1..=1e1).contains(&r) {
WEAKLY_ACTIVE
} else {
AMBIGUOUS
}
}
fn sign_of(x: Number) -> i8 {
if x > 0.0 {
1
} else if x < 0.0 {
-1
} else {
0
}
}
fn expand(compressed: &[Number], px: &Rc<dyn Matrix>, n: usize) -> Vec<Number> {
let em = px
.as_any()
.downcast_ref::<ExpansionMatrix>()
.expect("bound projection is an ExpansionMatrix (orig_ipopt_nlp builds no other kind)");
let idx = em.expanded_pos_indices();
assert_eq!(
idx.len(),
compressed.len(),
"compressed bound vector length disagrees with its expansion",
);
let mut full = vec![0.0; n];
for (k, &pos) in idx.iter().enumerate() {
full[pos as usize] = compressed[k];
}
full
}
fn present(px: &Rc<dyn Matrix>, n: usize) -> Vec<bool> {
let em = px
.as_any()
.downcast_ref::<ExpansionMatrix>()
.expect("bound projection is an ExpansionMatrix (orig_ipopt_nlp builds no other kind)");
let mut mask = vec![false; n];
for &pos in em.expanded_pos_indices() {
mask[pos as usize] = true;
}
mask
}
fn hessian_diagonal(hess: &Rc<dyn pounce_linalg::SymMatrix>, n: usize) -> Vec<Number> {
let mut diag = vec![0.0; n];
if let Some(t) = hess.as_any().downcast_ref::<SymTMatrix>() {
for ((&i, &j), &v) in t.irows().iter().zip(t.jcols()).zip(t.values()) {
if i == j {
diag[(i - 1) as usize] += v;
}
}
return diag;
}
let space = DenseVectorSpace::new(n as i32);
let mut e = DenseVector::new(space.clone());
let mut he = DenseVector::new(space);
for (i, d) in diag.iter_mut().enumerate() {
e.values_mut().fill(0.0);
e.values_mut()[i] = 1.0;
he.values_mut().fill(0.0);
hess.mult_vector(1.0, &e, 0.0, &mut he);
*d = he.values_mut()[i];
}
diag
}
fn zero_gradient_row(sigma: Number, floor: Number) -> Entry {
Entry {
status: UNIDENTIFIED,
ratio: sigma / floor,
q_sign: 0,
off_path: false,
contaminated: false,
sigma,
}
}
fn off_path(s: Number, z: Number, mu: Number) -> bool {
let comp = s * z;
comp > 10.0 * mu || comp < 0.1 * mu
}
fn contaminated(status: i8, r: Number, mu: Number) -> bool {
status == INACTIVE && r > 100.0 * mu
}
#[derive(Clone, Copy)]
struct Entry {
status: i8,
ratio: Number,
q_sign: i8,
off_path: bool,
contaminated: bool,
sigma: Number,
}
const NOT_CLASSIFIED: Entry = Entry {
status: UNBOUNDED,
ratio: Number::NAN,
q_sign: 0,
off_path: false,
contaminated: false,
sigma: 0.0,
};
fn classify_entry(sigma: Number, q_signed: Number, floor: Number, mu: Number) -> Entry {
let q_sign = sign_of(q_signed);
let q = q_signed.abs();
if q < floor {
return Entry {
status: UNIDENTIFIED,
ratio: sigma / floor,
q_sign,
off_path: false,
contaminated: false,
sigma,
};
}
let r = sigma / q;
let status = classify(r, mu);
Entry {
status,
ratio: r,
q_sign,
off_path: false,
contaminated: contaminated(status, r, mu),
sigma,
}
}
pub(crate) fn compute(bs: &PdSensBacksolver) -> ActivityReport {
let (data, cq, nlp) = bs.activity_handles();
let (mu, mult_z_l, mult_z_u, mult_v_l, mult_v_u, n, m_d) = {
let d = data.borrow();
let curr = d.curr.as_ref().expect("converged state has an iterate");
(
d.curr_mu,
Rc::clone(&curr.z_l),
Rc::clone(&curr.z_u),
Rc::clone(&curr.v_l),
Rc::clone(&curr.v_u),
curr.x.dim() as usize,
curr.s.dim() as usize,
)
};
let (px_l, px_u, pd_l, pd_u, obj_scale, d_scale) = {
let nl = nlp.borrow();
(
nl.px_l(),
nl.px_u(),
nl.pd_l(),
nl.pd_u(),
nl.obj_scaling_factor(),
nl.d_scale_vec(),
)
};
let cq = cq.borrow();
let d_var = bs.variable_scaling();
let dv = |i: usize| -> Number { d_var.map_or(1.0, |d| d[i]) };
let has_l = present(&px_l, n);
let has_u = present(&px_u, n);
let z_l = expand(&dense_to_vec(mult_z_l.as_ref()), &px_l, n);
let z_u = expand(&dense_to_vec(mult_z_u.as_ref()), &px_u, n);
let s_l = expand(&dense_to_vec(cq.curr_slack_x_l().as_ref()), &px_l, n);
let s_u = expand(&dense_to_vec(cq.curr_slack_x_u().as_ref()), &px_u, n);
let sigma_x: Vec<Number> = dense_to_vec(cq.curr_sigma_x().as_ref())
.iter()
.enumerate()
.map(|(i, &s)| s * dv(i) * dv(i))
.collect();
let hess = cq.curr_exact_hessian();
let diag: Vec<Number> = hessian_diagonal(&hess, n)
.iter()
.enumerate()
.map(|(i, &h)| h * dv(i) * dv(i))
.collect();
let max_abs_diag = diag.iter().fold(0.0, |a: Number, d| a.max(d.abs()));
let floor = Number::EPSILON.sqrt() * max_abs_diag.max(1.0);
let mut vars = vec![NOT_CLASSIFIED; n];
for i in 0..n {
if !(has_l[i] || has_u[i]) {
continue;
}
let mut e = classify_entry(sigma_x[i], diag[i], floor, mu);
e.off_path = (has_l[i] && off_path(s_l[i], z_l[i], mu))
|| (has_u[i] && off_path(s_u[i], z_u[i], mu));
e.sigma /= obj_scale;
vars[i] = e;
}
let rhas_l = present(&pd_l, m_d);
let rhas_u = present(&pd_u, m_d);
let v_l = expand(&dense_to_vec(mult_v_l.as_ref()), &pd_l, m_d);
let v_u = expand(&dense_to_vec(mult_v_u.as_ref()), &pd_u, m_d);
let rs_l = expand(&dense_to_vec(cq.curr_slack_s_l().as_ref()), &pd_l, m_d);
let rs_u = expand(&dense_to_vec(cq.curr_slack_s_u().as_ref()), &pd_u, m_d);
let sigma_s = dense_to_vec(cq.curr_sigma_s().as_ref());
let jac_d = cq.curr_jac_d();
let mut rows = vec![NOT_CLASSIFIED; m_d];
let fast = match (
jac_d.as_any().downcast_ref::<GenTMatrix>(),
hess.as_any().downcast_ref::<SymTMatrix>(),
) {
(Some(jt), Some(ht)) => {
let mut support: Vec<Vec<(usize, Number)>> = vec![Vec::new(); m_d];
for ((&r, &c), &v) in jt.irows().iter().zip(jt.jcols()).zip(jt.values()) {
let col = (c - 1) as usize;
support[(r - 1) as usize].push((col, v * dv(col)));
}
for sup in &mut support {
sup.sort_unstable_by_key(|&(c, _)| c);
sup.dedup_by(|a, b| {
if a.0 == b.0 {
b.1 += a.1;
true
} else {
false
}
});
}
let mut adj: Vec<Vec<(usize, Number)>> = vec![Vec::new(); n];
for ((&i, &l), &v) in ht.irows().iter().zip(ht.jcols()).zip(ht.values()) {
let (a, b) = ((i - 1) as usize, (l - 1) as usize);
let v = v * dv(a) * dv(b);
adj[a].push((b, v));
if a != b {
adj[b].push((a, v));
}
}
let mut scratch = vec![0.0; n];
for j in 0..m_d {
if !(rhas_l[j] || rhas_u[j]) {
continue;
}
let sup = &support[j];
let norm2: Number = sup.iter().map(|&(_, g)| g * g).sum();
rows[j] = if norm2 <= 0.0 {
zero_gradient_row(sigma_s[j], floor)
} else {
for &(k, g) in sup {
scratch[k] = g;
}
let mut ghg = 0.0;
for &(k, gk) in sup {
let mut acc = 0.0;
for &(l, v) in &adj[k] {
acc += v * scratch[l];
}
ghg += gk * acc;
}
for &(k, _) in sup {
scratch[k] = 0.0;
}
let mut e = classify_entry(sigma_s[j] * norm2, ghg / norm2, floor, mu);
e.sigma = sigma_s[j];
e
};
}
true
}
_ => false,
};
if !fast {
let mspace = DenseVectorSpace::new(m_d as i32);
let mut e_row = DenseVector::new(mspace);
let nspace = DenseVectorSpace::new(n as i32);
let mut grad = DenseVector::new(nspace.clone());
let mut hgrad = DenseVector::new(nspace);
for j in 0..m_d {
if !(rhas_l[j] || rhas_u[j]) {
continue;
}
e_row.values_mut().fill(0.0);
e_row.values_mut()[j] = 1.0;
grad.values_mut().fill(0.0);
jac_d.trans_mult_vector(1.0, &e_row, 0.0, &mut grad);
let norm2: Number = grad
.values_mut()
.iter()
.enumerate()
.map(|(i, g)| (*g * dv(i)) * (*g * dv(i)))
.sum();
rows[j] = if norm2 <= 0.0 {
zero_gradient_row(sigma_s[j], floor)
} else {
for (i, g) in grad.values_mut().iter_mut().enumerate() {
*g *= dv(i) * dv(i);
}
hgrad.values_mut().fill(0.0);
hess.mult_vector(1.0, &grad, 0.0, &mut hgrad);
let ghg: Number = {
let h = hgrad.values_mut();
grad.values_mut()
.iter()
.zip(h.iter())
.map(|(g, h)| g * h)
.sum()
};
let mut e = classify_entry(sigma_s[j] * norm2, ghg / norm2, floor, mu);
e.sigma = sigma_s[j];
e
};
}
}
for j in 0..m_d {
if !(rhas_l[j] || rhas_u[j]) {
continue;
}
rows[j].off_path = (rhas_l[j] && off_path(rs_l[j], v_l[j], mu))
|| (rhas_u[j] && off_path(rs_u[j], v_u[j], mu));
let dg = d_scale.as_ref().map_or(1.0, |v| v[j]);
rows[j].sigma *= dg * dg / obj_scale;
}
let nl = nlp.borrow();
let n_full_x = nl.n_full_x() as usize;
let n_full_g = nl.n_full_g() as usize;
let fixed_entry = Entry {
status: FIXED,
..NOT_CLASSIFIED
};
let mut var_full = vec![fixed_entry; n_full_x];
for (i, e) in vars.iter().enumerate() {
var_full[nl.var_x_to_full_x(i as Index) as usize] = *e;
}
let equality_entry = Entry {
status: EQUALITY,
..NOT_CLASSIFIED
};
let mut row_full = vec![equality_entry; n_full_g];
let mut d_pos = 0usize;
for (full_idx, slot) in row_full.iter_mut().enumerate() {
if nl.full_g_to_c_block(full_idx as Index).is_none() {
*slot = rows[d_pos];
d_pos += 1;
}
}
assert_eq!(d_pos, m_d, "inequality count disagrees with the c/d split");
ActivityReport {
mu,
var_status: var_full.iter().map(|e| e.status).collect(),
var_ratio: var_full.iter().map(|e| e.ratio).collect(),
var_q_sign: var_full.iter().map(|e| e.q_sign).collect(),
var_off_central_path: var_full.iter().map(|e| e.off_path).collect(),
var_contaminated: var_full.iter().map(|e| e.contaminated).collect(),
var_sigma: var_full.iter().map(|e| e.sigma).collect(),
row_status: row_full.iter().map(|e| e.status).collect(),
row_ratio: row_full.iter().map(|e| e.ratio).collect(),
row_q_sign: row_full.iter().map(|e| e.q_sign).collect(),
row_off_central_path: row_full.iter().map(|e| e.off_path).collect(),
row_contaminated: row_full.iter().map(|e| e.contaminated).collect(),
row_sigma: row_full.iter().map(|e| e.sigma).collect(),
}
}
pub(crate) fn row_normal(bs: &PdSensBacksolver, user_row: usize) -> Result<Vec<Number>, usize> {
let (data, cq, nlp) = bs.activity_handles();
let n = {
let d = data.borrow();
d.curr
.as_ref()
.expect("converged state has an iterate")
.x
.dim() as usize
};
let c_pos = {
let nl = nlp.borrow();
if user_row >= nl.n_full_g() as usize {
return Err(nl.n_full_g() as usize);
}
nl.full_g_to_c_block(user_row as Index)
};
let block_pos = match c_pos {
Some(p) => p as usize,
None => {
let nl = nlp.borrow();
(0..user_row)
.filter(|&g| nl.full_g_to_c_block(g as Index).is_none())
.count()
}
};
let row_scale = {
let nl = nlp.borrow();
let sv = if c_pos.is_some() {
nl.c_scale_vec()
} else {
nl.d_scale_vec()
};
sv.map_or(1.0, |v| v[block_pos])
};
let cq = cq.borrow();
let jac = if c_pos.is_some() {
cq.curr_jac_c()
} else {
cq.curr_jac_d()
};
let m_block = jac.n_rows() as usize;
let mspace = DenseVectorSpace::new(m_block as i32);
let mut e_row = DenseVector::new(mspace);
let nspace = DenseVectorSpace::new(n as i32);
let mut grad = DenseVector::new(nspace);
e_row.values_mut().fill(0.0);
e_row.values_mut()[block_pos] = 1.0;
grad.values_mut().fill(0.0);
jac.trans_mult_vector(1.0, &e_row, 0.0, &mut grad);
let d_var = bs.variable_scaling();
let nl = nlp.borrow();
let n_full_x = nl.n_full_x() as usize;
let mut full = vec![0.0; n_full_x];
let g = grad.values_mut();
for (i, slot) in g.iter().enumerate() {
let dx = d_var.map_or(1.0, |d| d[i]);
full[nl.var_x_to_full_x(i as Index) as usize] = *slot * dx / row_scale;
}
Ok(full)
}
pub(crate) fn hessian_vec(bs: &PdSensBacksolver, v_full: &[Number]) -> Result<Vec<Number>, usize> {
let (data, cq, nlp) = bs.activity_handles();
let n = {
let d = data.borrow();
d.curr
.as_ref()
.expect("converged state has an iterate")
.x
.dim() as usize
};
let (n_full_x, obj_scale) = {
let nl = nlp.borrow();
(nl.n_full_x() as usize, nl.obj_scaling_factor())
};
if v_full.len() != n_full_x {
return Err(n_full_x);
}
let d_var = bs.variable_scaling();
let nspace = DenseVectorSpace::new(n as i32);
let mut v_int = DenseVector::new(nspace.clone());
let mut hv = DenseVector::new(nspace);
{
let nl = nlp.borrow();
let vals = v_int.values_mut();
vals.fill(0.0);
for i in 0..n {
let dx = d_var.map_or(1.0, |d| d[i]);
vals[i] = v_full[nl.var_x_to_full_x(i as Index) as usize] * dx;
}
}
let hess = {
let cq = cq.borrow();
cq.curr_exact_hessian()
};
hess.mult_vector(1.0, &v_int, 0.0, &mut hv);
let nl = nlp.borrow();
let mut out = vec![0.0; n_full_x];
let h = hv.values_mut();
for (i, slot) in h.iter().enumerate() {
let dx = d_var.map_or(1.0, |d| d[i]);
out[nl.var_x_to_full_x(i as Index) as usize] = *slot * dx / obj_scale;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tight_mu_walks_all_five_regions() {
let mu = 1e-10; assert_eq!(classify(0.9e-5, mu), INACTIVE);
assert_eq!(classify(1.1e-5, mu), AMBIGUOUS); assert_eq!(classify(0.5, mu), WEAKLY_ACTIVE);
assert_eq!(classify(50.0, mu), AMBIGUOUS); assert_eq!(classify(2e5, mu), STRONGLY_ACTIVE);
}
#[test]
fn band_edges_are_inclusive_and_mu_edges_separate() {
let mu = 1e-10;
assert_eq!(classify(1e-1, mu), WEAKLY_ACTIVE);
assert_eq!(classify(1e1, mu), WEAKLY_ACTIVE);
assert_eq!(classify(0.99e-5, mu), INACTIVE);
assert_eq!(classify(1.01e-5, mu), AMBIGUOUS);
assert_eq!(classify(0.99e5, mu), AMBIGUOUS);
assert_eq!(classify(1.01e5, mu), STRONGLY_ACTIVE);
}
#[test]
fn loose_mu_refuses_the_weak_call() {
for mu in [1e-3, 1e-2, 1e-1] {
assert_eq!(classify(0.05, mu), INACTIVE);
assert_eq!(classify(1.0, mu), AMBIGUOUS);
assert_eq!(classify(50.0, mu), STRONGLY_ACTIVE);
}
assert_eq!(classify(1.0, 1e-4), WEAKLY_ACTIVE);
}
#[test]
fn off_path_is_a_factor_of_ten_both_ways() {
let mu = 1e-2;
assert!(!off_path(1.0, 1e-2, mu)); assert!(!off_path(0.5, 1e-2, mu)); assert!(off_path(1.0, 0.2, mu)); assert!(off_path(1.0, 5e-4, mu)); }
#[test]
fn contamination_is_mu_relative_and_inactive_only() {
let mu = 1e-10; assert!(contaminated(INACTIVE, 1e-6, mu));
assert!(!contaminated(INACTIVE, 5e-9, mu));
assert!(!contaminated(WEAKLY_ACTIVE, 1.0, mu));
assert!(!contaminated(STRONGLY_ACTIVE, 1e5, mu));
assert!(100.0 * mu < mu.sqrt());
}
#[test]
fn below_floor_reports_unidentified_with_the_sign() {
let e = classify_entry(0.5, 1e-12, 1e-8, 1e-10);
assert_eq!(e.status, UNIDENTIFIED);
assert_eq!(e.q_sign, 1);
let e = classify_entry(0.5, -1e-12, 1e-8, 1e-10);
assert_eq!(e.status, UNIDENTIFIED);
assert_eq!(e.q_sign, -1);
let e = classify_entry(1.0, -2.0, 1e-8, 1e-10);
assert_eq!(e.status, WEAKLY_ACTIVE);
assert_eq!(e.q_sign, -1);
assert_eq!(e.ratio, 0.5);
}
}