ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
SaydamMcKnewFastWagnerWhitinSolver.cs
Go to the documentation of this file.
1using System.Buffers;
7
9
10/// <summary>
11/// High-throughput full Wagner-Whitin implementation in the spirit of
12/// Saydam and McKnew's fast microcomputer program.
13/// </summary>
14/// <remarks>
15/// <para>
16/// Saydam and McKnew (1987) describe a very fast implementation of the full
17/// original Wagner-Whitin algorithm. They explicitly contrast their approach
18/// with Evans' low-storage implementation: Evans is preferred when storage is
19/// scarce, whereas their implementation emphasizes execution speed.
20/// </para>
21/// <para>
22/// This modern C# reconstruction follows that design trade-off by materializing
23/// the triangular regeneration-cost table in a single contiguous pooled array.
24/// The DP phase then reads precomputed arc costs with no repeated regeneration
25/// arithmetic.
26/// </para>
27/// <para>
28/// Time complexity is O(T²); working memory is O(T²). The flattened triangular
29/// layout avoids per-row objects and improves locality relative to a jagged
30/// matrix.
31/// </para>
32/// <para>
33/// Reference:
34/// C. Saydam and M. McKnew,
35/// "A Fast Microcomputer Program for Ordering Using the Wagner-Whitin
36/// Algorithm",
37/// Production and Inventory Management Journal 28(4), 15-19, 1987.
38/// </para>
39/// <para>
40/// The article's author-uploaded full-text record states that the logic is a
41/// full implementation of the original algorithm and that Evans' approach is
42/// preferable when array storage is in severe shortage.
43/// </para>
44/// </remarks>
46{
47 public string Name =>
48 "Saydam-McKnew fast Wagner-Whitin";
49
51 UlsSolverKind.Exact;
52
54 UlsProblem problem,
55 CancellationToken cancellationToken = default)
56 {
57 ArgumentNullException.ThrowIfNull(problem);
58 cancellationToken.ThrowIfCancellationRequested();
59
60 var horizon = problem.Horizon;
61
62 var triangularLength =
63 checked(
64 horizon *
65 (horizon + 1) /
66 2);
67
68 var arcCostBuffer =
69 ArrayPool<double>.Shared.Rent(
70 triangularLength);
71
72 var valueBuffer =
73 ArrayPool<double>.Shared.Rent(
74 horizon + 1);
75
76 var predecessorBuffer =
77 ArrayPool<int>.Shared.Rent(
78 horizon + 1);
79
80 var demandPrefixBuffer =
81 ArrayPool<double>.Shared.Rent(
82 horizon + 1);
83
84 try
85 {
86 var arcCosts =
87 arcCostBuffer.AsSpan(
88 0,
89 triangularLength);
90
91 var value =
92 valueBuffer.AsSpan(
93 0,
94 horizon + 1);
95
96 var predecessor =
97 predecessorBuffer.AsSpan(
98 0,
99 horizon + 1);
100
101 var demandPrefix =
102 demandPrefixBuffer.AsSpan(
103 0,
104 horizon + 1);
105
106 value.Fill(
107 double.PositiveInfinity);
108
109 predecessor.Fill(-1);
110 demandPrefix.Clear();
111
112 value[0] = 0.0;
113
114 var demands =
115 problem.Demands;
116
117 for (var period = 0;
118 period < horizon;
119 period++)
120 {
121 demandPrefix[period + 1] =
122 demandPrefix[period] +
123 demands[period];
124
125 if (!double.IsFinite(
126 demandPrefix[period + 1]))
127 {
128 throw new ArithmeticException(
129 "Numerical overflow in cumulative demand.");
130 }
131 }
132
133 MaterializeTriangularCosts(
134 problem,
135 arcCosts,
136 cancellationToken);
137
138 for (var endExclusive = 1;
139 endExclusive <= horizon;
140 endExclusive++)
141 {
142 cancellationToken.ThrowIfCancellationRequested();
143
144 var end =
145 endExclusive - 1;
146
147 var best =
148 double.PositiveInfinity;
149
150 var bestStart = -1;
151
152 if (demands[end] == 0.0)
153 {
154 best =
155 value[endExclusive - 1];
156
157 bestStart =
158 endExclusive - 1;
159 }
160
161 for (var start = 0;
162 start <= end;
163 start++)
164 {
165 var intervalDemand =
166 demandPrefix[end + 1] -
167 demandPrefix[start];
168
169 if (intervalDemand == 0.0)
170 {
171 continue;
172 }
173
174 var candidate =
175 value[start] +
176 arcCosts[
177 TriangularIndex(
178 start,
179 end,
180 horizon)];
181
182 if (!double.IsFinite(candidate))
183 {
184 throw new ArithmeticException(
185 "Numerical overflow in Saydam-McKnew DP.");
186 }
187
188 if (candidate < best ||
189 (candidate == best &&
190 start > bestStart))
191 {
192 best = candidate;
193 bestStart = start;
194 }
195 }
196
197 if (!double.IsFinite(best) ||
198 bestStart < 0)
199 {
200 throw new ArithmeticException(
201 $"No finite Saydam-McKnew value for prefix {endExclusive}.");
202 }
203
204 value[endExclusive] = best;
205 predecessor[endExclusive] =
206 bestStart;
207 }
208
210 problem,
211 predecessor,
212 Name,
213 cancellationToken);
214 }
215 finally
216 {
217 ArrayPool<double>.Shared.Return(
218 arcCostBuffer,
219 clearArray: false);
220
221 ArrayPool<double>.Shared.Return(
222 valueBuffer,
223 clearArray: false);
224
225 ArrayPool<int>.Shared.Return(
226 predecessorBuffer,
227 clearArray: false);
228
229 ArrayPool<double>.Shared.Return(
230 demandPrefixBuffer,
231 clearArray: false);
232 }
233 }
234
235 private static void MaterializeTriangularCosts(
236 UlsProblem problem,
237 Span<double> arcCosts,
238 CancellationToken cancellationToken)
239 {
240 var horizon = problem.Horizon;
241 var demands = problem.Demands;
242 var setupCosts = problem.SetupCosts;
243 var productionCosts =
244 problem.UnitProductionCosts;
245 var holdingCosts =
246 problem.HoldingCosts;
247
248 for (var start = 0;
249 start < horizon;
250 start++)
251 {
252 cancellationToken.ThrowIfCancellationRequested();
253
254 var quantity = 0.0;
255 var intervalCost = 0.0;
256 var deliveredUnitCost =
257 productionCosts[start];
258
259 for (var end = start;
260 end < horizon;
261 end++)
262 {
263 quantity += demands[end];
264
265 if (!double.IsFinite(quantity))
266 {
267 throw new ArithmeticException(
268 "Numerical overflow in interval demand.");
269 }
270
271 intervalCost +=
272 demands[end] *
273 deliveredUnitCost;
274
275 if (!double.IsFinite(intervalCost))
276 {
277 throw new ArithmeticException(
278 "Numerical overflow in interval cost.");
279 }
280
281 var cost =
282 quantity == 0.0
283 ? 0.0
284 : setupCosts[start] +
285 intervalCost;
286
287 if (!double.IsFinite(cost))
288 {
289 throw new ArithmeticException(
290 "Numerical overflow in regeneration cost.");
291 }
292
293 arcCosts[
294 TriangularIndex(
295 start,
296 end,
297 horizon)] = cost;
298
299 if (end < horizon - 1)
300 {
301 deliveredUnitCost +=
302 holdingCosts[end];
303
304 if (!double.IsFinite(deliveredUnitCost))
305 {
306 throw new ArithmeticException(
307 "Numerical overflow in delivered unit cost.");
308 }
309 }
310 }
311 }
312 }
313
314 private static int TriangularIndex(
315 int start,
316 int end,
317 int horizon)
318 {
319 // Number of cells in rows 0..start-1:
320 // start*horizon - start*(start-1)/2.
321 var rowOffset =
322 checked(
323 start * horizon -
324 start * (start - 1) / 2);
325
326 return checked(
327 rowOffset +
328 end -
329 start);
330 }
331}
High-throughput full Wagner-Whitin implementation in the spirit of Saydam and McKnew's fast microcomp...
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing 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.