23 MathematicalModelSolverPluginBase
25 private const string ExplicitExecutableEnvironmentVariable =
26 "LOTSIZING_CBC_EXECUTABLE";
28 private readonly ExternalSolverProcessRunner _processRunner =
31 private string _cachedExecutablePath =
34 private string _cachedVersion =
43 SolverCapability.LinearProgramming,
44 SolverCapability.MixedIntegerLinearProgramming,
45 SolverCapability.Interruption,
46 SolverCapability.LpExport,
47 SolverCapability.OptimalityGapReporting,
48 SolverCapability.SearchStatistics
67 "LotSizingDataModel.Solver.CoinOrCbc";
71 "LotSizingDataModel COIN-OR CBC Adapter";
82 public override async ValueTask<SolverAvailabilityInfo>
84 CancellationToken cancellationToken =
default)
86 cancellationToken.ThrowIfCancellationRequested();
88 string executablePath =
89 ResolveExecutablePath();
91 if (
string.IsNullOrWhiteSpace(executablePath))
94 new SolverAvailabilityInfo(
96 SolverAvailabilityStatus.NotInstalled)
101 notInstalled.AddDiagnostic(
102 "cbc executable was not found. Put cbc on PATH, define " +
103 "CBC_HOME or COINOR_HOME, or set " +
104 $
"{ExplicitExecutableEnvironmentVariable}.");
111 string workingDirectory =
112 Path.GetDirectoryName(executablePath) ??
113 Environment.CurrentDirectory;
115 ExternalSolverProcessResult processResult =
116 await _processRunner.RunAsync(
123 if (processResult.WasCancelled)
125 cancellationToken.ThrowIfCancellationRequested();
129 JoinOutput(processResult);
131 if (processResult.ExitCode != 0)
134 new SolverAvailabilityInfo(
136 SolverAvailabilityStatus.LoadFailure)
141 failed.AddDiagnostic(
142 FirstMeaningfulLine(output) ??
143 $
"cbc exited with code {processResult.ExitCode}.");
148 _cachedExecutablePath =
152 ParseVersion(output);
155 new SolverAvailabilityInfo(
157 SolverAvailabilityStatus.Available)
163 available.AddDiagnostic(
164 $
"CBC executable detected at '{executablePath}'.");
166 if (!
string.IsNullOrWhiteSpace(output))
168 available.AddDiagnostic(
169 FirstMeaningfulLine(output) ?? output.Trim());
174 catch (OperationCanceledException)
178 catch (Exception exception)
181 new SolverAvailabilityInfo(
183 SolverAvailabilityStatus.LoadFailure)
188 failed.AddDiagnostic(exception.Message);
194 protected override async ValueTask<MathematicalModelSolveResult>
196 MathematicalModelSolveRequest request,
197 CancellationToken cancellationToken)
199 ArgumentNullException.ThrowIfNull(request);
200 ArgumentNullException.ThrowIfNull(request.Model);
202 request.Parameters.EnsureValid();
204 string executablePath =
205 !
string.IsNullOrWhiteSpace(_cachedExecutablePath) &&
206 File.Exists(_cachedExecutablePath)
207 ? _cachedExecutablePath
208 : ResolveExecutablePath();
210 if (
string.IsNullOrWhiteSpace(executablePath))
212 throw new InvalidOperationException(
213 "COIN-OR CBC is not available. Call solver discovery/" +
214 "availability before attempting to solve.");
217 string temporaryDirectory =
218 ExternalSolverResultUtilities.CreateTemporaryDirectory(
226 string solutionPath =
232 Stopwatch.StartNew();
236 await PublishProgressAsync(
238 new SolverProgressSnapshot
240 Stage = SolverProgressStage.BuildingModel,
241 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
242 Message =
"Writing the portable LP model for COIN-OR CBC."
246 new PortableLpModelWriter().Write(
250 if (request.Parameters.ExportModel)
252 ExternalSolverResultUtilities.ExportPortableModel(
254 request.Parameters.ExportModelPath);
257 IReadOnlyList<string> parameterDiagnostics;
258 IReadOnlyList<string> arguments =
263 out parameterDiagnostics);
265 await PublishProgressAsync(
267 new SolverProgressSnapshot
269 Stage = ResolveOptimizationProgressStage(),
270 ElapsedSeconds = stopwatch.Elapsed.TotalSeconds,
271 Message =
"COIN-OR CBC optimization started."
275 ExternalSolverProcessResult processResult =
276 await _processRunner.RunAsync(
286 JoinOutput(processResult);
289 new Dictionary<int, double>(
290 NamedSolutionValueParser.ParseFile(solutionPath));
292 bool solutionFileExists =
293 File.Exists(solutionPath);
296 solutionFileExists &&
297 !SolutionFileSaysNoSolution(solutionPath);
303 foreach (MathematicalVariable variable
in request.Model.Variables)
312 new MathematicalModelSolveResult
317 !
string.IsNullOrWhiteSpace(_cachedVersion)
319 : ParseVersion(output),
320 SolveDuration = stopwatch.Elapsed,
321 HasFeasibleSolution = hasSolution,
323 MapTerminationReason(
327 processResult.WasCancelled),
329 IsOptimal(output, solutionPath)
332 ExternalSolverResultUtilities.PopulateVariableValues(
344 foreach (
string diagnostic
in parameterDiagnostics)
346 result.AddDiagnostic(diagnostic);
349 result.AddDiagnostic(
350 $
"cbc exit code: {processResult.ExitCode}.");
353 LastMeaningfulLine(output);
355 if (!
string.IsNullOrWhiteSpace(finalLine))
357 result.AddDiagnostic(
358 $
"CBC final log: {finalLine}");
361 await PublishProgressAsync(
363 new SolverProgressSnapshot
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,
374 $
"COIN-OR CBC terminated with reason " +
375 $
"'{result.TerminationReason}'."
377 CancellationToken.None);
383 ExternalSolverResultUtilities.TryDeleteDirectory(
391 _processRunner.Stop();
394 private static IReadOnlyList<string> BuildArguments(
395 SolverParameters parameters,
398 out IReadOnlyList<string> diagnostics)
412 parameters.TimeLimitSeconds);
416 parameters.ThreadCount);
420 parameters.RelativeMipGap);
424 parameters.AbsoluteMipGap);
428 parameters.NodeLimit);
432 parameters.SolutionLimit);
436 parameters.RandomSeed);
438 if (parameters.EnablePresolve.HasValue)
440 arguments.Add(
"-presolve");
442 parameters.EnablePresolve.Value
447 if (parameters.EnableCuts ==
false)
449 arguments.Add(
"-cuts");
450 arguments.Add(
"off");
453 if (parameters.EnableHeuristics ==
false)
455 arguments.Add(
"-heuristicsOnOff");
456 arguments.Add(
"off");
459 if (parameters.IterationLimit.HasValue)
462 "Generic parameter 'IterationLimit' is valid but is not " +
463 "translated by the CBC command-line adapter.");
466 if (parameters.MemoryLimitMegabytes.HasValue)
469 "Generic parameter 'MemoryLimitMegabytes' is valid but is " +
470 "not translated by the CBC command-line adapter.");
473 if (parameters.DeterministicMode)
476 "Generic parameter 'DeterministicMode' is valid but is not " +
477 "translated by the CBC command-line adapter.");
480 foreach (KeyValuePair<string, string> parameter
in
481 EnumerateNativeParameters(parameters.NativeParameters))
483 if (
string.IsNullOrWhiteSpace(parameter.Key) ||
484 parameter.Key.Any(
char.IsWhiteSpace))
487 $
"Ignored invalid CBC native parameter name " +
488 $
"'{parameter.Key}'.");
493 parameter.Key.StartsWith(
'-')
495 : $
"-{parameter.Key}");
496 arguments.Add(parameter.Value);
499 arguments.Add(
"-printingOptions");
500 arguments.Add(
"all");
501 arguments.Add(
"-solve");
502 arguments.Add(
"-solu");
503 arguments.Add(solutionPath);
504 arguments.Add(
"-quit");
506 diagnostics = messages;
510 private static void AddParameter<T>(
511 ICollection<string> arguments,
514 where T :
struct, IFormattable
521 arguments.Add($
"-{name}");
523 value.Value.ToString(
525 CultureInfo.InvariantCulture));
528 private static void PopulateStatistics(
529 MathematicalModelSolveResult result,
530 MathematicalModel model,
531 IReadOnlyDictionary<int, double> values,
535 if (TryParseSolutionObjective(
537 out
double solutionObjective))
539 result.ObjectiveValue = solutionObjective;
541 else if (result.HasFeasibleSolution &&
542 ExternalSolverResultUtilities.TryEvaluateObjective(
545 out
double recomputedObjective))
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 " +
555 BestPossibleRegex().Match(output);
557 if (boundMatch.Success &&
559 boundMatch.Groups[1].Value,
562 result.BestBound = bound;
564 else if (result.IsOptimal &&
565 result.ObjectiveValue.HasValue)
567 result.BestBound = result.ObjectiveValue;
571 NodesRegex().Matches(output)
573 .LastOrDefault(match => match.Success);
575 if (nodesMatch is not
null &&
576 nodesMatch.Success &&
578 nodesMatch.Groups[2].Value,
579 NumberStyles.Integer,
580 CultureInfo.InvariantCulture,
583 result.ExploredNodeCount = nodes;
586 if (nodesMatch is not
null &&
587 nodesMatch.Success &&
589 nodesMatch.Groups[1].Value,
590 NumberStyles.Integer,
591 CultureInfo.InvariantCulture,
592 out
long iterations))
594 result.IterationCount = iterations;
597 ExternalSolverResultUtilities.PopulateGapStatistics(
601 private static SolverTerminationReason MapTerminationReason(
609 return SolverTerminationReason.UserInterrupted;
612 if (IsOptimal(output, solutionPath))
614 return SolverTerminationReason.Optimal;
619 "infeasible or unbounded"))
621 return SolverTerminationReason.InfeasibleOrUnbounded;
626 "problem is infeasible",
627 "linear relaxation infeasible",
630 return SolverTerminationReason.Infeasible;
637 return SolverTerminationReason.Unbounded;
642 "exiting on maximum time",
645 return SolverTerminationReason.TimeLimit;
650 "exiting on maximum nodes"))
652 return SolverTerminationReason.NodeLimit;
657 "exiting on maximum solutions"))
659 return SolverTerminationReason.SolutionLimit;
663 ? SolverTerminationReason.Feasible
664 : SolverTerminationReason.Unknown;
667 private static bool IsOptimal(
673 "optimal solution found",
674 "result - optimal solution found"))
680 ReadFirstLine(solutionPath);
682 return firstLine.StartsWith(
684 StringComparison.OrdinalIgnoreCase);
687 private static bool SolutionFileSaysNoSolution(
691 ReadFirstLine(solutionPath);
693 return firstLine.Contains(
694 "no integer solution",
695 StringComparison.OrdinalIgnoreCase) ||
698 StringComparison.OrdinalIgnoreCase) ||
701 StringComparison.OrdinalIgnoreCase);
704 private static bool TryParseSolutionObjective(
706 out
double objective)
711 ReadFirstLine(solutionPath);
713 return NamedSolutionValueParser.TryParseNumberAfter(
715 @"objective\s+value\s*",
719 private static string ReadFirstLine(
722 if (
string.IsNullOrWhiteSpace(path) ||
729 new StreamReader(path);
731 return reader.ReadLine() ??
string.Empty;
734 private string ResolveExecutablePath()
736 return ExternalSolverExecutableLocator.Resolve(
737 ExplicitExecutableEnvironmentVariable,
738 [
"CBC_HOME",
"COINOR_HOME"],
740 Path.Combine(
"bin",
"cbc.exe"),
741 Path.Combine(
"bin",
"cbc"),
748 private static string ParseVersion(
752 VersionRegex().Match(text ??
string.Empty);
755 ? match.Groups[1].Value
759 private static string JoinOutput(
760 ExternalSolverProcessResult processResult)
766 processResult.StandardOutput,
767 processResult.StandardError
768 }.Where(text => !
string.IsNullOrWhiteSpace(text)));
771 private static string? FirstMeaningfulLine(
774 return SplitLines(text).FirstOrDefault();
777 private static string? LastMeaningfulLine(
780 return SplitLines(text).LastOrDefault();
783 private static IEnumerable<string> SplitLines(
786 return (text ??
string.Empty)
789 StringSplitOptions.RemoveEmptyEntries |
790 StringSplitOptions.TrimEntries)
791 .Where(line => !
string.IsNullOrWhiteSpace(line));
794 private static bool TryParseFinite(
798 return double.TryParse(
801 CultureInfo.InvariantCulture,
803 double.IsFinite(value);
806 private static bool ContainsAny(
808 params
string[] patterns)
814 StringComparison.OrdinalIgnoreCase));
818 @"(?:Version|version)\s*:?\s*([0-9]+(?:\.[0-9]+)+)",
819 RegexOptions.CultureInvariant)]
820 private static partial Regex VersionRegex();
823 @"best possible\s+([+\-0-9.eE]+)",
824 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
825 private static partial Regex BestPossibleRegex();
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()
833 foreach (
string candidate
in
834 new[] {
"Optimizing",
"Solving",
"Running" })
839 out SolverProgressStage stage))
849 return SolverProgressStage.BuildingModel;
852 private static IEnumerable<KeyValuePair<string, string>>
853 EnumerateNativeParameters(
854 System.Collections.IEnumerable? nativeParameters)
856 if (nativeParameters is
null)
861 foreach (
object? item
in nativeParameters)
868 if (item is KeyValuePair<string, string> pair)
875 ReadNativeParameterMember(
882 object? parameterValue =
883 ReadNativeParameterMember(
891 CultureInfo.InvariantCulture) ??
897 CultureInfo.InvariantCulture) ??
900 if (!
string.IsNullOrWhiteSpace(name))
902 yield
return new KeyValuePair<string, string>(
909 private static object? ReadNativeParameterMember(
911 params
string[] candidateNames)
913 Type type = instance.GetType();
915 foreach (
string candidateName
in candidateNames)
925 StringComparison.OrdinalIgnoreCase));
927 if (property is not
null)
929 return property.GetValue(instance);
939 StringComparison.OrdinalIgnoreCase));
941 if (field is not
null)
943 return field.GetValue(instance);