1use std::collections::HashMap;
3use std::ffi::OsString;
4use std::fs::File;
5use std::io::{BufRead, BufReader, Write};
6use std::path::{Path, PathBuf};
7
8use crate::lp_format::*;
9use crate::solvers::{
10 Solution, SolverProgram, SolverWithSolutionParsing, Status, WithMaxSeconds, WithMipGap,
11 WithMipStart,
12};
13use crate::util::buf_contains;
14
15#[derive(Debug, Clone)]
17pub struct GurobiSolver {
18 name: String,
19 command_name: String,
20 temp_solution_file: Option<PathBuf>,
21 temp_mip_start_file: Option<PathBuf>,
22 seconds: Option<u32>,
23 mipgap: Option<f32>,
24}
25
26impl Default for GurobiSolver {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl GurobiSolver {
33 pub fn new() -> GurobiSolver {
35 GurobiSolver {
36 name: "Gurobi".to_string(),
37 command_name: "gurobi_cl".to_string(),
38 temp_solution_file: None,
39 temp_mip_start_file: None,
40 seconds: None,
41 mipgap: None,
42 }
43 }
44 pub fn command_name(&self, command_name: String) -> GurobiSolver {
46 GurobiSolver {
47 name: self.name.clone(),
48 command_name,
49 temp_solution_file: self.temp_solution_file.clone(),
50 temp_mip_start_file: self.temp_mip_start_file.clone(),
51 seconds: None,
52 mipgap: self.mipgap,
53 }
54 }
55}
56
57impl SolverWithSolutionParsing for GurobiSolver {
58 fn read_specific_solution<'a, P: LpProblem<'a>>(
59 &self,
60 f: &File,
61 _problem: Option<&'a P>,
62 ) -> Result<Solution, String> {
63 let mut vars_value: HashMap<_, _> = HashMap::new();
64 let mut file = BufReader::new(f);
65 let mut buffer = String::new();
66 let _ = file.read_line(&mut buffer);
67
68 if buffer.split(' ').next().is_some() {
69 for line in file.lines() {
70 let l = line.unwrap();
71
72 if let Some('#') = l.chars().next() {
74 continue;
75 }
76
77 let result_line: Vec<_> = l.split_whitespace().collect();
78 if result_line.len() == 2 {
79 match result_line[1].parse::<f32>() {
80 Ok(n) => {
81 vars_value.insert(result_line[0].to_string(), n);
82 }
83 Err(e) => return Err(e.to_string()),
84 }
85 } else {
86 return Err("Incorrect solution format".to_string());
87 }
88 }
89 } else {
90 return Err("Incorrect solution format".to_string());
91 }
92 Ok(Solution::new(Status::Optimal, vars_value))
93 }
94}
95
96impl WithMaxSeconds<GurobiSolver> for GurobiSolver {
97 fn max_seconds(&self) -> Option<u32> {
98 self.seconds
99 }
100
101 fn with_max_seconds(&self, seconds: u32) -> GurobiSolver {
102 GurobiSolver {
103 seconds: Some(seconds),
104 ..(*self).clone()
105 }
106 }
107}
108
109impl WithMipGap<GurobiSolver> for GurobiSolver {
110 fn mip_gap(&self) -> Option<f32> {
111 self.mipgap
112 }
113
114 fn with_mip_gap(&self, mipgap: f32) -> Result<GurobiSolver, String> {
115 if mipgap.is_sign_positive() && mipgap.is_finite() {
116 Ok(GurobiSolver {
117 mipgap: Some(mipgap),
118 ..(*self).clone()
119 })
120 } else {
121 Err("Invalid MIP gap: must be positive and finite".to_string())
122 }
123 }
124}
125
126impl WithMipStart<GurobiSolver> for GurobiSolver {
127 fn with_mip_start(&self, assignments: &HashMap<String, f32>) -> Result<GurobiSolver, String> {
130 let mut tmp = tempfile::Builder::new()
131 .prefix("lp-solvers-gurobi-")
132 .suffix(".mst")
133 .tempfile()
134 .map_err(|e| e.to_string())?;
135
136 writeln!(tmp, "# MIP start (generated by lp-solvers)").map_err(|e| e.to_string())?;
137
138 let mut deterministic_assignments: Vec<_> = assignments.iter().collect();
139 deterministic_assignments.sort_by(|(a, _), (b, _)| a.cmp(b));
140
141 for (var, val) in deterministic_assignments {
142 writeln!(tmp, "{} {}", var, val).map_err(|e| e.to_string())?;
143 }
144 tmp.flush().map_err(|e| e.to_string())?;
145
146 let path = tmp.into_temp_path().keep().map_err(|e| e.to_string())?;
147
148 Ok(GurobiSolver {
149 temp_mip_start_file: Some(path),
150 ..(*self).clone()
151 })
152 }
153}
154
155impl SolverProgram for GurobiSolver {
156 fn command_name(&self) -> &str {
157 &self.command_name
158 }
159
160 fn arguments(&self, lp_file: &Path, solution_file: &Path) -> Vec<OsString> {
161 let mut arg0: OsString = "ResultFile=".into();
162 arg0.push(solution_file.as_os_str());
163
164 let mut args = vec![arg0];
165
166 if let Some(mipgap) = self.mip_gap() {
167 let mut arg_mipgap: OsString = "MIPGap=".into();
168 arg_mipgap.push::<OsString>(mipgap.to_string().into());
169 args.push(arg_mipgap);
170 }
171
172 if let Some(seconds) = self.max_seconds() {
173 let mut arg_timelimit: OsString = "TimeLimit=".into();
174 arg_timelimit.push::<OsString>(seconds.to_string().into());
175 args.push(arg_timelimit);
176 }
177
178 if let Some(mst_file_path) = &self.temp_mip_start_file {
179 let mut arg_inputfile: OsString = "InputFile=".into();
180 arg_inputfile.push::<OsString>(mst_file_path.clone().into_os_string());
181 args.push(arg_inputfile);
182 }
183
184 args.push(lp_file.into());
185
186 args
187 }
188
189 fn preferred_temp_solution_file(&self) -> Option<&Path> {
190 self.temp_solution_file.as_deref()
191 }
192
193 fn solution_suffix(&self) -> Option<&str> {
194 Some(".sol")
195 }
196
197 fn parse_stdout_status(&self, stdout: &[u8]) -> Option<Status> {
198 if buf_contains(stdout, "Optimal solution found (tolerance 1.00e-04)") {
199 Some(Status::Optimal)
200 } else if buf_contains(stdout, "Optimal solution found (tolerance ") {
201 Some(Status::MipGap)
202 } else if buf_contains(stdout, "Time limit reached") {
203 if buf_contains(stdout, "Best objective -,") {
204 Some(Status::NotSolved)
205 } else {
206 Some(Status::TimeLimit)
207 }
208 } else if buf_contains(stdout, "infeasible") || buf_contains(stdout, "Infeasible model") {
209 Some(Status::Infeasible)
210 } else {
211 None
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use crate::solvers::{GurobiSolver, SolverProgram, WithMaxSeconds, WithMipGap, WithMipStart};
219 use std::collections::HashMap;
220 use std::env::temp_dir;
221 use std::ffi::{OsStr, OsString};
222 use std::path::Path;
223
224 #[test]
225 fn cli_args_default() {
226 let solver = GurobiSolver::new();
227 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
228
229 let expected: Vec<OsString> = vec!["ResultFile=test.sol".into(), "test.lp".into()];
230
231 assert_eq!(args, expected);
232 }
233
234 #[test]
235 fn cli_args_seconds() {
236 let solver = GurobiSolver::new().with_max_seconds(10);
237 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
238
239 let expected: Vec<OsString> = vec![
240 "ResultFile=test.sol".into(),
241 "TimeLimit=10".into(),
242 "test.lp".into(),
243 ];
244
245 assert_eq!(args, expected);
246 }
247
248 #[test]
249 fn cli_args_mipgap() {
250 let solver = GurobiSolver::new()
251 .with_mip_gap(0.05)
252 .expect("mipgap should be valid");
253
254 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
255
256 let expected: Vec<OsString> = vec![
257 "ResultFile=test.sol".into(),
258 "MIPGap=0.05".into(),
259 "test.lp".into(),
260 ];
261
262 assert_eq!(args, expected);
263 }
264
265 #[test]
266 fn cli_args_input_file() {
267 let solver = GurobiSolver::new()
268 .with_mip_start(&HashMap::from([
269 ("x".to_owned(), 1.0_f32),
270 ("y".to_owned(), -2.5_f32),
271 ]))
272 .expect("mip start should be valid");
273
274 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
275
276 let input_file_argument = args
277 .iter()
278 .find(|a| a.to_string_lossy().starts_with("InputFile="))
279 .expect("expected an InputFile=... argument")
280 .to_string_lossy()
281 .to_string();
282
283 let input_file_path = Path::new(input_file_argument.strip_prefix("InputFile=").unwrap());
284
285 assert!(
286 input_file_path.exists(),
287 "MIP start file does not exist: {:?}",
288 input_file_path
289 );
290
291 assert_eq!(
292 input_file_path.extension(),
293 Some(OsStr::new("mst")),
294 "InputFile not an .mst: {:?}",
295 input_file_path
296 );
297
298 assert!(
299 input_file_path.starts_with(&temp_dir()),
300 "InputFile not under temp dir.\n temp: {:?}\n file: {:?}",
301 temp_dir(),
302 input_file_path
303 );
304 }
305
306 #[test]
307 fn cli_args_mipgap_negative() {
308 let solver = GurobiSolver::new().with_mip_gap(-0.05);
309 assert!(solver.is_err());
310 }
311
312 #[test]
313 fn cli_args_mipgap_infinite() {
314 let solver = GurobiSolver::new().with_mip_gap(f32::INFINITY);
315 assert!(solver.is_err());
316 }
317}