Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 85 additions & 3 deletions src/solvers/gurobi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
use std::collections::HashMap;
use std::ffi::OsString;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};

use crate::lp_format::*;
use crate::solvers::{
Solution, SolverProgram, SolverWithSolutionParsing, Status, WithMaxSeconds, WithMipGap,
WithMipStart,
};
use crate::util::buf_contains;

Expand All @@ -17,6 +18,7 @@ pub struct GurobiSolver {
name: String,
command_name: String,
temp_solution_file: Option<PathBuf>,
temp_mip_start_file: Option<PathBuf>,
seconds: Option<u32>,
mipgap: Option<f32>,
}
Expand All @@ -34,6 +36,7 @@ impl GurobiSolver {
name: "Gurobi".to_string(),
command_name: "gurobi_cl".to_string(),
temp_solution_file: None,
temp_mip_start_file: None,
seconds: None,
mipgap: None,
}
Expand All @@ -44,6 +47,7 @@ impl GurobiSolver {
name: self.name.clone(),
command_name,
temp_solution_file: self.temp_solution_file.clone(),
temp_mip_start_file: self.temp_mip_start_file.clone(),
seconds: None,
mipgap: self.mipgap,
}
Expand Down Expand Up @@ -119,6 +123,35 @@ impl WithMipGap<GurobiSolver> for GurobiSolver {
}
}

impl WithMipStart<GurobiSolver> for GurobiSolver {
/// create a (temporary) mip start file (.mst) and store the path reference in the solver struct.
/// file is persisted; caller may want to delete
fn with_mip_start(&self, assignments: &HashMap<String, f32>) -> Result<GurobiSolver, String> {
let mut tmp = tempfile::Builder::new()
.prefix("lp-solvers-gurobi-")
.suffix(".mst")
.tempfile()
.map_err(|e| e.to_string())?;

writeln!(tmp, "# MIP start (generated by lp-solvers)").map_err(|e| e.to_string())?;

let mut deterministic_assignments: Vec<_> = assignments.iter().collect();
deterministic_assignments.sort_by(|(a, _), (b, _)| a.cmp(b));

for (var, val) in deterministic_assignments {
writeln!(tmp, "{} {}", var, val).map_err(|e| e.to_string())?;
}
tmp.flush().map_err(|e| e.to_string())?;

let path = tmp.into_temp_path().keep().map_err(|e| e.to_string())?;

Ok(GurobiSolver {
temp_mip_start_file: Some(path),
..(*self).clone()
})
}
}

impl SolverProgram for GurobiSolver {
fn command_name(&self) -> &str {
&self.command_name
Expand All @@ -142,6 +175,12 @@ impl SolverProgram for GurobiSolver {
args.push(arg_timelimit);
}

if let Some(mst_file_path) = &self.temp_mip_start_file {
let mut arg_inputfile: OsString = "InputFile=".into();
arg_inputfile.push::<OsString>(mst_file_path.clone().into_os_string());
args.push(arg_inputfile);
}

args.push(lp_file.into());

args
Expand Down Expand Up @@ -176,8 +215,10 @@ impl SolverProgram for GurobiSolver {

#[cfg(test)]
mod tests {
use crate::solvers::{GurobiSolver, SolverProgram, WithMaxSeconds, WithMipGap};
use std::ffi::OsString;
use crate::solvers::{GurobiSolver, SolverProgram, WithMaxSeconds, WithMipGap, WithMipStart};
use std::collections::HashMap;
use std::env::temp_dir;
use std::ffi::{OsStr, OsString};
use std::path::Path;

#[test]
Expand Down Expand Up @@ -221,6 +262,47 @@ mod tests {
assert_eq!(args, expected);
}

#[test]
fn cli_args_input_file() {
let solver = GurobiSolver::new()
.with_mip_start(&HashMap::from([
("x".to_owned(), 1.0_f32),
("y".to_owned(), -2.5_f32),
]))
.expect("mip start should be valid");

let args = solver.arguments(Path::new("test.lp"), Path::new("test.sol"));

let input_file_argument = args
.iter()
.find(|a| a.to_string_lossy().starts_with("InputFile="))
.expect("expected an InputFile=... argument")
.to_string_lossy()
.to_string();

let input_file_path = Path::new(input_file_argument.strip_prefix("InputFile=").unwrap());

assert!(
input_file_path.exists(),
"MIP start file does not exist: {:?}",
input_file_path
);

assert_eq!(
input_file_path.extension(),
Some(OsStr::new("mst")),
"InputFile not an .mst: {:?}",
input_file_path
);

assert!(
input_file_path.starts_with(&temp_dir()),
"InputFile not under temp dir.\n temp: {:?}\n file: {:?}",
temp_dir(),
input_file_path
);
}

#[test]
fn cli_args_mipgap_negative() {
let solver = GurobiSolver::new().with_mip_gap(-0.05);
Expand Down
6 changes: 6 additions & 0 deletions src/solvers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ pub trait WithMipGap<T> {
fn with_mip_gap(&self, mipgap: f32) -> Result<T, String>;
}

/// Provide a MIP start: (partial) initial solution
pub trait WithMipStart<T> {
/// set MIP start
fn with_mip_start(&self, assignments: &HashMap<String, f32>) -> Result<T, String>;
}

/// A static version of a solver, where the solver itself doesn't hold any data
///
/// ```
Expand Down