ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
WagnerWhitinSolver.cs
Go to the documentation of this file.
1using System.Buffers;
5
7
8/// <summary>
9/// Solves ULS instances with Wagner-Whitin costs in linear time.
10/// </summary>
11/// <remarks>
12/// <para>
13/// This implementation is an exact solver. It does not use the classical
14/// <c>O(n^2)</c> Wagner-Whitin dynamic-programming scan. Instead, it implements
15/// the monotone lower-convex-envelope specialization described by
16/// Wagelmans, van Hoesel and Kolen (1992), which runs in <c>O(n)</c> time for
17/// the Wagner-Whitin/no-speculative-motive case and uses <c>O(n)</c> memory.
18/// </para>
19/// <para>
20/// The supported condition is
21/// <c>p[t] + h[t] &gt;= p[t+1]</c> for every adjacent pair of periods.
22/// Equivalently, the transformed marginal production costs are nonincreasing
23/// over time. The classical case with constant unit production costs and
24/// nonnegative holding costs is a special case.
25/// </para>
26/// <para>
27/// Algorithmic source:
28/// A. Wagelmans, S. van Hoesel, A. Kolen,
29/// "Economic Lot Sizing: An O(n log n) Algorithm That Runs in Linear Time
30/// in the Wagner-Whitin Case", Operations Research 40(S1), S145-S156, 1992,
31/// DOI: 10.1287/opre.40.1.S145.
32/// </para>
33/// <para>
34/// Historical references:
35/// H. M. Wagner and T. M. Whitin,
36/// "Dynamic Version of the Economic Lot Size Model", Management Science
37/// 5(1), 89-96, 1958, DOI: 10.1287/mnsc.5.1.89;
38/// J. R. Evans,
39/// "An Efficient Implementation of the Wagner-Whitin Algorithm for Dynamic
40/// Lot-Sizing", Journal of Operations Management 5(2), 229-235, 1985,
41/// DOI: 10.1016/0272-6963(85)90009-9.
42/// </para>
43/// <para>
44/// The C# implementation uses an equivalent monotone convex-hull form of the
45/// backward recurrence. Internal work arrays are rented from
46/// <see cref="ArrayPool{T}"/> to reduce garbage-collector pressure when the
47/// solver is repeatedly used as a subproblem.
48/// </para>
49/// </remarks>
50public sealed class WagnerWhitinSolver : IUlsSolver
51{
52 private const int CancellationCheckMask = 255;
53
54 /// <inheritdoc />
55 public string Name => "Wagner-Whitin (Wagelmans linear-time)";
56
57 /// <inheritdoc />
59
60 /// <summary>
61 /// Determines whether the problem satisfies the no-speculative-motive
62 /// condition required by the linear-time specialization.
63 /// </summary>
64 /// <param name="problem">The problem to inspect.</param>
65 /// <returns>
66 /// <see langword="true"/> when
67 /// <c>p[t] + h[t] &gt;= p[t+1]</c> for all adjacent periods;
68 /// otherwise <see langword="false"/>.
69 /// </returns>
70 public static bool IsApplicable(UlsProblem problem)
71 {
72 ArgumentNullException.ThrowIfNull(problem);
73
74 var productionCosts = problem.UnitProductionCosts;
75 var holdingCosts = problem.HoldingCosts;
76
77 for (var period = 0; period < problem.Horizon - 1; period++)
78 {
79 var currentDeliveredNextPeriod =
80 productionCosts[period] + holdingCosts[period];
81
82 if (!double.IsFinite(currentDeliveredNextPeriod) ||
83 currentDeliveredNextPeriod < productionCosts[period + 1])
84 {
85 return false;
86 }
87 }
88
89 return true;
90 }
91
92 /// <inheritdoc />
93 /// <exception cref="NotSupportedException">
94 /// Thrown when the problem violates the Wagner-Whitin/no-speculative-motive
95 /// cost condition required by this linear-time solver.
96 /// </exception>
97 /// <exception cref="ArithmeticException">
98 /// Thrown when a cumulative demand or intermediate cost cannot be
99 /// represented as a finite <see cref="double"/>.
100 /// </exception>
102 UlsProblem problem,
103 CancellationToken cancellationToken = default)
104 {
105 ArgumentNullException.ThrowIfNull(problem);
106 cancellationToken.ThrowIfCancellationRequested();
107
108 if (!IsApplicable(problem))
109 {
110 throw new NotSupportedException(
111 "WagnerWhitinSolver requires p[t] + h[t] >= p[t+1] " +
112 "for every adjacent pair of periods.");
113 }
114
115 var horizon = problem.Horizon;
116 var demands = problem.Demands;
117 var setupCosts = problem.SetupCosts;
118 var productionCosts = problem.UnitProductionCosts;
119 var holdingCosts = problem.HoldingCosts;
120
121 var suffixBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
122 var nextBuffer = ArrayPool<int>.Shared.Rent(horizon);
123 var hullBuffer = ArrayPool<HullLine>.Shared.Rent(horizon + 1);
124
125 try
126 {
127 var suffixDemand = suffixBuffer.AsSpan(0, horizon + 1);
128 var next = nextBuffer.AsSpan(0, horizon);
129 var hull = hullBuffer.AsSpan(0, horizon + 1);
130
131 BuildSuffixDemand(demands, suffixDemand);
132
133 var head = 0;
134 var tail = 0;
135 hull[0] = new HullLine(
136 slope: 0.0,
137 intercept: 0.0,
138 startX: double.NegativeInfinity,
139 period: horizon);
140
141 var nextValue = 0.0;
142 var accumulatedRelevantHoldingCost = 0.0;
143
144 for (var period = horizon - 1; period >= 0; period--)
145 {
146 if ((period & CancellationCheckMask) == 0)
147 {
148 cancellationToken.ThrowIfCancellationRequested();
149 }
150
151 // The terminal holding-cost coefficient is irrelevant because
152 // terminal inventory is fixed at zero. Omitting it is a common
153 // additive shift of the transformed marginal costs.
154 if (period < horizon - 1)
155 {
156 accumulatedRelevantHoldingCost = AddFinite(
157 accumulatedRelevantHoldingCost,
158 holdingCosts[period],
159 "transformed holding-cost suffix");
160 }
161
162 var transformedMarginalCost = AddFinite(
163 productionCosts[period],
164 accumulatedRelevantHoldingCost,
165 "transformed marginal production cost");
166
167 AdvanceQueryHead(
168 hull,
169 ref head,
170 tail,
171 transformedMarginalCost);
172
173 var bestLine = hull[head];
174 var coveredDemand =
175 suffixDemand[period] - suffixDemand[bestLine.Period];
176
177 var setupValue = AddFinite(
178 setupCosts[period],
179 MultiplyFinite(
180 transformedMarginalCost,
181 coveredDemand,
182 "transformed variable production cost"),
183 "setup plus transformed variable cost");
184
185 setupValue = AddFinite(
186 setupValue,
187 bestLine.Intercept,
188 "dynamic-programming value");
189
190 double value;
191
192 if (demands[period] == 0.0 && nextValue <= setupValue)
193 {
194 value = nextValue;
195 next[period] = -1;
196 }
197 else
198 {
199 value = setupValue;
200 next[period] = bestLine.Period;
201 }
202
203 nextValue = value;
204
205 AddLine(
206 hull,
207 ref head,
208 ref tail,
209 new HullLine(
210 slope: -suffixDemand[period],
211 intercept: value,
212 startX: double.NegativeInfinity,
213 period: period));
214 }
215
216 cancellationToken.ThrowIfCancellationRequested();
217
218 return BuildResult(
219 problem,
220 suffixDemand,
221 next,
222 cancellationToken);
223 }
224 finally
225 {
226 ArrayPool<double>.Shared.Return(suffixBuffer, clearArray: false);
227 ArrayPool<int>.Shared.Return(nextBuffer, clearArray: false);
228 ArrayPool<HullLine>.Shared.Return(hullBuffer, clearArray: false);
229 }
230 }
231
232 private UlsSolveResult BuildResult(
233 UlsProblem problem,
234 ReadOnlySpan<double> suffixDemand,
235 ReadOnlySpan<int> next,
236 CancellationToken cancellationToken)
237 {
238 var horizon = problem.Horizon;
239 var productionQuantities = new double[horizon];
240 var endingInventories = new double[horizon];
241 var setupDecisions = new bool[horizon];
242
243 var period = 0;
244
245 while (period < horizon)
246 {
247 if ((period & CancellationCheckMask) == 0)
248 {
249 cancellationToken.ThrowIfCancellationRequested();
250 }
251
252 var successor = next[period];
253
254 if (successor < 0)
255 {
256 period++;
257 continue;
258 }
259
260 if (successor <= period || successor > horizon)
261 {
262 throw new InvalidOperationException(
263 "The Wagner-Whitin predecessor chain is inconsistent.");
264 }
265
266 productionQuantities[period] =
267 suffixDemand[period] - suffixDemand[successor];
268 setupDecisions[period] = true;
269
270 for (var inventoryPeriod = period;
271 inventoryPeriod < successor;
272 inventoryPeriod++)
273 {
274 endingInventories[inventoryPeriod] =
275 suffixDemand[inventoryPeriod + 1] -
276 suffixDemand[successor];
277 }
278
279 period = successor;
280 }
281
282 var setupCost = 0.0;
283 var productionCost = 0.0;
284 var holdingCost = 0.0;
285
286 var setupCosts = problem.SetupCosts;
287 var productionCosts = problem.UnitProductionCosts;
288 var holdingCosts = problem.HoldingCosts;
289
290 for (period = 0; period < horizon; period++)
291 {
292 if ((period & CancellationCheckMask) == 0)
293 {
294 cancellationToken.ThrowIfCancellationRequested();
295 }
296
297 if (setupDecisions[period])
298 {
299 setupCost = AddFinite(
300 setupCost,
301 setupCosts[period],
302 "solution setup cost");
303 }
304
305 productionCost = AddFinite(
306 productionCost,
307 MultiplyFinite(
308 productionCosts[period],
309 productionQuantities[period],
310 "solution production cost"),
311 "solution production cost");
312
313 holdingCost = AddFinite(
314 holdingCost,
315 MultiplyFinite(
316 holdingCosts[period],
317 endingInventories[period],
318 "solution holding cost"),
319 "solution holding cost");
320 }
321
322 var solution = UlsSolution.FromOwnedBuffers(
323 productionQuantities,
324 endingInventories,
325 setupDecisions,
326 setupCost,
327 productionCost,
328 holdingCost);
329
330 return new UlsSolveResult(
331 Name,
332 UlsSolveStatus.Optimal,
333 solution);
334 }
335
336 private static void BuildSuffixDemand(
337 ReadOnlySpan<double> demands,
338 Span<double> suffixDemand)
339 {
340 suffixDemand[demands.Length] = 0.0;
341
342 for (var period = demands.Length - 1; period >= 0; period--)
343 {
344 suffixDemand[period] = AddFinite(
345 suffixDemand[period + 1],
346 demands[period],
347 "cumulative demand");
348 }
349 }
350
351 private static void AdvanceQueryHead(
352 ReadOnlySpan<HullLine> hull,
353 ref int head,
354 int tail,
355 double x)
356 {
357 while (head < tail && hull[head + 1].StartX <= x)
358 {
359 head++;
360 }
361 }
362
363 private static void AddLine(
364 Span<HullLine> hull,
365 ref int head,
366 ref int tail,
367 HullLine newLine)
368 {
369 while (tail >= head)
370 {
371 var last = hull[tail];
372
373 if (newLine.Slope == last.Slope)
374 {
375 if (newLine.Intercept >= last.Intercept)
376 {
377 return;
378 }
379
380 tail--;
381 continue;
382 }
383
384 if (last.Slope <= newLine.Slope)
385 {
386 throw new InvalidOperationException(
387 "The Wagner-Whitin convex hull received nonmonotone slopes.");
388 }
389
390 var startX = IntersectionX(last, newLine);
391
392 if (tail == head || startX > last.StartX)
393 {
394 break;
395 }
396
397 tail--;
398 }
399
400 if (tail < head)
401 {
402 tail = head;
403 hull[tail] = new HullLine(
404 newLine.Slope,
405 newLine.Intercept,
406 double.NegativeInfinity,
407 newLine.Period);
408 return;
409 }
410
411 var intersection = IntersectionX(hull[tail], newLine);
412
413 tail++;
414 hull[tail] = new HullLine(
415 newLine.Slope,
416 newLine.Intercept,
417 intersection,
418 newLine.Period);
419 }
420
421 private static double IntersectionX(
422 HullLine left,
423 HullLine right)
424 {
425 var denominator = left.Slope - right.Slope;
426
427 if (!(denominator > 0.0) || !double.IsFinite(denominator))
428 {
429 throw new ArithmeticException(
430 "A convex-hull line intersection has an invalid denominator.");
431 }
432
433 var numerator = right.Intercept - left.Intercept;
434 var intersection = numerator / denominator;
435
436 if (!double.IsFinite(intersection))
437 {
438 throw new ArithmeticException(
439 "A convex-hull line intersection is not finite.");
440 }
441
442 return intersection;
443 }
444
445 private static double AddFinite(
446 double left,
447 double right,
448 string operation)
449 {
450 var value = left + right;
451
452 if (!double.IsFinite(value))
453 {
454 throw new ArithmeticException(
455 $"Numerical overflow while computing {operation}.");
456 }
457
458 return value;
459 }
460
461 private static double MultiplyFinite(
462 double left,
463 double right,
464 string operation)
465 {
466 var value = left * right;
467
468 if (!double.IsFinite(value))
469 {
470 throw new ArithmeticException(
471 $"Numerical overflow while computing {operation}.");
472 }
473
474 return value;
475 }
476
477 private readonly struct HullLine
478 {
479 public HullLine(
480 double slope,
481 double intercept,
482 double startX,
483 int period)
484 {
485 Slope = slope;
486 Intercept = intercept;
487 StartX = startX;
488 Period = period;
489 }
490
491 public double Slope { get; }
492
493 public double Intercept { get; }
494
495 public double StartX { get; }
496
497 public int Period { get; }
498 }
499}
Solves ULS instances with Wagner-Whitin costs in linear time.
string Name
Gets the stable human-readable name of the solver.
UlsSolverKind Kind
Gets the broad family of the solver.
static bool IsApplicable(UlsProblem problem)
Determines whether the problem satisfies the no-speculative-motive condition required by the linear-t...
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Represents a validated classical uncapacitated lot-sizing problem.
Definition UlsProblem.cs:23
ReadOnlySpan< double > UnitProductionCosts
Gets unit production costs by period.
int Horizon
Gets the number of planning periods.
Definition UlsProblem.cs:82
ReadOnlySpan< double > HoldingCosts
Gets end-of-period unit holding costs by period.
ReadOnlySpan< double > Demands
Gets demand by period.
Definition UlsProblem.cs:92
ReadOnlySpan< double > SetupCosts
Gets fixed setup costs by period.
Definition UlsProblem.cs:97
Represents the outcome returned by a ULS solution strategy.
Defines the common strategy contract implemented by every ULS solver.
Definition IUlsSolver.cs:14
UlsSolverKind
Identifies the broad family of a ULS solution strategy.
UlsSolveStatus
Describes the mathematical status of a ULS solve.