lp_solvers/solvers/
glpk.rs

1//! GNU's glpk solver
2//! [https://www.gnu.org/software/glpk/]
3//!
4use std::collections::HashMap;
5use std::ffi::OsString;
6use std::fs::File;
7use std::io::{BufRead, BufReader, Error};
8use std::path::{Path, PathBuf};
9
10use crate::lp_format::*;
11use crate::solvers::{
12    Solution, SolverProgram, SolverWithSolutionParsing, Status, WithMaxSeconds, WithMipGap,
13};
14
15/// glpk solver
16#[derive(Debug, Clone)]
17pub struct GlpkSolver {
18    name: String,
19    command_name: String,
20    temp_solution_file: Option<PathBuf>,
21    seconds: Option<u32>,
22    mipgap: Option<f32>,
23}
24
25impl Default for GlpkSolver {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl GlpkSolver {
32    /// New glpk solver instance
33    pub fn new() -> GlpkSolver {
34        GlpkSolver {
35            name: "Glpk".to_string(),
36            command_name: "glpsol".to_string(),
37            temp_solution_file: None,
38            seconds: None,
39            mipgap: None,
40        }
41    }
42    /// Set the glpk command name
43    pub fn command_name(&self, command_name: String) -> GlpkSolver {
44        GlpkSolver {
45            name: self.name.clone(),
46            command_name,
47            temp_solution_file: self.temp_solution_file.clone(),
48            seconds: self.seconds,
49            mipgap: self.mipgap,
50        }
51    }
52    /// Set the temporary solution file to use
53    pub fn with_temp_solution_file(&self, temp_solution_file: String) -> GlpkSolver {
54        GlpkSolver {
55            name: self.name.clone(),
56            command_name: self.command_name.clone(),
57            temp_solution_file: Some(temp_solution_file.into()),
58            seconds: self.seconds,
59            mipgap: self.mipgap,
60        }
61    }
62}
63
64impl SolverWithSolutionParsing for GlpkSolver {
65    fn read_specific_solution<'a, P: LpProblem<'a>>(
66        &self,
67        f: &File,
68        _problem: Option<&'a P>,
69    ) -> Result<Solution, String> {
70        fn read_size(line: Option<Result<String, Error>>) -> Result<usize, String> {
71            match line {
72                Some(Ok(l)) => match l.split_whitespace().nth(1) {
73                    Some(value) => match value.parse::<usize>() {
74                        Ok(v) => Ok(v),
75                        _ => Err("Incorrect solution format".to_string()),
76                    },
77                    _ => Err("Incorrect solution format".to_string()),
78                },
79                _ => Err("Incorrect solution format".to_string()),
80            }
81        }
82        let mut vars_value: HashMap<_, _> = HashMap::new();
83
84        let file = BufReader::new(f);
85
86        let mut iter = file.lines();
87        let row = read_size(iter.nth(1))?;
88        let col = read_size(iter.next())?;
89        let status = match iter.nth(1) {
90            Some(Ok(status_line)) => match &status_line[12..] {
91                "INTEGER OPTIMAL" | "OPTIMAL" => Status::Optimal,
92                "INTEGER NON-OPTIMAL" | "FEASIBLE" => Status::SubOptimal,
93                "INFEASIBLE (FINAL)" | "INTEGER EMPTY" => Status::Infeasible,
94                "UNDEFINED" => Status::NotSolved,
95                "INTEGER UNDEFINED" | "UNBOUNDED" => Status::Unbounded,
96                _ => return Err("Incorrect solution format: Unknown solution status".to_string()),
97            },
98            _ => return Err("Incorrect solution format: No solution status found".to_string()),
99        };
100        let mut result_lines = iter.skip(row + 7);
101        for _ in 0..col {
102            let line = match result_lines.next() {
103                Some(Ok(l)) => l,
104                _ => {
105                    return Err("Incorrect solution format: Not all columns are present".to_string())
106                }
107            };
108            let result_line: Vec<_> = line.split_whitespace().collect();
109            if result_line.len() >= 4 {
110                match result_line[3].parse::<f32>() {
111                    Ok(n) => {
112                        vars_value.insert(result_line[1].to_string(), n);
113                    }
114                    Err(e) => return Err(e.to_string()),
115                }
116            } else {
117                return Err(
118                    "Incorrect solution format: Column specification has to few fields".to_string(),
119                );
120            }
121        }
122        Ok(Solution::new(status, vars_value))
123    }
124}
125
126impl WithMaxSeconds<GlpkSolver> for GlpkSolver {
127    fn max_seconds(&self) -> Option<u32> {
128        self.seconds
129    }
130
131    fn with_max_seconds(&self, seconds: u32) -> GlpkSolver {
132        GlpkSolver {
133            seconds: Some(seconds),
134            ..(*self).clone()
135        }
136    }
137}
138
139impl WithMipGap<GlpkSolver> for GlpkSolver {
140    fn mip_gap(&self) -> Option<f32> {
141        self.mipgap
142    }
143
144    fn with_mip_gap(&self, mipgap: f32) -> Result<GlpkSolver, String> {
145        if mipgap.is_sign_positive() && mipgap.is_finite() {
146            Ok(GlpkSolver {
147                mipgap: Some(mipgap),
148                ..(*self).clone()
149            })
150        } else {
151            Err("Invalid MIP gap: must be positive and finite".to_string())
152        }
153    }
154}
155
156impl SolverProgram for GlpkSolver {
157    fn command_name(&self) -> &str {
158        &self.command_name
159    }
160
161    fn arguments(&self, lp_file: &Path, solution_file: &Path) -> Vec<OsString> {
162        let mut args = vec![
163            "--lp".into(),
164            lp_file.into(),
165            "-o".into(),
166            solution_file.into(),
167        ];
168
169        if let Some(seconds) = self.max_seconds() {
170            args.push("--tmlim".into());
171            args.push(seconds.to_string().into());
172        }
173
174        if let Some(mipgap) = self.mip_gap() {
175            args.push("--mipgap".into());
176            args.push(mipgap.to_string().into());
177        }
178
179        args
180    }
181
182    fn preferred_temp_solution_file(&self) -> Option<&Path> {
183        self.temp_solution_file.as_deref()
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use crate::solvers::{GlpkSolver, SolverProgram, WithMaxSeconds, WithMipGap};
190    use std::ffi::OsString;
191    use std::path::Path;
192
193    #[test]
194    fn cli_args_default() {
195        let solver = GlpkSolver::new();
196        let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
197
198        let expected: Vec<OsString> = vec![
199            "--lp".into(),
200            "test.lp".into(),
201            "-o".into(),
202            "test.sol".into(),
203        ];
204
205        assert_eq!(args, expected);
206    }
207
208    #[test]
209    fn cli_args_seconds() {
210        let solver = GlpkSolver::new().with_max_seconds(10);
211        let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
212
213        let expected: Vec<OsString> = vec![
214            "--lp".into(),
215            "test.lp".into(),
216            "-o".into(),
217            "test.sol".into(),
218            "--tmlim".into(),
219            "10".into(),
220        ];
221
222        assert_eq!(args, expected);
223    }
224
225    #[test]
226    fn cli_args_mipgap() {
227        let solver = GlpkSolver::new()
228            .with_mip_gap(0.05)
229            .expect("mipgap should be valid");
230
231        let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
232
233        let expected: Vec<OsString> = vec![
234            "--lp".into(),
235            "test.lp".into(),
236            "-o".into(),
237            "test.sol".into(),
238            "--mipgap".into(),
239            "0.05".into(),
240        ];
241
242        assert_eq!(args, expected);
243    }
244
245    #[test]
246    fn cli_args_mipgap_negative() {
247        let solver = GlpkSolver::new().with_mip_gap(-0.05);
248        assert!(solver.is_err());
249    }
250
251    #[test]
252    fn cli_args_mipgap_infinite() {
253        let solver = GlpkSolver::new().with_mip_gap(f32::INFINITY);
254        assert!(solver.is_err());
255    }
256
257    #[test]
258    fn cli_args_multiple() {
259        let solver = GlpkSolver::new()
260            .with_max_seconds(10)
261            .with_mip_gap(0.05)
262            .expect("mipgap should be valid");
263
264        let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
265
266        let expected: Vec<OsString> = vec![
267            "--lp".into(),
268            "test.lp".into(),
269            "-o".into(),
270            "test.sol".into(),
271            "--tmlim".into(),
272            "10".into(),
273            "--mipgap".into(),
274            "0.05".into(),
275        ];
276
277        assert_eq!(args, expected);
278    }
279}