1use std::collections::HashMap;
4use std::ffi::OsString;
5use std::fs::File;
6use std::io::{BufRead, BufReader};
7use std::path::{Path, PathBuf};
8
9use crate::lp_format::*;
10use crate::solvers::{
11 Solution, SolverProgram, SolverWithSolutionParsing, Status, WithMaxSeconds, WithMipGap,
12 WithNbThreads,
13};
14
15#[derive(Debug, Clone)]
17pub struct CbcSolver {
18 name: String,
19 command_name: String,
20 temp_solution_file: Option<PathBuf>,
21 threads: Option<u32>,
22 seconds: Option<u32>,
23 mipgap: Option<f32>,
24}
25
26impl Default for CbcSolver {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl CbcSolver {
33 pub fn new() -> CbcSolver {
35 CbcSolver {
36 name: "Cbc".to_string(),
37 command_name: "cbc".to_string(),
38 temp_solution_file: None,
39 threads: None,
40 seconds: None,
41 mipgap: None,
42 }
43 }
44
45 pub fn command_name(&self, command_name: String) -> CbcSolver {
47 CbcSolver {
48 name: self.name.clone(),
49 command_name,
50 temp_solution_file: self.temp_solution_file.clone(),
51 threads: self.threads,
52 seconds: self.seconds,
53 mipgap: self.mipgap,
54 }
55 }
56
57 pub fn with_temp_solution_file(&self, temp_solution_file: String) -> CbcSolver {
59 CbcSolver {
60 name: self.name.clone(),
61 command_name: self.command_name.clone(),
62 temp_solution_file: Some(temp_solution_file.into()),
63 threads: self.threads,
64 seconds: self.seconds,
65 mipgap: self.mipgap,
66 }
67 }
68}
69
70impl SolverWithSolutionParsing for CbcSolver {
71 fn read_specific_solution<'a, P: LpProblem<'a>>(
72 &self,
73 f: &File,
74 problem: Option<&'a P>,
75 ) -> Result<Solution, String> {
76 let mut vars_value: HashMap<String, _> = HashMap::new();
77
78 if let Some(p) = problem {
81 for var in p.variables() {
82 vars_value.insert(var.name().to_string(), 0.0);
83 }
84 }
85
86 let mut file = BufReader::new(f);
87 let mut buffer = String::new();
88 let _ = file.read_line(&mut buffer);
89
90 let mut buffer_split = buffer.split_whitespace();
91
92 let status = if let Some(status) = buffer_split.next() {
93 match status {
94 "Optimal" => {
95 if let Some(substatus) = buffer_split.next() {
96 match substatus {
97 "(within" => Status::MipGap,
99 _ => Status::Optimal,
100 }
101 } else {
102 Status::Optimal
103 }
104 }
105 "Infeasible" | "Integer" => Status::Infeasible,
107 "Unbounded" => Status::Unbounded,
108 "Stopped" => Status::SubOptimal,
110 _ => Status::NotSolved,
111 }
112 } else {
113 return Err("Incorrect solution format".to_string());
114 };
115 for line in file.lines() {
116 let l = line.unwrap();
117 let mut result_line: Vec<_> = l.split_whitespace().collect();
118 if result_line[0] == "**" {
119 result_line.remove(0);
120 };
121 if result_line.len() == 4 {
122 match result_line[2].parse::<f32>() {
123 Ok(n) => {
124 vars_value.insert(result_line[1].to_string(), n);
125 }
126 Err(e) => return Err(e.to_string()),
127 }
128 } else {
129 return Err("Incorrect solution format".to_string());
130 }
131 }
132 Ok(Solution::new(status, vars_value))
133 }
134}
135
136impl WithMaxSeconds<CbcSolver> for CbcSolver {
137 fn max_seconds(&self) -> Option<u32> {
138 self.seconds
139 }
140 fn with_max_seconds(&self, seconds: u32) -> CbcSolver {
141 CbcSolver {
142 seconds: Some(seconds),
143 ..(*self).clone()
144 }
145 }
146}
147
148impl WithMipGap<CbcSolver> for CbcSolver {
149 fn mip_gap(&self) -> Option<f32> {
150 self.mipgap
151 }
152
153 fn with_mip_gap(&self, mipgap: f32) -> Result<CbcSolver, String> {
154 if mipgap.is_sign_positive() && mipgap.is_finite() {
155 Ok(CbcSolver {
156 mipgap: Some(mipgap),
157 ..(*self).clone()
158 })
159 } else {
160 Err("Invalid MIP gap: must be positive and finite".to_string())
161 }
162 }
163}
164
165impl WithNbThreads<CbcSolver> for CbcSolver {
166 fn nb_threads(&self) -> Option<u32> {
167 self.threads
168 }
169 fn with_nb_threads(&self, threads: u32) -> CbcSolver {
170 CbcSolver {
171 threads: Some(threads),
172 ..(*self).clone()
173 }
174 }
175}
176
177impl SolverProgram for CbcSolver {
178 fn command_name(&self) -> &str {
179 &self.command_name
180 }
181
182 fn arguments(&self, lp_file: &Path, solution_file: &Path) -> Vec<OsString> {
183 let mut args = vec![lp_file.as_os_str().to_owned()];
184 if let Some(mipgap) = self.mip_gap() {
185 args.push("ratiogap".into());
186 args.push(mipgap.to_string().into());
187 }
188 for (name, value) in [
189 ("seconds", self.max_seconds()),
190 ("threads", self.nb_threads()),
191 ]
192 .iter()
193 {
194 if let Some(val) = value {
195 args.push(name.into());
196 args.push(val.to_string().into());
197 }
198 }
199 args.extend_from_slice(&["solve".into(), "solution".into(), solution_file.into()]);
200 args
201 }
202
203 fn preferred_temp_solution_file(&self) -> Option<&Path> {
204 self.temp_solution_file.as_deref()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use crate::solvers::{CbcSolver, SolverProgram, WithMaxSeconds, WithMipGap, WithNbThreads};
211 use std::ffi::OsString;
212 use std::path::Path;
213
214 #[test]
215 fn cli_args_default() {
216 let solver = CbcSolver::new();
217 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
218
219 let expected: Vec<OsString> = vec![
220 "test.lp".into(),
221 "solve".into(),
222 "solution".into(),
223 "test.sol".into(),
224 ];
225
226 assert_eq!(args, expected);
227 }
228
229 #[test]
230 fn cli_args_seconds() {
231 let solver = CbcSolver::new().with_max_seconds(10);
232 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
233
234 let expected: Vec<OsString> = vec![
235 "test.lp".into(),
236 "seconds".into(),
237 "10".into(),
238 "solve".into(),
239 "solution".into(),
240 "test.sol".into(),
241 ];
242
243 assert_eq!(args, expected);
244 }
245
246 #[test]
247 fn cli_args_mipgap() {
248 let solver = CbcSolver::new()
249 .with_mip_gap(0.05)
250 .expect("mipgap should be valid");
251
252 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
253
254 let expected: Vec<OsString> = vec![
255 "test.lp".into(),
256 "ratiogap".into(),
257 "0.05".to_string().into(),
258 "solve".into(),
259 "solution".into(),
260 "test.sol".into(),
261 ];
262
263 assert_eq!(args, expected);
264 }
265
266 #[test]
267 fn cli_args_mipgap_negative() {
268 let solver = CbcSolver::new().with_mip_gap(-0.05);
269 assert!(solver.is_err());
270 }
271
272 #[test]
273 fn cli_args_mipgap_infinite() {
274 let solver = CbcSolver::new().with_mip_gap(f32::INFINITY);
275 assert!(solver.is_err());
276 }
277
278 #[test]
279 fn cli_args_threads() {
280 let solver = CbcSolver::new().with_nb_threads(3);
281 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
282
283 let expected: Vec<OsString> = vec![
284 "test.lp".into(),
285 "threads".into(),
286 "3".into(),
287 "solve".into(),
288 "solution".into(),
289 "test.sol".into(),
290 ];
291
292 assert_eq!(args, expected);
293 }
294
295 #[test]
296 fn cli_args_multiple() {
297 let solver = CbcSolver::new()
298 .with_nb_threads(3)
299 .with_max_seconds(10)
300 .with_mip_gap(0.05)
301 .expect("mipgap should be valid");
302
303 let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));
304
305 let expected: Vec<OsString> = vec![
306 "test.lp".into(),
307 "ratiogap".into(),
308 "0.05".into(),
309 "seconds".into(),
310 "10".into(),
311 "threads".into(),
312 "3".into(),
313 "solve".into(),
314 "solution".into(),
315 "test.sol".into(),
316 ];
317
318 assert_eq!(args, expected);
319 }
320}