1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
23
24use std::cell::RefCell;
25use std::rc::Rc;
26
27use pounce_common::exception::SolverException;
28use pounce_common::options_list::OptionsList;
29use pounce_common::reg_options::RegisteredOptions;
30use pounce_common::types::{Index, Number};
31use pounce_nlp::expression_provider::ExpressionProvider;
32use pounce_nlp::tnlp::{
33 BoundsInfo, IndexStyle, IpoptCq, IpoptData, IterStats, Linearity, MetaData, NlpInfo,
34 ScalingRequest, Solution, SparsityRequest, StartingPoint, TNLP,
35};
36
37pub mod auxiliary;
38pub mod block_solve;
39pub mod bound_tighten;
40pub mod btf;
41pub mod components;
42pub mod coupling;
43pub mod diagnostics;
44pub mod dulmage_mendelsohn;
45pub mod fbbt;
46pub mod incidence;
47pub mod inequality_projection;
48pub mod licq;
49pub mod matching;
50pub mod options;
51pub mod reduction_frame;
52pub mod redundant;
53pub mod trivial_elim;
54
55pub use block_solve::{
56 BlockEquations, BlockSolveError, BlockSolveOptions, BlockSolveOutcome, BlockSolver,
57 DampedNewtonSolver,
58};
59pub use bound_tighten::{tighten_bounds, LinearRow, TightenReport, INF_BOUND};
60pub use btf::{BlockTriangularBlock, BlockTriangularForm};
61pub use components::{SquareComponent, SquareComponents};
62pub use coupling::{classify_block, objective_gradient_support, AuxiliaryCouplingClass};
63pub use diagnostics::{AuxiliaryPreprocessingDiagnostics, AuxiliaryRejectionReason};
64pub use dulmage_mendelsohn::{DMPart, DulmageMendelsohnPartition};
65pub use incidence::{EqualityIncidence, InequalityIncidence, ProbeView};
66pub use licq::{licq_check, EqRow, LicqVerdict};
67pub use options::{register_options, AuxiliaryCouplingPolicy, LicqAction, PresolveOptions};
68pub use reduction_frame::{ReductionFrame, ReductionStack};
69pub use redundant::find_redundant_rows;
70
71#[derive(Debug)]
73pub enum PresolveError {
74 OptionsError(SolverException),
75}
76
77impl std::fmt::Display for PresolveError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::OptionsError(e) => write!(f, "presolve options error: {e}"),
81 }
82 }
83}
84
85impl std::error::Error for PresolveError {}
86
87impl From<SolverException> for PresolveError {
88 fn from(e: SolverException) -> Self {
89 Self::OptionsError(e)
90 }
91}
92
93pub fn wrap_with_presolve(
97 inner: Rc<RefCell<dyn TNLP>>,
98 opts: PresolveOptions,
99) -> Result<Rc<RefCell<dyn TNLP>>, PresolveError> {
100 if !opts.enabled {
101 return Ok(inner);
102 }
103 Ok(Rc::new(RefCell::new(PresolveTnlp::new(inner, opts))))
104}
105
106pub fn wrap_with_presolve_provider(
113 inner: Rc<RefCell<dyn TNLP>>,
114 expr_provider: Rc<RefCell<dyn ExpressionProvider>>,
115 opts: PresolveOptions,
116) -> Result<Rc<RefCell<dyn TNLP>>, PresolveError> {
117 if !opts.enabled {
118 return Ok(inner);
119 }
120 Ok(Rc::new(RefCell::new(
121 PresolveTnlp::with_expression_provider(inner, expr_provider, opts),
122 )))
123}
124
125pub fn wrap_from_options(
128 inner: Rc<RefCell<dyn TNLP>>,
129 options: &OptionsList,
130) -> Result<Rc<RefCell<dyn TNLP>>, PresolveError> {
131 let opts = PresolveOptions::from_options_list(options)?;
132 wrap_with_presolve(inner, opts)
133}
134
135pub struct CachedBounds {
138 pub x_l: Vec<Number>,
139 pub x_u: Vec<Number>,
140 pub g_l: Vec<Number>,
142 pub g_u: Vec<Number>,
144}
145
146pub struct PresolveTnlp {
148 inner: Rc<RefCell<dyn TNLP>>,
149 expr_provider: Option<Rc<RefCell<dyn ExpressionProvider>>>,
157 opts: PresolveOptions,
158
159 state: Option<PresolveState>,
161}
162
163struct PresolveState {
164 info_inner: NlpInfo,
165 info_outer: NlpInfo,
166 bounds: CachedBounds,
167
168 rows_kept: Vec<usize>,
171
172 jac_kept_idx: Vec<usize>,
174 jac_irow_outer: Vec<Index>,
177 jac_jcol_outer: Vec<Index>,
178
179 tighten_report: TightenReport,
181 fbbt_report: Option<crate::fbbt::FbbtReport>,
184 n_dropped_rows: Index,
186 licq_verdict: Option<LicqVerdict>,
188 z_l_warm: Vec<Number>,
192 z_u_warm: Vec<Number>,
194
195 scratch_g: Vec<Number>,
197 scratch_jac: Vec<Number>,
198 scratch_lambda: Vec<Number>,
199
200 aux_diagnostics: AuxiliaryPreprocessingDiagnostics,
203 #[allow(dead_code)]
205 reduction_stack: ReductionStack,
206}
207
208impl PresolveTnlp {
209 pub fn new(inner: Rc<RefCell<dyn TNLP>>, opts: PresolveOptions) -> Self {
214 Self {
215 inner,
216 expr_provider: None,
217 opts,
218 state: None,
219 }
220 }
221
222 pub fn with_expression_provider(
229 inner: Rc<RefCell<dyn TNLP>>,
230 expr_provider: Rc<RefCell<dyn ExpressionProvider>>,
231 opts: PresolveOptions,
232 ) -> Self {
233 Self {
234 inner,
235 expr_provider: Some(expr_provider),
236 opts,
237 state: None,
238 }
239 }
240
241 pub fn fbbt_report(&self) -> Option<crate::fbbt::FbbtReport> {
244 self.state.as_ref().and_then(|s| s.fbbt_report.clone())
245 }
246
247 pub fn tighten_report(&self) -> TightenReport {
249 self.state
250 .as_ref()
251 .map(|s| s.tighten_report.clone())
252 .unwrap_or_default()
253 }
254
255 pub fn n_dropped_rows(&self) -> Index {
258 self.state.as_ref().map(|s| s.n_dropped_rows).unwrap_or(0)
259 }
260
261 pub fn cached_bounds(&self) -> Option<&CachedBounds> {
263 self.state.as_ref().map(|s| &s.bounds)
264 }
265
266 pub fn licq_verdict(&self) -> Option<&LicqVerdict> {
269 self.state.as_ref().and_then(|s| s.licq_verdict.as_ref())
270 }
271
272 pub fn z_warm_starts(&self) -> Option<(&[Number], &[Number])> {
276 self.state
277 .as_ref()
278 .map(|s| (&s.z_l_warm[..], &s.z_u_warm[..]))
279 }
280
281 pub fn auxiliary_diagnostics(&self) -> AuxiliaryPreprocessingDiagnostics {
286 self.state
287 .as_ref()
288 .map(|s| s.aux_diagnostics.clone())
289 .unwrap_or_default()
290 }
291
292 fn ensure_init(&mut self) -> Option<&PresolveState> {
296 if self.state.is_some() {
297 return self.state.as_ref();
298 }
299
300 let info_inner = self.inner.borrow_mut().get_nlp_info()?;
301 let n = info_inner.n as usize;
302 let m_in = info_inner.m as usize;
303 let nnz_in = info_inner.nnz_jac_g as usize;
304
305 let mut x_l = vec![0.0; n];
307 let mut x_u = vec![0.0; n];
308 let mut g_l_inner = vec![0.0; m_in];
309 let mut g_u_inner = vec![0.0; m_in];
310 {
311 let mut inner = self.inner.borrow_mut();
312 if !inner.get_bounds_info(BoundsInfo {
313 x_l: &mut x_l,
314 x_u: &mut x_u,
315 g_l: &mut g_l_inner,
316 g_u: &mut g_u_inner,
317 }) {
318 return None;
319 }
320 }
321
322 let mut jac_irow_inner = vec![0 as Index; nnz_in];
324 let mut jac_jcol_inner = vec![0 as Index; nnz_in];
325 if nnz_in > 0 {
326 let mut inner = self.inner.borrow_mut();
327 if !inner.eval_jac_g(
328 None,
329 false,
330 SparsityRequest::Structure {
331 irow: &mut jac_irow_inner,
332 jcol: &mut jac_jcol_inner,
333 },
334 ) {
335 return None;
336 }
337 }
338
339 let mut linearity = vec![Linearity::NonLinear; m_in];
341 let have_linearity = if m_in > 0 {
342 self.inner
343 .borrow_mut()
344 .get_constraints_linearity(&mut linearity)
345 } else {
346 true
347 };
348
349 let mut x_probe = vec![0.0; n];
353 let mut z_l_probe = vec![0.0; n];
354 let mut z_u_probe = vec![0.0; n];
355 let mut lambda_probe = vec![0.0; m_in];
356 let started = self.inner.borrow_mut().get_starting_point(StartingPoint {
357 init_x: true,
358 x: &mut x_probe,
359 init_z: false,
360 z_l: &mut z_l_probe,
361 z_u: &mut z_u_probe,
362 init_lambda: false,
363 lambda: &mut lambda_probe,
364 });
365 if !started {
366 return None;
367 }
368
369 let mut jac_values_inner = vec![0.0; nnz_in];
371 if nnz_in > 0 {
372 let ok = self.inner.borrow_mut().eval_jac_g(
373 Some(&x_probe),
374 true,
375 SparsityRequest::Values {
376 values: &mut jac_values_inner,
377 },
378 );
379 if !ok {
380 return None;
381 }
382 }
383
384 let one_based = matches!(info_inner.index_style, IndexStyle::Fortran);
386 let mut by_row: Vec<Vec<(Index, Number)>> = vec![Vec::new(); m_in];
387 for k in 0..nnz_in {
388 let i = if one_based {
389 (jac_irow_inner[k] - 1) as usize
390 } else {
391 jac_irow_inner[k] as usize
392 };
393 let j = if one_based {
394 jac_jcol_inner[k] - 1
395 } else {
396 jac_jcol_inner[k]
397 };
398 if i < m_in && (j as usize) < n {
399 by_row[i].push((j, jac_values_inner[k]));
400 }
401 }
402 let linear_row_map: Vec<Option<LinearRow>> = (0..m_in)
403 .map(|i| {
404 if have_linearity && matches!(linearity[i], Linearity::Linear) {
405 Some(LinearRow {
406 entries: by_row[i].clone(),
407 lo: g_l_inner[i],
408 hi: g_u_inner[i],
409 })
410 } else {
411 None
412 }
413 })
414 .collect();
415 let inner_x_l = x_l.clone();
425 let inner_x_u = x_u.clone();
426
427 let mut row_kept_inner: Vec<bool> = vec![true; m_in];
434 let mut reduction_stack = ReductionStack::default();
435 let aux_diagnostics = if self.opts.auxiliary && m_in > 0 {
436 let mut g_at_probe = vec![0.0; m_in];
438 let g_ok = self
439 .inner
440 .borrow_mut()
441 .eval_g(&x_probe, true, &mut g_at_probe);
442 if !g_ok {
443 return None;
444 }
445 let mut grad_f_probe = vec![0.0; n];
446 let grad_ok = self
447 .inner
448 .borrow_mut()
449 .eval_grad_f(&x_probe, false, &mut grad_f_probe);
450 if !grad_ok {
451 return None;
452 }
453 let linearity_for_phase0: Vec<Linearity> = if have_linearity {
455 linearity.clone()
456 } else {
457 vec![Linearity::NonLinear; m_in]
458 };
459 let probe_view = auxiliary::Phase0Probe {
460 n_vars: n,
461 n_rows: m_in,
462 jac_irow: &jac_irow_inner,
463 jac_jcol: &jac_jcol_inner,
464 jac_values: &jac_values_inner,
465 g_l: &g_l_inner,
466 g_u: &g_u_inner,
467 g_at_probe: &g_at_probe,
468 linearity: &linearity_for_phase0,
469 one_based,
470 eq_tol: 1e-12,
471 x_probe: &x_probe,
472 grad_f: &grad_f_probe,
473 x_l: &x_l,
474 x_u: &x_u,
475 };
476 struct TnlpCallbackAdapter {
479 inner: Rc<RefCell<dyn TNLP>>,
480 }
481 impl auxiliary::Phase0TnlpCallback for TnlpCallbackAdapter {
482 fn eval_g_full(&mut self, x: &[Number], g: &mut [Number]) -> bool {
483 self.inner.borrow_mut().eval_g(x, true, g)
484 }
485 fn eval_jac_g_values(&mut self, x: &[Number], values: &mut [Number]) -> bool {
486 self.inner.borrow_mut().eval_jac_g(
487 Some(x),
488 true,
489 SparsityRequest::Values { values },
490 )
491 }
492 }
493 let mut adapter = TnlpCallbackAdapter {
494 inner: Rc::clone(&self.inner),
495 };
496 let mut large_solver = block_solve::RelaxedNewtonSolver;
497 let plan = auxiliary::run_auxiliary_phase0(
498 &self.opts,
499 &probe_view,
500 Some(&mut adapter),
501 Some(&mut large_solver),
502 );
503 if let Some(frame) = plan.frame {
504 for (k, &i) in frame.fixed_vars.iter().enumerate() {
506 x_l[i] = frame.fixed_values[k];
507 x_u[i] = frame.fixed_values[k];
508 }
509 for &r in &frame.dropped_rows {
511 row_kept_inner[r] = false;
512 }
513 reduction_stack.push(frame);
514 }
515 if self.opts.auxiliary_diagnostics {
518 tracing::info!(target: "pounce::presolve", "{}", plan.diagnostics);
519 }
520 plan.diagnostics
521 } else {
522 AuxiliaryPreprocessingDiagnostics::default()
523 };
524
525 let linear_rows: Vec<LinearRow> = linear_row_map
531 .iter()
532 .enumerate()
533 .filter_map(|(i, r)| if row_kept_inner[i] { r.clone() } else { None })
534 .collect();
535
536 let mut tighten_report = TightenReport::default();
538 if self.opts.bound_tightening && !linear_rows.is_empty() {
539 tighten_report = tighten_bounds(
540 &linear_rows,
541 &mut x_l,
542 &mut x_u,
543 self.opts.max_passes,
544 1e-12,
545 );
546 }
547
548 if tighten_report.infeasible && !reduction_stack.is_empty() {
557 tracing::warn!(
558 target: "pounce::presolve",
559 "auxiliary-equality elimination produced bounds inconsistent \
560 with kept linear rows; rolling back the elimination for this solve."
561 );
562 x_l.copy_from_slice(&inner_x_l);
563 x_u.copy_from_slice(&inner_x_u);
564 for kept in row_kept_inner.iter_mut() {
565 *kept = true;
566 }
567 reduction_stack = ReductionStack::default();
568 let full_linear_rows: Vec<LinearRow> =
571 linear_row_map.iter().filter_map(|r| r.clone()).collect();
572 tighten_report = TightenReport::default();
573 if self.opts.bound_tightening && !full_linear_rows.is_empty() {
574 tighten_report = tighten_bounds(
575 &full_linear_rows,
576 &mut x_l,
577 &mut x_u,
578 self.opts.max_passes,
579 1e-12,
580 );
581 }
582 }
583
584 let mut fbbt_report: Option<crate::fbbt::FbbtReport> = None;
592 if self.opts.fbbt && m_in > 0 {
593 if let Some(provider) = self.expr_provider.as_ref() {
594 let cfg = crate::fbbt::FbbtConfig {
595 tol: self.opts.fbbt_tol,
596 max_iter: self.opts.fbbt_max_iter.max(1) as usize,
597 max_constraints: self.opts.fbbt_max_constraints.max(0) as usize,
598 };
599 let provider_borrow = provider.borrow();
600 let report = crate::fbbt::run_fbbt(
601 &*provider_borrow,
602 n,
603 m_in,
604 &mut x_l,
605 &mut x_u,
606 &g_l_inner,
607 &g_u_inner,
608 &cfg,
609 );
610 fbbt_report = Some(report);
611 }
612 }
613
614 let warm_tol: Number = 1e-12;
619 let (z_l_warm, z_u_warm) = if self.opts.warm_z_bounds {
620 let v0 = self.opts.bound_mult_init_val;
621 let mut zl = vec![0.0; n];
622 let mut zu = vec![0.0; n];
623 for i in 0..n {
624 if x_l[i] > inner_x_l[i] + warm_tol {
625 zl[i] = v0;
626 }
627 if x_u[i] < inner_x_u[i] - warm_tol {
628 zu[i] = v0;
629 }
630 }
631 (zl, zu)
632 } else {
633 (vec![0.0; n], vec![0.0; n])
634 };
635
636 let mut n_dropped_rows: Index = 0;
640 if self.opts.redundant_constraint_removal {
641 let redundant_mask = find_redundant_rows(&linear_rows, &x_l, &x_u, 1e-9);
642 let mut linear_iter = redundant_mask.iter();
645 for (i, lr) in linear_row_map.iter().enumerate() {
646 if lr.is_some() {
647 let is_red = *linear_iter.next().unwrap_or(&false);
648 if is_red {
649 row_kept_inner[i] = false;
650 n_dropped_rows += 1;
651 }
652 }
653 }
654 }
655
656 let licq_verdict = if self.opts.licq_check {
658 let eq_tol: Number = 1e-12;
659 let mut eq_rows: Vec<EqRow> = Vec::new();
660 for (i, &kept) in row_kept_inner.iter().enumerate() {
661 if !kept {
662 continue;
663 }
664 if (g_u_inner[i] - g_l_inner[i]).abs() > eq_tol {
665 continue;
666 }
667 use std::collections::BTreeSet;
668 let mut cols: BTreeSet<Index> = BTreeSet::new();
669 for &(j, v) in &by_row[i] {
670 if v != 0.0 {
671 cols.insert(j);
672 }
673 }
674 eq_rows.push(EqRow {
675 cols: cols.into_iter().collect(),
676 });
677 }
678 Some(licq_check(&eq_rows, info_inner.n))
679 } else {
680 None
681 };
682
683 let mut rows_kept: Vec<usize> = Vec::with_capacity(m_in);
685 let mut row_inner_to_outer = vec![usize::MAX; m_in];
686 for (i, &kept) in row_kept_inner.iter().enumerate() {
687 if kept {
688 row_inner_to_outer[i] = rows_kept.len();
689 rows_kept.push(i);
690 }
691 }
692 let m_out = rows_kept.len();
693
694 let mut jac_kept_idx = Vec::new();
697 let mut jac_irow_outer = Vec::new();
698 let mut jac_jcol_outer = Vec::new();
699 for k in 0..nnz_in {
700 let i_inner = if one_based {
701 (jac_irow_inner[k] - 1) as usize
702 } else {
703 jac_irow_inner[k] as usize
704 };
705 if i_inner >= m_in {
706 continue;
707 }
708 if !row_kept_inner[i_inner] {
709 continue;
710 }
711 let outer = row_inner_to_outer[i_inner];
712 let outer_row_index = if one_based {
713 (outer as Index) + 1
714 } else {
715 outer as Index
716 };
717 jac_irow_outer.push(outer_row_index);
718 jac_jcol_outer.push(jac_jcol_inner[k]);
719 jac_kept_idx.push(k);
720 }
721 let nnz_out = jac_kept_idx.len();
722
723 let g_l: Vec<Number> = rows_kept.iter().map(|&i| g_l_inner[i]).collect();
725 let g_u: Vec<Number> = rows_kept.iter().map(|&i| g_u_inner[i]).collect();
726
727 let info_outer = NlpInfo {
728 n: info_inner.n,
729 m: m_out as Index,
730 nnz_jac_g: nnz_out as Index,
731 nnz_h_lag: info_inner.nnz_h_lag,
735 index_style: info_inner.index_style,
736 };
737
738 self.state = Some(PresolveState {
739 info_inner,
740 info_outer,
741 bounds: CachedBounds { x_l, x_u, g_l, g_u },
742 rows_kept,
743 jac_kept_idx,
744 jac_irow_outer,
745 jac_jcol_outer,
746 tighten_report,
747 fbbt_report,
748 n_dropped_rows,
749 licq_verdict,
750 z_l_warm,
751 z_u_warm,
752 scratch_g: vec![0.0; m_in],
753 scratch_jac: vec![0.0; nnz_in],
754 scratch_lambda: vec![0.0; m_in],
755 aux_diagnostics,
756 reduction_stack,
757 });
758 self.state.as_ref()
759 }
760}
761
762#[allow(clippy::expect_used)]
766impl TNLP for PresolveTnlp {
767 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
768 let s = self.ensure_init()?;
769 Some(s.info_outer)
770 }
771
772 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
773 let Some(s) = self.ensure_init() else {
774 return false;
775 };
776 b.x_l.copy_from_slice(&s.bounds.x_l);
777 b.x_u.copy_from_slice(&s.bounds.x_u);
778 b.g_l.copy_from_slice(&s.bounds.g_l);
779 b.g_u.copy_from_slice(&s.bounds.g_u);
780 true
781 }
782
783 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
784 let Some(_) = self.ensure_init() else {
787 return false;
788 };
789 let m_in = self.state.as_ref().expect("inited").info_inner.m as usize;
793 let mut z_l_full = vec![0.0; sp.z_l.len()];
794 let mut z_u_full = vec![0.0; sp.z_u.len()];
795 let mut lambda_full = vec![0.0; m_in];
796 let ok = self.inner.borrow_mut().get_starting_point(StartingPoint {
797 init_x: sp.init_x,
798 x: sp.x,
799 init_z: sp.init_z,
800 z_l: &mut z_l_full,
801 z_u: &mut z_u_full,
802 init_lambda: sp.init_lambda,
803 lambda: &mut lambda_full,
804 });
805 if !ok {
806 return false;
807 }
808 sp.z_l.copy_from_slice(&z_l_full);
809 sp.z_u.copy_from_slice(&z_u_full);
810 let s = self.state.as_ref().expect("inited");
811 if sp.init_z && self.opts.warm_z_bounds {
814 for (i, &hint) in s.z_l_warm.iter().enumerate() {
815 if hint > 0.0 && sp.z_l[i] <= 0.0 {
816 sp.z_l[i] = hint;
817 }
818 }
819 for (i, &hint) in s.z_u_warm.iter().enumerate() {
820 if hint > 0.0 && sp.z_u[i] <= 0.0 {
821 sp.z_u[i] = hint;
822 }
823 }
824 }
825 for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
826 sp.lambda[outer] = lambda_full[i_inner];
827 }
828 true
829 }
830
831 fn eval_f(&mut self, x: &[Number], new_x: bool) -> Option<Number> {
832 self.inner.borrow_mut().eval_f(x, new_x)
833 }
834
835 fn eval_grad_f(&mut self, x: &[Number], new_x: bool, grad_f: &mut [Number]) -> bool {
836 self.inner.borrow_mut().eval_grad_f(x, new_x, grad_f)
837 }
838
839 fn eval_g(&mut self, x: &[Number], new_x: bool, g: &mut [Number]) -> bool {
840 let Some(_) = self.ensure_init() else {
841 return false;
842 };
843 let s = self.state.as_mut().expect("inited");
844 if !self.inner.borrow_mut().eval_g(x, new_x, &mut s.scratch_g) {
845 return false;
846 }
847 for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
848 g[outer] = s.scratch_g[i_inner];
849 }
850 true
851 }
852
853 fn eval_jac_g(&mut self, x: Option<&[Number]>, new_x: bool, mode: SparsityRequest<'_>) -> bool {
854 let Some(_) = self.ensure_init() else {
855 return false;
856 };
857 match mode {
858 SparsityRequest::Structure { irow, jcol } => {
859 let s = self.state.as_ref().expect("inited");
860 irow.copy_from_slice(&s.jac_irow_outer);
861 jcol.copy_from_slice(&s.jac_jcol_outer);
862 true
863 }
864 SparsityRequest::Values { values } => {
865 let s = self.state.as_mut().expect("inited");
866 if !self.inner.borrow_mut().eval_jac_g(
867 x,
868 new_x,
869 SparsityRequest::Values {
870 values: &mut s.scratch_jac,
871 },
872 ) {
873 return false;
874 }
875 for (outer_k, &inner_k) in s.jac_kept_idx.iter().enumerate() {
876 values[outer_k] = s.scratch_jac[inner_k];
877 }
878 true
879 }
880 }
881 }
882
883 fn eval_h(
884 &mut self,
885 x: Option<&[Number]>,
886 new_x: bool,
887 obj_factor: Number,
888 lambda: Option<&[Number]>,
889 new_lambda: bool,
890 mode: SparsityRequest<'_>,
891 ) -> bool {
892 let Some(_) = self.ensure_init() else {
893 return false;
894 };
895 let lambda_full_opt = if let Some(lam) = lambda {
899 let s = self.state.as_mut().expect("inited");
900 for v in s.scratch_lambda.iter_mut() {
901 *v = 0.0;
902 }
903 for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
904 s.scratch_lambda[i_inner] = lam[outer];
905 }
906 Some(&s.scratch_lambda[..])
907 } else {
908 None
909 };
910 let lam_ref: Option<&[Number]> = lambda_full_opt;
912 self.inner
915 .borrow_mut()
916 .eval_h(x, new_x, obj_factor, lam_ref, new_lambda, mode)
917 }
918
919 fn finalize_solution(&mut self, sol: Solution<'_>, ip_data: &IpoptData, ip_cq: &IpoptCq) {
920 let Some(_) = self.ensure_init() else {
921 self.inner
923 .borrow_mut()
924 .finalize_solution(sol, ip_data, ip_cq);
925 return;
926 };
927 let (g_full, mut lambda_full, n_inner, m_inner, nnz_inner, one_based) = {
929 let s = self.state.as_mut().expect("inited");
930 self.inner
932 .borrow_mut()
933 .eval_g(sol.x, true, &mut s.scratch_g);
934 for v in s.scratch_lambda.iter_mut() {
935 *v = 0.0;
936 }
937 for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
938 s.scratch_lambda[i_inner] = sol.lambda[outer];
939 }
940 (
941 s.scratch_g.clone(),
942 s.scratch_lambda.clone(),
943 s.info_inner.n as usize,
944 s.info_inner.m as usize,
945 s.info_inner.nnz_jac_g as usize,
946 matches!(s.info_inner.index_style, IndexStyle::Fortran),
947 )
948 };
949
950 let frames: Vec<reduction_frame::ReductionFrame> = {
956 let s = self.state.as_ref().expect("inited");
957 s.reduction_stack.iter_top_down().cloned().collect()
958 };
959 if !frames.is_empty() && m_inner > 0 {
960 let mut grad_f = vec![0.0; n_inner];
961 let ok_grad = self
962 .inner
963 .borrow_mut()
964 .eval_grad_f(sol.x, true, &mut grad_f);
965 let mut jac_irow_inner = vec![0 as Index; nnz_inner];
967 let mut jac_jcol_inner = vec![0 as Index; nnz_inner];
968 let ok_struct = if nnz_inner > 0 {
969 self.inner.borrow_mut().eval_jac_g(
970 None,
971 false,
972 SparsityRequest::Structure {
973 irow: &mut jac_irow_inner,
974 jcol: &mut jac_jcol_inner,
975 },
976 )
977 } else {
978 true
979 };
980 let mut jac_values = vec![0.0; nnz_inner];
981 let ok_vals = if nnz_inner > 0 {
982 self.inner.borrow_mut().eval_jac_g(
983 Some(sol.x),
984 false,
985 SparsityRequest::Values {
986 values: &mut jac_values,
987 },
988 )
989 } else {
990 true
991 };
992 if ok_grad && ok_struct && ok_vals {
993 let mut jac_dense = vec![0.0; m_inner * n_inner];
995 for k in 0..nnz_inner {
996 let i = if one_based {
997 (jac_irow_inner[k] as isize - 1) as usize
998 } else {
999 jac_irow_inner[k] as usize
1000 };
1001 let j = if one_based {
1002 (jac_jcol_inner[k] as isize - 1) as usize
1003 } else {
1004 jac_jcol_inner[k] as usize
1005 };
1006 if i < m_inner && j < n_inner {
1007 jac_dense[i * n_inner + j] = jac_values[k];
1008 }
1009 }
1010 for frame in &frames {
1011 if let Ok(lam_dropped) =
1012 frame.recover_dropped_multipliers(&grad_f, &jac_dense, &lambda_full)
1013 {
1014 for (idx, &r) in frame.dropped_rows.iter().enumerate() {
1015 lambda_full[r] = lam_dropped[idx];
1016 }
1017 }
1018 }
1019 }
1020 }
1021 self.inner.borrow_mut().finalize_solution(
1022 Solution {
1023 status: sol.status,
1024 x: sol.x,
1025 z_l: sol.z_l,
1026 z_u: sol.z_u,
1027 g: &g_full,
1028 lambda: &lambda_full,
1029 obj_value: sol.obj_value,
1030 },
1031 ip_data,
1032 ip_cq,
1033 );
1034 }
1035
1036 fn get_var_con_metadata(&mut self, var: &mut MetaData, con: &mut MetaData) -> bool {
1037 let Some(_) = self.ensure_init() else {
1038 return false;
1039 };
1040 let mut inner_var = MetaData::default();
1044 let mut inner_con = MetaData::default();
1045 if !self
1046 .inner
1047 .borrow_mut()
1048 .get_var_con_metadata(&mut inner_var, &mut inner_con)
1049 {
1050 return false;
1051 }
1052 *var = inner_var;
1053 let s = self.state.as_ref().expect("inited");
1054 let m_in = s.info_inner.m as usize;
1055 *con = project_con_metadata(&inner_con, &s.rows_kept, m_in);
1056 true
1057 }
1058
1059 fn get_scaling_parameters(&mut self, req: ScalingRequest<'_>) -> bool {
1060 let Some(_) = self.ensure_init() else {
1061 return false;
1062 };
1063 let s = self.state.as_ref().expect("inited");
1064 let m_in = s.info_inner.m as usize;
1065 let mut inner_g = vec![1.0; m_in];
1067 let mut use_x = false;
1068 let mut use_g = false;
1069 let mut obj_scaling = 1.0;
1070 let inner_x_scaling_len = req.x_scaling.len();
1071 let mut inner_x = vec![1.0; inner_x_scaling_len];
1072 let ok = self
1073 .inner
1074 .borrow_mut()
1075 .get_scaling_parameters(ScalingRequest {
1076 obj_scaling: &mut obj_scaling,
1077 use_x_scaling: &mut use_x,
1078 x_scaling: &mut inner_x,
1079 use_g_scaling: &mut use_g,
1080 g_scaling: &mut inner_g,
1081 });
1082 if !ok {
1083 return false;
1084 }
1085 *req.obj_scaling = obj_scaling;
1086 *req.use_x_scaling = use_x;
1087 *req.use_g_scaling = use_g;
1088 req.x_scaling.copy_from_slice(&inner_x);
1089 for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
1090 req.g_scaling[outer] = inner_g[i_inner];
1091 }
1092 true
1093 }
1094
1095 fn get_variables_linearity(&mut self, types: &mut [Linearity]) -> bool {
1096 self.inner.borrow_mut().get_variables_linearity(types)
1097 }
1098
1099 fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
1100 let Some(_) = self.ensure_init() else {
1101 return false;
1102 };
1103 let m_in = self.state.as_ref().expect("inited").info_inner.m as usize;
1104 let mut full = vec![Linearity::NonLinear; m_in];
1105 if !self.inner.borrow_mut().get_constraints_linearity(&mut full) {
1106 return false;
1107 }
1108 let s = self.state.as_ref().expect("inited");
1109 for (outer, &i_inner) in s.rows_kept.iter().enumerate() {
1110 types[outer] = full[i_inner];
1111 }
1112 true
1113 }
1114
1115 fn get_number_of_nonlinear_variables(&mut self) -> Index {
1116 self.inner.borrow_mut().get_number_of_nonlinear_variables()
1117 }
1118
1119 fn get_list_of_nonlinear_variables(&mut self, pos_nonlin_vars: &mut [Index]) -> bool {
1120 self.inner
1121 .borrow_mut()
1122 .get_list_of_nonlinear_variables(pos_nonlin_vars)
1123 }
1124
1125 fn intermediate_callback(
1126 &mut self,
1127 stats: IterStats,
1128 ip_data: &IpoptData,
1129 ip_cq: &IpoptCq,
1130 ) -> bool {
1131 self.inner
1132 .borrow_mut()
1133 .intermediate_callback(stats, ip_data, ip_cq)
1134 }
1135
1136 fn finalize_metadata(&mut self, var: &MetaData, con: &MetaData) {
1137 let Some(_) = self.ensure_init() else {
1138 self.inner.borrow_mut().finalize_metadata(var, con);
1139 return;
1140 };
1141 let s = self.state.as_ref().expect("inited");
1142 let m_in = s.info_inner.m as usize;
1143 let con_full = expand_con_metadata(con, &s.rows_kept, m_in);
1144 self.inner.borrow_mut().finalize_metadata(var, &con_full);
1145 }
1146}
1147
1148fn project_con_metadata(inner: &MetaData, rows_kept: &[usize], m_in: usize) -> MetaData {
1152 let mut out = MetaData::default();
1153 for (k, v) in &inner.strings {
1154 out.strings.insert(
1155 k.clone(),
1156 if v.len() == m_in {
1157 rows_kept.iter().map(|&i| v[i].clone()).collect()
1158 } else {
1159 v.clone()
1160 },
1161 );
1162 }
1163 for (k, v) in &inner.integers {
1164 out.integers.insert(
1165 k.clone(),
1166 if v.len() == m_in {
1167 rows_kept.iter().map(|&i| v[i]).collect()
1168 } else {
1169 v.clone()
1170 },
1171 );
1172 }
1173 for (k, v) in &inner.numerics {
1174 out.numerics.insert(
1175 k.clone(),
1176 if v.len() == m_in {
1177 rows_kept.iter().map(|&i| v[i]).collect()
1178 } else {
1179 v.clone()
1180 },
1181 );
1182 }
1183 out
1184}
1185
1186fn expand_con_metadata(outer: &MetaData, rows_kept: &[usize], m_in: usize) -> MetaData {
1189 let m_out = rows_kept.len();
1190 let mut full = MetaData::default();
1191 for (k, v) in &outer.strings {
1192 let mut buf: Vec<String> = vec![String::new(); m_in];
1193 if v.len() == m_out {
1194 for (outer_i, val) in v.iter().enumerate() {
1195 buf[rows_kept[outer_i]] = val.clone();
1196 }
1197 full.strings.insert(k.clone(), buf);
1198 } else {
1199 full.strings.insert(k.clone(), v.clone());
1200 }
1201 }
1202 for (k, v) in &outer.integers {
1203 let mut buf: Vec<Index> = vec![0; m_in];
1204 if v.len() == m_out {
1205 for (outer_i, &val) in v.iter().enumerate() {
1206 buf[rows_kept[outer_i]] = val;
1207 }
1208 full.integers.insert(k.clone(), buf);
1209 } else {
1210 full.integers.insert(k.clone(), v.clone());
1211 }
1212 }
1213 for (k, v) in &outer.numerics {
1214 let mut buf: Vec<Number> = vec![0.0; m_in];
1215 if v.len() == m_out {
1216 for (outer_i, &val) in v.iter().enumerate() {
1217 buf[rows_kept[outer_i]] = val;
1218 }
1219 full.numerics.insert(k.clone(), buf);
1220 } else {
1221 full.numerics.insert(k.clone(), v.clone());
1222 }
1223 }
1224 full
1225}
1226
1227pub fn register(reg: &RegisteredOptions) -> Result<(), SolverException> {
1230 register_options(reg)
1231}
1232
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236
1237 struct Probe;
1238 impl TNLP for Probe {
1239 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1240 Some(NlpInfo {
1241 n: 1,
1242 m: 0,
1243 nnz_jac_g: 0,
1244 nnz_h_lag: 1,
1245 index_style: IndexStyle::C,
1246 })
1247 }
1248 fn get_bounds_info(&mut self, _b: BoundsInfo<'_>) -> bool {
1249 true
1250 }
1251 fn get_starting_point(&mut self, _sp: StartingPoint<'_>) -> bool {
1252 true
1253 }
1254 fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
1255 Some(0.0)
1256 }
1257 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
1258 true
1259 }
1260 fn eval_g(&mut self, _x: &[Number], _new_x: bool, _g: &mut [Number]) -> bool {
1261 true
1262 }
1263 fn eval_jac_g(
1264 &mut self,
1265 _x: Option<&[Number]>,
1266 _new_x: bool,
1267 _mode: SparsityRequest<'_>,
1268 ) -> bool {
1269 true
1270 }
1271 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
1272 }
1273
1274 #[test]
1275 fn disabled_returns_inner_unchanged() {
1276 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
1277 let opts = PresolveOptions {
1278 enabled: false,
1279 ..PresolveOptions::defaults()
1280 };
1281 let wrapped = wrap_with_presolve(Rc::clone(&inner), opts).unwrap();
1282 assert!(Rc::ptr_eq(&inner, &wrapped));
1283 }
1284
1285 #[test]
1286 fn enabled_wraps_and_forwards() {
1287 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
1288 let opts = PresolveOptions {
1289 enabled: true,
1290 ..PresolveOptions::defaults()
1291 };
1292 let wrapped = wrap_with_presolve(Rc::clone(&inner), opts).unwrap();
1293 assert!(!Rc::ptr_eq(&inner, &wrapped));
1294 let info = wrapped.borrow_mut().get_nlp_info().unwrap();
1295 assert_eq!(info.n, 1);
1296 assert_eq!(info.m, 0);
1297 }
1298
1299 #[test]
1300 fn register_options_roundtrip() {
1301 let reg = RegisteredOptions::default();
1302 register_options(®).unwrap();
1303 let opt = reg.get_option("presolve").expect("presolve registered");
1304 assert_eq!(opt.name, "presolve");
1305 }
1306
1307 #[test]
1308 fn auxiliary_phase0_noop_when_disabled() {
1309 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
1312 let opts = PresolveOptions {
1313 enabled: true,
1314 auxiliary: false,
1315 ..PresolveOptions::defaults()
1316 };
1317 let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
1318 let info = wrapped.get_nlp_info().expect("init ok");
1319 assert_eq!(info.n, 1);
1320 assert_eq!(info.m, 0);
1321 let diag = wrapped.auxiliary_diagnostics();
1322 assert_eq!(diag.blocks_eliminated, 0);
1323 assert_eq!(diag.vars_eliminated, 0);
1324 assert_eq!(diag.rows_eliminated, 0);
1325 }
1326
1327 #[test]
1328 fn auxiliary_phase0_noop_when_enabled_no_algos_yet() {
1329 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(Probe));
1332 let opts = PresolveOptions {
1333 enabled: true,
1334 auxiliary: true,
1335 auxiliary_coupling: AuxiliaryCouplingPolicy::Aggressive,
1336 ..PresolveOptions::defaults()
1337 };
1338 let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
1339 let info = wrapped.get_nlp_info().expect("init ok");
1340 assert_eq!(info.n, 1);
1341 assert_eq!(info.m, 0);
1342 let diag = wrapped.auxiliary_diagnostics();
1343 assert_eq!(diag.blocks_eliminated, 0);
1344 assert!(diag.rejection_reasons.is_empty());
1345 }
1346
1347 struct TwoVarSquareEq;
1351 impl TNLP for TwoVarSquareEq {
1352 fn get_nlp_info(&mut self) -> Option<NlpInfo> {
1353 Some(NlpInfo {
1354 n: 2,
1355 m: 2,
1356 nnz_jac_g: 4,
1357 nnz_h_lag: 0,
1358 index_style: IndexStyle::C,
1359 })
1360 }
1361 fn get_bounds_info(&mut self, b: BoundsInfo<'_>) -> bool {
1362 for v in b.x_l.iter_mut() {
1363 *v = -1e19;
1364 }
1365 for v in b.x_u.iter_mut() {
1366 *v = 1e19;
1367 }
1368 b.g_l[0] = 3.0;
1369 b.g_u[0] = 3.0;
1370 b.g_l[1] = 1.0;
1371 b.g_u[1] = 1.0;
1372 true
1373 }
1374 fn get_starting_point(&mut self, sp: StartingPoint<'_>) -> bool {
1375 if sp.init_x {
1376 sp.x[0] = 0.0;
1377 sp.x[1] = 0.0;
1378 }
1379 true
1380 }
1381 fn eval_f(&mut self, _x: &[Number], _new_x: bool) -> Option<Number> {
1382 Some(0.0)
1383 }
1384 fn eval_grad_f(&mut self, _x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
1385 for v in g.iter_mut() {
1386 *v = 0.0;
1387 }
1388 true
1389 }
1390 fn eval_g(&mut self, x: &[Number], _new_x: bool, g: &mut [Number]) -> bool {
1391 g[0] = x[0] + x[1];
1392 g[1] = x[0] - x[1];
1393 true
1394 }
1395 fn eval_jac_g(
1396 &mut self,
1397 _x: Option<&[Number]>,
1398 _new_x: bool,
1399 mode: SparsityRequest<'_>,
1400 ) -> bool {
1401 match mode {
1402 SparsityRequest::Structure { irow, jcol } => {
1403 irow.copy_from_slice(&[0, 0, 1, 1]);
1404 jcol.copy_from_slice(&[0, 1, 0, 1]);
1405 }
1406 SparsityRequest::Values { values } => {
1407 values.copy_from_slice(&[1.0, 1.0, 1.0, -1.0]);
1408 }
1409 }
1410 true
1411 }
1412 fn get_constraints_linearity(&mut self, types: &mut [Linearity]) -> bool {
1413 types[0] = Linearity::Linear;
1414 types[1] = Linearity::Linear;
1415 true
1416 }
1417 fn finalize_solution(&mut self, _sol: Solution<'_>, _d: &IpoptData, _q: &IpoptCq) {}
1418 }
1419
1420 #[test]
1421 fn phase0_via_tnlp_eliminates_square_block() {
1422 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
1423 let opts = PresolveOptions {
1424 enabled: true,
1425 auxiliary: true,
1426 auxiliary_coupling: AuxiliaryCouplingPolicy::Safe,
1427 ..PresolveOptions::defaults()
1428 };
1429 let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
1430 let info = wrapped.get_nlp_info().expect("init ok");
1431 assert_eq!(info.n, 2);
1433 assert_eq!(info.m, 0);
1435
1436 let diag = wrapped.auxiliary_diagnostics();
1437 assert_eq!(diag.blocks_eliminated, 1);
1438 assert_eq!(diag.vars_eliminated, 2);
1439 assert_eq!(diag.rows_eliminated, 2);
1440
1441 let bounds = wrapped.cached_bounds().expect("inited");
1442 assert!((bounds.x_l[0] - 2.0).abs() < 1e-12);
1443 assert!((bounds.x_u[0] - 2.0).abs() < 1e-12);
1444 assert!((bounds.x_l[1] - 1.0).abs() < 1e-12);
1445 assert!((bounds.x_u[1] - 1.0).abs() < 1e-12);
1446 }
1447
1448 #[test]
1449 fn phase0_via_tnlp_disabled_is_pass_through() {
1450 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
1453 let opts = PresolveOptions {
1454 enabled: true,
1455 auxiliary: false,
1456 ..PresolveOptions::defaults()
1457 };
1458 let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
1459 let info = wrapped.get_nlp_info().expect("init ok");
1460 assert_eq!(info.n, 2);
1461 assert_eq!(info.m, 2);
1462 let diag = wrapped.auxiliary_diagnostics();
1463 assert_eq!(diag.blocks_eliminated, 0);
1464 }
1465
1466 #[test]
1471 fn phase0_via_tnlp_diagnostics_flag_does_not_break_solve() {
1472 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
1473 let opts = PresolveOptions {
1474 enabled: true,
1475 auxiliary: true,
1476 auxiliary_diagnostics: true,
1477 ..PresolveOptions::defaults()
1478 };
1479 let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
1480 let info = wrapped.get_nlp_info().expect("init ok");
1481 assert_eq!(info.m, 0);
1482 let diag = wrapped.auxiliary_diagnostics();
1483 assert_eq!(diag.blocks_eliminated, 1);
1484 }
1485
1486 #[test]
1502 fn phase0_via_tnlp_no_infeasible_with_default_bound_tightening() {
1503 let inner: Rc<RefCell<dyn TNLP>> = Rc::new(RefCell::new(TwoVarSquareEq));
1504 let opts = PresolveOptions {
1505 enabled: true,
1506 auxiliary: true,
1507 bound_tightening: true,
1510 ..PresolveOptions::defaults()
1511 };
1512 let mut wrapped = PresolveTnlp::new(Rc::clone(&inner), opts);
1513 let info = wrapped.get_nlp_info().expect("init ok");
1514 assert_eq!(info.m, 0); let bounds = wrapped.cached_bounds().expect("inited");
1516 for i in 0..(info.n as usize) {
1518 assert!(
1519 bounds.x_l[i] <= bounds.x_u[i] + 1e-12,
1520 "x_l[{i}] = {} > x_u[{i}] = {}",
1521 bounds.x_l[i],
1522 bounds.x_u[i]
1523 );
1524 }
1525 let rpt = wrapped.tighten_report();
1527 assert!(!rpt.infeasible, "Phase 1 falsely flagged infeasibility");
1528 }
1529}