ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
BahlTajPlanningHorizonSolver.cs
Go to the documentation of this file.
1using System.Buffers;
6
8
9/// <summary>
10/// Implements the data-dependent Wagner-Whitin implementation proposed by
11/// Bahl and Taj, combining Evans' low-storage recurrence with the
12/// Wagner-Whitin Planning Horizon Theorem.
13/// </summary>
14/// <remarks>
15/// <para>
16/// Bahl and Taj (1991) explicitly modify the efficient Evans (1985)
17/// implementation by incorporating Wagner's setup/planning-horizon theorem.
18/// Once the optimal solution of a positive-demand prefix has its last setup
19/// at period <c>j</c>, the theorem permits all earlier candidate setup periods
20/// to be excluded from subsequent prefix optimizations. The lower candidate
21/// bound is therefore monotone and advances when the data reveal a later
22/// planning-horizon boundary.
23/// </para>
24/// <para>
25/// This implementation keeps Evans' incremental regeneration-interval costs:
26/// no triangular <c>O(n^2)</c> matrix is materialized. Candidates pruned by
27/// the planning-horizon bound are never updated again.
28/// </para>
29/// <para>
30/// The public implementation conservatively enforces the standard
31/// Wagner-Whitin / no-speculative-motive cost condition
32/// <c>p[t] + h[t] &gt;= p[t+1]</c>. Under that condition the planning-horizon
33/// pruning used here is valid.
34/// </para>
35/// <para>
36/// Worst-case time complexity remains <c>O(n^2)</c>; auxiliary working memory
37/// is <c>O(n)</c>. The actual number of candidate evaluations is data
38/// dependent. When successive optimal prefixes repeatedly establish new,
39/// late planning horizons, the practical work can approach <c>O(n)</c>.
40/// </para>
41/// <para>
42/// Primary implementation reference:
43/// H. C. Bahl and S. Taj,
44/// "A data-dependent efficient implementation of the Wagner-Whitin algorithm
45/// for lot-sizing",
46/// Computers &amp; Industrial Engineering 20(2), 289-291, 1991.
47/// DOI: 10.1016/0360-8352(91)90033-3.
48/// </para>
49/// <para>
50/// Low-storage recurrence reference:
51/// J. R. Evans,
52/// "An Efficient Implementation of the Wagner-Whitin Algorithm for Dynamic
53/// Lot-Sizing",
54/// Journal of Operations Management 5(2), 229-235, 1985.
55/// DOI: 10.1016/0272-6963(85)90009-9.
56/// </para>
57/// <para>
58/// Planning-horizon theorem reference:
59/// H. M. Wagner and T. M. Whitin,
60/// "Dynamic Version of the Economic Lot Size Model",
61/// Management Science 5(1), 89-96, 1958.
62/// DOI: 10.1287/mnsc.5.1.89.
63/// </para>
64/// <para>
65/// This is a modern C# realization of the algorithmic principle described by
66/// Bahl and Taj, not a transliteration of their original source code.
67/// </para>
68/// </remarks>
70{
71 private const int CancellationCheckMask = 255;
72
73 /// <inheritdoc />
74 public string Name =>
75 "Bahl-Taj planning-horizon Wagner-Whitin";
76
77 /// <inheritdoc />
79
80 /// <summary>
81 /// Determines whether the Wagner-Whitin / no-speculative-motive
82 /// condition holds for the supplied problem.
83 /// </summary>
84 public static bool IsApplicable(UlsProblem problem)
85 {
86 ArgumentNullException.ThrowIfNull(problem);
87
88 var productionCosts = problem.UnitProductionCosts;
89 var holdingCosts = problem.HoldingCosts;
90
91 for (var period = 0; period < problem.Horizon - 1; period++)
92 {
93 var deliveredNext =
94 productionCosts[period] +
95 holdingCosts[period];
96
97 if (!double.IsFinite(deliveredNext) ||
98 deliveredNext < productionCosts[period + 1])
99 {
100 return false;
101 }
102 }
103
104 return true;
105 }
106
107 /// <inheritdoc />
108 /// <exception cref="NotSupportedException">
109 /// Thrown when the Wagner-Whitin / no-speculative-motive condition is
110 /// violated.
111 /// </exception>
113 UlsProblem problem,
114 CancellationToken cancellationToken = default)
115 {
116 ArgumentNullException.ThrowIfNull(problem);
117 cancellationToken.ThrowIfCancellationRequested();
118
119 if (!IsApplicable(problem))
120 {
121 throw new NotSupportedException(
122 "BahlTajPlanningHorizonSolver requires " +
123 "p[t] + h[t] >= p[t+1] for every adjacent period.");
124 }
125
126 var horizon = problem.Horizon;
127
128 var valueBuffer =
129 ArrayPool<double>.Shared.Rent(horizon + 1);
130
131 var predecessorBuffer =
132 ArrayPool<int>.Shared.Rent(horizon + 1);
133
134 var intervalCostBuffer =
135 ArrayPool<double>.Shared.Rent(horizon);
136
137 var deliveredCostBuffer =
138 ArrayPool<double>.Shared.Rent(horizon);
139
140 var cumulativeDemandBuffer =
141 ArrayPool<double>.Shared.Rent(horizon);
142
143 try
144 {
145 var value =
146 valueBuffer.AsSpan(0, horizon + 1);
147
148 var predecessor =
149 predecessorBuffer.AsSpan(0, horizon + 1);
150
151 var intervalCost =
152 intervalCostBuffer.AsSpan(0, horizon);
153
154 var deliveredCost =
155 deliveredCostBuffer.AsSpan(0, horizon);
156
157 var cumulativeDemand =
158 cumulativeDemandBuffer.AsSpan(0, horizon);
159
160 value.Fill(double.PositiveInfinity);
161 predecessor.Fill(-1);
162 intervalCost.Clear();
163 deliveredCost.Clear();
164 cumulativeDemand.Clear();
165
166 value[0] = 0.0;
167
168 var demands = problem.Demands;
169 var setupCosts = problem.SetupCosts;
170 var productionCosts = problem.UnitProductionCosts;
171 var holdingCosts = problem.HoldingCosts;
172
173 // Earliest setup period that still needs to be considered.
174 // Wagner-Whitin's Planning Horizon Theorem makes this bound
175 // monotone nondecreasing.
176 var planningHorizonStart = 0;
177
178 for (var end = 0; end < horizon; end++)
179 {
180 if ((end & CancellationCheckMask) == 0)
181 {
182 cancellationToken.ThrowIfCancellationRequested();
183 }
184
185 // A setup beginning at 'end' becomes a candidate for this
186 // prefix and every later prefix until planning-horizon pruning
187 // makes it obsolete.
188 deliveredCost[end] = productionCosts[end];
189 intervalCost[end] = 0.0;
190 cumulativeDemand[end] = 0.0;
191
192 if (demands[end] == 0.0)
193 {
194 // Extending an already solved prefix through a zero-demand
195 // period requires no setup. Crucially, this zero-length arc
196 // is NOT a planning-horizon certificate: a setup in an
197 // earlier period can still be optimal for future positive
198 // demand. The newly introduced candidate 'end' is retained
199 // for those future periods.
200 value[end + 1] = value[end];
201 predecessor[end + 1] = end;
202
203 AdvanceDeliveredCosts(
204 planningHorizonStart,
205 end,
206 holdingCosts[end],
207 deliveredCost);
208
209 continue;
210 }
211
212 var best = double.PositiveInfinity;
213 var bestStart = -1;
214
215 for (var start = planningHorizonStart;
216 start <= end;
217 start++)
218 {
219 cumulativeDemand[start] = AddFinite(
220 cumulativeDemand[start],
221 demands[end],
222 "candidate cumulative demand");
223
224 intervalCost[start] = AddFinite(
225 intervalCost[start],
226 MultiplyFinite(
227 demands[end],
228 deliveredCost[start],
229 "candidate delivered demand cost"),
230 "candidate regeneration-interval cost");
231
232 var regenerationCost = AddFinite(
233 setupCosts[start],
234 intervalCost[start],
235 "candidate setup plus regeneration cost");
236
237 var candidate = AddFinite(
238 value[start],
239 regenerationCost,
240 "forward dynamic-programming candidate");
241
242 // On an exact tie retain the later setup period. It is also
243 // an optimal predecessor and establishes the strongest
244 // planning-horizon bound allowed by the theorem.
245 if (candidate < best ||
246 (candidate == best && start > bestStart))
247 {
248 best = candidate;
249 bestStart = start;
250 }
251 }
252
253 if (!double.IsFinite(best) ||
254 bestStart < planningHorizonStart ||
255 bestStart > end)
256 {
257 throw new ArithmeticException(
258 $"No finite Bahl-Taj value was obtained for period {end}.");
259 }
260
261 value[end + 1] = best;
262 predecessor[end + 1] = bestStart;
263
264 // This is the Bahl-Taj data-dependent pruning step.
265 planningHorizonStart = bestStart;
266
267 AdvanceDeliveredCosts(
268 planningHorizonStart,
269 end,
270 holdingCosts[end],
271 deliveredCost);
272 }
273
274 cancellationToken.ThrowIfCancellationRequested();
275
277 problem,
278 predecessor,
279 Name,
280 cancellationToken);
281 }
282 finally
283 {
284 ArrayPool<double>.Shared.Return(
285 valueBuffer,
286 clearArray: false);
287
288 ArrayPool<int>.Shared.Return(
289 predecessorBuffer,
290 clearArray: false);
291
292 ArrayPool<double>.Shared.Return(
293 intervalCostBuffer,
294 clearArray: false);
295
296 ArrayPool<double>.Shared.Return(
297 deliveredCostBuffer,
298 clearArray: false);
299
300 ArrayPool<double>.Shared.Return(
301 cumulativeDemandBuffer,
302 clearArray: false);
303 }
304 }
305
306 private static void AdvanceDeliveredCosts(
307 int firstActiveStart,
308 int end,
309 double holdingCost,
310 Span<double> deliveredCost)
311 {
312 if (holdingCost == 0.0)
313 {
314 return;
315 }
316
317 for (var start = firstActiveStart;
318 start <= end;
319 start++)
320 {
321 deliveredCost[start] = AddFinite(
322 deliveredCost[start],
323 holdingCost,
324 "candidate delivered unit cost");
325 }
326 }
327
328 private static double AddFinite(
329 double left,
330 double right,
331 string operation)
332 {
333 var value = left + right;
334
335 if (!double.IsFinite(value))
336 {
337 throw new ArithmeticException(
338 $"Numerical overflow while computing {operation}.");
339 }
340
341 return value;
342 }
343
344 private static double MultiplyFinite(
345 double left,
346 double right,
347 string operation)
348 {
349 var value = left * right;
350
351 if (!double.IsFinite(value))
352 {
353 throw new ArithmeticException(
354 $"Numerical overflow while computing {operation}.");
355 }
356
357 return value;
358 }
359}
Implements the data-dependent Wagner-Whitin implementation proposed by Bahl and Taj,...
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
string Name
Gets the stable human-readable name of the solver.
static bool IsApplicable(UlsProblem problem)
Determines whether the Wagner-Whitin / no-speculative-motive condition holds for the supplied problem...
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.