use pounce_common::types::{Index, Number};
use pounce_nlp::tnlp::Linearity;
use crate::block_solve::{BlockEquations, BlockSolveOptions, BlockSolver, DampedNewtonSolver};
use crate::btf::BlockTriangularForm;
use crate::components::SquareComponents;
use crate::coupling::{classify_block, objective_gradient_support, AuxiliaryCouplingClass};
use crate::diagnostics::{AuxiliaryPreprocessingDiagnostics, AuxiliaryRejectionReason};
use crate::dulmage_mendelsohn::DulmageMendelsohnPartition;
use crate::incidence::{EqualityIncidence, InequalityIncidence, ProbeView};
use crate::matching::hopcroft_karp;
use crate::options::{AuxiliaryCouplingPolicy, PresolveOptions};
use crate::reduction_frame::ReductionFrame;
pub struct Phase0Probe<'a> {
pub n_vars: usize,
pub n_rows: usize,
pub jac_irow: &'a [Index],
pub jac_jcol: &'a [Index],
pub jac_values: &'a [Number],
pub g_l: &'a [Number],
pub g_u: &'a [Number],
pub g_at_probe: &'a [Number],
pub linearity: &'a [Linearity],
pub one_based: bool,
pub eq_tol: Number,
pub x_probe: &'a [Number],
pub grad_f: &'a [Number],
pub x_l: &'a [Number],
pub x_u: &'a [Number],
}
pub trait Phase0TnlpCallback {
fn eval_g_full(&mut self, x: &[Number], g: &mut [Number]) -> bool;
fn eval_jac_g_values(&mut self, x: &[Number], values: &mut [Number]) -> bool;
}
#[derive(Debug, Clone)]
pub struct Phase0Plan {
pub diagnostics: AuxiliaryPreprocessingDiagnostics,
pub frame: Option<ReductionFrame>,
}
impl Default for Phase0Plan {
fn default() -> Self {
Self {
diagnostics: AuxiliaryPreprocessingDiagnostics::default(),
frame: None,
}
}
}
pub fn run_auxiliary_phase0(
opts: &PresolveOptions,
probe: &Phase0Probe<'_>,
mut tnlp: Option<&mut dyn Phase0TnlpCallback>,
mut large_solver: Option<&mut dyn crate::block_solve::LargeBlockSolver>,
) -> Phase0Plan {
let mut diag = AuxiliaryPreprocessingDiagnostics::default();
if !opts.auxiliary {
return Phase0Plan {
diagnostics: diag,
frame: None,
};
}
if matches!(opts.auxiliary_coupling, AuxiliaryCouplingPolicy::None) {
return Phase0Plan {
diagnostics: diag,
frame: None,
};
}
let start = std::time::Instant::now();
let trivial = crate::trivial_elim::find_trivial_eliminations(
probe.n_vars,
probe.n_rows,
probe.x_l,
probe.x_u,
probe.g_l,
probe.g_u,
probe.jac_irow,
probe.jac_jcol,
probe.jac_values,
probe.linearity,
probe.one_based,
probe.eq_tol,
1e19,
);
diag.trivially_fixed_vars = trivial.fixed_vars.len() as Index;
diag.trivially_free_rows = trivial.free_rows.len() as Index;
diag.trivially_slack_rows = trivial.trivially_slack_rows.len() as Index;
let excluded_vars_buf: Option<Vec<bool>> = if trivial.fixed_vars.is_empty() {
None
} else {
let mut v = vec![false; probe.n_vars];
for &i in &trivial.fixed_vars {
v[i] = true;
}
Some(v)
};
let excluded_rows_buf: Option<Vec<bool>> =
if trivial.free_rows.is_empty() && trivial.trivially_slack_rows.is_empty() {
None
} else {
let mut v = vec![false; probe.n_rows];
for &r in &trivial.free_rows {
v[r] = true;
}
for &r in &trivial.trivially_slack_rows {
v[r] = true;
}
Some(v)
};
let pv = ProbeView {
n_vars: probe.n_vars,
m_rows: probe.n_rows,
jac_irow: probe.jac_irow,
jac_jcol: probe.jac_jcol,
jac_values: Some(probe.jac_values),
g_l: probe.g_l,
g_u: probe.g_u,
linearity: Some(probe.linearity),
one_based: probe.one_based,
eq_tol: probe.eq_tol,
excluded_vars: excluded_vars_buf.as_deref(),
excluded_rows: excluded_rows_buf.as_deref(),
};
let t_inc = std::time::Instant::now();
let eq_inc = EqualityIncidence::from_probe(&pv);
let ineq_inc = InequalityIncidence::from_probe(&pv);
diag.stage_time_ms.incidence_ms = t_inc.elapsed().as_millis();
let t_match = std::time::Instant::now();
let matching = hopcroft_karp(&eq_inc);
diag.stage_time_ms.matching_ms = t_match.elapsed().as_millis();
let t_dm = std::time::Instant::now();
let dm = DulmageMendelsohnPartition::from_matching(&eq_inc, &matching);
diag.stage_time_ms.dm_ms = t_dm.elapsed().as_millis();
let t_comp = std::time::Instant::now();
let comps = SquareComponents::of_square_part(&eq_inc, &matching, &dm);
diag.stage_time_ms.components_ms = t_comp.elapsed().as_millis();
let obj_support = objective_gradient_support(probe.grad_f, 1e-12);
let is_linear_inner: Vec<bool> = probe
.linearity
.iter()
.map(|l| matches!(l, Linearity::Linear))
.collect();
let aggressive = matches!(opts.auxiliary_coupling, AuxiliaryCouplingPolicy::Aggressive);
let has_large_solver = large_solver.is_some();
let mut accepted_fixed_vars: Vec<usize> = Vec::new();
let mut accepted_fixed_values: Vec<Number> = Vec::new();
let mut accepted_dropped_rows: Vec<usize> = Vec::new();
let mut max_residual: Number = 0.0;
if !probe.x_probe.iter().all(|v| v.is_finite()) {
return Phase0Plan {
diagnostics: diag,
frame: None,
};
}
let mut x_running: Vec<Number> = probe.x_probe.to_vec();
for comp in &comps.components {
let t_btf = std::time::Instant::now();
let btf = BlockTriangularForm::of_component(&eq_inc, &matching, comp);
diag.stage_time_ms.btf_ms += t_btf.elapsed().as_millis();
for block in &btf.blocks {
diag.candidate_blocks += 1;
let all_linear = block
.eq_rows
.iter()
.map(|&k| eq_inc.eq_row_inner_idx[k])
.all(|r| is_linear_inner[r]);
if !all_linear && tnlp.is_none() {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::BlockSolveDiverged);
continue;
}
let is_large = block.eq_rows.len() > opts.auxiliary_max_block_dim as usize;
if is_large && !has_large_solver {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::BlockTooLarge);
continue;
}
let class = classify_block(block, &ineq_inc, &obj_support);
match class {
AuxiliaryCouplingClass::PureEquality => diag.class_counts.pure_equality += 1,
AuxiliaryCouplingClass::ObjectiveCoupled => {
diag.class_counts.objective_coupled += 1
}
AuxiliaryCouplingClass::InequalityCoupled => {
diag.class_counts.inequality_coupled += 1
}
AuxiliaryCouplingClass::ObjectiveAndInequalityCoupled => {
diag.class_counts.objective_and_inequality_coupled += 1
}
}
let allowed = match class {
AuxiliaryCouplingClass::PureEquality => true,
AuxiliaryCouplingClass::ObjectiveCoupled => aggressive,
AuxiliaryCouplingClass::InequalityCoupled => {
let inner_rows_for_check: Vec<usize> = block
.eq_rows
.iter()
.map(|&kk| eq_inc.eq_row_inner_idx[kk])
.collect();
let block_eqs_linear = inner_rows_for_check.iter().all(|&r| is_linear_inner[r]);
let coupled_ineq_rows: Vec<usize> = block
.cols
.iter()
.flat_map(|&c| ineq_inc.rows_for_var(c).iter().copied())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.map(|k| ineq_inc.ineq_row_inner_idx[k])
.collect();
let ineq_within_cap =
coupled_ineq_rows.len() <= opts.auxiliary_max_block_dim as usize;
let ineqs_linear = coupled_ineq_rows.iter().all(|&r| is_linear_inner[r]);
if block_eqs_linear && ineqs_linear && ineq_within_cap {
if let Some(res) = crate::inequality_projection::project_inequalities(
&inner_rows_for_check,
&block.cols,
&coupled_ineq_rows,
probe.n_vars,
probe.x_l,
probe.x_u,
probe.g_l,
probe.g_u,
probe.jac_irow,
probe.jac_jcol,
probe.jac_values,
probe.g_at_probe,
probe.x_probe,
probe.one_based,
) {
if res.all_implied {
diag.inequality_coupled_accepted_via_projection += 1;
true
} else {
false
}
} else {
false
}
} else {
false
}
}
_ => false,
};
if !allowed {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::CouplingDisallowed);
continue;
}
let k = block.eq_rows.len();
let inner_rows: Vec<usize> = block
.eq_rows
.iter()
.map(|&kk| eq_inc.eq_row_inner_idx[kk])
.collect();
let block_cols = &block.cols;
let bounds_lo: Vec<Number> = block_cols.iter().map(|&c| probe.x_l[c]).collect();
let bounds_hi: Vec<Number> = block_cols.iter().map(|&c| probe.x_u[c]).collect();
let bs_opts = BlockSolveOptions {
tol: opts.auxiliary_tol,
max_dim: opts.auxiliary_max_block_dim as usize,
bounds: Some((bounds_lo, bounds_hi)),
..Default::default()
};
let t_solve = std::time::Instant::now();
let solver_call = |x0: &[Number],
eqs: &mut dyn BlockEquations,
opts: &BlockSolveOptions|
-> Result<
crate::block_solve::BlockSolveOutcome,
crate::block_solve::BlockSolveError,
> {
if is_large {
large_solver
.as_deref_mut()
.expect("checked above")
.solve_large(x0, eqs, opts)
} else {
DampedNewtonSolver.solve(x0, eqs, opts)
}
};
let solve_result = if all_linear {
solve_linear_block(
probe,
&inner_rows,
block_cols,
&x_running,
&bs_opts,
solver_call,
)
} else {
let cb: &mut dyn Phase0TnlpCallback = *tnlp.as_mut().expect("checked above");
solve_nonlinear_block(
probe,
&inner_rows,
block_cols,
&x_running,
&bs_opts,
cb,
solver_call,
)
};
diag.stage_time_ms.block_solve_ms += t_solve.elapsed().as_millis();
let out = match solve_result {
Ok(o) => o,
Err(crate::block_solve::BlockSolveError::OutOfBounds) => {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::OutOfBounds);
continue;
}
Err(_) => {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::BlockSolveDiverged);
continue;
}
};
let t_resid = std::time::Instant::now();
let mut candidate_x = x_running.clone();
for (ii, &c) in block_cols.iter().enumerate() {
candidate_x[c] = out.x[ii];
}
let row_resid = if all_linear {
residual_norm_linear(probe, &inner_rows, &candidate_x)
} else {
let cb: &mut dyn Phase0TnlpCallback = *tnlp.as_mut().expect("checked above");
match residual_norm_nonlinear(probe, &inner_rows, &candidate_x, cb) {
Some(r) => r,
None => {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::BlockSolveDiverged);
continue;
}
}
};
diag.stage_time_ms.residual_check_ms += t_resid.elapsed().as_millis();
if row_resid > opts.auxiliary_tol {
diag.rejection_reasons
.push(AuxiliaryRejectionReason::ResidualCheckFailed);
continue;
}
max_residual = max_residual.max(row_resid);
for (ii, &c) in block_cols.iter().enumerate() {
accepted_fixed_vars.push(c);
accepted_fixed_values.push(out.x[ii]);
x_running[c] = out.x[ii];
}
for &r_inner in &inner_rows {
accepted_dropped_rows.push(r_inner);
}
diag.blocks_eliminated += 1;
if (k as Index) > diag.max_accepted_block_dim {
diag.max_accepted_block_dim = k as Index;
}
}
}
diag.vars_eliminated = accepted_fixed_vars.len() as Index;
diag.rows_eliminated = accepted_dropped_rows.len() as Index;
diag.max_block_residual = max_residual;
diag.total_time_ms = start.elapsed().as_millis();
let frame = if accepted_fixed_vars.is_empty() {
None
} else {
let mut order: Vec<usize> = (0..accepted_fixed_vars.len()).collect();
order.sort_by_key(|&i| accepted_fixed_vars[i]);
let fixed_vars_sorted: Vec<usize> = order.iter().map(|&i| accepted_fixed_vars[i]).collect();
let fixed_values_sorted: Vec<Number> =
order.iter().map(|&i| accepted_fixed_values[i]).collect();
let mut dropped_rows_sorted = accepted_dropped_rows.clone();
dropped_rows_sorted.sort_unstable();
Some(ReductionFrame::new(
probe.n_vars,
probe.n_rows,
fixed_vars_sorted,
fixed_values_sorted,
dropped_rows_sorted,
))
};
Phase0Plan {
diagnostics: diag,
frame,
}
}
fn solve_linear_block<F>(
probe: &Phase0Probe<'_>,
inner_rows: &[usize],
block_cols: &[usize],
x_running: &[Number],
bs_opts: &BlockSolveOptions,
solver_call: F,
) -> Result<crate::block_solve::BlockSolveOutcome, crate::block_solve::BlockSolveError>
where
F: FnOnce(
&[Number],
&mut dyn BlockEquations,
&BlockSolveOptions,
)
-> Result<crate::block_solve::BlockSolveOutcome, crate::block_solve::BlockSolveError>,
{
let k = inner_rows.len();
let mut a_block = vec![0.0; k * k];
let mut b_block = vec![0.0; k];
for (ii, &r_inner) in inner_rows.iter().enumerate() {
let mut sum_jx = 0.0;
let nnz = probe.jac_irow.len();
for kk in 0..nnz {
let i = if probe.one_based {
(probe.jac_irow[kk] as isize - 1) as usize
} else {
probe.jac_irow[kk] as usize
};
if i != r_inner {
continue;
}
let j = if probe.one_based {
(probe.jac_jcol[kk] as isize - 1) as usize
} else {
probe.jac_jcol[kk] as usize
};
let v = probe.jac_values[kk];
sum_jx += v * probe.x_probe[j];
if let Some(jj) = block_cols.iter().position(|&c| c == j) {
a_block[ii * k + jj] = v;
} else {
b_block[ii] -= v * x_running[j];
}
}
let const_r = probe.g_at_probe[r_inner] - sum_jx;
b_block[ii] += probe.g_l[r_inner] - const_r;
}
struct LinearBlock {
a: Vec<Number>,
b: Vec<Number>,
n: usize,
}
impl BlockEquations for LinearBlock {
fn dim(&self) -> usize {
self.n
}
fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
for i in 0..self.n {
let mut s = -self.b[i];
for j in 0..self.n {
s += self.a[i * self.n + j] * x[j];
}
f[i] = s;
}
true
}
fn jacobian(&mut self, _x: &[Number], j: &mut [Number]) -> bool {
j.copy_from_slice(&self.a);
true
}
}
let mut eqs = LinearBlock {
a: a_block,
b: b_block,
n: k,
};
let x0 = vec![0.0; k];
solver_call(&x0, &mut eqs, bs_opts)
}
fn solve_nonlinear_block<F>(
probe: &Phase0Probe<'_>,
inner_rows: &[usize],
block_cols: &[usize],
x_running: &[Number],
bs_opts: &BlockSolveOptions,
tnlp: &mut dyn Phase0TnlpCallback,
solver_call: F,
) -> Result<crate::block_solve::BlockSolveOutcome, crate::block_solve::BlockSolveError>
where
F: FnOnce(
&[Number],
&mut dyn BlockEquations,
&BlockSolveOptions,
)
-> Result<crate::block_solve::BlockSolveOutcome, crate::block_solve::BlockSolveError>,
{
let k = inner_rows.len();
struct NonlinearBlock<'a> {
n: usize,
nnz: usize,
inner_rows: &'a [usize],
block_cols: &'a [usize],
x_running: &'a [Number],
jac_irow: &'a [Index],
jac_jcol: &'a [Index],
g_l: &'a [Number],
one_based: bool,
tnlp: &'a mut dyn Phase0TnlpCallback,
full_x: Vec<Number>,
full_g: Vec<Number>,
full_jac_vals: Vec<Number>,
}
impl<'a> NonlinearBlock<'a> {
fn splice_full_x(&mut self, x_block: &[Number]) {
self.full_x.copy_from_slice(self.x_running);
for (ii, &c) in self.block_cols.iter().enumerate() {
self.full_x[c] = x_block[ii];
}
}
}
impl<'a> BlockEquations for NonlinearBlock<'a> {
fn dim(&self) -> usize {
self.n
}
fn eval(&mut self, x: &[Number], f: &mut [Number]) -> bool {
self.splice_full_x(x);
if !self.tnlp.eval_g_full(&self.full_x, &mut self.full_g) {
return false;
}
for (ii, &r) in self.inner_rows.iter().enumerate() {
f[ii] = self.full_g[r] - self.g_l[r];
}
true
}
fn jacobian(&mut self, x: &[Number], j: &mut [Number]) -> bool {
self.splice_full_x(x);
if !self
.tnlp
.eval_jac_g_values(&self.full_x, &mut self.full_jac_vals)
{
return false;
}
for v in j.iter_mut() {
*v = 0.0;
}
for kk in 0..self.nnz {
let i = if self.one_based {
(self.jac_irow[kk] as isize - 1) as usize
} else {
self.jac_irow[kk] as usize
};
let Some(ii) = self.inner_rows.iter().position(|&r| r == i) else {
continue;
};
let col = if self.one_based {
(self.jac_jcol[kk] as isize - 1) as usize
} else {
self.jac_jcol[kk] as usize
};
let Some(jj) = self.block_cols.iter().position(|&c| c == col) else {
continue;
};
j[ii * self.n + jj] = self.full_jac_vals[kk];
}
true
}
}
let nnz = probe.jac_irow.len();
let mut eqs = NonlinearBlock {
n: k,
nnz,
inner_rows,
block_cols,
x_running,
jac_irow: probe.jac_irow,
jac_jcol: probe.jac_jcol,
g_l: probe.g_l,
one_based: probe.one_based,
tnlp,
full_x: vec![0.0; probe.n_vars],
full_g: vec![0.0; probe.n_rows],
full_jac_vals: vec![0.0; nnz],
};
let x0: Vec<Number> = block_cols.iter().map(|&c| x_running[c]).collect();
solver_call(&x0, &mut eqs, bs_opts)
}
fn residual_norm_linear(
probe: &Phase0Probe<'_>,
inner_rows: &[usize],
candidate_x: &[Number],
) -> Number {
let mut row_resid: Number = 0.0;
let nnz = probe.jac_irow.len();
for &r_inner in inner_rows {
let mut s = 0.0;
let mut sum_jx = 0.0;
for kk in 0..nnz {
let i = if probe.one_based {
(probe.jac_irow[kk] as isize - 1) as usize
} else {
probe.jac_irow[kk] as usize
};
if i != r_inner {
continue;
}
let j = if probe.one_based {
(probe.jac_jcol[kk] as isize - 1) as usize
} else {
probe.jac_jcol[kk] as usize
};
let v = probe.jac_values[kk];
s += v * candidate_x[j];
sum_jx += v * probe.x_probe[j];
}
let const_r = probe.g_at_probe[r_inner] - sum_jx;
row_resid = row_resid.max((s + const_r - probe.g_l[r_inner]).abs());
}
row_resid
}
fn residual_norm_nonlinear(
probe: &Phase0Probe<'_>,
inner_rows: &[usize],
candidate_x: &[Number],
tnlp: &mut dyn Phase0TnlpCallback,
) -> Option<Number> {
let mut g_full = vec![0.0; probe.n_rows];
if !tnlp.eval_g_full(candidate_x, &mut g_full) {
return None;
}
let mut row_resid: Number = 0.0;
for &r in inner_rows {
row_resid = row_resid.max((g_full[r] - probe.g_l[r]).abs());
}
Some(row_resid)
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_probe<'a>(
n_vars: usize,
n_rows: usize,
jac_irow: &'a [Index],
jac_jcol: &'a [Index],
jac_values: &'a [Number],
g_l: &'a [Number],
g_u: &'a [Number],
g_at_probe: &'a [Number],
linearity: &'a [Linearity],
x_probe: &'a [Number],
grad_f: &'a [Number],
) -> Phase0Probe<'a> {
let x_l: Vec<Number> = vec![-1e19; n_vars];
let x_u: Vec<Number> = vec![1e19; n_vars];
Phase0Probe {
n_vars,
n_rows,
jac_irow,
jac_jcol,
jac_values,
g_l,
g_u,
g_at_probe,
linearity,
one_based: false,
eq_tol: 1e-12,
x_probe,
grad_f,
x_l: Box::leak(x_l.into_boxed_slice()),
x_u: Box::leak(x_u.into_boxed_slice()),
}
}
#[test]
fn noop_when_disabled() {
let probe = linear_probe(
2,
1,
&[0, 0],
&[0, 1],
&[1.0, 1.0],
&[1.0],
&[1.0],
&[0.0],
&[Linearity::Linear],
&[0.0, 0.0],
&[0.0, 0.0],
);
let opts = PresolveOptions::defaults();
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 0);
assert!(plan.frame.is_none());
}
#[test]
fn phase0_eliminates_linear_singleton() {
let probe = linear_probe(
1,
1,
&[0],
&[0],
&[1.0],
&[3.0],
&[3.0],
&[0.0],
&[Linearity::Linear],
&[0.0],
&[0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 1);
assert_eq!(plan.diagnostics.vars_eliminated, 1);
assert_eq!(plan.diagnostics.rows_eliminated, 1);
let frame = plan.frame.expect("accepted frame");
assert_eq!(frame.fixed_vars, vec![0]);
assert!((frame.fixed_values[0] - 3.0).abs() < 1e-12);
assert_eq!(frame.dropped_rows, vec![0]);
}
#[test]
fn phase0_eliminates_linear_2x2_block() {
let probe = linear_probe(
2,
2,
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[1.0, 1.0, 1.0, -1.0],
&[3.0, 1.0],
&[3.0, 1.0],
&[0.0, 0.0],
&[Linearity::Linear, Linearity::Linear],
&[0.0, 0.0],
&[0.0, 0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 1);
let frame = plan.frame.expect("accepted");
assert_eq!(frame.fixed_vars, vec![0, 1]);
assert!((frame.fixed_values[0] - 2.0).abs() < 1e-12);
assert!((frame.fixed_values[1] - 1.0).abs() < 1e-12);
}
#[test]
fn phase0_inequality_coupled_admitted_via_projection() {
let probe = linear_probe(
2,
2,
&[0, 1],
&[1, 1],
&[1.0, 1.0],
&[5.0, -1e19],
&[5.0, 10.0], &[0.0, 0.0],
&[Linearity::Linear, Linearity::Linear],
&[0.0, 0.0],
&[0.0, 0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
opts.auxiliary_coupling = AuxiliaryCouplingPolicy::Safe;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 1);
assert_eq!(
plan.diagnostics.inequality_coupled_accepted_via_projection,
1
);
assert!(plan.frame.is_some());
}
#[test]
fn phase0_inequality_coupled_rejected_when_not_implied() {
let probe = linear_probe(
2,
2,
&[0, 1],
&[1, 1],
&[1.0, 1.0],
&[5.0, -1e19],
&[5.0, 4.0],
&[0.0, 0.0],
&[Linearity::Linear, Linearity::Linear],
&[0.0, 0.0],
&[0.0, 0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
opts.auxiliary_coupling = AuxiliaryCouplingPolicy::Safe;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 0);
assert_eq!(
plan.diagnostics.inequality_coupled_accepted_via_projection,
0
);
assert!(plan.frame.is_none());
assert!(plan
.diagnostics
.rejection_reasons
.iter()
.any(|r| matches!(r, AuxiliaryRejectionReason::CouplingDisallowed)));
}
#[test]
fn phase0_rejects_nonlinear_row() {
let probe = linear_probe(
1,
1,
&[0],
&[0],
&[1.0],
&[3.0],
&[3.0],
&[0.0],
&[Linearity::NonLinear],
&[0.0],
&[0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 0);
assert!(plan.frame.is_none());
}
#[test]
fn phase0_none_policy_is_diagnostics_only() {
let probe = linear_probe(
1,
1,
&[0],
&[0],
&[1.0],
&[3.0],
&[3.0],
&[0.0],
&[Linearity::Linear],
&[0.0],
&[0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
opts.auxiliary_coupling = AuxiliaryCouplingPolicy::None;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert!(plan.frame.is_none());
assert_eq!(plan.diagnostics.blocks_eliminated, 0);
}
struct XyCallback;
impl Phase0TnlpCallback for XyCallback {
fn eval_g_full(&mut self, x: &[Number], g: &mut [Number]) -> bool {
g[0] = x[0] * x[1];
g[1] = x[0] + x[1];
true
}
fn eval_jac_g_values(&mut self, x: &[Number], values: &mut [Number]) -> bool {
values[0] = x[1];
values[1] = x[0];
values[2] = 1.0;
values[3] = 1.0;
true
}
}
#[test]
fn phase0_eliminates_nonlinear_block() {
let probe = linear_probe(
2,
2,
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[0.5, 3.0, 1.0, 1.0], &[1.0, 3.0],
&[1.0, 3.0],
&[1.5, 3.5], &[Linearity::NonLinear, Linearity::Linear],
&[3.0, 0.5],
&[0.0, 0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let mut tnlp = XyCallback;
let plan = run_auxiliary_phase0(&opts, &probe, Some(&mut tnlp), None);
assert_eq!(plan.diagnostics.blocks_eliminated, 1);
let frame = plan.frame.expect("accepted");
let v0 = frame.fixed_values[0];
let v1 = frame.fixed_values[1];
assert!(
(v0 * v1 - 1.0).abs() < 1e-8,
"x*y should be 1, got {v0}*{v1}={}",
v0 * v1
);
assert!(
(v0 + v1 - 3.0).abs() < 1e-8,
"x+y should be 3, got {}",
v0 + v1
);
}
#[test]
fn phase0_nonlinear_rejected_without_callback() {
let probe = linear_probe(
2,
2,
&[0, 0, 1, 1],
&[0, 1, 0, 1],
&[0.5, 3.0, 1.0, 1.0],
&[1.0, 3.0],
&[1.0, 3.0],
&[1.5, 3.5],
&[Linearity::NonLinear, Linearity::Linear],
&[3.0, 0.5],
&[0.0, 0.0],
);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 0);
assert!(plan.frame.is_none());
assert!(plan
.diagnostics
.rejection_reasons
.iter()
.any(|r| matches!(r, AuxiliaryRejectionReason::BlockSolveDiverged)));
}
fn diagonal_linear_probe(n: usize) -> Phase0Probe<'static> {
let irow: Vec<Index> = (0..n).map(|i| i as Index).collect();
let jcol: Vec<Index> = (0..n).map(|i| i as Index).collect();
let vals = vec![2.0; n];
let g_l: Vec<Number> = (1..=n).map(|i| i as Number).collect();
let g_u = g_l.clone();
let g_probe: Vec<Number> = vec![0.0; n]; let linearity = vec![Linearity::Linear; n];
let x_probe = vec![0.0; n];
let grad_f = vec![0.0; n];
let x_l_def = vec![-1e19; n];
let x_u_def = vec![1e19; n];
Phase0Probe {
n_vars: n,
n_rows: n,
jac_irow: Box::leak(irow.into_boxed_slice()),
jac_jcol: Box::leak(jcol.into_boxed_slice()),
jac_values: Box::leak(vals.into_boxed_slice()),
g_l: Box::leak(g_l.into_boxed_slice()),
g_u: Box::leak(g_u.into_boxed_slice()),
g_at_probe: Box::leak(g_probe.into_boxed_slice()),
linearity: Box::leak(linearity.into_boxed_slice()),
one_based: false,
eq_tol: 1e-12,
x_probe: Box::leak(x_probe.into_boxed_slice()),
grad_f: Box::leak(grad_f.into_boxed_slice()),
x_l: Box::leak(x_l_def.into_boxed_slice()),
x_u: Box::leak(x_u_def.into_boxed_slice()),
}
}
#[test]
fn phase0_large_block_rejected_without_solver() {
let probe = diagonal_linear_probe(10);
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.blocks_eliminated, 10);
}
#[test]
fn phase0_large_block_uses_fallback() {
let n = 10usize;
let mut irow = Vec::new();
let mut jcol = Vec::new();
let mut vals = Vec::new();
for i in 0..n {
for j in 0..n {
irow.push(i as Index);
jcol.push(j as Index);
vals.push(if i == j { 5.0 } else { 0.1 });
}
}
let g_l: Vec<Number> = (1..=n).map(|i| i as Number).collect();
let g_u = g_l.clone();
let g_probe = vec![0.0; n];
let linearity = vec![Linearity::Linear; n];
let x_probe = vec![0.0; n];
let grad_f = vec![0.0; n];
let x_l_def = vec![-1e19; n];
let x_u_def = vec![1e19; n];
let probe = Phase0Probe {
n_vars: n,
n_rows: n,
jac_irow: &irow,
jac_jcol: &jcol,
jac_values: &vals,
g_l: &g_l,
g_u: &g_u,
g_at_probe: &g_probe,
linearity: &linearity,
one_based: false,
eq_tol: 1e-12,
x_probe: &x_probe,
grad_f: &grad_f,
x_l: &x_l_def,
x_u: &x_u_def,
};
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan_no_fb = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan_no_fb.diagnostics.blocks_eliminated, 0);
assert!(plan_no_fb
.diagnostics
.rejection_reasons
.iter()
.any(|r| matches!(r, AuxiliaryRejectionReason::BlockTooLarge)));
let mut fb = crate::block_solve::RelaxedNewtonSolver;
let plan_with_fb = run_auxiliary_phase0(&opts, &probe, None, Some(&mut fb));
assert_eq!(plan_with_fb.diagnostics.blocks_eliminated, 1);
assert_eq!(plan_with_fb.diagnostics.vars_eliminated, n as Index);
let frame = plan_with_fb.frame.expect("accepted");
for (k, &i) in frame.fixed_vars.iter().enumerate() {
let expected_approx = (i + 1) as Number / 5.0;
assert!((frame.fixed_values[k] - expected_approx).abs() < 0.2);
}
}
#[test]
fn phase0_trivial_pre_pass_populates_diagnostics() {
let x_l = [1.0, 0.0];
let x_u = [1.0, 1.0];
let g_l = [0.0, -100.0];
let g_u = [0.0, 100.0];
let jac_irow: [Index; 3] = [0, 0, 1];
let jac_jcol: [Index; 3] = [0, 1, 1];
let jac_vals = [1.0, 1.0, 1.0];
let g_probe = [1.0, 0.5];
let linearity = [Linearity::Linear, Linearity::Linear];
let x_probe = [1.0, 0.5];
let grad_f = [0.0, 0.0];
let probe = Phase0Probe {
n_vars: 2,
n_rows: 2,
jac_irow: &jac_irow,
jac_jcol: &jac_jcol,
jac_values: &jac_vals,
g_l: &g_l,
g_u: &g_u,
g_at_probe: &g_probe,
linearity: &linearity,
one_based: false,
eq_tol: 1e-12,
x_probe: &x_probe,
grad_f: &grad_f,
x_l: &x_l,
x_u: &x_u,
};
let mut opts = PresolveOptions::defaults();
opts.auxiliary = true;
let plan = run_auxiliary_phase0(&opts, &probe, None, None);
assert_eq!(plan.diagnostics.trivially_fixed_vars, 1);
assert_eq!(plan.diagnostics.trivially_free_rows, 0);
assert_eq!(plan.diagnostics.trivially_slack_rows, 1);
}
}