1use highs::{HighsOptionValue, Model as HighsModel};
2use oximo_solver::{HasUniversal, SolverError, UniversalOptions};
3
4#[derive(Clone, Debug, Default)]
6pub struct HighsOptions {
7 pub universal: UniversalOptions,
8 pub mip_gap: Option<f64>,
9 pub presolve: Option<HighsPresolve>,
10 pub method: Option<HighsMethod>,
11 pub parallel: Option<bool>,
12}
13
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum HighsPresolve {
17 Off,
18 On,
19 Auto,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum HighsMethod {
25 Choose,
27 Simplex,
28 Ipm,
30 PdLp,
32}
33
34impl HighsOptions {
35 #[must_use]
36 pub fn mip_gap(mut self, gap: f64) -> Self {
37 self.mip_gap = Some(gap);
38 self
39 }
40
41 #[must_use]
42 pub fn presolve(mut self, p: HighsPresolve) -> Self {
43 self.presolve = Some(p);
44 self
45 }
46
47 #[must_use]
48 pub fn method(mut self, m: HighsMethod) -> Self {
49 self.method = Some(m);
50 self
51 }
52
53 #[must_use]
54 pub fn parallel(mut self, on: bool) -> Self {
55 self.parallel = Some(on);
56 self
57 }
58}
59
60impl HasUniversal for HighsOptions {
61 fn universal(&self) -> &UniversalOptions {
62 &self.universal
63 }
64
65 fn universal_mut(&mut self) -> &mut UniversalOptions {
66 &mut self.universal
67 }
68}
69
70fn set<V: HighsOptionValue>(
73 model: &mut HighsModel,
74 name: &str,
75 value: V,
76) -> Result<(), SolverError> {
77 model
78 .try_set_option(name, value)
79 .map_err(|e| SolverError::Backend(format!("HiGHS option {name:?}: {e}")))
80}
81
82pub(crate) fn apply(model: &mut HighsModel, o: &HighsOptions) -> Result<(), SolverError> {
88 if let Some(d) = o.universal.time_limit {
89 set(model, "time_limit", d.as_secs_f64())?;
90 }
91 if let Some(n) = o.universal.threads {
92 set(model, "threads", i32::try_from(n).unwrap_or(i32::MAX))?;
93 }
94 if let Some(b) = o.universal.verbose {
95 set(model, "output_flag", b)?;
96 set(model, "log_to_console", b)?;
97 }
98 if let Some(g) = o.mip_gap {
99 set(model, "mip_rel_gap", g)?;
100 }
101 if let Some(p) = o.presolve {
102 set(model, "presolve", presolve_str(p))?;
103 }
104 if let Some(m) = o.method {
105 set(model, "solver", method_str(m))?;
106 }
107 if let Some(p) = o.parallel {
108 set(model, "parallel", if p { "on" } else { "off" })?;
109 }
110 Ok(())
111}
112
113fn presolve_str(p: HighsPresolve) -> &'static str {
114 match p {
115 HighsPresolve::Off => "off",
116 HighsPresolve::On => "on",
117 HighsPresolve::Auto => "choose",
118 }
119}
120
121fn method_str(m: HighsMethod) -> &'static str {
122 match m {
123 HighsMethod::Choose => "choose",
124 HighsMethod::Simplex => "simplex",
125 HighsMethod::Ipm => "ipm",
126 HighsMethod::PdLp => "pdlp",
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use std::time::Duration;
133
134 use highs::{RowProblem, Sense as HighsSense};
135 use oximo_solver::UniversalOptionsExt;
136
137 use super::*;
138
139 fn empty_highs_model() -> HighsModel {
140 RowProblem::default().optimise(HighsSense::Minimise)
141 }
142
143 #[test]
144 fn builder_sets_all_fields() {
145 let o = HighsOptions::default()
146 .time_limit(Duration::from_secs(30))
147 .threads(8)
148 .verbose(true)
149 .mip_gap(0.01)
150 .presolve(HighsPresolve::Off)
151 .method(HighsMethod::Ipm)
152 .parallel(true);
153 assert_eq!(o.universal.time_limit, Some(Duration::from_secs(30)));
154 assert_eq!(o.universal.threads, Some(8));
155 assert_eq!(o.universal.verbose, Some(true));
156 assert_eq!(o.mip_gap, Some(0.01));
157 assert_eq!(o.presolve, Some(HighsPresolve::Off));
158 assert_eq!(o.method, Some(HighsMethod::Ipm));
159 assert_eq!(o.parallel, Some(true));
160 }
161
162 #[test]
163 fn apply_default_succeeds() {
164 let mut m = empty_highs_model();
165 apply(&mut m, &HighsOptions::default()).unwrap();
166 }
167
168 #[test]
169 fn apply_all_options_succeeds() {
170 let mut m = empty_highs_model();
171 let o = HighsOptions::default()
172 .time_limit(Duration::from_secs(10))
173 .threads(1)
174 .verbose(false)
175 .mip_gap(0.01)
176 .presolve(HighsPresolve::Off)
177 .method(HighsMethod::Simplex)
178 .parallel(false);
179 apply(&mut m, &o).unwrap();
180 }
181
182 #[test]
183 fn apply_every_method_variant() {
184 for method in
185 [HighsMethod::Choose, HighsMethod::Simplex, HighsMethod::Ipm, HighsMethod::PdLp]
186 {
187 let mut m = empty_highs_model();
188 apply(&mut m, &HighsOptions::default().method(method)).unwrap();
189 }
190 }
191
192 #[test]
193 fn apply_every_presolve_variant() {
194 for presolve in [HighsPresolve::Off, HighsPresolve::On, HighsPresolve::Auto] {
195 let mut m = empty_highs_model();
196 apply(&mut m, &HighsOptions::default().presolve(presolve)).unwrap();
197 }
198 }
199}