ULSAlgorithms 1.1.0-g3e5595996d
High-performance exact and heuristic algorithms for uncapacitated lot sizing
Loading...
Searching...
No Matches
McLarenOrderMomentSolver.cs
Go to the documentation of this file.
1using System.Buffers;
6
8
9/// <summary>
10/// Implements McLaren's Order Moment (MOM) lot-sizing heuristic.
11/// </summary>
12/// <remarks>
13/// <para>
14/// MOM combines an EOQ-derived time-between-orders estimate with a
15/// part-period target. A lot is extended until accumulated part-periods reach
16/// the Order Moment Target (OMT); the triggering demand is then subjected to
17/// the published marginal holding/setup test before the lot is closed.
18/// </para>
19/// <para>
20/// Original source: B. J. McLaren,
21/// "A Study of Multiple Level Lot Sizing Procedures for Material Requirements
22/// Planning Systems", Ph.D. dissertation, Purdue University, 1977.
23/// </para>
24/// <para>
25/// Reconstructed operational rule and formula source:
26/// L. Baciarello, M. D'Avino, R. Onori and M. M. Schiraldi,
27/// "Lot Sizing Heuristics Performance", International Journal of Engineering
28/// Business Management 5, 2013, DOI 10.5772/56004.
29/// </para>
30/// </remarks>
32{
33 public string Name =>
34 "McLaren Order Moment";
35
37 UlsSolverKind.Heuristic;
38
39 public static bool IsApplicable(
40 UlsProblem problem) =>
42
43 /// <summary>
44 /// Computes the Order Moment Target for the supplied stationary-cost
45 /// problem.
46 /// </summary>
47 public static double GetOrderMomentTarget(
48 UlsProblem problem)
49 {
50 ArgumentNullException.ThrowIfNull(problem);
51
53 problem,
54 "McLaren Order Moment");
55
56 if (problem.TotalDemand == 0.0)
57 {
58 return 0.0;
59 }
60
61 int horizon = problem.Horizon;
62 double holdingCost =
63 horizon > 1
64 ? problem.HoldingCosts[0]
65 : 0.0;
66
67 if (holdingCost == 0.0)
68 {
69 return double.PositiveInfinity;
70 }
71
72 double averageDemand =
73 problem.TotalDemand /
74 horizon;
75
76 double denominator =
77 holdingCost *
78 averageDemand;
79
80 if (denominator == 0.0)
81 {
82 return double.PositiveInfinity;
83 }
84
85 double ratio =
86 2.0 *
87 problem.SetupCosts[0] /
88 denominator;
89
90 if (double.IsPositiveInfinity(ratio))
91 {
92 return double.PositiveInfinity;
93 }
94
95 if (!double.IsFinite(ratio) ||
96 ratio < 0.0)
97 {
98 throw new ArithmeticException(
99 "Non-finite EOQ-derived ratio while computing the Order Moment Target.");
100 }
101
102 double timeBetweenOrders =
103 Math.Sqrt(ratio);
104
105 double truncatedTimeBetweenOrders =
106 Math.Floor(timeBetweenOrders);
107
108 double integerMoment =
109 truncatedTimeBetweenOrders *
110 (truncatedTimeBetweenOrders - 1.0) /
111 2.0;
112
113 double fractionalMoment =
114 (timeBetweenOrders -
115 truncatedTimeBetweenOrders) *
116 truncatedTimeBetweenOrders;
117
118 double target =
119 averageDemand *
120 (integerMoment +
121 fractionalMoment);
122
123 if (!double.IsFinite(target))
124 {
125 return double.PositiveInfinity;
126 }
127
128 return target;
129 }
130
132 UlsProblem problem,
133 CancellationToken cancellationToken = default)
134 {
135 ArgumentNullException.ThrowIfNull(problem);
136 cancellationToken.ThrowIfCancellationRequested();
137
139 problem,
140 Name);
141
142 int horizon = problem.Horizon;
143 int[] buffer =
144 ArrayPool<int>.Shared.Rent(horizon);
145
146 try
147 {
148 Span<int> cycleEnds =
149 buffer.AsSpan(0, horizon);
150
151 cycleEnds.Fill(-1);
152
153 ReadOnlySpan<double> demands =
154 problem.Demands;
155
156 int start =
158 demands,
159 0);
160
161 if (start >= horizon)
162 {
164 problem,
165 cycleEnds,
166 Name,
167 cancellationToken);
168 }
169
170 double holdingCost =
171 horizon > 1
172 ? problem.HoldingCosts[0]
173 : 0.0;
174
175 if (holdingCost == 0.0)
176 {
177 cycleEnds[start] =
178 horizon - 1;
179
181 problem,
182 cycleEnds,
183 Name,
184 cancellationToken);
185 }
186
187 double setupCost =
188 problem.SetupCosts[0];
189
190 double target =
191 GetOrderMomentTarget(problem);
192
193 while (start < horizon)
194 {
195 cancellationToken.ThrowIfCancellationRequested();
196
197 double partPeriods = 0.0;
198 int selectedEnd = start;
199 bool closed = false;
200
201 for (int candidate = start + 1;
202 candidate < horizon;
203 candidate++)
204 {
205 if ((candidate & 255) == 0)
206 {
207 cancellationToken.ThrowIfCancellationRequested();
208 }
209
210 double demand =
211 demands[candidate];
212
213 if (demand == 0.0)
214 {
215 selectedEnd = candidate;
216 continue;
217 }
218
219 double candidatePartPeriods =
220 partPeriods +
221 (candidate - start) *
222 demand;
223
224 if (!double.IsFinite(candidatePartPeriods))
225 {
226 throw new ArithmeticException(
227 "Numerical overflow while accumulating MOM part-periods.");
228 }
229
230 if (StrictlyBelow(
231 candidatePartPeriods,
232 target))
233 {
234 partPeriods =
235 candidatePartPeriods;
236
237 selectedEnd =
238 candidate;
239
240 continue;
241 }
242
243 double marginalHolding =
244 holdingCost *
245 (candidate - start) *
246 demand;
247
248 if (!double.IsFinite(marginalHolding))
249 {
250 throw new ArithmeticException(
251 "Numerical overflow while evaluating the MOM marginal test.");
252 }
253
254 if (LessOrEqual(
255 marginalHolding,
256 setupCost))
257 {
258 selectedEnd =
259 candidate;
260 }
261 else
262 {
263 selectedEnd =
264 candidate - 1;
265 }
266
267 cycleEnds[start] =
268 selectedEnd;
269
270 start =
272 demands,
273 selectedEnd + 1);
274
275 closed = true;
276 break;
277 }
278
279 if (!closed)
280 {
281 cycleEnds[start] =
282 horizon - 1;
283
284 break;
285 }
286 }
287
289 problem,
290 cycleEnds,
291 Name,
292 cancellationToken);
293 }
294 finally
295 {
296 ArrayPool<int>.Shared.Return(
297 buffer,
298 clearArray: false);
299 }
300 }
301
302 private static bool StrictlyBelow(
303 double value,
304 double target)
305 {
306 if (double.IsPositiveInfinity(target))
307 {
308 return true;
309 }
310
311 double tolerance =
312 1.0e-12 *
313 Math.Max(
314 1.0,
315 Math.Max(
316 Math.Abs(value),
317 Math.Abs(target)));
318
319 return value <
320 target - tolerance;
321 }
322
323 private static bool LessOrEqual(
324 double left,
325 double right)
326 {
327 double tolerance =
328 1.0e-12 *
329 Math.Max(
330 1.0,
331 Math.Max(
332 Math.Abs(left),
333 Math.Abs(right)));
334
335 return left <=
336 right + tolerance;
337 }
338}
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 McLaren's Order Moment (MOM) lot-sizing heuristic.
UlsSolveResult Solve(UlsProblem problem, CancellationToken cancellationToken=default)
Solves an uncapacitated lot-sizing problem.
UlsSolverKind Kind
Gets the broad family of the solver.
string Name
Gets the stable human-readable name of the solver.
static double GetOrderMomentTarget(UlsProblem problem)
Computes the Order Moment Target for the supplied stationary-cost problem.
Represents a validated classical uncapacitated lot-sizing problem.
Definition UlsProblem.cs:23
double TotalDemand
Gets the total demand over the complete planning horizon.
Definition UlsProblem.cs:87
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.