Skip to main content

oximo_highs/
options.rs

1use highs::Model as HighsModel;
2use oximo_solver::{HasUniversal, 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/// Apply typed [`HighsOptions`] onto a live HiGHS model.
71pub(crate) fn apply(model: &mut HighsModel, o: &HighsOptions) {
72    if let Some(d) = o.universal.time_limit {
73        model.set_option("time_limit", d.as_secs_f64());
74    }
75    if let Some(n) = o.universal.threads {
76        model.set_option("threads", i32::try_from(n).unwrap_or(i32::MAX));
77    }
78    if let Some(b) = o.universal.verbose {
79        model.set_option("output_flag", b);
80        model.set_option("log_to_console", b);
81    }
82    if let Some(g) = o.mip_gap {
83        model.set_option("mip_rel_gap", g);
84    }
85    if let Some(p) = o.presolve {
86        model.set_option("presolve", presolve_str(p));
87    }
88    if let Some(m) = o.method {
89        model.set_option("solver", method_str(m));
90    }
91    if let Some(p) = o.parallel {
92        model.set_option("parallel", if p { "on" } else { "off" });
93    }
94}
95
96fn presolve_str(p: HighsPresolve) -> &'static str {
97    match p {
98        HighsPresolve::Off => "off",
99        HighsPresolve::On => "on",
100        HighsPresolve::Auto => "choose",
101    }
102}
103
104fn method_str(m: HighsMethod) -> &'static str {
105    match m {
106        HighsMethod::Choose => "choose",
107        HighsMethod::Simplex => "simplex",
108        HighsMethod::Ipm => "ipm",
109        HighsMethod::PdLp => "pdlp",
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use std::time::Duration;
116
117    use highs::{RowProblem, Sense as HighsSense};
118    use oximo_solver::UniversalOptionsExt;
119
120    use super::*;
121
122    fn empty_highs_model() -> HighsModel {
123        RowProblem::default().optimise(HighsSense::Minimise)
124    }
125
126    #[test]
127    fn builder_sets_all_fields() {
128        let o = HighsOptions::default()
129            .time_limit(Duration::from_secs(30))
130            .threads(8)
131            .verbose(true)
132            .mip_gap(0.01)
133            .presolve(HighsPresolve::Off)
134            .method(HighsMethod::Ipm)
135            .parallel(true);
136        assert_eq!(o.universal.time_limit, Some(Duration::from_secs(30)));
137        assert_eq!(o.universal.threads, Some(8));
138        assert_eq!(o.universal.verbose, Some(true));
139        assert_eq!(o.mip_gap, Some(0.01));
140        assert_eq!(o.presolve, Some(HighsPresolve::Off));
141        assert_eq!(o.method, Some(HighsMethod::Ipm));
142        assert_eq!(o.parallel, Some(true));
143    }
144
145    #[test]
146    fn apply_default_does_not_panic() {
147        let mut m = empty_highs_model();
148        apply(&mut m, &HighsOptions::default());
149    }
150
151    #[test]
152    fn apply_all_options_does_not_panic() {
153        let mut m = empty_highs_model();
154        let o = HighsOptions::default()
155            .time_limit(Duration::from_secs(10))
156            .threads(1)
157            .verbose(false)
158            .mip_gap(0.01)
159            .presolve(HighsPresolve::Off)
160            .method(HighsMethod::Simplex)
161            .parallel(false);
162        apply(&mut m, &o);
163    }
164
165    #[test]
166    fn apply_every_method_variant() {
167        for method in
168            [HighsMethod::Choose, HighsMethod::Simplex, HighsMethod::Ipm, HighsMethod::PdLp]
169        {
170            let mut m = empty_highs_model();
171            apply(&mut m, &HighsOptions::default().method(method));
172        }
173    }
174
175    #[test]
176    fn apply_every_presolve_variant() {
177        for presolve in [HighsPresolve::Off, HighsPresolve::On, HighsPresolve::Auto] {
178            let mut m = empty_highs_model();
179            apply(&mut m, &HighsOptions::default().presolve(presolve));
180        }
181    }
182}