1use std::fmt::Write as FmtWrite;
2use std::path::PathBuf;
3
4use oximo_solver::{HasUniversal, UniversalOptions};
5
6#[derive(Clone, Debug, Default)]
24pub struct BaronOptions {
25 pub universal: UniversalOptions,
26 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
35const MANAGED_KEYS: &[&str] = &["resname", "timname", "results", "times"];
38
39macro_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 (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 (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 (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 (
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 (bool, do_local, "DoLocal", "Local search during upper bounding."),
178 (int, num_loc, "NumLoc", "Local searches in preprocessing; `-1`/`-2` for automatic."),
179 (
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 (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 (str, lic_name, "LicName", "Full path to the BARON license file."),
228 (
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 #[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 #[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 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
286fn 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
297pub fn write_options(bar: &mut String, o: &BaronOptions, res_name: &str, tim_name: &str) {
303 writeln!(bar, "OPTIONS{{").unwrap();
304
305 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 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 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 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 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 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\";"); 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}