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