ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
XpressLinearModelExecutor.cs
Go to the documentation of this file.
1using System.Diagnostics;
2using System.Globalization;
3using System.Reflection;
6
8
9/// <summary>
10/// Executes portable LP/MILP models through the optional FICO Xpress
11/// Optimizer .NET runtime loaded by reflection.
12/// </summary>
13public sealed class XpressLinearModelExecutor :
15{
16 private static readonly SemaphoreSlim RuntimeGate =
17 new(1, 1);
18
19 /// <inheritdoc />
21 SolverKind.Xpress;
22
23 /// <inheritdoc />
24 public async ValueTask<LinearModelSolveResult> SolveAsync(
25 LinearModel model,
26 SolverSelectionResult selection,
28 CancellationToken cancellationToken = default)
29 {
30 ArgumentNullException.ThrowIfNull(model);
31 ArgumentNullException.ThrowIfNull(selection);
32 ArgumentNullException.ThrowIfNull(options);
33
34 if (selection.SelectedSolver != SolverKind.Xpress)
35 {
36 throw new ArgumentException(
37 "The supplied selection does not target Xpress.",
38 nameof(selection));
39 }
40
41 string artifacts =
44 options);
45
46 var stopwatch =
47 Stopwatch.StartNew();
48
49 await RuntimeGate.WaitAsync(
50 cancellationToken);
51
52 try
53 {
54 string modelPath =
55 Path.Combine(
56 artifacts,
57 "model.lp");
58
60 model,
61 modelPath);
62
64 modelPath,
65 options);
66
67 Assembly? assembly =
69 out string resolvedPath);
70
71 if (assembly is null)
72 {
73 return new LinearModelSolveResult(
74 model.Name,
76 new SolverExecutionInfo(selection),
77 null,
78 null,
79 stopwatch.Elapsed,
80 "Optimizer.dll unavailable",
81 ["Xpress was selected but Optimizer.dll could not be loaded."]);
82 }
83
84 var api =
86 assembly);
87
88 api.Initialize();
89
90 object? problem =
91 null;
92
93 try
94 {
95 problem =
96 api.CreateProblem();
97
98 api.ReadProblem(
99 problem,
100 modelPath);
101
102 using CancellationTokenRegistration cancellationRegistration =
103 cancellationToken.Register(
104 static state =>
106 state!),
107 problem);
108
109 await Task.Run(
110 () =>
111 api.Optimize(problem),
112 CancellationToken.None);
113
114 stopwatch.Stop();
115
116 double[]? vector =
117 api.TryGetSolution(
118 problem);
119
120 IReadOnlyDictionary<int, double> values =
121 api.GetValues(
122 problem,
123 model,
124 vector,
125 out string mappingDiagnostic);
126
127 bool solutionExists =
128 vector is not null;
129
130 string nativeStatus =
131 api.TryGetStatus(
132 problem);
133
135 MapStatus(
136 nativeStatus,
137 problem,
138 api,
139 solutionExists);
140
141 var diagnostics =
142 new List<string>
143 {
144 $"Xpress runtime: {resolvedPath}."
145 };
146
147 if (!string.IsNullOrWhiteSpace(
148 mappingDiagnostic))
149 {
150 diagnostics.Add(
151 mappingDiagnostic);
152 }
153
154 if (!string.IsNullOrWhiteSpace(
155 nativeStatus))
156 {
157 diagnostics.Add(
158 $"Xpress status: {nativeStatus}.");
159 }
160
162 model,
163 selection,
164 options,
165 status,
166 values,
167 solutionExists,
168 stopwatch.Elapsed,
169 nativeStatus,
170 diagnostics,
171 artifacts);
172 }
173 finally
174 {
175 if (problem is not null)
176 {
177 api.DestroyProblem(
178 problem);
179 }
180
181 api.Free();
182 }
183 }
184 catch (OperationCanceledException)
185 {
186 stopwatch.Stop();
187
188 return new LinearModelSolveResult(
189 model.Name,
190 LinearModelSolveStatus.Cancelled,
191 new SolverExecutionInfo(selection),
192 null,
193 null,
194 stopwatch.Elapsed,
195 "Cancelled",
196 ["Xpress execution was cancelled by the caller."],
197 options.KeepTemporaryFiles
198 ? artifacts
199 : string.Empty);
200 }
201 catch (Exception exception)
202 {
203 stopwatch.Stop();
204
205 Exception effective =
206 exception is TargetInvocationException invocation &&
207 invocation.InnerException is Exception inner
208 ? inner
209 : exception;
210
211 return new LinearModelSolveResult(
212 model.Name,
214 new SolverExecutionInfo(selection),
215 null,
216 null,
217 stopwatch.Elapsed,
218 effective.GetType().Name,
219 [effective.Message],
220 options.KeepTemporaryFiles
221 ? artifacts
222 : string.Empty);
223 }
224 finally
225 {
226 RuntimeGate.Release();
227
229 artifacts,
230 options);
231 }
232 }
233
234 private static LinearModelSolveStatus MapStatus(
235 string status,
236 object problem,
238 bool hasSolution)
239 {
240 if (status.Contains(
241 "infeasible",
242 StringComparison.OrdinalIgnoreCase) &&
243 status.Contains(
244 "unbounded",
245 StringComparison.OrdinalIgnoreCase))
246 {
247 return LinearModelSolveStatus.InfeasibleOrUnbounded;
248 }
249
250 if (status.Contains(
251 "infeasible",
252 StringComparison.OrdinalIgnoreCase))
253 {
254 return LinearModelSolveStatus.Infeasible;
255 }
256
257 if (status.Contains(
258 "unbounded",
259 StringComparison.OrdinalIgnoreCase))
260 {
261 return LinearModelSolveStatus.Unbounded;
262 }
263
264 if (status.Contains(
265 "optimal",
266 StringComparison.OrdinalIgnoreCase))
267 {
268 return LinearModelSolveStatus.Optimal;
269 }
270
271 if (hasSolution)
272 {
273 double? objective =
275 problem,
276 "MIPBestObjVal",
277 "MIPObjVal",
278 "LPObjVal",
279 "ObjVal");
280
281 double? bound =
283 problem,
284 "BestBound",
285 "MIPBestBound");
286
287 if (objective.HasValue &&
288 bound.HasValue &&
289 Math.Abs(
290 objective.Value -
291 bound.Value) <=
292 1.0e-10 *
293 Math.Max(
294 1.0,
295 Math.Abs(objective.Value)))
296 {
297 return LinearModelSolveStatus.Optimal;
298 }
299
300 return LinearModelSolveStatus.Feasible;
301 }
302
303 return LinearModelSolveStatus.Unknown;
304 }
305}
306
307internal sealed class XpressExecutionReflectionApi
308{
309 private readonly Type _xprsType;
310 private readonly Type _problemType;
311
312 private XpressExecutionReflectionApi(
313 Type xprsType,
314 Type problemType)
315 {
316 _xprsType = xprsType;
317 _problemType = problemType;
318 }
319
320 internal static XpressExecutionReflectionApi Create(
321 Assembly assembly)
322 {
323 Type xprsType =
324 assembly.GetType(
325 "Optimizer.XPRS",
326 throwOnError: true,
327 ignoreCase: false)!;
328
329 Type problemType =
330 assembly.GetType(
331 "Optimizer.XPRSprob",
332 throwOnError: true,
333 ignoreCase: false)!;
334
335 return new XpressExecutionReflectionApi(
336 xprsType,
337 problemType);
338 }
339
340 internal void Initialize()
341 {
342 MethodInfo? oneArgument =
343 FindMethod(
344 _xprsType,
345 "Init",
346 isStatic: true,
347 parameterCount: 1);
348
349 if (oneArgument is not null)
350 {
351 oneArgument.Invoke(
352 null,
353 [string.Empty]);
354
355 return;
356 }
357
358 MethodInfo? zeroArgument =
359 FindMethod(
360 _xprsType,
361 "Init",
362 isStatic: true,
363 parameterCount: 0);
364
365 zeroArgument?.Invoke(
366 null,
367 null);
368
369 if (oneArgument is null &&
370 zeroArgument is null)
371 {
372 throw new MissingMethodException(
373 _xprsType.FullName,
374 "Init");
375 }
376 }
377
378 internal void Free()
379 {
380 try
381 {
382 FindMethod(
383 _xprsType,
384 "Free",
385 isStatic: true,
386 parameterCount: 0)?
387 .Invoke(
388 null,
389 null);
390 }
391 catch
392 {
393 // Cleanup must not mask the solve result.
394 }
395 }
396
397 internal object CreateProblem()
398 {
399 return Activator.CreateInstance(
400 _problemType) ??
401 throw new InvalidOperationException(
402 "Unable to create Optimizer.XPRSprob.");
403 }
404
405 internal void ReadProblem(
406 object problem,
407 string modelPath)
408 {
409 TrySetProperty(
410 problem,
411 "MPSFormat",
412 -1);
413
414 InvokeRequired(
415 problem,
416 "ReadProb",
417 modelPath,
418 string.Empty);
419 }
420
421 internal void Optimize(
422 object problem)
423 {
424 InvokeRequired(
425 problem,
426 "Optimize");
427 }
428
429 internal double[]? TryGetSolution(
430 object problem)
431 {
432 try
433 {
434 return InvokeRequired(
435 problem,
436 "GetSolution") as double[];
437 }
438 catch
439 {
440 return null;
441 }
442 }
443
444 internal IReadOnlyDictionary<int, double> GetValues(
445 object problem,
446 LinearModel model,
447 double[]? solution,
448 out string diagnostic)
449 {
450 diagnostic = string.Empty;
451
452 if (solution is null)
453 {
454 return new Dictionary<int, double>();
455 }
456
457 var byName =
458 new Dictionary<int, double>();
459
460 bool allResolved = true;
461
462 foreach (LinearVariable variable in model.Variables)
463 {
464 if (!TryGetColumnIndex(
465 problem,
467 variable.Id),
468 out int index) ||
469 index < 0 ||
470 index >= solution.Length)
471 {
472 allResolved = false;
473 break;
474 }
475
476 byName[variable.Id] =
477 solution[index];
478 }
479
480 if (allResolved &&
481 byName.Count == model.VariableCount)
482 {
483 diagnostic =
484 "Xpress solution values were mapped by portable variable name.";
485
486 return byName;
487 }
488
489 if (solution.Length ==
490 model.VariableCount)
491 {
492 var positional =
493 new Dictionary<int, double>();
494
495 for (int index = 0;
496 index < model.VariableCount;
497 index++)
498 {
499 positional[
500 model.Variables[index].Id] =
501 solution[index];
502 }
503
504 diagnostic =
505 "Xpress used the LP column-order fallback because GetIndex " +
506 "could not map every portable variable name.";
507
508 return positional;
509 }
510
511 diagnostic =
512 "Xpress returned a solution vector that could not be mapped safely.";
513
514 return new Dictionary<int, double>();
515 }
516
517 internal string TryGetStatus(
518 object problem)
519 {
520 return TryGetProperty(
521 problem,
522 "SolStatus",
523 "MIPStatus",
524 "LPStatus")?
525 .ToString() ??
526 string.Empty;
527 }
528
529 internal double? TryGetDoubleProperty(
530 object problem,
531 params string[] names)
532 {
533 object? value =
534 TryGetProperty(
535 problem,
536 names);
537
538 if (value is null)
539 {
540 return null;
541 }
542
543 try
544 {
545 double converted =
546 Convert.ToDouble(
547 value,
548 CultureInfo.InvariantCulture);
549
550 return double.IsFinite(converted)
551 ? converted
552 : null;
553 }
554 catch
555 {
556 return null;
557 }
558 }
559
560 internal void DestroyProblem(
561 object problem)
562 {
563 try
564 {
565 InvokeRequired(
566 problem,
567 "Destroy");
568 }
569 catch
570 {
571 if (problem is IDisposable disposable)
572 {
573 disposable.Dispose();
574 }
575 }
576 }
577
578 internal static void TryInterrupt(
579 object problem)
580 {
581 foreach (string name in
582 new[]
583 {
584 "Interrupt",
585 "Stop"
586 })
587 {
588 try
589 {
590 MethodInfo? method =
591 FindMethod(
592 problem.GetType(),
593 name,
594 isStatic: false,
595 parameterCount: 0);
596
597 if (method is null)
598 {
599 continue;
600 }
601
602 method.Invoke(
603 problem,
604 null);
605
606 return;
607 }
608 catch
609 {
610 // Best-effort interruption.
611 }
612 }
613 }
614
615 private static bool TryGetColumnIndex(
616 object problem,
617 string name,
618 out int index)
619 {
620 index = -1;
621
622 MethodInfo? twoParameter =
623 FindMethod(
624 problem.GetType(),
625 "GetIndex",
626 isStatic: false,
627 parameterCount: 2);
628
629 if (twoParameter is not null)
630 {
631 try
632 {
633 object? value =
634 twoParameter.Invoke(
635 problem,
636 [2, name]);
637
638 if (value is not null)
639 {
640 index =
641 Convert.ToInt32(
642 value,
643 CultureInfo.InvariantCulture);
644
645 return index >= 0;
646 }
647 }
648 catch
649 {
650 // Try out-parameter shape.
651 }
652 }
653
654 MethodInfo? threeParameter =
655 FindMethod(
656 problem.GetType(),
657 "GetIndex",
658 isStatic: false,
659 parameterCount: 3);
660
661 if (threeParameter is not null)
662 {
663 try
664 {
665 object?[] arguments =
666 [2, name, 0];
667
668 threeParameter.Invoke(
669 problem,
670 arguments);
671
672 index =
673 Convert.ToInt32(
674 arguments[2],
675 CultureInfo.InvariantCulture);
676
677 return index >= 0;
678 }
679 catch
680 {
681 // No safe named mapping.
682 }
683 }
684
685 return false;
686 }
687
688 private static object? TryGetProperty(
689 object target,
690 params string[] names)
691 {
692 foreach (string name in names)
693 {
694 PropertyInfo? property =
695 target.GetType().GetProperty(
696 name,
697 BindingFlags.Instance |
698 BindingFlags.Public |
699 BindingFlags.IgnoreCase);
700
701 if (property is not null)
702 {
703 return property.GetValue(
704 target);
705 }
706 }
707
708 return null;
709 }
710
711 private static bool TrySetProperty(
712 object target,
713 string name,
714 object value)
715 {
716 PropertyInfo? property =
717 target.GetType().GetProperty(
718 name,
719 BindingFlags.Instance |
720 BindingFlags.Public |
721 BindingFlags.IgnoreCase);
722
723 if (property is null ||
724 !property.CanWrite)
725 {
726 return false;
727 }
728
729 try
730 {
731 object converted =
732 Convert.ChangeType(
733 value,
734 property.PropertyType,
735 CultureInfo.InvariantCulture);
736
737 property.SetValue(
738 target,
739 converted);
740
741 return true;
742 }
743 catch
744 {
745 return false;
746 }
747 }
748
749 private static object? InvokeRequired(
750 object target,
751 string name,
752 params object?[] arguments)
753 {
754 MethodInfo method =
755 FindMethod(
756 target.GetType(),
757 name,
758 isStatic: false,
759 parameterCount: arguments.Length) ??
760 throw new MissingMethodException(
761 target.GetType().FullName,
762 name);
763
764 return method.Invoke(
765 target,
766 arguments);
767 }
768
769 private static MethodInfo? FindMethod(
770 Type type,
771 string name,
772 bool isStatic,
773 int parameterCount)
774 {
775 BindingFlags flags =
776 BindingFlags.Public |
777 (isStatic
778 ? BindingFlags.Static
779 : BindingFlags.Instance);
780
781 return type
782 .GetMethods(flags)
783 .FirstOrDefault(
784 method =>
785 string.Equals(
786 method.Name,
787 name,
788 StringComparison.OrdinalIgnoreCase) &&
789 method.GetParameters().Length ==
790 parameterCount);
791 }
792}
Locates and loads the optional FICO Xpress Optimizer managed assembly.
static ? Assembly TryLoad(out string resolvedPath)
Loads an already loaded Optimizer assembly, an explicitly located assembly, or an assembly resolvable...
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.
static string GetVariableName(int variableId)
Gets the stable portable variable name.
IReadOnlyDictionary< int, double > GetValues(object problem, LinearModel model, double[]? solution, out string diagnostic)
Executes portable LP/MILP models through the optional FICO Xpress Optimizer .NET runtime loaded by re...
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 one variable in a portable linear mathematical model.
int Id
Gets the stable zero-based variable identifier.
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.
Executes the portable LinearModel with one concrete solver.
LinearModelSolveStatus
Describes the termination state of a solver-backed portable linear model.