ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
LsCuttingPlaneSolverBase.cs
Go to the documentation of this file.
1using System.Diagnostics;
15
17
18/// <summary>
19/// Base implementation of an exact ULS cut-and-solve algorithm using classical
20/// (l,S) inequalities at the root LP relaxation followed by an exact MILP solve.
21/// </summary>
22public abstract class LsCuttingPlaneSolverBase :
25{
26 private readonly ILsCutSeparator _separator;
27 private readonly LinearModelSolver _modelSolver;
28 private readonly LinearModelSolveOptions _executionOptions;
29 private readonly LsCuttingPlaneOptions _cuttingPlaneOptions;
30
32 string name,
33 ILsCutSeparator separator,
34 LinearModelSolver? modelSolver = null,
35 LinearModelSolveOptions? executionOptions = null,
36 LsCuttingPlaneOptions? cuttingPlaneOptions = null)
37 {
38 if (string.IsNullOrWhiteSpace(name))
39 {
40 throw new ArgumentException(
41 "A solver name is required.",
42 nameof(name));
43 }
44
45 Name = name.Trim();
46 _separator =
47 separator ??
48 throw new ArgumentNullException(nameof(separator));
49 _modelSolver =
50 modelSolver ??
52 _executionOptions =
53 CloneExecutionOptions(
54 executionOptions ??
56 _cuttingPlaneOptions =
57 CloneCuttingPlaneOptions(
58 cuttingPlaneOptions ??
60 }
61
62 public string Name { get; }
63
65 UlsSolverKind.Exact;
66
68 _separator.Method;
69
70 public bool IsApplicable(
71 UlsProblem problem)
72 {
73 ArgumentNullException.ThrowIfNull(problem);
74 return _separator.IsApplicable(problem);
75 }
76
78 UlsProblem problem,
79 CancellationToken cancellationToken = default)
80 {
81 ArgumentNullException.ThrowIfNull(problem);
82 cancellationToken.ThrowIfCancellationRequested();
83
84 return Task.Run(
85 async () =>
86 await SolveAsync(
87 problem,
88 cancellationToken)
89 .ConfigureAwait(false),
90 CancellationToken.None)
91 .GetAwaiter()
92 .GetResult();
93 }
94
95 public async ValueTask<UlsSolveResult> SolveAsync(
96 UlsProblem problem,
97 CancellationToken cancellationToken = default)
98 {
99 ArgumentNullException.ThrowIfNull(problem);
100 cancellationToken.ThrowIfCancellationRequested();
101
102 if (!IsApplicable(problem))
103 {
104 throw new NotSupportedException(
105 $"{Name} is not applicable to the supplied ULS cost structure.");
106 }
107
108 var formulationBuilder =
110
111 UlsFormulation aggregate =
112 formulationBuilder.Build(problem);
113
114 LinearModel currentLp =
116 aggregate.Model);
117
118 var addedCuts =
119 new List<(string Name, LsCutDefinition Cut)>();
120
121 var knownCuts =
122 new HashSet<string>(
123 StringComparer.Ordinal);
124
125 var iterationReports =
126 new List<CutIterationReport>();
127
128 var convergenceIterations =
129 new List<CuttingPlaneIterationStatistics>();
130
131 int sequence = 0;
132
133 SolverExecutionInfo? selectedSolver =
134 null;
135
136 for (int iteration = 0;
137 iteration < _cuttingPlaneOptions.MaximumIterations;
138 iteration++)
139 {
140 cancellationToken.ThrowIfCancellationRequested();
141
142 LinearModelSolveOptions iterationOptions =
143 selectedSolver is null
144 ? CloneExecutionOptions(_executionOptions)
145 : CreatePinnedExecutionOptions(
146 _executionOptions,
147 selectedSolver.SelectedSolver);
148
149 LinearModelSolveResult lpExecution =
150 await _modelSolver.SolveAsync(
151 currentLp,
152 iterationOptions,
153 cancellationToken)
154 .ConfigureAwait(false);
155
156 if (lpExecution.Solver is not null &&
157 selectedSolver is null)
158 {
159 selectedSolver =
160 lpExecution.Solver;
161 }
162
163 if (!lpExecution.HasFeasibleSolution ||
164 !lpExecution.ObjectiveValue.HasValue)
165 {
166 return BuildEarlyFailure(
167 lpExecution,
168 selectedSolver,
169 iterationReports,
170 convergenceIterations,
171 "Root LP relaxation could not be solved to a valid " +
172 "feasible point.");
173 }
174
175 var stopwatch =
176 Stopwatch.StartNew();
177
178 IReadOnlyList<LsSeparatedCut> candidates =
179 _separator.Separate(
180 problem,
181 aggregate,
182 lpExecution.VariableValues);
183
184 stopwatch.Stop();
185
186 var records =
187 new List<CutRecord>(
188 candidates.Count);
189
190 var eligibleIndices =
191 new List<int>();
192
193 var preDisposition =
194 new Dictionary<int, (CutDisposition Disposition, string Reason)>();
195
196 var keys =
197 new string[candidates.Count];
198
199 var seenThisIteration =
200 new HashSet<string>(
201 StringComparer.Ordinal);
202
203 for (int index = 0;
204 index < candidates.Count;
205 index++)
206 {
207 LsSeparatedCut candidate =
208 candidates[index];
209
210 string key =
212 candidate.Definition);
213
214 keys[index] = key;
215
216 if (candidate.Violation <=
217 _cuttingPlaneOptions.ViolationTolerance)
218 {
219 preDisposition[index] =
220 (
221 CutDisposition.BelowTolerance,
222 $"Violation {candidate.Violation:G17} <= " +
223 $"tolerance " +
224 $"{_cuttingPlaneOptions.ViolationTolerance:G17}."
225 );
226
227 continue;
228 }
229
230 if (candidate.Efficacy <
231 _cuttingPlaneOptions.MinimumEfficacy)
232 {
233 preDisposition[index] =
234 (
235 CutDisposition.NotSelected,
236 $"Efficacy {candidate.Efficacy:G17} < minimum " +
237 $"{_cuttingPlaneOptions.MinimumEfficacy:G17}."
238 );
239
240 continue;
241 }
242
243 if (knownCuts.Contains(key) ||
244 !seenThisIteration.Add(key))
245 {
246 preDisposition[index] =
247 (
248 CutDisposition.Duplicate,
249 "An equivalent (l,S) cut is already present."
250 );
251
252 continue;
253 }
254
255 eligibleIndices.Add(index);
256 }
257
258 HashSet<int> selectedIndices =
260 candidates,
261 eligibleIndices,
262 _cuttingPlaneOptions);
263
264 var newlyAdded =
265 new List<(string Name, LsCutDefinition Cut)>();
266
267 int selectedCount = 0;
268
269 for (int index = 0;
270 index < candidates.Count;
271 index++)
272 {
273 LsSeparatedCut candidate =
274 candidates[index];
275
276 CutDisposition disposition;
277 string reason;
278 string rowName =
279 string.Empty;
280
281 if (preDisposition.TryGetValue(
282 index,
283 out var prior))
284 {
285 disposition =
286 prior.Disposition;
287
288 reason =
289 prior.Reason;
290 }
291 else if (!selectedIndices.Contains(index))
292 {
293 disposition =
294 CutDisposition.NotSelected;
295
296 reason =
297 $"Eligible violated cut not selected by policy " +
298 $"'{_cuttingPlaneOptions.SelectionPolicy}'.";
299 }
300 else
301 {
302 disposition =
303 CutDisposition.Added;
304
305 rowName =
306 $"ls_{_separator.Method}_{iteration}_{sequence}";
307
308 reason =
309 $"Selected by '{_cuttingPlaneOptions.SelectionPolicy}' " +
310 "and added to the portable LP model.";
311
312 selectedCount++;
313
314 knownCuts.Add(
315 keys[index]);
316
317 newlyAdded.Add(
318 (rowName, candidate.Definition));
319
320 addedCuts.Add(
321 (rowName, candidate.Definition));
322 }
323
324 records.Add(
325 new CutRecord(
326 sequence++,
327 iteration,
328 _separator.Method,
329 candidate.Definition,
330 candidate.Violation,
331 candidate.Efficacy,
332 disposition,
333 rowName,
334 reason));
335 }
336
337 var iterationReport =
339 iteration,
340 records,
341 stopwatch.Elapsed);
342
343 iterationReports.Add(
344 iterationReport);
345
346 double[] positiveViolations =
347 candidates
348 .Where(
349 candidate =>
350 candidate.Violation > 0.0)
351 .Select(
352 candidate =>
353 candidate.Violation)
354 .ToArray();
355
356 convergenceIterations.Add(
358 iteration,
359 lpExecution.ObjectiveValue.Value,
360 lpExecution.SolveDuration,
361 stopwatch.Elapsed,
362 generatedCandidates:
363 candidates.Count,
364 eligibleCandidates:
365 eligibleIndices.Count,
366 selectedCuts:
367 selectedCount,
368 cutsAdded:
369 newlyAdded.Count,
370 cumulativeCutsAdded:
371 addedCuts.Count,
372 maximumViolation:
373 candidates.Count == 0
374 ? 0.0
375 : candidates.Max(
376 candidate =>
377 candidate.Violation),
378 meanPositiveViolation:
379 positiveViolations.Length == 0
380 ? 0.0
381 : positiveViolations.Average(),
382 maximumEfficacy:
383 candidates.Count == 0
384 ? 0.0
385 : candidates.Max(
386 candidate =>
387 candidate.Efficacy)));
388
389 if (newlyAdded.Count == 0)
390 {
391 break;
392 }
393
394 currentLp =
396 currentLp,
397 newlyAdded);
398 }
399
400 if (selectedSolver is null)
401 {
402 return new UlsSolveResult(
403 Name,
404 UlsSolveStatus.NotSolved,
405 message:
406 "No optimization solver was selected during root separation.");
407 }
408
409 LinearModel strengthenedMip =
411 aggregate.Model,
412 addedCuts);
413
414 LinearModelSolveOptions finalOptions =
415 CreatePinnedExecutionOptions(
416 _executionOptions,
417 selectedSolver.SelectedSolver);
418
419 LinearModelSolveResult finalExecution =
420 await _modelSolver.SolveAsync(
421 strengthenedMip,
422 finalOptions,
423 cancellationToken)
424 .ConfigureAwait(false);
425
426 var cutReport =
428 iterationReports);
429
430 var convergence =
432 convergenceIterations,
433 finalExecution.ObjectiveValue);
434
435 var executionReport =
437 selectedSolver,
438 cutReport,
439 convergence);
440
441 if (!finalExecution.HasFeasibleSolution)
442 {
444 Name,
445 MapStatusWithoutSolution(
446 finalExecution.Status),
447 _separator.Method,
448 executionReport,
449 finalExecution,
450 solution: null,
451 message:
452 BuildMessage(
453 finalExecution,
454 cutReport,
455 convergence));
456 }
457
458 try
459 {
460 UlsSolution solution =
462 problem,
463 aggregate,
464 finalExecution.VariableValues,
465 finalOptions.ZeroTolerance,
466 finalOptions.FeasibilityTolerance);
467
468 UlsSolutionValidationResult validation =
470 problem,
471 solution,
472 finalOptions.FeasibilityTolerance);
473
474 if (!validation.IsFeasible)
475 {
477 Name,
478 UlsSolveStatus.Failed,
479 _separator.Method,
480 executionReport,
481 finalExecution,
482 solution: null,
483 message:
484 "ULS-domain validation rejected the reconstructed " +
485 "cutting-plane solution: " +
486 string.Join(
487 " | ",
488 validation.Diagnostics));
489 }
490
491 if (finalExecution.ObjectiveValue.HasValue &&
492 !ObjectivesAgree(
493 finalExecution.ObjectiveValue.Value,
494 solution.TotalCost,
495 finalOptions.FeasibilityTolerance))
496 {
498 Name,
499 UlsSolveStatus.Failed,
500 _separator.Method,
501 executionReport,
502 finalExecution,
503 solution: null,
504 message:
505 $"Portable-model objective " +
506 $"{finalExecution.ObjectiveValue.Value:G17} differs " +
507 $"from reconstructed ULS objective " +
508 $"{solution.TotalCost:G17}.");
509 }
510
511 UlsSolveStatus finalStatus =
512 finalExecution.Status ==
515 : UlsSolveStatus.Feasible;
516
518 Name,
519 finalStatus,
520 _separator.Method,
521 executionReport,
522 finalExecution,
523 solution,
524 BuildMessage(
525 finalExecution,
526 cutReport,
527 convergence));
528 }
529 catch (Exception exception)
530 when (exception is not OperationCanceledException)
531 {
533 Name,
534 UlsSolveStatus.Failed,
535 _separator.Method,
536 executionReport,
537 finalExecution,
538 solution: null,
539 message:
540 $"ULS solution reconstruction failed: " +
541 $"{exception.Message}");
542 }
543 }
544
545 private UlsSolveResult BuildEarlyFailure(
546 LinearModelSolveResult execution,
547 SolverExecutionInfo? selectedSolver,
548 IReadOnlyList<CutIterationReport> iterationReports,
549 IReadOnlyList<CuttingPlaneIterationStatistics> convergenceIterations,
550 string prefix)
551 {
552 if (selectedSolver is null)
553 {
554 return new UlsSolveResult(
555 Name,
556 MapStatusWithoutSolution(
557 execution.Status),
558 message:
559 prefix +
560 " " +
561 string.Join(
562 " | ",
563 execution.Diagnostics));
564 }
565
566 var cutReport =
568 iterationReports);
569
570 var convergence =
572 convergenceIterations,
573 finalMipObjective: null);
574
576 Name,
577 MapStatusWithoutSolution(
578 execution.Status),
579 _separator.Method,
581 selectedSolver,
582 cutReport,
583 convergence),
584 execution,
585 solution: null,
586 message:
587 prefix +
588 " " +
589 BuildMessage(
590 execution,
591 cutReport,
592 convergence));
593 }
594
595 private static UlsSolveStatus MapStatusWithoutSolution(
597 {
598 return status switch
599 {
601 UlsSolveStatus.Infeasible,
602
606 UlsSolveStatus.NotSolved,
607
608 _ =>
610 };
611 }
612
613 private static bool ObjectivesAgree(
614 double modelObjective,
615 double ulsObjective,
616 double tolerance)
617 {
618 double scale =
619 Math.Max(
620 1.0,
621 Math.Max(
622 Math.Abs(modelObjective),
623 Math.Abs(ulsObjective)));
624
625 return Math.Abs(
626 modelObjective -
627 ulsObjective) <=
628 tolerance * scale;
629 }
630
631 private static string BuildMessage(
632 LinearModelSolveResult execution,
633 CutGenerationReport cuts,
634 CuttingPlaneConvergenceReport convergence)
635 {
636 var parts =
637 new List<string>
638 {
639 $"Cuts generated: {cuts.CutsGenerated}",
640 $"cuts added: {cuts.CutsAdded}",
641 $"not selected: {cuts.NotSelected}",
642 $"separation iterations: {cuts.IterationCount}"
643 };
644
645 if (convergence.RootBoundImprovement.HasValue)
646 {
647 parts.Add(
648 $"root bound improvement: " +
649 $"{convergence.RootBoundImprovement.Value:G17}");
650 }
651
652 if (convergence.RootGapClosedFraction.HasValue)
653 {
654 parts.Add(
655 $"root gap closed: " +
656 $"{convergence.RootGapClosedFraction.Value:P2}");
657 }
658
659 if (execution.Solver is not null)
660 {
661 parts.Add(
662 $"Optimization engine: " +
663 $"{execution.Solver.SolverName} " +
664 $"{execution.Solver.SolverVersion}".Trim());
665 }
666
667 if (!string.IsNullOrWhiteSpace(
668 execution.NativeStatus))
669 {
670 parts.Add(
671 $"Native status: {execution.NativeStatus}");
672 }
673
674 parts.AddRange(
675 execution.Diagnostics);
676
677 return string.Join(
678 " | ",
679 parts.Where(
680 static part =>
681 !string.IsNullOrWhiteSpace(part)));
682 }
683
684 private static LinearModelSolveOptions CreatePinnedExecutionOptions(
685 LinearModelSolveOptions source,
686 SolverKind solverKind)
687 {
688 LinearModelSolveOptions result =
689 CloneExecutionOptions(source);
690
691 result.Solver =
692 solverKind;
693
694 result.AllowFallbackWhenExplicit =
695 false;
696
697 return result;
698 }
699
700 private static LinearModelSolveOptions CloneExecutionOptions(
701 LinearModelSolveOptions source)
702 {
703 source.EnsureValid();
704
705 return new LinearModelSolveOptions
706 {
707 Solver = source.Solver,
708 AllowFallbackWhenExplicit =
710 FeasibilityTolerance =
712 ZeroTolerance =
713 source.ZeroTolerance,
714 IntegralityTolerance =
716 NearIntegerTolerance =
718 ExportModelPath =
719 source.ExportModelPath,
720 KeepTemporaryFiles =
721 source.KeepTemporaryFiles,
722 TemporaryRootPath =
723 source.TemporaryRootPath
724 };
725 }
726
727 private static LsCuttingPlaneOptions CloneCuttingPlaneOptions(
728 LsCuttingPlaneOptions source)
729 {
730 source.EnsureValid();
731
732 return new LsCuttingPlaneOptions
733 {
734 MaximumIterations =
735 source.MaximumIterations,
736 ViolationTolerance =
737 source.ViolationTolerance,
738 MinimumEfficacy =
739 source.MinimumEfficacy,
740 SelectionPolicy =
741 source.SelectionPolicy,
742 MaximumCutsPerIteration =
743 source.MaximumCutsPerIteration
744 };
745 }
746}
Complete cutting-plane traceability report for one solver-backed solve.
Traceability report for one cutting-plane iteration.
Trace record for one generated cutting-plane constraint.
Definition CutRecord.cs:7
Summarizes root-bound evolution and separation effort for one exact cut-and-solve execution.
double? RootGapClosedFraction
Gets the fraction of the initial LP-to-MIP gap closed by root cutting planes. Returns null when the d...
double? RootBoundImprovement
Gets absolute improvement of the root lower bound.
Combines solver-selection provenance, cutting-plane traceability and root convergence statistics.
Numerical convergence statistics for one root cutting-plane iteration.
static string Create(LsCutDefinition definition)
Definition LsCutKey.cs:7
static LinearModel AddCuts(LinearModel source, IEnumerable<(string Name, LsCutDefinition Cut)> cuts)
static LinearModel CreateLpRelaxation(LinearModel source)
static HashSet< int > Select(IReadOnlyList< LsSeparatedCut > candidates, IReadOnlyCollection< int > eligibleIndices, LsCuttingPlaneOptions options)
Configures root LP (l,S) separation before the final exact MILP solve.
int MaximumIterations
Gets or sets the maximum number of root separation iterations.
CutSelectionPolicy SelectionPolicy
Gets or sets the cut-pool selection policy.
double MinimumEfficacy
Gets or sets the minimum efficacy required before a cut is eligible. The default zero preserves v0....
double ViolationTolerance
Gets or sets the positive violation required before a cut is eligible.
One candidate (l,S) inequality produced by a separation procedure.
LsCutDefinition Definition
Gets the solver-independent inequality definition.
double Violation
Gets RHS - LHS for the canonical greater-than-or-equal form. Positive values are violated.
double Efficacy
Gets violation divided by the Euclidean coefficient norm.
string Name
Gets the stable human-readable name of the solver.
UlsSolverKind Kind
Gets the broad family of the solver.
LsCuttingPlaneSolverBase(string name, ILsCutSeparator separator, LinearModelSolver? modelSolver=null, LinearModelSolveOptions? executionOptions=null, LsCuttingPlaneOptions? cuttingPlaneOptions=null)
async ValueTask< UlsSolveResult > SolveAsync(UlsProblem problem, CancellationToken cancellationToken=default)
Solves a ULS problem asynchronously.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing problem.
static UlsSolution Map(UlsProblem problem, UlsFormulation formulation, IReadOnlyDictionary< int, double > values, double zeroTolerance, double feasibilityTolerance)
Builds the classical aggregate ULS mixed-integer formulation with production, setup and end-of-period...
Solver-independent ULS mathematical formulation plus semantic variable map.
LinearModel Model
Gets the portable model.
Represents a validated classical uncapacitated lot-sizing problem.
Definition UlsProblem.cs:23
Configures one solver-backed execution of a portable linear model.
SolverKind Solver
Gets or sets the requested solver. The default is automatic selection.
bool AllowFallbackWhenExplicit
Gets or sets whether an explicitly requested solver may fall back to another solver when unavailable.
double NearIntegerTolerance
Gets or sets the tolerance used to clean continuous values that are numerically indistinguishable fro...
double ZeroTolerance
Gets or sets the tolerance used to identify numerical zero before validation and objective reconstruc...
double IntegralityTolerance
Gets or sets the integrality tolerance used both for normalization and the independent solution check...
bool KeepTemporaryFiles
Gets or sets whether temporary model/solution/log artifacts are retained.
string ExportModelPath
Gets or sets an optional path receiving the exact LP model submitted to the selected solver.
double FeasibilityTolerance
Gets or sets the feasibility tolerance used by the independent solution checker.
Result of executing a solver-independent linear or mixed-integer model.
SolverExecutionInfo? Solver
Gets selected-solver provenance, when a solver was selected.
bool HasFeasibleSolution
Gets whether the result contains an independently valid solution.
TimeSpan SolveDuration
Gets elapsed solver execution time.
string NativeStatus
Gets the provider-native status/log summary.
IReadOnlyList< string > Diagnostics
Gets execution and validation diagnostics.
IReadOnlyDictionary< int, double > VariableValues
Gets variable values keyed by portable variable id.
LinearModelSolveStatus Status
Gets the normalized solve status after independent validation.
double? ObjectiveValue
Gets the independently recomputed objective value.
High-level solver-independent execution service for portable linear models.
Immutable solver-independent linear or mixed-integer linear model.
Definition LinearModel.cs:7
Serializable-style immutable snapshot of the solver selected for one solver-backed ULS execution.
SolverKind SelectedSolver
Gets the selected concrete solver.
ULS result enriched with the complete (l,S) cut-generation report.
Represents a feasible production plan for a ULS problem.
Definition UlsSolution.cs:7
double TotalCost
Gets the complete objective value.
Represents the outcome returned by a ULS solution strategy.
Independent ULS-domain validation report for one production plan.
IReadOnlyList< string > Diagnostics
Gets validation diagnostics.
bool IsFeasible
Gets whether the plan passed every ULS-domain check.
Independently verifies a ULS production plan against the original UlsProblem.
static UlsSolutionValidationResult Validate(UlsProblem problem, UlsSolution solution, double tolerance=1.0e-7)
Validates inventory balance, nonnegativity, setup linking, final inventory and all objective componen...
Optional asynchronous companion contract for ULS strategies whose implementation delegates work to an...
Defines the common strategy contract implemented by every ULS solver.
Definition IUlsSolver.cs:14
Separates classical ULS (l,S) inequalities from a fractional aggregate lot-sizing solution.
UlsSolverKind
Identifies the broad family of a ULS solution strategy.
CutSeparationMethod
Identifies the separation procedure that generated a cut.
CutDisposition
Describes what happened to a generated cut after separation.
LinearModelSolveStatus
Describes the termination state of a solver-backed portable linear model.
@ Cancelled
The computation was cancelled by the caller.
@ SolverUnavailable
No usable optimization solver was available.
SolverKind
Identifies a mathematical optimization solver that can be used by solver-backed ULS algorithms.
Definition SolverKind.cs:15
UlsSolveStatus
Describes the mathematical status of a ULS solve.
@ Optimal
A globally optimal solution has been found.
@ Failed
The solver failed before producing a valid mathematical conclusion.