ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
HeadyZhuEconomicPartPeriodSolver.cs
Go to the documentation of this file.
1using System.Buffers;
6
8
9/// <summary>
10/// Implements the Heady-Zhu family of improved Wagner-Whitin procedures using
11/// the Planning Horizon Theorem and the Economic-Part-Period pruning concept.
12/// </summary>
13/// <remarks>
14/// <para>
15/// Heady and Zhu (1994) report an improved exact implementation of the
16/// Wagner-Whitin algorithm based on the Planning Horizon Theorem and the
17/// Economic-Part-Period concept.
18/// </para>
19/// <para>
20/// This implementation realizes the fixed-cost specialization of that
21/// algorithmic idea. It requires a constant setup cost, a constant unit
22/// production cost and a constant per-unit-per-period holding cost over the
23/// economically relevant periods. These assumptions make the
24/// Economic-Part-Period cutoff exact and auditable.
25/// </para>
26/// <para>
27/// For fixed setup cost <c>A</c> and holding cost <c>h</c>, the economic
28/// part-period threshold is <c>A / h</c>. When extending an order one period
29/// earlier would add more than <c>A</c> of incremental holding cost, that
30/// candidate and all still earlier candidates are dominated by opening an
31/// additional setup. The backward predecessor scan can therefore stop.
32/// </para>
33/// <para>
34/// The solver also applies the Wagner-Whitin Planning Horizon Theorem: once a
35/// positive-demand prefix has an optimal last setup period, predecessor periods
36/// before that setup need not be reconsidered for later prefixes.
37/// </para>
38/// <para>
39/// Worst-case time remains <c>O(n^2)</c>, auxiliary working memory is
40/// <c>O(n)</c>, and the actual number of predecessor evaluations is strongly
41/// data dependent. Under favorable demand/cost ratios the scan length remains
42/// small and practical execution can be close to linear.
43/// </para>
44/// <para>
45/// Original reference:
46/// R. B. Heady and Z. Zhu,
47/// "An Improved Implementation of the Wagner-Whitin Algorithm",
48/// Production and Operations Management 3(1), 55-63, 1994.
49/// DOI: 10.1111/j.1937-5956.1994.tb00109.x.
50/// </para>
51/// <para>
52/// Explicit fixed-cost Economic-Part-Period implementation reference:
53/// S. J. Sadjadi, M. B. Gh. Aryanezhad and H. A. Sadeghi,
54/// "An Improved WAGNER-WHITIN Algorithm",
55/// International Journal of Industrial Engineering &amp; Production Research
56/// 20(3), 117-123, 2009.
57/// The paper gives the fixed-cost cutoff <c>DPP = A / H</c> and demonstrates
58/// the branch-pruning procedure on a 12-period example.
59/// </para>
60/// <para>
61/// Planning-horizon reference:
62/// H. M. Wagner and T. M. Whitin,
63/// "Dynamic Version of the Economic Lot Size Model",
64/// Management Science 5(1), 89-96, 1958.
65/// DOI: 10.1287/mnsc.5.1.89.
66/// </para>
67/// <para>
68/// The implementation is a modern, allocation-conscious reconstruction of the
69/// published algorithmic principles. It does not claim to be a transliteration
70/// of the unavailable original Heady-Zhu program listing.
71/// </para>
72/// </remarks>
74{
75 private const int CancellationCheckMask = 255;
76
77 /// <inheritdoc />
78 public string Name =>
79 "Heady-Zhu economic-part-period Wagner-Whitin";
80
81 /// <inheritdoc />
83
84 /// <summary>
85 /// Determines whether the fixed-cost assumptions required by this
86 /// implementation hold.
87 /// </summary>
88 public static bool IsApplicable(UlsProblem problem)
89 {
90 ArgumentNullException.ThrowIfNull(problem);
91
92 var horizon = problem.Horizon;
93 var setupCosts = problem.SetupCosts;
94 var productionCosts = problem.UnitProductionCosts;
95 var holdingCosts = problem.HoldingCosts;
96
97 var setupCost = setupCosts[0];
98 var productionCost = productionCosts[0];
99
100 for (var period = 1; period < horizon; period++)
101 {
102 if (setupCosts[period] != setupCost ||
103 productionCosts[period] != productionCost)
104 {
105 return false;
106 }
107 }
108
109 // The final holding-cost entry is irrelevant when terminal inventory
110 // is zero, so only transitions 0..horizon-2 need be constant.
111 if (horizon > 1)
112 {
113 var holdingCost = holdingCosts[0];
114
115 for (var period = 1; period < horizon - 1; period++)
116 {
117 if (holdingCosts[period] != holdingCost)
118 {
119 return false;
120 }
121 }
122 }
123
124 return true;
125 }
126
127 /// <summary>
128 /// Returns the Economic-Part-Period threshold <c>A / h</c>.
129 /// </summary>
130 /// <remarks>
131 /// Returns positive infinity when the relevant holding cost is zero.
132 /// </remarks>
133 public static double GetEconomicPartPeriodThreshold(UlsProblem problem)
134 {
135 ArgumentNullException.ThrowIfNull(problem);
136
137 if (!IsApplicable(problem))
138 {
139 throw new NotSupportedException(
140 "The Economic-Part-Period threshold requires constant " +
141 "setup, production and relevant holding costs.");
142 }
143
144 var holdingCost =
145 problem.Horizon > 1
146 ? problem.HoldingCosts[0]
147 : 0.0;
148
149 return holdingCost == 0.0
150 ? double.PositiveInfinity
151 : problem.SetupCosts[0] / holdingCost;
152 }
153
154 /// <inheritdoc />
155 /// <exception cref="NotSupportedException">
156 /// Thrown when setup costs, production costs, or economically relevant
157 /// holding costs are not constant.
158 /// </exception>
160 UlsProblem problem,
161 CancellationToken cancellationToken = default)
162 {
163 ArgumentNullException.ThrowIfNull(problem);
164 cancellationToken.ThrowIfCancellationRequested();
165
166 if (!IsApplicable(problem))
167 {
168 throw new NotSupportedException(
169 "HeadyZhuEconomicPartPeriodSolver requires constant setup " +
170 "costs, constant unit production costs and constant relevant " +
171 "holding costs.");
172 }
173
174 var horizon = problem.Horizon;
175
176 var valueBuffer =
177 ArrayPool<double>.Shared.Rent(horizon + 1);
178
179 var predecessorBuffer =
180 ArrayPool<int>.Shared.Rent(horizon + 1);
181
182 try
183 {
184 var value =
185 valueBuffer.AsSpan(0, horizon + 1);
186
187 var predecessor =
188 predecessorBuffer.AsSpan(0, horizon + 1);
189
190 value.Fill(double.PositiveInfinity);
191 predecessor.Fill(-1);
192
193 value[0] = 0.0;
194
195 var demands = problem.Demands;
196 var setupCost = problem.SetupCosts[0];
197 var productionCost = problem.UnitProductionCosts[0];
198
199 var holdingCost =
200 horizon > 1
201 ? problem.HoldingCosts[0]
202 : 0.0;
203
204 var planningHorizonStart = 0;
205
206 for (var end = 0; end < horizon; end++)
207 {
208 if ((end & CancellationCheckMask) == 0)
209 {
210 cancellationToken.ThrowIfCancellationRequested();
211 }
212
213 if (demands[end] == 0.0)
214 {
215 // No setup is needed to extend a solved prefix through a
216 // zero-demand period. Do not advance the planning-horizon
217 // bound: this transition does not certify a new setup.
218 value[end + 1] = value[end];
219 predecessor[end + 1] = end;
220 continue;
221 }
222
223 var best = double.PositiveInfinity;
224 var bestStart = -1;
225
226 // Demand covered by the current candidate setup period through
227 // 'end'. Before the first candidate it is empty.
228 var segmentDemand = 0.0;
229
230 // Holding cost of the current candidate interval.
231 var intervalHoldingCost = 0.0;
232
233 for (var start = end;
234 start >= planningHorizonStart;
235 start--)
236 {
237 if (((end - start) & CancellationCheckMask) == 0)
238 {
239 cancellationToken.ThrowIfCancellationRequested();
240 }
241
242 if (start < end)
243 {
244 // Moving the candidate setup one period earlier makes
245 // every unit already in the segment wait one additional
246 // period. This is the incremental part-period cost.
247 var incrementalHoldingCost = MultiplyFinite(
248 holdingCost,
249 segmentDemand,
250 "economic part-period incremental holding cost");
251
252 // Economic-Part-Period dominance:
253 // if one more period of carrying the already covered
254 // future demand costs more than opening an extra setup,
255 // this candidate is dominated by splitting at start+1.
256 // Earlier starts only increase the carried future
257 // demand, so the entire remaining scan can stop.
258 if (incrementalHoldingCost > setupCost)
259 {
260 break;
261 }
262
263 intervalHoldingCost = AddFinite(
264 intervalHoldingCost,
265 incrementalHoldingCost,
266 "candidate interval holding cost");
267 }
268
269 segmentDemand = AddFinite(
270 segmentDemand,
271 demands[start],
272 "candidate interval demand");
273
274 var variableProductionCost = MultiplyFinite(
275 productionCost,
276 segmentDemand,
277 "candidate production cost");
278
279 var candidate = AddFinite(
280 value[start],
281 setupCost,
282 "forward dynamic-programming candidate");
283
284 candidate = AddFinite(
285 candidate,
286 variableProductionCost,
287 "forward dynamic-programming candidate");
288
289 candidate = AddFinite(
290 candidate,
291 intervalHoldingCost,
292 "forward dynamic-programming candidate");
293
294 // Scanning from latest to earliest means strict comparison
295 // retains the latest optimal predecessor on a tie. This
296 // gives the strongest valid planning-horizon bound.
297 if (candidate < best)
298 {
299 best = candidate;
300 bestStart = start;
301 }
302 }
303
304 if (!double.IsFinite(best) ||
305 bestStart < planningHorizonStart ||
306 bestStart > end)
307 {
308 throw new ArithmeticException(
309 $"No finite Heady-Zhu value was obtained for period {end}.");
310 }
311
312 value[end + 1] = best;
313 predecessor[end + 1] = bestStart;
314
315 // Wagner-Whitin Planning Horizon Theorem.
316 planningHorizonStart = bestStart;
317 }
318
319 cancellationToken.ThrowIfCancellationRequested();
320
322 problem,
323 predecessor,
324 Name,
325 cancellationToken);
326 }
327 finally
328 {
329 ArrayPool<double>.Shared.Return(
330 valueBuffer,
331 clearArray: false);
332
333 ArrayPool<int>.Shared.Return(
334 predecessorBuffer,
335 clearArray: false);
336 }
337 }
338
339 private static double AddFinite(
340 double left,
341 double right,
342 string operation)
343 {
344 var value = left + right;
345
346 if (!double.IsFinite(value))
347 {
348 throw new ArithmeticException(
349 $"Numerical overflow while computing {operation}.");
350 }
351
352 return value;
353 }
354
355 private static double MultiplyFinite(
356 double left,
357 double right,
358 string operation)
359 {
360 var value = left * right;
361
362 if (!double.IsFinite(value))
363 {
364 throw new ArithmeticException(
365 $"Numerical overflow while computing {operation}.");
366 }
367
368 return value;
369 }
370}
Implements the Heady-Zhu family of improved Wagner-Whitin procedures using the Planning Horizon Theor...
static bool IsApplicable(UlsProblem problem)
Determines whether the fixed-cost assumptions required by this implementation hold.
string Name
Gets the stable human-readable name of the solver.
static double GetEconomicPartPeriodThreshold(UlsProblem problem)
Returns the Economic-Part-Period threshold A / h.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
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.