Skip to main content

oximo_gams/
options.rs

1use std::fmt::Write as FmtWrite;
2use std::path::PathBuf;
3
4use oximo_solver::{HasUniversal, UniversalOptions};
5
6use crate::solver_options::GamsSolverConfig;
7
8/// GAMS-specific solver options.
9#[derive(Clone, Debug, Default)]
10pub struct GamsOptions {
11    pub universal: UniversalOptions,
12    pub mip_gap: Option<f64>,
13    /// Sub-solver selection with optional typed options.
14    /// Translates to `option {LP|MIP} = <name>;` plus a `<solver>.opt` file
15    /// when options are set.
16    pub solver: Option<GamsSolverConfig>,
17    /// Override for the `gams` executable. When `None`, `"gams"` is looked up
18    /// from `PATH`.
19    pub gams_path: Option<PathBuf>,
20}
21
22/// Named GAMS sub-solver. Use [`GamsSolver::Custom`] for any name that isn't
23/// a pre-enumerated variant.
24///
25/// Reference: <https://www.gams.com/latest/docs/S_MAIN.html#SOLVERS_MODEL_TYPES>
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub enum GamsSolver {
28    /// ALPHAECP: MINLP, MIQCP
29    AlphaEcp,
30    /// ANTIGONE: NLP, CNS, DNLP, MINLP, QCP, MIQCP, Global
31    Antigone,
32    /// BARON: LP, MIP, NLP, CNS, DNLP, MINLP, QCP, MIQCP, Global
33    Baron,
34    /// CBC: LP, MIP
35    Cbc,
36    /// CONOPT: LP, NLP, CNS, DNLP, QCP
37    Conopt,
38    /// COPT: LP, MIP, QCP, MIQCP
39    Copt,
40    /// CPLEX: LP, MIP, QCP, MIQCP
41    Cplex,
42    /// DECIS: LP, Stochastic
43    Decis,
44    /// DICOPT: MINLP, MIQCP
45    Dicopt,
46    /// GLPK: LP, MIP (not in GAMS docs but recognized)
47    Glpk,
48    /// GUROBI: LP, MIP, NLP, DNLP, MINLP, QCP, MIQCP, Global
49    Gurobi,
50    /// GUSS: LP, MIP, NLP, MCP, CNS, DNLP, MINLP, QCP, MIQCP
51    Guss,
52    /// HiGHS: LP, MIP
53    Highs,
54    /// IPOPT: LP, NLP, CNS, DNLP, QCP
55    Ipopt,
56    /// JAMS: EMP
57    Jams,
58    /// KESTREL: all model types (remote solver submission)
59    Kestrel,
60    /// KNITRO: LP, NLP, MCP, MPEC, CNS, DNLP, MINLP, QCP, MIQCP
61    Knitro,
62    /// LINDO: LP, MIP, NLP, DNLP, MINLP, QCP, MIQCP, Stochastic, Global
63    Lindo,
64    /// LINDOGLOBAL: LP, MIP, NLP, DNLP, MINLP, QCP, MIQCP, Global
65    LindoGlobal,
66    /// MILES: MCP
67    Miles,
68    /// MINOS: LP, NLP, CNS, DNLP, QCP
69    Minos,
70    /// MOSEK: LP, MIP, NLP, DNLP, MINLP, QCP, MIQCP
71    Mosek,
72    /// NLPEC: MCP, MPEC
73    Nlpec,
74    /// ODHCPLEX: MIP, MIQCP
75    OdhCplex,
76    /// PATH: MCP, CNS
77    Path,
78    /// QUADMINOS: LP
79    QuadMinos,
80    /// RESHOP: EMP
81    Reshop,
82    /// SBB: MINLP, MIQCP
83    Sbb,
84    /// SCIP: MIP, NLP, CNS, DNLP, MINLP, QCP, MIQCP, Global
85    Scip,
86    /// SHOT: MINLP, MIQCP
87    Shot,
88    /// SNOPT: LP, NLP, CNS, DNLP, QCP
89    Snopt,
90    /// SOPLEX: LP
91    Soplex,
92    /// XPRESS: LP, MIP, NLP, CNS, DNLP, MINLP, QCP, MIQCP, Global
93    Xpress,
94    /// Any other GAMS-recognized solver name, emitted verbatim.
95    Custom(String),
96}
97
98impl GamsSolver {
99    /// GAMS solver keyword used in the `option {LP|MIP} = ...;` statement.
100    #[must_use]
101    pub fn name(&self) -> &str {
102        match self {
103            Self::AlphaEcp => "ALPHAECP",
104            Self::Antigone => "ANTIGONE",
105            Self::Baron => "BARON",
106            Self::Cbc => "CBC",
107            Self::Conopt => "CONOPT",
108            Self::Copt => "COPT",
109            Self::Cplex => "CPLEX",
110            Self::Decis => "DECIS",
111            Self::Dicopt => "DICOPT",
112            Self::Glpk => "GLPK",
113            Self::Gurobi => "GUROBI",
114            Self::Guss => "GUSS",
115            Self::Highs => "HIGHS",
116            Self::Ipopt => "IPOPT",
117            Self::Jams => "JAMS",
118            Self::Kestrel => "KESTREL",
119            Self::Knitro => "KNITRO",
120            Self::Lindo => "LINDO",
121            Self::LindoGlobal => "LINDOGLOBAL",
122            Self::Miles => "MILES",
123            Self::Minos => "MINOS",
124            Self::Mosek => "MOSEK",
125            Self::Nlpec => "NLPEC",
126            Self::OdhCplex => "ODHCPLEX",
127            Self::Path => "PATH",
128            Self::QuadMinos => "QUADMINOS",
129            Self::Reshop => "RESHOP",
130            Self::Sbb => "SBB",
131            Self::Scip => "SCIP",
132            Self::Shot => "SHOT",
133            Self::Snopt => "SNOPT",
134            Self::Soplex => "SOPLEX",
135            Self::Xpress => "XPRESS",
136            Self::Custom(s) => s.as_str(),
137        }
138    }
139}
140
141impl GamsOptions {
142    #[must_use]
143    pub fn mip_gap(mut self, gap: f64) -> Self {
144        self.mip_gap = Some(gap);
145        self
146    }
147
148    #[must_use]
149    pub fn solver(mut self, s: impl Into<GamsSolverConfig>) -> Self {
150        self.solver = Some(s.into());
151        self
152    }
153
154    #[must_use]
155    pub fn gams_path(mut self, p: impl Into<PathBuf>) -> Self {
156        self.gams_path = Some(p.into());
157        self
158    }
159}
160
161impl HasUniversal for GamsOptions {
162    fn universal(&self) -> &UniversalOptions {
163        &self.universal
164    }
165
166    fn universal_mut(&mut self) -> &mut UniversalOptions {
167        &mut self.universal
168    }
169}
170
171/// Emit GAMS option statements into `gms` before the `Solve` statement.
172///
173/// `solve_type` is the GAMS model type (`"LP"` / `"MIP"` / `"NLP"` / `"MINLP"`
174/// / `"QCP"` / `"MIQCP"`), used to scope the `solver` option.
175pub fn write_options(gms: &mut String, o: &GamsOptions, solve_type: &str) {
176    if let Some(d) = o.universal.time_limit {
177        writeln!(gms, "option ResLim = {};", d.as_secs_f64()).unwrap();
178    }
179    if let Some(g) = o.mip_gap {
180        writeln!(gms, "option OptCR = {g};").unwrap();
181    }
182    if let Some(n) = o.universal.threads {
183        writeln!(gms, "option threads = {n};").unwrap();
184    }
185    if let Some(s) = &o.solver {
186        writeln!(gms, "option {solve_type} = {};", s.gams_name()).unwrap();
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use std::time::Duration;
193
194    use oximo_solver::UniversalOptionsExt;
195
196    use super::*;
197
198    #[test]
199    fn builder_sets_fields() {
200        use crate::solver_options::{GamsBaronOptions, GamsSolverConfig};
201        let o = GamsOptions::default()
202            .time_limit(Duration::from_secs(45))
203            .threads(2)
204            .mip_gap(0.001)
205            .verbose(true)
206            .solver(GamsSolverConfig::Baron(GamsBaronOptions::default()))
207            .gams_path("/opt/gams/gams");
208        assert_eq!(o.universal.time_limit, Some(Duration::from_secs(45)));
209        assert_eq!(o.universal.threads, Some(2));
210        assert_eq!(o.mip_gap, Some(0.001));
211        assert!(matches!(o.solver, Some(GamsSolverConfig::Baron(_))));
212        assert_eq!(o.gams_path.as_deref(), Some(std::path::Path::new("/opt/gams/gams")));
213    }
214
215    #[test]
216    fn write_options_emits_solver_baron() {
217        use crate::solver_options::{GamsBaronOptions, GamsSolverConfig};
218        let o = GamsOptions::default().solver(GamsSolverConfig::Baron(GamsBaronOptions::default()));
219        let mut gms = String::new();
220        write_options(&mut gms, &o, "MIP");
221        assert!(gms.contains("option MIP = BARON;"), "got: {gms}");
222    }
223
224    #[test]
225    fn write_options_emits_custom_solver_verbatim() {
226        let o = GamsOptions::default().solver(GamsSolver::Custom("MOSEK".into()));
227        let mut gms = String::new();
228        write_options(&mut gms, &o, "LP");
229        assert!(gms.contains("option LP = MOSEK;"), "got: {gms}");
230    }
231
232    #[test]
233    fn write_options_emits_time_and_gap() {
234        let o = GamsOptions::default().time_limit(Duration::from_secs(10)).mip_gap(0.05).threads(4);
235        let mut gms = String::new();
236        write_options(&mut gms, &o, "MIP");
237        assert!(gms.contains("option ResLim = 10"));
238        assert!(gms.contains("option OptCR = 0.05"));
239        assert!(gms.contains("option threads = 4"));
240    }
241
242    #[test]
243    fn write_options_empty_for_default() {
244        let o = GamsOptions::default();
245        let mut gms = String::new();
246        write_options(&mut gms, &o, "LP");
247        assert!(gms.is_empty());
248    }
249}