use pounce_common::types::{Number, lower_bound_present, upper_bound_present};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum VarRecovery {
Kept(usize),
Constant(Number),
Affine {
rep: usize,
coeff: Number,
offset: Number,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ElimStep {
pub row: usize,
pub var: usize,
pub pivot: Number,
}
#[derive(Debug, Clone, Copy)]
pub struct PlanConfig {
pub eq_tol: Number,
pub coeff_tol: Number,
pub feas_tol: Number,
pub max_passes: usize,
}
impl Default for PlanConfig {
fn default() -> Self {
Self {
eq_tol: 1e-12,
coeff_tol: 1e-12,
feas_tol: 1e-8,
max_passes: 50,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PlanInput<'a> {
pub n_vars: usize,
pub n_rows: usize,
pub rows: &'a [Vec<(usize, Number)>],
pub row_const: &'a [Number],
pub g_l: &'a [Number],
pub g_u: &'a [Number],
pub eligible: &'a [bool],
pub x_l: &'a [Number],
pub x_u: &'a [Number],
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct LinearEqElimReport {
pub n_constant_vars: usize,
pub n_aggregated_vars: usize,
pub n_rows_eliminated: usize,
pub n_redundant_rows: usize,
pub passes: usize,
pub pass_cap_hit: bool,
pub infeasible: bool,
}
#[derive(Debug, Clone, Default)]
pub struct EliminationPlan {
pub n_full: usize,
pub m_full: usize,
pub recovery: Vec<VarRecovery>,
pub vars_kept: Vec<usize>,
pub row_kept: Vec<bool>,
pub rows_kept: Vec<usize>,
pub x_l_red: Vec<Number>,
pub x_u_red: Vec<Number>,
pub x_l_src: Vec<usize>,
pub x_u_src: Vec<usize>,
pub steps: Vec<ElimStep>,
pub parent: Vec<Option<(usize, Number)>>,
pub report: LinearEqElimReport,
}
impl EliminationPlan {
pub fn identity(n_vars: usize, n_rows: usize, x_l: &[Number], x_u: &[Number]) -> Self {
Self {
n_full: n_vars,
m_full: n_rows,
recovery: (0..n_vars).map(VarRecovery::Kept).collect(),
vars_kept: (0..n_vars).collect(),
row_kept: vec![true; n_rows],
rows_kept: (0..n_rows).collect(),
x_l_red: x_l.to_vec(),
x_u_red: x_u.to_vec(),
x_l_src: (0..n_vars).collect(),
x_u_src: (0..n_vars).collect(),
steps: Vec::new(),
parent: vec![None; n_vars],
report: LinearEqElimReport::default(),
}
}
pub fn is_identity(&self) -> bool {
self.steps.is_empty() && self.report.n_redundant_rows == 0
}
pub fn n_reduced_vars(&self) -> usize {
self.vars_kept.len()
}
pub fn n_reduced_rows(&self) -> usize {
self.rows_kept.len()
}
pub fn lift_x(&self, x_red: &[Number], out: &mut [Number]) {
debug_assert_eq!(out.len(), self.n_full);
for (red, &full) in self.vars_kept.iter().enumerate() {
out[full] = x_red[red];
}
for (i, rec) in self.recovery.iter().enumerate() {
match *rec {
VarRecovery::Kept(_) => {}
VarRecovery::Constant(c) => out[i] = c,
VarRecovery::Affine { rep, coeff, offset } => out[i] = coeff * out[rep] + offset,
}
}
}
pub fn project_x(&self, x_full: &[Number], out: &mut [Number]) {
for (red, &full) in self.vars_kept.iter().enumerate() {
out[red] = x_full[full];
}
}
}
struct Box2 {
lo: Vec<Number>,
hi: Vec<Number>,
lo_src: Vec<usize>,
hi_src: Vec<usize>,
}
impl Box2 {
fn from_declared(x_l: &[Number], x_u: &[Number]) -> Self {
Self {
lo_src: (0..x_l.len()).collect(),
hi_src: (0..x_u.len()).collect(),
lo: x_l
.iter()
.map(|&v| {
if lower_bound_present(v) {
v
} else {
Number::NEG_INFINITY
}
})
.collect(),
hi: x_u
.iter()
.map(|&v| {
if upper_bound_present(v) {
v
} else {
Number::INFINITY
}
})
.collect(),
}
}
}
struct Substitutions {
rep: Vec<usize>,
to_root: Vec<(Number, Number)>,
cluster_size: Vec<usize>,
root_const: Vec<Option<Number>>,
}
impl Substitutions {
fn new(n: usize) -> Self {
Self {
rep: (0..n).collect(),
to_root: vec![(1.0, 0.0); n],
cluster_size: vec![1; n],
root_const: vec![None; n],
}
}
fn find(&mut self, i: usize) -> (usize, Number, Number) {
let mut cur = i;
let mut path: Vec<usize> = Vec::new();
while self.rep[cur] != cur {
path.push(cur);
cur = self.rep[cur];
}
let root = cur;
let (mut acc_a, mut acc_b) = (1.0, 0.0);
for &node in path.iter().rev() {
let (a, b) = self.to_root[node];
let na = a * acc_a;
let nb = a * acc_b + b;
self.rep[node] = root;
self.to_root[node] = (na, nb);
acc_a = na;
acc_b = nb;
}
if i == root {
(root, 1.0, 0.0)
} else {
let (a, b) = self.to_root[i];
(root, a, b)
}
}
}
pub fn build_plan(input: &PlanInput<'_>, cfg: &PlanConfig) -> EliminationPlan {
let n = input.n_vars;
let m = input.n_rows;
let identity = || EliminationPlan::identity(n, m, input.x_l, input.x_u);
if n == 0 {
return identity();
}
let mut subs = Substitutions::new(n);
let mut bounds = Box2::from_declared(input.x_l, input.x_u);
let mut parent: Vec<Option<(usize, Number)>> = vec![None; n];
let mut steps: Vec<ElimStep> = Vec::new();
let mut row_consumed = vec![false; m];
let mut redundant_rows: Vec<usize> = Vec::new();
let mut report = LinearEqElimReport::default();
for j in 0..n {
let (lo, hi) = (bounds.lo[j], bounds.hi[j]);
if !lo.is_finite() || !hi.is_finite() {
continue;
}
if hi - lo <= cfg.eq_tol * lo.abs().max(hi.abs()).max(1.0) {
subs.root_const[j] = Some(0.5 * (lo + hi));
report.n_constant_vars += 1;
}
}
let mut candidates: Vec<usize> = (0..m)
.filter(|&r| {
input.eligible[r]
&& lower_bound_present(input.g_l[r])
&& upper_bound_present(input.g_u[r])
&& (input.g_u[r] - input.g_l[r]).abs() <= cfg.eq_tol * input.g_l[r].abs().max(1.0)
})
.collect();
if candidates.is_empty() {
return finish(
n,
m,
input,
subs,
bounds,
parent,
steps,
redundant_rows,
report,
);
}
let mut terms: Vec<(usize, Number)> = Vec::new();
for pass in 0..cfg.max_passes.max(1) {
let mut changed = false;
report.passes = pass + 1;
for &r in &candidates {
if row_consumed[r] {
continue;
}
terms.clear();
let mut rhs = input.g_l[r] - input.row_const[r];
let mut row_scale: Number = 0.0;
let mut ok = true;
for &(j, a) in &input.rows[r] {
if a == 0.0 {
continue;
}
if j >= n {
ok = false;
break;
}
let (root, ra, rb) = subs.find(j);
row_scale = row_scale.max((a * ra).abs());
match subs.root_const[root] {
Some(c) => rhs -= a * (ra * c + rb),
None => {
rhs -= a * rb;
match terms.iter_mut().find(|(v, _)| *v == root) {
Some(slot) => slot.1 += a * ra,
None => terms.push((root, a * ra)),
}
}
}
}
if !ok || !rhs.is_finite() {
continue;
}
let drop_below = cfg.coeff_tol * row_scale.max(1.0);
terms.retain(|&(_, a)| a.abs() > drop_below);
match terms.len() {
0 => {
if rhs.abs() <= cfg.feas_tol * row_scale.max(1.0) {
row_consumed[r] = true;
redundant_rows.push(r);
report.n_redundant_rows += 1;
changed = true;
} else {
report.infeasible = true;
return abandoned(identity(), report);
}
}
1 => {
let (v, a) = terms[0];
let value = rhs / a;
if !value.is_finite() {
continue;
}
match clamp_into_box(value, bounds.lo[v], bounds.hi[v], cfg.feas_tol) {
Some(pinned) => {
subs.root_const[v] = Some(pinned);
bounds.lo[v] = pinned;
bounds.hi[v] = pinned;
row_consumed[r] = true;
steps.push(ElimStep {
row: r,
var: v,
pivot: a,
});
report.n_constant_vars += 1;
report.n_rows_eliminated += 1;
changed = true;
}
None => {
report.infeasible = true;
return abandoned(identity(), report);
}
}
}
2 => {
let (v0, a0) = terms[0];
let (v1, a1) = terms[1];
let (elim, keep, a_elim, a_keep) = if a0.abs() > 4.0 * a1.abs() {
(v0, v1, a0, a1)
} else if a1.abs() > 4.0 * a0.abs() {
(v1, v0, a1, a0)
} else if subs.cluster_size[v0] <= subs.cluster_size[v1] {
(v0, v1, a0, a1)
} else {
(v1, v0, a1, a0)
};
let alpha = -a_keep / a_elim;
let beta = rhs / a_elim;
if !alpha.is_finite() || !beta.is_finite() || alpha == 0.0 {
continue;
}
if !transfer_bounds(&mut bounds, elim, keep, alpha, beta, cfg.feas_tol) {
report.infeasible = true;
return abandoned(identity(), report);
}
subs.rep[elim] = keep;
subs.to_root[elim] = (alpha, beta);
subs.cluster_size[keep] += subs.cluster_size[elim];
parent[elim] = Some((keep, alpha));
row_consumed[r] = true;
steps.push(ElimStep {
row: r,
var: elim,
pivot: a_elim,
});
report.n_aggregated_vars += 1;
report.n_rows_eliminated += 1;
changed = true;
}
_ => {}
}
}
candidates.retain(|&r| !row_consumed[r]);
if !changed || candidates.is_empty() {
break;
}
if pass + 1 == cfg.max_passes.max(1) {
report.pass_cap_hit = true;
}
}
finish(
n,
m,
input,
subs,
bounds,
parent,
steps,
redundant_rows,
report,
)
}
fn abandoned(mut plan: EliminationPlan, report: LinearEqElimReport) -> EliminationPlan {
plan.report = LinearEqElimReport {
infeasible: report.infeasible,
passes: report.passes,
..LinearEqElimReport::default()
};
plan
}
fn clamp_into_box(value: Number, lo: Number, hi: Number, tol: Number) -> Option<Number> {
let scale = value
.abs()
.max(lo.abs().min(1e19))
.max(hi.abs().min(1e19))
.max(1.0);
if lo.is_finite() && value < lo {
if lo - value > tol * scale {
return None;
}
return Some(lo);
}
if hi.is_finite() && value > hi {
if value - hi > tol * scale {
return None;
}
return Some(hi);
}
Some(value)
}
fn transfer_bounds(
bounds: &mut Box2,
elim: usize,
keep: usize,
alpha: Number,
beta: Number,
tol: Number,
) -> bool {
let (lo_e, hi_e) = (bounds.lo[elim], bounds.hi[elim]);
let a = (lo_e - beta) / alpha;
let b = (hi_e - beta) / alpha;
let (mut derived_lo, mut derived_hi) = if alpha > 0.0 { (a, b) } else { (b, a) };
if !derived_lo.is_finite() {
derived_lo = Number::NEG_INFINITY;
}
if !derived_hi.is_finite() {
derived_hi = Number::INFINITY;
}
let (src_lo, src_hi) = if alpha > 0.0 {
(bounds.lo_src[elim], bounds.hi_src[elim])
} else {
(bounds.hi_src[elim], bounds.lo_src[elim])
};
if derived_lo > bounds.lo[keep] {
bounds.lo[keep] = derived_lo;
bounds.lo_src[keep] = src_lo;
}
if derived_hi < bounds.hi[keep] {
bounds.hi[keep] = derived_hi;
bounds.hi_src[keep] = src_hi;
}
let (lo, hi) = (bounds.lo[keep], bounds.hi[keep]);
if lo.is_finite() && hi.is_finite() && lo > hi {
let scale = lo.abs().max(hi.abs()).max(1.0);
if lo - hi > tol * scale {
return false;
}
let mid = 0.5 * (lo + hi);
bounds.lo[keep] = mid;
bounds.hi[keep] = mid;
}
true
}
#[allow(clippy::too_many_arguments)]
fn finish(
n: usize,
m: usize,
input: &PlanInput<'_>,
mut subs: Substitutions,
bounds: Box2,
parent: Vec<Option<(usize, Number)>>,
steps: Vec<ElimStep>,
redundant_rows: Vec<usize>,
report: LinearEqElimReport,
) -> EliminationPlan {
if steps.is_empty() && redundant_rows.is_empty() {
let mut plan = EliminationPlan::identity(n, m, input.x_l, input.x_u);
plan.report = report;
return plan;
}
let mut recovery = vec![VarRecovery::Kept(usize::MAX); n];
let mut vars_kept: Vec<usize> = Vec::new();
let mut reduced_of = vec![usize::MAX; n];
for (j, slot) in reduced_of.iter_mut().enumerate() {
let (root, _, _) = subs.find(j);
if root == j && subs.root_const[j].is_none() {
*slot = vars_kept.len();
vars_kept.push(j);
}
}
if vars_kept.is_empty() {
let mut plan = EliminationPlan::identity(n, m, input.x_l, input.x_u);
plan.report = LinearEqElimReport {
passes: report.passes,
..LinearEqElimReport::default()
};
return plan;
}
for j in 0..n {
let (root, a, b) = subs.find(j);
recovery[j] = match subs.root_const[root] {
Some(c) => VarRecovery::Constant(a * c + b),
None if root == j => VarRecovery::Kept(reduced_of[j]),
None => VarRecovery::Affine {
rep: root,
coeff: a,
offset: b,
},
};
}
let mut row_kept = vec![true; m];
for s in &steps {
row_kept[s.row] = false;
}
for &r in &redundant_rows {
row_kept[r] = false;
}
let rows_kept: Vec<usize> = (0..m).filter(|&r| row_kept[r]).collect();
let mut x_l_red = Vec::with_capacity(vars_kept.len());
let mut x_u_red = Vec::with_capacity(vars_kept.len());
let mut x_l_src = Vec::with_capacity(vars_kept.len());
let mut x_u_src = Vec::with_capacity(vars_kept.len());
for &j in &vars_kept {
if bounds.lo[j].is_finite() {
x_l_red.push(bounds.lo[j]);
x_l_src.push(bounds.lo_src[j]);
} else {
x_l_red.push(input.x_l[j]);
x_l_src.push(j);
}
if bounds.hi[j].is_finite() {
x_u_red.push(bounds.hi[j]);
x_u_src.push(bounds.hi_src[j]);
} else {
x_u_red.push(input.x_u[j]);
x_u_src.push(j);
}
}
EliminationPlan {
n_full: n,
m_full: m,
recovery,
vars_kept,
row_kept,
rows_kept,
x_l_red,
x_u_red,
x_l_src,
x_u_src,
steps,
parent,
report,
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Fixture {
rows: Vec<Vec<(usize, Number)>>,
row_const: Vec<Number>,
g_l: Vec<Number>,
g_u: Vec<Number>,
eligible: Vec<bool>,
x_l: Vec<Number>,
x_u: Vec<Number>,
n: usize,
}
impl Fixture {
fn new(n: usize) -> Self {
Self {
rows: Vec::new(),
row_const: Vec::new(),
g_l: Vec::new(),
g_u: Vec::new(),
eligible: Vec::new(),
x_l: vec![-1e19; n],
x_u: vec![1e19; n],
n,
}
}
fn eq(mut self, entries: &[(usize, Number)], b: Number) -> Self {
self.rows.push(entries.to_vec());
self.row_const.push(0.0);
self.g_l.push(b);
self.g_u.push(b);
self.eligible.push(true);
self
}
fn opaque(mut self, entries: &[(usize, Number)], lo: Number, hi: Number) -> Self {
self.rows.push(entries.to_vec());
self.row_const.push(0.0);
self.g_l.push(lo);
self.g_u.push(hi);
self.eligible.push(false);
self
}
fn bounds(mut self, j: usize, lo: Number, hi: Number) -> Self {
self.x_l[j] = lo;
self.x_u[j] = hi;
self
}
fn plan(&self) -> EliminationPlan {
build_plan(
&PlanInput {
n_vars: self.n,
n_rows: self.rows.len(),
rows: &self.rows,
row_const: &self.row_const,
g_l: &self.g_l,
g_u: &self.g_u,
eligible: &self.eligible,
x_l: &self.x_l,
x_u: &self.x_u,
},
&PlanConfig::default(),
)
}
}
fn assert_rows_hold(f: &Fixture, plan: &EliminationPlan, y: &[Number]) {
let mut x = vec![0.0; f.n];
plan.lift_x(y, &mut x);
for (r, entries) in f.rows.iter().enumerate() {
if plan.row_kept[r] || !f.eligible[r] {
continue;
}
let lhs: Number =
entries.iter().map(|&(j, a)| a * x[j]).sum::<Number>() + f.row_const[r];
assert!(
(lhs - f.g_l[r]).abs() < 1e-9,
"dropped row {r} violated: {lhs} != {}",
f.g_l[r]
);
}
}
#[test]
fn singleton_row_pins_its_variable() {
let f = Fixture::new(2).eq(&[(0, 2.0)], 6.0);
let p = f.plan();
assert_eq!(p.recovery[0], VarRecovery::Constant(3.0));
assert_eq!(p.recovery[1], VarRecovery::Kept(0));
assert_eq!(p.vars_kept, vec![1]);
assert_eq!(p.rows_kept, Vec::<usize>::new());
assert_eq!(p.report.n_constant_vars, 1);
assert_rows_hold(&f, &p, &[7.5]);
}
#[test]
fn free_free_pair_aggregates_with_no_anchor() {
let f = Fixture::new(2).eq(&[(0, 1.0), (1, -1.0)], 0.0);
let p = f.plan();
assert_eq!(p.n_reduced_vars(), 1);
assert_eq!(p.n_reduced_rows(), 0);
assert_eq!(p.report.n_aggregated_vars, 1);
assert_rows_hold(&f, &p, &[4.25]);
let mut x = vec![0.0; 2];
p.lift_x(&[4.25], &mut x);
assert!((x[0] - x[1]).abs() < 1e-12);
}
#[test]
fn chains_propagate_regardless_of_row_order() {
let f = Fixture::new(5)
.eq(&[(2, 1.0), (3, -1.0)], 0.0)
.eq(&[(1, 1.0), (2, -1.0)], 0.0)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.eq(&[(3, 2.0)], 8.0);
let p = f.plan();
assert_eq!(p.vars_kept, vec![4]);
assert_eq!(p.n_reduced_rows(), 0);
let mut x = vec![0.0; 5];
p.lift_x(&[9.0], &mut x);
for (j, v) in x.iter().take(4).enumerate() {
assert!((v - 4.0).abs() < 1e-12, "x{j} = {v}");
}
assert_rows_hold(&f, &p, &[9.0]);
}
#[test]
fn a_fully_determined_model_stands_down() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.eq(&[(1, 2.0)], 8.0);
let p = f.plan();
assert!(p.is_identity());
assert_eq!(p.n_reduced_vars(), 2);
assert_eq!(p.n_reduced_rows(), 2);
}
#[test]
fn chain_with_a_free_tail_collapses_to_one_column() {
let f = Fixture::new(4)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.eq(&[(1, 1.0), (2, -1.0)], 0.0)
.eq(&[(2, 1.0), (3, -1.0)], 0.0);
let p = f.plan();
assert_eq!(p.n_reduced_vars(), 1);
assert_eq!(p.n_reduced_rows(), 0);
let mut x = vec![0.0; 4];
p.lift_x(&[2.5], &mut x);
for v in &x {
assert!((v - 2.5).abs() < 1e-12, "{x:?}");
}
assert_rows_hold(&f, &p, &[2.5]);
}
#[test]
fn every_recovery_representative_is_a_survivor() {
let f = Fixture::new(5)
.eq(&[(0, 1.0), (1, -2.0)], 1.0)
.eq(&[(1, 1.0), (2, -3.0)], 2.0)
.eq(&[(2, 1.0), (3, -4.0)], 3.0);
let p = f.plan();
for rec in &p.recovery {
if let VarRecovery::Affine { rep, coeff, .. } = *rec {
assert!(
matches!(p.recovery[rep], VarRecovery::Kept(_)),
"representative {rep} is not a survivor"
);
assert!(coeff != 0.0);
}
}
assert_rows_hold(&f, &p, &vec![1.0; p.n_reduced_vars()]);
}
#[test]
fn a_fixed_variable_exposes_a_two_term_row() {
let f = Fixture::new(3)
.eq(&[(0, 1.0), (1, 1.0), (2, 1.0)], 10.0)
.bounds(2, 4.0, 4.0);
let p = f.plan();
assert_eq!(p.recovery[2], VarRecovery::Constant(4.0));
assert_eq!(p.n_reduced_vars(), 1);
let mut x = vec![0.0; 3];
p.lift_x(&[1.5], &mut x);
assert!((x[0] + x[1] + x[2] - 10.0).abs() < 1e-12, "{x:?}");
}
#[test]
fn bounds_transfer_onto_the_survivor() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, -2.0)], 0.0)
.bounds(0, 4.0, 10.0);
let p = f.plan();
assert_eq!(p.vars_kept, vec![1]);
assert!((p.x_l_red[0] - 2.0).abs() < 1e-12, "{:?}", p.x_l_red);
assert!((p.x_u_red[0] - 5.0).abs() < 1e-12, "{:?}", p.x_u_red);
}
#[test]
fn negative_coefficient_flips_the_transferred_bounds() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, 1.0)], 0.0)
.bounds(0, 1.0, 3.0);
let p = f.plan();
assert_eq!(p.vars_kept, vec![1]);
assert!((p.x_l_red[0] + 3.0).abs() < 1e-12, "{:?}", p.x_l_red);
assert!((p.x_u_red[0] + 1.0).abs() < 1e-12, "{:?}", p.x_u_red);
}
#[test]
fn a_transferred_bound_names_the_column_it_came_from() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, -2.0)], 0.0)
.bounds(0, -1e19, 1.0)
.bounds(1, -4.0, 1e19);
let p = f.plan();
assert_eq!(p.vars_kept, vec![1]);
assert!((p.x_u_red[0] - 0.5).abs() < 1e-12, "{:?}", p.x_u_red);
assert_eq!(p.x_u_src, vec![0], "the upper bound is x0's");
assert_eq!(p.x_l_src, vec![1], "the lower bound is x1's own");
}
#[test]
fn a_negative_coefficient_flips_which_side_the_provenance_lands_on() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, 2.0)], 0.0)
.bounds(0, -1e19, 1.0);
let p = f.plan();
assert_eq!(p.vars_kept, vec![1]);
assert!((p.x_l_red[0] + 0.5).abs() < 1e-12, "{:?}", p.x_l_red);
assert_eq!(p.x_l_src, vec![0], "x1's lower bound is x0's upper bound");
assert_eq!(p.x_u_src, vec![1], "nothing tightened x1 from above");
}
#[test]
fn provenance_survives_a_chain_of_transfers() {
let f = Fixture::new(3)
.eq(&[(0, 1.0), (1, 1.0)], 0.0)
.eq(&[(1, 1.0), (2, 0.1)], 0.0)
.bounds(0, -1e19, 1.0);
let p = f.plan();
assert_eq!(p.vars_kept, vec![2]);
assert!((p.x_u_red[0] - 10.0).abs() < 1e-12, "{:?}", p.x_u_red);
assert_eq!(p.x_u_src, vec![0]);
assert_eq!(p.x_l_src, vec![2], "nothing tightened x2 from below");
assert_eq!(
p.recovery[0],
VarRecovery::Affine {
rep: 2,
coeff: 0.1,
offset: 0.0
}
);
}
#[test]
fn a_tied_transfer_leaves_the_provenance_on_the_survivor() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, -2.0)], 0.0)
.bounds(0, -1e19, 1.0)
.bounds(1, -1e19, 0.5);
let p = f.plan();
assert_eq!(p.vars_kept, vec![1]);
assert!((p.x_u_red[0] - 0.5).abs() < 1e-12, "{:?}", p.x_u_red);
assert_eq!(p.x_u_src, vec![1]);
}
#[test]
fn provenance_and_the_recovery_map_agree_on_every_reduced_bound() {
let f = Fixture::new(4)
.eq(&[(0, 1.0), (1, 3.0)], 6.0)
.eq(&[(1, 2.0), (2, -0.5)], 1.0)
.opaque(&[(2, 1.0), (3, 1.0)], 0.0, 10.0)
.bounds(0, -2.0, 7.0)
.bounds(1, -5.0, 5.0)
.bounds(2, -20.0, 20.0);
let p = f.plan();
assert!(!p.is_identity());
for (red, &kept) in p.vars_kept.iter().enumerate() {
for (src, red_bound, upper) in [
(p.x_l_src[red], p.x_l_red[red], false),
(p.x_u_src[red], p.x_u_red[red], true),
] {
if src == kept || !red_bound.is_finite() || red_bound.abs() >= 1e19 {
continue;
}
let VarRecovery::Affine { rep, coeff, offset } = p.recovery[src] else {
panic!(
"provenance {src} is not an affine image: {:?}",
p.recovery[src]
);
};
assert_eq!(rep, kept, "provenance {src} names a different survivor");
let origin = if upper != (coeff < 0.0) {
f.x_u[src]
} else {
f.x_l[src]
};
let lifted = coeff * red_bound + offset;
assert!(
(lifted - origin).abs() < 1e-12,
"reduced bound {red_bound} lifts to {lifted}, not {src}'s {origin}"
);
}
}
}
#[test]
fn absent_bounds_keep_the_callers_sentinel() {
let f = Fixture::new(2).eq(&[(0, 1.0), (1, -1.0)], 0.0);
let p = f.plan();
assert_eq!(p.x_l_red[0], -1e19);
assert_eq!(p.x_u_red[0], 1e19);
}
#[test]
fn redundant_row_after_substitution_is_dropped() {
let f = Fixture::new(3)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.eq(&[(1, 1.0), (2, -1.0)], 0.0)
.eq(&[(0, 1.0), (2, -1.0)], 0.0);
let p = f.plan();
assert_eq!(p.n_reduced_vars(), 1);
assert_eq!(p.n_reduced_rows(), 0);
assert_eq!(p.report.n_redundant_rows, 1);
}
#[test]
fn contradiction_abandons_the_whole_plan() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.eq(&[(0, 1.0), (1, -1.0)], 1.0);
let p = f.plan();
assert!(p.report.infeasible);
assert!(
p.is_identity(),
"a contradictory model must be handed back whole"
);
assert_eq!(p.n_reduced_vars(), 2);
assert_eq!(p.n_reduced_rows(), 2);
}
#[test]
fn a_singleton_outside_its_box_abandons_the_plan() {
let f = Fixture::new(2).eq(&[(0, 1.0)], 5.0).bounds(0, 0.0, 1.0);
let p = f.plan();
assert!(p.report.infeasible);
assert!(p.is_identity());
}
#[test]
fn a_float_noise_excursion_clamps_instead_of_abandoning() {
let f = Fixture::new(2)
.eq(&[(0, 1.0)], 0.1 + 0.2)
.bounds(0, 0.0, 0.3);
let p = f.plan();
assert!(!p.report.infeasible);
assert_eq!(p.recovery[0], VarRecovery::Constant(0.3));
}
#[test]
fn an_emptied_survivor_box_abandons_the_plan() {
let f = Fixture::new(2)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.bounds(0, 5.0, 6.0)
.bounds(1, 1.0, 2.0);
let p = f.plan();
assert!(p.report.infeasible);
assert!(p.is_identity());
}
#[test]
fn ineligible_rows_are_never_consumed() {
let f = Fixture::new(2).opaque(&[(0, 1.0), (1, -1.0)], 0.0, 0.0);
let p = f.plan();
assert!(p.is_identity());
assert_eq!(p.n_reduced_vars(), 2);
}
#[test]
fn an_inequality_row_is_never_consumed() {
let mut f = Fixture::new(2);
f.rows.push(vec![(0, 1.0), (1, -1.0)]);
f.row_const.push(0.0);
f.g_l.push(0.0);
f.g_u.push(1.0);
f.eligible.push(true);
let p = f.plan();
assert!(p.is_identity());
}
#[test]
fn a_one_sided_row_at_the_sentinel_is_not_an_equality() {
let mut f = Fixture::new(2);
f.rows.push(vec![(0, 1.0), (1, -1.0)]);
f.row_const.push(0.0);
f.g_l.push(-1e19);
f.g_u.push(-1e19);
f.eligible.push(true);
let p = f.plan();
assert!(p.is_identity());
}
#[test]
fn the_row_constant_is_honoured() {
let mut f = Fixture::new(2);
f.rows.push(vec![(0, 1.0), (1, -1.0)]);
f.row_const.push(3.0);
f.g_l.push(0.0);
f.g_u.push(0.0);
f.eligible.push(true);
let p = f.plan();
let mut x = vec![0.0; 2];
p.lift_x(&[2.0], &mut x);
assert!((x[0] - x[1] + 3.0).abs() < 1e-12, "{x:?}");
}
#[test]
fn three_term_rows_are_left_alone() {
let f = Fixture::new(3).eq(&[(0, 1.0), (1, 1.0), (2, 1.0)], 1.0);
let p = f.plan();
assert!(p.is_identity());
}
#[test]
fn steps_are_recorded_in_application_order_with_live_pivots() {
let f = Fixture::new(3)
.eq(&[(0, 2.0), (1, -1.0)], 0.0)
.eq(&[(1, 3.0), (2, -1.0)], 0.0);
let p = f.plan();
assert_eq!(p.steps.len(), 2);
assert_eq!(p.steps[0].row, 0);
assert!(p.steps[0].pivot != 0.0);
assert_eq!(p.steps[1].row, 1);
for s in &p.steps {
assert!(!matches!(p.recovery[s.var], VarRecovery::Kept(_)));
}
}
#[test]
fn parent_edges_point_at_later_or_never_eliminated_columns() {
let f = Fixture::new(4)
.eq(&[(0, 1.0), (1, -1.0)], 0.0)
.eq(&[(1, 1.0), (2, -1.0)], 0.0)
.eq(&[(2, 1.0), (3, -1.0)], 0.0);
let p = f.plan();
let mut step_of = [usize::MAX; 4];
for (t, s) in p.steps.iter().enumerate() {
step_of[s.var] = t;
}
for (i, edge) in p.parent.iter().enumerate() {
if let Some((parent, _)) = *edge {
let ti = step_of[i];
let tp = step_of[parent];
assert!(ti != usize::MAX);
assert!(tp == usize::MAX || tp > ti, "{i} -> {parent}");
}
}
}
#[test]
fn identity_plan_round_trips() {
let p = EliminationPlan::identity(3, 2, &[-1.0, -1.0, -1.0], &[1.0, 1.0, 1.0]);
assert!(p.is_identity());
let mut x = vec![0.0; 3];
p.lift_x(&[1.0, 2.0, 3.0], &mut x);
assert_eq!(x, vec![1.0, 2.0, 3.0]);
let mut y = vec![0.0; 3];
p.project_x(&x, &mut y);
assert_eq!(y, vec![1.0, 2.0, 3.0]);
}
#[test]
fn a_long_alias_chain_stays_shallow() {
let mut f = Fixture::new(400);
for j in 0..399 {
f = f.eq(&[(j, 1.0), (j + 1, -1.0)], 0.0);
}
let p = f.plan();
assert_eq!(p.n_reduced_vars(), 1);
let mut depth = 0usize;
for i in 0..400 {
let mut d = 0usize;
let mut cur = i;
while let Some((parent, _)) = p.parent[cur] {
cur = parent;
d += 1;
}
depth = depth.max(d);
}
assert!(
depth <= 32,
"elimination forest depth {depth} is not shallow"
);
let mut x = vec![0.0; 400];
p.lift_x(&[7.0], &mut x);
for v in &x {
assert!((v - 7.0).abs() < 1e-12);
}
}
}