use super::helpers::{apply_fixed_variable, col_has_structural_q, skip_step};
use super::state::{QpPostsolveStep, QpPresolveResult, Workspace};
use crate::qp::QpProblem;
use crate::tolerances::ZERO_TOL;
pub(super) fn step1_fix_var(prob: &QpProblem, ws: &mut Workspace) -> Result<(), QpPresolveResult> {
if skip_step(1) {
return Ok(());
}
let n = prob.num_vars;
for j in 0..n {
if ws.removed_cols[j] {
continue;
}
let (lb, ub) = ws.bounds[j];
if lb > ub + ZERO_TOL {
return Err(QpPresolveResult::infeasible(prob));
}
if (lb - ub).abs() < ZERO_TOL {
let val = lb;
const LARGE_B_THRESHOLD: f64 = 1e5;
let max_b_change: f64 = {
let col_start = prob.a.col_ptr[j];
let col_end = prob.a.col_ptr[j + 1];
(col_start..col_end)
.filter(|&k| !ws.removed_rows[prob.a.row_ind[k]])
.map(|k| (prob.a.values[k] * val).abs())
.fold(0.0f64, f64::max)
};
if max_b_change > LARGE_B_THRESHOLD {
continue;
}
apply_fixed_variable(j, val, prob, ws);
ws.removed_cols[j] = true;
ws.postsolve_stack
.push(QpPostsolveStep::FixedVar { idx: j, val });
}
}
Ok(())
}
pub(super) fn step2_singleton_row(
prob: &QpProblem,
ws: &mut Workspace,
) -> Result<(), QpPresolveResult> {
if skip_step(2) {
return Ok(());
}
let m = prob.num_constraints;
for i in 0..m {
if ws.removed_rows[i] {
continue;
}
let active: Vec<(usize, f64)> = ws.row_entries[i]
.iter()
.filter(|&&(j, _)| !ws.removed_cols[j])
.copied()
.collect();
if active.len() != 1 {
continue;
}
let (j, a_ij) = active[0];
if a_ij.abs() < ZERO_TOL {
continue;
}
if prob.constraint_types[i] == crate::problem::ConstraintType::Eq {
let val = ws.b[i] / a_ij;
let (lb, ub) = ws.bounds[j];
if val >= lb - ZERO_TOL && val <= ub + ZERO_TOL {
let val = val.clamp(lb, ub);
apply_fixed_variable(j, val, prob, ws);
ws.removed_cols[j] = true;
ws.removed_rows[i] = true;
ws.postsolve_stack.push(QpPostsolveStep::SingletonRow {
row: i,
col: j,
val,
});
}
continue;
}
let val_raw = ws.b[i] / a_ij;
let (lb, ub) = ws.bounds[j];
let val = val_raw.clamp(lb, ub);
if (val - lb).abs() < ZERO_TOL && (val - ub).abs() < ZERO_TOL {
apply_fixed_variable(j, val, prob, ws);
ws.removed_cols[j] = true;
ws.removed_rows[i] = true;
ws.postsolve_stack.push(QpPostsolveStep::SingletonRow {
row: i,
col: j,
val,
});
}
}
Ok(())
}
pub(super) fn step3_singleton_col(
prob: &QpProblem,
ws: &mut Workspace,
deadline: Option<std::time::Instant>,
) -> Result<(), QpPresolveResult> {
if skip_step(3) {
return Ok(());
}
let n = prob.num_vars;
let m = prob.num_constraints;
for j in 0..n {
if deadline.is_some_and(|d| std::time::Instant::now() >= d) {
return Ok(());
}
if ws.removed_cols[j] {
continue;
}
if col_has_structural_q(&prob.q, j) {
continue;
}
let active_rows: Vec<usize> = (0..m)
.filter(|&i| {
!ws.removed_rows[i]
&& ws.row_entries[i]
.iter()
.any(|&(jj, v)| jj == j && v.abs() > ZERO_TOL)
})
.collect();
if active_rows.len() != 1 {
continue;
}
let i = active_rows[0];
if prob.constraint_types[i] != crate::problem::ConstraintType::Le {
continue;
}
let a_ij = ws.row_entries[i]
.iter()
.find(|&&(jj, _)| jj == j)
.map(|&(_, v)| v)
.unwrap_or(0.0);
if a_ij.abs() < ZERO_TOL {
continue;
}
let (lb, ub) = ws.bounds[j];
let val = if ws.c[j] > ZERO_TOL && a_ij > ZERO_TOL {
if lb == f64::NEG_INFINITY {
0.0
} else {
lb
}
} else if ws.c[j] < -ZERO_TOL && a_ij < -ZERO_TOL {
if ub == f64::INFINITY {
0.0
} else {
ub
}
} else if ws.c[j].abs() <= ZERO_TOL {
if a_ij > ZERO_TOL {
if lb == f64::NEG_INFINITY {
0.0
} else {
lb
}
} else if ub == f64::INFINITY {
0.0
} else {
ub
}
} else {
continue;
};
apply_fixed_variable(j, val, prob, ws);
ws.removed_cols[j] = true;
ws.postsolve_stack
.push(QpPostsolveStep::FixedVar { idx: j, val });
}
Ok(())
}
pub(super) fn step4_empty(prob: &QpProblem, ws: &mut Workspace) -> Result<(), QpPresolveResult> {
if skip_step(4) {
return Ok(());
}
let n = prob.num_vars;
let m = prob.num_constraints;
for i in 0..m {
if ws.removed_rows[i] {
continue;
}
let active_count = ws.row_entries[i]
.iter()
.filter(|&&(j, _)| !ws.removed_cols[j])
.count();
if active_count == 0 {
let infeasible = match prob.constraint_types[i] {
crate::problem::ConstraintType::Le => ws.b[i] < -ZERO_TOL,
crate::problem::ConstraintType::Ge => ws.b[i] > ZERO_TOL,
crate::problem::ConstraintType::Eq => ws.b[i].abs() > ZERO_TOL,
};
if infeasible {
return Err(QpPresolveResult::infeasible(prob));
}
ws.removed_rows[i] = true;
}
}
for j in 0..n {
if ws.removed_cols[j] {
continue;
}
let a_nnz = {
let start = prob.a.col_ptr[j];
let end = prob.a.col_ptr[j + 1];
(start..end)
.filter(|&k| {
let row = prob.a.row_ind[k];
!ws.removed_rows[row] && prob.a.values[k].abs() > ZERO_TOL
})
.count()
};
if a_nnz > 0 {
continue;
}
if col_has_structural_q(&prob.q, j) {
continue;
}
let (lb, ub) = ws.bounds[j];
let cj = ws.c[j];
if cj > ZERO_TOL && !lb.is_finite() {
return Err(QpPresolveResult::unbounded(prob));
}
if cj < -ZERO_TOL && !ub.is_finite() {
return Err(QpPresolveResult::unbounded(prob));
}
let val = if cj > ZERO_TOL {
lb
} else if cj < -ZERO_TOL {
ub
} else if lb.is_finite() {
lb
} else if ub.is_finite() {
ub
} else {
0.0
};
ws.obj_offset += cj * val;
ws.removed_cols[j] = true;
ws.postsolve_stack
.push(QpPostsolveStep::EmptyCol { idx: j, val });
}
Ok(())
}