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_cpu_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)]
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 top-level solve algorithm. Defaults to the interior-point
30    /// method when omitted.
31    pub algorithm: Option<PounceAlgorithm>,
32    /// Macro-generated typed options, kept by value kind and applied in order.
33    num_opts: Vec<(&'static str, f64)>,
34    int_opts: Vec<(&'static str, i32)>,
35    str_opts: Vec<(&'static str, String)>,
36    bool_opts: Vec<(&'static str, bool)>,
37    /// Escape hatch: raw POUNCE options applied last.
38    pub extra: Vec<(String, PounceOptionValue)>,
39}
40
41/// `mu_strategy` values.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum MuStrategy {
44    Monotone,
45    Adaptive,
46}
47
48/// POUNCE algorithms available through its Rust library API.
49///
50/// Both algorithms accept every continuous [`ModelKind`] supported by this
51/// backend. `ActiveSetSqp` is a general NLP algorithm despite its QP
52/// subproblems, so oximo intentionally permits it for LP, QP, QCP, and NLP
53/// models. POUNCE's `lp-ipm`, `qp-ipm`, and `socp` selectors are CLI-only and
54/// are not exposed here.
55#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
56pub enum PounceAlgorithm {
57    /// POUNCE's IPOPT-lineage primal-dual interior-point method.
58    #[default]
59    InteriorPoint,
60    /// Active-set sequential quadratic programming.
61    ActiveSetSqp,
62}
63
64impl PounceAlgorithm {
65    pub(crate) const fn as_str(self) -> &'static str {
66        match self {
67            Self::InteriorPoint => "interior-point",
68            Self::ActiveSetSqp => "active-set-sqp",
69        }
70    }
71
72    pub(crate) const fn supports(self, kind: ModelKind) -> bool {
73        match self {
74            Self::InteriorPoint | Self::ActiveSetSqp => {
75                matches!(kind, ModelKind::LP | ModelKind::QP | ModelKind::QCP | ModelKind::NLP)
76            }
77        }
78    }
79}
80
81/// A raw POUNCE option value for [`PounceOptions::extra`].
82#[derive(Clone, Debug, PartialEq)]
83pub enum PounceOptionValue {
84    Num(f64),
85    Int(i32),
86    Str(String),
87    Bool(bool),
88}
89
90impl From<f64> for PounceOptionValue {
91    fn from(v: f64) -> Self {
92        Self::Num(v)
93    }
94}
95
96impl From<i32> for PounceOptionValue {
97    fn from(v: i32) -> Self {
98        Self::Int(v)
99    }
100}
101
102impl From<&str> for PounceOptionValue {
103    fn from(v: &str) -> Self {
104        Self::Str(v.to_owned())
105    }
106}
107
108impl From<bool> for PounceOptionValue {
109    fn from(v: bool) -> Self {
110        Self::Bool(v)
111    }
112}
113
114// Generates one typed builder method per POUNCE option, keyed by value kind.
115// The method name matches the option string.
116macro_rules! pounce_options {
117    ($( ($kind:ident, $method:ident, $tag:literal) ),* $(,)?) => {
118        $(pounce_options!(@impl $kind, $method, $tag);)*
119    };
120    (@impl num, $method:ident, $tag:literal) => {
121        #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
122        #[must_use]
123        pub fn $method(mut self, v: f64) -> Self {
124            self.num_opts.push(($tag, v));
125            self
126        }
127    };
128    (@impl int, $method:ident, $tag:literal) => {
129        #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
130        #[must_use]
131        pub fn $method(mut self, v: i32) -> Self {
132            self.int_opts.push(($tag, v));
133            self
134        }
135    };
136    (@impl str, $method:ident, $tag:literal) => {
137        #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
138        #[must_use]
139        pub fn $method(mut self, v: impl Into<String>) -> Self {
140            self.str_opts.push(($tag, v.into()));
141            self
142        }
143    };
144    (@impl bool, $method:ident, $tag:literal) => {
145        #[doc = concat!("Set the POUNCE `", $tag, "` option.")]
146        #[must_use]
147        pub fn $method(mut self, v: bool) -> Self {
148            self.bool_opts.push(($tag, v));
149            self
150        }
151    };
152}
153
154impl PounceOptions {
155    pounce_options!(
156        // Barrier-parameter (μ) strategy (`mu_strategy` has a dedicated setter)
157        (str, mu_oracle, "mu_oracle"),
158        (num, mu_init, "mu_init"),
159        (num, mu_min, "mu_min"),
160        (num, mu_max, "mu_max"),
161        (num, mu_max_fact, "mu_max_fact"),
162        (num, mu_target, "mu_target"),
163        (num, mu_linear_decrease_factor, "mu_linear_decrease_factor"),
164        (num, mu_superlinear_decrease_power, "mu_superlinear_decrease_power"),
165        (num, barrier_tol_factor, "barrier_tol_factor"),
166        (num, sigma_max, "sigma_max"),
167        (num, sigma_min, "sigma_min"),
168        (str, adaptive_mu_globalization, "adaptive_mu_globalization"),
169        // Quality-function oracle
170        (str, quality_function_norm_type, "quality_function_norm_type"),
171        (str, quality_function_centrality, "quality_function_centrality"),
172        (str, quality_function_balancing_term, "quality_function_balancing_term"),
173        (int, quality_function_max_section_steps, "quality_function_max_section_steps"),
174        (num, quality_function_section_sigma_tol, "quality_function_section_sigma_tol"),
175        (num, quality_function_section_qf_tol, "quality_function_section_qf_tol"),
176        // Adaptive-μ globalization
177        (num, adaptive_mu_safeguard_factor, "adaptive_mu_safeguard_factor"),
178        (num, adaptive_mu_monotone_init_factor, "adaptive_mu_monotone_init_factor"),
179        (bool, adaptive_mu_restore_previous_iterate, "adaptive_mu_restore_previous_iterate"),
180        (int, adaptive_mu_kkterror_red_iters, "adaptive_mu_kkterror_red_iters"),
181        (num, adaptive_mu_kkterror_red_fact, "adaptive_mu_kkterror_red_fact"),
182        (str, adaptive_mu_kkt_norm_type, "adaptive_mu_kkt_norm_type"),
183        // L1 penalty-barrier wrapper
184        (bool, l1_exact_penalty_barrier, "l1_exact_penalty_barrier"),
185        (bool, l1_fallback_on_restoration_failure, "l1_fallback_on_restoration_failure"),
186        (num, l1_penalty_init, "l1_penalty_init"),
187        (num, l1_penalty_max, "l1_penalty_max"),
188        (num, l1_penalty_increase_factor, "l1_penalty_increase_factor"),
189        (int, l1_penalty_max_outer_iter, "l1_penalty_max_outer_iter"),
190        (num, l1_slack_tol, "l1_slack_tol"),
191        (num, l1_steering_factor, "l1_steering_factor"),
192        // NLP presolve
193        (bool, presolve, "presolve"),
194        (bool, presolve_bound_tightening, "presolve_bound_tightening"),
195        (bool, presolve_redundant_constraint_removal, "presolve_redundant_constraint_removal"),
196        (bool, presolve_linear_eq_reduction, "presolve_linear_eq_reduction"),
197        (bool, presolve_licq_check, "presolve_licq_check"),
198        (str, presolve_licq_action, "presolve_licq_action"),
199        (bool, presolve_warm_z_bounds, "presolve_warm_z_bounds"),
200        (num, presolve_bound_mult_init_val, "presolve_bound_mult_init_val"),
201        (int, presolve_max_passes, "presolve_max_passes"),
202        (int, presolve_print_level, "presolve_print_level"),
203        // Feasibility-based bound tightening
204        (bool, presolve_fbbt, "presolve_fbbt"),
205        (num, fbbt_tol, "fbbt_tol"),
206        (int, fbbt_max_iter, "fbbt_max_iter"),
207        (int, fbbt_max_constraints, "fbbt_max_constraints"),
208        // Auxiliary-equality preprocessing
209        (bool, presolve_auxiliary, "presolve_auxiliary"),
210        (str, presolve_auxiliary_coupling, "presolve_auxiliary_coupling"),
211        (num, presolve_auxiliary_tol, "presolve_auxiliary_tol"),
212        (int, presolve_auxiliary_max_block_dim, "presolve_auxiliary_max_block_dim"),
213        (num, presolve_auxiliary_wall_time_fraction, "presolve_auxiliary_wall_time_fraction"),
214        (bool, presolve_auxiliary_diagnostics, "presolve_auxiliary_diagnostics"),
215        // FERAL backend (pure-Rust sparse symmetric linear solver).
216        // `feral_infeasibility_scaling_retry` is registered but never read by
217        // the library (the retry is implemented by the POUNCE CLI), so setting
218        // it would silently do nothing — it is deliberately not exposed.
219        (str, linear_solver, "linear_solver"),
220        (str, feral_ordering, "feral_ordering"),
221        (str, feral_scaling, "feral_scaling"),
222        (num, feral_pivtol, "feral_pivtol"),
223        (bool, feral_refine, "feral_refine"),
224        (bool, feral_cascade_break, "feral_cascade_break"),
225        (bool, feral_fma, "feral_fma"),
226        (num, feral_singular_pivot_floor, "feral_singular_pivot_floor"),
227    );
228
229    #[must_use]
230    pub fn tol(mut self, tol: f64) -> Self {
231        self.tol = Some(tol);
232        self
233    }
234
235    #[must_use]
236    pub fn max_iter(mut self, n: u32) -> Self {
237        self.max_iter = Some(n);
238        self
239    }
240
241    #[must_use]
242    pub fn print_level(mut self, level: u32) -> Self {
243        self.print_level = Some(level);
244        self
245    }
246
247    #[must_use]
248    pub fn mu_strategy(mut self, s: MuStrategy) -> Self {
249        self.mu_strategy = Some(s);
250        self
251    }
252
253    /// Select POUNCE's top-level algorithm.
254    #[must_use]
255    pub fn algorithm(mut self, algorithm: PounceAlgorithm) -> Self {
256        self.algorithm = Some(algorithm);
257        self
258    }
259
260    /// Set a raw POUNCE option by name (the escape hatch for anything not
261    /// covered by a typed setter). Applied last, so it overrides the typed
262    /// options. An unknown name or invalid value fails the solve.
263    #[must_use]
264    pub fn set(mut self, name: impl Into<String>, value: impl Into<PounceOptionValue>) -> Self {
265        self.extra.push((name.into(), value.into()));
266        self
267    }
268
269    pub(crate) fn num_opts(&self) -> &[(&'static str, f64)] {
270        &self.num_opts
271    }
272
273    pub(crate) fn int_opts(&self) -> &[(&'static str, i32)] {
274        &self.int_opts
275    }
276
277    pub(crate) fn str_opts(&self) -> &[(&'static str, String)] {
278        &self.str_opts
279    }
280
281    pub(crate) fn bool_opts(&self) -> &[(&'static str, bool)] {
282        &self.bool_opts
283    }
284}
285
286impl HasUniversal for PounceOptions {
287    fn universal(&self) -> &UniversalOptions {
288        &self.universal
289    }
290
291    fn universal_mut(&mut self) -> &mut UniversalOptions {
292        &mut self.universal
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn typed_setters_push_onto_the_right_vecs() {
302        let o = PounceOptions::default()
303            .mu_oracle("probing")
304            .mu_init(0.05)
305            .presolve(true)
306            .presolve_max_passes(5)
307            .feral_refine(false);
308        assert_eq!(o.str_opts, vec![("mu_oracle", "probing".to_owned())]);
309        assert_eq!(o.num_opts, vec![("mu_init", 0.05)]);
310        assert_eq!(o.int_opts, vec![("presolve_max_passes", 5)]);
311        assert_eq!(o.bool_opts, vec![("presolve", true), ("feral_refine", false)]);
312    }
313
314    #[test]
315    fn default_vecs_are_empty() {
316        let o = PounceOptions::default();
317        assert!(o.num_opts.is_empty());
318        assert!(o.int_opts.is_empty());
319        assert!(o.str_opts.is_empty());
320        assert!(o.bool_opts.is_empty());
321        assert!(o.extra.is_empty());
322    }
323
324    #[test]
325    fn same_option_twice_keeps_both_entries() {
326        let o = PounceOptions::default().mu_init(0.1).mu_init(0.5);
327        assert_eq!(o.num_opts, vec![("mu_init", 0.1), ("mu_init", 0.5)]);
328    }
329
330    #[test]
331    fn clone_preserves_all_vecs() {
332        let o = PounceOptions::default().mu_init(0.1).presolve_max_passes(2).presolve(true);
333        let c = o.clone();
334        assert_eq!(o.num_opts, c.num_opts);
335        assert_eq!(o.int_opts, c.int_opts);
336        assert_eq!(o.bool_opts, c.bool_opts);
337    }
338
339    #[test]
340    fn set_pushes_onto_extra_with_bool() {
341        let o = PounceOptions::default().set("presolve", true).set("acceptable_tol", 1e-5);
342        assert_eq!(
343            o.extra,
344            vec![
345                ("presolve".to_owned(), PounceOptionValue::Bool(true)),
346                ("acceptable_tol".to_owned(), PounceOptionValue::Num(1e-5)),
347            ]
348        );
349    }
350}