1use oximo_core::ModelKind;
2use oximo_solver::{HasUniversal, UniversalOptions};
3
4#[derive(Clone, Debug, Default, PartialEq)]
19pub struct PounceOptions {
20 pub universal: UniversalOptions,
21 pub tol: Option<f64>,
23 pub max_iter: Option<u32>,
25 pub print_level: Option<u32>,
27 pub mu_strategy: Option<MuStrategy>,
29 pub algorithm: Option<PounceAlgorithm>,
32 pub solver_selection: Option<PounceSolverSelection>,
36 num_opts: Vec<(&'static str, f64)>,
38 int_opts: Vec<(&'static str, i32)>,
39 str_opts: Vec<(&'static str, String)>,
40 bool_opts: Vec<(&'static str, bool)>,
41 pub extra: Vec<(String, PounceOptionValue)>,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum MuStrategy {
48 Monotone,
49 Adaptive,
50}
51
52#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
60pub enum PounceAlgorithm {
61 #[default]
63 InteriorPoint,
64 ActiveSetSqp,
66}
67
68#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub enum PounceSolverSelection {
71 #[default]
74 Auto,
75 Nlp,
77 LpIpm,
79 QpIpm,
81 QpActiveSet,
83 Socp,
85}
86
87impl PounceSolverSelection {
88 pub(crate) const fn as_str(self) -> &'static str {
89 match self {
90 Self::Auto => "auto",
91 Self::Nlp => "nlp",
92 Self::LpIpm => "lp-ipm",
93 Self::QpIpm => "qp-ipm",
94 Self::QpActiveSet => "qp-active-set",
95 Self::Socp => "socp",
96 }
97 }
98
99 pub(crate) fn parse(value: &str) -> Option<Self> {
100 match value {
101 "auto" => Some(Self::Auto),
102 "nlp" => Some(Self::Nlp),
103 "lp-ipm" => Some(Self::LpIpm),
104 "qp-ipm" => Some(Self::QpIpm),
105 "qp-active-set" => Some(Self::QpActiveSet),
106 "socp" => Some(Self::Socp),
107 _ => None,
108 }
109 }
110}
111
112impl PounceAlgorithm {
113 pub(crate) const fn as_str(self) -> &'static str {
114 match self {
115 Self::InteriorPoint => "interior-point",
116 Self::ActiveSetSqp => "active-set-sqp",
117 }
118 }
119
120 pub(crate) const fn supports(self, kind: ModelKind) -> bool {
121 match self {
122 Self::InteriorPoint | Self::ActiveSetSqp => {
123 matches!(
124 kind,
125 ModelKind::LP
126 | ModelKind::QP
127 | ModelKind::QCP
128 | ModelKind::SOCP
129 | ModelKind::NLP
130 )
131 }
132 }
133 }
134}
135
136#[derive(Clone, Debug, PartialEq)]
138pub enum PounceOptionValue {
139 Num(f64),
140 Int(i32),
141 Str(String),
142 Bool(bool),
143}
144
145impl From<f64> for PounceOptionValue {
146 fn from(v: f64) -> Self {
147 Self::Num(v)
148 }
149}
150
151impl From<i32> for PounceOptionValue {
152 fn from(v: i32) -> Self {
153 Self::Int(v)
154 }
155}
156
157impl From<&str> for PounceOptionValue {
158 fn from(v: &str) -> Self {
159 Self::Str(v.to_owned())
160 }
161}
162
163impl From<bool> for PounceOptionValue {
164 fn from(v: bool) -> Self {
165 Self::Bool(v)
166 }
167}
168
169macro_rules! pounce_options {
172 ($( ($kind:ident, $method:ident, $tag:literal) ),* $(,)?) => {
173 $(pounce_options!(@impl $kind, $method, $tag);)*
174 };
175 (@impl num, $method:ident, $tag:literal) => {
176 #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
177 #[must_use]
178 pub fn $method(mut self, v: f64) -> Self {
179 self.num_opts.push(($tag, v));
180 self
181 }
182 };
183 (@impl int, $method:ident, $tag:literal) => {
184 #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
185 #[must_use]
186 pub fn $method(mut self, v: i32) -> Self {
187 self.int_opts.push(($tag, v));
188 self
189 }
190 };
191 (@impl str, $method:ident, $tag:literal) => {
192 #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
193 #[must_use]
194 pub fn $method(mut self, v: impl Into<String>) -> Self {
195 self.str_opts.push(($tag, v.into()));
196 self
197 }
198 };
199 (@impl bool, $method:ident, $tag:literal) => {
200 #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
201 #[must_use]
202 pub fn $method(mut self, v: bool) -> Self {
203 self.bool_opts.push(($tag, v));
204 self
205 }
206 };
207}
208
209impl PounceOptions {
210 pounce_options!(
211 (str, mu_oracle, "mu_oracle"),
213 (num, mu_init, "mu_init"),
214 (num, mu_min, "mu_min"),
215 (num, mu_max, "mu_max"),
216 (num, mu_max_fact, "mu_max_fact"),
217 (num, mu_target, "mu_target"),
218 (num, mu_linear_decrease_factor, "mu_linear_decrease_factor"),
219 (num, mu_superlinear_decrease_power, "mu_superlinear_decrease_power"),
220 (num, barrier_tol_factor, "barrier_tol_factor"),
221 (num, sigma_max, "sigma_max"),
222 (num, sigma_min, "sigma_min"),
223 (str, adaptive_mu_globalization, "adaptive_mu_globalization"),
224 (int, adam_warmup_iters, "adam_warmup_iters"),
225 (num, adam_warmup_learning_rate, "adam_warmup_learning_rate"),
226 (num, adam_warmup_penalty, "adam_warmup_penalty"),
227 (num, adaptive_mu_budget_pin_fraction, "adaptive_mu_budget_pin_fraction"),
228 (int, adaptive_mu_max_free_returns, "adaptive_mu_max_free_returns"),
229 (num, alpha_red_factor_min, "alpha_red_factor_min"),
230 (num, bound_frac, "bound_frac"),
231 (str, bound_mult_init_method, "bound_mult_init_method"),
232 (num, bound_mult_init_val, "bound_mult_init_val"),
233 (num, bound_push, "bound_push"),
234 (num, constr_mult_init_max, "constr_mult_init_max"),
235 (num, slack_bound_frac, "slack_bound_frac"),
236 (num, slack_bound_push, "slack_bound_push"),
237 (str, least_square_init_duals, "least_square_init_duals"),
238 (str, least_square_init_primal, "least_square_init_primal"),
239 (str, start_point_conditioner, "start_point_conditioner"),
240 (num, start_point_perturbation, "start_point_perturbation"),
241 (int, start_point_perturbation_seed, "start_point_perturbation_seed"),
242 (str, warm_start_recentering, "warm_start_recentering"),
243 (str, crossover, "crossover"),
244 (int, crossover_max_iter, "crossover_max_iter"),
245 (num, crossover_mult_tol, "crossover_mult_tol"),
246 (num, crossover_primal_tol, "crossover_primal_tol"),
247 (str, fd_hessian_coloring, "fd_hessian_coloring"),
248 (str, fd_hessian_pattern, "fd_hessian_pattern"),
249 (num, fd_hessian_reuse_tol, "fd_hessian_reuse_tol"),
250 (int, limited_memory_ls_failure_restarts, "limited_memory_ls_failure_restarts"),
251 (int, neg_curv_escapes, "neg_curv_escapes"),
252 (int, partitioned_block_size, "partitioned_block_size"),
253 (num, partitioned_curvature_cap, "partitioned_curvature_cap"),
254 (str, partitioned_elements, "partitioned_elements"),
255 (int, partitioned_max_element, "partitioned_max_element"),
256 (str, partitioned_update_type, "partitioned_update_type"),
257 (bool, feral_increase_quality, "feral_increase_quality"),
258 (bool, feral_increase_quality_retry, "feral_increase_quality_retry"),
259 (int, feral_refine_steps, "feral_refine_steps"),
260 (num, feral_refine_target, "feral_refine_target"),
261 (bool, ma57_batched_backsolve, "ma57_batched_backsolve"),
262 (bool, dual_divergence_retry, "dual_divergence_retry"),
263 (num, dual_divergence_retry_du_floor, "dual_divergence_retry_du_floor"),
264 (num, dual_divergence_retry_step_tol, "dual_divergence_retry_step_tol"),
265 (bool, infeasibility_perturbed_start_retry, "infeasibility_perturbed_start_retry"),
266 (int, perturb_delta_c_max_rungs, "perturb_delta_c_max_rungs"),
267 (str, quality_function_norm_type, "quality_function_norm_type"),
269 (str, quality_function_centrality, "quality_function_centrality"),
270 (str, quality_function_balancing_term, "quality_function_balancing_term"),
271 (int, quality_function_max_section_steps, "quality_function_max_section_steps"),
272 (num, quality_function_section_sigma_tol, "quality_function_section_sigma_tol"),
273 (num, quality_function_section_qf_tol, "quality_function_section_qf_tol"),
274 (num, adaptive_mu_safeguard_factor, "adaptive_mu_safeguard_factor"),
276 (num, adaptive_mu_monotone_init_factor, "adaptive_mu_monotone_init_factor"),
277 (bool, adaptive_mu_restore_previous_iterate, "adaptive_mu_restore_previous_iterate"),
278 (int, adaptive_mu_kkterror_red_iters, "adaptive_mu_kkterror_red_iters"),
279 (num, adaptive_mu_kkterror_red_fact, "adaptive_mu_kkterror_red_fact"),
280 (str, adaptive_mu_kkt_norm_type, "adaptive_mu_kkt_norm_type"),
281 (bool, l1_exact_penalty_barrier, "l1_exact_penalty_barrier"),
283 (bool, l1_fallback_on_restoration_failure, "l1_fallback_on_restoration_failure"),
284 (num, l1_penalty_init, "l1_penalty_init"),
285 (num, l1_penalty_max, "l1_penalty_max"),
286 (num, l1_penalty_increase_factor, "l1_penalty_increase_factor"),
287 (int, l1_penalty_max_outer_iter, "l1_penalty_max_outer_iter"),
288 (num, l1_slack_tol, "l1_slack_tol"),
289 (num, l1_steering_factor, "l1_steering_factor"),
290 (bool, presolve, "presolve"),
292 (bool, presolve_bound_tightening, "presolve_bound_tightening"),
293 (bool, presolve_redundant_constraint_removal, "presolve_redundant_constraint_removal"),
294 (bool, presolve_linear_eq_reduction, "presolve_linear_eq_reduction"),
295 (bool, presolve_licq_check, "presolve_licq_check"),
296 (str, presolve_licq_action, "presolve_licq_action"),
297 (bool, presolve_warm_z_bounds, "presolve_warm_z_bounds"),
298 (num, presolve_bound_mult_init_val, "presolve_bound_mult_init_val"),
299 (int, presolve_max_passes, "presolve_max_passes"),
300 (int, presolve_print_level, "presolve_print_level"),
301 (bool, presolve_fbbt, "presolve_fbbt"),
303 (num, fbbt_tol, "fbbt_tol"),
304 (int, fbbt_max_iter, "fbbt_max_iter"),
305 (int, fbbt_max_constraints, "fbbt_max_constraints"),
306 (bool, presolve_auxiliary, "presolve_auxiliary"),
308 (str, presolve_auxiliary_coupling, "presolve_auxiliary_coupling"),
309 (num, presolve_auxiliary_tol, "presolve_auxiliary_tol"),
310 (int, presolve_auxiliary_max_block_dim, "presolve_auxiliary_max_block_dim"),
311 (num, presolve_auxiliary_wall_time_fraction, "presolve_auxiliary_wall_time_fraction"),
312 (bool, presolve_auxiliary_diagnostics, "presolve_auxiliary_diagnostics"),
313 (str, linear_solver, "linear_solver"),
315 (str, feral_ordering, "feral_ordering"),
316 (str, feral_scaling, "feral_scaling"),
317 (num, feral_pivtol, "feral_pivtol"),
318 (bool, feral_refine, "feral_refine"),
319 (bool, feral_cascade_break, "feral_cascade_break"),
320 (bool, feral_fma, "feral_fma"),
321 (num, feral_singular_pivot_floor, "feral_singular_pivot_floor"),
322 (num, acceptable_progress_kappa, "acceptable_progress_kappa"),
324 (num, dual_inf_scale_kappa, "dual_inf_scale_kappa"),
325 (num, feral_inertia_pivot_floor, "feral_inertia_pivot_floor"),
326 (bool, infeasibility_mu_strategy_retry, "infeasibility_mu_strategy_retry"),
327 (num, primal_noise_floor_kappa, "primal_noise_floor_kappa"),
328 (num, qp_tau_max, "qp_tau_max"),
329 (int, resto_decline_deferrals, "resto_decline_deferrals"),
330 (num, resto_decline_progress_ratio, "resto_decline_progress_ratio"),
331 (int, sqp_qp_max_schur_updates_before_refactor, "sqp_qp_max_schur_updates_before_refactor"),
332 (bool, sqp_qp_use_homotopy, "sqp_qp_use_homotopy"),
333 (bool, sqp_qp_use_schur_updates, "sqp_qp_use_schur_updates"),
334 (num, theta_max_adaptive_factor, "theta_max_adaptive_factor"),
335 (int, theta_max_adaptive_max_raises, "theta_max_adaptive_max_raises"),
336 (int, theta_max_adaptive_trigger, "theta_max_adaptive_trigger"),
337 (num, theta_max_row_scale_kappa, "theta_max_row_scale_kappa"),
338 (bool, qp_presolve, "qp_presolve"),
340 (num, qp_tau, "qp_tau"),
341 (num, qp_reg, "qp_reg"),
342 (num, qp_infeas_tol, "qp_infeas_tol"),
343 (bool, qp_hsde, "qp_hsde"),
344 (bool, qp_equilibrate, "qp_equilibrate"),
345 (bool, qp_crossover, "qp_crossover"),
346 (int, qp_gondzio_corr, "qp_gondzio_corr"),
347 (bool, feral_infeasibility_scaling_retry, "feral_infeasibility_scaling_retry"),
348 (int, sqp_qp_max_iter, "sqp_qp_max_iter"),
350 (num, sqp_qp_feas_tol, "sqp_qp_feas_tol"),
351 (num, sqp_qp_opt_tol, "sqp_qp_opt_tol"),
352 (num, sqp_qp_elastic_gamma, "sqp_qp_elastic_gamma"),
353 (str, sqp_qp_anti_cycling, "sqp_qp_anti_cycling"),
354 (bool, sqp_qp_certify_second_order, "sqp_qp_certify_second_order"),
355 (num, bound_relax_factor, "bound_relax_factor"),
356 (num, constr_viol_tol, "constr_viol_tol"),
357 );
358
359 #[must_use]
360 pub fn tol(mut self, tol: f64) -> Self {
361 self.tol = Some(tol);
362 self
363 }
364
365 #[must_use]
366 pub fn max_iter(mut self, n: u32) -> Self {
367 self.max_iter = Some(n);
368 self
369 }
370
371 #[must_use]
372 pub fn print_level(mut self, level: u32) -> Self {
373 self.print_level = Some(level);
374 self
375 }
376
377 #[must_use]
378 pub fn mu_strategy(mut self, s: MuStrategy) -> Self {
379 self.mu_strategy = Some(s);
380 self
381 }
382
383 #[must_use]
385 pub fn algorithm(mut self, algorithm: PounceAlgorithm) -> Self {
386 self.algorithm = Some(algorithm);
387 self
388 }
389
390 #[must_use]
392 pub fn solver_selection(mut self, selection: PounceSolverSelection) -> Self {
393 self.solver_selection = Some(selection);
394 self
395 }
396
397 #[must_use]
401 pub fn set(mut self, name: impl Into<String>, value: impl Into<PounceOptionValue>) -> Self {
402 self.extra.push((name.into(), value.into()));
403 self
404 }
405
406 pub(crate) fn num_opts(&self) -> &[(&'static str, f64)] {
407 &self.num_opts
408 }
409
410 pub(crate) fn int_opts(&self) -> &[(&'static str, i32)] {
411 &self.int_opts
412 }
413
414 pub(crate) fn str_opts(&self) -> &[(&'static str, String)] {
415 &self.str_opts
416 }
417
418 pub(crate) fn bool_opts(&self) -> &[(&'static str, bool)] {
419 &self.bool_opts
420 }
421
422 fn effective_value<T>(
423 &self,
424 name: &str,
425 typed: impl Iterator<Item = (&'static str, T)>,
426 from_raw: impl Fn(&PounceOptionValue) -> Option<T>,
427 ) -> Option<T> {
428 let mut value = typed.filter(|(key, _)| *key == name).map(|(_, value)| value).last();
429 for (key, raw) in &self.extra {
430 if key == name {
431 value = from_raw(raw);
432 }
433 }
434 value
435 }
436
437 pub(crate) fn effective_num(&self, name: &str) -> Option<f64> {
438 self.effective_value(name, self.num_opts.iter().map(|&(key, value)| (key, value)), |raw| {
439 match raw {
440 PounceOptionValue::Num(value) => Some(*value),
441 _ => None,
442 }
443 })
444 }
445
446 pub(crate) fn effective_int(&self, name: &str) -> Option<i32> {
447 self.effective_value(name, self.int_opts.iter().map(|&(key, value)| (key, value)), |raw| {
448 match raw {
449 PounceOptionValue::Int(value) => Some(*value),
450 _ => None,
451 }
452 })
453 }
454
455 pub(crate) fn effective_bool(&self, name: &str) -> Option<bool> {
456 self.effective_value(name, self.bool_opts.iter().map(|&(key, value)| (key, value)), |raw| {
457 match raw {
458 PounceOptionValue::Bool(value) => Some(*value),
459 PounceOptionValue::Str(value)
460 if matches!(value.as_str(), "yes" | "true" | "on") =>
461 {
462 Some(true)
463 }
464 PounceOptionValue::Str(value)
465 if matches!(value.as_str(), "no" | "false" | "off") =>
466 {
467 Some(false)
468 }
469 _ => None,
470 }
471 })
472 }
473
474 pub(crate) fn effective_string(&self, name: &str) -> Option<String> {
475 self.effective_value(
476 name,
477 self.str_opts.iter().map(|(key, value)| (*key, value.clone())),
478 |raw| match raw {
479 PounceOptionValue::Str(value) => Some(value.clone()),
480 _ => None,
481 },
482 )
483 }
484}
485
486impl HasUniversal for PounceOptions {
487 fn universal(&self) -> &UniversalOptions {
488 &self.universal
489 }
490
491 fn universal_mut(&mut self) -> &mut UniversalOptions {
492 &mut self.universal
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn typed_setters_push_onto_the_right_vecs() {
502 let o = PounceOptions::default()
503 .mu_oracle("probing")
504 .mu_init(0.05)
505 .presolve(true)
506 .presolve_max_passes(5)
507 .feral_refine(false);
508 assert_eq!(o.str_opts, vec![("mu_oracle", "probing".to_owned())]);
509 assert_eq!(o.num_opts, vec![("mu_init", 0.05)]);
510 assert_eq!(o.int_opts, vec![("presolve_max_passes", 5)]);
511 assert_eq!(o.bool_opts, vec![("presolve", true), ("feral_refine", false)]);
512 }
513
514 #[test]
515 fn default_vecs_are_empty() {
516 let o = PounceOptions::default();
517 assert!(o.num_opts.is_empty());
518 assert!(o.int_opts.is_empty());
519 assert!(o.str_opts.is_empty());
520 assert!(o.bool_opts.is_empty());
521 assert!(o.extra.is_empty());
522 }
523
524 #[test]
525 fn same_option_twice_keeps_both_entries() {
526 let o = PounceOptions::default().mu_init(0.1).mu_init(0.5);
527 assert_eq!(o.num_opts, vec![("mu_init", 0.1), ("mu_init", 0.5)]);
528 }
529
530 #[test]
531 fn clone_preserves_all_vecs() {
532 let o = PounceOptions::default().mu_init(0.1).presolve_max_passes(2).presolve(true);
533 let c = o.clone();
534 assert_eq!(o.num_opts, c.num_opts);
535 assert_eq!(o.int_opts, c.int_opts);
536 assert_eq!(o.bool_opts, c.bool_opts);
537 }
538
539 #[test]
540 fn set_pushes_onto_extra_with_bool() {
541 let o = PounceOptions::default().set("presolve", true).set("acceptable_tol", 1e-5);
542 assert_eq!(
543 o.extra,
544 vec![
545 ("presolve".to_owned(), PounceOptionValue::Bool(true)),
546 ("acceptable_tol".to_owned(), PounceOptionValue::Num(1e-5)),
547 ]
548 );
549 }
550
551 #[test]
552 fn pounce_setters_use_the_declared_storage_kinds() {
553 let o = PounceOptions::default()
554 .solver_selection(PounceSolverSelection::Socp)
555 .acceptable_progress_kappa(0.2)
556 .resto_decline_deferrals(2)
557 .infeasibility_mu_strategy_retry(false)
558 .qp_tau(0.9)
559 .qp_presolve(true)
560 .sqp_qp_anti_cycling("bland");
561 assert_eq!(o.solver_selection, Some(PounceSolverSelection::Socp));
562 assert!(o.num_opts.contains(&("acceptable_progress_kappa", 0.2)));
563 assert!(o.num_opts.contains(&("qp_tau", 0.9)));
564 assert!(o.int_opts.contains(&("resto_decline_deferrals", 2)));
565 assert!(o.bool_opts.contains(&("infeasibility_mu_strategy_retry", false)));
566 assert!(o.bool_opts.contains(&("qp_presolve", true)));
567 assert!(o.str_opts.contains(&("sqp_qp_anti_cycling", "bland".to_owned())));
568 }
569
570 #[test]
571 fn solver_selection_parses_every_public_value() {
572 for (text, expected) in [
573 ("auto", PounceSolverSelection::Auto),
574 ("nlp", PounceSolverSelection::Nlp),
575 ("lp-ipm", PounceSolverSelection::LpIpm),
576 ("qp-ipm", PounceSolverSelection::QpIpm),
577 ("qp-active-set", PounceSolverSelection::QpActiveSet),
578 ("socp", PounceSolverSelection::Socp),
579 ] {
580 assert_eq!(PounceSolverSelection::parse(text), Some(expected));
581 assert_eq!(expected.as_str(), text);
582 }
583 assert_eq!(PounceSolverSelection::parse("unknown"), None);
584 }
585
586 #[test]
587 fn algorithms_report_their_names_and_supported_model_kinds() {
588 for (algorithm, name) in [
589 (PounceAlgorithm::InteriorPoint, "interior-point"),
590 (PounceAlgorithm::ActiveSetSqp, "active-set-sqp"),
591 ] {
592 assert_eq!(algorithm.as_str(), name);
593 for kind in
594 [ModelKind::LP, ModelKind::QP, ModelKind::QCP, ModelKind::SOCP, ModelKind::NLP]
595 {
596 assert!(algorithm.supports(kind), "{algorithm:?} should support {kind:?}");
597 }
598 assert!(!algorithm.supports(ModelKind::MILP));
599 }
600 }
601
602 #[test]
603 fn wrong_kind_raw_overrides_clear_typed_values() {
604 let options = PounceOptions::default()
605 .qp_tau(0.9)
606 .set("qp_tau", true)
607 .sqp_qp_max_iter(20)
608 .set("sqp_qp_max_iter", "wrong")
609 .qp_presolve(true)
610 .set("qp_presolve", 1.0)
611 .sqp_qp_anti_cycling("bland")
612 .set("sqp_qp_anti_cycling", false);
613 assert_eq!(options.effective_num("qp_tau"), None);
614 assert_eq!(options.effective_int("sqp_qp_max_iter"), None);
615 assert_eq!(options.effective_bool("qp_presolve"), None);
616 assert_eq!(options.effective_string("sqp_qp_anti_cycling"), None);
617 }
618}