Skip to main content

oximo_highs/
options.rs

1use highs::{HighsOptionValue, Model as HighsModel};
2use oximo_solver::{HasUniversal, SolverError, UniversalOptions};
3
4/// HiGHS-specific solver options.
5#[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/// HiGHS presolve options.
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum HighsPresolve {
17    Off,
18    On,
19    Auto,
20}
21
22/// HiGHS LP / root-relaxation algorithm.
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum HighsMethod {
25    /// Let HiGHS pick.
26    Choose,
27    Simplex,
28    /// Interior-point method.
29    Ipm,
30    /// First-order primal-dual LP solver.
31    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
70/// Set a single HiGHS option through the non-panicking `try_set_option`,
71/// mapping any failure to a [`SolverError::Backend`].
72fn 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
82/// Apply typed [`HighsOptions`] onto a live HiGHS model.
83///
84/// # Errors
85///
86/// Returns [`SolverError::Backend`] if HiGHS rejects an option name or value.
87pub(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}