ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
CplexLinearModelExecutor.cs
Go to the documentation of this file.
1using System.Diagnostics;
3
5
6/// <summary>
7/// Executes portable LP/MILP models through the stand-alone CPLEX optimizer.
8/// </summary>
9/// <remarks>
10/// Availability is still validated by the dynamic CPLEX runtime adapter. The
11/// execution backend then invokes the cplex command-line program from the same
12/// runtime directory and parses its XML .sol file.
13/// </remarks>
14public sealed class CplexLinearModelExecutor :
16{
17 private readonly ExternalSolverProcessRunner _runner =
18 new();
19
20 /// <inheritdoc />
22 SolverKind.Cplex;
23
24 /// <inheritdoc />
25 public async ValueTask<LinearModelSolveResult> SolveAsync(
26 LinearModel model,
27 SolverSelectionResult selection,
29 CancellationToken cancellationToken = default)
30 {
31 ArgumentNullException.ThrowIfNull(model);
32 ArgumentNullException.ThrowIfNull(selection);
33 ArgumentNullException.ThrowIfNull(options);
34
35 if (selection.SelectedSolver != SolverKind.Cplex ||
36 selection.Availability is null)
37 {
38 throw new ArgumentException(
39 "The supplied selection does not target CPLEX.",
40 nameof(selection));
41 }
42
43 string executable =
44 ResolveCplexExecutable(
45 selection.Availability);
46
47 if (string.IsNullOrWhiteSpace(executable))
48 {
49 return new LinearModelSolveResult(
50 model.Name,
52 new SolverExecutionInfo(selection),
53 null,
54 null,
55 TimeSpan.Zero,
56 "cplex executable unavailable",
57 [
58 "CPLEX runtime discovery succeeded, but the stand-alone " +
59 "cplex executable was not found in the selected runtime " +
60 "directory."
61 ]);
62 }
63
64 string artifacts =
67 options);
68
69 var stopwatch =
70 Stopwatch.StartNew();
71
72 try
73 {
74 string modelPath =
75 Path.Combine(
76 artifacts,
77 "model.lp");
78
79 string solutionPath =
80 Path.Combine(
81 artifacts,
82 "solution.sol");
83
85 model,
86 modelPath);
87
89 modelPath,
90 options);
91
92 string commands =
93 string.Join(
94 Environment.NewLine,
95 [
96 $"read {Quote(modelPath)}",
97 "optimize",
98 $"write {Quote(solutionPath)}",
99 "quit",
100 string.Empty
101 ]);
102
104 await _runner.RunAsync(
105 executable,
106 [],
107 artifacts,
108 commands,
109 cancellationToken);
110
111 stopwatch.Stop();
112
113 string output =
114 process.CombinedOutput;
115
116 CplexXmlSolution solution =
118 solutionPath);
119
120 bool solutionExists =
121 File.Exists(solutionPath);
122
124 MapStatus(
125 solution.Status,
126 output,
127 solutionExists,
128 process.ExitCode);
129
131 model,
132 selection,
133 options,
134 status,
135 solution.Values,
136 solutionExists,
137 stopwatch.Elapsed,
138 !string.IsNullOrWhiteSpace(solution.Status)
139 ? solution.Status
141 [
142 $"cplex exit code: {process.ExitCode}.",
143 !string.IsNullOrWhiteSpace(solution.Status)
144 ? $"CPLEX solution status: {solution.Status}."
146 ],
147 artifacts);
148 }
149 catch (OperationCanceledException)
150 {
151 stopwatch.Stop();
152
153 return new LinearModelSolveResult(
154 model.Name,
155 LinearModelSolveStatus.Cancelled,
156 new SolverExecutionInfo(selection),
157 null,
158 null,
159 stopwatch.Elapsed,
160 "Cancelled",
161 ["CPLEX execution was cancelled by the caller."],
162 options.KeepTemporaryFiles
163 ? artifacts
164 : string.Empty);
165 }
166 catch (Exception exception)
167 {
168 stopwatch.Stop();
169
170 return new LinearModelSolveResult(
171 model.Name,
173 new SolverExecutionInfo(selection),
174 null,
175 null,
176 stopwatch.Elapsed,
177 exception.GetType().Name,
178 [exception.Message],
179 options.KeepTemporaryFiles
180 ? artifacts
181 : string.Empty);
182 }
183 finally
184 {
186 artifacts,
187 options);
188 }
189 }
190
191 private static string ResolveCplexExecutable(
192 SolverAvailabilityInfo availability)
193 {
194 string directory =
195 availability.NativeLibraryPath;
196
197 if (string.IsNullOrWhiteSpace(directory))
198 {
199 directory =
200 Path.GetDirectoryName(
201 availability.ManagedAssemblyPath) ??
202 string.Empty;
203 }
204
205 if (string.IsNullOrWhiteSpace(directory))
206 {
207 return string.Empty;
208 }
209
210 string[] candidates =
211 OperatingSystem.IsWindows()
212 ? ["cplex.exe", "cplex"]
213 : ["cplex", "cplex.bin"];
214
215 foreach (string name in candidates)
216 {
217 string path =
218 Path.Combine(
219 directory,
220 name);
221
222 if (File.Exists(path))
223 {
224 return Path.GetFullPath(path);
225 }
226 }
227
228 return string.Empty;
229 }
230
231 private static string Quote(
232 string path) =>
233 "\"" +
234 path.Replace(
235 "\"",
236 "\"\"",
237 StringComparison.Ordinal) +
238 "\"";
239
240 private static LinearModelSolveStatus MapStatus(
241 string nativeStatus,
242 string output,
243 bool solutionExists,
244 int exitCode)
245 {
246 // The status stored in the CPLEX XML solution is authoritative.
247 // Console logs can legitimately contain words such as "infeasible"
248 // while reporting presolve statistics, MIP starts, node information,
249 // or intermediate incumbents. Searching the complete console output
250 // before the native status can therefore misclassify a valid solution.
251 if (!string.IsNullOrWhiteSpace(nativeStatus))
252 {
253 string native =
254 nativeStatus.Trim();
255
256 if (native.Contains(
257 "infeasible or unbounded",
258 StringComparison.OrdinalIgnoreCase))
259 {
260 return LinearModelSolveStatus.InfeasibleOrUnbounded;
261 }
262
263 // CPXMIP_OPTIMAL_TOL / "integer optimal, tolerance":
264 // feasible incumbent accepted within MIP gap tolerances, but
265 // global optimality is not proved.
266 if (native.Contains(
267 "optimal, tolerance",
268 StringComparison.OrdinalIgnoreCase) ||
269 native.Contains(
270 "optimal within tolerance",
271 StringComparison.OrdinalIgnoreCase))
272 {
273 return LinearModelSolveStatus.Feasible;
274 }
275
276 if (native.Contains(
277 "optimal",
278 StringComparison.OrdinalIgnoreCase))
279 {
280 return LinearModelSolveStatus.Optimal;
281 }
282
283 if (native.Contains(
284 "infeasible",
285 StringComparison.OrdinalIgnoreCase))
286 {
287 return LinearModelSolveStatus.Infeasible;
288 }
289
290 if (native.Contains(
291 "unbounded",
292 StringComparison.OrdinalIgnoreCase))
293 {
294 return LinearModelSolveStatus.Unbounded;
295 }
296 }
297
298 // Fallback only when the XML/native status is absent or unrecognized.
299 if (output.Contains(
300 "infeasible or unbounded",
301 StringComparison.OrdinalIgnoreCase))
302 {
303 return LinearModelSolveStatus.InfeasibleOrUnbounded;
304 }
305
306 if (output.Contains(
307 "optimal, tolerance",
308 StringComparison.OrdinalIgnoreCase) ||
309 output.Contains(
310 "optimal within tolerance",
311 StringComparison.OrdinalIgnoreCase))
312 {
313 return solutionExists
314 ? LinearModelSolveStatus.Feasible
315 : LinearModelSolveStatus.Unknown;
316 }
317
318 if (output.Contains(
319 "optimal",
320 StringComparison.OrdinalIgnoreCase))
321 {
322 return LinearModelSolveStatus.Optimal;
323 }
324
325 if (output.Contains(
326 "infeasible",
327 StringComparison.OrdinalIgnoreCase))
328 {
329 return LinearModelSolveStatus.Infeasible;
330 }
331
332 if (output.Contains(
333 "unbounded",
334 StringComparison.OrdinalIgnoreCase))
335 {
336 return LinearModelSolveStatus.Unbounded;
337 }
338
339 if (solutionExists)
340 {
341 return LinearModelSolveStatus.Feasible;
342 }
343
344 return exitCode == 0
345 ? LinearModelSolveStatus.Unknown
346 : LinearModelSolveStatus.Failed;
347 }
348}
Parses CPLEX XML .sol files written by the stand-alone CPLEX optimizer.
Runs a solver command-line process with cancellation and captured output.
Configures one solver-backed execution of a portable linear model.
bool KeepTemporaryFiles
Gets or sets whether temporary model/solution/log artifacts are retained.
Result of executing a solver-independent linear or mixed-integer model.
Writes a conservative LP representation understood by all four supported execution backends.
void Write(LinearModel model, string path)
Writes the supplied model to an LP file.
Executes portable LP/MILP models through the stand-alone CPLEX optimizer.
async ValueTask< LinearModelSolveResult > SolveAsync(LinearModel model, SolverSelectionResult selection, LinearModelSolveOptions options, CancellationToken cancellationToken=default)
Executes one portable model using an already selected solver.
SolverKind SolverKind
Gets the concrete solver implemented by this executor.
static LinearModelSolveResult BuildSolutionResult(LinearModel model, SolverSelectionResult selection, LinearModelSolveOptions options, LinearModelSolveStatus proposedStatus, IReadOnlyDictionary< int, double > returnedValues, bool hasCandidateSolution, TimeSpan duration, string nativeStatus, IEnumerable< string > diagnostics, string artifactDirectory)
static string CreateArtifactDirectory(SolverKind solverKind, LinearModelSolveOptions options)
static void DeleteArtifactsUnlessRetained(string directory, LinearModelSolveOptions options)
static void ExportModelIfRequested(string generatedModelPath, LinearModelSolveOptions options)
Immutable solver-independent linear or mixed-integer linear model.
Definition LinearModel.cs:7
Describes solver availability and the installation selected by an adapter.
string NativeLibraryPath
Gets the native library or executable path, when applicable.
string ManagedAssemblyPath
Gets the managed assembly path, when applicable.
Serializable-style immutable snapshot of the solver selected for one solver-backed ULS execution.
Describes the outcome of optimization-solver selection.
SolverKind SelectedSolver
Gets the selected solver kind, or Unknown when none was selected.
SolverAvailabilityInfo? Availability
Gets machine-availability information for the selected solver.
Executes the portable LinearModel with one concrete solver.
LinearModelSolveStatus
Describes the termination state of a solver-backed portable linear model.
record CplexXmlSolution(string Status, IReadOnlyDictionary< int, double > Values)
record ExternalSolverProcessResult(int ExitCode, string StandardOutput, string StandardError)