ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
ChowdhuryBakiAzabSolver.cs
Go to the documentation of this file.
1using System.Buffers;
6
8
9/// <summary>
10/// Implements the linear-time Wagner-Whitin algorithm of
11/// Chowdhury, Baki and Azab.
12/// </summary>
13/// <remarks>
14/// <para>
15/// The algorithm works backwards on the Wagner-Whitin shortest-path network.
16/// Instead of constructing the triangular advantage matrices introduced in the
17/// paper, it maintains only the active diagonals, scheduled deletion events,
18/// and the stack summaries required by Algorithm 1.
19/// </para>
20/// <para>
21/// Time complexity: <c>O(T)</c>.
22/// Auxiliary working memory: <c>O(T)</c>.
23/// </para>
24/// <para>
25/// The implementation follows Algorithm 1 and Theorems 1-5 of the detailed
26/// primary exposition in N. T. Chowdhury's doctoral dissertation, Chapter 2,
27/// which corresponds to the published article:
28/// N. T. Chowdhury, M. F. Baki and A. Azab,
29/// "Dynamic Economic Lot-Sizing Problem: A new O(T) Algorithm for the
30/// Wagner-Whitin Model",
31/// Computers &amp; Industrial Engineering 117, 6-18, 2018.
32/// DOI: 10.1016/j.cie.2018.01.010.
33/// </para>
34/// <para>
35/// The paper assumes stationary unit holding cost <c>h</c>, time-varying setup
36/// costs <c>f[t]</c>, and the Wagner-Whitin cost structure. Constant unit
37/// production cost may be present because it adds the same amount to every
38/// feasible policy.
39/// </para>
40/// <para>
41/// To preserve the published arithmetic exactly, this public implementation
42/// conservatively requires strictly positive demands and strictly positive
43/// stationary holding cost when the horizon contains more than one period.
44/// </para>
45/// </remarks>
47{
48 private const int CancellationCheckMask = 255;
49
50 /// <inheritdoc />
51 public string Name => "Chowdhury-Baki-Azab O(T)";
52
53 /// <inheritdoc />
55
56 /// <summary>
57 /// Determines whether the problem satisfies the published algorithm's
58 /// stationary-cost domain used by this implementation.
59 /// </summary>
60 public static bool IsApplicable(UlsProblem problem)
61 {
62 ArgumentNullException.ThrowIfNull(problem);
63
64 var horizon = problem.Horizon;
65 var demands = problem.Demands;
66 var productionCosts = problem.UnitProductionCosts;
67 var holdingCosts = problem.HoldingCosts;
68
69 for (var period = 0; period < horizon; period++)
70 {
71 if (!(demands[period] > 0.0))
72 {
73 return false;
74 }
75 }
76
77 var productionCost = productionCosts[0];
78
79 for (var period = 1; period < horizon; period++)
80 {
81 if (productionCosts[period] != productionCost)
82 {
83 return false;
84 }
85 }
86
87 if (horizon <= 1)
88 {
89 return true;
90 }
91
92 var holdingCost = holdingCosts[0];
93
94 if (!(holdingCost > 0.0))
95 {
96 return false;
97 }
98
99 for (var period = 1; period < horizon - 1; period++)
100 {
101 if (holdingCosts[period] != holdingCost)
102 {
103 return false;
104 }
105 }
106
107 return true;
108 }
109
110 /// <inheritdoc />
111 /// <exception cref="NotSupportedException">
112 /// Thrown when demands are not strictly positive, relevant holding costs
113 /// are not stationary and positive, or unit production costs are not
114 /// constant.
115 /// </exception>
117 UlsProblem problem,
118 CancellationToken cancellationToken = default)
119 {
120 ArgumentNullException.ThrowIfNull(problem);
121 cancellationToken.ThrowIfCancellationRequested();
122
123 if (!IsApplicable(problem))
124 {
125 throw new NotSupportedException(
126 "ChowdhuryBakiAzabSolver requires strictly positive demands, " +
127 "constant unit production costs, and a strictly positive " +
128 "stationary relevant holding cost.");
129 }
130
131 var horizon = problem.Horizon;
132
133 if (horizon == 1)
134 {
135 var predecessor = new int[2];
136 predecessor[0] = -1;
137 predecessor[1] = 0;
138
140 problem,
141 predecessor,
142 Name,
143 cancellationToken);
144 }
145
146 var eventCapacity = checked((2 * horizon) + 8);
147
148 var gBuffer = ArrayPool<double>.Shared.Rent(horizon + 2);
149 var aBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
150 var bBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
151 var prefixDemandBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
152 var prefixWeightedDemandBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
153
154 var activePreviousBuffer = ArrayPool<int>.Shared.Rent(horizon + 1);
155 var activeNextBuffer = ArrayPool<int>.Shared.Rent(horizon + 1);
156 var bestSuccessorBuffer = ArrayPool<int>.Shared.Rent(horizon + 1);
157 var listHeadBuffer = ArrayPool<int>.Shared.Rent(horizon + 1);
158 var eventPeriodBuffer = ArrayPool<int>.Shared.Rent(eventCapacity);
159 var eventNextBuffer = ArrayPool<int>.Shared.Rent(eventCapacity);
160 var predecessorBuffer = ArrayPool<int>.Shared.Rent(horizon + 1);
161
162 try
163 {
164 var g = gBuffer.AsSpan(0, horizon + 2);
165 var a = aBuffer.AsSpan(0, horizon + 1);
166 var b = bBuffer.AsSpan(0, horizon + 1);
167 var prefixDemand = prefixDemandBuffer.AsSpan(0, horizon + 1);
168 var prefixWeightedDemand =
169 prefixWeightedDemandBuffer.AsSpan(0, horizon + 1);
170
171 var activePrevious =
172 activePreviousBuffer.AsSpan(0, horizon + 1);
173 var activeNext =
174 activeNextBuffer.AsSpan(0, horizon + 1);
175 var bestSuccessor =
176 bestSuccessorBuffer.AsSpan(0, horizon + 1);
177 var listHead =
178 listHeadBuffer.AsSpan(0, horizon + 1);
179 var eventPeriod =
180 eventPeriodBuffer.AsSpan(0, eventCapacity);
181 var eventNext =
182 eventNextBuffer.AsSpan(0, eventCapacity);
183 var predecessor =
184 predecessorBuffer.AsSpan(0, horizon + 1);
185
186 g.Clear();
187 a.Clear();
188 b.Clear();
189 prefixDemand.Clear();
190 prefixWeightedDemand.Clear();
191 activePrevious.Clear();
192 activeNext.Clear();
193 bestSuccessor.Clear();
194 listHead.Fill(-1);
195 predecessor.Fill(-1);
196
197 var demands = problem.Demands;
198 var setupCosts = problem.SetupCosts;
199 var holdingCost = problem.HoldingCosts[0];
200
201 for (var period = 1; period <= horizon; period++)
202 {
203 var demand = demands[period - 1];
204
205 prefixDemand[period] = AddFinite(
206 prefixDemand[period - 1],
207 demand,
208 "cumulative demand");
209
210 prefixWeightedDemand[period] = AddFinite(
211 prefixWeightedDemand[period - 1],
212 MultiplyFinite(period, demand, "weighted demand"),
213 "weighted cumulative demand");
214 }
215
216 activePrevious[1] = 0;
217 activeNext[0] = 1;
218
219 for (var diagonal = 2; diagonal <= horizon; diagonal++)
220 {
221 activePrevious[diagonal] = diagonal - 1;
222 }
223
224 for (var diagonal = 1; diagonal < horizon; diagonal++)
225 {
226 activeNext[diagonal] = diagonal + 1;
227 }
228
229 g[horizon + 1] = 0.0;
230 a[horizon] = 0.0;
231 g[horizon] = setupCosts[horizon - 1];
232 bestSuccessor[horizon] = horizon + 1;
233
234 var bestDiagonal = horizon - 1;
235 var eventCount = 0;
236 var cancellationCounter = 0;
237
238 for (var k = horizon - 1; k >= 1; k--)
239 {
240 if ((cancellationCounter++ & CancellationCheckMask) == 0)
241 {
242 cancellationToken.ThrowIfCancellationRequested();
243 }
244
245 a[k] =
246 g[k + 1] -
247 g[k + 2] -
248 MultiplyFinite(
249 holdingCost,
250 demands[k],
251 "Algorithm 1 advantage");
252
253 EnsureFinite(a[k], "Algorithm 1 advantage");
254
255 b[k] = MultiplyFinite(
256 holdingCost,
257 demands[k],
258 "Algorithm 1 slope");
259
260 var u = ClampedCeilingRatio(
261 a[k],
262 b[k],
263 horizon);
264
265 if (u <= k - 1)
266 {
267 Schedule(
268 k - u,
269 k,
270 listHead,
271 eventPeriod,
272 eventNext,
273 ref eventCount);
274 }
275
276 var eventIndex = listHead[k];
277
278 while (eventIndex >= 0)
279 {
280 if ((cancellationCounter++ & CancellationCheckMask) == 0)
281 {
282 cancellationToken.ThrowIfCancellationRequested();
283 }
284
285 var p = eventPeriod[eventIndex];
286
287 if (p <= bestDiagonal &&
288 activeNext[activePrevious[p]] == p)
289 {
290 var delta =
291 a[p] -
292 MultiplyFinite(
293 p - k,
294 b[p],
295 "stack advantage shift");
296
297 var aggregatedSlope = b[p];
298 var stackHead = p;
299
300 while (delta <= 0.0 &&
301 p <= bestDiagonal)
302 {
303 if ((cancellationCounter++ &
304 CancellationCheckMask) == 0)
305 {
306 cancellationToken.ThrowIfCancellationRequested();
307 }
308
309 if (p < bestDiagonal)
310 {
311 activeNext[activePrevious[p]] =
312 activeNext[p];
313
314 activePrevious[activeNext[p]] =
315 activePrevious[p];
316
317 p = activeNext[p];
318
319 delta = AddFinite(
320 delta,
321 a[p] -
322 MultiplyFinite(
323 p - k,
324 b[p],
325 "stack advantage shift"),
326 "stack advantage aggregation");
327
328 aggregatedSlope = AddFinite(
329 aggregatedSlope,
330 b[p],
331 "stack slope aggregation");
332
333 if (delta > 0.0)
334 {
335 a[p] = AddFinite(
336 delta,
337 MultiplyFinite(
338 p - k,
339 aggregatedSlope,
340 "stack compressed intercept"),
341 "stack compressed intercept");
342
343 b[p] = aggregatedSlope;
344
345 u = ClampedCeilingRatio(
346 delta,
347 b[p],
348 horizon);
349
350 if (u <= k - 1)
351 {
352 Schedule(
353 k - u,
354 p,
355 listHead,
356 eventPeriod,
357 eventNext,
358 ref eventCount);
359 }
360 }
361 }
362 else
363 {
364 bestDiagonal =
365 activePrevious[stackHead];
366 }
367 }
368 }
369
370 eventIndex = eventNext[eventIndex];
371 }
372
373 bestSuccessor[k] = bestDiagonal + 2;
374
375 var holdingArcCost = ComputeHoldingArcCost(
376 k,
377 bestSuccessor[k],
378 holdingCost,
379 prefixDemand,
380 prefixWeightedDemand);
381
382 g[k] = AddFinite(
383 setupCosts[k - 1],
384 holdingArcCost,
385 "backward shortest-path cost");
386
387 g[k] = AddFinite(
388 g[k],
389 g[bestSuccessor[k]],
390 "backward shortest-path cost");
391 }
392
393 var node = 1;
394
395 while (node <= horizon)
396 {
397 cancellationToken.ThrowIfCancellationRequested();
398
399 var next = bestSuccessor[node];
400
401 if (next <= node ||
402 next > horizon + 1)
403 {
404 throw new InvalidOperationException(
405 "The Chowdhury-Baki-Azab successor chain is inconsistent.");
406 }
407
408 predecessor[next - 1] = node - 1;
409 node = next;
410 }
411
413 problem,
414 predecessor,
415 Name,
416 cancellationToken);
417 }
418 finally
419 {
420 ArrayPool<double>.Shared.Return(gBuffer, clearArray: false);
421 ArrayPool<double>.Shared.Return(aBuffer, clearArray: false);
422 ArrayPool<double>.Shared.Return(bBuffer, clearArray: false);
423 ArrayPool<double>.Shared.Return(prefixDemandBuffer, clearArray: false);
424 ArrayPool<double>.Shared.Return(
425 prefixWeightedDemandBuffer,
426 clearArray: false);
427
428 ArrayPool<int>.Shared.Return(activePreviousBuffer, clearArray: false);
429 ArrayPool<int>.Shared.Return(activeNextBuffer, clearArray: false);
430 ArrayPool<int>.Shared.Return(bestSuccessorBuffer, clearArray: false);
431 ArrayPool<int>.Shared.Return(listHeadBuffer, clearArray: false);
432 ArrayPool<int>.Shared.Return(eventPeriodBuffer, clearArray: false);
433 ArrayPool<int>.Shared.Return(eventNextBuffer, clearArray: false);
434 ArrayPool<int>.Shared.Return(predecessorBuffer, clearArray: false);
435 }
436 }
437
438 private static void Schedule(
439 int list,
440 int period,
441 Span<int> listHead,
442 Span<int> eventPeriod,
443 Span<int> eventNext,
444 ref int eventCount)
445 {
446 if ((uint)list >= (uint)listHead.Length)
447 {
448 throw new InvalidOperationException(
449 "Algorithm 1 attempted to schedule an invalid list index.");
450 }
451
452 if (eventCount >= eventPeriod.Length)
453 {
454 throw new InvalidOperationException(
455 "Algorithm 1 exceeded the proven O(T) event bound.");
456 }
457
458 eventPeriod[eventCount] = period;
459 eventNext[eventCount] = listHead[list];
460 listHead[list] = eventCount;
461 eventCount++;
462 }
463
464 private static int ClampedCeilingRatio(
465 double numerator,
466 double denominator,
467 int upperBound)
468 {
469 if (!(denominator > 0.0) ||
470 !double.IsFinite(denominator))
471 {
472 throw new ArithmeticException(
473 "Algorithm 1 requires a finite positive b(k).");
474 }
475
476 var ratio = numerator / denominator;
477
478 if (!double.IsFinite(ratio))
479 {
480 throw new ArithmeticException(
481 "Algorithm 1 produced a non-finite advantage ratio.");
482 }
483
484 if (ratio <= 0.0)
485 {
486 return 0;
487 }
488
489 if (ratio >= upperBound)
490 {
491 return upperBound;
492 }
493
494 return (int)Math.Ceiling(ratio);
495 }
496
497 private static double ComputeHoldingArcCost(
498 int startNode,
499 int successorNode,
500 double holdingCost,
501 ReadOnlySpan<double> prefixDemand,
502 ReadOnlySpan<double> prefixWeightedDemand)
503 {
504 var lastDemandPeriod = successorNode - 1;
505
506 if (lastDemandPeriod <= startNode)
507 {
508 return 0.0;
509 }
510
511 var futureDemand =
512 prefixDemand[lastDemandPeriod] -
513 prefixDemand[startNode];
514
515 var weightedFutureDemand =
516 prefixWeightedDemand[lastDemandPeriod] -
517 prefixWeightedDemand[startNode];
518
519 var partPeriods =
520 weightedFutureDemand -
521 MultiplyFinite(
522 startNode,
523 futureDemand,
524 "part-period quantity");
525
526 return MultiplyFinite(
527 holdingCost,
528 partPeriods,
529 "regeneration-interval holding cost");
530 }
531
532 private static double AddFinite(
533 double left,
534 double right,
535 string operation)
536 {
537 var value = left + right;
538 EnsureFinite(value, operation);
539 return value;
540 }
541
542 private static double MultiplyFinite(
543 double left,
544 double right,
545 string operation)
546 {
547 var value = left * right;
548 EnsureFinite(value, operation);
549 return value;
550 }
551
552 private static void EnsureFinite(
553 double value,
554 string operation)
555 {
556 if (!double.IsFinite(value))
557 {
558 throw new ArithmeticException(
559 $"Numerical overflow while computing {operation}.");
560 }
561 }
562}
Implements the linear-time Wagner-Whitin algorithm of Chowdhury, Baki and Azab.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
static bool IsApplicable(UlsProblem problem)
Determines whether the problem satisfies the published algorithm's stationary-cost domain used by thi...
string Name
Gets the stable human-readable name of the solver.
UlsSolverKind Kind
Gets the broad family of the solver.
Reconstructs a zero-inventory-order ULS solution from shortest-path predecessors.
static UlsSolveResult Build(UlsProblem problem, ReadOnlySpan< int > predecessor, string solverName, CancellationToken cancellationToken)
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.