LotSizingDataModel.Solver.Xpress 2.0.1
Xpress solver adapter for the LotSizingDataModel solver abstraction.
Loading...
Searching...
No Matches
XpressSolverAdapter.cs
Go to the documentation of this file.
1using System.Diagnostics;
2using System.Globalization;
3using System.Reflection;
4using LotSizingDataModel.Solver.Adapters;
5using LotSizingDataModel.Solver.Common;
6using LotSizingDataModel.Solver.Configuration;
7using LotSizingDataModel.Solver.Execution;
8using LotSizingDataModel.Solver.External;
9using LotSizingDataModel.Solver.Modeling;
10using LotSizingDataModel.Solver.Monitoring;
11
13
14/// <summary>
15/// Solves generic linear and mixed-integer linear models with FICO Xpress MP.
16/// </summary>
17/// <remarks>
18/// <para>
19/// The official Xpress .NET Optimizer assembly is loaded at runtime through
20/// reflection. This keeps Xpress optional: the project itself compiles when no
21/// Xpress installation is present.
22/// </para>
23/// <para>
24/// The adapter uses the long-standing <c>Optimizer.XPRS</c> and
25/// <c>Optimizer.XPRSprob</c> API surface (Init, ReadProb, Optimize,
26/// GetSolution, Destroy and Free).
27/// </para>
28/// </remarks>
29public sealed class XpressSolverAdapter :
30 MathematicalModelSolverPluginBase
31{
32 private static readonly SemaphoreSlim RuntimeGate =
33 new(1, 1);
34
35 private readonly object _nativeSyncRoot =
36 new();
37
38 private object? _activeProblem;
39
40 private string _cachedVersion =
41 string.Empty;
42
43 /// <summary>
44 /// Initializes the FICO Xpress MP adapter.
45 /// </summary>
47 : base(
48 [
49 SolverCapability.LinearProgramming,
50 SolverCapability.MixedIntegerLinearProgramming,
51 SolverCapability.LpExport,
52 SolverCapability.OptimalityGapReporting,
53 SolverCapability.SearchStatistics
54 ])
55 {
56 }
57
58 /// <inheritdoc />
59 public override SolverKind SolverKind =>
60 SolverKind.Xpress;
61
62 /// <inheritdoc />
63 public override string SolverName =>
64 "FICO Xpress MP";
65
66 /// <inheritdoc />
67 public override string SolverVersion =>
68 _cachedVersion;
69
70 /// <inheritdoc />
71 public override string AdapterId =>
72 "LotSizingDataModel.Solver.Xpress";
73
74 /// <inheritdoc />
75 public override string AdapterName =>
76 "LotSizingDataModel Xpress Adapter";
77
78 /// <inheritdoc />
79 public override string AdapterVersion =>
80 "1.0.0";
81
82 /// <inheritdoc />
83 public override string MinimumSupportedSolverVersion =>
84 "9.0";
85
86 /// <inheritdoc />
87 public override async ValueTask<SolverAvailabilityInfo>
89 CancellationToken cancellationToken = default)
90 {
91 cancellationToken.ThrowIfCancellationRequested();
92
93 Assembly? assembly;
94 string resolvedPath;
95
96 try
97 {
98 assembly =
100 out resolvedPath);
101 }
102 catch (Exception exception)
103 {
104 return CreateFailureAvailability(
105 exception.Message);
106 }
107
108 if (assembly is null)
109 {
110 var notInstalled =
111 new SolverAvailabilityInfo(
112 SolverKind.Xpress,
113 SolverAvailabilityStatus.NotInstalled)
114 {
116 };
117
118 notInstalled.AddDiagnostic(
119 "The FICO Xpress Optimizer .NET assembly was not found. " +
120 "Define XPRESSDIR, make Optimizer.dll probeable, or set " +
121 $"{XpressRuntimeLocator.ExplicitAssemblyEnvironmentVariable}.");
122
123 return notInstalled;
124 }
125
126 await RuntimeGate.WaitAsync(
127 cancellationToken);
128
129 try
130 {
133 assembly);
134
135 api.Initialize();
136
137 try
138 {
139 _cachedVersion =
140 api.TryGetVersion();
141
142 var available =
143 new SolverAvailabilityInfo(
144 SolverKind.Xpress,
145 SolverAvailabilityStatus.Available)
146 {
148 SolverVersion = _cachedVersion
149 };
150
151 available.AddDiagnostic(
152 $"FICO Xpress Optimizer managed runtime loaded from " +
153 $"'{resolvedPath}'.");
154
155 available.AddDiagnostic(
156 "XPRS.Init completed successfully, including native " +
157 "runtime/license initialization.");
158
159 return available;
160 }
161 finally
162 {
163 api.Free();
164 }
165 }
166 catch (OperationCanceledException)
167 {
168 throw;
169 }
170 catch (Exception exception)
171 {
172 return CreateFailureAvailability(
173 Unwrap(exception).Message);
174 }
175 finally
176 {
177 RuntimeGate.Release();
178 }
179 }
180
181 /// <inheritdoc />
182 protected override async ValueTask<MathematicalModelSolveResult>
184 MathematicalModelSolveRequest request,
185 CancellationToken cancellationToken)
186 {
187 ArgumentNullException.ThrowIfNull(request);
188 ArgumentNullException.ThrowIfNull(request.Model);
189
190 request.Parameters.EnsureValid();
191
192 Assembly? assembly =
194 out _);
195
196 if (assembly is null)
197 {
198 throw new InvalidOperationException(
199 "FICO Xpress MP is not available. Call solver discovery/" +
200 "availability before attempting to solve.");
201 }
202
203 string temporaryDirectory =
204 ExternalSolverResultUtilities.CreateTemporaryDirectory(
205 "xpress");
206
207 string modelPath =
208 Path.Combine(
209 temporaryDirectory,
210 "model.lp");
211
212 string solutionPath =
213 Path.Combine(
214 temporaryDirectory,
215 "solution.asc");
216
217 var stopwatch =
218 Stopwatch.StartNew();
219
220 await RuntimeGate.WaitAsync(
221 cancellationToken);
222
223 try
224 {
225 await PublishProgressAsync(
226 request,
227 new SolverProgressSnapshot
228 {
229 Stage = SolverProgressStage.BuildingModel,
230 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
231 Message = "Writing the portable LP model for FICO Xpress."
232 },
233 cancellationToken);
234
235 new PortableLpModelWriter().Write(
236 request.Model,
237 modelPath);
238
239 if (request.Parameters.ExportModel)
240 {
241 ExternalSolverResultUtilities.ExportPortableModel(
242 modelPath,
243 request.Parameters.ExportModelPath);
244 }
245
248 assembly);
249
250 api.Initialize();
251
252 object? problem =
253 null;
254
255 try
256 {
257 problem =
258 api.CreateProblem();
259
260 SetActiveProblem(problem);
261
262 using CancellationTokenRegistration cancellationRegistration =
263 cancellationToken.Register(RequestNativeStop);
264
265 api.ReadProblem(
266 problem,
267 modelPath);
268
269 IReadOnlyList<string> parameterDiagnostics =
270 api.ApplyParameters(
271 problem,
272 request.Parameters);
273
274 await PublishProgressAsync(
275 request,
276 new SolverProgressSnapshot
277 {
278 Stage = ResolveOptimizationProgressStage(),
279 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
280 Message = "FICO Xpress optimization started."
281 },
282 cancellationToken);
283
284 // Optimize is a native blocking operation. Run it off the
285 // caller's continuation thread while RequestNativeStop remains
286 // able to make a best-effort interrupt request.
287 await Task.Run(
288 () => api.Optimize(problem),
289 CancellationToken.None);
290
291 stopwatch.Stop();
292
293 double[]? solution =
294 api.TryGetSolution(problem);
295
296 IReadOnlyDictionary<int, double> values =
298 problem,
299 request.Model,
300 solution,
301 solutionPath,
302 out string mappingDiagnostic);
303
304 string status = api.TryGetStatus(problem);
305 bool hasSolution = solution is not null &&
306 values.Count == request.Model.VariableCount &&
307 !status.Contains("infeasible", StringComparison.OrdinalIgnoreCase) &&
308 !status.Contains("unbounded", StringComparison.OrdinalIgnoreCase);
309 if (!hasSolution)
310 {
311 values = new Dictionary<int, double>();
312 }
313
314 var result =
315 new MathematicalModelSolveResult
316 {
317 SolverKind = SolverKind.Xpress,
320 !string.IsNullOrWhiteSpace(_cachedVersion)
321 ? _cachedVersion
322 : api.TryGetVersion(),
323 SolveDuration = stopwatch.Elapsed,
324 HasFeasibleSolution = hasSolution,
325 IsOptimal =
326 IsOptimal(
327 status,
328 problem,
329 api,
330 hasSolution),
331 TerminationReason =
332 MapTerminationReason(
333 status,
334 problem,
335 api,
336 hasSolution,
337 cancellationToken.IsCancellationRequested),
338 ExploredNodeCount =
340 problem,
341 "Nodes",
342 "NodeCount"),
343 IterationCount =
345 problem,
346 "SimplexIter",
347 "SimplexIterations",
348 "LPIterations")
349 };
350
351 ExternalSolverResultUtilities.PopulateVariableValues(
352 result,
353 request.Model,
354 values);
355
356 PopulateObjectiveAndGap(
357 result,
358 request.Model,
359 values,
360 problem,
361 api);
362
363 foreach (string diagnostic in parameterDiagnostics)
364 {
365 result.AddDiagnostic(diagnostic);
366 }
367
368 if (!string.IsNullOrWhiteSpace(mappingDiagnostic))
369 {
370 result.AddDiagnostic(mappingDiagnostic);
371 }
372
373 result.AddDiagnostic(
374 string.IsNullOrWhiteSpace(status)
375 ? "Xpress status was not exposed by the loaded API surface."
376 : $"Xpress status: {status}.");
377
378 await PublishProgressAsync(
379 request,
380 new SolverProgressSnapshot
381 {
382 Stage = SolverProgressStage.Completed,
383 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
384 IncumbentObjective = result.ObjectiveValue,
385 BestBound = result.BestBound,
386 AbsoluteGap = result.AbsoluteGap,
387 RelativeGap = result.RelativeGap,
388 ExploredNodeCount = result.ExploredNodeCount,
389 IterationCount = result.IterationCount,
390 Message =
391 $"FICO Xpress terminated with reason " +
392 $"'{result.TerminationReason}'."
393 },
394 CancellationToken.None);
395
396 return result;
397 }
398 finally
399 {
400 if (problem is not null)
401 {
402 ClearActiveProblem(problem);
403 api.DestroyProblem(problem);
404 }
405
406 api.Free();
407 }
408 }
409 finally
410 {
411 RuntimeGate.Release();
412
413 ExternalSolverResultUtilities.TryDeleteDirectory(
414 temporaryDirectory);
415 }
416 }
417
418 /// <inheritdoc />
419 protected override void RequestNativeStop()
420 {
421 object? problem;
422
423 lock (_nativeSyncRoot)
424 {
425 problem = _activeProblem;
426 }
427
428 if (problem is null)
429 {
430 return;
431 }
432
434 }
435
436 private static void PopulateObjectiveAndGap(
437 MathematicalModelSolveResult result,
438 MathematicalModel model,
439 IReadOnlyDictionary<int, double> values,
440 object problem,
442 {
443 bool isMixedInteger = model.Variables.Any(
444 variable => variable.VariableType != MathematicalVariableType.Continuous);
445 double? objective = result.HasFeasibleSolution
447 problem,
448 isMixedInteger
449 ? ["MIPObjVal", "MIPBestObjVal", "ObjVal"]
450 : ["LPObjVal", "ObjVal"])
451 : null;
452
453 if (objective.HasValue)
454 {
455 result.ObjectiveValue =
456 objective.Value;
457 }
458 else if (result.HasFeasibleSolution &&
459 ExternalSolverResultUtilities.TryEvaluateObjective(
460 model,
461 values,
462 out double recomputedObjective))
463 {
464 result.ObjectiveValue =
465 recomputedObjective;
466
467 result.AddDiagnostic(
468 "Xpress objective was recomputed from the returned solution " +
469 "because the loaded Optimizer API exposed no recognized " +
470 "objective attribute/property.");
471 }
472
473 result.BestBound = isMixedInteger
474 ? api.TryGetDoubleProperty(problem, "BestBound", "MIPBestBound")
475 : null;
476 if (!result.BestBound.HasValue && result.IsOptimal)
477 {
478 result.BestBound = result.ObjectiveValue;
479 }
480
481 ExternalSolverResultUtilities.PopulateGapStatistics(
482 result);
483 }
484
485 private static bool IsOptimal(
486 string status,
487 object problem,
488 XpressReflectionApi api,
489 bool hasSolution)
490 {
491 if (status.Contains(
492 "optimal",
493 StringComparison.OrdinalIgnoreCase))
494 {
495 return true;
496 }
497
498 if (!hasSolution)
499 {
500 return false;
501 }
502
503 double? objective =
504 api.TryGetDoubleProperty(
505 problem,
506 "MIPBestObjVal",
507 "MIPObjVal",
508 "LPObjVal",
509 "ObjVal");
510
511 double? bound =
512 api.TryGetDoubleProperty(
513 problem,
514 "BestBound",
515 "MIPBestBound");
516
517 return objective.HasValue &&
518 bound.HasValue &&
519 Math.Abs(objective.Value - bound.Value) <=
520 1.0e-10 *
521 Math.Max(
522 1.0,
523 Math.Abs(objective.Value));
524 }
525
526 private static SolverTerminationReason MapTerminationReason(
527 string status,
528 object problem,
529 XpressReflectionApi api,
530 bool hasSolution,
531 bool cancellationRequested)
532 {
533 if (cancellationRequested)
534 {
535 return SolverTerminationReason.UserInterrupted;
536 }
537
538 if (IsOptimal(status, problem, api, hasSolution))
539 {
540 return SolverTerminationReason.Optimal;
541 }
542
543 if (status.Contains(
544 "infeasible",
545 StringComparison.OrdinalIgnoreCase) &&
546 status.Contains(
547 "unbounded",
548 StringComparison.OrdinalIgnoreCase))
549 {
550 return SolverTerminationReason.InfeasibleOrUnbounded;
551 }
552
553 if (status.Contains(
554 "infeasible",
555 StringComparison.OrdinalIgnoreCase))
556 {
557 return SolverTerminationReason.Infeasible;
558 }
559
560 if (status.Contains(
561 "unbounded",
562 StringComparison.OrdinalIgnoreCase))
563 {
564 return SolverTerminationReason.Unbounded;
565 }
566
567 if (status.Contains(
568 "time",
569 StringComparison.OrdinalIgnoreCase))
570 {
571 return SolverTerminationReason.TimeLimit;
572 }
573
574 if (status.Contains(
575 "node",
576 StringComparison.OrdinalIgnoreCase))
577 {
578 return SolverTerminationReason.NodeLimit;
579 }
580
581 if (status.Contains(
582 "solution",
583 StringComparison.OrdinalIgnoreCase) &&
584 status.Contains(
585 "limit",
586 StringComparison.OrdinalIgnoreCase))
587 {
588 return SolverTerminationReason.SolutionLimit;
589 }
590
591 return hasSolution
592 ? SolverTerminationReason.Feasible
593 : SolverTerminationReason.Unknown;
594 }
595
596 private SolverAvailabilityInfo CreateFailureAvailability(
597 string diagnostic)
598 {
599 var failed =
600 new SolverAvailabilityInfo(
601 SolverKind.Xpress,
602 SolverAvailabilityStatus.LoadFailure)
603 {
605 };
606
607 failed.AddDiagnostic(diagnostic);
608 return failed;
609 }
610
611 private void SetActiveProblem(
612 object problem)
613 {
614 lock (_nativeSyncRoot)
615 {
616 _activeProblem = problem;
617 }
618 }
619
620 private void ClearActiveProblem(
621 object problem)
622 {
623 lock (_nativeSyncRoot)
624 {
625 if (ReferenceEquals(
626 _activeProblem,
627 problem))
628 {
629 _activeProblem = null;
630 }
631 }
632 }
633
634 private static SolverProgressStage ResolveOptimizationProgressStage()
635 {
636 foreach (string candidate in
637 new[] { "Optimizing", "Solving", "Running" })
638 {
639 if (Enum.TryParse(
640 candidate,
641 ignoreCase: true,
642 out SolverProgressStage stage))
643 {
644 return stage;
645 }
646 }
647
648 // BuildingModel is known to exist in the current solver contract.
649 // Older contracts may not expose a dedicated optimization/running
650 // stage, so this is the safest binary-compatible fallback.
651 return SolverProgressStage.BuildingModel;
652 }
653
654 private static Exception Unwrap(
655 Exception exception)
656 {
657 if (exception is TargetInvocationException invocation &&
658 invocation.InnerException is Exception innerException)
659 {
660 return innerException;
661 }
662
663 return exception;
664 }
665}
666
667/// <summary>
668/// Reflection bridge over the optional official FICO Xpress Optimizer .NET
669/// assembly.
670/// </summary>
671internal sealed class XpressReflectionApi
672{
673 private readonly Type _xprsType;
674 private readonly Type _problemType;
675
676 private XpressReflectionApi(
677 Type xprsType,
678 Type problemType)
679 {
680 _xprsType = xprsType;
681 _problemType = problemType;
682 }
683
684 internal static XpressReflectionApi Create(
685 Assembly assembly)
686 {
687 ArgumentNullException.ThrowIfNull(assembly);
688
689 Type xprsType =
690 assembly.GetType(
691 "Optimizer.XPRS",
692 throwOnError: true,
693 ignoreCase: false)!;
694
695 Type problemType =
696 assembly.GetType(
697 "Optimizer.XPRSprob",
698 throwOnError: true,
699 ignoreCase: false)!;
700
701 return new XpressReflectionApi(
702 xprsType,
703 problemType);
704 }
705
706 internal void Initialize()
707 {
708 InvokeRequiredStatic(
709 _xprsType,
710 "Init",
711 string.Empty);
712 }
713
714 internal void Free()
715 {
716 try
717 {
718 InvokeRequiredStatic(
719 _xprsType,
720 "Free");
721 }
722 catch
723 {
724 // Resource cleanup must not mask the primary result/error.
725 }
726 }
727
728 internal object CreateProblem()
729 {
730 return Activator.CreateInstance(_problemType) ??
731 throw new InvalidOperationException(
732 "Unable to create Optimizer.XPRSprob.");
733 }
734
735 internal void ReadProblem(
736 object problem,
737 string modelPath)
738 {
739 TrySetProperty(
740 problem,
741 "MPSFormat",
742 -1);
743
744 InvokeRequired(
745 problem,
746 "ReadProb",
747 modelPath,
748 string.Empty);
749 }
750
751 internal void Optimize(
752 object problem)
753 {
754 InvokeRequired(
755 problem,
756 "Optimize");
757 }
758
759 internal double[]? TryGetSolution(
760 object problem)
761 {
762 try
763 {
764 object? value =
765 InvokeRequired(
766 problem,
767 "GetSolution");
768
769 return value as double[];
770 }
771 catch
772 {
773 return null;
774 }
775 }
776
777 internal IReadOnlyList<string> ApplyParameters(
778 object problem,
779 SolverParameters parameters)
780 {
781 var diagnostics =
782 new List<string>();
783
784 if (parameters.TimeLimitSeconds.HasValue)
785 {
786 int seconds =
787 Math.Max(
788 1,
789 (int)Math.Ceiling(
790 parameters.TimeLimitSeconds.Value));
791
792 if (!TrySetProperty(
793 problem,
794 "MaxTime",
795 -seconds))
796 {
797 diagnostics.Add(
798 "Xpress generic parameter 'TimeLimitSeconds' could not " +
799 "be mapped through the loaded API surface.");
800 }
801 }
802
803 TrySetWithDiagnostic(
804 problem,
805 "Threads",
806 parameters.ThreadCount,
807 diagnostics,
808 nameof(parameters.ThreadCount));
809
810 TrySetWithDiagnostic(
811 problem,
812 "MaxNode",
813 parameters.NodeLimit,
814 diagnostics,
815 nameof(parameters.NodeLimit));
816
817 TrySetWithDiagnostic(
818 problem,
819 "MaxMipSol",
820 parameters.SolutionLimit,
821 diagnostics,
822 nameof(parameters.SolutionLimit));
823
824 TrySetWithDiagnostic(
825 problem,
826 "MIPRelStop",
827 parameters.RelativeMipGap,
828 diagnostics,
829 nameof(parameters.RelativeMipGap));
830
831 TrySetWithDiagnostic(
832 problem,
833 "MIPAbsStop",
834 parameters.AbsoluteMipGap,
835 diagnostics,
836 nameof(parameters.AbsoluteMipGap));
837
838 if (parameters.EnablePresolve.HasValue &&
839 !TrySetProperty(
840 problem,
841 "Presolve",
842 parameters.EnablePresolve.Value ? 1 : 0))
843 {
844 diagnostics.Add(
845 "Xpress generic parameter 'EnablePresolve' could not be " +
846 "mapped through the loaded API surface.");
847 }
848
849 if (parameters.EnableCuts == false &&
850 !TrySetProperty(
851 problem,
852 "CutStrategy",
853 0))
854 {
855 diagnostics.Add(
856 "Xpress generic parameter 'EnableCuts' could not be mapped " +
857 "through the loaded API surface.");
858 }
859
860 if (parameters.EnableHeuristics == false &&
861 !TrySetProperty(
862 problem,
863 "HeurEmphasis",
864 0))
865 {
866 diagnostics.Add(
867 "Xpress generic parameter 'EnableHeuristics' could not be " +
868 "mapped through the loaded API surface.");
869 }
870
871 AddDeferred(
872 diagnostics,
873 parameters.RandomSeed.HasValue,
874 nameof(parameters.RandomSeed));
875 AddDeferred(
876 diagnostics,
877 parameters.IterationLimit.HasValue,
878 nameof(parameters.IterationLimit));
879 AddDeferred(
880 diagnostics,
881 parameters.MemoryLimitMegabytes.HasValue,
882 nameof(parameters.MemoryLimitMegabytes));
883 AddDeferred(
884 diagnostics,
885 parameters.DeterministicMode,
886 nameof(parameters.DeterministicMode));
887
888 foreach (KeyValuePair<string, string> parameter in
889 EnumerateNativeParameters(parameters.NativeParameters))
890 {
891 if (!TrySetProperty(
892 problem,
893 parameter.Key,
894 parameter.Value))
895 {
896 diagnostics.Add(
897 $"Xpress native parameter '{parameter.Key}' could not be " +
898 "mapped as a public XPRSprob property.");
899 }
900 }
901
902 return diagnostics;
903 }
904
905 internal IReadOnlyDictionary<int, double> GetValuesByPortableName(
906 object problem,
907 MathematicalModel model,
908 double[]? solution,
909 string solutionPath,
910 out string diagnostic)
911 {
912 diagnostic = string.Empty;
913
914 if (solution is null)
915 {
916 return new Dictionary<int, double>();
917 }
918
919 var byIndex =
920 new Dictionary<int, double>();
921
922 bool allIndexesResolved =
923 true;
924
925 foreach (MathematicalVariable variable in model.Variables)
926 {
927 if (!TryGetColumnIndex(
928 problem,
929 PortableLpModelWriter.GetVariableName(variable.Id),
930 out int columnIndex) ||
931 columnIndex < 0 ||
932 columnIndex >= solution.Length)
933 {
934 allIndexesResolved = false;
935 break;
936 }
937
938 byIndex[variable.Id] =
939 solution[columnIndex];
940 }
941
942 if (allIndexesResolved &&
943 byIndex.Count == model.VariableCount)
944 {
945 diagnostic =
946 "Xpress solution values were mapped by column name through " +
947 "XPRSprob.GetIndex.";
948 return byIndex;
949 }
950
951 if (TryWriteSolution(
952 problem,
953 solutionPath))
954 {
955 IReadOnlyDictionary<int, double> parsed =
957 solutionPath);
958
959 if (parsed.Count > 0)
960 {
961 diagnostic =
962 "Xpress solution values were mapped from the native ASCII " +
963 "solution output using portable variable names.";
964 return parsed;
965 }
966 }
967
968 if (solution.Length == model.VariableCount)
969 {
970 var positional =
971 new Dictionary<int, double>();
972
973 for (int index = 0; index < model.VariableCount; index++)
974 {
975 positional[model.Variables[index].Id] =
976 solution[index];
977 }
978
979 diagnostic =
980 "Xpress solution values used the LP column-order fallback " +
981 "because the loaded API exposed neither GetIndex nor a " +
982 "parseable ASCII solution. The independent checker remains " +
983 "the final validity guard.";
984
985 return positional;
986 }
987
988 diagnostic =
989 "Xpress returned a solution vector, but variable names could not " +
990 "be mapped safely to the generic mathematical model.";
991
992 return new Dictionary<int, double>();
993 }
994
995 internal string TryGetStatus(
996 object problem)
997 {
998 object? value =
999 TryGetProperty(
1000 problem,
1001 "SolStatus",
1002 "MIPStatus",
1003 "LPStatus");
1004
1005 return value?.ToString() ??
1006 string.Empty;
1007 }
1008
1009 internal string TryGetVersion()
1010 {
1011 object? value =
1012 TryGetStaticProperty(
1013 _xprsType,
1014 "Version",
1015 "VERSION");
1016
1017 if (value is not null)
1018 {
1019 return value.ToString() ?? string.Empty;
1020 }
1021
1022 try
1023 {
1024 return InvokeRequiredStatic(_xprsType, "GetVersion")?.ToString()
1025 ?? string.Empty;
1026 }
1027 catch
1028 {
1029 return string.Empty;
1030 }
1031 }
1032
1033 internal double? TryGetDoubleProperty(
1034 object problem,
1035 params string[] names)
1036 {
1037 object? value =
1038 TryGetProperty(
1039 problem,
1040 names);
1041
1042 if (value is null)
1043 {
1044 return null;
1045 }
1046
1047 try
1048 {
1049 double converted =
1050 Convert.ToDouble(
1051 value,
1052 CultureInfo.InvariantCulture);
1053
1054 return double.IsFinite(converted)
1055 ? converted
1056 : null;
1057 }
1058 catch
1059 {
1060 return null;
1061 }
1062 }
1063
1064 internal long? TryGetInt64Property(
1065 object problem,
1066 params string[] names)
1067 {
1068 object? value =
1069 TryGetProperty(
1070 problem,
1071 names);
1072
1073 if (value is null)
1074 {
1075 return null;
1076 }
1077
1078 try
1079 {
1080 return Convert.ToInt64(
1081 value,
1082 CultureInfo.InvariantCulture);
1083 }
1084 catch
1085 {
1086 return null;
1087 }
1088 }
1089
1090 internal void DestroyProblem(
1091 object problem)
1092 {
1093 try
1094 {
1095 InvokeRequired(
1096 problem,
1097 "Destroy");
1098 }
1099 catch
1100 {
1101 if (problem is IDisposable disposable)
1102 {
1103 disposable.Dispose();
1104 }
1105 }
1106 }
1107
1108 internal static void TryInterrupt(
1109 object problem)
1110 {
1111 foreach (string methodName in new[]
1112 {
1113 "Interrupt",
1114 "Stop"
1115 })
1116 {
1117 try
1118 {
1119 MethodInfo? method =
1120 FindMethod(
1121 problem.GetType(),
1122 methodName,
1123 parameterCount: 0);
1124
1125 if (method is null)
1126 {
1127 continue;
1128 }
1129
1130 method.Invoke(
1131 problem,
1132 null);
1133 return;
1134 }
1135 catch
1136 {
1137 // Best-effort interruption only.
1138 }
1139 }
1140 }
1141
1142 private static bool TryGetColumnIndex(
1143 object problem,
1144 string name,
1145 out int index)
1146 {
1147 index = -1;
1148
1149 MethodInfo? twoParameter =
1150 FindMethod(
1151 problem.GetType(),
1152 "GetIndex",
1153 parameterCount: 2);
1154
1155 if (twoParameter is not null)
1156 {
1157 try
1158 {
1159 object? value =
1160 twoParameter.Invoke(
1161 problem,
1162 [2, name]);
1163
1164 if (value is not null)
1165 {
1166 index =
1167 Convert.ToInt32(
1168 value,
1169 CultureInfo.InvariantCulture);
1170 return index >= 0;
1171 }
1172 }
1173 catch
1174 {
1175 // Try the out-parameter shape below.
1176 }
1177 }
1178
1179 MethodInfo? threeParameter =
1180 FindMethod(
1181 problem.GetType(),
1182 "GetIndex",
1183 parameterCount: 3);
1184
1185 if (threeParameter is not null)
1186 {
1187 try
1188 {
1189 object?[] arguments =
1190 [2, name, 0];
1191
1192 threeParameter.Invoke(
1193 problem,
1194 arguments);
1195
1196 index =
1197 Convert.ToInt32(
1198 arguments[2],
1199 CultureInfo.InvariantCulture);
1200
1201 return index >= 0;
1202 }
1203 catch
1204 {
1205 // Fall through to alternative mapping methods.
1206 }
1207 }
1208
1209 return false;
1210 }
1211
1212 private static bool TryWriteSolution(
1213 object problem,
1214 string solutionPath)
1215 {
1216 foreach (int parameterCount in new[] { 2, 1 })
1217 {
1218 MethodInfo? method =
1219 FindMethod(
1220 problem.GetType(),
1221 "WriteSol",
1222 parameterCount);
1223
1224 if (method is null)
1225 {
1226 continue;
1227 }
1228
1229 try
1230 {
1231 object?[] arguments =
1232 parameterCount == 2
1233 ? [solutionPath, string.Empty]
1234 : [solutionPath];
1235
1236 method.Invoke(
1237 problem,
1238 arguments);
1239
1240 if (File.Exists(solutionPath))
1241 {
1242 return true;
1243 }
1244
1245 string? directory =
1246 Path.GetDirectoryName(solutionPath);
1247
1248 string stem =
1249 Path.GetFileNameWithoutExtension(solutionPath);
1250
1251 if (!string.IsNullOrWhiteSpace(directory))
1252 {
1253 string? candidate =
1254 Directory
1255 .EnumerateFiles(
1256 directory,
1257 stem + "*",
1258 SearchOption.TopDirectoryOnly)
1259 .FirstOrDefault();
1260
1261 if (!string.IsNullOrWhiteSpace(candidate))
1262 {
1263 File.Copy(
1264 candidate,
1265 solutionPath,
1266 overwrite: true);
1267 return true;
1268 }
1269 }
1270 }
1271 catch
1272 {
1273 // Try another method shape or mapping technique.
1274 }
1275 }
1276
1277 return false;
1278 }
1279
1280 private static void TrySetWithDiagnostic<T>(
1281 object problem,
1282 string propertyName,
1283 T? value,
1284 ICollection<string> diagnostics,
1285 string genericParameterName)
1286 where T : struct
1287 {
1288 if (!value.HasValue)
1289 {
1290 return;
1291 }
1292
1293 if (!TrySetProperty(
1294 problem,
1295 propertyName,
1296 value.Value))
1297 {
1298 diagnostics.Add(
1299 $"Xpress generic parameter '{genericParameterName}' could " +
1300 "not be mapped through the loaded API surface.");
1301 }
1302 }
1303
1304 private static void AddDeferred(
1305 ICollection<string> diagnostics,
1306 bool condition,
1307 string parameterName)
1308 {
1309 if (!condition)
1310 {
1311 return;
1312 }
1313
1314 diagnostics.Add(
1315 $"Generic parameter '{parameterName}' is valid but is not yet " +
1316 "translated by the reflection-based Xpress adapter.");
1317 }
1318
1319 private static bool TrySetProperty(
1320 object target,
1321 string name,
1322 object value)
1323 {
1324 PropertyInfo? property =
1325 target.GetType()
1326 .GetProperties(
1327 BindingFlags.Instance |
1328 BindingFlags.Public)
1329 .FirstOrDefault(
1330 candidate =>
1331 candidate.CanWrite &&
1332 string.Equals(
1333 candidate.Name,
1334 name,
1335 StringComparison.OrdinalIgnoreCase));
1336
1337 if (property is null)
1338 {
1339 return false;
1340 }
1341
1342 try
1343 {
1344 object converted =
1345 ConvertForMember(
1346 value,
1347 property.PropertyType);
1348
1349 property.SetValue(
1350 target,
1351 converted);
1352
1353 return true;
1354 }
1355 catch
1356 {
1357 return false;
1358 }
1359 }
1360
1361 private static object? TryGetProperty(
1362 object target,
1363 params string[] names)
1364 {
1365 foreach (string name in names)
1366 {
1367 PropertyInfo? property =
1368 target.GetType()
1369 .GetProperties(
1370 BindingFlags.Instance |
1371 BindingFlags.Public)
1372 .FirstOrDefault(
1373 candidate =>
1374 candidate.CanRead &&
1375 string.Equals(
1376 candidate.Name,
1377 name,
1378 StringComparison.OrdinalIgnoreCase));
1379
1380 if (property is null)
1381 {
1382 continue;
1383 }
1384
1385 try
1386 {
1387 return property.GetValue(target);
1388 }
1389 catch
1390 {
1391 // Try next alias.
1392 }
1393 }
1394
1395 return null;
1396 }
1397
1398 private static object? TryGetStaticProperty(
1399 Type type,
1400 params string[] names)
1401 {
1402 foreach (string name in names)
1403 {
1404 PropertyInfo? property =
1405 type.GetProperties(
1406 BindingFlags.Static |
1407 BindingFlags.Public)
1408 .FirstOrDefault(
1409 candidate =>
1410 candidate.CanRead &&
1411 string.Equals(
1412 candidate.Name,
1413 name,
1414 StringComparison.OrdinalIgnoreCase));
1415
1416 if (property is null)
1417 {
1418 continue;
1419 }
1420
1421 try
1422 {
1423 return property.GetValue(null);
1424 }
1425 catch
1426 {
1427 // Try next alias.
1428 }
1429 }
1430
1431 return null;
1432 }
1433
1434 private static object? InvokeRequired(
1435 object target,
1436 string methodName,
1437 params object?[] arguments)
1438 {
1439 MethodInfo method =
1440 FindCompatibleMethod(
1441 target.GetType(),
1442 methodName,
1443 arguments,
1444 isStatic: false) ??
1445 throw new MissingMethodException(
1446 target.GetType().FullName,
1447 methodName);
1448
1449 return method.Invoke(
1450 target,
1451 ConvertArguments(
1452 method,
1453 arguments));
1454 }
1455
1456 private static object? InvokeRequiredStatic(
1457 Type type,
1458 string methodName,
1459 params object?[] arguments)
1460 {
1461 MethodInfo method =
1462 FindCompatibleMethod(
1463 type,
1464 methodName,
1465 arguments,
1466 isStatic: true) ??
1467 throw new MissingMethodException(
1468 type.FullName,
1469 methodName);
1470
1471 return method.Invoke(
1472 null,
1473 ConvertArguments(
1474 method,
1475 arguments));
1476 }
1477
1478 private static MethodInfo? FindCompatibleMethod(
1479 Type type,
1480 string methodName,
1481 object?[] arguments,
1482 bool isStatic)
1483 {
1484 BindingFlags flags =
1485 BindingFlags.Public |
1486 (isStatic
1487 ? BindingFlags.Static
1488 : BindingFlags.Instance);
1489
1490 return type
1491 .GetMethods(flags)
1492 .Where(
1493 method =>
1494 string.Equals(
1495 method.Name,
1496 methodName,
1497 StringComparison.OrdinalIgnoreCase))
1498 .Where(
1499 method =>
1500 method.GetParameters().Length ==
1501 arguments.Length)
1502 .FirstOrDefault();
1503 }
1504
1505 private static MethodInfo? FindMethod(
1506 Type type,
1507 string methodName,
1508 int parameterCount)
1509 {
1510 return type
1511 .GetMethods(
1512 BindingFlags.Public |
1513 BindingFlags.Instance)
1514 .FirstOrDefault(
1515 method =>
1516 string.Equals(
1517 method.Name,
1518 methodName,
1519 StringComparison.OrdinalIgnoreCase) &&
1520 method.GetParameters().Length == parameterCount);
1521 }
1522
1523 private static object?[] ConvertArguments(
1524 MethodInfo method,
1525 object?[] arguments)
1526 {
1527 ParameterInfo[] parameters =
1528 method.GetParameters();
1529
1530 var converted =
1531 new object?[arguments.Length];
1532
1533 for (int index = 0; index < arguments.Length; index++)
1534 {
1535 object? value =
1536 arguments[index];
1537
1538 converted[index] =
1539 value is null
1540 ? null
1541 : ConvertForMember(
1542 value,
1543 parameters[index].ParameterType);
1544 }
1545
1546 return converted;
1547 }
1548
1549 private static object ConvertForMember(
1550 object value,
1551 Type targetType)
1552 {
1553 Type effectiveTarget =
1554 targetType.IsByRef
1555 ? targetType.GetElementType() ?? targetType
1556 : targetType;
1557
1558 if (effectiveTarget.IsInstanceOfType(value))
1559 {
1560 return value;
1561 }
1562
1563 if (effectiveTarget.IsEnum)
1564 {
1565 if (value is string text)
1566 {
1567 return Enum.Parse(
1568 effectiveTarget,
1569 text,
1570 ignoreCase: true);
1571 }
1572
1573 object underlying =
1574 Convert.ChangeType(
1575 value,
1576 Enum.GetUnderlyingType(effectiveTarget),
1577 CultureInfo.InvariantCulture);
1578
1579 return Enum.ToObject(
1580 effectiveTarget,
1581 underlying);
1582 }
1583
1584 return Convert.ChangeType(
1585 value,
1586 effectiveTarget,
1587 CultureInfo.InvariantCulture);
1588 }
1589 private static SolverProgressStage ResolveOptimizationProgressStage()
1590 {
1591 foreach (string candidate in
1592 new[] { "Optimizing", "Solving", "Running" })
1593 {
1594 if (Enum.TryParse(
1595 candidate,
1596 ignoreCase: true,
1597 out SolverProgressStage stage))
1598 {
1599 return stage;
1600 }
1601 }
1602
1603 // BuildingModel exists in the current solver contract and is the
1604 // safest fallback for older contracts that do not expose a dedicated
1605 // optimization/running stage. The accompanying message still reports
1606 // the actual activity precisely.
1607 return SolverProgressStage.BuildingModel;
1608 }
1609
1610 private static IEnumerable<KeyValuePair<string, string>>
1611 EnumerateNativeParameters(
1612 System.Collections.IEnumerable? nativeParameters)
1613 {
1614 if (nativeParameters is null)
1615 {
1616 yield break;
1617 }
1618
1619 foreach (object? item in nativeParameters)
1620 {
1621 if (item is null)
1622 {
1623 continue;
1624 }
1625
1626 if (item is KeyValuePair<string, string> pair)
1627 {
1628 yield return pair;
1629 continue;
1630 }
1631
1632 object? nameValue =
1633 ReadNativeParameterMember(
1634 item,
1635 "Name",
1636 "Key",
1637 "ParameterName",
1638 "Parameter");
1639
1640 object? parameterValue =
1641 ReadNativeParameterMember(
1642 item,
1643 "Value",
1644 "ParameterValue");
1645
1646 string name =
1647 Convert.ToString(
1648 nameValue,
1649 CultureInfo.InvariantCulture) ??
1650 string.Empty;
1651
1652 string value =
1653 Convert.ToString(
1654 parameterValue,
1655 CultureInfo.InvariantCulture) ??
1656 string.Empty;
1657
1658 if (!string.IsNullOrWhiteSpace(name))
1659 {
1660 yield return new KeyValuePair<string, string>(
1661 name,
1662 value);
1663 }
1664 }
1665 }
1666
1667 private static object? ReadNativeParameterMember(
1668 object instance,
1669 params string[] candidateNames)
1670 {
1671 Type type = instance.GetType();
1672
1673 foreach (string candidateName in candidateNames)
1674 {
1675 var property =
1676 type.GetProperties()
1677 .FirstOrDefault(
1678 candidate =>
1679 candidate.CanRead &&
1680 string.Equals(
1681 candidate.Name,
1682 candidateName,
1683 StringComparison.OrdinalIgnoreCase));
1684
1685 if (property is not null)
1686 {
1687 return property.GetValue(instance);
1688 }
1689
1690 var field =
1691 type.GetFields()
1692 .FirstOrDefault(
1693 candidate =>
1694 string.Equals(
1695 candidate.Name,
1696 candidateName,
1697 StringComparison.OrdinalIgnoreCase));
1698
1699 if (field is not null)
1700 {
1701 return field.GetValue(instance);
1702 }
1703 }
1704
1705 return null;
1706 }
1707
1708}
Parses Xpress ASCII solution rows written with the standard Optimizer solution layout.
static IReadOnlyDictionary< int, double > ParseFile(string path)
Parses variable activities keyed by the portable mathematical variable identifier embedded in names o...
Reflection bridge over the optional official FICO Xpress Optimizer .NET assembly.
void ReadProblem(object problem, string modelPath)
IReadOnlyList< string > ApplyParameters(object problem, SolverParameters parameters)
static XpressReflectionApi Create(Assembly assembly)
long? TryGetInt64Property(object problem, params string[] names)
double? TryGetDoubleProperty(object problem, params string[] names)
IReadOnlyDictionary< int, double > GetValuesByPortableName(object problem, MathematicalModel model, double[]? solution, string solutionPath, out string diagnostic)
Locates and loads the optional FICO Xpress Optimizer managed assembly.
static ? Assembly TryLoad(out string resolvedPath)
Loads the already available Optimizer assembly or resolves it from common Xpress installation locatio...
override async ValueTask< SolverAvailabilityInfo > CheckAvailabilityAsync(CancellationToken cancellationToken=default)
override async ValueTask< MathematicalModelSolveResult > SolveCoreAsync(MathematicalModelSolveRequest request, CancellationToken cancellationToken)
XpressSolverAdapter()
Initializes the FICO Xpress MP adapter.