use super::transforms::{PostsolveStep, PresolveResult};
use crate::options::{SolverOptions, WarmStartBasis};
use crate::problem::{ConstraintType, LpProblem, SolveStatus, SolverResult};
use crate::simplex::build_standard_form;
use crate::simplex::crash::compute_crash_basis;
use crate::sparse::CscMatrix;
use crate::tolerances::{COMP_SLACK_REL_TOL, LARGE_PROBLEM_THRESHOLD, PIVOT_TOL, ZERO_TOL};
use std::time::Instant;
const WARM_BASIS_BUILD_TOL: f64 = 1e-9;
const MARKOWITZ_PIVOT_RATIO: f64 = 0.1;
const GS_MAX_ITER: usize = 50;
const GS_CONV_TOL: f64 = 1e-12;
fn should_run_kept_perturbation(unresolved: bool, n: usize, m: usize) -> bool {
unresolved && n + m <= LARGE_PROBLEM_THRESHOLD
}
fn crossover_deadline_with_reserve(deadline: Option<Instant>, now: Instant) -> Option<Instant> {
deadline.map(|d| now + d.saturating_duration_since(now) / 2)
}
#[cfg(test)]
thread_local! {
static POSTSOLVE_PASS_TRACE: std::cell::RefCell<Vec<&'static str>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(test)]
fn trace_pass(name: &'static str) {
POSTSOLVE_PASS_TRACE.with(|t| t.borrow_mut().push(name));
}
#[cfg(not(test))]
#[inline(always)]
fn trace_pass(_: &'static str) {}
#[cfg(test)]
fn drain_postsolve_pass_trace() -> Vec<&'static str> {
POSTSOLVE_PASS_TRACE.with(|t| std::mem::take(&mut *t.borrow_mut()))
}
fn row_slack_and_scale(orig_problem: &LpProblem, i: usize, solution: &[f64]) -> (f64, f64) {
let row_entries = collect_row_entries(orig_problem, i);
let ax_i: f64 = row_entries.iter().map(|&(j, a)| a * solution[j]).sum();
let b_i = orig_problem.b[i];
let slack = match orig_problem.constraint_types[i] {
ConstraintType::Le => b_i - ax_i,
ConstraintType::Ge => ax_i - b_i,
ConstraintType::Eq => 0.0,
};
let scale = 1.0 + b_i.abs() + ax_i.abs();
(slack, scale)
}
fn is_row_nonbinding(orig_problem: &LpProblem, i: usize, solution: &[f64]) -> bool {
let (slack, scale) = row_slack_and_scale(orig_problem, i, solution);
slack > COMP_SLACK_REL_TOL * scale
}
fn build_and_solve_cleanup_lp(
orig_problem: &LpProblem,
presolve_result: &PresolveResult,
solution: &[f64],
dual_solution_known: &[f64],
deadline: Option<Instant>,
allow_kept_perturbation: bool,
) -> Option<Vec<f64>> {
if let Some(d) = deadline {
if Instant::now() >= d {
return None;
}
}
let n = orig_problem.num_vars;
let m = orig_problem.num_constraints;
let deleted_rows: Vec<usize> = (0..m)
.filter(|&i| presolve_result.row_map[i].is_none())
.collect();
let k = deleted_rows.len();
if k == 0 {
return None;
}
let row_to_var: std::collections::HashMap<usize, usize> = deleted_rows
.iter()
.enumerate()
.map(|(idx, &r)| (r, idx))
.collect();
let use_kept_perturbation = allow_kept_perturbation && n + m <= LARGE_PROBLEM_THRESHOLD;
let coupled_kept: Vec<usize> = if use_kept_perturbation {
let mut row_to_cols: Vec<Vec<usize>> = vec![Vec::new(); m];
for j in 0..n {
if let Ok((rows, _)) = orig_problem.a.get_column(j) {
for &row in rows {
row_to_cols[row].push(j);
}
}
}
let mut col_affected: Vec<bool> = vec![false; n];
let mut col_queue: Vec<usize> = Vec::new();
for &del_row in &deleted_rows {
for &j in &row_to_cols[del_row] {
if !col_affected[j] {
col_affected[j] = true;
col_queue.push(j);
}
}
}
for step in &presolve_result.postsolve_stack {
if let PostsolveStep::LinearSubstitution { orig_col, .. } = step {
let j = *orig_col;
if !col_affected[j] {
col_affected[j] = true;
col_queue.push(j);
}
}
}
let mut kept_in_set: Vec<bool> = vec![false; m];
let mut coupled: Vec<usize> = Vec::new();
let mut head = 0usize;
while head < col_queue.len() {
let j = col_queue[head];
head += 1;
if let Ok((rows, _)) = orig_problem.a.get_column(j) {
for &row in rows {
if presolve_result.row_map[row].is_some() && !kept_in_set[row] {
kept_in_set[row] = true;
coupled.push(row);
for &j2 in &row_to_cols[row] {
if !col_affected[j2] {
col_affected[j2] = true;
col_queue.push(j2);
}
}
}
}
}
}
coupled
} else {
Vec::new()
};
let row_to_kept_var: std::collections::HashMap<usize, usize> = coupled_kept
.iter()
.enumerate()
.map(|(idx, &r)| (r, idx))
.collect();
let m_kept = coupled_kept.len();
let m_kept_var = if use_kept_perturbation { m_kept } else { 0 };
let dy_offset = k;
let slack_offset = k + m_kept_var;
let mut rc_known = orig_problem.c.clone();
for j in 0..n {
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (kk, &row) in rows.iter().enumerate() {
if presolve_result.row_map[row].is_some() {
rc_known[j] -= vals[kk] * dual_solution_known[row];
}
}
}
}
let mut tri_rows: Vec<usize> = Vec::new();
let mut tri_cols: Vec<usize> = Vec::new();
let mut tri_vals: Vec<f64> = Vec::new();
let mut b_clean: Vec<f64> = Vec::new();
let mut ct_clean: Vec<ConstraintType> = Vec::new();
for j in 0..n {
let x_j = solution[j];
let (lb_j, ub_j) = orig_problem.bounds[j];
let at_lb = lb_j.is_finite() && (x_j - lb_j).abs() < at_lb_tol(lb_j);
let at_ub = ub_j.is_finite() && (x_j - ub_j).abs() < at_ub_tol(ub_j);
let fixed =
lb_j.is_finite() && ub_j.is_finite() && (ub_j - lb_j).abs() < fixed_tol(lb_j, ub_j);
if fixed {
continue;
}
let mut col_terms: Vec<(usize, f64)> = Vec::new();
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (kk, &row) in rows.iter().enumerate() {
if let Some(&var_idx) = row_to_var.get(&row) {
col_terms.push((var_idx, vals[kk]));
} else if use_kept_perturbation {
if let Some(&kept_idx) = row_to_kept_var.get(&row) {
col_terms.push((dy_offset + kept_idx, vals[kk]));
}
}
}
}
if col_terms.is_empty() {
continue;
}
let ct = if at_lb && !at_ub {
ConstraintType::Le
} else if at_ub && !at_lb {
ConstraintType::Ge
} else {
ConstraintType::Eq
};
let row_idx = b_clean.len();
for &(var_idx, a) in &col_terms {
tri_rows.push(row_idx);
tri_cols.push(var_idx);
tri_vals.push(a);
}
b_clean.push(rc_known[j]);
ct_clean.push(ct);
}
for step in &presolve_result.postsolve_stack {
if let PostsolveStep::LinearSubstitution { orig_col, .. } = step {
let j = *orig_col;
let mut col_terms: Vec<(usize, f64)> = Vec::new();
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (kk, &row) in rows.iter().enumerate() {
if let Some(&var_idx) = row_to_var.get(&row) {
col_terms.push((var_idx, vals[kk]));
} else if use_kept_perturbation {
if let Some(&kept_idx) = row_to_kept_var.get(&row) {
col_terms.push((dy_offset + kept_idx, vals[kk]));
}
}
}
}
if col_terms.is_empty() {
continue;
}
let row_idx = b_clean.len();
for &(var_idx, a) in &col_terms {
tri_rows.push(row_idx);
tri_cols.push(var_idx);
tri_vals.push(a);
}
b_clean.push(rc_known[j]);
ct_clean.push(ConstraintType::Eq);
}
}
if b_clean.is_empty() {
return None;
}
let m_clean = b_clean.len();
let mut slack_count = 0usize;
let mut slack_cols_per_row: Vec<(usize, Option<usize>)> = Vec::with_capacity(m_clean);
for ct in &ct_clean {
match ct {
ConstraintType::Eq => {
let pos = slack_offset + slack_count;
let neg = slack_offset + slack_count + 1;
slack_cols_per_row.push((pos, Some(neg)));
slack_count += 2;
}
_ => {
slack_cols_per_row.push((slack_offset + slack_count, None));
slack_count += 1;
}
}
}
for (row_idx, (s_pos, s_neg_opt)) in slack_cols_per_row.iter().enumerate() {
let sign = match ct_clean[row_idx] {
ConstraintType::Le => -1.0,
ConstraintType::Ge => 1.0,
ConstraintType::Eq => 1.0,
};
tri_rows.push(row_idx);
tri_cols.push(*s_pos);
tri_vals.push(sign);
if let Some(s_neg) = s_neg_opt {
tri_rows.push(row_idx);
tri_cols.push(*s_neg);
tri_vals.push(-1.0);
}
}
let total_vars = slack_offset + slack_count;
let mut bounds_clean: Vec<(f64, f64)> = Vec::with_capacity(total_vars);
for &i in &deleted_rows {
let nonbinding = is_row_nonbinding(orig_problem, i, solution);
if nonbinding {
bounds_clean.push((0.0, 0.0));
continue;
}
match orig_problem.constraint_types[i] {
ConstraintType::Le => bounds_clean.push((f64::NEG_INFINITY, 0.0)),
ConstraintType::Ge => bounds_clean.push((0.0, f64::INFINITY)),
ConstraintType::Eq => bounds_clean.push((f64::NEG_INFINITY, f64::INFINITY)),
}
}
if use_kept_perturbation {
for &i in &coupled_kept {
let y_kept_i = dual_solution_known[i];
let nonbinding = is_row_nonbinding(orig_problem, i, solution);
if nonbinding {
bounds_clean.push((-y_kept_i, -y_kept_i));
continue;
}
match orig_problem.constraint_types[i] {
ConstraintType::Le => bounds_clean.push((f64::NEG_INFINITY, -y_kept_i)),
ConstraintType::Ge => bounds_clean.push((-y_kept_i, f64::INFINITY)),
ConstraintType::Eq => bounds_clean.push((f64::NEG_INFINITY, f64::INFINITY)),
}
}
}
for _ in 0..slack_count {
bounds_clean.push((0.0, f64::INFINITY));
}
let mut c_clean = vec![0.0f64; total_vars];
for j in slack_offset..total_vars {
c_clean[j] = 1.0;
}
let a_clean = CscMatrix::from_triplets(&tri_rows, &tri_cols, &tri_vals, m_clean, total_vars)
.expect("triplets invariant");
let b_clean_keep = b_clean.clone();
let ct_clean_keep = ct_clean.clone();
let cleanup_lp =
LpProblem::new_general(c_clean, a_clean, b_clean, ct_clean, bounds_clean, None).ok()?;
let opts = SolverOptions {
presolve: false,
warm_start: None,
deadline,
..SolverOptions::default()
};
let r1 = crate::simplex::solve_without_presolve(&cleanup_lp, &opts);
if r1.status != SolveStatus::Optimal || r1.solution.len() != total_vars {
return None;
}
let y_del_phase1: Vec<f64> = r1.solution[..k].to_vec();
let dy_phase1: Vec<f64> = if use_kept_perturbation {
r1.solution[dy_offset..dy_offset + m_kept_var].to_vec()
} else {
Vec::new()
};
let slack_phase1: Vec<f64> = r1.solution[slack_offset..].to_vec();
let assemble_full_y = |y_del: &[f64], dy: &[f64]| -> Vec<f64> {
let mut y = dual_solution_known.to_vec();
for (idx, &row) in deleted_rows.iter().enumerate() {
y[row] = y_del[idx];
}
if use_kept_perturbation {
for (idx, &row) in coupled_kept.iter().enumerate() {
y[row] = dual_solution_known[row] + dy[idx];
}
}
y
};
let n_yvars = k + m_kept_var;
let phase2_total_vars = 3 * n_yvars;
let phase2_total_cons = m_clean + n_yvars;
let mut p2_tri_rows: Vec<usize> = Vec::with_capacity(tri_rows.len() + 3 * n_yvars);
let mut p2_tri_cols: Vec<usize> = Vec::with_capacity(tri_rows.len() + 3 * n_yvars);
let mut p2_tri_vals: Vec<f64> = Vec::with_capacity(tri_rows.len() + 3 * n_yvars);
let mut p2_b: Vec<f64> = Vec::with_capacity(phase2_total_cons);
let mut p2_ct: Vec<ConstraintType> = Vec::with_capacity(phase2_total_cons);
let p2_slack_offset = slack_offset;
for (orig_idx, (slack_pos, slack_neg_opt)) in slack_cols_per_row.iter().enumerate() {
for (k_t, &row) in tri_rows.iter().enumerate() {
if row != orig_idx {
continue;
}
let col = tri_cols[k_t];
if col >= p2_slack_offset {
continue;
}
p2_tri_rows.push(orig_idx);
p2_tri_cols.push(col);
p2_tri_vals.push(tri_vals[k_t]);
}
let s_p_val = slack_phase1[*slack_pos - p2_slack_offset];
let rhs = match ct_clean_keep[orig_idx] {
ConstraintType::Le => b_clean_keep[orig_idx] + s_p_val,
ConstraintType::Ge => b_clean_keep[orig_idx] - s_p_val,
ConstraintType::Eq => {
let s_n_val = slack_phase1[slack_neg_opt.unwrap() - p2_slack_offset];
b_clean_keep[orig_idx] - s_p_val + s_n_val
}
};
p2_b.push(rhs);
p2_ct.push(ct_clean_keep[orig_idx]);
}
for i in 0..n_yvars {
let row_idx = m_clean + i;
p2_tri_rows.push(row_idx);
p2_tri_cols.push(i);
p2_tri_vals.push(1.0);
p2_tri_rows.push(row_idx);
p2_tri_cols.push(n_yvars + i);
p2_tri_vals.push(-1.0);
p2_tri_rows.push(row_idx);
p2_tri_cols.push(2 * n_yvars + i);
p2_tri_vals.push(1.0);
p2_b.push(0.0);
p2_ct.push(ConstraintType::Eq);
}
let mut p2_bounds: Vec<(f64, f64)> = Vec::with_capacity(phase2_total_vars);
for &i in &deleted_rows {
if is_row_nonbinding(orig_problem, i, solution) {
p2_bounds.push((0.0, 0.0));
continue;
}
match orig_problem.constraint_types[i] {
ConstraintType::Le => p2_bounds.push((f64::NEG_INFINITY, 0.0)),
ConstraintType::Ge => p2_bounds.push((0.0, f64::INFINITY)),
ConstraintType::Eq => p2_bounds.push((f64::NEG_INFINITY, f64::INFINITY)),
}
}
if use_kept_perturbation {
for &i in &coupled_kept {
let y_kept_i = dual_solution_known[i];
if is_row_nonbinding(orig_problem, i, solution) {
p2_bounds.push((-y_kept_i, -y_kept_i));
continue;
}
match orig_problem.constraint_types[i] {
ConstraintType::Le => p2_bounds.push((f64::NEG_INFINITY, -y_kept_i)),
ConstraintType::Ge => p2_bounds.push((-y_kept_i, f64::INFINITY)),
ConstraintType::Eq => p2_bounds.push((f64::NEG_INFINITY, f64::INFINITY)),
}
}
}
for _ in 0..(2 * n_yvars) {
p2_bounds.push((0.0, f64::INFINITY));
}
let mut p2_c = vec![0.0f64; phase2_total_vars];
for j in n_yvars..(3 * n_yvars) {
p2_c[j] = 1.0;
}
let p2_a = match CscMatrix::from_triplets(
&p2_tri_rows,
&p2_tri_cols,
&p2_tri_vals,
phase2_total_cons,
phase2_total_vars,
) {
Ok(m) => m,
Err(_) => return Some(assemble_full_y(&y_del_phase1, &dy_phase1)),
};
let p2_lp = match LpProblem::new_general(p2_c, p2_a, p2_b, p2_ct, p2_bounds, None) {
Ok(l) => l,
Err(_) => return Some(assemble_full_y(&y_del_phase1, &dy_phase1)),
};
let r2 = crate::simplex::solve_without_presolve(&p2_lp, &opts);
if r2.status == SolveStatus::Optimal && r2.solution.len() == phase2_total_vars {
let y_del_p2: Vec<f64> = r2.solution[..k].to_vec();
let dy_p2: Vec<f64> = if use_kept_perturbation {
r2.solution[dy_offset..dy_offset + m_kept_var].to_vec()
} else {
Vec::new()
};
Some(assemble_full_y(&y_del_p2, &dy_p2))
} else {
Some(assemble_full_y(&y_del_phase1, &dy_phase1))
}
}
fn collect_row_entries(orig_problem: &LpProblem, i: usize) -> Vec<(usize, f64)> {
let mut out = Vec::new();
for j in 0..orig_problem.num_vars {
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
if row == i {
out.push((j, vals[k]));
}
}
}
}
out
}
const BOUND_ACTIVE_REL_TOL: f64 = 1e-6;
#[inline]
fn at_lb_tol(lb: f64) -> f64 {
BOUND_ACTIVE_REL_TOL * (1.0 + lb.abs())
}
#[inline]
fn at_ub_tol(ub: f64) -> f64 {
BOUND_ACTIVE_REL_TOL * (1.0 + ub.abs())
}
#[inline]
fn fixed_tol(lb: f64, ub: f64) -> f64 {
let lb_s = if lb.is_finite() { lb.abs() } else { 0.0 };
let ub_s = if ub.is_finite() { ub.abs() } else { 0.0 };
BOUND_ACTIVE_REL_TOL * (1.0 + lb_s.max(ub_s))
}
fn recover_removed_row_dual(
orig_problem: &LpProblem,
i: usize,
solution: &[f64],
dual_solution: &[f64],
) -> f64 {
if is_row_nonbinding(orig_problem, i, solution) {
return 0.0;
}
let row_entries = collect_row_entries(orig_problem, i);
let mut min_y_i = f64::NEG_INFINITY;
let mut max_y_i = f64::INFINITY;
for &(j, a_ij) in &row_entries {
if a_ij.abs() < f64::EPSILON {
continue;
}
let mut rc_at_yi0 = orig_problem.c[j];
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
if row == i {
continue;
}
rc_at_yi0 -= vals[k] * dual_solution[row];
}
}
let x_j = solution[j];
let (lb_j, ub_j) = orig_problem.bounds[j];
let at_lb = lb_j.is_finite() && (x_j - lb_j).abs() < at_lb_tol(lb_j);
let at_ub = ub_j.is_finite() && (x_j - ub_j).abs() < at_ub_tol(ub_j);
let fixed =
lb_j.is_finite() && ub_j.is_finite() && (ub_j - lb_j).abs() < fixed_tol(lb_j, ub_j);
if fixed {
continue;
}
let bound_val = rc_at_yi0 / a_ij;
if at_lb && !at_ub {
if a_ij > 0.0 {
if bound_val < max_y_i {
max_y_i = bound_val;
}
} else if bound_val > min_y_i {
min_y_i = bound_val;
}
} else if at_ub && !at_lb {
if a_ij > 0.0 {
if bound_val > min_y_i {
min_y_i = bound_val;
}
} else if bound_val < max_y_i {
max_y_i = bound_val;
}
} else {
if bound_val < max_y_i {
max_y_i = bound_val;
}
if bound_val > min_y_i {
min_y_i = bound_val;
}
}
}
let (sign_lb, sign_ub) = match orig_problem.constraint_types[i] {
ConstraintType::Le => (f64::NEG_INFINITY, 0.0),
ConstraintType::Ge => (0.0, f64::INFINITY),
ConstraintType::Eq => (f64::NEG_INFINITY, f64::INFINITY),
};
let lb_y = sign_lb.max(min_y_i);
let ub_y = sign_ub.min(max_y_i);
if lb_y <= ub_y {
if lb_y <= 0.0 && ub_y >= 0.0 {
0.0
} else if ub_y < 0.0 {
ub_y
} else {
lb_y
}
} else {
0.0
}
}
fn recover_warm_start_basis(orig_problem: &LpProblem, solution: &[f64]) -> Option<WarmStartBasis> {
let sf = build_standard_form(orig_problem);
let n_orig = orig_problem.num_vars;
let n_total = sf.n_total;
let n_shifted = sf.n_shifted;
let m_ext = sf.m;
if solution.len() != n_orig {
return None;
}
let mut x_std = vec![0.0_f64; n_total];
for j in 0..n_orig {
let info = &sf.orig_var_info[j];
let xj = solution[j];
if info.new_vars.len() == 2 {
let plus_idx = info.new_vars[0].0;
let minus_idx = info.new_vars[1].0;
x_std[plus_idx] = xj.max(0.0);
x_std[minus_idx] = (-xj).max(0.0);
} else {
let (idx, coeff) = info.new_vars[0];
let val = if coeff > 0.0 {
xj - info.offset
} else {
info.offset - xj
};
x_std[idx] = val.max(0.0);
}
}
let mut row_struct_sum = vec![0.0_f64; m_ext];
for j in 0..n_shifted {
if x_std[j].abs() < WARM_BASIS_BUILD_TOL {
continue;
}
if let Ok((rows, vals)) = sf.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
row_struct_sum[row] += vals[k] * x_std[j];
}
}
}
for j in n_shifted..n_total {
if let Ok((rows, vals)) = sf.a.get_column(j) {
if rows.len() == 1 && vals[0].abs() > 0.0 {
let i = rows[0];
let coeff = vals[0];
let slack = (sf.b[i] - row_struct_sum[i]) / coeff;
x_std[j] = slack.max(0.0);
}
}
}
let (mut basis, _needs_art, num_art) = compute_crash_basis(
&sf.a,
&sf.b,
m_ext,
n_shifted,
&sf.initial_basis,
&sf.needs_artificial,
);
if num_art > 0 {
return None;
}
let mut basic_at_row: Vec<usize> = vec![usize::MAX; n_total];
for (i, &col) in basis.iter().enumerate() {
basic_at_row[col] = i;
}
let mut active_struct: Vec<(f64, usize)> = (0..n_shifted)
.filter(|&j| x_std[j] > WARM_BASIS_BUILD_TOL && basic_at_row[j] == usize::MAX)
.map(|j| (x_std[j], j))
.collect();
active_struct.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
for (_xj, j) in active_struct {
if let Ok((rows, vals)) = sf.a.get_column(j) {
let mut col_max = 0.0_f64;
for &v in vals.iter() {
if v.abs() > col_max {
col_max = v.abs();
}
}
if col_max < WARM_BASIS_BUILD_TOL {
continue;
}
let pivot_min = (MARKOWITZ_PIVOT_RATIO * col_max).max(WARM_BASIS_BUILD_TOL);
let mut best: Option<(f64, usize)> = None;
for (k, &row) in rows.iter().enumerate() {
let abs = vals[k].abs();
if abs < pivot_min {
continue;
}
let cur = basis[row];
let cur_is_at_bound_slack = cur >= n_shifted && x_std[cur] <= WARM_BASIS_BUILD_TOL;
if !cur_is_at_bound_slack {
continue;
}
if best.is_none_or(|(b, _)| abs > b) {
best = Some((abs, row));
}
}
if let Some((_, row)) = best {
let leaving = basis[row];
basic_at_row[leaving] = usize::MAX;
basis[row] = j;
basic_at_row[j] = row;
}
}
}
let x_b: Vec<f64> = basis
.iter()
.map(|&j| x_std.get(j).copied().unwrap_or(0.0))
.collect();
Some(WarmStartBasis { basis, x_b })
}
pub fn run_postsolve(
result: &SolverResult,
presolve_result: &PresolveResult,
orig_problem: &LpProblem,
deadline: Option<Instant>,
recover_warm_basis: bool,
) -> SolverResult {
let n = presolve_result.orig_num_vars;
let m = presolve_result.orig_num_constraints;
let mut solution = vec![0.0f64; n];
let mut dual_solution = vec![0.0f64; m];
let input_dual_is_ipm = result.reduced_costs.is_empty() && !result.dual_solution.is_empty();
for (j, &maybe_jj) in presolve_result.col_map.iter().enumerate() {
if let Some(jj) = maybe_jj {
if jj < result.solution.len() {
solution[j] = result.solution[jj];
}
}
}
for (i, &maybe_ii) in presolve_result.row_map.iter().enumerate() {
if let Some(ii) = maybe_ii {
if ii < result.dual_solution.len() {
dual_solution[i] = if input_dual_is_ipm {
-result.dual_solution[ii]
} else {
result.dual_solution[ii]
};
}
}
}
for step in presolve_result.postsolve_stack.iter().rev() {
match step {
PostsolveStep::FixedVariable { orig_col, value } => {
solution[*orig_col] = *value;
}
PostsolveStep::EmptyColumn { orig_col, value } => {
solution[*orig_col] = *value;
}
PostsolveStep::EmptyRow { orig_row } => {
dual_solution[*orig_row] =
recover_removed_row_dual(orig_problem, *orig_row, &solution, &dual_solution);
}
PostsolveStep::SingletonRow {
orig_col,
orig_row,
value,
} => {
solution[*orig_col] = *value;
dual_solution[*orig_row] =
recover_removed_row_dual(orig_problem, *orig_row, &solution, &dual_solution);
}
PostsolveStep::RedundantConstraint { orig_row } => {
dual_solution[*orig_row] =
recover_removed_row_dual(orig_problem, *orig_row, &solution, &dual_solution);
}
PostsolveStep::BoundsTightened => {}
PostsolveStep::LinearSubstitution {
orig_col,
orig_row,
pivot,
rhs,
others,
col_orig_entries,
c_orig,
} => {
let mut sum_others = 0.0f64;
for &(other_col, coeff) in others {
sum_others += coeff * solution[other_col];
}
solution[*orig_col] = (rhs - sum_others) / pivot;
if let Some(piv_row) = orig_row {
if col_orig_entries.is_empty() && c_orig.abs() <= ZERO_TOL {
dual_solution[*piv_row] = recover_removed_row_dual(
orig_problem,
*piv_row,
&solution,
&dual_solution,
);
} else {
let mut sum_other_rows = 0.0f64;
for &(row_i, a_ij) in col_orig_entries {
if row_i == *piv_row {
continue;
}
sum_other_rows += a_ij * dual_solution[row_i];
}
dual_solution[*piv_row] = (c_orig - sum_other_rows) / pivot;
}
}
}
}
}
let mut slack = orig_problem.b.clone();
for (j, &sol_j) in solution.iter().enumerate().take(n) {
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
slack[row] -= vals[k] * sol_j;
}
}
}
let y_loop = dual_solution.clone();
let y_gs = {
let mut y = y_loop.clone();
let mut linsub_rows: std::collections::HashSet<usize> = std::collections::HashSet::new();
for step in &presolve_result.postsolve_stack {
if let PostsolveStep::LinearSubstitution {
col_orig_entries,
c_orig,
orig_row: Some(r),
..
} = step
{
if !(col_orig_entries.is_empty() && c_orig.abs() <= ZERO_TOL) {
linsub_rows.insert(*r);
}
}
}
'gs_outer: for _ in 0..GS_MAX_ITER {
if deadline.is_some_and(|d| Instant::now() >= d) {
break 'gs_outer;
}
let mut max_diff = 0.0f64;
for i in 0..m {
if presolve_result.row_map[i].is_some() {
continue;
}
if linsub_rows.contains(&i) {
continue;
}
if i & 0x3ff == 0 && deadline.is_some_and(|d| Instant::now() >= d) {
break 'gs_outer;
}
let new_y = recover_removed_row_dual(orig_problem, i, &solution, &y);
let diff = (y[i] - new_y).abs();
if diff > max_diff {
max_diff = diff;
}
y[i] = new_y;
}
for step in &presolve_result.postsolve_stack {
if let PostsolveStep::LinearSubstitution {
orig_row: Some(piv),
col_orig_entries,
c_orig,
pivot,
..
} = step
{
if col_orig_entries.is_empty() && c_orig.abs() <= ZERO_TOL {
continue;
}
let mut sum = 0.0f64;
for &(row_i, a_ij) in col_orig_entries {
if row_i == *piv {
continue;
}
sum += a_ij * y[row_i];
}
let new_y = (c_orig - sum) / pivot;
let diff = (y[*piv] - new_y).abs();
if diff > max_diff {
max_diff = diff;
}
y[*piv] = new_y;
}
}
if max_diff < GS_CONV_TOL {
break 'gs_outer;
}
}
y
};
let dfeas_bound = |y: &[f64]| -> f64 {
let mut max_viol = 0.0f64;
for j in 0..n {
let (lb_j, ub_j) = orig_problem.bounds[j];
let fixed =
lb_j.is_finite() && ub_j.is_finite() && (ub_j - lb_j).abs() < fixed_tol(lb_j, ub_j);
if fixed {
continue;
}
let at_lb = lb_j.is_finite() && (solution[j] - lb_j).abs() < at_lb_tol(lb_j);
let at_ub = ub_j.is_finite() && (solution[j] - ub_j).abs() < at_ub_tol(ub_j);
let mut rc = orig_problem.c[j];
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
rc -= vals[k] * y[row];
}
}
let viol = if at_lb && !at_ub {
f64::max(0.0, -rc)
} else if at_ub && !at_lb {
f64::max(0.0, rc)
} else {
rc.abs()
};
if viol > max_viol {
max_viol = viol;
}
}
max_viol
};
let df_loop = dfeas_bound(&y_loop);
let df_gs = dfeas_bound(&y_gs);
let cheap_min = df_loop.min(df_gs);
let gate = PIVOT_TOL;
let crossover: Option<Vec<f64>> =
if matches!(result.status, SolveStatus::Optimal) && cheap_min > gate {
trace_pass("crossover");
let xover_deadline = crossover_deadline_with_reserve(deadline, Instant::now());
crate::simplex::crossover_dual_from_primal(orig_problem, &solution, xover_deadline)
.map(|(y, _rc)| y)
} else {
None
};
let df_xover = crossover.as_ref().map_or(f64::INFINITY, |y| dfeas_bound(y));
let crossover_certified = df_xover <= gate;
let (y_cl_nopert, y_cl_pert) = if cheap_min <= gate || crossover_certified {
(None, None)
} else {
trace_pass("cleanup_nopert");
let t0_nopert = std::time::Instant::now();
let y_nopert = build_and_solve_cleanup_lp(
orig_problem,
presolve_result,
&solution,
&y_gs,
deadline,
false,
);
let t_nopert = t0_nopert.elapsed();
let df_nopert = y_nopert.as_ref().map_or(f64::INFINITY, |y| dfeas_bound(y));
let so_far = cheap_min.min(df_nopert);
let y_pert = if !should_run_kept_perturbation(
so_far > gate,
orig_problem.num_vars,
orig_problem.num_constraints,
) {
None
} else {
trace_pass("cleanup_pert");
let now = std::time::Instant::now();
let pert_budget = t_nopert.saturating_mul(4);
let pert_deadline = match deadline {
Some(d) => Some(d.min(now + pert_budget)),
None => Some(now + pert_budget),
};
build_and_solve_cleanup_lp(
orig_problem,
presolve_result,
&solution,
&y_gs,
pert_deadline,
true,
)
};
(y_nopert, y_pert)
};
let df_cl_nopert = y_cl_nopert
.as_ref()
.map_or(f64::INFINITY, |y| dfeas_bound(y));
let df_cl_pert = y_cl_pert.as_ref().map_or(f64::INFINITY, |y| dfeas_bound(y));
let df_cl_min = df_cl_nopert.min(df_cl_pert);
const LSQ_CLEANUP_REL_IMPROVE: f64 = 1e-3;
let cleanup_stagnant =
df_cl_min.is_finite() && df_cl_min >= cheap_min * (1.0 - LSQ_CLEANUP_REL_IMPROVE);
let y_lsq: Option<Vec<f64>> = if cheap_min <= gate || crossover_certified || cleanup_stagnant {
#[cfg(debug_assertions)]
{
#[allow(clippy::print_stderr)]
if cleanup_stagnant {
eprintln!(
"[postsolve] LSQ skip: improvement-stagnant (cheap_min={:.3e} df_cl_min={:.3e})",
cheap_min, df_cl_min
);
}
}
None
} else if m > 0 {
trace_pass("lsq");
let q_empty = CscMatrix::new(n, n);
let qp = crate::qp::QpProblem::new(
q_empty,
orig_problem.c.clone(),
orig_problem.a.clone(),
orig_problem.b.clone(),
orig_problem.bounds.clone(),
orig_problem.constraint_types.clone(),
)
.ok();
qp.and_then(|qp| {
let seed = y_cl_pert
.as_ref()
.or(y_cl_nopert.as_ref())
.cloned()
.unwrap_or_else(|| y_gs.clone());
let tmp_result = crate::problem::SolverResult {
solution: solution.clone(),
dual_solution: seed,
..Default::default()
};
crate::qp::compute_lsq_dual_y(&qp, &tmp_result, deadline)
})
} else {
None
};
let df_lsq = y_lsq.as_ref().map_or(f64::INFINITY, |y| dfeas_bound(y));
let min_df = df_loop
.min(df_gs)
.min(df_cl_nopert)
.min(df_cl_pert)
.min(df_lsq)
.min(df_xover);
if df_loop == min_df {
dual_solution = y_loop;
} else if df_gs == min_df {
dual_solution = y_gs;
} else if df_cl_nopert == min_df {
dual_solution = y_cl_nopert.expect("df_cl_nopert finite implies Some");
} else if df_cl_pert == min_df {
dual_solution = y_cl_pert.expect("df_cl_pert finite implies Some");
} else if df_lsq == min_df {
dual_solution = y_lsq.expect("df_lsq finite implies Some");
} else {
dual_solution = crossover.expect("df_xover finite implies Some");
}
let mut reduced_costs = orig_problem.c.clone();
for (j, rc) in reduced_costs.iter_mut().enumerate().take(n) {
if let Ok((rows, vals)) = orig_problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
*rc -= vals[k] * dual_solution[row];
}
}
}
let postsolve_dfeas_recomputed = dfeas_bound(&dual_solution);
let objective = result.objective + presolve_result.obj_offset;
let warm_start_basis = if recover_warm_basis && matches!(result.status, SolveStatus::Optimal) {
recover_warm_start_basis(orig_problem, &solution)
} else {
None
};
SolverResult {
status: result.status.clone(),
objective,
solution,
dual_solution,
reduced_costs,
slack,
warm_start_basis,
iterations: result.iterations,
postsolve_dfeas: Some(postsolve_dfeas_recomputed),
..Default::default()
}
}
#[cfg(test)]
mod cleanup_comp_tests {
use super::*;
use crate::presolve::transforms::{PostsolveStep, PresolveResult};
use crate::problem::{ConstraintType, LpProblem};
use crate::sparse::CscMatrix;
use std::sync::Once;
const DRIFT_MAGNITUDE: f64 = 5e-3;
const COMP_RESID_TIGHT: f64 = 1e-9;
fn presolve_result_with_deleted_row(problem: &LpProblem, deleted_row: usize) -> PresolveResult {
let n = problem.num_vars;
let m = problem.num_constraints;
let col_map = (0..n).map(Some).collect();
let row_map: Vec<Option<usize>> = (0..m)
.map(|i| {
if i == deleted_row {
None
} else {
Some(if i < deleted_row { i } else { i - 1 })
}
})
.collect();
let postsolve_stack = vec![PostsolveStep::EmptyRow {
orig_row: deleted_row,
}];
PresolveResult {
reduced_problem: problem.clone(),
postsolve_stack,
orig_num_vars: n,
orig_num_constraints: m,
col_map,
row_map,
was_reduced: true,
obj_offset: 0.0,
}
}
fn rc_sign_violation(problem: &LpProblem, solution: &[f64], y: &[f64]) -> f64 {
let mut max_v = 0.0_f64;
for j in 0..problem.num_vars {
let (lb, ub) = problem.bounds[j];
let at_lb = lb.is_finite() && (solution[j] - lb).abs() < 1e-6;
let at_ub = ub.is_finite() && (solution[j] - ub).abs() < 1e-6;
let mut rc = problem.c[j];
if let Ok((rows, vals)) = problem.a.get_column(j) {
for (k, &row) in rows.iter().enumerate() {
rc -= vals[k] * y[row];
}
}
let v = if at_lb && !at_ub {
(-rc).max(0.0)
} else if at_ub && !at_lb {
rc.max(0.0)
} else {
rc.abs()
};
if v > max_v {
max_v = v;
}
}
max_v
}
fn comp_residual(problem: &LpProblem, solution: &[f64], y: &[f64]) -> f64 {
let mut max_c = 0.0_f64;
for i in 0..problem.num_constraints {
let (slack, scale) = row_slack_and_scale(problem, i, solution);
let prod = (y[i] * slack).abs() / scale;
if prod > max_c {
max_c = prod;
}
}
max_c
}
fn fixture_eq_kept_le_deleted() -> (LpProblem, Vec<f64>, Vec<f64>, usize) {
let a = CscMatrix::from_triplets(&[0, 0, 1], &[0, 1, 1], &[1.0, 1.0, 1.0], 2, 2).unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![1.0, 10.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let solution = vec![0.0, 1.0];
let dual_known = vec![1.0 + DRIFT_MAGNITUDE, 0.0];
(lp, solution, dual_known, 1)
}
fn fixture_eq_kept_ge_deleted() -> (LpProblem, Vec<f64>, Vec<f64>, usize) {
let a =
CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[1.0, 1.0, -1.0, -1.0], 2, 2)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![1.0, -10.0],
vec![ConstraintType::Eq, ConstraintType::Ge],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let solution = vec![0.0, 1.0];
let dual_known = vec![1.0 + DRIFT_MAGNITUDE, 0.0];
(lp, solution, dual_known, 1)
}
fn init_logger() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {});
}
fn run_fixture(
problem: &LpProblem,
solution: &[f64],
dual_known: &[f64],
deleted_row: usize,
) -> Vec<f64> {
init_logger();
let presolve_result = presolve_result_with_deleted_row(problem, deleted_row);
let y = build_and_solve_cleanup_lp(
problem,
&presolve_result,
solution,
dual_known,
None,
false,
)
.expect("cleanup LP must converge for the sentinel fixture");
assert_eq!(y.len(), problem.num_constraints);
y
}
#[test]
fn cleanup_lp_eq_kept_le_deleted_comp_holds() {
let (lp, sol, dual, del) = fixture_eq_kept_le_deleted();
let y = run_fixture(&lp, &sol, &dual, del);
let comp = comp_residual(&lp, &sol, &y);
assert!(
comp < COMP_RESID_TIGHT,
"comp={:.3e} >= {:.0e}; y={:?} (clamp on non-binding Le row must pin y[{}]=0)",
comp,
COMP_RESID_TIGHT,
y,
del,
);
assert_eq!(
y[del], 0.0,
"non-binding Le deleted row y must be 0, got {}",
y[del]
);
}
#[test]
fn cleanup_lp_eq_kept_ge_deleted_comp_holds() {
let (lp, sol, dual, del) = fixture_eq_kept_ge_deleted();
let y = run_fixture(&lp, &sol, &dual, del);
let comp = comp_residual(&lp, &sol, &y);
assert!(
comp < COMP_RESID_TIGHT,
"comp={:.3e} >= {:.0e}; y={:?} (clamp on non-binding Ge row must pin y[{}]=0)",
comp,
COMP_RESID_TIGHT,
y,
del,
);
assert_eq!(
y[del], 0.0,
"non-binding Ge deleted row y must be 0, got {}",
y[del]
);
}
#[test]
fn cleanup_lp_unclamped_dual_violates_comp_detector() {
let (lp, sol, _dual, _del) = fixture_eq_kept_le_deleted();
let broken_y = vec![1.0 + DRIFT_MAGNITUDE, -DRIFT_MAGNITUDE];
let comp = comp_residual(&lp, &sol, &broken_y);
assert!(
comp >= DRIFT_MAGNITUDE * 0.5,
"broken dual comp={:.3e} should be >= {:.3e}; detector is no-op'd",
comp,
DRIFT_MAGNITUDE * 0.5,
);
let _rc_v_inherited = rc_sign_violation(&lp, &sol, &broken_y);
}
#[test]
fn is_row_nonbinding_detects_known_patterns() {
let cases: Vec<(ConstraintType, f64, f64, bool)> = vec![
(ConstraintType::Le, 10.0, 5.0, true), (ConstraintType::Le, 10.0, 10.0, false), (ConstraintType::Ge, 1.0, 100.0, true), (ConstraintType::Ge, 1.0, 1.0, false),
(ConstraintType::Eq, 1.0, 1.0, false),
(ConstraintType::Eq, 1.0, 0.5, false), ];
for (i, (ct, b, ax, expected)) in cases.iter().enumerate() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![0.0],
a,
vec![*b],
vec![*ct],
vec![(f64::NEG_INFINITY, f64::INFINITY)],
None,
)
.unwrap();
let got = is_row_nonbinding(&lp, 0, &[*ax]);
assert_eq!(
got, *expected,
"case {} ({:?}, b={}, ax={}): expected {}, got {}",
i, ct, b, ax, expected, got,
);
}
}
#[test]
fn recover_removed_row_dual_excludes_self_row_in_stationarity() {
let a = CscMatrix::from_triplets(&[0usize, 1], &[0usize, 0], &[2.0, 1.0], 2, 1).unwrap();
let lp = LpProblem::new_general(
vec![7.0],
a,
vec![52.0, 26.0],
vec![ConstraintType::Ge, ConstraintType::Eq],
vec![(0.0, 100.0)],
None,
)
.unwrap();
let x = vec![26.0]; let y_with_bad_self = vec![11.0, 3.0];
let y0 = recover_removed_row_dual(&lp, 0, &x, &y_with_bad_self);
assert!(
(y0 - 2.0).abs() < 1e-12,
"row dual recovery must enforce interior rc=0, expected y0=2 got {}",
y0
);
let y_reapplied = recover_removed_row_dual(&lp, 0, &x, &[y0, 3.0]);
assert!(
(y_reapplied - y0).abs() < 1e-12,
"GS update must not oscillate after recovery: y0={} -> {}",
y0,
y_reapplied
);
let rc0 = 7.0 - 2.0 * y0 - 1.0 * 3.0;
assert!(
rc0.abs() < 1e-12,
"interior/basic column must satisfy rc=0 after recovery, rc={}",
rc0
);
}
}
#[cfg(test)]
mod warm_basis_recovery_tests {
use super::*;
use crate::options::{SimplexMethod, SolverOptions};
use crate::problem::{ConstraintType, LpProblem, SolveStatus};
use crate::simplex::{build_standard_form, solve, solve_with};
use crate::sparse::CscMatrix;
fn opts_recover() -> SolverOptions {
SolverOptions {
recover_warm_start_basis: true,
..SolverOptions::default()
}
}
fn lp_dual_fixed() -> LpProblem {
let a = CscMatrix::from_triplets(&[0, 0, 1, 2], &[0, 1, 0, 1], &[1.0, 1.0, 1.0, 1.0], 3, 2)
.unwrap();
LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![6.0, 4.0, 4.0],
vec![ConstraintType::Le; 3],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap()
}
fn lp_singleton_row() -> LpProblem {
let a = CscMatrix::from_triplets(&[0, 1, 1], &[0, 0, 1], &[1.0, 1.0, 1.0], 2, 2).unwrap();
LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![2.0, 5.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap()
}
fn lp_non_reducible() -> LpProblem {
let a = CscMatrix::from_triplets(&[0, 0, 1, 2], &[0, 1, 0, 1], &[1.0, 1.0, 1.0, 1.0], 3, 2)
.unwrap();
LpProblem::new_general(
vec![-1.0, -2.0],
a,
vec![4.0, 3.0, 3.0],
vec![ConstraintType::Le; 3],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap()
}
fn assert_basis_well_formed(lp: &LpProblem, basis: &[usize], context: &str) {
let sf = build_standard_form(lp);
assert_eq!(
basis.len(),
sf.m,
"[{}] basis len {} != m_ext {}",
context,
basis.len(),
sf.m,
);
for (i, &col) in basis.iter().enumerate() {
assert!(
col < sf.n_total,
"[{}] basis[{}] = {} ≥ n_total {} (artificial leakage)",
context,
i,
col,
sf.n_total,
);
}
let mut seen = vec![false; sf.n_total];
for &col in basis {
assert!(
!seen[col],
"[{}] basis has duplicate column {}",
context, col
);
seen[col] = true;
}
}
fn assert_warm_round_trip(lp_a: &LpProblem, lp_b: &LpProblem, context: &str) {
let r1 = solve_with(lp_a, &opts_recover());
assert_eq!(r1.status, SolveStatus::Optimal, "[{}] lp_a status", context);
let ws = r1
.warm_start_basis
.as_ref()
.unwrap_or_else(|| panic!("[{}] postsolve returned warm_start_basis=None", context));
assert_basis_well_formed(lp_a, &ws.basis, context);
let opts_warm = SolverOptions {
warm_start: Some(ws.clone()),
simplex_method: SimplexMethod::Dual,
presolve: false,
..SolverOptions::default()
};
let r2 = solve_with(lp_b, &opts_warm);
assert_eq!(
r2.status,
SolveStatus::Optimal,
"[{}] warm-start round-trip on lp_b did not reach Optimal",
context,
);
}
#[test]
fn warm_basis_from_dual_fixed_lp() {
let lp = lp_dual_fixed();
assert_warm_round_trip(&lp, &lp, "dual_fixed/self");
let mut lp2 = lp_dual_fixed();
lp2.b = vec![5.0, 3.0, 3.0];
assert_warm_round_trip(&lp, &lp2, "dual_fixed/rhs_change");
}
#[test]
fn warm_basis_from_singleton_row_lp() {
let lp = lp_singleton_row();
assert_warm_round_trip(&lp, &lp, "singleton_row/self");
}
#[test]
fn warm_basis_from_non_reducible_lp() {
let lp = lp_non_reducible();
let r = solve(&lp);
assert_eq!(r.status, SolveStatus::Optimal);
assert!(
r.warm_start_basis.is_some(),
"non-reducible path lost its native simplex warm_start_basis",
);
assert_basis_well_formed(
&lp,
&r.warm_start_basis.as_ref().unwrap().basis,
"non_reducible",
);
}
#[test]
fn noop_proof_returns_none_fails_round_trip() {
let lp = lp_dual_fixed();
let solution = vec![0.0, 0.0];
let recovered = recover_warm_start_basis(&lp, &solution);
assert!(
recovered.is_some(),
"recover_warm_start_basis must produce a basis for dual-fixed LP \
(no-op would return None and re-introduce #65)",
);
let basis = recovered.unwrap().basis;
let sf = build_standard_form(&lp);
assert_eq!(basis.len(), sf.m, "recovered basis must have length m_ext");
for &c in &basis {
assert!(c < sf.n_total, "recovered basis col {} ≥ n_total", c);
}
}
#[test]
fn warm_basis_includes_active_variables() {
let lp = lp_non_reducible();
let r = solve(&lp);
assert_eq!(r.status, SolveStatus::Optimal);
let basis = &r.warm_start_basis.as_ref().unwrap().basis;
let sf = build_standard_form(&lp);
assert!(
basis.contains(&0)
|| sf.orig_var_info[0]
.new_vars
.iter()
.any(|&(idx, _)| basis.contains(&idx)),
"active x0=1 not in warm-start basis: {:?}",
basis,
);
assert!(
basis.contains(&1)
|| sf.orig_var_info[1]
.new_vars
.iter()
.any(|&(idx, _)| basis.contains(&idx)),
"active x1=3 not in warm-start basis: {:?}",
basis,
);
}
#[test]
fn default_skips_warm_basis_recovery() {
let lp = lp_dual_fixed();
let r_default = solve(&lp);
assert_eq!(r_default.status, SolveStatus::Optimal);
assert!(
r_default.warm_start_basis.is_none(),
"default options must NOT pay warm-basis recovery cost \
(postsolve recovery should be opt-in via recover_warm_start_basis=true)",
);
let r_optin = solve_with(&lp, &opts_recover());
assert_eq!(r_optin.status, SolveStatus::Optimal);
assert!(
r_optin.warm_start_basis.is_some(),
"opt-in flag must restore the postsolve warm-basis synthesis",
);
let lp_sr = lp_singleton_row();
let r_sr_default = solve(&lp_sr);
assert_eq!(r_sr_default.status, SolveStatus::Optimal);
assert!(
r_sr_default.warm_start_basis.is_none(),
"singleton-row presolve path must also skip recovery by default",
);
let r_sr_optin = solve_with(&lp_sr, &opts_recover());
assert!(r_sr_optin.warm_start_basis.is_some());
}
#[test]
fn non_reducible_basis_independent_of_recovery_flag() {
let lp = lp_non_reducible();
let r_default = solve(&lp);
let r_optin = solve_with(&lp, &opts_recover());
assert!(
r_default.warm_start_basis.is_some(),
"non-reducible default path must keep native simplex basis"
);
assert!(
r_optin.warm_start_basis.is_some(),
"non-reducible opt-in path must keep native simplex basis"
);
}
}
#[cfg(test)]
mod bound_active_tol_tests {
use super::*;
#[test]
fn test_sentinel_c4_large_scale_bound_active_tol() {
let lb = 1e6_f64;
let x = lb + 0.5;
assert!(
(x - lb).abs() > BOUND_ACTIVE_REL_TOL,
"absolute BOUND_ACTIVE_REL_TOL alone would misclassify x as interior"
);
let tol = at_lb_tol(lb);
assert!(
(x - lb).abs() < tol,
"at_lb_tol={} must classify x=lb+0.5 as at-lb for lb=1e6",
tol
);
}
#[test]
fn test_bound_active_tol_unit_scale() {
assert!(
(at_lb_tol(0.0) - 1e-6).abs() < 1e-20,
"at_lb_tol(0) should be 1e-6, got {}",
at_lb_tol(0.0)
);
assert!(
(at_ub_tol(1.0) - 2e-6).abs() < 1e-20,
"at_ub_tol(1) should be 2e-6, got {}",
at_ub_tol(1.0)
);
assert!(
(fixed_tol(0.0, 1.0) - 2e-6).abs() < 1e-20,
"fixed_tol(0,1) should be 2e-6, got {}",
fixed_tol(0.0, 1.0)
);
}
#[test]
fn test_sentinel_c4_independent_lb_ub_scaling_at_lb() {
let lb = 0.0_f64;
let ub = 1e12_f64;
let x = 5e5_f64;
let old_tol = BOUND_ACTIVE_REL_TOL * (1.0 + lb.abs() + ub.abs());
assert!(
(x - lb).abs() < old_tol,
"old formula must mis-classify x=5e5 as at-lb (old_tol={})",
old_tol
);
let new_tol = at_lb_tol(lb);
assert!(
(x - lb).abs() >= new_tol,
"at_lb_tol={} must NOT classify x=5e5 as at-lb for lb=0,ub=1e12",
new_tol
);
}
#[test]
fn test_sentinel_c4_independent_lb_ub_scaling_fixed() {
let lb = 1e6_f64;
let ub = 1e6_f64 + 1.5_f64;
let gap = ub - lb;
let old_tol = BOUND_ACTIVE_REL_TOL * (1.0 + lb.abs() + ub.abs());
assert!(
gap < old_tol,
"old formula must mis-classify [1e6,1e6+1.5] as fixed (old_tol={})",
old_tol
);
let new_tol = fixed_tol(lb, ub);
assert!(
gap >= new_tol,
"fixed_tol={} must NOT classify [1e6,1e6+1.5] as fixed (gap={})",
new_tol,
gap
);
}
}
#[cfg(test)]
mod ipm_dual_convention_tests {
use super::*;
#[test]
fn ipm_dual_is_converted_before_reduced_cost_recovery() {
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
let lp = LpProblem::new_general(
vec![1.0],
a,
vec![1.0],
vec![ConstraintType::Ge],
vec![(0.0, f64::INFINITY)],
None,
)
.unwrap();
let presolve = PresolveResult::no_reduction(&lp);
let raw_ipm = SolverResult {
status: SolveStatus::Optimal,
objective: 1.0,
solution: vec![1.0],
dual_solution: vec![-1.0],
reduced_costs: vec![],
..Default::default()
};
let lifted = run_postsolve(&raw_ipm, &presolve, &lp, Some(Instant::now()), false);
assert_eq!(
lifted.dual_solution,
vec![1.0],
"IPM/prove convention y=-1 must become LP simplex convention y=+1"
);
assert_eq!(lifted.reduced_costs.len(), 1);
assert!(
lifted.reduced_costs[0].abs() < 1e-12,
"simplex reduced cost must be c - A^T y = 0, got {}",
lifted.reduced_costs[0]
);
}
}
#[cfg(test)]
mod crossover_first_tests {
use super::*;
fn lp_clean_vertex() -> (LpProblem, Vec<f64>) {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![2.0, 3.0],
a,
vec![1.0],
vec![ConstraintType::Eq],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
(lp, vec![1.0, 0.0])
}
fn result_with_dual(status: SolveStatus, solution: &[f64], y: Vec<f64>) -> SolverResult {
SolverResult {
status,
objective: 0.0,
solution: solution.to_vec(),
dual_solution: y,
reduced_costs: vec![0.0; solution.len()],
..Default::default()
}
}
#[test]
fn crossover_first_certifies_and_skips_cleanup() {
let (lp, x) = lp_clean_vertex();
let presolve = PresolveResult::no_reduction(&lp);
let reduced = result_with_dual(SolveStatus::Optimal, &x, vec![0.0]);
let _ = drain_postsolve_pass_trace();
let lifted = run_postsolve(&reduced, &presolve, &lp, None, false);
let trace = drain_postsolve_pass_trace();
assert_eq!(
trace,
vec!["crossover"],
"crossover must run first and, on certifying, elide cleanup/LSQ; trace={trace:?}"
);
assert!(
(lifted.dual_solution[0] - 2.0).abs() < 1e-6,
"crossover dual must recover y0 = 2, got {}",
lifted.dual_solution[0]
);
assert!(
lifted.postsolve_dfeas.unwrap() <= PIVOT_TOL,
"crossover-first dual must be feasible (dfeas ≤ gate), got {:?}",
lifted.postsolve_dfeas
);
}
#[test]
fn cheap_feasible_dual_runs_no_recovery_pass() {
let (lp, x) = lp_clean_vertex();
let presolve = PresolveResult::no_reduction(&lp);
let reduced = result_with_dual(SolveStatus::Optimal, &x, vec![2.0]);
let _ = drain_postsolve_pass_trace();
let lifted = run_postsolve(&reduced, &presolve, &lp, None, false);
let trace = drain_postsolve_pass_trace();
assert!(
trace.is_empty(),
"a feasible cheap dual must skip every recovery pass; trace={trace:?}"
);
assert!(lifted.postsolve_dfeas.unwrap() <= PIVOT_TOL);
}
#[test]
fn non_optimal_status_skips_crossover() {
let (lp, x) = lp_clean_vertex();
let presolve = PresolveResult::no_reduction(&lp);
let reduced = result_with_dual(SolveStatus::Infeasible, &x, vec![0.0]);
let _ = drain_postsolve_pass_trace();
let _ = run_postsolve(&reduced, &presolve, &lp, None, false);
let trace = drain_postsolve_pass_trace();
assert!(
!trace.contains(&"crossover"),
"non-Optimal status must not run crossover; trace={trace:?}"
);
assert!(
trace.contains(&"cleanup_nopert"),
"cleanup fallback must run when crossover is skipped; trace={trace:?}"
);
}
#[test]
fn should_run_kept_perturbation_respects_threshold() {
assert!(!should_run_kept_perturbation(true, LARGE_PROBLEM_THRESHOLD, 1));
assert!(!should_run_kept_perturbation(
true,
LARGE_PROBLEM_THRESHOLD + 1,
0
));
assert!(should_run_kept_perturbation(
true,
LARGE_PROBLEM_THRESHOLD - 1,
1
));
assert!(!should_run_kept_perturbation(false, 10, 10));
}
#[test]
fn crossover_reserves_half_deadline_for_cleanup_fallback() {
let now = Instant::now();
let span = std::time::Duration::from_secs(100);
let deadline = now + span;
let sub = crossover_deadline_with_reserve(Some(deadline), now)
.expect("a finite deadline must yield a finite crossover sub-deadline");
assert!(
sub < deadline,
"crossover sub-deadline must precede the full deadline so cleanup keeps budget"
);
assert_eq!(
deadline.saturating_duration_since(sub),
span / 2,
"fallback reserve must be half the remaining wall-clock"
);
assert_eq!(
sub.saturating_duration_since(now),
span / 2,
"crossover must keep half the wall-clock for the certify (good) case"
);
assert_eq!(crossover_deadline_with_reserve(None, now), None);
let sub_zero = crossover_deadline_with_reserve(Some(now), now)
.expect("a finite (lapsed) deadline still yields a finite sub-deadline");
assert_eq!(
sub_zero, now,
"a fully-lapsed deadline must grant crossover zero budget, not extend it"
);
}
#[test]
fn pert_variant_not_called_above_threshold() {
let n = LARGE_PROBLEM_THRESHOLD; let rows = vec![0usize; n];
let cols: Vec<usize> = (0..n).collect();
let vals = vec![1.0_f64; n];
let a = CscMatrix::from_triplets(&rows, &cols, &vals, 1, n).unwrap();
let lp = LpProblem::new_general(
vec![1.0; n],
a,
vec![1.0],
vec![ConstraintType::Eq],
vec![(0.0, f64::INFINITY); n],
None,
)
.unwrap();
let presolve = PresolveResult::no_reduction(&lp);
let reduced = result_with_dual(SolveStatus::Infeasible, &vec![0.0; n], vec![2.0]);
let _ = drain_postsolve_pass_trace();
let _ = run_postsolve(&reduced, &presolve, &lp, None, false);
let trace = drain_postsolve_pass_trace();
assert!(
trace.contains(&"cleanup_nopert"),
"plain cleanup variant must run on the infeasible cheap dual; trace={trace:?}"
);
assert!(
!trace.contains(&"cleanup_pert"),
"redundant pert variant must be skipped above LARGE_PROBLEM_THRESHOLD; trace={trace:?}"
);
}
}