Skip to main content

oximo_baron/
options.rs

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