ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
AggarwalParkSolver.cs
Go to the documentation of this file.
1using System.Buffers;
7
9
10/// <summary>
11/// Implements the Aggarwal-Park recursive Monge-matrix algorithm for the
12/// uncapacitated economic lot-sizing problem.
13/// </summary>
14/// <remarks>
15/// <para>
16/// Aggarwal and Park showed that dynamic programs arising in uncapacitated
17/// economic lot sizing can be accelerated by exploiting Monge-array structure.
18/// Their general ELS algorithm uses recursive matrix searching and runs in
19/// <c>O(n log n)</c> time.
20/// </para>
21/// <para>
22/// This implementation uses the forward transformed-cost recurrence
23/// </para>
24/// <code>
25/// F(t) = min(j &lt; t)
26/// F(j) + f[j] - r[j] D[j] + r[j] D[t],
27/// </code>
28/// <para>
29/// where <c>D[t]</c> is cumulative demand and
30/// <c>r[j] = p[j] + sum(h[k], k=j..n-2)</c>. A CDQ-style divide-and-conquer
31/// over the time indices turns every cross-recursion relaxation into a
32/// rectangular implicit matrix. Predecessor columns are ordered by
33/// nonincreasing <c>r[j]</c>; because cumulative demand is nondecreasing, each
34/// such matrix is Monge. Its row minima are found with SMAWK in linear time.
35/// </para>
36/// <para>
37/// The total work over the divide-and-conquer recursion is
38/// <c>O(n log n)</c>. Temporary storage is <c>O(n)</c>. All large temporary
39/// arrays are obtained from <see cref="ArrayPool{T}"/>.
40/// </para>
41/// <para>
42/// Primary reference:
43/// A. Aggarwal and J. K. Park,
44/// "Improved Algorithms for Economic Lot Size Problems",
45/// Operations Research 41(3), 549-571, 1993.
46/// DOI: 10.1287/opre.41.3.549.
47/// </para>
48/// <para>
49/// A contemporary independent exposition explicitly identifies the
50/// Aggarwal-Park ELS implementation as a recursive matrix-searching algorithm
51/// and describes their divide-and-conquer treatment of the general,
52/// nonmonotone-cost case:
53/// S. van Hoesel, A. Wagelmans and B. Moerman,
54/// "Using Geometric Techniques to Improve Dynamic Programming Algorithms for
55/// the Economic Lot-Sizing Problem and Extensions",
56/// European Journal of Operational Research 75(2), 312-331, 1994.
57/// DOI: 10.1016/0377-2217(94)90077-9.
58/// </para>
59/// <para>
60/// This is a modern array-based realization of the published matrix-search
61/// approach, not a delegation to the Wagelmans convex-envelope solver or the
62/// Federgruen-Tzur predecessor-tree solver.
63/// </para>
64/// </remarks>
65public sealed class AggarwalParkSolver : IUlsSolver
66{
67 /// <inheritdoc />
68 public string Name =>
69 "Aggarwal-Park Monge matrix search O(n log n)";
70
71 /// <inheritdoc />
73
74 /// <inheritdoc />
76 UlsProblem problem,
77 CancellationToken cancellationToken = default)
78 {
79 ArgumentNullException.ThrowIfNull(problem);
80 cancellationToken.ThrowIfCancellationRequested();
81
82 var horizon = problem.Horizon;
83
84 var prefixBuffer =
85 ArrayPool<double>.Shared.Rent(horizon + 1);
86
87 var transformedCostBuffer =
88 ArrayPool<double>.Shared.Rent(horizon);
89
90 var valueBuffer =
91 ArrayPool<double>.Shared.Rent(horizon + 1);
92
93 var predecessorBuffer =
94 ArrayPool<int>.Shared.Rent(horizon + 1);
95
96 var interceptBuffer =
97 ArrayPool<double>.Shared.Rent(horizon);
98
99 var orderBuffer =
100 ArrayPool<int>.Shared.Rent(horizon);
101
102 var scratchBuffer =
103 ArrayPool<int>.Shared.Rent(horizon);
104
105 var argMinBuffer =
106 ArrayPool<int>.Shared.Rent(horizon + 1);
107
108 var matrixWorkspaceBuffer =
109 ArrayPool<int>.Shared.Rent(
110 checked((2 * (horizon + 1)) + 8));
111
112 var sortKeyBuffer =
113 ArrayPool<double>.Shared.Rent(horizon);
114
115 try
116 {
117 var prefixDemand =
118 prefixBuffer.AsSpan(0, horizon + 1);
119
120 var transformedCost =
121 transformedCostBuffer.AsSpan(0, horizon);
122
123 var value =
124 valueBuffer.AsSpan(0, horizon + 1);
125
126 var predecessor =
127 predecessorBuffer.AsSpan(0, horizon + 1);
128
129 var intercept =
130 interceptBuffer.AsSpan(0, horizon);
131
132 var order =
133 orderBuffer.AsSpan(0, horizon);
134
135 var scratch =
136 scratchBuffer.AsSpan(0, horizon);
137
138 var argMin =
139 argMinBuffer.AsSpan(0, horizon + 1);
140
141 var matrixWorkspace =
142 matrixWorkspaceBuffer.AsSpan(
143 0,
144 checked((2 * (horizon + 1)) + 8));
145
146 var sortKeys =
147 sortKeyBuffer.AsSpan(0, horizon);
148
149 BuildPrefixDemand(
150 problem.Demands,
151 prefixDemand);
152
153 BuildTransformedProductionCosts(
154 problem.UnitProductionCosts,
155 problem.HoldingCosts,
156 transformedCost);
157
158 value.Fill(double.PositiveInfinity);
159 predecessor.Fill(-1);
160 intercept.Clear();
161 argMin.Fill(-1);
162
163 value[0] = 0.0;
164
165 for (var period = 0;
166 period < horizon;
167 period++)
168 {
169 order[period] = period;
170
171 // Array.Sort is ascending. Negating the key produces
172 // nonincreasing transformed marginal costs.
173 sortKeys[period] =
174 -transformedCost[period];
175 }
176
177 Array.Sort(
178 sortKeyBuffer,
179 orderBuffer,
180 0,
181 horizon);
182
183 CanonicalizeEqualSlopeGroups(
184 sortKeyBuffer,
185 orderBuffer,
186 horizon);
187
188 SolveRange(
189 left: 0,
190 right: horizon,
191 orderStart: 0,
192 orderCount: horizon,
193 problem,
194 prefixDemand,
195 transformedCost,
196 value,
197 predecessor,
198 intercept,
199 order,
200 scratch,
201 argMin,
202 matrixWorkspace,
203 cancellationToken);
204
205 cancellationToken.ThrowIfCancellationRequested();
206
208 problem,
209 predecessor,
210 Name,
211 cancellationToken);
212 }
213 finally
214 {
215 ArrayPool<double>.Shared.Return(
216 prefixBuffer,
217 clearArray: false);
218
219 ArrayPool<double>.Shared.Return(
220 transformedCostBuffer,
221 clearArray: false);
222
223 ArrayPool<double>.Shared.Return(
224 valueBuffer,
225 clearArray: false);
226
227 ArrayPool<int>.Shared.Return(
228 predecessorBuffer,
229 clearArray: false);
230
231 ArrayPool<double>.Shared.Return(
232 interceptBuffer,
233 clearArray: false);
234
235 ArrayPool<int>.Shared.Return(
236 orderBuffer,
237 clearArray: false);
238
239 ArrayPool<int>.Shared.Return(
240 scratchBuffer,
241 clearArray: false);
242
243 ArrayPool<int>.Shared.Return(
244 argMinBuffer,
245 clearArray: false);
246
247 ArrayPool<int>.Shared.Return(
248 matrixWorkspaceBuffer,
249 clearArray: false);
250
251 ArrayPool<double>.Shared.Return(
252 sortKeyBuffer,
253 clearArray: false);
254 }
255 }
256
257 private static void SolveRange(
258 int left,
259 int right,
260 int orderStart,
261 int orderCount,
262 UlsProblem problem,
263 ReadOnlySpan<double> prefixDemand,
264 ReadOnlySpan<double> transformedCost,
265 Span<double> value,
266 Span<int> predecessor,
267 Span<double> intercept,
268 Span<int> order,
269 Span<int> scratch,
270 Span<int> argMin,
271 Span<int> matrixWorkspace,
272 CancellationToken cancellationToken)
273 {
274 cancellationToken.ThrowIfCancellationRequested();
275
276 if (left == right)
277 {
278 FinalizeState(
279 left,
280 problem,
281 prefixDemand,
282 transformedCost,
283 value,
284 predecessor,
285 intercept);
286
287 return;
288 }
289
290 var middle =
291 left + ((right - left) >> 1);
292
293 var leftOrderCount = 0;
294
295 for (var position = 0;
296 position < orderCount;
297 position++)
298 {
299 if (order[orderStart + position] <= middle)
300 {
301 leftOrderCount++;
302 }
303 }
304
305 var nextLeft = orderStart;
306 var nextRight = orderStart + leftOrderCount;
307
308 for (var position = 0;
309 position < orderCount;
310 position++)
311 {
312 var period =
313 order[orderStart + position];
314
315 if (period <= middle)
316 {
317 scratch[nextLeft++] = period;
318 }
319 else
320 {
321 scratch[nextRight++] = period;
322 }
323 }
324
325 scratch
326 .Slice(orderStart, orderCount)
327 .CopyTo(
328 order.Slice(orderStart, orderCount));
329
330 var rightOrderStart =
331 orderStart + leftOrderCount;
332
333 var rightOrderCount =
334 orderCount - leftOrderCount;
335
336 SolveRange(
337 left,
338 middle,
339 orderStart,
340 leftOrderCount,
341 problem,
342 prefixDemand,
343 transformedCost,
344 value,
345 predecessor,
346 intercept,
347 order,
348 scratch,
349 argMin,
350 matrixWorkspace,
351 cancellationToken);
352
353 RelaxCrossMatrix(
354 targetStart: middle + 1,
355 targetEnd: right,
356 predecessorColumns:
357 order.Slice(
358 orderStart,
359 leftOrderCount),
360 prefixDemand,
361 transformedCost,
362 intercept,
363 value,
364 predecessor,
365 argMin,
366 matrixWorkspace);
367
368 SolveRange(
369 middle + 1,
370 right,
371 rightOrderStart,
372 rightOrderCount,
373 problem,
374 prefixDemand,
375 transformedCost,
376 value,
377 predecessor,
378 intercept,
379 order,
380 scratch,
381 argMin,
382 matrixWorkspace,
383 cancellationToken);
384
385 MergeSlopeOrderedChildren(
386 orderStart,
387 leftOrderCount,
388 rightOrderCount,
389 transformedCost,
390 order,
391 scratch);
392 }
393
394 private static void RelaxCrossMatrix(
395 int targetStart,
396 int targetEnd,
397 ReadOnlySpan<int> predecessorColumns,
398 ReadOnlySpan<double> prefixDemand,
399 ReadOnlySpan<double> transformedCost,
400 ReadOnlySpan<double> intercept,
401 Span<double> value,
402 Span<int> predecessor,
403 Span<int> argMin,
404 Span<int> matrixWorkspace)
405 {
406 if (predecessorColumns.IsEmpty ||
407 targetStart > targetEnd)
408 {
409 return;
410 }
411
412 var rowCount =
413 targetEnd - targetStart + 1;
414
415 AggarwalParkMatrixSearch.FindRowMinima(
416 targetStart,
417 rowCount,
418 predecessorColumns,
419 prefixDemand,
420 transformedCost,
421 intercept,
422 argMin,
423 matrixWorkspace);
424
425 for (var target = targetStart;
426 target <= targetEnd;
427 target++)
428 {
429 var candidatePredecessor =
430 argMin[target];
431
432 if (candidatePredecessor < 0)
433 {
434 throw new InvalidOperationException(
435 "Aggarwal-Park matrix search did not return a predecessor.");
436 }
437
438 var candidateValue =
439 AggarwalParkMatrixSearch.Evaluate(
440 target,
441 candidatePredecessor,
442 prefixDemand,
443 transformedCost,
444 intercept);
445
446 if (candidateValue < value[target])
447 {
448 value[target] = candidateValue;
449 predecessor[target] =
450 candidatePredecessor;
451 }
452 }
453 }
454
455 private static void FinalizeState(
456 int state,
457 UlsProblem problem,
458 ReadOnlySpan<double> prefixDemand,
459 ReadOnlySpan<double> transformedCost,
460 Span<double> value,
461 Span<int> predecessor,
462 Span<double> intercept)
463 {
464 if (state > 0 &&
465 problem.Demands[state - 1] == 0.0 &&
466 value[state - 1] <= value[state])
467 {
468 value[state] =
469 value[state - 1];
470
471 predecessor[state] =
472 state - 1;
473 }
474
475 if (!double.IsFinite(value[state]))
476 {
477 throw new ArithmeticException(
478 $"No finite Aggarwal-Park dynamic-programming value was obtained for state {state}.");
479 }
480
481 if (state >= problem.Horizon)
482 {
483 return;
484 }
485
486 var transformedDemandCost =
487 transformedCost[state] *
488 prefixDemand[state];
489
490 var candidateIntercept =
491 value[state] +
492 problem.SetupCosts[state] -
493 transformedDemandCost;
494
495 if (!double.IsFinite(transformedDemandCost) ||
496 !double.IsFinite(candidateIntercept))
497 {
498 throw new ArithmeticException(
499 "Numerical overflow while constructing an Aggarwal-Park predecessor line.");
500 }
501
502 intercept[state] =
503 candidateIntercept;
504 }
505
506 private static void BuildPrefixDemand(
507 ReadOnlySpan<double> demands,
508 Span<double> prefixDemand)
509 {
510 prefixDemand[0] = 0.0;
511
512 for (var period = 0;
513 period < demands.Length;
514 period++)
515 {
516 var next =
517 prefixDemand[period] +
518 demands[period];
519
520 if (!double.IsFinite(next))
521 {
522 throw new ArithmeticException(
523 "Numerical overflow while computing cumulative demand.");
524 }
525
526 prefixDemand[period + 1] = next;
527 }
528 }
529
530 private static void BuildTransformedProductionCosts(
531 ReadOnlySpan<double> productionCosts,
532 ReadOnlySpan<double> holdingCosts,
533 Span<double> transformedCost)
534 {
535 var holdingSuffix = 0.0;
536
537 for (var period = productionCosts.Length - 1;
538 period >= 0;
539 period--)
540 {
541 if (period < productionCosts.Length - 1)
542 {
543 holdingSuffix +=
544 holdingCosts[period];
545
546 if (!double.IsFinite(holdingSuffix))
547 {
548 throw new ArithmeticException(
549 "Numerical overflow while computing holding-cost suffix.");
550 }
551 }
552
553 var value =
554 productionCosts[period] +
555 holdingSuffix;
556
557 if (!double.IsFinite(value))
558 {
559 throw new ArithmeticException(
560 "Numerical overflow while computing transformed production cost.");
561 }
562
563 transformedCost[period] = value;
564 }
565 }
566
567 private static void CanonicalizeEqualSlopeGroups(
568 double[] sortedKeys,
569 int[] order,
570 int length)
571 {
572 var start = 0;
573
574 while (start < length)
575 {
576 var end = start + 1;
577
578 while (end < length &&
579 sortedKeys[end] == sortedKeys[start])
580 {
581 end++;
582 }
583
584 if (end - start > 1)
585 {
586 Array.Sort(
587 order,
588 start,
589 end - start);
590 }
591
592 start = end;
593 }
594 }
595
596 private static void MergeSlopeOrderedChildren(
597 int orderStart,
598 int leftCount,
599 int rightCount,
600 ReadOnlySpan<double> transformedCost,
601 Span<int> order,
602 Span<int> scratch)
603 {
604 if (leftCount == 0 ||
605 rightCount == 0)
606 {
607 return;
608 }
609
610 var leftPosition = orderStart;
611 var leftEnd = orderStart + leftCount;
612
613 var rightPosition = leftEnd;
614 var rightEnd = rightPosition + rightCount;
615
616 var destination = orderStart;
617
618 while (leftPosition < leftEnd &&
619 rightPosition < rightEnd)
620 {
621 var leftPeriod =
622 order[leftPosition];
623
624 var rightPeriod =
625 order[rightPosition];
626
627 if (ComesBefore(
628 leftPeriod,
629 rightPeriod,
630 transformedCost))
631 {
632 scratch[destination++] =
633 leftPeriod;
634
635 leftPosition++;
636 }
637 else
638 {
639 scratch[destination++] =
640 rightPeriod;
641
642 rightPosition++;
643 }
644 }
645
646 while (leftPosition < leftEnd)
647 {
648 scratch[destination++] =
649 order[leftPosition++];
650 }
651
652 while (rightPosition < rightEnd)
653 {
654 scratch[destination++] =
655 order[rightPosition++];
656 }
657
658 scratch
659 .Slice(
660 orderStart,
661 leftCount + rightCount)
662 .CopyTo(
663 order.Slice(
664 orderStart,
665 leftCount + rightCount));
666 }
667
668 private static bool ComesBefore(
669 int leftPeriod,
670 int rightPeriod,
671 ReadOnlySpan<double> transformedCost)
672 {
673 var leftSlope =
674 transformedCost[leftPeriod];
675
676 var rightSlope =
677 transformedCost[rightPeriod];
678
679 if (leftSlope > rightSlope)
680 {
681 return true;
682 }
683
684 if (leftSlope < rightSlope)
685 {
686 return false;
687 }
688
689 return leftPeriod < rightPeriod;
690 }
691}
Implements the Aggarwal-Park recursive Monge-matrix algorithm for the uncapacitated economic lot-sizi...
UlsSolverKind Kind
Gets the broad family of the solver.
string Name
Gets the stable human-readable name of the solver.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing problem.The solve result.
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.