ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
LyuLeeParallelSolver.cs
Go to the documentation of this file.
1using System.Buffers;
2using System.Threading.Tasks;
7
9
10/// <summary>
11/// Parallel exact dynamic lot-sizing solver inspired by Lyu and Lee's
12/// lower-triangular parallel Wagner-Whitin computation.
13/// </summary>
14/// <remarks>
15/// <para>
16/// For every horizon endpoint the predecessor cells of the lower-triangular
17/// dynamic-programming matrix are independent once earlier DP values have been
18/// finalized. This implementation partitions those predecessor cells across
19/// worker threads, performs a local minimum reduction, then lets the calling
20/// thread select the global minimum.
21/// </para>
22/// <para>
23/// Arc costs are evaluated in O(1) using cumulative demand and cumulative
24/// demand-weighted holding-cost prefixes. The full triangular matrix is not
25/// materialized, which preserves O(T) auxiliary memory.
26/// </para>
27/// <para>
28/// Sequential work is O(T^2). With <c>p</c> effective workers, the candidate
29/// evaluation work is ideally O(T^2/p), subject to synchronization, scheduling
30/// and finite-horizon overheads.
31/// </para>
32/// <para>
33/// Reference:
34/// J.-J. Lyu and M.-C. Lee,
35/// "A parallel algorithm for the dynamic lot-sizing problem",
36/// Computers &amp; Industrial Engineering 41(2), 127-134, 2001.
37/// DOI: 10.1016/S0360-8352(01)00047-X.
38/// </para>
39/// <para>
40/// The original paper describes a distributed parallel implementation and
41/// reports near-linear empirical speedup. The asymptotic statement used by
42/// this library is deliberately implementation-specific: this C# reconstruction
43/// performs O(T^2) total predecessor work and has ideal O(T^2/p) candidate
44/// evaluation span with p effective workers, plus scheduling/reduction overhead.
45/// It is a modern shared-memory realization, not a transliteration of the
46/// authors' original PVM implementation.
47/// </para>
48/// </remarks>
49public sealed class LyuLeeParallelSolver : IUlsSolver
50{
51 /// <summary>
52 /// Initializes a solver using all available processors and a parallel
53 /// threshold of 128 predecessor candidates.
54 /// </summary>
56 : this(-1, 128)
57 {
58 }
59
60 /// <summary>
61 /// Initializes a solver with explicit parallelism controls.
62 /// </summary>
63 /// <param name="maxDegreeOfParallelism">
64 /// Maximum number of workers, or -1 to use the runtime default processor
65 /// count.
66 /// </param>
67 /// <param name="parallelThreshold">
68 /// Minimum predecessor count before parallel evaluation is attempted.
69 /// </param>
71 int maxDegreeOfParallelism,
72 int parallelThreshold = 128)
73 {
74 if (maxDegreeOfParallelism == 0 ||
75 maxDegreeOfParallelism < -1)
76 {
77 throw new ArgumentOutOfRangeException(
78 nameof(maxDegreeOfParallelism));
79 }
80
81 if (parallelThreshold < 1)
82 {
83 throw new ArgumentOutOfRangeException(
84 nameof(parallelThreshold));
85 }
86
87 MaxDegreeOfParallelism = maxDegreeOfParallelism;
88 ParallelThreshold = parallelThreshold;
89 }
90
91 /// <inheritdoc />
92 public string Name => "Lyu-Lee parallel dynamic lot-sizing";
93
94 /// <inheritdoc />
96
97 /// <summary>
98 /// Gets the configured maximum degree of parallelism.
99 /// </summary>
100 public int MaxDegreeOfParallelism { get; }
101
102 /// <summary>
103 /// Gets the minimum predecessor count for parallel evaluation.
104 /// </summary>
105 public int ParallelThreshold { get; }
106
107 /// <inheritdoc />
109 UlsProblem problem,
110 CancellationToken cancellationToken = default)
111 {
112 ArgumentNullException.ThrowIfNull(problem);
113 cancellationToken.ThrowIfCancellationRequested();
114
115 var horizon = problem.Horizon;
116
117 var effectiveWorkerLimit =
119 ? Math.Max(1, Environment.ProcessorCount)
121
122 var valueBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
123 var predecessorBuffer = ArrayPool<int>.Shared.Rent(horizon + 1);
124 var demandPrefixBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
125 var holdingPrefixBuffer = ArrayPool<double>.Shared.Rent(horizon + 1);
126 var weightedDemandPrefixBuffer =
127 ArrayPool<double>.Shared.Rent(horizon + 1);
128
129 var workerBestValueBuffer =
130 ArrayPool<double>.Shared.Rent(effectiveWorkerLimit);
131 var workerBestPredecessorBuffer =
132 ArrayPool<int>.Shared.Rent(effectiveWorkerLimit);
133
134 try
135 {
136 var value = valueBuffer;
137 var predecessor = predecessorBuffer;
138 var demandPrefix = demandPrefixBuffer;
139 var holdingPrefix = holdingPrefixBuffer;
140 var weightedDemandPrefix = weightedDemandPrefixBuffer;
141
142 Array.Fill(
143 value,
144 double.PositiveInfinity,
145 0,
146 horizon + 1);
147
148 Array.Fill(
149 predecessor,
150 -1,
151 0,
152 horizon + 1);
153
154 demandPrefix[0] = 0.0;
155 holdingPrefix[0] = 0.0;
156 weightedDemandPrefix[0] = 0.0;
157 value[0] = 0.0;
158
159 var demands = problem.Demands;
160 var holdingCosts = problem.HoldingCosts;
161
162 for (var period = 0; period < horizon; period++)
163 {
164 demandPrefix[period + 1] = AddFinite(
165 demandPrefix[period],
166 demands[period],
167 "cumulative demand");
168
169 weightedDemandPrefix[period + 1] = AddFinite(
170 weightedDemandPrefix[period],
171 MultiplyFinite(
172 demands[period],
173 holdingPrefix[period],
174 "demand-weighted holding prefix"),
175 "demand-weighted holding prefix");
176
177 holdingPrefix[period + 1] = AddFinite(
178 holdingPrefix[period],
179 holdingCosts[period],
180 "cumulative holding cost");
181 }
182
183 for (var end = 1; end <= horizon; end++)
184 {
185 cancellationToken.ThrowIfCancellationRequested();
186
187 double best;
188 int bestStart;
189
190 if (end < ParallelThreshold ||
191 effectiveWorkerLimit <= 1)
192 {
193 (best, bestStart) = FindBestSerial(
194 end,
195 problem,
196 value,
197 demandPrefix,
198 holdingPrefix,
199 weightedDemandPrefix,
200 cancellationToken);
201 }
202 else
203 {
204 var workerCount =
205 Math.Min(effectiveWorkerLimit, end);
206
207 var options = new ParallelOptions
208 {
209 CancellationToken = cancellationToken,
210 MaxDegreeOfParallelism = workerCount
211 };
212
213 global::System.Threading.Tasks.Parallel.For(
214 0,
215 workerCount,
216 options,
217 worker =>
218 {
219 var first =
220 (end * worker) / workerCount;
221
222 var lastExclusive =
223 (end * (worker + 1)) / workerCount;
224
225 var localBest = double.PositiveInfinity;
226 var localBestStart = -1;
227
228 for (var start = first;
229 start < lastExclusive;
230 start++)
231 {
232 if ((start & 255) == 0)
233 {
234 cancellationToken.ThrowIfCancellationRequested();
235 }
236
237 var candidate = EvaluateCandidate(
238 start,
239 end,
240 problem,
241 value,
242 demandPrefix,
243 holdingPrefix,
244 weightedDemandPrefix);
245
246 if (candidate < localBest ||
247 (candidate == localBest &&
248 (localBestStart < 0 ||
249 start < localBestStart)))
250 {
251 localBest = candidate;
252 localBestStart = start;
253 }
254 }
255
256 workerBestValueBuffer[worker] = localBest;
257 workerBestPredecessorBuffer[worker] =
258 localBestStart;
259 });
260
261 best = double.PositiveInfinity;
262 bestStart = -1;
263
264 for (var worker = 0;
265 worker < workerCount;
266 worker++)
267 {
268 var candidate =
269 workerBestValueBuffer[worker];
270
271 var candidateStart =
272 workerBestPredecessorBuffer[worker];
273
274 if (candidate < best ||
275 (candidate == best &&
276 candidateStart >= 0 &&
277 (bestStart < 0 ||
278 candidateStart < bestStart)))
279 {
280 best = candidate;
281 bestStart = candidateStart;
282 }
283 }
284 }
285
286 if (!double.IsFinite(best) ||
287 bestStart < 0)
288 {
289 throw new ArithmeticException(
290 $"No finite Lyu-Lee value was obtained for state {end}.");
291 }
292
293 value[end] = best;
294 predecessor[end] = bestStart;
295 }
296
298 problem,
299 predecessor.AsSpan(0, horizon + 1),
300 Name,
301 cancellationToken);
302 }
303 finally
304 {
305 ArrayPool<double>.Shared.Return(valueBuffer, clearArray: false);
306 ArrayPool<int>.Shared.Return(predecessorBuffer, clearArray: false);
307 ArrayPool<double>.Shared.Return(
308 demandPrefixBuffer,
309 clearArray: false);
310 ArrayPool<double>.Shared.Return(
311 holdingPrefixBuffer,
312 clearArray: false);
313 ArrayPool<double>.Shared.Return(
314 weightedDemandPrefixBuffer,
315 clearArray: false);
316 ArrayPool<double>.Shared.Return(
317 workerBestValueBuffer,
318 clearArray: false);
319 ArrayPool<int>.Shared.Return(
320 workerBestPredecessorBuffer,
321 clearArray: false);
322 }
323 }
324
325 private static (double Best, int BestStart) FindBestSerial(
326 int end,
327 UlsProblem problem,
328 double[] value,
329 double[] demandPrefix,
330 double[] holdingPrefix,
331 double[] weightedDemandPrefix,
332 CancellationToken cancellationToken)
333 {
334 var best = double.PositiveInfinity;
335 var bestStart = -1;
336
337 for (var start = 0; start < end; start++)
338 {
339 if ((start & 255) == 0)
340 {
341 cancellationToken.ThrowIfCancellationRequested();
342 }
343
344 var candidate = EvaluateCandidate(
345 start,
346 end,
347 problem,
348 value,
349 demandPrefix,
350 holdingPrefix,
351 weightedDemandPrefix);
352
353 if (candidate < best ||
354 (candidate == best &&
355 (bestStart < 0 ||
356 start < bestStart)))
357 {
358 best = candidate;
359 bestStart = start;
360 }
361 }
362
363 return (best, bestStart);
364 }
365
366 private static double EvaluateCandidate(
367 int start,
368 int end,
369 UlsProblem problem,
370 double[] value,
371 double[] demandPrefix,
372 double[] holdingPrefix,
373 double[] weightedDemandPrefix)
374 {
375 var segmentDemand =
376 demandPrefix[end] -
377 demandPrefix[start];
378
379 if (segmentDemand == 0.0)
380 {
381 return value[start];
382 }
383
384 var transformedUnitCost =
385 problem.UnitProductionCosts[start] -
386 holdingPrefix[start];
387
388 var variableCost = AddFinite(
389 MultiplyFinite(
390 transformedUnitCost,
391 segmentDemand,
392 "regeneration-interval variable cost"),
393 weightedDemandPrefix[end] -
394 weightedDemandPrefix[start],
395 "regeneration-interval variable cost");
396
397 var arcCost = AddFinite(
398 problem.SetupCosts[start],
399 variableCost,
400 "regeneration-interval cost");
401
402 return AddFinite(
403 value[start],
404 arcCost,
405 "dynamic-programming candidate");
406 }
407
408 private static double AddFinite(
409 double left,
410 double right,
411 string operation)
412 {
413 var result = left + right;
414
415 if (!double.IsFinite(result))
416 {
417 throw new ArithmeticException(
418 $"Numerical overflow while computing {operation}.");
419 }
420
421 return result;
422 }
423
424 private static double MultiplyFinite(
425 double left,
426 double right,
427 string operation)
428 {
429 var result = left * right;
430
431 if (!double.IsFinite(result))
432 {
433 throw new ArithmeticException(
434 $"Numerical overflow while computing {operation}.");
435 }
436
437 return result;
438 }
439}
int MaxDegreeOfParallelism
Gets the configured maximum degree of parallelism.
LyuLeeParallelSolver(int maxDegreeOfParallelism, int parallelThreshold=128)
Initializes a solver with explicit parallelism controls.
string Name
Gets the stable human-readable name of the solver.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing problem.The solve result.
UlsSolverKind Kind
Gets the broad family of the solver.
int ParallelThreshold
Gets the minimum predecessor count for parallel evaluation.
LyuLeeParallelSolver()
Initializes a solver using all available processors and a parallel threshold of 128 predecessor candi...
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.