From e7a161d60080b77d8ac513c4ce191cb03cdb254d Mon Sep 17 00:00:00 2001 From: Gabriel Gehrke Date: Fri, 9 Jan 2026 13:07:53 +0100 Subject: [PATCH 1/3] Defined WithMipStart trait --- src/solvers/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 8283d70..4dba75a 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -218,6 +218,12 @@ pub trait WithMipGap { fn with_mip_gap(&self, mipgap: f32) -> Result; } +/// Provide a MIP start: (partial) initial solution +pub trait WithMipStart { + /// set MIP start + fn with_mip_start(&self, assignments: &HashMap) -> Result; +} + /// A static version of a solver, where the solver itself doesn't hold any data /// /// ``` From 1f289c998ea7eb65f470f47eb81e2bc70123a1a6 Mon Sep 17 00:00:00 2001 From: Gabriel Gehrke Date: Fri, 9 Jan 2026 13:10:02 +0100 Subject: [PATCH 2/3] Implemented WithMipStart for GurobiSolver --- src/solvers/gurobi.rs | 80 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/src/solvers/gurobi.rs b/src/solvers/gurobi.rs index ea01eb5..d3d8eb6 100644 --- a/src/solvers/gurobi.rs +++ b/src/solvers/gurobi.rs @@ -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; @@ -17,6 +18,7 @@ pub struct GurobiSolver { name: String, command_name: String, temp_solution_file: Option, + temp_mip_start_file: Option, seconds: Option, mipgap: Option, } @@ -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, } @@ -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, } @@ -119,6 +123,35 @@ impl WithMipGap for GurobiSolver { } } +impl WithMipStart 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) -> Result { + 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 @@ -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::(mst_file_path.clone().into_os_string()); + args.push(arg_inputfile); + } + args.push(lp_file.into()); args @@ -176,7 +215,8 @@ impl SolverProgram for GurobiSolver { #[cfg(test)] mod tests { - use crate::solvers::{GurobiSolver, SolverProgram, WithMaxSeconds, WithMipGap}; + use crate::solvers::{GurobiSolver, SolverProgram, WithMaxSeconds, WithMipGap, WithMipStart}; + use std::collections::HashMap; use std::ffi::OsString; use std::path::Path; @@ -221,6 +261,42 @@ 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 = input_file_argument.strip_prefix("InputFile=").unwrap(); + assert!( + input_file_path.starts_with("/tmp/"), + "InputFile not in /tmp: {}", + input_file_path + ); + assert!( + input_file_path.ends_with(".mst"), + "InputFile not an .mst: {}", + input_file_path + ); + assert!( + std::path::Path::new(input_file_path).exists(), + "MIP start file does not exist: {}", + input_file_path + ); + } + #[test] fn cli_args_mipgap_negative() { let solver = GurobiSolver::new().with_mip_gap(-0.05); From b65f4c07fbdaf7bd109c24400eb7ebbade7fc717 Mon Sep 17 00:00:00 2001 From: Gabriel Gehrke Date: Fri, 9 Jan 2026 18:17:21 +0100 Subject: [PATCH 3/3] fix test case: Replaced use of hardcoded /tmp directory with temp_dir() call --- src/solvers/gurobi.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/solvers/gurobi.rs b/src/solvers/gurobi.rs index d3d8eb6..0ad0fd9 100644 --- a/src/solvers/gurobi.rs +++ b/src/solvers/gurobi.rs @@ -217,7 +217,8 @@ impl SolverProgram for GurobiSolver { mod tests { use crate::solvers::{GurobiSolver, SolverProgram, WithMaxSeconds, WithMipGap, WithMipStart}; use std::collections::HashMap; - use std::ffi::OsString; + use std::env::temp_dir; + use std::ffi::{OsStr, OsString}; use std::path::Path; #[test] @@ -279,20 +280,25 @@ mod tests { .to_string_lossy() .to_string(); - let input_file_path = input_file_argument.strip_prefix("InputFile=").unwrap(); + let input_file_path = Path::new(input_file_argument.strip_prefix("InputFile=").unwrap()); + assert!( - input_file_path.starts_with("/tmp/"), - "InputFile not in /tmp: {}", + input_file_path.exists(), + "MIP start file does not exist: {:?}", input_file_path ); - assert!( - input_file_path.ends_with(".mst"), - "InputFile not an .mst: {}", + + assert_eq!( + input_file_path.extension(), + Some(OsStr::new("mst")), + "InputFile not an .mst: {:?}", input_file_path ); + assert!( - std::path::Path::new(input_file_path).exists(), - "MIP start file does not exist: {}", + input_file_path.starts_with(&temp_dir()), + "InputFile not under temp dir.\n temp: {:?}\n file: {:?}", + temp_dir(), input_file_path ); }