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