1use pounce_common::exception::SolverException;
9use pounce_common::options_list::OptionsList;
10use pounce_common::reg_options::RegisteredOptions;
11use pounce_common::types::{Index, Number};
12
13#[derive(Debug, Clone, Copy)]
19pub struct PresolveOptions {
20 pub enabled: bool,
23 pub bound_tightening: bool,
25 pub redundant_constraint_removal: bool,
27 pub linear_eq_reduction: bool,
29 pub licq_check: bool,
31 pub print_level: Index,
33 pub max_passes: Index,
35 pub licq_action: LicqAction,
38 pub warm_z_bounds: bool,
41 pub bound_mult_init_val: Number,
43 pub auxiliary: bool,
48 pub auxiliary_tol: Number,
52 pub auxiliary_max_block_dim: Index,
56 pub auxiliary_wall_time_fraction: Number,
59 pub auxiliary_coupling: AuxiliaryCouplingPolicy,
62 pub auxiliary_diagnostics: bool,
65 pub fbbt: bool,
69 pub fbbt_tol: Number,
71 pub fbbt_max_iter: Index,
73 pub fbbt_max_constraints: Index,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum LicqAction {
80 Warn,
82 AutoL1,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum AuxiliaryCouplingPolicy {
94 None,
96 Safe,
98 Aggressive,
100}
101
102impl PresolveOptions {
103 pub fn defaults() -> Self {
105 Self {
106 enabled: false,
107 bound_tightening: true,
108 redundant_constraint_removal: true,
109 linear_eq_reduction: false,
110 licq_check: true,
111 print_level: 0,
112 max_passes: 3,
113 licq_action: LicqAction::Warn,
114 warm_z_bounds: true,
115 bound_mult_init_val: 1.0,
116 auxiliary: false,
117 auxiliary_tol: 1e-8,
118 auxiliary_max_block_dim: 8,
119 auxiliary_wall_time_fraction: 0.1,
120 auxiliary_coupling: AuxiliaryCouplingPolicy::Safe,
121 auxiliary_diagnostics: false,
122 fbbt: false,
123 fbbt_tol: 1e-6,
124 fbbt_max_iter: 10,
125 fbbt_max_constraints: 0,
126 }
127 }
128
129 pub fn from_options_list(opts: &OptionsList) -> Result<Self, SolverException> {
132 let enabled = opts.get_bool_value("presolve", "")?.0;
133 let bound_tightening = opts.get_bool_value("presolve_bound_tightening", "")?.0;
134 let redundant_constraint_removal = opts
135 .get_bool_value("presolve_redundant_constraint_removal", "")?
136 .0;
137 let linear_eq_reduction = opts.get_bool_value("presolve_linear_eq_reduction", "")?.0;
138 let licq_check = opts.get_bool_value("presolve_licq_check", "")?.0;
139 let print_level = opts.get_integer_value("presolve_print_level", "")?.0;
140 let max_passes = opts.get_integer_value("presolve_max_passes", "")?.0;
141 let licq_action = match opts
142 .get_string_value("presolve_licq_action", "")?
143 .0
144 .as_str()
145 {
146 "auto_l1" => LicqAction::AutoL1,
147 _ => LicqAction::Warn,
149 };
150 let warm_z_bounds = opts.get_bool_value("presolve_warm_z_bounds", "")?.0;
151 let bound_mult_init_val = opts
152 .get_numeric_value("presolve_bound_mult_init_val", "")?
153 .0;
154 let auxiliary = opts.get_bool_value("presolve_auxiliary", "")?.0;
155 let auxiliary_tol = opts.get_numeric_value("presolve_auxiliary_tol", "")?.0;
156 let auxiliary_max_block_dim = opts
157 .get_integer_value("presolve_auxiliary_max_block_dim", "")?
158 .0;
159 let auxiliary_wall_time_fraction = opts
160 .get_numeric_value("presolve_auxiliary_wall_time_fraction", "")?
161 .0;
162 let auxiliary_coupling = match opts
163 .get_string_value("presolve_auxiliary_coupling", "")?
164 .0
165 .as_str()
166 {
167 "none" => AuxiliaryCouplingPolicy::None,
168 "aggressive" => AuxiliaryCouplingPolicy::Aggressive,
169 _ => AuxiliaryCouplingPolicy::Safe,
171 };
172 let auxiliary_diagnostics = opts.get_bool_value("presolve_auxiliary_diagnostics", "")?.0;
173 let fbbt = opts.get_bool_value("presolve_fbbt", "")?.0;
174 let fbbt_tol = opts.get_numeric_value("fbbt_tol", "")?.0;
175 let fbbt_max_iter = opts.get_integer_value("fbbt_max_iter", "")?.0;
176 let fbbt_max_constraints = opts.get_integer_value("fbbt_max_constraints", "")?.0;
177 Ok(Self {
178 enabled,
179 bound_tightening,
180 redundant_constraint_removal,
181 linear_eq_reduction,
182 licq_check,
183 print_level,
184 max_passes,
185 licq_action,
186 warm_z_bounds,
187 bound_mult_init_val,
188 auxiliary,
189 auxiliary_tol,
190 auxiliary_max_block_dim,
191 auxiliary_wall_time_fraction,
192 auxiliary_coupling,
193 auxiliary_diagnostics,
194 fbbt,
195 fbbt_tol,
196 fbbt_max_iter,
197 fbbt_max_constraints,
198 })
199 }
200}
201
202pub fn register_options(reg: &RegisteredOptions) -> Result<(), SolverException> {
205 reg.set_registering_category("NLP Presolve");
206
207 reg.add_bool_option(
208 "presolve",
209 "Master switch for algorithmic NLP preprocessing.",
210 false,
211 "If yes, wraps the user TNLP with a presolve layer that may \
212 tighten variable bounds, drop redundant constraints, and \
213 detect rank-deficient equality blocks before the IPM starts. \
214 Off by default; the per-pass options below are then dormant.",
215 )?;
216
217 reg.add_bool_option(
218 "presolve_bound_tightening",
219 "Tighten variable bounds via constraint propagation.",
220 true,
221 "When presolve is enabled, iteratively propagates each linear \
222 constraint into implied bounds on its variables (Andersen's \
223 LP presolve §2 adapted to NLP).",
224 )?;
225
226 reg.add_bool_option(
227 "presolve_redundant_constraint_removal",
228 "Drop constraints implied by current variable bounds.",
229 true,
230 "For each linear constraint, checks whether [lo, hi] is \
231 implied by the box [x_l, x_u]; drops those that are.",
232 )?;
233
234 reg.add_bool_option(
235 "presolve_linear_eq_reduction",
236 "Eliminate variables via linear-equality rows.",
237 false,
238 "Reduces problem dimension by Gauss-eliminating against \
239 linear equality rows. Off by default because it changes \
240 the variable count and complicates sensitivity integration.",
241 )?;
242
243 reg.add_bool_option(
244 "presolve_licq_check",
245 "Detect rank-deficient equality blocks before the IPM starts.",
246 true,
247 "Probes rank(J_c) at the starting point via a sparse symbolic \
248 factor. Interlocks with `presolve_licq_action` to (optionally) \
249 auto-activate the ℓ₁-exact penalty-barrier wrapper.",
250 )?;
251
252 reg.add_string_option(
253 "presolve_licq_action",
254 "Action when presolve_licq_check reports rank deficiency.",
255 "warn",
256 &[
257 ("warn", "Report on the journalist; do not modify the solve."),
258 (
259 "auto_l1",
260 "Set l1_exact_penalty_barrier=yes so the ℓ₁ wrapper takes over.",
261 ),
262 ],
263 "Only consulted when presolve_licq_check=yes and the verdict \
264 is non-full-rank.",
265 )?;
266
267 reg.add_bounded_integer_option(
268 "presolve_print_level",
269 "Per-pass progress reporting for presolve.",
270 0,
271 8,
272 0,
273 "0 silent; 5 prints a one-line summary per pass; 8 prints \
274 per-transformation detail (intended for debugging).",
275 )?;
276
277 reg.add_bounded_integer_option(
278 "presolve_max_passes",
279 "Maximum fixed-point iterations across presolve passes.",
280 1,
281 50,
282 3,
283 "Bound tightening (Phase 1) is iterated until no bound \
284 changes or this cap is hit.",
285 )?;
286
287 reg.add_bool_option(
288 "presolve_warm_z_bounds",
289 "Warm-start bound multipliers for bounds tightened by presolve.",
290 true,
291 "When a variable's lower (upper) bound is moved by Phase 1 \
292 tightening, that side is likely active at the optimum. With \
293 this option on and `init_z=yes` requested, the wrapper sets \
294 z_l (z_u) for those variables to `presolve_bound_mult_init_val` \
295 instead of the global default.",
296 )?;
297
298 reg.add_lower_bounded_number_option(
299 "presolve_bound_mult_init_val",
300 "Value used when warm-starting bound multipliers from presolve.",
301 0.0,
302 true,
303 1.0,
304 "Only consulted when presolve_warm_z_bounds=yes.",
305 )?;
306
307 reg.add_bool_option(
310 "presolve_auxiliary",
311 "Master switch for auxiliary-equality preprocessing (Phase 0).",
312 false,
313 "Structural NLP preprocessing: incidence graph → bipartite \
314 matching → Dulmage-Mendelsohn → block-triangular form → \
315 per-block solve → postsolve multiplier recovery. Defaults \
316 off; will land incrementally across pounce#53.",
317 )?;
318
319 reg.add_lower_bounded_number_option(
320 "presolve_auxiliary_tol",
321 "Residual tolerance for accepting an auxiliary block solve.",
322 0.0,
323 true,
324 1e-8,
325 "Full-space KKT residual after the candidate reduction must \
326 stay within this; otherwise the reduction is rejected.",
327 )?;
328
329 reg.add_bounded_integer_option(
330 "presolve_auxiliary_max_block_dim",
331 "Largest block the lightweight Newton solver will attempt.",
332 1,
333 1024,
334 8,
335 "Blocks above this dimension fall through to the BlockSolver \
336 trait (IPM-backed fallback lands in PR 11 of pounce#53).",
337 )?;
338
339 reg.add_bounded_number_option(
340 "presolve_auxiliary_wall_time_fraction",
341 "Fraction of overall wall-time budget Phase 0 may spend.",
342 0.0,
343 true,
344 1.0,
345 false,
346 0.1,
347 "When the solver enforces a wall-time limit, Phase 0 is given \
348 this fraction of the budget; honoured by PR 8 onwards.",
349 )?;
350
351 reg.add_string_option(
352 "presolve_auxiliary_coupling",
353 "Coupling-class gate for auxiliary block elimination.",
354 "safe",
355 &[
356 (
357 "none",
358 "Run Phase 0 for diagnostics only; eliminate nothing.",
359 ),
360 (
361 "safe",
362 "Eliminate only PureEquality blocks (matches ripopt default).",
363 ),
364 (
365 "aggressive",
366 "Also accept ObjectiveCoupled blocks as postsolve candidates.",
367 ),
368 ],
369 "Only consulted when presolve_auxiliary=yes.",
370 )?;
371
372 reg.add_bool_option(
373 "presolve_auxiliary_diagnostics",
374 "Emit the auxiliary-preprocessing diagnostics summary.",
375 false,
376 "When yes, the diagnostics struct (block counts, timings, \
377 rejection reasons) is published via the journalist after \
378 Phase 0 runs.",
379 )?;
380
381 reg.add_bool_option(
382 "presolve_fbbt",
383 "Feasibility-Based Bound Tightening on nonlinear constraints.",
384 false,
385 "When yes, runs interval-arithmetic propagation through each \
386 constraint's expression DAG to tighten variable bounds before \
387 the IPM starts (issue #62). Off by default — only TNLPs that \
388 implement the `ExpressionProvider` trait (currently: `.nl` \
389 files via pounce-cli's `NlTnlp`) participate; others are no-op.",
390 )?;
391
392 reg.add_lower_bounded_number_option(
393 "fbbt_tol",
394 "Minimum bound improvement to keep FBBT iterating.",
395 0.0,
396 true,
397 1.0e-6,
398 "Per Belotti et al. (2010), FBBT may not converge finitely \
399 even on linear constraints, so termination is tolerance-based: \
400 a sweep that produces no per-variable improvement above this \
401 threshold stops the outer loop.",
402 )?;
403
404 reg.add_bounded_integer_option(
405 "fbbt_max_iter",
406 "Outer-sweep cap for FBBT.",
407 1,
408 100,
409 10,
410 "Hard ceiling on the number of FBBT sweeps over all \
411 constraints. The outer loop terminates earlier as soon as a \
412 sweep produces no tightening above `fbbt_tol`.",
413 )?;
414
415 reg.add_lower_bounded_integer_option(
416 "fbbt_max_constraints",
417 "Per-sweep cap on the number of constraints FBBT inspects.",
418 0,
419 0,
420 "Useful as a wall-time guard on very large problems where the \
421 first few constraints carry most of the tightening. `0` means \
422 unlimited.",
423 )?;
424
425 reg.set_registering_category("");
426 Ok(())
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use std::rc::Rc;
433
434 fn reg_with_presolve() -> Rc<RegisteredOptions> {
435 let reg = RegisteredOptions::default();
436 register_options(®).unwrap();
437 Rc::new(reg)
438 }
439
440 #[test]
441 fn defaults_round_trip() {
442 let reg = reg_with_presolve();
443 let opts = OptionsList::with_registered(reg);
444 let p = PresolveOptions::from_options_list(&opts).unwrap();
445 assert!(!p.enabled);
446 assert!(p.bound_tightening);
447 assert!(p.redundant_constraint_removal);
448 assert!(!p.linear_eq_reduction);
449 assert!(p.licq_check);
450 assert_eq!(p.licq_action, LicqAction::Warn);
451 assert_eq!(p.print_level, 0);
452 assert_eq!(p.max_passes, 3);
453 }
454
455 #[test]
456 fn enabling_master_switch_round_trips() {
457 let reg = reg_with_presolve();
458 let mut opts = OptionsList::with_registered(reg);
459 opts.set_string_value("presolve", "yes", true, false)
460 .unwrap();
461 opts.set_string_value("presolve_licq_action", "auto_l1", true, false)
462 .unwrap();
463 opts.set_integer_value("presolve_max_passes", 5, true, false)
464 .unwrap();
465 let p = PresolveOptions::from_options_list(&opts).unwrap();
466 assert!(p.enabled);
467 assert_eq!(p.licq_action, LicqAction::AutoL1);
468 assert_eq!(p.max_passes, 5);
469 }
470
471 #[test]
472 fn invalid_licq_action_rejected_at_set_time() {
473 let reg = reg_with_presolve();
474 let mut opts = OptionsList::with_registered(reg);
475 let err = opts
477 .set_string_value("presolve_licq_action", "bogus", true, false)
478 .err();
479 assert!(err.is_some(), "invalid enum should be rejected at set time");
480 }
481
482 #[test]
483 fn auxiliary_defaults_round_trip() {
484 let reg = reg_with_presolve();
485 let opts = OptionsList::with_registered(reg);
486 let p = PresolveOptions::from_options_list(&opts).unwrap();
487 assert!(!p.auxiliary);
488 assert_eq!(p.auxiliary_tol, 1e-8);
489 assert_eq!(p.auxiliary_max_block_dim, 8);
490 assert_eq!(p.auxiliary_wall_time_fraction, 0.1);
491 assert_eq!(p.auxiliary_coupling, AuxiliaryCouplingPolicy::Safe);
492 assert!(!p.auxiliary_diagnostics);
493 }
494
495 #[test]
496 fn auxiliary_master_switch_round_trips() {
497 let reg = reg_with_presolve();
498 let mut opts = OptionsList::with_registered(reg);
499 opts.set_string_value("presolve_auxiliary", "yes", true, false)
500 .unwrap();
501 opts.set_string_value("presolve_auxiliary_coupling", "aggressive", true, false)
502 .unwrap();
503 opts.set_numeric_value("presolve_auxiliary_tol", 1e-10, true, false)
504 .unwrap();
505 opts.set_integer_value("presolve_auxiliary_max_block_dim", 16, true, false)
506 .unwrap();
507 opts.set_string_value("presolve_auxiliary_diagnostics", "yes", true, false)
508 .unwrap();
509 let p = PresolveOptions::from_options_list(&opts).unwrap();
510 assert!(p.auxiliary);
511 assert_eq!(p.auxiliary_coupling, AuxiliaryCouplingPolicy::Aggressive);
512 assert_eq!(p.auxiliary_tol, 1e-10);
513 assert_eq!(p.auxiliary_max_block_dim, 16);
514 assert!(p.auxiliary_diagnostics);
515 }
516
517 #[test]
518 fn invalid_auxiliary_coupling_rejected_at_set_time() {
519 let reg = reg_with_presolve();
520 let mut opts = OptionsList::with_registered(reg);
521 let err = opts
522 .set_string_value("presolve_auxiliary_coupling", "bogus", true, false)
523 .err();
524 assert!(err.is_some(), "invalid enum should be rejected at set time");
525 }
526}