ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
KarniMaximumPartPeriodGainSolver.cs
Go to the documentation of this file.
1using System.Buffers;
6
8
9/// <summary>
10/// Implements Karni's Maximum Part-Period Gain (MPG) heuristic.
11/// </summary>
12/// <remarks>
13/// <para>
14/// MPG starts from Lot-for-Lot and is deliberately non-forward. At each
15/// iteration it selects the globally smallest current part-period cost of
16/// deleting a replenishment boundary. The adjacent lots are merged while the
17/// required part-periods do not exceed the Economic Part Period S/h.
18/// </para>
19/// <para>
20/// The implementation uses a lazy-invalidated priority queue plus an
21/// array-backed doubly-linked list of active replenishment lots. This preserves
22/// the global greedy merge rule while avoiding repeated full-horizon scans.
23/// </para>
24/// <para>
25/// Original source: R. Karni,
26/// "Maximum Part-Period Gain (MPG)—A Lot Sizing Procedure for Unconstrained
27/// and Constrained Requirements Planning Systems",
28/// Production and Inventory Management 22(2), 91-98, 1981.
29/// </para>
30/// <para>
31/// Detailed reconstruction and numerical example:
32/// L. Baciarello, M. D'Avino, R. Onori and M. M. Schiraldi,
33/// "Lot Sizing Heuristics Performance", 2013, DOI 10.5772/56004.
34/// </para>
35/// </remarks>
37{
38 private readonly record struct MergeCandidate(
39 int RightStart,
40 int Version);
41
42 public string Name =>
43 "Karni Maximum Part-Period Gain";
44
46 UlsSolverKind.Heuristic;
47
48 public static bool IsApplicable(
49 UlsProblem problem) =>
51
53 UlsProblem problem,
54 CancellationToken cancellationToken = default)
55 {
56 ArgumentNullException.ThrowIfNull(problem);
57 cancellationToken.ThrowIfCancellationRequested();
58
60 problem,
61 Name);
62
63 int horizon = problem.Horizon;
64
65 int[] cycleBuffer =
66 ArrayPool<int>.Shared.Rent(horizon);
67
68 try
69 {
70 Span<int> cycleEnds =
71 cycleBuffer.AsSpan(0, horizon);
72
73 cycleEnds.Fill(-1);
74
75 ReadOnlySpan<double> demands =
76 problem.Demands;
77
78 int first =
80 demands,
81 0);
82
83 if (first >= horizon)
84 {
86 problem,
87 cycleEnds,
88 Name,
89 cancellationToken);
90 }
91
92 double holdingCost =
93 horizon > 1
94 ? problem.HoldingCosts[0]
95 : 0.0;
96
97 if (holdingCost == 0.0)
98 {
99 cycleEnds[first] =
100 horizon - 1;
101
103 problem,
104 cycleEnds,
105 Name,
106 cancellationToken);
107 }
108
109 double economicPartPeriod =
110 problem.SetupCosts[0] /
111 holdingCost;
112
113 var previous =
114 new int[horizon];
115
116 var next =
117 new int[horizon];
118
119 var quantity =
120 new double[horizon];
121
122 var active =
123 new bool[horizon];
124
125 var version =
126 new int[horizon];
127
128 Array.Fill(previous, -1);
129 Array.Fill(next, -1);
130
131 int previousPositive = -1;
132
133 for (int period = first;
134 period < horizon;
135 period++)
136 {
137 if ((period & 255) == 0)
138 {
139 cancellationToken.ThrowIfCancellationRequested();
140 }
141
142 if (demands[period] == 0.0)
143 {
144 continue;
145 }
146
147 active[period] = true;
148 quantity[period] = demands[period];
149 previous[period] = previousPositive;
150
151 if (previousPositive >= 0)
152 {
153 next[previousPositive] =
154 period;
155 }
156
157 previousPositive =
158 period;
159 }
160
161 var queue =
162 new PriorityQueue<
163 MergeCandidate,
164 (double PartPeriods, int RightStart)>();
165
166 for (int right = next[first];
167 right >= 0;
168 right = next[right])
169 {
170 Enqueue(right);
171 }
172
173 while (queue.TryDequeue(
174 out MergeCandidate candidate,
175 out _))
176 {
177 cancellationToken.ThrowIfCancellationRequested();
178
179 int right =
180 candidate.RightStart;
181
182 if (!active[right] ||
183 candidate.Version != version[right] ||
184 previous[right] < 0)
185 {
186 continue;
187 }
188
189 int left =
190 previous[right];
191
192 double partPeriods =
193 (right - left) *
194 quantity[right];
195
196 if (!double.IsFinite(partPeriods))
197 {
198 throw new ArithmeticException(
199 "Numerical overflow while evaluating an MPG merge.");
200 }
201
202 if (StrictlyGreater(
203 partPeriods,
204 economicPartPeriod))
205 {
206 // Because the queue is ordered by the current part-period
207 // priority of every valid adjacent merge, no remaining
208 // valid merge can satisfy the EPP threshold.
209 break;
210 }
211
212 int rightNeighbor =
213 next[right];
214
215 quantity[left] =
216 AddFinite(
217 quantity[left],
218 quantity[right],
219 "MPG merged lot quantity");
220
221 version[left]++;
222
223 active[right] = false;
224 version[right]++;
225
226 next[left] =
227 rightNeighbor;
228
229 if (rightNeighbor >= 0)
230 {
231 previous[rightNeighbor] =
232 left;
233
234 version[rightNeighbor]++;
235 }
236
237 Enqueue(left);
238 Enqueue(rightNeighbor);
239 }
240
241 for (int start = first;
242 start >= 0;)
243 {
244 if (!active[start])
245 {
246 throw new InvalidOperationException(
247 "Invalid MPG active-lot chain.");
248 }
249
250 int following =
251 next[start];
252
253 cycleEnds[start] =
254 following >= 0
255 ? following - 1
256 : horizon - 1;
257
258 start =
259 following;
260 }
261
263 problem,
264 cycleEnds,
265 Name,
266 cancellationToken);
267
268 void Enqueue(
269 int right)
270 {
271 if (right < 0 ||
272 !active[right] ||
273 previous[right] < 0)
274 {
275 return;
276 }
277
278 int left =
279 previous[right];
280
281 double partPeriods =
282 (right - left) *
283 quantity[right];
284
285 if (!double.IsFinite(partPeriods))
286 {
287 throw new ArithmeticException(
288 "Numerical overflow while creating an MPG merge candidate.");
289 }
290
291 queue.Enqueue(
292 new MergeCandidate(
293 right,
294 version[right]),
295 (
296 partPeriods,
297 right
298 ));
299 }
300 }
301 finally
302 {
303 ArrayPool<int>.Shared.Return(
304 cycleBuffer,
305 clearArray: false);
306 }
307 }
308
309 private static bool StrictlyGreater(
310 double left,
311 double right)
312 {
313 double tolerance =
314 1.0e-12 *
315 Math.Max(
316 1.0,
317 Math.Max(
318 Math.Abs(left),
319 Math.Abs(right)));
320
321 return left >
322 right + tolerance;
323 }
324
325 private static double AddFinite(
326 double left,
327 double right,
328 string operation)
329 {
330 double value =
331 left + right;
332
333 if (!double.IsFinite(value))
334 {
335 throw new ArithmeticException(
336 $"Numerical overflow while computing {operation}.");
337 }
338
339 return value;
340 }
341}
Shared applicability checks for classical stationary-cost lot-sizing heuristics.
static void ThrowIfNotStationary(UlsProblem problem, string solverName)
static int FindNextPositiveDemand(ReadOnlySpan< double > demands, int start)
Builds and validates a zero-backlogging heuristic solution from a set of replenishment cycles.
static UlsSolveResult Build(UlsProblem problem, ReadOnlySpan< int > cycleEnds, string solverName, CancellationToken cancellationToken)
Implements Karni's Maximum Part-Period Gain (MPG) heuristic.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing problem.
string Name
Gets the stable human-readable name of the solver.
Represents a validated classical uncapacitated lot-sizing problem.
Definition UlsProblem.cs:23
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.