52 private const int CancellationCheckMask = 255;
55 public string Name =>
"Wagner-Whitin (Wagelmans linear-time)";
72 ArgumentNullException.ThrowIfNull(problem);
77 for (var period = 0; period < problem.Horizon - 1; period++)
79 var currentDeliveredNextPeriod =
80 productionCosts[period] + holdingCosts[period];
82 if (!
double.IsFinite(currentDeliveredNextPeriod) ||
83 currentDeliveredNextPeriod < productionCosts[period + 1])
103 CancellationToken cancellationToken =
default)
105 ArgumentNullException.ThrowIfNull(problem);
106 cancellationToken.ThrowIfCancellationRequested();
110 throw new NotSupportedException(
111 "WagnerWhitinSolver requires p[t] + h[t] >= p[t+1] " +
112 "for every adjacent pair of periods.");
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);
127 var suffixDemand = suffixBuffer.AsSpan(0, horizon + 1);
128 var next = nextBuffer.AsSpan(0, horizon);
129 var hull = hullBuffer.AsSpan(0, horizon + 1);
131 BuildSuffixDemand(demands, suffixDemand);
135 hull[0] =
new HullLine(
138 startX:
double.NegativeInfinity,
142 var accumulatedRelevantHoldingCost = 0.0;
144 for (var period = horizon - 1; period >= 0; period--)
146 if ((period & CancellationCheckMask) == 0)
148 cancellationToken.ThrowIfCancellationRequested();
154 if (period < horizon - 1)
156 accumulatedRelevantHoldingCost = AddFinite(
157 accumulatedRelevantHoldingCost,
158 holdingCosts[period],
159 "transformed holding-cost suffix");
162 var transformedMarginalCost = AddFinite(
163 productionCosts[period],
164 accumulatedRelevantHoldingCost,
165 "transformed marginal production cost");
171 transformedMarginalCost);
173 var bestLine = hull[head];
175 suffixDemand[period] - suffixDemand[bestLine.Period];
177 var setupValue = AddFinite(
180 transformedMarginalCost,
182 "transformed variable production cost"),
183 "setup plus transformed variable cost");
185 setupValue = AddFinite(
188 "dynamic-programming value");
192 if (demands[period] == 0.0 && nextValue <= setupValue)
200 next[period] = bestLine.Period;
210 slope: -suffixDemand[period],
212 startX:
double.NegativeInfinity,
216 cancellationToken.ThrowIfCancellationRequested();
226 ArrayPool<double>.Shared.Return(suffixBuffer, clearArray:
false);
227 ArrayPool<int>.Shared.Return(nextBuffer, clearArray:
false);
228 ArrayPool<HullLine>.Shared.Return(hullBuffer, clearArray:
false);
234 ReadOnlySpan<double> suffixDemand,
235 ReadOnlySpan<int> next,
236 CancellationToken cancellationToken)
239 var productionQuantities =
new double[horizon];
240 var endingInventories =
new double[horizon];
241 var setupDecisions =
new bool[horizon];
245 while (period < horizon)
247 if ((period & CancellationCheckMask) == 0)
249 cancellationToken.ThrowIfCancellationRequested();
252 var successor = next[period];
260 if (successor <= period || successor > horizon)
262 throw new InvalidOperationException(
263 "The Wagner-Whitin predecessor chain is inconsistent.");
266 productionQuantities[period] =
267 suffixDemand[period] - suffixDemand[successor];
268 setupDecisions[period] =
true;
270 for (var inventoryPeriod = period;
271 inventoryPeriod < successor;
274 endingInventories[inventoryPeriod] =
275 suffixDemand[inventoryPeriod + 1] -
276 suffixDemand[successor];
283 var productionCost = 0.0;
284 var holdingCost = 0.0;
290 for (period = 0; period < horizon; period++)
292 if ((period & CancellationCheckMask) == 0)
294 cancellationToken.ThrowIfCancellationRequested();
297 if (setupDecisions[period])
299 setupCost = AddFinite(
302 "solution setup cost");
305 productionCost = AddFinite(
308 productionCosts[period],
309 productionQuantities[period],
310 "solution production cost"),
311 "solution production cost");
313 holdingCost = AddFinite(
316 holdingCosts[period],
317 endingInventories[period],
318 "solution holding cost"),
319 "solution holding cost");
322 var solution = UlsSolution.FromOwnedBuffers(
323 productionQuantities,
330 return new UlsSolveResult(
336 private static void BuildSuffixDemand(
337 ReadOnlySpan<double> demands,
338 Span<double> suffixDemand)
340 suffixDemand[demands.Length] = 0.0;
342 for (var period = demands.Length - 1; period >= 0; period--)
344 suffixDemand[period] = AddFinite(
345 suffixDemand[period + 1],
347 "cumulative demand");
351 private static void AdvanceQueryHead(
352 ReadOnlySpan<HullLine> hull,
357 while (head < tail && hull[head + 1].StartX <= x)
363 private static void AddLine(
371 var last = hull[tail];
373 if (newLine.Slope == last.Slope)
375 if (newLine.Intercept >= last.Intercept)
384 if (last.Slope <= newLine.Slope)
386 throw new InvalidOperationException(
387 "The Wagner-Whitin convex hull received nonmonotone slopes.");
390 var startX = IntersectionX(last, newLine);
392 if (tail == head || startX > last.StartX)
403 hull[tail] =
new HullLine(
406 double.NegativeInfinity,
411 var intersection = IntersectionX(hull[tail], newLine);
414 hull[tail] =
new HullLine(
421 private static double IntersectionX(
425 var denominator = left.Slope - right.Slope;
427 if (!(denominator > 0.0) || !
double.IsFinite(denominator))
429 throw new ArithmeticException(
430 "A convex-hull line intersection has an invalid denominator.");
433 var numerator = right.Intercept - left.Intercept;
434 var intersection = numerator / denominator;
436 if (!
double.IsFinite(intersection))
438 throw new ArithmeticException(
439 "A convex-hull line intersection is not finite.");
445 private static double AddFinite(
450 var value = left + right;
452 if (!
double.IsFinite(value))
454 throw new ArithmeticException(
455 $
"Numerical overflow while computing {operation}.");
461 private static double MultiplyFinite(
466 var value = left * right;
468 if (!
double.IsFinite(value))
470 throw new ArithmeticException(
471 $
"Numerical overflow while computing {operation}.");
477 private readonly
struct HullLine
486 Intercept = intercept;
491 public double Slope {
get; }
493 public double Intercept {
get; }
495 public double StartX {
get; }
497 public int Period {
get; }