52 LotSizingInstance instance,
53 ProductionRouting routing,
57 ArgumentNullException.ThrowIfNull(instance);
58 ArgumentNullException.ThrowIfNull(routing);
59 ArgumentNullException.ThrowIfNull(options);
63 if (period < 1 || period > instance.PlanningHorizon)
65 throw new ArgumentOutOfRangeException(
68 "The period must belong to the planning horizon.");
75 "Configured fixed ProductionSetupBigM.",
79 double grossRequirement;
84 CalculateFullHorizonGrossRequirement(
88 catch (InvalidOperationException)
90 return CreateFallback(
92 "Automatic gross-requirement propagation could " +
93 "not establish a finite bound.");
96 double safetyStockRequirement =
97 CalculateSafetyStockRequirement(
101 double structuralBound =
104 safetyStockRequirement);
106 double minimumLotSize =
107 routing.MinimumLotSize?[period] ?? 0.0;
109 if (!
double.IsFinite(minimumLotSize) ||
110 minimumLotSize < 0.0)
112 return CreateFallback(
114 "The routing minimum lot size is not a valid " +
115 "finite non-negative number.");
123 if (routing.LotSizeMultiple is not
null)
126 routing.LotSizeMultiple[period];
128 if (!
double.IsFinite(multiple) || multiple <= 0.0)
130 return CreateFallback(
132 "The routing lot-size multiple is not a " +
133 "valid finite positive number.");
139 Math.Ceiling(bound / multiple) * multiple;
143 if (!
double.IsFinite(bound) || bound < 0.0)
145 return CreateFallback(
147 "The automatically computed production bound " +
155 "No external, BOM-induced, safety-stock, or " +
156 "minimum-lot requirement exists for this item.",
162 $
"Automatic structural bound: grossRequirement=" +
163 $
"{grossRequirement:G17}; safetyStock=" +
164 $
"{safetyStockRequirement:G17}; minimumLotSize=" +
165 $
"{minimumLotSize:G17}.",
169 private static double CalculateFullHorizonGrossRequirement(
170 LotSizingInstance instance,
174 new Dictionary<int, double>();
179 return CalculateGrossRequirementRecursive(
186 private static double CalculateGrossRequirementRecursive(
187 LotSizingInstance instance,
189 IDictionary<int, double> memo,
192 if (memo.TryGetValue(itemId, out
double cached))
197 if (!visiting.Add(itemId))
199 throw new InvalidOperationException(
200 "The bill of materials contains a cycle.");
208 in instance.SupplyChain.Demands.Where(
209 demand => demand.ItemId == itemId))
212 period <= instance.PlanningHorizon;
218 demand.GetQuantity(period));
223 ComponentRequirement requirement
224 in instance.SupplyChain.ComponentRequirements.Where(
226 requirement.ComponentItemId == itemId))
228 double parentRequirement =
229 CalculateGrossRequirementRecursive(
231 requirement.ParentItemId,
235 double inducedRequirement =
236 parentRequirement * requirement.Quantity;
238 if (!
double.IsFinite(inducedRequirement) ||
239 inducedRequirement < 0.0)
241 throw new InvalidOperationException(
242 "The propagated BOM requirement is not finite.");
251 visiting.Remove(itemId);
252 memo[itemId] = result;
257 private static double CalculateSafetyStockRequirement(
258 LotSizingInstance instance,
266 in instance.SupplyChain.Inventories.Where(
267 inventory => inventory.ItemId == itemId))
269 if (inventory.SafetyStock is
null)
278 period <= instance.PlanningHorizon;
284 inventory.SafetyStock[period]);
296 private static ProductionSetupBigMEstimate CreateFallback(
297 StandardLotSizingFormulationOptions options,
300 return new ProductionSetupBigMEstimate(
301 options.ProductionSetupBigM,
302 reason +
" Configured ProductionSetupBigM fallback " +
303 $
"{options.ProductionSetupBigM:G17} was used.",
307 private static double checkedFiniteSum(
311 double result = left + right;
313 if (!
double.IsFinite(result) || result < 0.0)
315 throw new InvalidOperationException(
316 "A structural Big-M calculation overflowed or " +
317 "produced a negative value.");