LotSizingDataModel.Solver.Gurobi 2.0.1
Gurobi solver adapter for the LotSizingDataModel solver abstraction.
Loading...
Searching...
No Matches
GurobiSolverAdapter.cs
Go to the documentation of this file.
1using System.Diagnostics;
2using System.Globalization;
3using System.Text.RegularExpressions;
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 Gurobi through
16/// the official <c>gurobi_cl</c> command-line executable.
17/// </summary>
18/// <remarks>
19/// The adapter deliberately has no compile-time dependency on the Gurobi .NET
20/// assembly. A machine without Gurobi can therefore build and use the rest of
21/// LotSizingDataModel unchanged.
22/// </remarks>
23public sealed partial class GurobiSolverAdapter :
24 MathematicalModelSolverPluginBase
25{
26 private const string ExplicitExecutableEnvironmentVariable =
27 "LOTSIZING_GUROBI_EXECUTABLE";
28
29 private readonly ExternalSolverProcessRunner _processRunner =
30 new();
31
32 private string _cachedExecutablePath =
33 string.Empty;
34
35 private string _cachedVersion =
36 string.Empty;
37
38 /// <summary>
39 /// Initializes the Gurobi solver adapter.
40 /// </summary>
42 : base(
43 [
44 SolverCapability.LinearProgramming,
45 SolverCapability.MixedIntegerLinearProgramming,
46 SolverCapability.Interruption,
47 SolverCapability.LpExport,
48 SolverCapability.OptimalityGapReporting,
49 SolverCapability.SearchStatistics
50 ])
51 {
52 }
53
54 /// <inheritdoc />
55 public override SolverKind SolverKind =>
56 SolverKind.Gurobi;
57
58 /// <inheritdoc />
59 public override string SolverName =>
60 "Gurobi Optimizer";
61
62 /// <inheritdoc />
63 public override string SolverVersion =>
64 _cachedVersion;
65
66 /// <inheritdoc />
67 public override string AdapterId =>
68 "LotSizingDataModel.Solver.Gurobi";
69
70 /// <inheritdoc />
71 public override string AdapterName =>
72 "LotSizingDataModel Gurobi Adapter";
73
74 /// <inheritdoc />
75 public override string AdapterVersion =>
76 "1.0.0";
77
78 /// <inheritdoc />
79 public override string MinimumSupportedSolverVersion =>
80 "12.0";
81
82 /// <inheritdoc />
83 public override async ValueTask<SolverAvailabilityInfo>
85 CancellationToken cancellationToken = default)
86 {
87 cancellationToken.ThrowIfCancellationRequested();
88
89 string executablePath =
90 ResolveExecutablePath();
91
92 if (string.IsNullOrWhiteSpace(executablePath))
93 {
94 var notInstalled =
95 new SolverAvailabilityInfo(
96 SolverKind.Gurobi,
97 SolverAvailabilityStatus.NotInstalled)
98 {
100 };
101
102 notInstalled.AddDiagnostic(
103 "gurobi_cl was not found. Install Gurobi, put gurobi_cl " +
104 "on PATH, define GUROBI_HOME, or set " +
105 $"{ExplicitExecutableEnvironmentVariable}.");
106
107 return notInstalled;
108 }
109
110 try
111 {
112 string workingDirectory =
113 Path.GetDirectoryName(executablePath) ??
114 Environment.CurrentDirectory;
115
116 ExternalSolverProcessResult processResult =
117 await _processRunner.RunAsync(
118 executablePath,
119 ["--version"],
120 workingDirectory,
121 standardInput: null,
122 cancellationToken);
123
124 if (processResult.WasCancelled)
125 {
126 cancellationToken.ThrowIfCancellationRequested();
127 }
128
129 string text =
130 JoinOutput(processResult);
131
132 if (processResult.ExitCode != 0)
133 {
134 var failed =
135 new SolverAvailabilityInfo(
136 SolverKind.Gurobi,
137 SolverAvailabilityStatus.LoadFailure)
138 {
140 };
141
142 failed.AddDiagnostic(
143 FirstMeaningfulLine(text) ??
144 $"gurobi_cl exited with code {processResult.ExitCode}.");
145
146 return failed;
147 }
148
149 ExternalSolverProcessResult licenseResult =
150 await _processRunner.RunAsync(
151 executablePath,
152 ["--license"],
153 workingDirectory,
154 standardInput: null,
155 cancellationToken);
156
157 if (licenseResult.WasCancelled)
158 {
159 cancellationToken.ThrowIfCancellationRequested();
160 }
161
162 string licenseText =
163 JoinOutput(licenseResult);
164
165 if (licenseResult.ExitCode != 0)
166 {
167 var failed =
168 new SolverAvailabilityInfo(
169 SolverKind.Gurobi,
170 SolverAvailabilityStatus.LoadFailure)
171 {
173 SolverVersion = ParseVersion(text)
174 };
175
176 failed.AddDiagnostic(
177 FirstMeaningfulLine(licenseText) ??
178 "gurobi_cl could be executed, but its license " +
179 "information could not be resolved.");
180
181 return failed;
182 }
183
184 _cachedExecutablePath =
185 executablePath;
186
187 _cachedVersion =
188 ParseVersion(text);
189
190 var available =
191 new SolverAvailabilityInfo(
192 SolverKind.Gurobi,
193 SolverAvailabilityStatus.Available)
194 {
196 SolverVersion = _cachedVersion
197 };
198
199 available.AddDiagnostic(
200 $"Gurobi command-line executable detected at " +
201 $"'{executablePath}'.");
202
203 if (!string.IsNullOrWhiteSpace(text))
204 {
205 available.AddDiagnostic(
206 FirstMeaningfulLine(text) ?? text.Trim());
207 }
208
209 if (!string.IsNullOrWhiteSpace(licenseText))
210 {
211 available.AddDiagnostic(
212 FirstMeaningfulLine(licenseText) ?? licenseText.Trim());
213 }
214
215 return available;
216 }
217 catch (OperationCanceledException)
218 {
219 throw;
220 }
221 catch (Exception exception)
222 {
223 var failed =
224 new SolverAvailabilityInfo(
225 SolverKind.Gurobi,
226 SolverAvailabilityStatus.LoadFailure)
227 {
229 };
230
231 failed.AddDiagnostic(exception.Message);
232 return failed;
233 }
234 }
235
236 /// <inheritdoc />
237 protected override async ValueTask<MathematicalModelSolveResult>
239 MathematicalModelSolveRequest request,
240 CancellationToken cancellationToken)
241 {
242 ArgumentNullException.ThrowIfNull(request);
243 ArgumentNullException.ThrowIfNull(request.Model);
244
245 request.Parameters.EnsureValid();
246
247 string executablePath =
248 !string.IsNullOrWhiteSpace(_cachedExecutablePath) &&
249 File.Exists(_cachedExecutablePath)
250 ? _cachedExecutablePath
251 : ResolveExecutablePath();
252
253 if (string.IsNullOrWhiteSpace(executablePath))
254 {
255 throw new InvalidOperationException(
256 "Gurobi is not available. Call solver discovery/availability " +
257 "before attempting to solve.");
258 }
259
260 string temporaryDirectory =
261 ExternalSolverResultUtilities.CreateTemporaryDirectory(
262 "gurobi");
263
264 string modelPath =
265 Path.Combine(
266 temporaryDirectory,
267 "model.lp");
268
269 string solutionPath =
270 Path.Combine(
271 temporaryDirectory,
272 "solution.sol");
273
274 var stopwatch =
275 Stopwatch.StartNew();
276
277 try
278 {
279 await PublishProgressAsync(
280 request,
281 new SolverProgressSnapshot
282 {
283 Stage = SolverProgressStage.BuildingModel,
284 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
285 Message = "Writing the portable LP model for Gurobi."
286 },
287 cancellationToken);
288
289 new PortableLpModelWriter().Write(
290 request.Model,
291 modelPath);
292
293 if (request.Parameters.ExportModel)
294 {
295 ExternalSolverResultUtilities.ExportPortableModel(
296 modelPath,
297 request.Parameters.ExportModelPath);
298 }
299
300 IReadOnlyList<string> parameterDiagnostics;
301 IReadOnlyList<string> arguments =
302 BuildArguments(
303 request.Parameters,
304 modelPath,
305 solutionPath,
306 out parameterDiagnostics);
307
308 await PublishProgressAsync(
309 request,
310 new SolverProgressSnapshot
311 {
312 Stage = ResolveOptimizationProgressStage(),
313 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
314 Message = "Gurobi optimization started."
315 },
316 cancellationToken);
317
318 ExternalSolverProcessResult processResult =
319 await _processRunner.RunAsync(
320 executablePath,
321 arguments,
322 temporaryDirectory,
323 standardInput: null,
324 cancellationToken);
325
326 stopwatch.Stop();
327
328 string output =
329 JoinOutput(processResult);
330
331 IReadOnlyDictionary<int, double> values =
332 NamedSolutionValueParser.ParseFile(
333 solutionPath);
334
335 bool hasSolution =
336 values.Count > 0 ||
337 (request.Model.VariableCount == 0 &&
338 File.Exists(solutionPath));
339
340 var result =
341 new MathematicalModelSolveResult
342 {
343 SolverKind = SolverKind.Gurobi,
346 !string.IsNullOrWhiteSpace(_cachedVersion)
347 ? _cachedVersion
348 : ParseVersion(output),
349 SolveDuration = stopwatch.Elapsed,
350 HasFeasibleSolution = hasSolution,
351 TerminationReason =
352 MapTerminationReason(
353 output,
354 hasSolution,
355 processResult.WasCancelled),
356 IsOptimal = IsOptimal(output)
357 };
358
359 ExternalSolverResultUtilities.PopulateVariableValues(
360 result,
361 request.Model,
362 values);
363
364 PopulateStatistics(
365 result,
366 request.Model,
367 values,
368 output);
369
370 foreach (string diagnostic in parameterDiagnostics)
371 {
372 result.AddDiagnostic(diagnostic);
373 }
374
375 result.AddDiagnostic(
376 $"gurobi_cl exit code: {processResult.ExitCode}.");
377
378 string? finalLine =
379 LastMeaningfulLine(output);
380
381 if (!string.IsNullOrWhiteSpace(finalLine))
382 {
383 result.AddDiagnostic(
384 $"Gurobi final log: {finalLine}");
385 }
386
387 await PublishProgressAsync(
388 request,
389 new SolverProgressSnapshot
390 {
391 Stage = SolverProgressStage.Completed,
392 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
393 IncumbentObjective = result.ObjectiveValue,
394 BestBound = result.BestBound,
395 AbsoluteGap = result.AbsoluteGap,
396 RelativeGap = result.RelativeGap,
397 ExploredNodeCount = result.ExploredNodeCount,
398 IterationCount = result.IterationCount,
399 Message =
400 $"Gurobi terminated with reason " +
401 $"'{result.TerminationReason}'."
402 },
403 CancellationToken.None);
404
405 return result;
406 }
407 finally
408 {
409 ExternalSolverResultUtilities.TryDeleteDirectory(
410 temporaryDirectory);
411 }
412 }
413
414 /// <inheritdoc />
415 protected override void RequestNativeStop()
416 {
417 _processRunner.Stop();
418 }
419
420 private static IReadOnlyList<string> BuildArguments(
421 SolverParameters parameters,
422 string modelPath,
423 string solutionPath,
424 out IReadOnlyList<string> diagnostics)
425 {
426 var arguments =
427 new List<string>
428 {
429 $"ResultFile={solutionPath}"
430 };
431
432 var messages =
433 new List<string>();
434
435 AddParameter(
436 arguments,
437 "TimeLimit",
438 parameters.TimeLimitSeconds);
439 AddParameter(
440 arguments,
441 "Threads",
442 parameters.ThreadCount);
443 AddParameter(
444 arguments,
445 "MIPGap",
446 parameters.RelativeMipGap);
447 AddParameter(
448 arguments,
449 "MIPGapAbs",
450 parameters.AbsoluteMipGap);
451 AddParameter(
452 arguments,
453 "NodeLimit",
454 parameters.NodeLimit);
455 AddParameter(
456 arguments,
457 "SolutionLimit",
458 parameters.SolutionLimit);
459 AddParameter(
460 arguments,
461 "IterationLimit",
462 parameters.IterationLimit);
463 AddParameter(
464 arguments,
465 "Seed",
466 parameters.RandomSeed);
467
468 if (parameters.EnablePresolve == false)
469 {
470 arguments.Add("Presolve=0");
471 }
472
473 if (parameters.EnableCuts == false)
474 {
475 arguments.Add("Cuts=0");
476 }
477
478 if (parameters.EnableHeuristics == false)
479 {
480 arguments.Add("Heuristics=0");
481 }
482
483 if (parameters.MemoryLimitMegabytes.HasValue)
484 {
485 messages.Add(
486 "Generic parameter 'MemoryLimitMegabytes' is valid but is " +
487 "not translated by the command-line Gurobi adapter.");
488 }
489
490 if (parameters.DeterministicMode)
491 {
492 messages.Add(
493 "Generic parameter 'DeterministicMode' is valid but is not " +
494 "translated by the command-line Gurobi adapter.");
495 }
496
497 foreach (KeyValuePair<string, string> parameter in
498 EnumerateNativeParameters(parameters.NativeParameters))
499 {
500 if (string.IsNullOrWhiteSpace(parameter.Key) ||
501 parameter.Key.Contains('=') ||
502 parameter.Key.Any(char.IsWhiteSpace))
503 {
504 messages.Add(
505 $"Ignored invalid Gurobi native parameter name " +
506 $"'{parameter.Key}'.");
507 continue;
508 }
509
510 arguments.Add(
511 $"{parameter.Key}={parameter.Value}");
512 }
513
514 arguments.Add(modelPath);
515 diagnostics = messages;
516 return arguments;
517 }
518
519 private static void AddParameter<T>(
520 ICollection<string> arguments,
521 string name,
522 T? value)
523 where T : struct, IFormattable
524 {
525 if (!value.HasValue)
526 {
527 return;
528 }
529
530 arguments.Add(
531 $"{name}={value.Value.ToString(null, CultureInfo.InvariantCulture)}");
532 }
533
534 private static void PopulateStatistics(
535 MathematicalModelSolveResult result,
536 MathematicalModel model,
537 IReadOnlyDictionary<int, double> values,
538 string output)
539 {
540 if (TryParseBestObjectiveAndBound(
541 output,
542 out double incumbent,
543 out double bound))
544 {
545 result.ObjectiveValue = incumbent;
546 result.BestBound = bound;
547 }
548 else if (TryParseOptimalObjective(
549 output,
550 out double optimalObjective))
551 {
552 result.ObjectiveValue = optimalObjective;
553 result.BestBound = optimalObjective;
554 }
555 else if (result.HasFeasibleSolution &&
556 ExternalSolverResultUtilities.TryEvaluateObjective(
557 model,
558 values,
559 out double recomputedObjective))
560 {
561 result.ObjectiveValue = recomputedObjective;
562 result.AddDiagnostic(
563 "Gurobi objective was recomputed from the returned solution " +
564 "because no objective value could be parsed from the log.");
565 }
566
567 Match nodeMatch =
568 NodeCountRegex().Match(output);
569
570 if (nodeMatch.Success &&
571 long.TryParse(
572 nodeMatch.Groups[1].Value,
573 NumberStyles.Integer,
574 CultureInfo.InvariantCulture,
575 out long nodes))
576 {
577 result.ExploredNodeCount = nodes;
578 }
579
580 Match iterationMatch =
581 IterationCountRegex().Match(output);
582
583 if (iterationMatch.Success &&
584 long.TryParse(
585 iterationMatch.Groups[1].Value,
586 NumberStyles.Integer,
587 CultureInfo.InvariantCulture,
588 out long iterations))
589 {
590 result.IterationCount = iterations;
591 }
592
593 ExternalSolverResultUtilities.PopulateGapStatistics(
594 result);
595 }
596
597 private static SolverTerminationReason MapTerminationReason(
598 string output,
599 bool hasSolution,
600 bool wasCancelled)
601 {
602 if (wasCancelled)
603 {
604 return SolverTerminationReason.UserInterrupted;
605 }
606
607 if (IsOptimal(output))
608 {
609 return SolverTerminationReason.Optimal;
610 }
611
612 if (output.Contains(
613 "infeasible or unbounded",
614 StringComparison.OrdinalIgnoreCase))
615 {
616 return SolverTerminationReason.InfeasibleOrUnbounded;
617 }
618
619 if (output.Contains(
620 "model is infeasible",
621 StringComparison.OrdinalIgnoreCase))
622 {
623 return SolverTerminationReason.Infeasible;
624 }
625
626 if (output.Contains(
627 "model is unbounded",
628 StringComparison.OrdinalIgnoreCase))
629 {
630 return SolverTerminationReason.Unbounded;
631 }
632
633 if (output.Contains(
634 "time limit reached",
635 StringComparison.OrdinalIgnoreCase))
636 {
637 return SolverTerminationReason.TimeLimit;
638 }
639
640 if (output.Contains(
641 "node limit reached",
642 StringComparison.OrdinalIgnoreCase))
643 {
644 return SolverTerminationReason.NodeLimit;
645 }
646
647 if (output.Contains(
648 "iteration limit reached",
649 StringComparison.OrdinalIgnoreCase))
650 {
651 return SolverTerminationReason.IterationLimit;
652 }
653
654 if (output.Contains(
655 "solution limit reached",
656 StringComparison.OrdinalIgnoreCase))
657 {
658 return SolverTerminationReason.SolutionLimit;
659 }
660
661 return hasSolution
662 ? SolverTerminationReason.Feasible
663 : SolverTerminationReason.Unknown;
664 }
665
666 private static bool IsOptimal(
667 string output)
668 {
669 return output.Contains(
670 "optimal solution found",
671 StringComparison.OrdinalIgnoreCase) ||
672 output.Contains(
673 "optimal objective",
674 StringComparison.OrdinalIgnoreCase);
675 }
676
677 private static bool TryParseBestObjectiveAndBound(
678 string output,
679 out double objective,
680 out double bound)
681 {
682 objective = default;
683 bound = default;
684
685 Match match =
686 BestObjectiveRegex().Match(output);
687
688 return match.Success &&
689 TryParseFinite(match.Groups[1].Value, out objective) &&
690 TryParseFinite(match.Groups[2].Value, out bound);
691 }
692
693 private static bool TryParseOptimalObjective(
694 string output,
695 out double objective)
696 {
697 objective = default;
698
699 Match match =
700 OptimalObjectiveRegex().Match(output);
701
702 return match.Success &&
703 TryParseFinite(match.Groups[1].Value, out objective);
704 }
705
706 private string ResolveExecutablePath()
707 {
708 return ExternalSolverExecutableLocator.Resolve(
709 ExplicitExecutableEnvironmentVariable,
710 ["GUROBI_HOME"],
711 [
712 Path.Combine("bin", "gurobi_cl.exe"),
713 Path.Combine("bin", "gurobi_cl")
714 ],
715 ["gurobi_cl.exe", "gurobi_cl"]);
716 }
717
718 private static string JoinOutput(
719 ExternalSolverProcessResult processResult)
720 {
721 return string.Join(
722 Environment.NewLine,
723 new[]
724 {
725 processResult.StandardOutput,
726 processResult.StandardError
727 }.Where(text => !string.IsNullOrWhiteSpace(text)));
728 }
729
730 private static string ParseVersion(
731 string text)
732 {
733 Match match =
734 VersionRegex().Match(text ?? string.Empty);
735
736 return match.Success
737 ? match.Groups[1].Value
738 : string.Empty;
739 }
740
741 private static bool TryParseFinite(
742 string text,
743 out double value)
744 {
745 return double.TryParse(
746 text,
747 NumberStyles.Float,
748 CultureInfo.InvariantCulture,
749 out value) &&
750 double.IsFinite(value);
751 }
752
753 private static string? FirstMeaningfulLine(
754 string text)
755 {
756 return SplitLines(text).FirstOrDefault();
757 }
758
759 private static string? LastMeaningfulLine(
760 string text)
761 {
762 return SplitLines(text).LastOrDefault();
763 }
764
765 private static IEnumerable<string> SplitLines(
766 string text)
767 {
768 return (text ?? string.Empty)
769 .Split(
770 ['\r', '\n'],
771 StringSplitOptions.RemoveEmptyEntries |
772 StringSplitOptions.TrimEntries)
773 .Where(line => !string.IsNullOrWhiteSpace(line));
774 }
775
776 [GeneratedRegex(
777 @"Gurobi Optimizer version\s+([0-9]+(?:\.[0-9]+)+)",
778 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
779 private static partial Regex VersionRegex();
780
781 [GeneratedRegex(
782 @"Best objective\s+([+\-0-9.eE]+),\s*best bound\s+([+\-0-9.eE]+)",
783 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
784 private static partial Regex BestObjectiveRegex();
785
786 [GeneratedRegex(
787 @"Optimal objective\s+([+\-0-9.eE]+)",
788 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
789 private static partial Regex OptimalObjectiveRegex();
790
791 [GeneratedRegex(
792 @"Explored\s+([0-9]+)\s+nodes",
793 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
794 private static partial Regex NodeCountRegex();
795
796 [GeneratedRegex(
797 @"([0-9]+)\s+simplex iterations",
798 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
799 private static partial Regex IterationCountRegex();
800 private static SolverProgressStage ResolveOptimizationProgressStage()
801 {
802 foreach (string candidate in
803 new[] { "Optimizing", "Solving", "Running" })
804 {
805 if (Enum.TryParse(
806 candidate,
807 ignoreCase: true,
808 out SolverProgressStage stage))
809 {
810 return stage;
811 }
812 }
813
814 // BuildingModel exists in the current solver contract and is the
815 // safest fallback for older contracts that do not expose a dedicated
816 // optimization/running stage. The accompanying message still reports
817 // the actual activity precisely.
818 return SolverProgressStage.BuildingModel;
819 }
820
821 private static IEnumerable<KeyValuePair<string, string>>
822 EnumerateNativeParameters(
823 System.Collections.IEnumerable? nativeParameters)
824 {
825 if (nativeParameters is null)
826 {
827 yield break;
828 }
829
830 foreach (object? item in nativeParameters)
831 {
832 if (item is null)
833 {
834 continue;
835 }
836
837 if (item is KeyValuePair<string, string> pair)
838 {
839 yield return pair;
840 continue;
841 }
842
843 object? nameValue =
844 ReadNativeParameterMember(
845 item,
846 "Name",
847 "Key",
848 "ParameterName",
849 "Parameter");
850
851 object? parameterValue =
852 ReadNativeParameterMember(
853 item,
854 "Value",
855 "ParameterValue");
856
857 string name =
858 Convert.ToString(
859 nameValue,
860 CultureInfo.InvariantCulture) ??
861 string.Empty;
862
863 string value =
864 Convert.ToString(
865 parameterValue,
866 CultureInfo.InvariantCulture) ??
867 string.Empty;
868
869 if (!string.IsNullOrWhiteSpace(name))
870 {
871 yield return new KeyValuePair<string, string>(
872 name,
873 value);
874 }
875 }
876 }
877
878 private static object? ReadNativeParameterMember(
879 object instance,
880 params string[] candidateNames)
881 {
882 Type type = instance.GetType();
883
884 foreach (string candidateName in candidateNames)
885 {
886 var property =
887 type.GetProperties()
888 .FirstOrDefault(
889 candidate =>
890 candidate.CanRead &&
891 string.Equals(
892 candidate.Name,
893 candidateName,
894 StringComparison.OrdinalIgnoreCase));
895
896 if (property is not null)
897 {
898 return property.GetValue(instance);
899 }
900
901 var field =
902 type.GetFields()
903 .FirstOrDefault(
904 candidate =>
905 string.Equals(
906 candidate.Name,
907 candidateName,
908 StringComparison.OrdinalIgnoreCase));
909
910 if (field is not null)
911 {
912 return field.GetValue(instance);
913 }
914 }
915
916 return null;
917 }
918
919}
GurobiSolverAdapter()
Initializes the Gurobi solver adapter.
override async ValueTask< MathematicalModelSolveResult > SolveCoreAsync(MathematicalModelSolveRequest request, CancellationToken cancellationToken)
override async ValueTask< SolverAvailabilityInfo > CheckAvailabilityAsync(CancellationToken cancellationToken=default)