1use std::fmt::Write as FmtWrite;
2use std::path::PathBuf;
3
4use oximo_solver::{HasUniversal, UniversalOptions};
5
6use crate::solver_options::GamsSolverConfig;
7
8#[derive(Clone, Debug, Default)]
10pub struct GamsOptions {
11 pub universal: UniversalOptions,
12 pub mip_gap: Option<f64>,
13 pub solver: Option<GamsSolverConfig>,
17 pub gams_path: Option<PathBuf>,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq)]
27pub enum GamsSolver {
28 AlphaEcp,
30 Antigone,
32 Baron,
34 Cbc,
36 Conopt,
38 Copt,
40 Cplex,
42 Decis,
44 Dicopt,
46 Glpk,
48 Gurobi,
50 Guss,
52 Highs,
54 Ipopt,
56 Jams,
58 Kestrel,
60 Knitro,
62 Lindo,
64 LindoGlobal,
66 Miles,
68 Minos,
70 Mosek,
72 Nlpec,
74 OdhCplex,
76 Path,
78 QuadMinos,
80 Reshop,
82 Sbb,
84 Scip,
86 Shot,
88 Snopt,
90 Soplex,
92 Xpress,
94 Custom(String),
96}
97
98impl GamsSolver {
99 #[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
171pub 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}