lp_solvers/solvers/
cbc.rs

1//! The coin-or cbc solver.
2//! [https://github.com/coin-or/Cbc#cbc]
3use 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/// The coin-or cbc solver
16#[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    /// Crate a cbc solver instance
34    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    /// set the name of the executable to use
46    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    /// Set the temporary solution file to use
58    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        // populate default values for all vars
79        // CBC keeps only non-zero values from a number of variables
80        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                            // MIP gap stops are "Optimal (within gap tolerance)"
98                            "(within" => Status::MipGap,
99                            _ => Status::Optimal,
100                        }
101                    } else {
102                        Status::Optimal
103                    }
104                }
105                // Infeasible status is either "Infeasible" or "Integer infeasible"
106                "Infeasible" | "Integer" => Status::Infeasible,
107                "Unbounded" => Status::Unbounded,
108                // "Stopped" can be "on time", "on iterations", "on difficulties" or "on ctrl-c"
109                "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}