LotSizingDataModel.Solver.Cplex 2.0.1
IBM ILOG CPLEX adapter and installation discovery integration.
Loading...
Searching...
No Matches
CplexSolverAdapter.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.IO;
5using System.Threading;
6using System.Threading.Tasks;
7using ILOG.CPLEX;
8using NativeCplex = global::ILOG.CPLEX.Cplex;
9using LotSizingDataModel.Solver.Adapters;
10using LotSizingDataModel.Solver.Common;
11using LotSizingDataModel.Solver.Execution;
12using LotSizingDataModel.Solver.Modeling;
13using LotSizingDataModel.Solver.Monitoring;
14
16
17/// <summary>
18/// Solves generic linear and mixed-integer linear models with
19/// IBM ILOG CPLEX.
20/// </summary>
21public sealed class CplexSolverAdapter :
22 MathematicalModelSolverPluginBase
23{
24 private readonly object _nativeSyncRoot =
25 new();
26
27 private NativeCplex.Aborter? _activeAborter;
28
29 /// <summary>
30 /// Initializes the CPLEX solver adapter.
31 /// </summary>
33 : base(
34 [
35 SolverCapability.LinearProgramming,
36 SolverCapability.MixedIntegerLinearProgramming,
37 SolverCapability.Interruption,
38 SolverCapability.LpExport,
39 SolverCapability.MpsExport,
40 SolverCapability.OptimalityGapReporting,
41 SolverCapability.SearchStatistics
42 ])
43 {
44 }
45
46 /// <summary>
47 /// Gets the generic solver kind.
48 /// </summary>
49 public override SolverKind SolverKind =>
50 SolverKind.Cplex;
51
52 /// <summary>
53 /// Gets the solver display name.
54 /// </summary>
55 public override string SolverName =>
56 "IBM ILOG CPLEX";
57
58 /// <summary>
59 /// Gets the native solver version when available.
60 /// </summary>
61 public override string SolverVersion
62 {
63 get
64 {
65 try
66 {
67 var cplex =
68 new NativeCplex();
69
70 try
71 {
72 return cplex.Version;
73 }
74 finally
75 {
76 cplex.End();
77 }
78 }
79 catch
80 {
81 return string.Empty;
82 }
83 }
84 }
85
86 /// <summary>
87 /// Gets the unique adapter identifier.
88 /// </summary>
89 public override string AdapterId =>
90 "LotSizingDataModel.Solver.Cplex";
91
92 /// <summary>
93 /// Gets the adapter display name.
94 /// </summary>
95 public override string AdapterName =>
96 "LotSizingDataModel CPLEX Adapter";
97
98 /// <summary>
99 /// Gets the adapter implementation version.
100 /// </summary>
101 public override string AdapterVersion =>
102 "1.0.0";
103
104 /// <summary>
105 /// Gets the minimum supported CPLEX version.
106 /// </summary>
107 public override string MinimumSupportedSolverVersion =>
108 "22.1.1";
109
110 /// <summary>
111 /// Checks whether CPLEX can be instantiated and therefore
112 /// loaded by the current process.
113 /// </summary>
114 public override ValueTask<SolverAvailabilityInfo>
116 CancellationToken cancellationToken = default)
117 {
118 cancellationToken.ThrowIfCancellationRequested();
119
120 CplexInstallationDiscoveryResult installationDiscovery =
122
123 try
124 {
125 var cplex =
126 new NativeCplex();
127
128 try
129 {
130 var availability =
131 new SolverAvailabilityInfo(
132 SolverKind.Cplex,
133 SolverAvailabilityStatus.Available)
134 {
135 SolverName =
137
139 cplex.Version
140 };
141
142 if (installationDiscovery.Installation is not null)
143 {
144 availability.InstallationPath =
145 installationDiscovery.Installation.RootDirectory;
146
147 availability.ManagedAssemblyPath =
148 installationDiscovery.Installation
149 .ManagedAssemblyDirectory;
150
151 availability.AddDiagnostic(
152 $"Compatible CPLEX installation family " +
153 $"'{installationDiscovery.Installation.Version}' " +
154 $"was discovered from " +
155 $"'{installationDiscovery.Installation.DiscoverySource}'.");
156 }
157
158 foreach (
159 string diagnostic
160 in installationDiscovery.Diagnostics)
161 {
162 availability.AddDiagnostic(
163 diagnostic);
164 }
165
166 availability.AddDiagnostic(
167 "The CPLEX managed and native runtime " +
168 "libraries were loaded successfully.");
169
170 return ValueTask.FromResult(
171 availability);
172 }
173 finally
174 {
175 cplex.End();
176 }
177 }
178 catch (Exception exception)
179 {
180 SolverAvailabilityStatus status =
181 installationDiscovery.IsFound
182 ? SolverAvailabilityStatus.LoadFailure
183 : SolverAvailabilityStatus.NotInstalled;
184
185 var availability =
186 new SolverAvailabilityInfo(
187 SolverKind.Cplex,
188 status)
189 {
190 SolverName =
192 };
193
194 if (installationDiscovery.Installation is not null)
195 {
196 availability.InstallationPath =
197 installationDiscovery.Installation.RootDirectory;
198
199 availability.ManagedAssemblyPath =
200 installationDiscovery.Installation
201 .ManagedAssemblyDirectory;
202 }
203
204 foreach (
205 string diagnostic
206 in installationDiscovery.Diagnostics)
207 {
208 availability.AddDiagnostic(
209 diagnostic);
210 }
211
212 availability.AddDiagnostic(
213 exception.Message);
214
215 return ValueTask.FromResult(
216 availability);
217 }
218 }
219
220 /// <summary>
221 /// Translates and solves one generic mathematical model.
222 /// </summary>
223 protected override async ValueTask<MathematicalModelSolveResult>
225 MathematicalModelSolveRequest request,
226 CancellationToken cancellationToken)
227 {
228 ArgumentNullException.ThrowIfNull(request);
229 ArgumentNullException.ThrowIfNull(request.Model);
230
231 var stopwatch =
232 Stopwatch.StartNew();
233
234 await PublishProgressAsync(
235 request,
236 new SolverProgressSnapshot
237 {
238 Stage =
239 SolverProgressStage.BuildingModel,
240
241 ElapsedSeconds =
242 stopwatch.Elapsed.TotalSeconds,
243
244 Message =
245 "Translating the generic mathematical model " +
246 "to IBM ILOG Concert."
247 },
248 cancellationToken);
249
250 var translator =
252
253 CplexModelTranslationResult translation =
254 translator.Translate(
255 request.Model);
256
257 NativeCplex cplex =
258 translation.Cplex;
259
260 var aborter =
261 new NativeCplex.Aborter();
262
263 cplex.Use(
264 aborter);
265
266 SetActiveAborter(
267 aborter);
268
269 try
270 {
271 var parameterMapper =
273
274 IReadOnlyList<string> parameterDiagnostics =
275 parameterMapper.Apply(
276 cplex,
277 request.Parameters);
278
279 if (request.Parameters.ExportModel)
280 {
281 string path =
282 ResolveExportPath(
283 request.Parameters.ExportModelPath);
284
285 cplex.ExportModel(
286 path);
287 }
288
289 using CancellationTokenRegistration cancellationRegistration =
290 cancellationToken.Register(
291 static state =>
292 {
293 ((NativeCplex.Aborter)state!).Abort();
294 },
295 aborter);
296
297 await PublishProgressAsync(
298 request,
299 new SolverProgressSnapshot
300 {
301 Stage =
302 SolverProgressStage.Searching,
303
304 ElapsedSeconds =
305 stopwatch.Elapsed.TotalSeconds,
306
307 Message =
308 "CPLEX optimization started."
309 },
310 cancellationToken);
311
312 bool hasSolution =
313 await Task.Run(
314 () => cplex.Solve(),
315 CancellationToken.None);
316
317 stopwatch.Stop();
318
319 var result =
320 BuildResult(
321 request.Model,
322 translation,
323 cplex,
324 hasSolution,
325 stopwatch.Elapsed,
326 cancellationToken.IsCancellationRequested);
327
328 foreach (string diagnostic in parameterDiagnostics)
329 {
330 result.AddDiagnostic(
331 diagnostic);
332 }
333
334 await PublishProgressAsync(
335 request,
336 new SolverProgressSnapshot
337 {
338 Stage =
339 SolverProgressStage.Completed,
340
341 ElapsedSeconds =
342 stopwatch.Elapsed.TotalSeconds,
343
344 IncumbentObjective =
345 result.ObjectiveValue,
346
347 BestBound =
348 result.BestBound,
349
350 AbsoluteGap =
351 result.AbsoluteGap,
352
353 RelativeGap =
354 result.RelativeGap,
355
356 ExploredNodeCount =
357 result.ExploredNodeCount,
358
359 IterationCount =
360 result.IterationCount,
361
362 Message =
363 $"CPLEX terminated with reason " +
364 $"'{result.TerminationReason}'."
365 },
366 CancellationToken.None);
367
368 return result;
369 }
370 finally
371 {
372 ClearActiveAborter(
373 aborter);
374
375 try
376 {
377 cplex.Remove(
378 aborter);
379 }
380 catch
381 {
382 // Model disposal remains the priority.
383 }
384
385 aborter.End();
386 cplex.End();
387 }
388 }
389
390 /// <summary>
391 /// Requests native CPLEX interruption.
392 /// </summary>
393 protected override void RequestNativeStop()
394 {
395 NativeCplex.Aborter? aborter;
396
397 lock (_nativeSyncRoot)
398 {
399 aborter =
400 _activeAborter;
401 }
402
403 aborter?.Abort();
404 }
405
406 private static MathematicalModelSolveResult BuildResult(
407 MathematicalModel model,
408 CplexModelTranslationResult translation,
409 NativeCplex cplex,
410 bool hasSolution,
411 TimeSpan solveDuration,
412 bool cancellationRequested)
413 {
414 var result =
415 new MathematicalModelSolveResult
416 {
417 SolverKind =
418 SolverKind.Cplex,
419
420 SolverName =
421 "IBM ILOG CPLEX",
422
424 cplex.Version,
425
426 SolveDuration =
427 solveDuration,
428
429 HasFeasibleSolution =
430 hasSolution,
431
432 IsOptimal =
433 string.Equals(
434 cplex.GetStatus().ToString(),
435 "Optimal",
436 StringComparison.OrdinalIgnoreCase),
437
438 TerminationReason =
439 MapTerminationReason(
440 cplex.GetStatus().ToString(),
441 cplex.GetCplexStatus().ToString(),
442 hasSolution,
443 cancellationRequested),
444
445 ExploredNodeCount =
446 cplex.Nnodes64,
447
448 IterationCount =
449 cplex.Niterations64
450 };
451
452 if (hasSolution)
453 {
454 result.ObjectiveValue =
455 cplex.ObjValue;
456
457 foreach (MathematicalVariable variable in model.Variables)
458 {
459 double value =
460 cplex.GetValue(
461 translation.GetVariable(
462 variable.Id));
463
464 result.AddVariableValue(
465 new MathematicalVariableValue(
466 variable.Id,
467 value,
468 variable.Name,
469 variable.DomainKey));
470 }
471
472 TryPopulateMipStatistics(
473 result,
474 cplex);
475 }
476
477 result.AddDiagnostic(
478 $"CPLEX status: {cplex.GetStatus()}.");
479
480 result.AddDiagnostic(
481 $"CPLEX detailed status: {cplex.GetCplexStatus()}.");
482
483 return result;
484 }
485
486 private static void TryPopulateMipStatistics(
487 MathematicalModelSolveResult result,
488 NativeCplex cplex)
489 {
490 try
491 {
492 result.BestBound =
493 cplex.GetBestObjValue();
494
495 if (result.ObjectiveValue.HasValue &&
496 result.BestBound.HasValue)
497 {
498 result.AbsoluteGap =
499 Math.Abs(
500 result.ObjectiveValue.Value -
501 result.BestBound.Value);
502
503 result.RelativeGap =
504 result.AbsoluteGap.Value /
505 (1e-10 +
506 Math.Abs(
507 result.ObjectiveValue.Value));
508 }
509 }
510 catch
511 {
512 // LPs and some termination states may not expose
513 // MIP-specific bound information.
514 }
515 }
516
517 private static SolverTerminationReason MapTerminationReason(
518 string status,
519 string subStatus,
520 bool hasSolution,
521 bool cancellationRequested)
522 {
523 if (cancellationRequested)
524 {
525 return SolverTerminationReason.UserInterrupted;
526 }
527
528 if (status.Equals(
529 "Optimal",
530 StringComparison.OrdinalIgnoreCase))
531 {
532 return SolverTerminationReason.Optimal;
533 }
534
535 if (status.Equals(
536 "Infeasible",
537 StringComparison.OrdinalIgnoreCase))
538 {
539 return SolverTerminationReason.Infeasible;
540 }
541
542 if (status.Equals(
543 "Unbounded",
544 StringComparison.OrdinalIgnoreCase))
545 {
546 return SolverTerminationReason.Unbounded;
547 }
548
549 if (status.Contains(
550 "InfeasibleOrUnbounded",
551 StringComparison.OrdinalIgnoreCase))
552 {
553 return SolverTerminationReason.InfeasibleOrUnbounded;
554 }
555
556 if (subStatus.Contains(
557 "TimeLim",
558 StringComparison.OrdinalIgnoreCase))
559 {
560 return SolverTerminationReason.TimeLimit;
561 }
562
563 if (subStatus.Contains(
564 "NodeLim",
565 StringComparison.OrdinalIgnoreCase))
566 {
567 return SolverTerminationReason.NodeLimit;
568 }
569
570 if (subStatus.Contains(
571 "ItLim",
572 StringComparison.OrdinalIgnoreCase))
573 {
574 return SolverTerminationReason.IterationLimit;
575 }
576
577 if (subStatus.Contains(
578 "SolLim",
579 StringComparison.OrdinalIgnoreCase))
580 {
581 return SolverTerminationReason.SolutionLimit;
582 }
583
584 return hasSolution
585 ? SolverTerminationReason.Feasible
586 : SolverTerminationReason.Unknown;
587 }
588
589 private static string ResolveExportPath(
590 string configuredPath)
591 {
592 if (string.IsNullOrWhiteSpace(
593 configuredPath))
594 {
595 return Path.GetFullPath(
596 "LotSizingModel.lp");
597 }
598
599 string path =
600 Path.GetFullPath(
601 configuredPath);
602
603 string? directory =
604 Path.GetDirectoryName(
605 path);
606
607 if (!string.IsNullOrWhiteSpace(
608 directory))
609 {
610 Directory.CreateDirectory(
611 directory);
612 }
613
614 return path;
615 }
616
617 private void SetActiveAborter(
618 NativeCplex.Aborter aborter)
619 {
620 lock (_nativeSyncRoot)
621 {
622 _activeAborter =
623 aborter;
624 }
625 }
626
627 private void ClearActiveAborter(
628 NativeCplex.Aborter aborter)
629 {
630 lock (_nativeSyncRoot)
631 {
632 if (ReferenceEquals(
633 _activeAborter,
634 aborter))
635 {
636 _activeAborter =
637 null;
638 }
639 }
640 }
641}
global::ILOG.CPLEX.Cplex NativeCplex
Represents the result of compatible CPLEX installation discovery.
CplexInstallationInfo? Installation
Gets the selected compatible installation.
string RootDirectory
Gets the CPLEX Studio root directory.
Describes and discovers supported IBM ILOG CPLEX installations.
static CplexInstallationDiscoveryResult Discover()
Finds the newest compatible CPLEX installation visible to the current process.
Stores the native CPLEX model objects created from one solver-independent mathematical model.
INumVar GetVariable(int variableId)
Resolves a native variable by generic mathematical identifier.
Translates a solver-independent linear mathematical model to IBM ILOG Concert/CPLEX objects.
Maps normalized solver parameters to stable CPLEX parameters.
override string MinimumSupportedSolverVersion
Gets the minimum supported CPLEX version.
override SolverKind SolverKind
Gets the generic solver kind.
override string AdapterName
Gets the adapter display name.
override string AdapterVersion
Gets the adapter implementation version.
override string SolverVersion
Gets the native solver version when available.
override async ValueTask< MathematicalModelSolveResult > SolveCoreAsync(MathematicalModelSolveRequest request, CancellationToken cancellationToken)
Translates and solves one generic mathematical model.
override ValueTask< SolverAvailabilityInfo > CheckAvailabilityAsync(CancellationToken cancellationToken=default)
Checks whether CPLEX can be instantiated and therefore loaded by the current process.
override string SolverName
Gets the solver display name.
CplexSolverAdapter()
Initializes the CPLEX solver adapter.
override void RequestNativeStop()
Requests native CPLEX interruption.
override string AdapterId
Gets the unique adapter identifier.