1use 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#[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 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 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 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}