#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::cell::RefCell;
use std::rc::Rc;
use pounce_common::exception::SolverException;
use pounce_common::options_list::OptionsList;
use pounce_common::reg_options::RegisteredOptions;
use pounce_common::types::{Index, Number};
use pounce_nlp::expression_provider::ExpressionProvider;
use pounce_nlp::tnlp::{
BoundsInfo, IndexStyle, IpoptCq, IpoptData, IterStats, Linearity, MetaData, NlpInfo,
ScalingRequest, Solution, SparsityRequest, StartingPoint, TNLP,
};
pub mod auxiliary;
pub mod block_solve;
pub mod bound_tighten;
pub mod btf;
pub mod components;
pub mod coupling;
pub mod diagnostics;
pub mod dulmage_mendelsohn;
pub mod fbbt;
pub mod incidence;
pub mod inequality_projection;
pub mod licq;
pub mod matching;
pub mod options;
pub mod reduction_frame;
pub mod redundant;
pub mod trivial_elim;
pub use block_solve::{
BlockEquations, BlockSolveError, BlockSolveOptions, BlockSolveOutcome, BlockSolver,
DampedNewtonSolver,
};
pub use bound_tighten::{tighten_bounds, LinearRow, TightenReport, INF_BOUND};
pub use btf::{BlockTriangularBlock, BlockTriangularForm};
pub use components::{SquareComponent, SquareComponents};
pub use coupling::{classify_block, objective_gradient_support, AuxiliaryCouplingClass};
pub use diagnostics::{AuxiliaryPreprocessingDiagnostics, AuxiliaryRejectionReason};
pub use dulmage_mendelsohn::{DMPart, DulmageMendelsohnPartition};
pub use incidence::{EqualityIncidence, InequalityIncidence, ProbeView};
pub use licq::{licq_check, EqRow, LicqVerdict};
pub use options::{register_options, AuxiliaryCouplingPolicy, LicqAction, PresolveOptions};
pub use reduction_frame::{ReductionFrame, ReductionStack};
pub use redundant::find_redundant_rows;
#[derive(Debug)]
pub enum PresolveError {
OptionsError(SolverException),
}
impl std::fmt::Display for PresolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OptionsError(e) => write!(f, "presolve options error: {e}"),
}
}
}
impl std::error::Error for PresolveError {}
impl From<SolverException> for PresolveError {
fn from(e: SolverException) -> Self {
Self::OptionsError(e)
}
}
pub fn wrap_with_presolve(
inner: Rc<RefCell<dyn TNLP>>,
opts: PresolveOptions,
) -> Result<Rc<RefCell<dyn TNLP>>, PresolveError> {
if !opts.enabled {
return Ok(inner);
}
Ok(Rc::new(RefCell::new(PresolveTnlp::new(inner, opts))))
}
pub fn wrap_with_presolve_provider(
inner: Rc<RefCell<dyn TNLP>>,
expr_provider: Rc<RefCell<dyn ExpressionProvider>>,
opts: PresolveOptions,
) -> Result<Rc<RefCell<dyn TNLP>>, PresolveError> {
if !opts.enabled {
return Ok(inner);
}
Ok(Rc::new(RefCell::new(
PresolveTnlp::with_expression_provider(inner, expr_provider, opts),
)))
}
pub fn wrap_from_options(
inner: Rc<RefCell<dyn TNLP>>,
options: &OptionsList,
) -> Result<Rc<RefCell<dyn TNLP>>, PresolveError> {
let opts = PresolveOptions::from_options_list(options)?;
wrap_with_presolve(inner, opts)
}
pub struct CachedBounds {
pub x_l: Vec<Number>,
pub x_u: Vec<Number>,
pub g_l: Vec<Number>,
pub g_u: Vec<Number>,
}
pub struct PresolveTnlp {
inner: Rc<RefCell<dyn TNLP>>,
expr_provider: Option<Rc<RefCell<dyn ExpressionProvider>>>,
opts: PresolveOptions,
state: Option<PresolveState>,
}
struct PresolveState {
info_inner: NlpInfo,
info_outer: NlpInfo,
bounds: CachedBounds,
rows_kept: Vec<usize>,
jac_kept_idx: Vec<usize>,
jac_irow_outer: Vec<Index>,
jac_jcol_outer: Vec<Index>,
tighten_report: TightenReport,
fbbt_report: Option<crate::fbbt::FbbtReport>,
n_dropped_rows: Index,
licq_verdict: Option<LicqVerdict>,
z_l_warm: Vec<Number>,
z_u_warm: Vec<Number>,
scratch_g: Vec<Number>,
scratch_jac: Vec<Number>,
scratch_lambda: Vec<Number>,
aux_diagnostics: AuxiliaryPreprocessingDiagnostics,
#[allow(dead_code)]
reduction_stack: ReductionStack,
}
impl PresolveTnlp {
pub fn new(inner: Rc<RefCell<dyn TNLP>>, opts: PresolveOptions) -> Self {
Self {
inner,
expr_provider: None,
opts,
state: None,
}
}
pub fn with_expression_provider(
inner: Rc<RefCell<dyn TNLP>>,
expr_provider: Rc<RefCell<dyn ExpressionProvider>>,
opts: PresolveOptions,
) -> Self {
Self {
inner,
expr_provider: Some(expr_provider),
opts,
state: None,
}
}
pub fn fbbt_report(&self) -> Option<crate::fbbt::FbbtReport> {
self.state.as_ref().and_then(|s| s.fbbt_report.clone())
}
pub fn tighten_report(&self) -> TightenReport {
self.state
.as_ref()
.map(|s| s.tighten_report.clone())
.unwrap_or_default()
}
pub fn n_dropped_rows(&self) -> Index {
self.state.as_ref().map(|s| s.n_dropped_rows).unwrap_or(0)
}
pub fn cached_bounds(&self) -> Option<&CachedBounds> {
self.state.as_ref().map(|s| &s.bounds)
}
pub fn licq_verdict(&self) -> Option<&LicqVerdict> {
self.state.as_ref().and_then(|s| s.licq_verdict.as_ref())
}
pub fn z_warm_starts(&self) -> Option<(&[Number], &[Number])> {
self.state
.as_ref()
.map(|s| (&s.z_l_warm[..], &s.z_u_warm[..]))
}
pub fn auxiliary_diagnostics(&self) -> AuxiliaryPreprocessingDiagnostics {
self.state
.as_ref()
.map(|s| s.aux_diagnostics.clone())
.unwrap_or_default()
}
fn ensure_init(&mut self) -> Option<&PresolveState> {
if self.state.is_some() {
return self.state.as_ref();
}
let info_inner = self.inner.borrow_mut().get_nlp_info()?;
let n = info_inner.n as usize;
let m_in = info_inner.m as usize;
let nnz_in = info_inner.nnz_jac_g as usize;
let mut x_l = vec![0.0; n];
let mut x_u = vec![0.0; n];
let mut g_l_inner = vec![0.0; m_in];
let mut g_u_inner = vec![0.0; m_in];
{
let mut inner = self.inner.borrow_mut();
if !inner.get_bounds_info(BoundsInfo {
x_l: &mut x_l,
x_u: &mut x_u,
g_l: &mut g_l_inner,
g_u: &mut g_u_inner,
}) {
return None;
}
}
let mut jac_irow_inner = vec![0 as Index; nnz_in];
let mut jac_jcol_inner = vec![0 as Index; nnz_in];
if nnz_in > 0 {
let mut inner = self.inner.borrow_mut();
if !inner.eval_jac_g(
None,
false,
SparsityRequest::Structure {
irow: &mut jac_irow_inner,
jcol: &mut jac_jcol_inner,
},
) {
return None;
}
}
let mut linearity = vec![Linearity::NonLinear; m_in];
let have_linearity = if m_in > 0 {
self.inner
.borrow_mut()
.get_constraints_linearity(&mut linearity)
} else {
true
};
let mut x_probe = vec![0.0; n];
let mut z_l_probe = vec![0.0; n];
let mut z_u_probe = vec![0.0; n];
let mut lambda_probe = vec![0.0; m_in];
let started = self.inner.borrow_mut().get_starting_point(StartingPoint {
init_x: true,
x: &mut x_probe,
init_z: false,
z_l: &mut z_l_probe,
z_u: &mut z_u_probe,
init_lambda: false,
lambda: &mut lambda_probe,
});
if !started {
return None;
}
let mut jac_values_inner = vec![0.0; nnz_in];
if nnz_in > 0 {
let ok = self.inner.borrow_mut().eval_jac_g(
Some(&x_probe),
true,
SparsityRequest::Values {
values: &mut jac_values_inner,
},
);
if !ok {
return None;
}
}
let one_based = matches!(info_inner.index_style, IndexStyle::Fortran);
let mut by_row: Vec<Vec<(Index, Number)>> = vec![Vec::new(); m_in];
for k in 0..nnz_in {
let i = if one_based {
(jac_irow_inner[k] - 1) as usize
} else {
jac_irow_inner[k] as usize
};
let j = if one_based {
jac_jcol_inner[k] - 1
} else {
jac_jcol_inner[k]
};
if i < m_in && (j as usize) < n {
by_row[i].push((j, jac_values_inner[k]));
}
}
let linear_row_map: Vec<Option<LinearRow>> = (0..m_in)
.map(|i| {
if have_linearity && matches!(linearity[i], Linearity::Linear) {
Some(LinearRow {
entries: by_row[i].clone(),
lo: g_l_inner[i],
hi: g_u_inner[i],
})
} else {
None
}
})
.collect();
let inner_x_l = x_l.clone();
let inner_x_u = x_u.clone();
let mut row_kept_inner: Vec<bool> = vec![true; m_in];
let mut reduction_stack = ReductionStack::default();
let aux_diagnostics = if self.opts.auxiliary && m_in > 0 {
let mut g_at_probe = vec![0.0; m_in];
let g_ok = self
.inner
.borrow_mut()
.eval_g(&x_probe, true, &mut g_at_probe);
if !g_ok {
return None;
}
let mut grad_f_probe = vec![0.0; n];
let grad_ok = self
.inner
.borrow_mut()
.eval_grad_f(&x_probe, false, &mut grad_f_probe);
if !grad_ok {
return None;
}
let linearity_for_phase0: Vec<Linearity> = if have_linearity {
linearity.clone()
} else {
vec![Linearity::NonLinear; m_in]
};
let probe_view = auxiliary::Phase0Probe {
n_vars: n,
n_rows: m_in,
jac_irow: &jac_irow_inner,
jac_jcol: &jac_jcol_inner,
jac_values: &jac_values_inner,
g_l: &g_l_inner,
g_u: &g_u_inner,
g_at_probe: &g_at_probe,
linearity: &linearity_for_phase0,
one_based,
eq_tol: 1e-12,
x_probe: &x_probe,
grad_f: &grad_f_probe,
x_l: &x_l,
x_u: &x_u,
};
struct TnlpCallbackAdapter {
inner: Rc<RefCell<dyn TNLP>>,
}
impl auxiliary::Phase0TnlpCallback for TnlpCallbackAdapter {
fn eval_g_full(&mut self, x: &[Number], g: &mut [Number]) -> bool {
self.inner.borrow_mut().eval_g(x, true, g)
}
fn eval_jac_g_values(&mut self, x: &[Number], values: &mut [Number]) -> bool {
self.inner.borrow_mut().eval_jac_g(
Some(x),
true,
SparsityRequest::Values { values },
)
}
}
let mut adapter = TnlpCallbackAdapter {
inner: Rc::clone(&self.inner),
};
let mut large_solver = block_solve::RelaxedNewtonSolver;
let plan = auxiliary::run_auxiliary_phase0(
&self.opts,
&probe_view,
Some(&mut adapter),
Some(&mut large_solver),
);
if let Some(frame) = plan.frame {
for (k, &i) in frame.fixed_vars.iter().enumerate() {
x_l[i] = frame.fixed_values[k];
x_u[i] = frame.fixed_values[k];
}
for &r in &frame.dropped_rows {
row_kept_inner[r] = false;
}
reduction_stack.push(frame);
}
if self.opts.auxiliary_diagnostics {
tracing::info!(target: "pounce::presolve", "{}", plan.diagnostics);
}
plan.diagnostics
} else {
AuxiliaryPreprocessingDiagnostics::default()
};
let linear_rows: Vec<LinearRow> = linear_row_map
.iter()
.enumerate()
.filter_map(|(i, r)| if row_kept_inner[i] { r.clone() } else { None })
.collect();
let mut tighten_report = TightenReport::default();
if self.opts.bound_tightening && !linear_rows.is_empty() {
tighten_report = tighten_bounds(
&linear_rows,
&mut x_l,
&mut x_u,
self.opts.max_passes,
1e-12,
);
}
if tighten_report.infeasible && !reduction_stack.is_empty() {
tracing::warn!(
target: "pounce::presolve",
"auxiliary-equality elimination produced bounds inconsistent \
with kept linear rows; rolling back the elimination for this solve."
);
x_l.copy_from_slice(&inner_x_l);
x_u.copy_from_slice(&inner_x_u);
for kept in row_kept_inner.iter_mut() {
*kept = true;
}
reduction_stack = ReductionStack::default();
let full_linear_rows: Vec<LinearRow> =
linear_row_map.iter().filter_map(|r| r.clone()).collect();
tighten_report = TightenReport::default();
if self.opts.bound_tightening && !full_linear_rows.is_empty() {
tighten_report = tighten_bounds(
&full_linear_rows,
&mut x_l,
&mut x_u,
self.opts.max_passes,
1e-12,
);
}
}
let mut fbbt_report: Option<crate::fbbt::FbbtReport> = None;
if self.opts.fbbt && m_in > 0 {
if let Some(provider) = self.expr_provider.as_ref() {
let cfg = crate::fbbt::FbbtConfig {
tol: self.opts.fbbt_tol,
max_iter: self.opts.fbbt_max_iter.max(1) as usize,
max_constraints: self.opts.fbbt_max_constraints.max(0) as usize,
};
let provider_borrow = provider.borrow();
let report = crate::fbbt::run_fbbt(
&*provider_borrow,
n,
m_in,
&mut x_l,
&mut x_u,
&g_l_inner,
&g_u_inner,
&cfg,
);
fbbt_report = Some(report);
}
}
let warm_tol: Number = 1e-12;
let (z_l_warm, z_u_warm) = if self.opts.warm_z_bounds {
let v0 = self.opts.bound_mult_init_val;
let mut zl = vec![0.0; n];
let mut zu = vec![0.0; n];
for i in 0..n {
if x_l[i] > inner_x_l[i] + warm_tol {
zl[i] = v0;
}
if x_u[i] < inner_x_u[i] - warm_tol {
zu[i] = v0;
}
}
(zl, zu)
} else {
(vec![0.0; n], vec![0.0; n])
};
let mut n_dropped_rows: Index = 0;
if self.opts.redundant_constraint_removal {
let redundant_mask = find_redundant_rows(&linear_rows, &x_l, &x_u, 1e-9);
let mut linear_iter = redundant_mask.iter();
for (i, lr) in linear_row_map.iter().enumerate() {
if lr.is_some() {
let is_red = *linear_iter.next().unwrap_or(&false);
if is_red {
row_kept_inner[i] = false;
n_dropped_rows += 1;
}
}
}
}
let licq_verdict = if self.opts.licq_check {
let eq_tol: Number = 1e-12;
let mut eq_rows: Vec<EqRow> = Vec::new();
for (i, &kept) in row_kept_inner.iter().enumerate() {
if !kept {
continue;
}
if (g_u_inner[i] - g_l_inner[i]).abs() > eq_tol {
continue;
}
use std::collections::BTreeSet;
let mut cols: BTreeSet<Index> = BTreeSet::new();
for &(j, v) in &by_row[i] {
if v != 0.0 {
cols.insert(j);
}
}
eq_rows.push(EqRow {
cols: cols.into_iter().collect(),
});
}
Some(licq_check(&eq_rows, info_inner.n))
} else {
None
};
let mut rows_kept: Vec<usize> = Vec::with_capacity(m_in);
let mut row_inner_to_outer = vec![usize::MAX; m_in];
for (i, &kept) in row_kept_inner.iter().enumerate() {
if kept {
row_inner_to_outer[i] = rows_kept.len();
rows_kept.push(i);
}
}
let m_out = rows_kept.len();
let mut jac_kept_idx = Vec::new();
let mut jac_irow_outer = Vec::new();
let mut jac_jcol_outer = Vec::new();
for k in 0..nnz_in {
let i_inner = if one_based {
(jac_irow_inner[k] - 1) as usize
} else {
jac_irow_inner[k] as usize
};
if i_inner >= m_in {
continue;
}
if !row_kept_inner[i_inner] {
continue;
}
let outer = row_inner_to_outer[i_inner];
let outer_row_index = if one_based {
(outer as Index) + 1
} else {
outer as Index
};
jac_irow_outer.push(outer_row_index);
jac_jcol_outer.push(jac_jcol_inner[k]);
jac_kept_idx.push(k);
}
let nnz_out = jac_kept_idx.len();
let g_l: Vec<Number> = rows_kept.iter().map(|&i| g_l_inner[i]).collect();
let g_u: Vec<Number> = rows_kept.iter().map(|&i| g_u_inner[i]).collect();
let info_outer = NlpInfo {
n: info_inner.n,
m: m_out as Index,
nnz_jac_g: nnz_out as Index,
nnz_h_lag: info_inner.nnz_h_lag,
index_style: info_inner.index_style,
};
self.state = Some(PresolveState {
info_inner,
info_outer,
bounds: CachedBounds { x_l, x_u, g_l, g_u },
rows_kept,
jac_kept_idx,
jac_irow_outer,
jac_jcol_outer,
tighten_report,
fbbt_report,
n_dropped_rows,
licq_verdict,
z_l_warm,
z_u_warm,
scratch_g: vec![0.0; m_in],
scratch_jac: vec![0.0; nnz_in],
scratch_lambda: vec![0.0; m_in],
aux_diagnostics,
reduction_stack,
});
self.state.as_ref()
}
}
#[allow(clippy::expect_used)]
impl TNLP for PresolveTnlp {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
let s = self.ensure_init()?;
Some(s.info_outer)
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
let Some(s) = self.ensure_init() else {
return false;
};
b.x_l.copy_from_slice(&s.bounds.x_l);
b.x_u.copy_from_slice(&s.bounds.x_u);
b.g_l.copy_from_slice(&s.bounds.g_l);
b.g_u.copy_from_slice(&s.bounds.g_u);
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
let m_in = self.state.as_ref().expect("inited").info_inner.m as usize;
let mut z_l_full = vec![0.0; sp.z_l.len()];
let mut z_u_full = vec![0.0; sp.z_u.len()];
let mut lambda_full = vec![0.0; m_in];
let ok = self.inner.borrow_mut().get_starting_point(StartingPoint {
init_x: sp.init_x,
x: sp.x,
init_z: sp.init_z,
z_l: &mut z_l_full,
z_u: &mut z_u_full,
init_lambda: sp.init_lambda,
lambda: &mut lambda_full,
});
if !ok {
return false;
}
sp.z_l.copy_from_slice(&z_l_full);
sp.z_u.copy_from_slice(&z_u_full);
let s = self.state.as_ref().expect("inited");
if sp.init_z && self.opts.warm_z_bounds {
for (i, &hint) in s.z_l_warm.iter().enumerate() {
if hint > 0.0 && sp.z_l[i] <= 0.0 {
sp.z_l[i] = hint;
}
}
for (i, &hint) in s.z_u_warm.iter().enumerate() {
if hint > 0.0 && sp.z_u[i] <= 0.0 {
sp.z_u[i] = hint;
}
}
}
for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
sp.lambda[outer] = lambda_full[i_inner];
}
true
}
fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
self.inner.borrow_mut().eval_f(x, new_x)
}
fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
self.inner.borrow_mut().eval_grad_f(x, new_x, grad_f)
}
fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
let s = self.state.as_mut().expect("inited");
if !self.inner.borrow_mut().eval_g(x, new_x, &mut s.scratch_g) {
return false;
}
for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
g[outer] = s.scratch_g[i_inner];
}
true
}
fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
match mode {
SparsityRequest::Structure { irow, jcol } => {
let s = self.state.as_ref().expect("inited");
irow.copy_from_slice(&s.jac_irow_outer);
jcol.copy_from_slice(&s.jac_jcol_outer);
true
}
SparsityRequest::Values { values } => {
let s = self.state.as_mut().expect("inited");
if !self.inner.borrow_mut().eval_jac_g(
x,
new_x,
SparsityRequest::Values {
values: &mut s.scratch_jac,
},
) {
return false;
}
for (outer_k, &inner_k) in s.jac_kept_idx.iter().enumerate() {
values[outer_k] = s.scratch_jac[inner_k];
}
true
}
}
}
fn eval_h(
&mut self,
x: Option<&[Number]>,
new_x: bool,
obj_factor: Number,
lambda: Option<&[Number]>,
new_lambda: bool,
mode: SparsityRequest<'_>,
) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
let lambda_full_opt = if let Some(lam) = lambda {
let s = self.state.as_mut().expect("inited");
for v in s.scratch_lambda.iter_mut() {
*v = 0.0;
}
for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
s.scratch_lambda[i_inner] = lam[outer];
}
Some(&s.scratch_lambda[..])
} else {
None
};
let lam_ref: Option<&[Number]> = lambda_full_opt;
self.inner
.borrow_mut()
.eval_h(x, new_x, obj_factor, lam_ref, new_lambda, mode)
}
fn finalize_solution(&mut self, sol: Solution<'_>, ip_data: &IpoptData, ip_cq: &IpoptCq) {
let Some(_) = self.ensure_init() else {
self.inner
.borrow_mut()
.finalize_solution(sol, ip_data, ip_cq);
return;
};
let (g_full, mut lambda_full, n_inner, m_inner, nnz_inner, one_based) = {
let s = self.state.as_mut().expect("inited");
self.inner
.borrow_mut()
.eval_g(sol.x, true, &mut s.scratch_g);
for v in s.scratch_lambda.iter_mut() {
*v = 0.0;
}
for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
s.scratch_lambda[i_inner] = sol.lambda[outer];
}
(
s.scratch_g.clone(),
s.scratch_lambda.clone(),
s.info_inner.n as usize,
s.info_inner.m as usize,
s.info_inner.nnz_jac_g as usize,
matches!(s.info_inner.index_style, IndexStyle::Fortran),
)
};
let frames: Vec<reduction_frame::ReductionFrame> = {
let s = self.state.as_ref().expect("inited");
s.reduction_stack.iter_top_down().cloned().collect()
};
if !frames.is_empty() && m_inner > 0 {
let mut grad_f = vec![0.0; n_inner];
let ok_grad = self
.inner
.borrow_mut()
.eval_grad_f(sol.x, true, &mut grad_f);
let mut jac_irow_inner = vec![0 as Index; nnz_inner];
let mut jac_jcol_inner = vec![0 as Index; nnz_inner];
let ok_struct = if nnz_inner > 0 {
self.inner.borrow_mut().eval_jac_g(
None,
false,
SparsityRequest::Structure {
irow: &mut jac_irow_inner,
jcol: &mut jac_jcol_inner,
},
)
} else {
true
};
let mut jac_values = vec![0.0; nnz_inner];
let ok_vals = if nnz_inner > 0 {
self.inner.borrow_mut().eval_jac_g(
Some(sol.x),
false,
SparsityRequest::Values {
values: &mut jac_values,
},
)
} else {
true
};
if ok_grad && ok_struct && ok_vals {
let mut jac_dense = vec![0.0; m_inner * n_inner];
for k in 0..nnz_inner {
let i = if one_based {
(jac_irow_inner[k] as isize - 1) as usize
} else {
jac_irow_inner[k] as usize
};
let j = if one_based {
(jac_jcol_inner[k] as isize - 1) as usize
} else {
jac_jcol_inner[k] as usize
};
if i < m_inner && j < n_inner {
jac_dense[i * n_inner + j] = jac_values[k];
}
}
for frame in &frames {
if let Ok(lam_dropped) =
frame.recover_dropped_multipliers(&grad_f, &jac_dense, &lambda_full)
{
for (idx, &r) in frame.dropped_rows.iter().enumerate() {
lambda_full[r] = lam_dropped[idx];
}
}
}
}
}
self.inner.borrow_mut().finalize_solution(
Solution {
status: sol.status,
x: sol.x,
z_l: sol.z_l,
z_u: sol.z_u,
g: &g_full,
lambda: &lambda_full,
obj_value: sol.obj_value,
},
ip_data,
ip_cq,
);
}
fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
let mut inner_var = MetaData::default();
let mut inner_con = MetaData::default();
if !self
.inner
.borrow_mut()
.get_var_con_metadata(&mut inner_var, &mut inner_con)
{
return false;
}
*var = inner_var;
let s = self.state.as_ref().expect("inited");
let m_in = s.info_inner.m as usize;
*con = project_con_metadata(&inner_con, &s.rows_kept, m_in);
true
}
fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
let s = self.state.as_ref().expect("inited");
let m_in = s.info_inner.m as usize;
let mut inner_g = vec![1.0; m_in];
let mut use_x = false;
let mut use_g = false;
let mut obj_scaling = 1.0;
let inner_x_scaling_len = req.x_scaling.len();
let mut inner_x = vec![1.0; inner_x_scaling_len];
let ok = self
.inner
.borrow_mut()
.get_scaling_parameters(ScalingRequest {
obj_scaling: &mut obj_scaling,
use_x_scaling: &mut use_x,
x_scaling: &mut inner_x,
use_g_scaling: &mut use_g,
g_scaling: &mut inner_g,
});
if !ok {
return false;
}
*req.obj_scaling = obj_scaling;
*req.use_x_scaling = use_x;
*req.use_g_scaling = use_g;
req.x_scaling.copy_from_slice(&inner_x);
for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
req.g_scaling[outer] = inner_g[i_inner];
}
true
}
fn get_variables_linearity(&mut self, types: &mut [Linearity]) -> bool {
self.inner.borrow_mut().get_variables_linearity(types)
}
fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
let Some(_) = self.ensure_init() else {
return false;
};
let m_in = self.state.as_ref().expect("inited").info_inner.m as usize;
let mut full = vec![Linearity::NonLinear; m_in];
if !self.inner.borrow_mut().get_constraints_linearity(&mut full) {
return false;
}
let s = self.state.as_ref().expect("inited");
for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
types[outer] = full[i_inner];
}
true
}
fn get_number_of_nonlinear_variables(&mut self) -> Index {
self.inner.borrow_mut().get_number_of_nonlinear_variables()
}
fn get_list_of_nonlinear_variables(&mut self, pos_nonlin_vars: &mut [Index]) -> bool {
self.inner
.borrow_mut()
.get_list_of_nonlinear_variables(pos_nonlin_vars)
}
fn intermediate_callback(
&mut self,
stats: IterStats,
ip_data: &IpoptData,
ip_cq: &IpoptCq,
) -> bool {
self.inner
.borrow_mut()
.intermediate_callback(stats, ip_data, ip_cq)
}
fn finalize_metadata(&mut self, var: &MetaData, con: &MetaData) {
let Some(_) = self.ensure_init() else {
self.inner.borrow_mut().finalize_metadata(var, con);
return;
};
let s = self.state.as_ref().expect("inited");
let m_in = s.info_inner.m as usize;
let con_full = expand_con_metadata(con, &s.rows_kept, m_in);
self.inner.borrow_mut().finalize_metadata(var, &con_full);
}
}
fn project_con_metadata(inner: &MetaData, rows_kept: &[usize], m_in: usize) -> MetaData {
let mut out = MetaData::default();
for (k, v) in &inner.strings {
out.strings.insert(
k.clone(),
if v.len() == m_in {
rows_kept.iter().map(|&i| v[i].clone()).collect()
} else {
v.clone()
},
);
}
for (k, v) in &inner.integers {
out.integers.insert(
k.clone(),
if v.len() == m_in {
rows_kept.iter().map(|&i| v[i]).collect()
} else {
v.clone()
},
);
}
for (k, v) in &inner.numerics {
out.numerics.insert(
k.clone(),
if v.len() == m_in {
rows_kept.iter().map(|&i| v[i]).collect()
} else {
v.clone()
},
);
}
out
}
fn expand_con_metadata(outer: &MetaData, rows_kept: &[usize], m_in: usize) -> MetaData {
let m_out = rows_kept.len();
let mut full = MetaData::default();
for (k, v) in &outer.strings {
let mut buf: Vec<String> = vec![String::new(); m_in];
if v.len() == m_out {
for (outer_i, val) in v.iter().enumerate() {
buf[rows_kept[outer_i]] = val.clone();
}
full.strings.insert(k.clone(), buf);
} else {
full.strings.insert(k.clone(), v.clone());
}
}
for (k, v) in &outer.integers {
let mut buf: Vec<Index> = vec![0; m_in];
if v.len() == m_out {
for (outer_i, &val) in v.iter().enumerate() {
buf[rows_kept[outer_i]] = val;
}
full.integers.insert(k.clone(), buf);
} else {
full.integers.insert(k.clone(), v.clone());
}
}
for (k, v) in &outer.numerics {
let mut buf: Vec<Number> = vec![0.0; m_in];
if v.len() == m_out {
for (outer_i, &val) in v.iter().enumerate() {
buf[rows_kept[outer_i]] = val;
}
full.numerics.insert(k.clone(), buf);
} else {
full.numerics.insert(k.clone(), v.clone());
}
}
full
}
pub fn register(reg: &RegisteredOptions) -> Result<(), SolverException> {
register_options(reg)
}
#[cfg(test)]
mod tests {
use super::*;
struct Probe;
impl TNLP for Probe {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 1,
m: 0,
nnz_jac_g: 0,
nnz_h_lag: 1,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, _b: BoundsInfo<'_>) -> bool {
true
}
fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
true
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
_mode: SparsityRequest<'_>,
) -> bool {
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn disabled_returns_inner_unchanged() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
let opts = PresolveOptions {
enabled: false,
..PresolveOptions::defaults()
};
let wrapped = wrap_with_presolve(Rc::clone(&inner), opts).unwrap();
assert!(Rc::ptr_eq(&inner, &wrapped));
}
#[test]
fn enabled_wraps_and_forwards() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
let opts = PresolveOptions {
enabled: true,
..PresolveOptions::defaults()
};
let wrapped = wrap_with_presolve(Rc::clone(&inner), opts).unwrap();
assert!(!Rc::ptr_eq(&inner, &wrapped));
let info = wrapped.borrow_mut().get_nlp_info().unwrap();
assert_eq!(info.n, 1);
assert_eq!(info.m, 0);
}
#[test]
fn register_options_roundtrip() {
let reg = RegisteredOptions::default();
register_options(®).unwrap();
let opt = reg.get_option("presolve").expect("presolve registered");
assert_eq!(opt.name, "presolve");
}
#[test]
fn auxiliary_phase0_noop_when_disabled() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
let opts = PresolveOptions {
enabled: true,
auxiliary: false,
..PresolveOptions::defaults()
};
let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
let info = wrapped.get_nlp_info().expect("init ok");
assert_eq!(info.n, 1);
assert_eq!(info.m, 0);
let diag = wrapped.auxiliary_diagnostics();
assert_eq!(diag.blocks_eliminated, 0);
assert_eq!(diag.vars_eliminated, 0);
assert_eq!(diag.rows_eliminated, 0);
}
#[test]
fn auxiliary_phase0_noop_when_enabled_no_algos_yet() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
let opts = PresolveOptions {
enabled: true,
auxiliary: true,
auxiliary_coupling: AuxiliaryCouplingPolicy::Aggressive,
..PresolveOptions::defaults()
};
let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
let info = wrapped.get_nlp_info().expect("init ok");
assert_eq!(info.n, 1);
assert_eq!(info.m, 0);
let diag = wrapped.auxiliary_diagnostics();
assert_eq!(diag.blocks_eliminated, 0);
assert!(diag.rejection_reasons.is_empty());
}
struct TwoVarSquareEq;
impl TNLP for TwoVarSquareEq {
fn get_nlp_info(&mut self) -> Option<NlpInfo> {
Some(NlpInfo {
n: 2,
m: 2,
nnz_jac_g: 4,
nnz_h_lag: 0,
index_style: IndexStyle::C,
})
}
fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
for v in b.x_l.iter_mut() {
*v = -1e19;
}
for v in b.x_u.iter_mut() {
*v = 1e19;
}
b.g_l[0] = 3.0;
b.g_u[0] = 3.0;
b.g_l[1] = 1.0;
b.g_u[1] = 1.0;
true
}
fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
if sp.init_x {
sp.x[0] = 0.0;
sp.x[1] = 0.0;
}
true
}
fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
Some(0.0)
}
fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
for v in g.iter_mut() {
*v = 0.0;
}
true
}
fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
g[0] = x[0] + x[1];
g[1] = x[0] - x[1];
true
}
fn eval_jac_g(
&mut self,
_x: Option<&[Number]>,
_new_x: bool,
mode: SparsityRequest<'_>,
) -> bool {
match mode {
SparsityRequest::Structure { irow, jcol } => {
irow.copy_from_slice(&[0, 0, 1, 1]);
jcol.copy_from_slice(&[0, 1, 0, 1]);
}
SparsityRequest::Values { values } => {
values.copy_from_slice(&[1.0, 1.0, 1.0, -1.0]);
}
}
true
}
fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
types[0] = Linearity::Linear;
types[1] = Linearity::Linear;
true
}
fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
}
#[test]
fn phase0_via_tnlp_eliminates_square_block() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
let opts = PresolveOptions {
enabled: true,
auxiliary: true,
auxiliary_coupling: AuxiliaryCouplingPolicy::Safe,
..PresolveOptions::defaults()
};
let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
let info = wrapped.get_nlp_info().expect("init ok");
assert_eq!(info.n, 2);
assert_eq!(info.m, 0);
let diag = wrapped.auxiliary_diagnostics();
assert_eq!(diag.blocks_eliminated, 1);
assert_eq!(diag.vars_eliminated, 2);
assert_eq!(diag.rows_eliminated, 2);
let bounds = wrapped.cached_bounds().expect("inited");
assert!((bounds.x_l[0] - 2.0).abs() < 1e-12);
assert!((bounds.x_u[0] - 2.0).abs() < 1e-12);
assert!((bounds.x_l[1] - 1.0).abs() < 1e-12);
assert!((bounds.x_u[1] - 1.0).abs() < 1e-12);
}
#[test]
fn phase0_via_tnlp_disabled_is_pass_through() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
let opts = PresolveOptions {
enabled: true,
auxiliary: false,
..PresolveOptions::defaults()
};
let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
let info = wrapped.get_nlp_info().expect("init ok");
assert_eq!(info.n, 2);
assert_eq!(info.m, 2);
let diag = wrapped.auxiliary_diagnostics();
assert_eq!(diag.blocks_eliminated, 0);
}
#[test]
fn phase0_via_tnlp_diagnostics_flag_does_not_break_solve() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
let opts = PresolveOptions {
enabled: true,
auxiliary: true,
auxiliary_diagnostics: true,
..PresolveOptions::defaults()
};
let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
let info = wrapped.get_nlp_info().expect("init ok");
assert_eq!(info.m, 0);
let diag = wrapped.auxiliary_diagnostics();
assert_eq!(diag.blocks_eliminated, 1);
}
#[test]
fn phase0_via_tnlp_no_infeasible_with_default_bound_tightening() {
let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
let opts = PresolveOptions {
enabled: true,
auxiliary: true,
bound_tightening: true,
..PresolveOptions::defaults()
};
let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
let info = wrapped.get_nlp_info().expect("init ok");
assert_eq!(info.m, 0); let bounds = wrapped.cached_bounds().expect("inited");
for i in 0..(info.n as usize) {
assert!(
bounds.x_l[i] <= bounds.x_u[i] + 1e-12,
"x_l[{i}] = {} > x_u[{i}] = {}",
bounds.x_l[i],
bounds.x_u[i]
);
}
let rpt = wrapped.tighten_report();
assert!(!rpt.infeasible, "Phase 1 falsely flagged infeasibility");
}
}