Skip to main content

oximo_baron/
options.rs

1use std::fmt::Write as FmtWrite;
2use std::path::PathBuf;
3
4use oximo_core::ConstraintHandle;
5use oximo_solver::{HasUniversal, UniversalOptions};
6
7/// BARON-specific solver options.
8///
9/// Universal options (`time_limit`, `threads`, `verbose`) come from the embedded
10/// [`UniversalOptions`] via [`UniversalOptionsExt`](oximo_solver::UniversalOptionsExt)
11/// and map to the BARON `MaxTime`, `threads`, and `PrLevel` keywords respectively.
12///
13/// Every other knob is a BARON option written verbatim into the `OPTIONS{ ... }`
14/// block of the generated `.bar` file. Each builder method below corresponds to a
15/// documented BARON option. The method name is the snake_case form of the BARON
16/// keyword (e.g. `EpsR` -> `.eps_r(..)`). For any keyword not covered by a typed
17/// builder, use [`BaronOptions::raw`].
18///
19/// `ResName`, `TimName`, `results`, and `times` are managed by the backend (it
20/// needs the results and times files to parse the solution) and are not
21/// user-settable, entries for those keywords passed through [`raw`] are ignored.
22///
23/// [`raw`]: BaronOptions::raw
24#[derive(Clone, Debug, Default)]
25pub struct BaronOptions {
26    pub universal: UniversalOptions,
27    /// Override for the `baron` executable. When `None`, `"baron"` is looked up
28    /// from `PATH`.
29    pub baron_path: Option<PathBuf>,
30    int_opts: Vec<(&'static str, i64)>,
31    dbl_opts: Vec<(&'static str, f64)>,
32    str_opts: Vec<(&'static str, String)>,
33    raw: Vec<(String, String)>,
34    convex_equation_handles: Vec<ConstraintHandle>,
35}
36
37/// BARON keywords the backend writes itself; user attempts to set these via
38/// [`BaronOptions::raw`] are ignored so they cannot break solution parsing.
39const MANAGED_KEYS: &[&str] = &["resname", "timname", "results", "times"];
40
41// Generates one typed builder method per BARON option.
42macro_rules! baron_opts {
43    ($( ($kind:ident, $method:ident, $keyword:literal $(, $doc:literal)?) ),* $(,)?) => {
44        $(baron_opts!(@impl $kind, $method, $keyword $(, $doc)?);)*
45    };
46    (@impl int, $method:ident, $keyword:literal $(, $doc:literal)?) => {
47        $(#[doc = $doc])?
48        #[doc = concat!("\n\nBARON option `", $keyword, "`.")]
49        #[must_use]
50        pub fn $method(mut self, v: i64) -> Self {
51            self.int_opts.push(($keyword, v));
52            self
53        }
54    };
55    (@impl bool, $method:ident, $keyword:literal $(, $doc:literal)?) => {
56        $(#[doc = $doc])?
57        #[doc = concat!("\n\nBARON option `", $keyword, "` (written as `0`/`1`).")]
58        #[must_use]
59        pub fn $method(mut self, v: bool) -> Self {
60            self.int_opts.push(($keyword, i64::from(v)));
61            self
62        }
63    };
64    (@impl dbl, $method:ident, $keyword:literal $(, $doc:literal)?) => {
65        $(#[doc = $doc])?
66        #[doc = concat!("\n\nBARON option `", $keyword, "`.")]
67        #[must_use]
68        pub fn $method(mut self, v: f64) -> Self {
69            self.dbl_opts.push(($keyword, v));
70            self
71        }
72    };
73    (@impl str, $method:ident, $keyword:literal $(, $doc:literal)?) => {
74        $(#[doc = $doc])?
75        #[doc = concat!("\n\nBARON option `", $keyword, "` (written quoted).")]
76        #[must_use]
77        pub fn $method(mut self, v: impl Into<String>) -> Self {
78            self.str_opts.push(($keyword, v.into()));
79            self
80        }
81    };
82}
83
84impl BaronOptions {
85    baron_opts!(
86        // 7.1 Termination options
87        // (MaxTime and threads are set via the universal `time_limit`/`threads`
88        // options; use `.raw("MaxTime", "-1")` for the no-limit sentinel.)
89        (dbl, eps_a, "EpsA", "Absolute termination tolerance (>= 1e-12)."),
90        (dbl, eps_r, "EpsR", "Relative termination tolerance (nonnegative)."),
91        (
92            bool,
93            delta_term,
94            "DeltaTerm",
95            "Terminate on insufficient progress over `DeltaT` seconds."
96        ),
97        (dbl, delta_t, "DeltaT", "Progress window in seconds for `DeltaTerm`."),
98        (dbl, delta_a, "DeltaA", "Absolute improvement threshold for `DeltaTerm`."),
99        (dbl, delta_r, "DeltaR", "Relative improvement threshold for `DeltaTerm`."),
100        (dbl, cut_off, "CutOff", "Ignore solutions worse than this objective value."),
101        (dbl, target, "Target", "Terminate once a solution at least as good as this is found."),
102        (dbl, abs_con_feas_tol, "AbsConFeasTol", "Absolute constraint feasibility tolerance."),
103        (
104            dbl,
105            rel_con_feas_tol,
106            "RelConFeasTol",
107            "Relative constraint feasibility tolerance (0..=0.1)."
108        ),
109        (dbl, abs_int_feas_tol, "AbsIntFeasTol", "Absolute integer feasibility tolerance."),
110        (
111            dbl,
112            rel_int_feas_tol,
113            "RelIntFeasTol",
114            "Relative integer feasibility tolerance (0..=0.1)."
115        ),
116        (
117            dbl,
118            primal_cs_tol,
119            "PrimalCSTol",
120            "Absolute tolerance for primal complementary slackness."
121        ),
122        (dbl, dual_cs_tol, "DualCSTol", "Absolute tolerance for dual complementary slackness."),
123        (dbl, dual_feas_tol, "DualFeasTol", "Absolute tolerance for dual feasibility."),
124        (dbl, ec_tol, "ECTol", "Absolute tolerance for equilibrium-condition complementarity."),
125        (dbl, box_tol, "BoxTol", "Boxes smaller than this are eliminated (>= 1e-12)."),
126        (bool, first_feas, "FirstFeas", "Terminate once `NumSol` feasible solutions are found."),
127        (bool, first_loc, "FirstLoc", "Terminate once a local optimum is found."),
128        (int, max_iter, "MaxIter", "Branch-and-reduce iteration limit; `-1` for unlimited."),
129        (bool, want_dual, "WantDual", "Return a dual solution for the best primal point."),
130        (dbl, dual_budget, "DualBudget", "Extra time (s) to compute a dual solution on timeout."),
131        (int, num_sol, "NumSol", "Number of feasible solutions to find; `-1` for all."),
132        (
133            dbl,
134            isol_tol,
135            "IsolTol",
136            "Separation distance between distinct solutions (with `NumSol`)."
137        ),
138        // 7.2 Relaxation options
139        (int, n_outer1, "NOuter1", "Number of outer approximators of convex univariate functions."),
140        (
141            int,
142            n_out_per_var,
143            "NOutPerVar",
144            "Outer approximators per variable for convex functions."
145        ),
146        (int, n_out_iter, "NOutIter", "Rounds of cutting-plane generation at node relaxation."),
147        (
148            int,
149            out_grid,
150            "OutGrid",
151            "Grid points per variable for convex multivariate approximators."
152        ),
153        // 7.3 Range reduction options
154        (bool, tdo, "TDo", "Nonlinear-feasibility-based range reduction (poor man's NLPs)."),
155        (bool, mdo, "MDo", "Marginals-based range reduction."),
156        (bool, lbttdo, "LBTTDo", "Linear-feasibility-based range reduction (poor man's LPs)."),
157        (bool, obttdo, "OBTTDo", "Optimality-based bound tightening."),
158        (int, pdo, "PDo", "Probing: `-2` auto, `-1` all variables, `0` none, `n` on n variables."),
159        // 7.4 Tree management options
160        (
161            int,
162            br_var_stra,
163            "BrVarStra",
164            "Branching variable strategy (0 dynamic, 1 violation, 2 edge)."
165        ),
166        (
167            int,
168            br_pt_stra,
169            "BrPtStra",
170            "Branching point strategy (0 dynamic, 1 omega, 2 bisection, 3 mix)."
171        ),
172        (
173            int,
174            node_sel,
175            "NodeSel",
176            "Node selection rule (0 dynamic, 1 best-bound, 2 LIFO, 3 min-infeas)."
177        ),
178        // 7.5 Local search options
179        (bool, do_local, "DoLocal", "Local search during upper bounding."),
180        (int, num_loc, "NumLoc", "Local searches in preprocessing; `-1`/`-2` for automatic."),
181        // 7.6 Output and file name options
182        (
183            int,
184            pr_level,
185            "PrLevel",
186            "Print level (`0` silent). Overrides the `verbose` option when set."
187        ),
188        (int, pr_freq, "PrFreq", "Log output frequency in nodes."),
189        (dbl, pr_time_freq, "PrTimeFreq", "Log output frequency in seconds."),
190        (bool, loc_res, "LocRes", "Write detailed local-search results to the results file."),
191        (str, pro_name, "ProName", "Problem name (<= 10 characters)."),
192        // 7.7 Subsolver options
193        (int, lp_sol, "LPSol", "LP/MIP subsolver (`-1` auto, 3 CPLEX, 8 CLP/CBC, 15 HSL LA04)."),
194        (
195            bool,
196            allow_cplex,
197            "AllowCPLEX",
198            "Permit CPLEX as an LP/MIP subsolver under auto selection."
199        ),
200        (bool, allow_cbc, "AllowCBC", "Permit CBC as an LP/MIP subsolver under auto selection."),
201        (
202            bool,
203            allow_hsl,
204            "AllowHSL",
205            "Permit HSL's LA04 as an LP/MIP subsolver under auto selection."
206        ),
207        (str, cplex_lib_name, "CplexLibName", "Full path to the CPLEX callable libraries."),
208        (int, lp_alg, "LPAlg", "LP algorithm (0 auto, 1 primal, 2 dual, 3 barrier)."),
209        (
210            int,
211            nlp_sol,
212            "NLPSol",
213            "NLP subsolver (`-1` auto, 0 none, 9 IPOPT, 10 FilterSD, 14 FilterSQP)."
214        ),
215        (
216            bool,
217            allow_filter_sd,
218            "AllowFilterSD",
219            "Permit FilterSD as an NLP subsolver under auto selection."
220        ),
221        (
222            bool,
223            allow_filter_sqp,
224            "AllowFilterSQP",
225            "Permit FilterSQP as an NLP subsolver under auto selection."
226        ),
227        (bool, allow_ipopt, "AllowIpopt", "Permit IPOPT as an NLP subsolver under auto selection."),
228        // 7.8 Licensing options
229        (str, lic_name, "LicName", "Full path to the BARON license file."),
230        // 7.9 Other options
231        (
232            int,
233            comp_iis,
234            "CompIIS",
235            "Search for an IIS on infeasible models (0 off .. 5 algorithms)."
236        ),
237        (bool, iis_int, "IISint", "Consider general integers (not binaries) as part of an IIS."),
238        (
239            int,
240            iis_order,
241            "IISorder",
242            "Constraint ordering for the IIS search (`-1` auto, 1..=3, >=4 seed)."
243        ),
244        (bool, problem_is_convex, "ProblemIsConvex", "Assert the continuous relaxation is convex."),
245        (int, seed, "seed", "Initial seed for BARON's random number generator (positive)."),
246    );
247
248    /// Override for the `baron` executable path. When unset, `"baron"` is looked
249    /// up from `PATH`.
250    #[must_use]
251    pub fn baron_path(mut self, p: impl Into<PathBuf>) -> Self {
252        self.baron_path = Some(p.into());
253        self
254    }
255
256    /// Assert that one algebraic constraint defines a convex feasible set.
257    ///
258    /// BARON emits the corresponding row in `CONVEX_EQUATIONS`, allowing it to
259    /// generate supporting hyperplanes for the complete set instead of relaxing
260    /// the expression operation by operation. This is a correctness-sensitive
261    /// assertion: incorrectly marking a nonconvex constraint can make BARON
262    /// return an incorrect answer without warning.
263    ///
264    /// Two-sided range constraints are currently rejected because the BARON
265    /// adapter emits them as separate lower and upper equations. Use two explicit
266    /// single-sided constraints and mark only sides whose feasible sets are known
267    /// to be convex.
268    #[must_use]
269    pub fn convex_equation(self, constraint: ConstraintHandle) -> Self {
270        self.convex_equations([constraint])
271    }
272
273    /// Assert that each algebraic constraint defines a convex feasible set.
274    ///
275    /// Duplicate IDs are ignored while preserving their first occurrence.
276    /// See [`Self::convex_equation`] for correctness and range-constraint
277    /// requirements.
278    #[must_use]
279    pub fn convex_equations<I>(mut self, constraints: I) -> Self
280    where
281        I: IntoIterator<Item = ConstraintHandle>,
282    {
283        for constraint in constraints {
284            if !self.convex_equation_handles.contains(&constraint) {
285                self.convex_equation_handles.push(constraint);
286            }
287        }
288        self
289    }
290
291    /// Set an arbitrary BARON option by keyword, written verbatim as
292    /// `keyword: value;` in the `OPTIONS{ ... }` block. Use this for any option
293    /// without a dedicated builder. The `value` is emitted as-is, so use quote
294    /// strings (e.g. `("LpSol", "8")`, `("NLPSol", "6")`).
295    ///
296    /// Keywords managed by the backend (`ResName`, `TimName`, `results`,
297    /// `times`) are ignored.
298    #[must_use]
299    pub fn raw(mut self, keyword: impl Into<String>, value: impl Into<String>) -> Self {
300        self.raw.push((keyword.into(), value.into()));
301        self
302    }
303
304    /// Whether `CompIIS` has been set (to any value) via [`comp_iis`](Self::comp_iis)
305    /// or [`raw`](Self::raw). The IIS backend uses this to avoid overriding a
306    /// user-chosen algorithm when it enables IIS search.
307    pub(crate) fn has_comp_iis(&self) -> bool {
308        self.int_opts.iter().any(|(k, _)| *k == "CompIIS")
309            || self.raw.iter().any(|(k, _)| k.eq_ignore_ascii_case("CompIIS"))
310    }
311
312    pub(crate) fn convex_equation_handles(&self) -> &[ConstraintHandle] {
313        &self.convex_equation_handles
314    }
315}
316
317impl HasUniversal for BaronOptions {
318    fn universal(&self) -> &UniversalOptions {
319        &self.universal
320    }
321
322    fn universal_mut(&mut self) -> &mut UniversalOptions {
323        &mut self.universal
324    }
325}
326
327/// Format an `f64` for use inside a BARON `OPTIONS{}` value.
328fn fmt(v: f64) -> String {
329    if v == f64::INFINITY {
330        return "1e51".into();
331    }
332    if v == f64::NEG_INFINITY {
333        return "-1e51".into();
334    }
335    format!("{v}")
336}
337
338/// Emit the `OPTIONS{ ... }` block into `bar`.
339///
340/// `res_name` / `tim_name` are the (relative) result and times file names the
341/// backend will read back, they are written into the block so BARON produces
342/// them in the working directory.
343pub fn write_options(bar: &mut String, o: &BaronOptions, res_name: &str, tim_name: &str) {
344    writeln!(bar, "OPTIONS{{").unwrap();
345
346    // Backend-managed: we need the results and times files to parse the result.
347    // (`summary` is left at BARON's default so `CompIIS` can write its IIS.)
348    writeln!(bar, "results: 1;").unwrap();
349    writeln!(bar, "times: 1;").unwrap();
350    writeln!(bar, "ResName: \"{res_name}\";").unwrap();
351    writeln!(bar, "TimName: \"{tim_name}\";").unwrap();
352
353    // Universal options.
354    // Emit `-1` (no limit) when the user sets no `time_limit`,
355    // matching the other oximo backends.
356    match o.universal.time_limit {
357        Some(d) => writeln!(bar, "MaxTime: {};", d.as_secs_f64()).unwrap(),
358        None => writeln!(bar, "MaxTime: -1;").unwrap(),
359    }
360    if let Some(n) = o.universal.threads {
361        writeln!(bar, "threads: {n};").unwrap();
362    }
363    if let Some(v) = o.universal.verbose {
364        writeln!(bar, "PrLevel: {};", i64::from(v)).unwrap();
365    }
366
367    // Typed BARON options (a later `pr_level(..)` deliberately overrides the
368    // `verbose`-derived PrLevel above, since BARON honours the last setting).
369    for (k, v) in &o.int_opts {
370        writeln!(bar, "{k}: {v};").unwrap();
371    }
372    for (k, v) in &o.dbl_opts {
373        writeln!(bar, "{k}: {};", fmt(*v)).unwrap();
374    }
375    for (k, v) in &o.str_opts {
376        writeln!(bar, "{k}: \"{v}\";").unwrap();
377    }
378
379    // Raw passthrough, skipping anything the backend manages.
380    for (k, v) in &o.raw {
381        if MANAGED_KEYS.contains(&k.to_ascii_lowercase().as_str()) {
382            continue;
383        }
384        writeln!(bar, "{k}: {v};").unwrap();
385    }
386
387    writeln!(bar, "}}").unwrap();
388    writeln!(bar).unwrap();
389}
390
391#[cfg(test)]
392mod tests {
393    use std::time::Duration;
394
395    use oximo_core::prelude::*;
396    use oximo_solver::UniversalOptionsExt;
397
398    use super::*;
399
400    fn render(o: &BaronOptions) -> String {
401        let mut bar = String::new();
402        write_options(&mut bar, o, "res.lst", "tim.lst");
403        bar
404    }
405
406    #[test]
407    fn builder_sets_universal_and_baron_path() {
408        let o = BaronOptions::default()
409            .time_limit(Duration::from_secs(45))
410            .threads(3)
411            .verbose(true)
412            .eps_r(1e-4)
413            .baron_path("/opt/baron/baron");
414        assert_eq!(o.universal.time_limit, Some(Duration::from_secs(45)));
415        assert_eq!(o.universal.threads, Some(3));
416        assert_eq!(o.universal.verbose, Some(true));
417        assert_eq!(o.dbl_opts, vec![("EpsR", 1e-4)]);
418        assert_eq!(o.baron_path.as_deref(), Some(std::path::Path::new("/opt/baron/baron")));
419    }
420
421    #[test]
422    fn convex_equation_builders_preserve_order_and_deduplicate() {
423        let model = Model::new("convex_options");
424        variable!(model, x);
425        let c0 = constraint!(model, c0, x <= 0.0);
426        let c1 = constraint!(model, c1, x <= 1.0);
427        let c2 = constraint!(model, c2, x <= 2.0);
428        let options = BaronOptions::default().convex_equation(c2).convex_equations([c0, c2, c1]);
429
430        assert_eq!(options.convex_equation_handles(), &[c2, c0, c1]);
431    }
432
433    #[test]
434    fn default_emits_only_managed_block() {
435        let bar = render(&BaronOptions::default());
436        assert!(bar.contains("results: 1;"));
437        assert!(bar.contains("times: 1;"));
438        assert!(bar.contains("ResName: \"res.lst\";"));
439        assert!(bar.contains("TimName: \"tim.lst\";"));
440        assert!(bar.trim_start().starts_with("OPTIONS{"));
441        assert!(bar.contains('}'));
442    }
443
444    #[test]
445    fn write_options_emits_universal_and_typed() {
446        let o = BaronOptions::default()
447            .time_limit(Duration::from_secs(10))
448            .threads(4)
449            .eps_r(0.01)
450            .eps_a(1e-6)
451            .max_iter(1000)
452            .first_feas(true);
453        let bar = render(&o);
454        assert!(bar.contains("MaxTime: 10;"), "{bar}");
455        assert!(bar.contains("threads: 4;"), "{bar}");
456        assert!(bar.contains("EpsR: 0.01;"), "{bar}");
457        assert!(bar.contains("EpsA: 0.000001;"), "{bar}");
458        assert!(bar.contains("MaxIter: 1000;"), "{bar}");
459        assert!(bar.contains("FirstFeas: 1;"), "{bar}");
460    }
461
462    #[test]
463    fn verbose_false_emits_prlevel_zero() {
464        let bar = render(&BaronOptions::default().verbose(false));
465        assert!(bar.contains("PrLevel: 0;"), "{bar}");
466    }
467
468    #[test]
469    fn solution_pool_does_not_override_user_verbosity() {
470        let bar = render(&BaronOptions::default().num_sol(5).verbose(false));
471        assert!(bar.contains("PrLevel: 0;"), "{bar}");
472        assert!(!bar.contains("PrLevel: 1;"), "{bar}");
473    }
474
475    #[test]
476    fn newly_added_options_emit_correctly() {
477        let o = BaronOptions::default()
478            .target(1.5)
479            .comp_iis(4)
480            .allow_ipopt(true)
481            .allow_filter_sqp(false)
482            .problem_is_convex(true)
483            .seed(19_631_963)
484            .pro_name("robot")
485            .lic_name("/opt/baron/baronlice.txt");
486        let bar = render(&o);
487        assert!(bar.contains("Target: 1.5;"), "{bar}");
488        assert!(bar.contains("CompIIS: 4;"), "{bar}");
489        assert!(bar.contains("AllowIpopt: 1;"), "{bar}");
490        assert!(bar.contains("AllowFilterSQP: 0;"), "{bar}");
491        assert!(bar.contains("ProblemIsConvex: 1;"), "{bar}");
492        assert!(bar.contains("seed: 19631963;"), "{bar}");
493        assert!(bar.contains("ProName: \"robot\";"), "{bar}");
494        assert!(bar.contains("LicName: \"/opt/baron/baronlice.txt\";"), "{bar}");
495    }
496
497    #[test]
498    fn default_disables_baron_time_cap() {
499        // No time_limit => MaxTime: -1 so BARON does not silently stop.
500        assert!(render(&BaronOptions::default()).contains("MaxTime: -1;"));
501        let bar = render(&BaronOptions::default().time_limit(Duration::from_secs(60)));
502        assert!(bar.contains("MaxTime: 60;"), "{bar}");
503    }
504
505    #[test]
506    fn summary_not_forced() {
507        // CompIIS writes its IIS to the summary file, so we must not suppress it.
508        assert!(!render(&BaronOptions::default()).contains("summary"));
509    }
510
511    #[test]
512    fn raw_passthrough_and_managed_filtered() {
513        let o = BaronOptions::default().raw("LBTTDo", "1").raw("ResName", "\"hack.lst\";"); // managed: must be ignored
514        let bar = render(&o);
515        assert!(bar.contains("LBTTDo: 1;"), "{bar}");
516        assert!(!bar.contains("hack.lst"), "managed key must be filtered:\n{bar}");
517    }
518}