ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
SolverBackedUlsFormulationSolverBase.cs
Go to the documentation of this file.
9
11
12/// <summary>
13/// Base class for exact ULS strategies that build a mathematical formulation
14/// and solve it through the portable optimization execution layer.
15/// </summary>
19{
20 private readonly IUlsFormulationBuilder _formulationBuilder;
21 private readonly LinearModelSolver _modelSolver;
22 private readonly LinearModelSolveOptions _executionOptions;
23
24 /// <summary>Initializes one solver-backed formulation strategy.</summary>
26 string name,
27 IUlsFormulationBuilder formulationBuilder,
28 LinearModelSolver? modelSolver = null,
29 LinearModelSolveOptions? executionOptions = null)
30 {
31 if (string.IsNullOrWhiteSpace(name))
32 {
33 throw new ArgumentException(
34 "A solver name is required.",
35 nameof(name));
36 }
37
38 Name = name.Trim();
39
40 _formulationBuilder =
41 formulationBuilder ??
42 throw new ArgumentNullException(
43 nameof(formulationBuilder));
44
45 _modelSolver =
46 modelSolver ??
48
49 _executionOptions =
50 CloneOptions(
51 executionOptions ??
53 }
54
55 /// <inheritdoc />
56 public string Name { get; }
57
58 /// <inheritdoc />
60 UlsSolverKind.Exact;
61
62 /// <summary>Gets the formulation implemented by this strategy.</summary>
64 _formulationBuilder.Kind;
65
66 /// <summary>Tests formulation applicability to one ULS problem.</summary>
67 public bool IsApplicable(
68 UlsProblem problem)
69 {
70 ArgumentNullException.ThrowIfNull(problem);
71
72 return _formulationBuilder.IsApplicable(
73 problem);
74 }
75
76 /// <inheritdoc />
78 UlsProblem problem,
79 CancellationToken cancellationToken = default)
80 {
81 ArgumentNullException.ThrowIfNull(problem);
82 cancellationToken.ThrowIfCancellationRequested();
83
84 // The common historical IUlsSolver API is synchronous. Run the async
85 // solver-backed path on the thread pool so a UI SynchronizationContext
86 // cannot deadlock the provider's asynchronous continuations.
87 return Task.Run(
88 async () =>
89 await SolveAsync(
90 problem,
91 cancellationToken)
92 .ConfigureAwait(false),
93 CancellationToken.None)
94 .GetAwaiter()
95 .GetResult();
96 }
97
98 /// <inheritdoc />
99 public async ValueTask<UlsSolveResult> SolveAsync(
100 UlsProblem problem,
101 CancellationToken cancellationToken = default)
102 {
103 ArgumentNullException.ThrowIfNull(problem);
104 cancellationToken.ThrowIfCancellationRequested();
105
106 if (!IsApplicable(problem))
107 {
108 throw new NotSupportedException(
109 $"{Name} is not applicable to the supplied ULS cost structure.");
110 }
111
112 UlsFormulation formulation =
113 _formulationBuilder.Build(
114 problem);
115
117 CloneOptions(
118 _executionOptions);
119
120 LinearModelSolveResult execution =
121 await _modelSolver.SolveAsync(
122 formulation.Model,
123 options,
124 cancellationToken)
125 .ConfigureAwait(false);
126
127 execution =
128 await TryRecoverRejectedCandidateAsync(
129 formulation.Model,
130 execution,
131 options,
132 cancellationToken)
133 .ConfigureAwait(false);
134
135 if (!execution.HasFeasibleSolution)
136 {
138 Name,
139 MapStatusWithoutSolution(
140 execution.Status),
141 formulation.Kind,
142 execution,
143 solution: null,
144 message:
145 BuildMessage(
146 execution));
147 }
148
149 try
150 {
151 UlsSolution solution =
153 problem,
154 formulation,
155 execution.VariableValues,
156 options.ZeroTolerance,
157 options.FeasibilityTolerance);
158
159 UlsSolutionValidationResult validation =
161 problem,
162 solution,
163 options.FeasibilityTolerance);
164
165 if (!validation.IsFeasible)
166 {
168 Name,
169 UlsSolveStatus.Failed,
170 formulation.Kind,
171 execution,
172 solution: null,
173 message:
174 "The mathematical model returned a valid native " +
175 "solution, but ULS-domain reconstruction failed: " +
176 string.Join(
177 " | ",
178 validation.Diagnostics));
179 }
180
181 if (execution.ObjectiveValue.HasValue &&
182 !ObjectivesAgree(
183 execution.ObjectiveValue.Value,
184 solution.TotalCost,
185 options.FeasibilityTolerance))
186 {
188 Name,
189 UlsSolveStatus.Failed,
190 formulation.Kind,
191 execution,
192 solution: null,
193 message:
194 $"Portable-model objective " +
195 $"{execution.ObjectiveValue.Value:G17} differs from " +
196 $"reconstructed ULS objective " +
197 $"{solution.TotalCost:G17}.");
198 }
199
200 UlsSolveStatus status =
201 execution.Status ==
204 : UlsSolveStatus.Feasible;
205
207 Name,
208 status,
209 formulation.Kind,
210 execution,
211 solution,
212 BuildMessage(
213 execution));
214 }
215 catch (Exception exception)
216 when (exception is not OperationCanceledException)
217 {
219 Name,
220 UlsSolveStatus.Failed,
221 formulation.Kind,
222 execution,
223 solution: null,
224 message:
225 $"ULS solution reconstruction failed: {exception.Message}");
226 }
227 }
228
229 private async ValueTask<LinearModelSolveResult>
230 TryRecoverRejectedCandidateAsync(
231 LinearModel originalModel,
232 LinearModelSolveResult execution,
234 CancellationToken cancellationToken)
235 {
236 if (!options.EnableFixedIntegerPolishing ||
237 !ShouldAttemptFixedIntegerPolishing(
238 originalModel,
239 execution,
240 options))
241 {
242 return execution;
243 }
244
245 LinearModel fixedIntegerModel;
246
247 try
248 {
249 fixedIntegerModel =
250 BuildFixedIntegerPolishingModel(
251 originalModel,
252 execution.VariableValues,
253 options.IntegralityTolerance);
254 }
255 catch (Exception exception)
256 when (exception is not OperationCanceledException)
257 {
258 return AppendDiagnostic(
259 execution,
260 "Fixed-integer polishing could not build the recovery model: " +
261 exception.Message);
262 }
263
264 LinearModelSolveOptions polishOptions =
265 CloneOptions(options);
266
267 // Never overwrite an explicitly requested export of the original MIP.
268 polishOptions.ExportModelPath =
269 string.Empty;
270
271 LinearModelSolveResult polished;
272
273 try
274 {
275 polished =
276 await _modelSolver.SolveAsync(
277 fixedIntegerModel,
278 polishOptions,
279 cancellationToken)
280 .ConfigureAwait(false);
281 }
282 catch (OperationCanceledException)
283 {
284 throw;
285 }
286 catch (Exception exception)
287 {
288 return AppendDiagnostic(
289 execution,
290 "Fixed-integer polishing failed during continuous re-optimization: " +
291 exception.Message);
292 }
293
294 if (!polished.HasFeasibleSolution)
295 {
296 return AppendDiagnostic(
297 execution,
298 "Fixed-integer polishing did not recover an independently " +
299 $"feasible solution (status {polished.Status}; native " +
300 $"'{polished.NativeStatus}').");
301 }
302
303 LinearModelSolveStatus recoveredStatus =
304 execution.SolverReportedStatus ==
305 LinearModelSolveStatus.Optimal
306 ? LinearModelSolveStatus.Optimal
307 : LinearModelSolveStatus.Feasible;
308
309 var diagnostics =
310 execution.Diagnostics
311 .Concat(
312 [
313 "Fixed-integer polishing recovered the candidate by " +
314 "fixing normalized integer decisions and re-optimizing " +
315 "the remaining continuous model."
316 ])
317 .Concat(polished.Diagnostics)
318 .ToArray();
319
320 string nativeStatus =
321 string.IsNullOrWhiteSpace(execution.NativeStatus)
322 ? "fixed-integer polish: " + polished.NativeStatus
323 : execution.NativeStatus +
324 " | fixed-integer polish: " +
325 polished.NativeStatus;
326
327 return new LinearModelSolveResult(
328 originalModel.Name,
329 recoveredStatus,
330 polished.Solver ??
331 execution.Solver,
332 polished.VariableValues,
333 polished.Validation,
334 execution.SolveDuration +
335 polished.SolveDuration,
336 nativeStatus,
337 diagnostics,
338 polished.ArtifactDirectory)
339 {
340 // Critical scientific rule: a CPXMIP_OPTIMAL_TOL incumbent stays
341 // non-proven even when its fixed-integer LP is polished optimally.
342 SolverReportedStatus =
343 execution.SolverReportedStatus
344 };
345 }
346
347 private static bool ShouldAttemptFixedIntegerPolishing(
348 LinearModel model,
349 LinearModelSolveResult execution,
350 LinearModelSolveOptions options)
351 {
352 if (!model.IsMixedInteger ||
353 execution.VariableValues.Count == 0 ||
354 execution.Validation is null ||
355 execution.Validation.IsFeasible ||
356 execution.SolverReportedStatus is not (
357 LinearModelSolveStatus.Optimal or
358 LinearModelSolveStatus.Feasible))
359 {
360 return false;
361 }
362
363 foreach (LinearVariable variable in model.Variables)
364 {
365 if (variable.Type == LinearVariableType.Continuous)
366 {
367 continue;
368 }
369
370 if (!execution.VariableValues.TryGetValue(
371 variable.Id,
372 out double value) ||
373 !double.IsFinite(value))
374 {
375 return false;
376 }
377
378 double rounded =
379 Math.Round(
380 value,
381 MidpointRounding.AwayFromZero);
382
383 if (Math.Abs(value - rounded) >
384 options.IntegralityTolerance)
385 {
386 return false;
387 }
388 }
389
390 return true;
391 }
392
393 private static LinearModel BuildFixedIntegerPolishingModel(
394 LinearModel original,
395 IReadOnlyDictionary<int, double> candidateValues,
396 double integralityTolerance)
397 {
398 var variables =
399 new LinearVariable[original.VariableCount];
400
401 for (int index = 0;
402 index < original.VariableCount;
403 index++)
404 {
405 LinearVariable variable =
406 original.Variables[index];
407
408 if (variable.Type ==
409 LinearVariableType.Continuous)
410 {
411 variables[index] =
412 variable;
413
414 continue;
415 }
416
417 if (!candidateValues.TryGetValue(
418 variable.Id,
419 out double candidate) ||
420 !double.IsFinite(candidate))
421 {
422 throw new InvalidOperationException(
423 $"No finite candidate value exists for integer variable " +
424 $"'{variable.Name}'.");
425 }
426
427 double fixedValue =
428 Math.Round(
429 candidate,
430 MidpointRounding.AwayFromZero);
431
432 if (Math.Abs(candidate - fixedValue) >
433 integralityTolerance)
434 {
435 throw new InvalidOperationException(
436 $"Integer variable '{variable.Name}' is too fractional " +
437 "for fixed-integer polishing.");
438 }
439
440 if (fixedValue <
441 variable.LowerBound -
442 integralityTolerance ||
443 fixedValue >
444 variable.UpperBound +
445 integralityTolerance)
446 {
447 throw new InvalidOperationException(
448 $"Rounded integer value {fixedValue:G17} for " +
449 $"'{variable.Name}' lies outside its original bounds.");
450 }
451
452 // Convert the fixed integer decision to a continuous fixed
453 // variable. The recovery solve is therefore an LP, not another MIP.
454 variables[index] =
455 new LinearVariable(
456 variable.Id,
457 variable.Name,
458 LinearVariableType.Continuous,
459 fixedValue,
460 fixedValue);
461 }
462
463 return new LinearModel(
464 original.Name + "-FixedIntegerPolish",
465 variables,
466 original.Constraints,
467 original.Objective);
468 }
469
470 private static LinearModelSolveResult AppendDiagnostic(
471 LinearModelSolveResult execution,
472 string message)
473 {
474 return new LinearModelSolveResult(
475 execution.ModelName,
476 execution.Status,
477 execution.Solver,
478 execution.VariableValues,
479 execution.Validation,
480 execution.SolveDuration,
481 execution.NativeStatus,
482 execution.Diagnostics.Concat([message]),
483 execution.ArtifactDirectory)
484 {
485 SolverReportedStatus =
486 execution.SolverReportedStatus
487 };
488 }
489 private static UlsSolveStatus MapStatusWithoutSolution(
491 {
492 return status switch
493 {
494 LinearModelSolveStatus.Infeasible =>
495 UlsSolveStatus.Infeasible,
496
497 LinearModelSolveStatus.Cancelled or
498 LinearModelSolveStatus.SolverUnavailable or
499 LinearModelSolveStatus.Unknown =>
500 UlsSolveStatus.NotSolved,
501
502 LinearModelSolveStatus.Unbounded or
503 LinearModelSolveStatus.InfeasibleOrUnbounded or
504 LinearModelSolveStatus.Failed =>
505 UlsSolveStatus.Failed,
506
507 LinearModelSolveStatus.Optimal or
508 LinearModelSolveStatus.Feasible =>
509 UlsSolveStatus.Failed,
510
511 _ =>
512 UlsSolveStatus.Failed
513 };
514 }
515
516 private static bool ObjectivesAgree(
517 double modelObjective,
518 double ulsObjective,
519 double tolerance)
520 {
521 double scale =
522 Math.Max(
523 1.0,
524 Math.Max(
525 Math.Abs(modelObjective),
526 Math.Abs(ulsObjective)));
527
528 return Math.Abs(
529 modelObjective -
530 ulsObjective) <=
531 tolerance * scale;
532 }
533
534 private static string BuildMessage(
535 LinearModelSolveResult execution)
536 {
537 var parts =
538 new List<string>();
539
540 if (execution.Solver is not null)
541 {
542 parts.Add(
543 $"Optimization engine: " +
544 $"{execution.Solver.SolverName} " +
545 $"{execution.Solver.SolverVersion}".Trim());
546 }
547
548 if (!string.IsNullOrWhiteSpace(
549 execution.NativeStatus))
550 {
551 parts.Add(
552 $"Native status: {execution.NativeStatus}");
553 }
554
555 if (execution.Diagnostics.Count > 0)
556 {
557 parts.AddRange(
558 execution.Diagnostics);
559 }
560
561 return string.Join(
562 " | ",
563 parts.Where(
564 static part =>
565 !string.IsNullOrWhiteSpace(part)));
566 }
567
568 private static LinearModelSolveOptions CloneOptions(
569 LinearModelSolveOptions source)
570 {
571 source.EnsureValid();
572
573 return new LinearModelSolveOptions
574 {
575 Solver = source.Solver,
576 AllowFallbackWhenExplicit =
578 FeasibilityTolerance =
580 ZeroTolerance =
581 source.ZeroTolerance,
582 IntegralityTolerance =
584 NearIntegerTolerance =
586 EnableFixedIntegerPolishing =
588 ExportModelPath =
589 source.ExportModelPath,
590 KeepTemporaryFiles =
591 source.KeepTemporaryFiles,
592 TemporaryRootPath =
593 source.TemporaryRootPath
594 };
595 }
596}
597
static UlsSolution Map(UlsProblem problem, UlsFormulation formulation, IReadOnlyDictionary< int, double > values, double zeroTolerance, double feasibilityTolerance)
SolverBackedUlsFormulationSolverBase(string name, IUlsFormulationBuilder formulationBuilder, LinearModelSolver? modelSolver=null, LinearModelSolveOptions? executionOptions=null)
Initializes one solver-backed formulation strategy.
UlsFormulationKind FormulationKind
Gets the formulation implemented by this strategy.
bool IsApplicable(UlsProblem problem)
Tests formulation applicability to one ULS problem.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing problem.The solve result.
async ValueTask< UlsSolveResult > SolveAsync(UlsProblem problem, CancellationToken cancellationToken=default)
Solves a ULS problem asynchronously.
Solver-independent ULS mathematical formulation plus semantic variable map.
UlsFormulationKind Kind
Gets the formulation kind.
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.
bool EnableFixedIntegerPolishing
Gets or sets whether a solver candidate rejected only after numerical normalization may be recovered ...
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.
LinearModelSolutionValidation? Validation
Gets the independent validation result, when a solution exists.
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.
string ArtifactDirectory
Gets the retained temporary artifact directory when KeepTemporaryFiles was enabled.
IReadOnlyList< string > Diagnostics
Gets execution and validation diagnostics.
IReadOnlyDictionary< int, double > VariableValues
Gets variable values keyed by portable variable id.
LinearModelSolveStatus SolverReportedStatus
Gets the status reported by the optimization engine before independent portable-model validation can ...
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
IReadOnlyList< LinearVariable > Variables
Gets all variables.
LinearObjective Objective
Gets the minimization objective.
IReadOnlyList< LinearConstraint > Constraints
Gets all constraints.
bool IsMixedInteger
Gets whether the model contains integer or binary variables.
int VariableCount
Gets the number of variables.
LinearVariableType Type
Gets the variable domain.
string Name
Gets the solver-independent variable name.
int Id
Gets the stable zero-based variable identifier.
ULS solve result enriched with mathematical-formulation and optimization engine provenance.
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
Builds a solver-independent mathematical-programming formulation of ULS.
UlsSolverKind
Identifies the broad family of a ULS solution strategy.
UlsFormulationKind
Identifies a mathematical-programming formulation of classical ULS.
LinearModelSolveStatus
Describes the termination state of a solver-backed portable linear model.
LinearVariableType
Identifies the domain of a variable in a portable linear mathematical model.
UlsSolveStatus
Describes the mathematical status of a ULS solve.
@ Optimal
A globally optimal solution has been found.