ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
WagelmansGeneralSolver.cs
Go to the documentation of this file.
1using System.Buffers;
5
7
8/// <summary>
9/// Solves the general uncapacitated economic lot-sizing problem in
10/// <c>O(n log n)</c> time using the backward geometric algorithm of
11/// Wagelmans, van Hoesel and Kolen.
12/// </summary>
13/// <remarks>
14/// <para>
15/// The solver implements the backward dynamic-programming formulation of
16/// Wagelmans, van Hoesel and Kolen (1992). Cumulative demand coordinates are
17/// monotone, so candidate continuation states can be inserted into a lower
18/// convex envelope with a simple array-backed stack. General transformed
19/// production costs are not necessarily monotone; each query is therefore
20/// located by binary search, yielding <c>O(n log n)</c> total time and
21/// <c>O(n)</c> auxiliary memory.
22/// </para>
23/// <para>
24/// The implementation uses the standard zero-holding-cost transformation
25/// <c>r[t] = p[t] + sum(h[j], j=t..n-2)</c>. The last holding-cost coefficient
26/// is omitted because terminal inventory is fixed to zero; omitting this common
27/// additive shift does not change any minimizing production periods.
28/// </para>
29/// <para>
30/// Original algorithmic source:
31/// A. Wagelmans, S. van Hoesel, A. Kolen,
32/// "Economic Lot Sizing: An O(n log n) Algorithm That Runs in Linear Time
33/// in the Wagner-Whitin Case", Operations Research 40(S1), S145-S156, 1992.
34/// DOI: 10.1287/opre.40.1.S145.
35/// </para>
36/// <para>
37/// Implementation/data-structure source:
38/// S. van Hoesel, A. Wagelmans, B. Moerman,
39/// "Using Geometric Techniques to Improve Dynamic Programming Algorithms for
40/// the Economic Lot-Sizing Problem and Extensions",
41/// European Journal of Operational Research 75(2), 312-331, 1994.
42/// DOI: 10.1016/0377-2217(94)90077-9.
43/// </para>
44/// <para>
45/// The 1994 computational study reports that the backward geometric algorithm
46/// is especially effective and emphasizes that only a stack plus binary search
47/// is required. This C# implementation follows that backward formulation with
48/// contiguous pooled arrays and no LINQ in the hot path.
49/// </para>
50/// <para>
51/// The papers allow more general signed cost coefficients. The current
52/// <see cref="UlsProblem"/> contract deliberately restricts input demand and
53/// costs to finite non-negative values, so this implementation exposes the
54/// corresponding non-negative-cost subset of the published model.
55/// </para>
56/// </remarks>
58{
59 private const int CancellationCheckMask = 255;
60
61 /// <inheritdoc />
62 public string Name => "Wagelmans general O(n log n)";
63
64 /// <inheritdoc />
66
67 /// <inheritdoc />
68 /// <exception cref="ArithmeticException">
69 /// Thrown when a cumulative demand, transformed cost, line intersection,
70 /// or dynamic-programming value is not representable as a finite
71 /// <see cref="double"/>.
72 /// </exception>
74 UlsProblem problem,
75 CancellationToken cancellationToken = default)
76 {
77 ArgumentNullException.ThrowIfNull(problem);
78 cancellationToken.ThrowIfCancellationRequested();
79
80 var horizon = problem.Horizon;
81
82 var prefixBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
83 var transformedCostBuffer = ArrayPool<double>.Shared.Rent(horizon);
84 var valueBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
85 var successorBuffer = ArrayPool<int>.Shared.Rent(horizon);
86 var hullBuffer = ArrayPool<HullLine>.Shared.Rent(horizon + 1);
87
88 try
89 {
90 var prefixDemand = prefixBuffer.AsSpan(0, horizon + 1);
91 var transformedCost = transformedCostBuffer.AsSpan(0, horizon);
92 var value = valueBuffer.AsSpan(0, horizon + 1);
93 var successor = successorBuffer.AsSpan(0, horizon);
94 var hull = hullBuffer.AsSpan(0, horizon + 1);
95
96 BuildPrefixDemand(problem.Demands, prefixDemand);
97 BuildTransformedProductionCosts(
98 problem.UnitProductionCosts,
99 problem.HoldingCosts,
100 transformedCost);
101
102 value[horizon] = 0.0;
103 successor.Fill(-1);
104
105 var hullCount = 1;
106 hull[0] = new HullLine(
107 slope: prefixDemand[horizon],
108 intercept: 0.0,
109 startX: double.NegativeInfinity,
110 successorPeriod: horizon);
111
112 var setupCosts = problem.SetupCosts;
113 var demands = problem.Demands;
114
115 for (var period = horizon - 1; period >= 0; period--)
116 {
117 if ((period & CancellationCheckMask) == 0)
118 {
119 cancellationToken.ThrowIfCancellationRequested();
120 }
121
122 var queryX = transformedCost[period];
123 var bestLine = Query(hull, hullCount, queryX);
124
125 var coveredDemand =
126 prefixDemand[bestLine.SuccessorPeriod] -
127 prefixDemand[period];
128
129 var setupValue = AddFinite(
130 setupCosts[period],
131 MultiplyFinite(
132 queryX,
133 coveredDemand,
134 "transformed variable production cost"),
135 "setup plus transformed variable production cost");
136
137 setupValue = AddFinite(
138 setupValue,
139 bestLine.Intercept,
140 "backward dynamic-programming value");
141
142 if (demands[period] == 0.0 &&
143 value[period + 1] <= setupValue)
144 {
145 value[period] = value[period + 1];
146 successor[period] = -1;
147 }
148 else
149 {
150 value[period] = setupValue;
151 successor[period] = bestLine.SuccessorPeriod;
152 }
153
154 hullCount = AddLine(
155 hull,
156 hullCount,
157 new HullLine(
158 slope: prefixDemand[period],
159 intercept: value[period],
160 startX: double.NegativeInfinity,
161 successorPeriod: period));
162 }
163
164 cancellationToken.ThrowIfCancellationRequested();
165
166 return BuildResult(
167 problem,
168 prefixDemand,
169 successor,
170 cancellationToken);
171 }
172 finally
173 {
174 ArrayPool<double>.Shared.Return(prefixBuffer, clearArray: false);
175 ArrayPool<double>.Shared.Return(transformedCostBuffer, clearArray: false);
176 ArrayPool<double>.Shared.Return(valueBuffer, clearArray: false);
177 ArrayPool<int>.Shared.Return(successorBuffer, clearArray: false);
178 ArrayPool<HullLine>.Shared.Return(hullBuffer, clearArray: false);
179 }
180 }
181
182 private static void BuildPrefixDemand(
183 ReadOnlySpan<double> demands,
184 Span<double> prefixDemand)
185 {
186 prefixDemand[0] = 0.0;
187
188 for (var period = 0; period < demands.Length; period++)
189 {
190 prefixDemand[period + 1] = AddFinite(
191 prefixDemand[period],
192 demands[period],
193 "cumulative demand");
194 }
195 }
196
197 private static void BuildTransformedProductionCosts(
198 ReadOnlySpan<double> productionCosts,
199 ReadOnlySpan<double> holdingCosts,
200 Span<double> transformedCosts)
201 {
202 var relevantHoldingSuffix = 0.0;
203
204 for (var period = productionCosts.Length - 1;
205 period >= 0;
206 period--)
207 {
208 if (period < productionCosts.Length - 1)
209 {
210 relevantHoldingSuffix = AddFinite(
211 relevantHoldingSuffix,
212 holdingCosts[period],
213 "holding-cost suffix");
214 }
215
216 transformedCosts[period] = AddFinite(
217 productionCosts[period],
218 relevantHoldingSuffix,
219 "transformed production cost");
220 }
221 }
222
223 private static HullLine Query(
224 ReadOnlySpan<HullLine> hull,
225 int hullCount,
226 double x)
227 {
228 var low = 0;
229 var high = hullCount - 1;
230
231 while (low < high)
232 {
233 var middle = low + ((high - low + 1) >> 1);
234
235 if (hull[middle].StartX <= x)
236 {
237 low = middle;
238 }
239 else
240 {
241 high = middle - 1;
242 }
243 }
244
245 return hull[low];
246 }
247
248 private static int AddLine(
249 Span<HullLine> hull,
250 int hullCount,
251 HullLine newLine)
252 {
253 while (hullCount > 0)
254 {
255 var last = hull[hullCount - 1];
256
257 if (newLine.Slope == last.Slope)
258 {
259 if (newLine.Intercept >= last.Intercept)
260 {
261 return hullCount;
262 }
263
264 hullCount--;
265 continue;
266 }
267
268 if (last.Slope <= newLine.Slope)
269 {
270 throw new InvalidOperationException(
271 "WagelmansGeneralSolver received nonmonotone cumulative-demand slopes.");
272 }
273
274 var startX = IntersectionX(last, newLine);
275
276 if (hullCount == 1 || startX > last.StartX)
277 {
278 break;
279 }
280
281 hullCount--;
282 }
283
284 var activationX =
285 hullCount == 0
286 ? double.NegativeInfinity
287 : IntersectionX(hull[hullCount - 1], newLine);
288
289 hull[hullCount] = new HullLine(
290 newLine.Slope,
291 newLine.Intercept,
292 activationX,
293 newLine.SuccessorPeriod);
294
295 return hullCount + 1;
296 }
297
298 private static double IntersectionX(
299 HullLine left,
300 HullLine right)
301 {
302 var denominator = left.Slope - right.Slope;
303
304 if (!(denominator > 0.0) || !double.IsFinite(denominator))
305 {
306 throw new ArithmeticException(
307 "A Wagelmans convex-envelope intersection has an invalid denominator.");
308 }
309
310 var numerator = right.Intercept - left.Intercept;
311 var intersection = numerator / denominator;
312
313 if (!double.IsFinite(intersection))
314 {
315 throw new ArithmeticException(
316 "A Wagelmans convex-envelope intersection is not finite.");
317 }
318
319 return intersection;
320 }
321
322 private static UlsSolveResult BuildResult(
323 UlsProblem problem,
324 ReadOnlySpan<double> prefixDemand,
325 ReadOnlySpan<int> successor,
326 CancellationToken cancellationToken)
327 {
328 var horizon = problem.Horizon;
329 var production = new double[horizon];
330 var inventory = new double[horizon];
331 var setup = new bool[horizon];
332
333 var period = 0;
334
335 while (period < horizon)
336 {
337 if ((period & CancellationCheckMask) == 0)
338 {
339 cancellationToken.ThrowIfCancellationRequested();
340 }
341
342 var next = successor[period];
343
344 if (next < 0)
345 {
346 period++;
347 continue;
348 }
349
350 if (next <= period || next > horizon)
351 {
352 throw new InvalidOperationException(
353 "The Wagelmans successor chain is inconsistent.");
354 }
355
356 production[period] =
357 prefixDemand[next] - prefixDemand[period];
358
359 setup[period] = production[period] > 0.0;
360 period = next;
361 }
362
363 var setupCost = 0.0;
364 var productionCost = 0.0;
365 var holdingCost = 0.0;
366 var runningInventory = 0.0;
367
368 var demands = problem.Demands;
369 var setupCosts = problem.SetupCosts;
370 var productionCosts = problem.UnitProductionCosts;
371 var holdingCosts = problem.HoldingCosts;
372
373 for (period = 0; period < horizon; period++)
374 {
375 if ((period & CancellationCheckMask) == 0)
376 {
377 cancellationToken.ThrowIfCancellationRequested();
378 }
379
380 if (setup[period])
381 {
382 setupCost = AddFinite(
383 setupCost,
384 setupCosts[period],
385 "solution setup cost");
386 }
387
388 productionCost = AddFinite(
389 productionCost,
390 MultiplyFinite(
391 productionCosts[period],
392 production[period],
393 "solution production cost"),
394 "solution production cost");
395
396 runningInventory = AddFinite(
397 runningInventory,
398 production[period],
399 "inventory balance");
400
401 runningInventory -= demands[period];
402
403 var tolerance = 1e-10 * Math.Max(
404 1.0,
405 Math.Max(Math.Abs(runningInventory), Math.Abs(demands[period])));
406
407 if (runningInventory < -tolerance)
408 {
409 throw new InvalidOperationException(
410 $"Reconstructed solution has negative inventory at period {period}.");
411 }
412
413 if (runningInventory < 0.0)
414 {
415 runningInventory = 0.0;
416 }
417
418 inventory[period] = runningInventory;
419
420 holdingCost = AddFinite(
421 holdingCost,
422 MultiplyFinite(
423 holdingCosts[period],
424 inventory[period],
425 "solution holding cost"),
426 "solution holding cost");
427 }
428
429 var solution = UlsSolution.FromOwnedBuffers(
430 production,
431 inventory,
432 setup,
433 setupCost,
434 productionCost,
435 holdingCost);
436
437 return new UlsSolveResult(
438 "Wagelmans general O(n log n)",
439 UlsSolveStatus.Optimal,
440 solution);
441 }
442
443 private static double AddFinite(
444 double left,
445 double right,
446 string operation)
447 {
448 var value = left + right;
449
450 if (!double.IsFinite(value))
451 {
452 throw new ArithmeticException(
453 $"Numerical overflow while computing {operation}.");
454 }
455
456 return value;
457 }
458
459 private static double MultiplyFinite(
460 double left,
461 double right,
462 string operation)
463 {
464 var value = left * right;
465
466 if (!double.IsFinite(value))
467 {
468 throw new ArithmeticException(
469 $"Numerical overflow while computing {operation}.");
470 }
471
472 return value;
473 }
474
475 private readonly struct HullLine
476 {
477 public HullLine(
478 double slope,
479 double intercept,
480 double startX,
481 int successorPeriod)
482 {
483 Slope = slope;
484 Intercept = intercept;
485 StartX = startX;
486 SuccessorPeriod = successorPeriod;
487 }
488
489 public double Slope { get; }
490
491 public double Intercept { get; }
492
493 public double StartX { get; }
494
495 public int SuccessorPeriod { get; }
496 }
497}
Solves the general uncapacitated economic lot-sizing problem in O(n log n) time using the backward ge...
string Name
Gets the stable human-readable name of the solver.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
UlsSolverKind Kind
Gets the broad family of the solver.
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.