1use std::fmt::Write as FmtWrite;
2use std::path::PathBuf;
3
4use oximo_solver::{HasUniversal, UniversalOptions};
5
6#[derive(Clone, Debug, Default)]
27pub struct BaronOptions {
28 pub universal: UniversalOptions,
29 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
38const MANAGED_KEYS: &[&str] = &["resname", "timname", "results", "times"];
41
42macro_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 (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 (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 (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 (
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 (bool, do_local, "DoLocal", "Local search during upper bounding."),
181 (int, num_loc, "NumLoc", "Local searches in preprocessing; `-1`/`-2` for automatic."),
182 (
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 (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 (str, lic_name, "LicName", "Full path to the BARON license file."),
231 (
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 #[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 #[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
281fn 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
292pub fn write_options(bar: &mut String, o: &BaronOptions, res_name: &str, tim_name: &str) {
298 writeln!(bar, "OPTIONS{{").unwrap();
299
300 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 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 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 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 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 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\";"); 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}