#![allow(clippy::field_reassign_with_default)]
use super::*;
use crate::options::{SimplexMethod, SolverOptions};
use crate::problem::{ConstraintType, LpProblem, SolveStatus};
use crate::sparse::CscMatrix;
use crate::test_kkt::assert_solver_invariants_lp;
use crate::tolerances::PIVOT_TOL;
fn make_lp(
c: Vec<f64>,
rows: &[usize],
cols: &[usize],
vals: &[f64],
nrows: usize,
ncols: usize,
b: Vec<f64>,
) -> LpProblem {
let a = CscMatrix::from_triplets(rows, cols, vals, nrows, ncols).unwrap();
LpProblem::new(c, a, b).unwrap()
}
#[test]
fn test_timeout_result_with_incumbent_uses_original_objective() {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![3.0, 1.0],
a,
vec![1.0],
vec![ConstraintType::Ge],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
let basis = sf.initial_basis.clone();
let x_b = sf.b.clone();
let col_scale = vec![1.0; sf.n_total];
let result = timeout_result_with_incumbent(&sf, &lp, &basis, &x_b, &col_scale, 42);
assert_eq!(result.status, SolveStatus::Timeout);
assert_eq!(
result.iterations, 42,
"iter arg は SolverResult.iterations へ反映"
);
assert_eq!(result.solution.len(), 2);
let expected_obj =
lp.c.iter()
.zip(result.solution.iter())
.map(|(&ci, &xi)| ci * xi)
.sum::<f64>();
assert!(
(result.objective - expected_obj).abs() < 1e-12,
"obj={}",
result.objective
);
}
#[test]
fn test_timeout_result_with_incumbent_no_double_count_on_shifted_lp() {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![3.0, 1.0],
a,
vec![1.0],
vec![ConstraintType::Ge],
vec![(2.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let sf = build_standard_form(&lp);
assert!(
sf.obj_offset.abs() > 1e-9,
"test LP must be shifted (obj_offset != 0); got {}",
sf.obj_offset
);
let basis = sf.initial_basis.clone();
let x_b = sf.b.clone();
let col_scale = vec![1.0; sf.n_total];
let result = timeout_result_with_incumbent(&sf, &lp, &basis, &x_b, &col_scale, 7);
let expected_obj = lp
.c
.iter()
.zip(result.solution.iter())
.map(|(&ci, &xi)| ci * xi)
.sum::<f64>();
assert!(
(result.objective - expected_obj).abs() < 1e-9,
"shifted-LP incumbent obj must be c·solution (no obj_offset double-count); \
reported {} vs c·solution {} (diff = obj_offset {} ⇒ double-count)",
result.objective,
expected_obj,
sf.obj_offset
);
}
#[test]
fn test_reconcile_final_basis_state_recomputes_xb_and_y() {
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 2, 1, 2], &[1.0, 1.0, 1.0, 1.0], 2, 3)
.unwrap();
let b = vec![3.0, 5.0];
let c = vec![4.0, 2.0, 1.0];
let basis = vec![0usize, 2usize];
let mut x_b = vec![0.0, 0.0];
let mut y = vec![0.0, 0.0];
reconcile_final_basis_state(&a, &b, &c, &basis, &mut x_b, &mut y, 50, None).unwrap();
assert!((x_b[0] + 2.0).abs() < 1e-12, "x_b[0]={}", x_b[0]);
assert!((x_b[1] - 5.0).abs() < 1e-12, "x_b[1]={}", x_b[1]);
assert!((y[0] - 4.0).abs() < 1e-12, "y[0]={}", y[0]);
assert!((y[1] + 3.0).abs() < 1e-12, "y[1]={}", y[1]);
}
#[test]
fn test_extract_solution_uses_dd_for_split_variable_cancellation() {
let sf = StandardForm {
a: CscMatrix::new(3, 3),
b: vec![0.0, 0.0, 0.0],
c: vec![0.0, 0.0, 0.0],
m: 3,
n_shifted: 3,
n_total: 3,
initial_basis: vec![0, 1, 2],
needs_artificial: vec![false, false, false],
num_artificial: 0,
obj_offset: 0.0,
n_orig: 1,
orig_var_info: vec![OrigVarInfo {
offset: 0.0,
new_vars: vec![(0, 1.0), (1, 1.0), (2, -1.0)],
}],
row_negated: vec![false, false, false],
};
let basis = vec![0usize, 1usize, 2usize];
let x_b = vec![1.0_f64, 1.0e16_f64, 1.0e16_f64];
let col_scale = vec![1.0, 1.0, 1.0];
let solution = extract_solution(&sf, &basis, &x_b, &col_scale);
assert_eq!(solution.len(), 1);
assert!(
(solution[0] - 1.0).abs() < 1e-12,
"split-variable recomposition should preserve unit residual, got {}",
solution[0]
);
}
#[test]
fn test_basic_2var() {
let lp = make_lp(
vec![-1.0, -1.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.objective - (-4.0)).abs() < PIVOT_TOL,
"Expected objective -4.0, got {}",
result.objective
);
let x1 = result.solution[0];
let x2 = result.solution[1];
assert!((-PIVOT_TOL..=3.0 + PIVOT_TOL).contains(&x1), "x1={}", x1);
assert!((-PIVOT_TOL..=3.0 + PIVOT_TOL).contains(&x2), "x2={}", x2);
assert!((x1 + x2 - 4.0).abs() < PIVOT_TOL);
}
#[test]
fn test_basic_3var() {
let lp = make_lp(
vec![-2.0, -3.0, -1.0],
&[0, 0, 0, 1, 1, 2, 2],
&[0, 1, 2, 0, 1, 1, 2],
&[1.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0],
3,
3,
vec![10.0, 14.0, 8.0],
);
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
let x = &result.solution;
assert!(x[0] >= -PIVOT_TOL);
assert!(x[1] >= -PIVOT_TOL);
assert!(x[2] >= -PIVOT_TOL);
assert!(x[0] + x[1] + x[2] <= 10.0 + PIVOT_TOL);
assert!(2.0 * x[0] + x[1] <= 14.0 + PIVOT_TOL);
assert!(x[1] + x[2] <= 8.0 + PIVOT_TOL);
assert!(
(result.objective - (-28.0)).abs() < PIVOT_TOL,
"Expected objective -28.0, got {}",
result.objective
);
}
#[test]
fn test_unbounded() {
let lp = make_lp(
vec![-1.0, 0.0],
&[0, 0],
&[0, 1],
&[1.0, -1.0],
1,
2,
vec![1.0],
);
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Unbounded);
}
#[test]
fn test_infeasible() {
let lp = make_lp(vec![1.0], &[0], &[0], &[1.0], 1, 1, vec![-1.0]);
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Infeasible);
}
#[test]
fn test_degenerate_zero_vars() {
let a = CscMatrix::new(0, 0);
let lp = LpProblem::new(vec![], a, vec![]).unwrap();
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!((result.objective).abs() < PIVOT_TOL);
}
#[test]
fn test_zero_constraints_unbounded() {
let a = CscMatrix::new(0, 1);
let lp = LpProblem::new(vec![-1.0], a, vec![]).unwrap();
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Unbounded);
}
#[test]
fn test_zero_constraints_optimal() {
let a = CscMatrix::new(0, 1);
let lp = LpProblem::new(vec![1.0], a, vec![]).unwrap();
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!((result.objective).abs() < PIVOT_TOL);
}
#[test]
fn test_solve_with_default_options() {
let lp = make_lp(
vec![-1.0, -2.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let result_default = solve(&lp);
let result_with = solve_with(&lp, &SolverOptions::default());
assert_eq!(result_default.status, result_with.status);
assert!(
(result_default.objective - result_with.objective).abs() < PIVOT_TOL,
"solve() and solve_with(default) should return same objective"
);
}
#[test]
fn test_simplex_ge_defensive() {
use crate::problem::ConstraintType;
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![-1.0, -1.0],
a,
vec![1.0],
vec![ConstraintType::Ge],
vec![(0.0, 10.0), (0.0, 10.0)],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.timeout_secs = Some(5.0);
let start = std::time::Instant::now();
let result = solve_with(&lp, &opts);
assert!(
start.elapsed().as_secs_f64() < 6.0,
"test_simplex_ge_defensive: wall-clock 6秒超過"
);
assert_eq!(
result.status,
SolveStatus::Optimal,
"Status should be Optimal"
);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.objective - (-20.0)).abs() < PIVOT_TOL,
"Expected obj=-20.0, got {}",
result.objective
);
assert!(
result.solution[0] >= -PIVOT_TOL && result.solution[0] <= 10.0 + PIVOT_TOL,
"x should be in [0, 10], got {}",
result.solution[0]
);
assert!(
result.solution[1] >= -PIVOT_TOL && result.solution[1] <= 10.0 + PIVOT_TOL,
"y should be in [0, 10], got {}",
result.solution[1]
);
assert!(
(result.solution[0] + result.solution[1] - 20.0).abs() < PIVOT_TOL,
"x + y should be 20.0, got {}",
result.solution[0] + result.solution[1]
);
}
#[test]
fn test_dual_solution_basic_le_constraints() {
let lp = make_lp(
vec![-1.0, -2.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let mut opts = SolverOptions::default();
opts.timeout_secs = Some(10.0);
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.objective - (-7.0)).abs() < PIVOT_TOL,
"Expected obj=-7.0, got {}",
result.objective
);
assert_eq!(
result.dual_solution.len(),
3,
"dual_solution should have 3 elements"
);
assert!(
(result.dual_solution[0] - (-1.0)).abs() < PIVOT_TOL,
"y[0] should be -1.0, got {}",
result.dual_solution[0]
);
assert!(
result.dual_solution[1].abs() < PIVOT_TOL,
"y[1] should be 0.0 (non-binding), got {}",
result.dual_solution[1]
);
assert!(
(result.dual_solution[2] - (-1.0)).abs() < PIVOT_TOL,
"y[2] should be -1.0, got {}",
result.dual_solution[2]
);
assert_eq!(result.slack.len(), 3, "slack should have 3 elements");
assert!(
result.slack[0].abs() < PIVOT_TOL,
"slack[0] should be 0 (binding), got {}",
result.slack[0]
);
assert!(
(result.slack[1] - 2.0).abs() < PIVOT_TOL,
"slack[1] should be 2.0 (non-binding), got {}",
result.slack[1]
);
assert!(
result.slack[2].abs() < PIVOT_TOL,
"slack[2] should be 0 (binding), got {}",
result.slack[2]
);
assert_eq!(
result.reduced_costs.len(),
2,
"reduced_costs should have 2 elements"
);
assert!(
result.reduced_costs[0].abs() < PIVOT_TOL,
"rc[0] should be 0 (basic), got {}",
result.reduced_costs[0]
);
assert!(
result.reduced_costs[1].abs() < PIVOT_TOL,
"rc[1] should be 0 (basic), got {}",
result.reduced_costs[1]
);
}
#[test]
fn test_large_coefficient_lp() {
let lp = make_lp(
vec![-1e12, 1e-12],
&[0, 0],
&[0, 1],
&[1.0, 1.0],
1,
2,
vec![1.0],
);
let result = solve(&lp);
assert!(
result.status == SolveStatus::Optimal || result.status == SolveStatus::Timeout,
"Expected Optimal or Timeout, got {:?}",
result.status
);
assert!(!result.objective.is_nan(), "Objective should not be NaN");
assert!(
result.objective.is_finite(),
"Objective should be finite for bounded LP"
);
if result.status == SolveStatus::Optimal {
assert_solver_invariants_lp(&result, &lp);
}
let lp_zero = make_lp(
vec![0.0, 0.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![2.0, 1.0, 1.0],
);
let result_zero = solve(&lp_zero);
assert_eq!(
result_zero.status,
SolveStatus::Optimal,
"Expected Optimal for zero-objective LP"
);
assert_solver_invariants_lp(&result_zero, &lp_zero);
assert!(
result_zero.objective.abs() < PIVOT_TOL,
"Expected objective=0.0, got {}",
result_zero.objective
);
}
#[test]
fn test_highly_degenerate_lp() {
let lp = make_lp(
vec![-1.0, -1.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![2.0, 1.0, 1.0],
);
let result = solve(&lp);
assert_eq!(
result.status,
SolveStatus::Optimal,
"Expected Optimal for degenerate LP"
);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.objective - (-2.0)).abs() < PIVOT_TOL,
"Expected objective=-2.0, got {}",
result.objective
);
let x1 = result.solution[0];
let x2 = result.solution[1];
assert!((x1 - 1.0).abs() < PIVOT_TOL, "Expected x1=1.0, got {}", x1);
assert!((x2 - 1.0).abs() < PIVOT_TOL, "Expected x2=1.0, got {}", x2);
}
#[test]
fn test_dual_solution_equality_constraint() {
use crate::problem::ConstraintType;
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, 2.0],
a,
vec![6.0, 5.0],
vec![ConstraintType::Eq, ConstraintType::Le],
vec![(0.0, f64::INFINITY), (0.0, f64::INFINITY)],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.timeout_secs = Some(10.0);
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.objective - 6.0).abs() < PIVOT_TOL,
"Expected obj=6.0, got {}",
result.objective
);
assert_eq!(
result.dual_solution.len(),
2,
"dual_solution should have 2 elements"
);
assert!(
(result.dual_solution[0] - 1.0).abs() < PIVOT_TOL,
"y[0] (Eq constraint shadow price) should be 1.0, got {}",
result.dual_solution[0]
);
assert!(
result.dual_solution[1].abs() < PIVOT_TOL,
"y[1] (Le constraint, non-binding) should be 0.0, got {}",
result.dual_solution[1]
);
assert_eq!(result.slack.len(), 2, "slack should have 2 elements");
assert!(
result.slack[0].abs() < PIVOT_TOL,
"slack[0] (Eq constraint) should be 0, got {}",
result.slack[0]
);
assert!(
(result.slack[1] - 5.0).abs() < PIVOT_TOL,
"slack[1] (x2<=5, non-binding) should be 5.0, got {}",
result.slack[1]
);
assert_eq!(
result.reduced_costs.len(),
2,
"reduced_costs should have 2 elements"
);
assert!(
result.reduced_costs[0].abs() < PIVOT_TOL,
"rc[0] (x1, basic) should be 0.0, got {}",
result.reduced_costs[0]
);
assert!(
(result.reduced_costs[1] - 1.0).abs() < PIVOT_TOL,
"rc[1] (x2, non-basic) should be 1.0, got {}",
result.reduced_costs[1]
);
}
#[test]
fn test_free_variables_phase_i() {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0],
a,
vec![2.0],
vec![crate::problem::ConstraintType::Eq],
vec![(f64::NEG_INFINITY, f64::INFINITY); 2],
None,
)
.unwrap();
let result = solve(&lp);
assert_eq!(
result.status,
SolveStatus::Optimal,
"Expected Optimal for free-variable LP with Eq constraint, got {:?}",
result.status
);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.solution[0] + result.solution[1] - 2.0).abs() < 1e-6,
"Expected x1+x2=2, got x1={}, x2={}, sum={}",
result.solution[0],
result.solution[1],
result.solution[0] + result.solution[1]
);
}
#[test]
fn test_hs51_feasibility_lp() {
let a = CscMatrix::from_triplets(
&[0, 1, 0, 1, 4, 5, 2, 3, 2, 3, 2, 3, 4, 5],
&[0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 4],
&[
1.0, -1.0, 3.0, -3.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -2.0, 2.0, -1.0, 1.0,
],
6,
5,
)
.unwrap();
let lp = LpProblem::new_general(
vec![0.0; 5],
a,
vec![4.0, -4.0, 0.0, 0.0, 0.0, 0.0],
vec![crate::problem::ConstraintType::Le; 6],
vec![(f64::NEG_INFINITY, f64::INFINITY); 5],
None,
)
.unwrap();
let result = solve(&lp);
assert_eq!(
result.status,
SolveStatus::Optimal,
"HS51 feasibility LP: Expected Optimal, got {:?}",
result.status
);
assert_solver_invariants_lp(&result, &lp);
let x = &result.solution;
assert!(
(x[0] + 3.0 * x[1] - 4.0).abs() < 1e-6,
"Constraint x1+3x2=4 violated: {}",
x[0] + 3.0 * x[1]
);
}
#[test]
fn test_finite_ub_zero_constraints() {
let a = CscMatrix::new(0, 1);
let lp = LpProblem::new_general(
vec![-1.0], a,
vec![],
vec![],
vec![(0.0, 3.0)], None,
)
.unwrap();
let result = solve(&lp);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.solution[0] - 3.0).abs() < PIVOT_TOL,
"Expected x=3, got {}",
result.solution[0]
);
assert!(
(result.objective - (-3.0)).abs() < PIVOT_TOL,
"Expected obj=-3, got {}",
result.objective
);
}
fn solve_m0(c: Vec<f64>, bounds: Vec<(f64, f64)>) -> (LpProblem, crate::problem::SolverResult) {
let n = c.len();
let a = CscMatrix::new(0, n);
let lp = LpProblem::new_general(c, a, vec![], vec![], bounds, None).unwrap();
let result = solve_without_presolve(&lp, &SolverOptions::default());
(lp, result)
}
#[test]
fn test_m0_zero_cost_ub_negative_regression() {
let (lp, result) = solve_m0(vec![0.0], vec![(f64::NEG_INFINITY, -1.0)]);
assert_eq!(
result.status,
SolveStatus::Optimal,
"Expected Optimal, got {:?}",
result.status
);
assert_solver_invariants_lp(&result, &lp);
let x = result.solution[0];
assert!(
x <= -1.0 + PIVOT_TOL,
"x={x} violates ub=-1 (pre-fix bug: x=0 was returned)"
);
}
#[test]
fn test_m0_positive_cost_lb_finite() {
let (lp, result) = solve_m0(vec![1.0], vec![(-3.0, f64::INFINITY)]);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.solution[0] - (-3.0)).abs() < PIVOT_TOL,
"Expected x=-3, got {}",
result.solution[0]
);
assert!(
(result.objective - (-3.0)).abs() < PIVOT_TOL,
"Expected obj=-3, got {}",
result.objective
);
}
#[test]
fn test_m0_positive_cost_lb_infinite_unbounded() {
let (_lp, result) = solve_m0(vec![1.0], vec![(f64::NEG_INFINITY, f64::INFINITY)]);
assert_eq!(
result.status,
SolveStatus::Unbounded,
"c>0 + lb=-inf must be Unbounded, got {:?}",
result.status
);
}
#[test]
fn test_m0_negative_cost_ub_infinite_unbounded() {
let (_lp, result) = solve_m0(vec![-1.0], vec![(0.0, f64::INFINITY)]);
assert_eq!(
result.status,
SolveStatus::Unbounded,
"c<0 + ub=+inf must be Unbounded, got {:?}",
result.status
);
}
#[test]
fn test_m0_negative_cost_ub_finite() {
let (lp, result) = solve_m0(vec![-1.0], vec![(0.0, 3.0)]);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
(result.solution[0] - 3.0).abs() < PIVOT_TOL,
"Expected x=3, got {}",
result.solution[0]
);
assert!(
(result.objective - (-3.0)).abs() < PIVOT_TOL,
"Expected obj=-3, got {}",
result.objective
);
}
#[test]
fn test_m0_zero_cost_lb_finite() {
let (lp, result) = solve_m0(vec![0.0], vec![(2.0, f64::INFINITY)]);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!(
result.solution[0] >= 2.0 - PIVOT_TOL,
"x={} must be >= lb=2",
result.solution[0]
);
}
#[test]
fn test_m0_zero_cost_lb_inf_ub_finite() {
let (lp, result) = solve_m0(vec![0.0], vec![(f64::NEG_INFINITY, -1.0)]);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
let x = result.solution[0];
assert!(x <= -1.0 + PIVOT_TOL, "x={x} violates ub=-1");
}
#[test]
fn test_m0_zero_cost_both_bounds_finite() {
let (lp, result) = solve_m0(vec![0.0], vec![(1.0, 5.0)]);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
let x = result.solution[0];
assert!(
(1.0 - PIVOT_TOL..=5.0 + PIVOT_TOL).contains(&x),
"x={x} outside [1,5]"
);
}
#[test]
fn test_m0_zero_cost_both_bounds_infinite() {
let (lp, result) = solve_m0(vec![0.0], vec![(f64::NEG_INFINITY, f64::INFINITY)]);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
}
#[test]
fn test_m0_multi_var_mixed_costs() {
let (lp, result) = solve_m0(
vec![-1.0, 1.0, 0.0],
vec![(0.0, 4.0), (-2.0, f64::INFINITY), (3.0, 7.0)],
);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
let x = &result.solution;
assert!(
(x[0] - 4.0).abs() < PIVOT_TOL,
"var0 (c<0): expected x=4, got {}",
x[0]
);
assert!(
(x[1] - (-2.0)).abs() < PIVOT_TOL,
"var1 (c>0): expected x=-2, got {}",
x[1]
);
assert!(
x[2] >= 3.0 - PIVOT_TOL && x[2] <= 7.0 + PIVOT_TOL,
"var2 (c=0): expected x∈[3,7], got {}",
x[2]
);
assert!(
(result.objective - (-6.0)).abs() < PIVOT_TOL,
"Expected obj=-6, got {}",
result.objective
);
}
#[test]
fn test_primal_simplex_timeout() {
let n = 200usize;
let m = 100usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let opts = SolverOptions {
deadline: Some(std::time::Instant::now() - std::time::Duration::from_secs(1)),
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Timeout);
}
#[test]
fn test_lp_timeout() {
let n = 200usize;
let m = 100usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let opts = SolverOptions {
timeout_secs: Some(0.0),
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Timeout);
}
#[test]
fn test_lp_cancel() {
use std::sync::Arc;
let n = 200usize;
let m = 100usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let opts = SolverOptions {
cancel_flag: Some(Arc::clone(&cancel)),
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Timeout);
}
#[test]
fn test_singular_initial_basis_not_optimal() {
use crate::simplex::pricing::DantzigPricing;
let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 2, 2).unwrap();
let c = vec![0.0, 0.0];
let mut x_b = vec![1.0, 0.0];
let mut basis = vec![0usize, 0];
let mut pricing = DantzigPricing;
let opts = SolverOptions::default();
let b = vec![1.0, 0.0];
let mut iters = 0usize;
let outcome = revised_simplex_core(
&a,
&mut x_b,
&c,
&b,
&mut basis,
2,
2,
2,
&mut pricing,
&opts,
&mut iters,
false,
);
assert!(!matches!(outcome, SimplexOutcome::Optimal(..)));
}
#[test]
fn test_solve_does_not_return_max_iterations() {
for method in [SimplexMethod::Primal, SimplexMethod::Dual] {
let lp = make_lp(
vec![-1.0, -1.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let opts = SolverOptions {
simplex_method: method,
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_ne!(
result.status,
SolveStatus::MaxIterations,
"method={:?}",
method
);
}
}
#[test]
fn test_refactor_failed_no_deadline_returns_timeout() {
use crate::simplex::pricing::DantzigPricing;
let a = CscMatrix::from_triplets(&[0, 0, 0], &[0, 1, 2], &[1.0, 1.0, 1.0], 1, 3).unwrap();
let c = vec![-1.0, -1.0, 0.0];
let mut x_b = vec![4.0];
let mut basis = vec![2usize];
let mut pricing = DantzigPricing;
let opts = SolverOptions {
deadline: None,
max_etas: 1,
..SolverOptions::default()
};
let b = vec![4.0];
let mut iters = 0usize;
let outcome = revised_simplex_core(
&a,
&mut x_b,
&c,
&b,
&mut basis,
1,
3,
3,
&mut pricing,
&opts,
&mut iters,
false,
);
assert!(matches!(
outcome,
SimplexOutcome::Optimal(..) | SimplexOutcome::Timeout(_) | SimplexOutcome::SingularBasis
));
}
#[test]
fn test_presolve_respects_deadline_small() {
let n = 200usize;
let m = 100usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let opts = SolverOptions {
timeout_secs: Some(0.0),
presolve: true,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Timeout);
}
#[test]
fn test_large_scale_presolve_respects_deadline() {
let n = 2000usize;
let m = 1000usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let start = std::time::Instant::now();
let opts = SolverOptions {
timeout_secs: Some(0.0),
presolve: true,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
let elapsed = start.elapsed();
assert_eq!(result.status, SolveStatus::Timeout);
assert!(
elapsed.as_secs_f64() < 0.5,
"elapsed={:.3}s",
elapsed.as_secs_f64()
);
}
#[test]
fn test_timeout_elapsed_within_budget() {
let n = 200usize;
let m = 100usize;
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..m {
for j in 0..n {
rows.push(i);
cols.push(j);
vals.push(1.0);
}
}
let lp = make_lp(vec![-1.0; n], &rows, &cols, &vals, m, n, vec![1.0; m]);
let timeout_secs = 0.01f64;
let opts = SolverOptions {
timeout_secs: Some(timeout_secs),
presolve: false,
..SolverOptions::default()
};
let start = std::time::Instant::now();
let result = solve_with(&lp, &opts);
let elapsed = start.elapsed().as_secs_f64();
assert!(matches!(
result.status,
SolveStatus::Timeout | SolveStatus::Optimal
));
assert!(
elapsed < timeout_secs * 3.0 + 0.5,
"elapsed={:.3}s",
elapsed
);
}
#[test]
fn test_no_deadline_converges_finite() {
let lp = make_lp(
vec![-1.0, -1.0],
&[0, 0, 1, 2],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0],
3,
2,
vec![4.0, 3.0, 3.0],
);
let opts = SolverOptions {
timeout_secs: None,
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
}
#[test]
fn test_extract_dual_info_ub_dual() {
let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
let problem = LpProblem::new_general(
vec![-2.0, -1.0],
a,
vec![4.0],
vec![ConstraintType::Le],
vec![(0.0, 2.0), (0.0, 3.0)],
None,
)
.unwrap();
let opts = SolverOptions {
timeout_secs: None,
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&problem, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"status should be Optimal"
);
assert_solver_invariants_lp(&result, &problem);
let x = &result.solution;
assert!(
(x[0] - 2.0).abs() < 1e-6,
"x[0]={} should be at upper bound 2.0",
x[0]
);
assert!((x[1] - 2.0).abs() < 1e-6, "x[1]={} should be 2.0", x[1]);
let rc = &result.reduced_costs;
assert!(
rc[0] <= 1e-6,
"rc[0]={} should be <= 0 (x[0] at upper bound)",
rc[0]
);
assert!(
rc[1].abs() < 1e-6,
"rc[1]={} should be ≈ 0 (x[1] is basic)",
rc[1]
);
let ub0 = 2.0_f64;
let upper_comp = (ub0 - x[0]) * (-rc[0]).max(0.0);
assert!(
upper_comp.abs() < 1e-8,
"upper complementarity={} should be ≈ 0",
upper_comp
);
}
#[test]
fn test_degenerate_eq_zero_rhs_artificials() {
use crate::problem::ConstraintType;
let a = CscMatrix::from_triplets(
&[0, 1, 3, 0, 2, 1, 2, 3],
&[0, 0, 0, 1, 1, 2, 3, 3],
&[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
4,
4,
)
.unwrap();
let lp = LpProblem::new_general(
vec![0.0, 0.0, 0.0, -1.0],
a,
vec![0.0, 0.0, 1.0, 2.0],
vec![
ConstraintType::Eq,
ConstraintType::Eq,
ConstraintType::Eq,
ConstraintType::Le,
],
vec![(0.0, f64::INFINITY); 4],
None,
)
.unwrap();
let opts = SolverOptions {
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_ne!(result.status, SolveStatus::NumericalError);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!((result.objective - (-1.0)).abs() < 1e-6);
}
#[test]
fn test_multiple_zero_rhs_eq_artificials() {
use crate::problem::ConstraintType;
let a = CscMatrix::from_triplets(
&[0, 3, 4, 0, 1, 4, 1, 2, 4, 2, 4, 3, 4],
&[0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4],
&[
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
],
5,
5,
)
.unwrap();
let lp = LpProblem::new_general(
vec![0.0, 0.0, 0.0, 0.0, -1.0],
a,
vec![0.0, 0.0, 0.0, 1.0, 2.0],
vec![
ConstraintType::Eq,
ConstraintType::Eq,
ConstraintType::Eq,
ConstraintType::Eq,
ConstraintType::Le,
],
vec![(0.0, f64::INFINITY); 5],
None,
)
.unwrap();
let opts = SolverOptions {
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_ne!(result.status, SolveStatus::NumericalError);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
assert!((result.objective - (-1.0)).abs() < 1e-6);
}
#[test]
fn test_hs51_free_var_no_singular_basis() {
let a = CscMatrix::from_triplets(
&[0, 1, 0, 1, 4, 5, 2, 3, 2, 3, 2, 3, 4, 5],
&[0, 0, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4, 4, 4],
&[
1.0, -1.0, 3.0, -3.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -2.0, 2.0, -1.0, 1.0,
],
6,
5,
)
.unwrap();
let lp = LpProblem::new_general(
vec![0.0; 5],
a,
vec![4.0, -4.0, 0.0, 0.0, 0.0, 0.0],
vec![crate::problem::ConstraintType::Le; 6],
vec![(f64::NEG_INFINITY, f64::INFINITY); 5],
None,
)
.unwrap();
let opts = SolverOptions {
presolve: false,
..SolverOptions::default()
};
let result = solve_with(&lp, &opts);
assert_ne!(result.status, SolveStatus::NumericalError);
assert_eq!(result.status, SolveStatus::Optimal);
assert_solver_invariants_lp(&result, &lp);
}
#[test]
fn primal_pivot_clean_early_exit_fires_when_no_degenerate_artificials() {
use std::sync::atomic::Ordering;
let before = primal::PIVOT_CLEAN_EARLY_EXIT_COUNT.load(Ordering::SeqCst);
let m = 4;
let rows: Vec<usize> = (0..m).collect();
let cols: Vec<usize> = (0..m).collect();
let a = CscMatrix::from_triplets(&rows, &cols, &vec![1.0f64; m], m, m).unwrap();
let lp = LpProblem::new_general(
vec![1.0f64; m],
a,
vec![1.0f64; m],
vec![ConstraintType::Eq; m],
vec![(0.0f64, f64::INFINITY); m],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false; opts.simplex_method = SimplexMethod::Primal; let result = solve_with(&lp, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"diagonal Eq LP must be Optimal"
);
assert!(
(result.objective - 4.0).abs() < 1e-6,
"obj={}",
result.objective
);
let after = primal::PIVOT_CLEAN_EARLY_EXIT_COUNT.load(Ordering::SeqCst);
assert!(
after > before,
"early-exit must fire when no degenerate artificials remain \
(before={before}, after={after}); removing the early-exit causes no-op FAIL here"
);
}
#[test]
fn primal_pivot_cleanup_runs_when_degenerate_artificial_in_basis() {
use std::sync::atomic::Ordering;
struct Case {
a: CscMatrix,
b: Vec<f64>,
c: Vec<f64>,
n: usize,
expected_obj: f64,
label: &'static str,
}
let case_a = Case {
a: CscMatrix::from_triplets(&[0, 1], &[0, 0], &[1.0, 1.0], 2, 1).unwrap(),
b: vec![1.0, 1.0],
c: vec![1.0],
n: 1,
expected_obj: 1.0,
label: "dup_x0_eq_1",
};
let case_b = Case {
a: CscMatrix::from_triplets(
&[0, 0, 1, 1, 2, 2],
&[0, 1, 0, 1, 0, 1],
&[1.0, 1.0, 1.0, 1.0, 1.0, 1.0],
3,
2,
)
.unwrap(),
b: vec![2.0, 2.0, 2.0],
c: vec![1.0, 1.0],
n: 2,
expected_obj: 2.0,
label: "triple_x0_plus_x1_eq_2",
};
for case in [case_a, case_b] {
let before = primal::PIVOT_CLEAN_CLEANUP_RAN_COUNT.load(Ordering::SeqCst);
let lp = LpProblem::new_general(
case.c,
case.a,
case.b.clone(),
vec![ConstraintType::Eq; case.b.len()],
vec![(0.0, f64::INFINITY); case.n],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false; opts.simplex_method = SimplexMethod::Primal; let result = solve_with(&lp, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"[{}] redundant Eq LP must be Optimal",
case.label
);
assert!(
(result.objective - case.expected_obj).abs() < 1e-6,
"[{}] obj={} expected={}",
case.label,
result.objective,
case.expected_obj
);
let after = primal::PIVOT_CLEAN_CLEANUP_RAN_COUNT.load(Ordering::SeqCst);
assert!(
after > before,
"[{}] cleanup must run (early-exit must NOT fire) while a degenerate \
artificial is basic (before={before}, after={after}); a mis-firing \
early-exit would strand the artificial and stagnate this counter",
case.label
);
}
}
#[test]
fn b2_obj_progress_reset_fires_on_improving_objective() {
use std::sync::atomic::Ordering;
let before = OBJ_PROGRESS_RESET_COUNT.load(Ordering::SeqCst);
{
let a = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 1, 2], &[1.0, 1.0, 1.0, 1.0], 2, 3)
.unwrap();
let lp = LpProblem::new_general(
vec![1.0, 1.0, 1.0],
a,
vec![3.0, 2.0],
vec![ConstraintType::Eq; 2],
vec![(0.0, f64::INFINITY); 3],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false;
let result = solve_with(&lp, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"Pattern A must be Optimal"
);
assert!(
(result.objective - 3.0).abs() < 1e-6,
"Pattern A obj={}",
result.objective
);
}
{
let a = CscMatrix::from_triplets(
&[0, 0, 0, 1, 1, 1, 2, 2, 2],
&[0, 1, 2, 0, 1, 2, 0, 1, 2],
&[6.0, 4.0, 2.0, 3.0, 2.0, 5.0, 5.0, 6.0, 5.0],
3,
3,
)
.unwrap();
let lp = LpProblem::new_general(
vec![-5.0, -4.0, -3.0],
a,
vec![240.0, 270.0, 420.0],
vec![ConstraintType::Le; 3],
vec![(0.0, f64::INFINITY); 3],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false;
let result = solve_with(&lp, &opts);
assert_eq!(
result.status,
SolveStatus::Optimal,
"Pattern B must be Optimal"
);
assert!(
result.objective < 0.0,
"Pattern B must have negative optimal, got {}",
result.objective
);
}
let after = OBJ_PROGRESS_RESET_COUNT.load(Ordering::SeqCst);
assert!(
after > before,
"OBJ_PROGRESS_RESET_COUNT must increment when objective improves \
(before={before}, after={after}); a non-incrementing counter means \
best_obj was initialized to INFINITY (B2 バグ復帰) making progress_eps=∞ \
so the improvement condition never fires"
);
}
#[test]
fn batch_pivot_out_uses_single_lu_and_no_btrans() {
const N: usize = 50;
let m = N + 1;
let n = N + 1;
let mut trip_rows = vec![0usize];
let mut trip_cols = vec![0usize];
let mut trip_vals = vec![1.0f64];
for i in 1..=N {
trip_rows.extend_from_slice(&[i, i]);
trip_cols.extend_from_slice(&[0, i]);
trip_vals.extend_from_slice(&[1.0, -1.0]);
}
let a = CscMatrix::from_triplets(&trip_rows, &trip_cols, &trip_vals, m, n).unwrap();
let lp = LpProblem::new_general(
vec![1.0f64; n],
a,
vec![1.0f64; m],
vec![ConstraintType::Eq; m],
vec![(0.0f64, f64::INFINITY); n],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false;
opts.simplex_method = SimplexMethod::Primal;
opts.use_lp_crash_basis = false;
let btran_before = primal::PIVOT_OUT_BTRAN_COUNT.with(|c| c.get());
let batch_lu_before = primal::PIVOT_OUT_BATCH_LU_COUNT.with(|c| c.get());
let result = solve_with(&lp, &opts);
let btran_after = primal::PIVOT_OUT_BTRAN_COUNT.with(|c| c.get());
let batch_lu_after = primal::PIVOT_OUT_BATCH_LU_COUNT.with(|c| c.get());
assert_eq!(
result.status,
SolveStatus::Optimal,
"LP with {N} degenerate artificials must reach Optimal; got {:?}",
result.status
);
assert!(
(result.objective - 1.0).abs() < 1e-6,
"expected obj=1.0, got {}",
result.objective
);
assert_eq!(
btran_after, btran_before,
"batch path must issue zero BTRANs for {N} degenerate artificials; \
sequential path would add {N} (before={btran_before}, after={btran_after})"
);
assert_eq!(
batch_lu_after,
batch_lu_before + 1,
"batch path must call exactly one LU for {N} matched rows; \
sequential path never increments this counter \
(before={batch_lu_before}, after={batch_lu_after})"
);
}
#[test]
fn batch_pivot_out_falls_back_to_sequential_for_ill_conditioned_basis() {
const DELTA: f64 = 0.001;
let a = CscMatrix::from_triplets(
&[0, 1, 2, 0, 1, 0, 2, 0, 1],
&[0, 0, 0, 1, 1, 2, 2, 3, 3],
&[1.0, 1.0, 1.0, 1.0, -0.5, 0.7, 1.0, 1.0, 1.0 + DELTA],
3,
4,
)
.unwrap();
let lp = LpProblem::new_general(
vec![0.0; 4],
a,
vec![1.0; 3],
vec![ConstraintType::Eq; 3],
vec![(0.0, f64::INFINITY); 4],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false;
opts.use_lp_crash_basis = false;
opts.simplex_method = SimplexMethod::Primal;
let fallback_before = primal::PIVOT_OUT_SEQUENTIAL_FALLBACK_COUNT.with(|c| c.get());
let result = solve_with(&lp, &opts);
let fallback_after = primal::PIVOT_OUT_SEQUENTIAL_FALLBACK_COUNT.with(|c| c.get());
assert_eq!(
result.status,
SolveStatus::Optimal,
"LP with ill-conditioned batch must reach Optimal via sequential fallback; got {:?}",
result.status
);
assert!(
result.objective.abs() < 1e-8,
"trivial zero-cost objective must be 0.0; got {}",
result.objective
);
assert!(
fallback_after > fallback_before,
"FTRAN stability check must detect ill-conditioned batch (ratio δ={DELTA} < \
PIVOT_STABILITY_THRESHOLD=0.01) and trigger sequential fallback; \
PIVOT_OUT_SEQUENTIAL_FALLBACK_COUNT must increase \
(before={fallback_before}, after={fallback_after}). \
No-op: removing the stability check keeps this at zero — assertion fails."
);
}
#[test]
fn batch_pivot_out_uncommitted_rows_fallback_fires_for_rank_saturated_batch() {
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![0.0, 0.0],
a,
vec![0.0, 0.0],
vec![ConstraintType::Eq; 2],
vec![(0.0, f64::INFINITY); 2],
None,
)
.unwrap();
let mut opts = SolverOptions::default();
opts.presolve = false;
opts.use_lp_crash_basis = false;
opts.simplex_method = SimplexMethod::Primal;
let uncommitted_before =
primal::PIVOT_OUT_UNCOMMITTED_SEQUENTIAL_COUNT.with(|c| c.get());
let btran_before = primal::PIVOT_OUT_BTRAN_COUNT.with(|c| c.get());
let result = solve_with(&lp, &opts);
let uncommitted_after =
primal::PIVOT_OUT_UNCOMMITTED_SEQUENTIAL_COUNT.with(|c| c.get());
let btran_after = primal::PIVOT_OUT_BTRAN_COUNT.with(|c| c.get());
assert_eq!(
result.status,
SolveStatus::Optimal,
"rank-saturated batch LP must reach Optimal via uncommitted_rows sequential \
fallback; got {:?}",
result.status
);
assert!(
uncommitted_after > uncommitted_before,
"uncommitted_rows fallback must fire (match_offset==0 path); \
PIVOT_OUT_UNCOMMITTED_SEQUENTIAL_COUNT must increase \
(before={uncommitted_before}, after={uncommitted_after}). \
No-op: removing match_offset==0 uncommitted_rows path keeps this zero."
);
assert!(
btran_after > btran_before,
"sequential BTRAN must be issued for uncommitted rows \
(before={btran_before}, after={btran_after}). \
No-op: skipping uncommitted_rows path issues no BTRANs — assertion fails."
);
}
#[test]
fn batch_pivot_out_cleans_uncommitted_matched_rows_sequentially() {
let a_ext = CscMatrix::from_triplets(
&[
0, 1, 1, 2, 0, 1, 2, 2, 1, 2, ],
&[
0, 0, 1, 1, 2, 2, 2, 3, 4, 5, ],
&[
1.0, 1.0, 2.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, ],
3,
6,
)
.unwrap();
let sf = super::standard_form::StandardForm {
a: CscMatrix::from_triplets(&[], &[], &[], 3, 4).unwrap(),
b: vec![1.0, 0.0, 0.0],
c: vec![0.0; 4],
m: 3,
n_shifted: 4,
n_total: 4,
initial_basis: vec![0, 4, 5],
needs_artificial: vec![false, true, true],
num_artificial: 2,
obj_offset: 0.0,
n_orig: 4,
orig_var_info: Vec::new(),
row_negated: vec![false; 3],
};
let mut basis = vec![0usize, 4, 5];
let x_b = vec![1.0, 0.0, 0.0];
let opts = SolverOptions::default();
primal::test_pivot_out_degenerate_artificials(&a_ext, &mut basis, &x_b, &sf, &opts);
assert!(
basis.iter().all(|&col| col < sf.n_total),
"all degenerate artificials must be pivoted out; got basis={basis:?}"
);
}