1use std::fmt::Write as FmtWrite;
2use std::path::PathBuf;
3
4use oximo_core::ConstraintHandle;
5use oximo_solver::{HasUniversal, UniversalOptions};
6
7#[derive(Clone, Debug, Default)]
25pub struct BaronOptions {
26 pub universal: UniversalOptions,
27 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
37const MANAGED_KEYS: &[&str] = &["resname", "timname", "results", "times"];
40
41macro_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 (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 (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 (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 (
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 (bool, do_local, "DoLocal", "Local search during upper bounding."),
180 (int, num_loc, "NumLoc", "Local searches in preprocessing; `-1`/`-2` for automatic."),
181 (
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 (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 (str, lic_name, "LicName", "Full path to the BARON license file."),
230 (
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 #[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 #[must_use]
269 pub fn convex_equation(self, constraint: ConstraintHandle) -> Self {
270 self.convex_equations([constraint])
271 }
272
273 #[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 #[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 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
327fn 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
338pub fn write_options(bar: &mut String, o: &BaronOptions, res_name: &str, tim_name: &str) {
344 writeln!(bar, "OPTIONS{{").unwrap();
345
346 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 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 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 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 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 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\";"); 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}