use crate::problem::ConstraintType;
use crate::qp::problem::QpProblem;
use crate::sparse::CscMatrix;
#[inline]
#[allow(clippy::needless_range_loop)]
pub(crate) fn spmv(a: &CscMatrix, x: &[f64], out: &mut [f64]) {
out.iter_mut().for_each(|v| *v = 0.0);
for col in 0..a.ncols {
let xv = x[col];
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
out[a.row_ind[k]] += a.values[k] * xv;
}
}
}
#[inline]
#[allow(clippy::needless_range_loop)]
pub(crate) fn spmtv(a: &CscMatrix, v: &[f64], out: &mut [f64]) {
out.iter_mut().for_each(|o| *o = 0.0);
for col in 0..a.ncols {
let mut s = 0.0;
for k in a.col_ptr[col]..a.col_ptr[col + 1] {
s += a.values[k] * v[a.row_ind[k]];
}
out[col] = s;
}
}
#[inline]
pub(crate) fn norm_inf(v: &[f64]) -> f64 {
v.iter().fold(0.0_f64, |a, &x| a.max(x.abs()))
}
pub(crate) fn build_extended_constraints(
problem: &QpProblem,
) -> (CscMatrix, Vec<f64>, usize, usize, usize, Vec<bool>) {
let n = problem.num_vars;
let m = problem.num_constraints;
let n_lb: usize = problem
.bounds
.iter()
.filter(|&&(lb, _)| lb.is_finite())
.count();
let n_ub: usize = problem
.bounds
.iter()
.filter(|&&(_, ub)| ub.is_finite())
.count();
let m_ext = m + n_lb + n_ub;
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f64> = Vec::new();
let mut b_ext = Vec::with_capacity(m_ext);
let mut is_eq_ext = Vec::with_capacity(m_ext);
for col in 0..n {
for k in problem.a.col_ptr[col]..problem.a.col_ptr[col + 1] {
let row = problem.a.row_ind[k];
let sign = if problem.constraint_types[row] == ConstraintType::Ge {
-1.0
} else {
1.0
};
rows.push(row);
cols.push(col);
vals.push(problem.a.values[k] * sign);
}
}
for i in 0..m {
match problem.constraint_types[i] {
ConstraintType::Eq => {
b_ext.push(problem.b[i]);
is_eq_ext.push(true);
}
ConstraintType::Le => {
b_ext.push(problem.b[i]);
is_eq_ext.push(false);
}
ConstraintType::Ge => {
b_ext.push(-problem.b[i]);
is_eq_ext.push(false);
}
}
}
let mut lb_row = m;
for (j, &(lb, _)) in problem.bounds.iter().enumerate() {
if lb.is_finite() {
rows.push(lb_row);
cols.push(j);
vals.push(-1.0);
b_ext.push(-lb);
is_eq_ext.push(false);
lb_row += 1;
}
}
let mut ub_row = m + n_lb;
for (j, &(_, ub)) in problem.bounds.iter().enumerate() {
if ub.is_finite() {
rows.push(ub_row);
cols.push(j);
vals.push(1.0);
b_ext.push(ub);
is_eq_ext.push(false);
ub_row += 1;
}
}
let a_ext = if m_ext == 0 || rows.is_empty() {
CscMatrix::new(0, n)
} else {
CscMatrix::from_triplets(&rows, &cols, &vals, m_ext, n).unwrap()
};
(a_ext, b_ext, m_ext, m, n_lb, is_eq_ext)
}
pub(crate) fn collapse_extended_dual(
dual: &[f64],
m_orig: usize,
constraint_types: &[ConstraintType],
) -> Vec<f64> {
let mut result = Vec::with_capacity(m_orig);
for i in 0..m_orig {
let d = dual[i];
if constraint_types[i] == ConstraintType::Ge {
result.push(-d);
} else {
result.push(d);
}
}
result
}
pub(crate) struct AugmentedKktCache {
col_ptr: Vec<usize>,
row_ind: Vec<usize>,
static_values: Vec<f64>,
diag_var_slot: Vec<usize>,
diag_con_slot: Vec<usize>,
n: usize,
m_ext: usize,
}
impl AugmentedKktCache {
pub(crate) fn materialize(&self, sigma_vec: &[f64], delta_p: f64, delta_d: f64) -> CscMatrix {
debug_assert_eq!(sigma_vec.len(), self.m_ext);
let mut values = self.static_values.clone();
for j in 0..self.n {
values[self.diag_var_slot[j]] += delta_p;
}
for k in 0..self.m_ext {
values[self.diag_con_slot[k]] = -(sigma_vec[k] + delta_d);
}
let total = self.n + self.m_ext;
CscMatrix {
col_ptr: self.col_ptr.clone(),
row_ind: self.row_ind.clone(),
values,
nrows: total,
ncols: total,
}
}
pub(crate) fn permute(&self, perm: &[usize]) -> PermutedAugmentedKkt {
let total = self.n + self.m_ext;
debug_assert_eq!(perm.len(), total);
let mut inv_perm = vec![0usize; total];
for (k, &i) in perm.iter().enumerate() {
inv_perm[i] = k;
}
let nnz = self.row_ind.len();
let mut perm_entries: Vec<(usize, usize, usize)> = Vec::with_capacity(nnz); for col in 0..total {
let cs = self.col_ptr[col];
let ce = self.col_ptr[col + 1];
let new_col_for_orig_col = inv_perm[col];
for s in cs..ce {
let row = self.row_ind[s];
let new_row = inv_perm[row];
let new_col = new_col_for_orig_col;
let (r, c) = if new_row <= new_col {
(new_row, new_col)
} else {
(new_col, new_row)
};
perm_entries.push((r, c, s));
}
}
perm_entries.sort_unstable_by(|a, b| a.1.cmp(&b.1).then(a.0.cmp(&b.0)));
let mut new_col_ptr = Vec::with_capacity(total + 1);
let mut new_row_ind = Vec::with_capacity(nnz);
let mut new_static_values = Vec::with_capacity(nnz);
let mut orig_slot_for_new_slot = Vec::with_capacity(nnz);
let mut new_diag_var_slot = vec![usize::MAX; self.n];
let mut new_diag_con_slot = vec![usize::MAX; self.m_ext];
new_col_ptr.push(0);
let mut cur_col = 0usize;
for &(r, c, orig_s) in &perm_entries {
while cur_col < c {
new_col_ptr.push(new_row_ind.len());
cur_col += 1;
}
let new_slot = new_row_ind.len();
new_row_ind.push(r);
new_static_values.push(self.static_values[orig_s]);
orig_slot_for_new_slot.push(orig_s);
if r == c {
let orig_idx = perm[c];
if orig_idx < self.n {
new_diag_var_slot[orig_idx] = new_slot;
} else {
new_diag_con_slot[orig_idx - self.n] = new_slot;
}
}
}
while cur_col < total {
new_col_ptr.push(new_row_ind.len());
cur_col += 1;
}
debug_assert!(new_diag_var_slot.iter().all(|&s| s != usize::MAX));
debug_assert!(new_diag_con_slot.iter().all(|&s| s != usize::MAX));
PermutedAugmentedKkt {
col_ptr: new_col_ptr,
row_ind: new_row_ind,
static_values: new_static_values,
diag_var_slot: new_diag_var_slot,
diag_con_slot: new_diag_con_slot,
n: self.n,
m_ext: self.m_ext,
}
}
}
pub(crate) struct PermutedAugmentedKkt {
col_ptr: Vec<usize>,
row_ind: Vec<usize>,
static_values: Vec<f64>,
diag_var_slot: Vec<usize>,
diag_con_slot: Vec<usize>,
n: usize,
m_ext: usize,
}
impl PermutedAugmentedKkt {
pub(crate) fn materialize(&self, sigma_vec: &[f64], delta_p: f64, delta_d: f64) -> CscMatrix {
debug_assert_eq!(sigma_vec.len(), self.m_ext);
let mut values = self.static_values.clone();
for j in 0..self.n {
values[self.diag_var_slot[j]] += delta_p;
}
for k in 0..self.m_ext {
values[self.diag_con_slot[k]] = -(sigma_vec[k] + delta_d);
}
let total = self.n + self.m_ext;
CscMatrix {
col_ptr: self.col_ptr.clone(),
row_ind: self.row_ind.clone(),
values,
nrows: total,
ncols: total,
}
}
}
pub(crate) fn compute_inertia_correction(q: &CscMatrix) -> f64 {
if q.nrows == 0 || q.values.iter().all(|&v| v == 0.0) {
return 0.0;
}
if crate::linalg::ldl::is_q_psd_by_cholesky(q) {
return 0.0;
}
crate::linalg::gershgorin::psd_shift_from_gershgorin(q)
}
#[allow(clippy::needless_range_loop)]
pub(crate) fn build_augmented_cache(q: &CscMatrix, a_ext: &CscMatrix) -> AugmentedKktCache {
let n = q.nrows;
let m_ext = a_ext.nrows;
let total = n + m_ext;
let mut col_entries: Vec<Vec<(usize, f64)>> = vec![Vec::new(); total];
let mut diag_var_present = vec![false; n];
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
if row <= col {
col_entries[col].push((row, q.values[k]));
if row == col {
diag_var_present[col] = true;
}
}
}
}
for j in 0..n {
if !diag_var_present[j] {
col_entries[j].push((j, 0.0));
}
}
for j in 0..n {
for idx in a_ext.col_ptr[j]..a_ext.col_ptr[j + 1] {
let k = a_ext.row_ind[idx];
col_entries[n + k].push((j, a_ext.values[idx]));
}
}
for k in 0..m_ext {
col_entries[n + k].push((n + k, 0.0));
}
for entries in col_entries.iter_mut() {
entries.sort_by_key(|&(r, _)| r);
}
let nnz: usize = col_entries.iter().map(|v| v.len()).sum();
let mut col_ptr = Vec::with_capacity(total + 1);
let mut row_ind = Vec::with_capacity(nnz);
let mut static_values = Vec::with_capacity(nnz);
let mut diag_var_slot = vec![usize::MAX; n];
let mut diag_con_slot = vec![usize::MAX; m_ext];
col_ptr.push(0);
for col in 0..total {
for &(row, val) in col_entries[col].iter() {
let slot = row_ind.len();
row_ind.push(row);
static_values.push(val);
if row == col {
if col < n {
diag_var_slot[col] = slot;
} else {
diag_con_slot[col - n] = slot;
}
}
}
col_ptr.push(row_ind.len());
}
debug_assert!(diag_var_slot.iter().all(|&s| s != usize::MAX));
debug_assert!(diag_con_slot.iter().all(|&s| s != usize::MAX));
AugmentedKktCache {
col_ptr,
row_ind,
static_values,
diag_var_slot,
diag_con_slot,
n,
m_ext,
}
}
#[allow(clippy::needless_range_loop)]
pub(crate) fn build_augmented_system(
q: &CscMatrix,
a_ext: &CscMatrix,
sigma_vec: &[f64],
delta_p: f64,
delta_d: f64,
) -> CscMatrix {
let n = q.nrows;
let m_ext = a_ext.nrows;
let total = n + m_ext;
let mut rows: Vec<usize> = Vec::new();
let mut cols: Vec<usize> = Vec::new();
let mut vals: Vec<f64> = Vec::new();
let mut diag_added = vec![false; n];
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
if row <= col {
let v = q.values[k] + if row == col { delta_p } else { 0.0 };
rows.push(row);
cols.push(col);
vals.push(v);
if row == col {
diag_added[col] = true;
}
}
}
}
for i in 0..n {
if !diag_added[i] {
rows.push(i);
cols.push(i);
vals.push(delta_p);
}
}
for j in 0..n {
for idx in a_ext.col_ptr[j]..a_ext.col_ptr[j + 1] {
let k = a_ext.row_ind[idx];
let v = a_ext.values[idx];
rows.push(j);
cols.push(n + k);
vals.push(v);
}
}
for k in 0..m_ext {
rows.push(n + k);
cols.push(n + k);
vals.push(-(sigma_vec[k] + delta_d));
}
if rows.is_empty() {
CscMatrix::new(total, total)
} else {
CscMatrix::from_triplets(&rows, &cols, &vals, total, total).unwrap()
}
}
pub(crate) fn build_schur_system(
q: &CscMatrix,
a_ext: &CscMatrix,
sigma_vec: &[f64],
rho_p: f64,
delta_d: f64,
) -> (CscMatrix, Vec<f64>) {
use std::collections::BTreeMap;
let n = q.nrows;
let m_ext = a_ext.nrows;
let d_inv: Vec<f64> = sigma_vec.iter().map(|&s| 1.0 / (s + delta_d)).collect();
let a_t = a_ext.transpose();
let mut acc: BTreeMap<(usize, usize), f64> = BTreeMap::new();
for k in 0..m_ext {
let start = a_t.col_ptr[k];
let end = a_t.col_ptr[k + 1];
let row_entries: Vec<(usize, f64)> = (start..end)
.map(|p| (a_t.row_ind[p], a_t.values[p]))
.collect();
let dk = d_inv[k];
for (idx_a, &(i, v_i)) in row_entries.iter().enumerate() {
for &(j, v_j) in &row_entries[idx_a..] {
let (lo, hi) = if i <= j { (i, j) } else { (j, i) };
*acc.entry((hi, lo)).or_insert(0.0) += dk * v_i * v_j;
}
}
}
for col in 0..n {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
let row = q.row_ind[k];
if row <= col {
*acc.entry((col, row)).or_insert(0.0) += q.values[k];
}
}
}
for i in 0..n {
*acc.entry((i, i)).or_insert(0.0) += rho_p;
}
let mut col_ptr = vec![0_usize; n + 1];
let mut row_ind: Vec<usize> = Vec::with_capacity(acc.len());
let mut values: Vec<f64> = Vec::with_capacity(acc.len());
for ((col, row), val) in acc {
row_ind.push(row);
values.push(val);
col_ptr[col + 1] = row_ind.len();
}
for i in 1..=n {
if col_ptr[i] < col_ptr[i - 1] {
col_ptr[i] = col_ptr[i - 1];
}
}
let s = CscMatrix {
col_ptr,
row_ind,
values,
nrows: n,
ncols: n,
};
(s, d_inv)
}
#[cfg(test)]
pub(crate) struct KktCache {
pub mat: CscMatrix,
pub part1_diag_idx: Vec<usize>,
pub q_diag_base: Vec<f64>,
pub part3_diag_idx: Vec<usize>,
pub part1_updated_idx: Vec<usize>,
}
#[cfg(test)]
pub(crate) fn collect_part1_diag_indices(aug_mat: &CscMatrix, n: usize) -> Vec<usize> {
(0..n).map(|i| aug_mat.col_ptr[i + 1] - 1).collect()
}
#[cfg(test)]
pub(crate) fn collect_part3_diag_indices(
aug_mat: &CscMatrix,
n: usize,
m_ext: usize,
) -> Vec<usize> {
(0..m_ext).map(|k| aug_mat.col_ptr[n + k + 1] - 1).collect()
}
#[cfg(test)]
pub(crate) fn collect_q_diag_base(q: &CscMatrix, n: usize) -> Vec<f64> {
let mut base = vec![0.0f64; n];
for (col, val) in base.iter_mut().enumerate().take(n) {
for k in q.col_ptr[col]..q.col_ptr[col + 1] {
if q.row_ind[k] == col {
*val = q.values[k];
break;
}
}
}
base
}
#[cfg(test)]
pub(crate) fn update_augmented_values(
cache: &mut KktCache,
sigma_vec: &[f64],
delta_p: f64,
delta_d: f64,
) {
for &i in &cache.part1_updated_idx {
let idx = cache.part1_diag_idx[i];
cache.mat.values[idx] = cache.q_diag_base[i] + delta_p;
}
for (k, &idx) in cache.part3_diag_idx.iter().enumerate() {
cache.mat.values[idx] = -(sigma_vec[k] + delta_d);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn upper_tri_csc(n: usize, entries: &[(usize, usize, f64)]) -> CscMatrix {
let rows: Vec<usize> = entries.iter().map(|&(r, _, _)| r).collect();
let cols: Vec<usize> = entries.iter().map(|&(_, c, _)| c).collect();
let vals: Vec<f64> = entries.iter().map(|&(_, _, v)| v).collect();
CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap()
}
#[test]
fn test_compute_inertia_correction_psd_no_correction() {
let q = upper_tri_csc(2, &[(0, 0, 1.0), (0, 1, 1.1), (1, 1, 2.0)]);
let delta = compute_inertia_correction(&q);
assert_eq!(delta, 0.0, "got {}", delta);
}
#[test]
fn test_compute_inertia_correction_indefinite() {
let q = upper_tri_csc(2, &[(0, 0, 1.0), (1, 1, -2.0)]);
let delta = compute_inertia_correction(&q);
assert!(delta > 0.0, "got {}", delta);
assert!((delta - 2.0).abs() < 1e-10, "expected 2.0, got {}", delta);
}
#[test]
fn test_compute_inertia_correction_zero_matrix() {
let q = upper_tri_csc(3, &[]);
assert_eq!(compute_inertia_correction(&q), 0.0);
}
#[test]
#[allow(clippy::type_complexity)]
fn sentinel_inertia_zero_diag_indefinite_multi_pattern() {
let cases: &[(&str, usize, &[(usize, usize, f64)], f64)] = &[
("pure_bilinear_2x2", 2, &[(0, 1, 1.0)], 1.0),
(
"zero_plus_negative_diag",
2,
&[(0, 1, 1.0), (1, 1, -1.0)],
2.0,
),
("zero_diag_3x3_chain", 3, &[(0, 1, 1.0), (1, 2, 1.0)], 2.0),
(
"zero_diag_4x4_extreme_offdiag",
4,
&[(0, 0, 2.0), (0, 3, 3.0)],
3.0,
),
];
for &(label, n, entries, expected_min) in cases {
let q = upper_tri_csc(n, entries);
let delta = compute_inertia_correction(&q);
assert!(
delta >= expected_min - 1e-12,
"[{label}] expected δ_ic ≥ {expected_min}, got {delta} (indefinite Q misclassified as PSD)"
);
}
}
#[test]
#[allow(clippy::type_complexity)]
fn sentinel_inertia_psd_no_over_correction() {
let psd_cases: &[(&str, usize, &[(usize, usize, f64)])] = &[
("pd_2x2_diag", 2, &[(0, 0, 1.0), (1, 1, 1.0)]),
(
"pd_2x2_offdiag",
2,
&[(0, 0, 4.0), (0, 1, 1.0), (1, 1, 3.0)],
),
(
"psd_singular_rank1",
2,
&[(0, 0, 1.0), (0, 1, 1.0), (1, 1, 1.0)],
),
("psd_with_zero_eig_3x3", 3, &[(0, 0, 1.0), (1, 1, 1.0)]),
(
"pd_large_offdiag_gershgorin_false_alarm",
2,
&[(0, 0, 1.0), (0, 1, 1.1), (1, 1, 2.0)],
),
];
for &(label, n, entries) in psd_cases {
let q = upper_tri_csc(n, entries);
let delta = compute_inertia_correction(&q);
assert_eq!(
delta, 0.0,
"[{label}] PSD matrix must not get correction, got δ_ic={delta}"
);
}
}
#[test]
fn test_collect_part1_diag_indices() {
let q = upper_tri_csc(2, &[(0, 0, 2.0), (0, 1, 0.5), (1, 1, 3.0)]);
let a_rows = vec![0usize, 1];
let a_cols = vec![0usize, 1];
let a_vals = vec![1.0f64, 1.0];
let a_ext = CscMatrix::from_triplets(&a_rows, &a_cols, &a_vals, 2, 2).unwrap();
let sigma_vec = [0.5f64, 0.8];
let aug = build_augmented_system(&q, &a_ext, &sigma_vec, 0.1, 0.05);
let n = 2;
let diag_idx = collect_part1_diag_indices(&aug, n);
for i in 0..n {
let idx = diag_idx[i];
assert_eq!(aug.row_ind[idx], i);
}
assert!((aug.values[diag_idx[0]] - 2.1).abs() < 1e-14);
assert!((aug.values[diag_idx[1]] - 3.1).abs() < 1e-14);
}
#[test]
fn test_collect_part3_diag_indices() {
let q = upper_tri_csc(2, &[(0, 0, 2.0), (0, 1, 0.5), (1, 1, 3.0)]);
let a_rows = vec![0usize, 1];
let a_cols = vec![0usize, 1];
let a_vals = vec![1.0f64, 1.0];
let a_ext = CscMatrix::from_triplets(&a_rows, &a_cols, &a_vals, 2, 2).unwrap();
let sigma_vec = [0.5f64, 0.8];
let aug = build_augmented_system(&q, &a_ext, &sigma_vec, 0.1, 0.05);
let n = 2;
let m_ext = 2;
let diag_idx = collect_part3_diag_indices(&aug, n, m_ext);
for k in 0..m_ext {
let idx = diag_idx[k];
assert_eq!(aug.row_ind[idx], n + k);
}
assert!((aug.values[diag_idx[0]] - (-0.55)).abs() < 1e-14);
assert!((aug.values[diag_idx[1]] - (-0.85)).abs() < 1e-14);
}
#[test]
fn test_spmv_basic() {
let rows = vec![0usize, 1, 0, 1];
let cols = vec![0usize, 0, 1, 1];
let vals = vec![1.0f64, 3.0, 2.0, 4.0];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 2, 2).unwrap();
let x = [1.0f64, 1.0];
let mut out = [0.0f64, 0.0];
spmv(&a, &x, &mut out);
assert!((out[0] - 3.0).abs() < 1e-14, "out[0]={}", out[0]);
assert!((out[1] - 7.0).abs() < 1e-14, "out[1]={}", out[1]);
}
#[test]
fn test_norm_inf_basic() {
assert!((norm_inf(&[3.0, -5.0, 2.0]) - 5.0).abs() < 1e-15);
assert!((norm_inf(&[0.0, 0.0]) - 0.0).abs() < 1e-15);
assert!((norm_inf(&[-7.0]) - 7.0).abs() < 1e-15);
}
#[test]
fn test_build_extended_constraints_dimensions() {
use crate::qp::problem::QpProblem;
let a = CscMatrix::from_triplets(&[0usize, 0], &[0usize, 1], &[1.0f64, 1.0], 1, 2).unwrap();
let q = CscMatrix::new(2, 2);
let prob =
QpProblem::new_all_le(q, vec![0.0; 2], a, vec![3.0], vec![(0.0, f64::INFINITY); 2])
.unwrap();
let (a_ext, b_ext, m_ext, m_orig, n_lb, is_eq_ext) = build_extended_constraints(&prob);
assert_eq!(m_orig, 1);
assert_eq!(n_lb, 2);
assert_eq!(m_ext, 3);
assert_eq!(a_ext.nrows, 3);
assert_eq!(a_ext.ncols, 2);
assert_eq!(b_ext.len(), 3);
assert_eq!(is_eq_ext, vec![false, false, false]);
}
#[test]
fn test_update_augmented_values_matches_build() {
let q = upper_tri_csc(2, &[(0, 0, 2.0), (0, 1, 0.5), (1, 1, 3.0)]);
let a_rows = vec![0usize, 1];
let a_cols = vec![0usize, 1];
let a_vals = vec![1.0f64, 1.0];
let a_ext = CscMatrix::from_triplets(&a_rows, &a_cols, &a_vals, 2, 2).unwrap();
let n = 2;
let m_ext = 2;
let sigma1 = [0.5f64, 0.8];
let aug_init = build_augmented_system(&q, &a_ext, &sigma1, 0.1, 0.05);
let part1_idx = collect_part1_diag_indices(&aug_init, n);
let part3_idx = collect_part3_diag_indices(&aug_init, n, m_ext);
let q_base = collect_q_diag_base(&q, n);
let mut cache = KktCache {
mat: aug_init,
part1_diag_idx: part1_idx,
q_diag_base: q_base,
part3_diag_idx: part3_idx,
part1_updated_idx: (0..n).collect(),
};
let sigma2 = [1.2f64, 0.3];
let dp2 = 0.02f64;
let dd2 = 0.01f64;
update_augmented_values(&mut cache, &sigma2, dp2, dd2);
let aug_ref = build_augmented_system(&q, &a_ext, &sigma2, dp2, dd2);
assert_eq!(cache.mat.values.len(), aug_ref.values.len());
for (i, (&got, &expected)) in cache
.mat
.values
.iter()
.zip(aug_ref.values.iter())
.enumerate()
{
assert!(
(got - expected).abs() < 1e-14,
"values[{i}]: got={got} expected={expected}"
);
}
}
#[test]
fn test_build_extended_constraints_bound_rows() {
use crate::qp::problem::QpProblem;
let q = CscMatrix::new(2, 2);
let a = CscMatrix::new(0, 2);
let prob = QpProblem::new_all_le(
q,
vec![0.0; 2],
a,
vec![],
vec![(0.0, 5.0), (f64::NEG_INFINITY, 10.0)],
)
.unwrap();
let (a_ext, b_ext, m_ext, m_orig, n_lb, _) = build_extended_constraints(&prob);
assert_eq!(m_orig, 0);
assert_eq!(n_lb, 1);
assert_eq!(m_ext, 3);
assert_eq!(b_ext, vec![-0.0, 5.0, 10.0]);
let mut row_entries: Vec<Vec<(usize, f64)>> = vec![vec![]; m_ext];
for col in 0..a_ext.ncols {
for k in a_ext.col_ptr[col]..a_ext.col_ptr[col + 1] {
row_entries[a_ext.row_ind[k]].push((col, a_ext.values[k]));
}
}
assert_eq!(row_entries[0], vec![(0, -1.0)]);
assert_eq!(row_entries[1], vec![(0, 1.0)]);
assert_eq!(row_entries[2], vec![(1, 1.0)]);
}
#[test]
fn test_collapse_extended_dual_ge_sign_flip() {
use crate::problem::ConstraintType as CT;
let dual_ext = vec![1.0_f64, 2.0, 3.0, 4.0];
let cts = vec![CT::Le, CT::Ge, CT::Eq];
let collapsed = collapse_extended_dual(&dual_ext, 3, &cts);
assert_eq!(collapsed, vec![1.0, -2.0, 3.0]);
}
}