Skip to main content

oximo_pounce/
options.rs

1use oximo_core::ModelKind;
2use oximo_solver::{HasUniversal, UniversalOptions};
3
4/// POUNCE-specific solver options.
5///
6/// For more information about POUNCE's options, see the
7/// [documented option reference](https://kitchingroup.cheme.cmu.edu/pounce/options.html).
8///
9/// Invalid option names or out-of-range values are reported by POUNCE and
10/// surface as a [`SolverError::Backend`](oximo_solver::SolverError::Backend) at
11/// solve time.
12///
13/// `UniversalOptions` mapping:
14///     `time_limit` -> `max_wall_time`,
15///     `verbose` -> `print_level` 5 (else 0) and captures the iteration log
16///     into [`SolverResult::raw_log`](oximo_solver::SolverResult::raw_log),
17///     `threads` is ignored.
18#[derive(Clone, Debug, Default, PartialEq)]
19pub struct PounceOptions {
20    pub universal: UniversalOptions,
21    /// Desired convergence tolerance (`tol`).
22    pub tol: Option<f64>,
23    /// Iteration limit (`max_iter`).
24    pub max_iter: Option<u32>,
25    /// Output verbosity 0–12 (`print_level`); overrides `verbose`.
26    pub print_level: Option<u32>,
27    /// Barrier parameter update strategy (`mu_strategy`).
28    pub mu_strategy: Option<MuStrategy>,
29    /// POUNCE's general-NLP algorithm. Defaults to the interior-point method
30    /// whenever structural routing selects the NLP engine.
31    pub algorithm: Option<PounceAlgorithm>,
32    /// Structural solver route. [`PounceSolverSelection::Auto`] is used when
33    /// omitted and sends provably convex models to POUNCE's specialized
34    /// convex engines.
35    pub solver_selection: Option<PounceSolverSelection>,
36    /// Macro-generated typed options, kept by value kind and applied in order.
37    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    /// Escape hatch: raw POUNCE options applied last.
42    pub extra: Vec<(String, PounceOptionValue)>,
43}
44
45/// `mu_strategy` values.
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum MuStrategy {
48    Monotone,
49    Adaptive,
50}
51
52/// POUNCE algorithms available through its Rust library API.
53///
54/// Both algorithms accept every continuous [`ModelKind`] supported by this
55/// backend. `ActiveSetSqp` is a general NLP algorithm despite its QP
56/// subproblems, so oximo intentionally permits it for LP, QP, QCP, and NLP
57/// models. Specialized convex engines are selected separately with
58/// [`PounceSolverSelection`].
59#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
60pub enum PounceAlgorithm {
61    /// POUNCE's IPOPT-lineage primal-dual interior-point method.
62    #[default]
63    InteriorPoint,
64    /// Active-set sequential quadratic programming.
65    ActiveSetSqp,
66}
67
68/// POUNCE solver route selected after classifying the oximo model.
69#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub enum PounceSolverSelection {
71    /// Use a specialized convex engine when convexity is certified, otherwise
72    /// use the general NLP engine.
73    #[default]
74    Auto,
75    /// Always use the general NLP engine.
76    Nlp,
77    /// Force the convex LP interior-point route (LP models only).
78    LpIpm,
79    /// Force the convex QP interior-point route (LP or convex QP).
80    QpIpm,
81    /// Force POUNCE's direct parametric active-set QP engine.
82    QpActiveSet,
83    /// Force the conic interior-point route (convex LP/QP/SOCP).
84    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/// A raw POUNCE option value for [`PounceOptions::extra`].
137#[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
169// Generates one typed builder method per POUNCE option, keyed by value kind.
170// The method name matches the option string.
171macro_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        // Barrier-parameter (μ) strategy (`mu_strategy` has a dedicated setter)
212        (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        // Quality-function oracle
268        (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        // Adaptive-μ globalization
275        (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        // L1 penalty-barrier wrapper
282        (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        // NLP presolve
291        (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        // Feasibility-based bound tightening
302        (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        // Auxiliary-equality preprocessing
307        (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        // FERAL backend (pure-Rust sparse symmetric linear solver).
314        (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        // POUNCE convergence, restoration, scaling, and retry controls.
323        (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        // Specialized convex engine controls.
339        (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        // Active-set QP tuning shared by direct QP and NLP-SQP routes.
349        (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    /// Select POUNCE's top-level algorithm.
384    #[must_use]
385    pub fn algorithm(mut self, algorithm: PounceAlgorithm) -> Self {
386        self.algorithm = Some(algorithm);
387        self
388    }
389
390    /// Select POUNCE's structural solver route.
391    #[must_use]
392    pub fn solver_selection(mut self, selection: PounceSolverSelection) -> Self {
393        self.solver_selection = Some(selection);
394        self
395    }
396
397    /// Set a raw POUNCE option by name (the escape hatch for anything not
398    /// covered by a typed setter). Applied last, so it overrides the typed
399    /// options. An unknown name or invalid value fails the solve.
400    #[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}