17 LotSizingInstance instance,
20 ArgumentNullException.ThrowIfNull(instance);
21 ArgumentNullException.ThrowIfNull(source);
27 NormalizeToMinimization(
30 AddPeriodicOperatingExpenditureCoverage(
35 CashFlowPolicy? cashPolicy =
36 instance.SupplyChain.CashFlowPolicy;
38 if (cashPolicy is
null)
43 EconomicCriterion = economic,
44 FinancialCriterion =
null,
49 if (!cashPolicy.HasConsistentPlanningHorizon ||
50 cashPolicy.PlanningHorizon != instance.PlanningHorizon)
52 throw new InvalidOperationException(
53 "CashFlowPolicy must use the instance planning horizon.");
56 if (Math.Abs(economic.
Constant) > 1.0e-12)
58 throw new InvalidOperationException(
59 "Cash-flow execution requires a zero constant in the economic objective because an unperiodized constant has no unambiguous cash-flow timing.");
62 int financialHorizon =
63 instance.PlanningHorizon +
65 cashPolicy.ReceiptDelayPeriods,
66 cashPolicy.DisbursementDelayPeriods);
69 AddCashBalanceVariables(
74 AddCashBalanceConstraints(
78 instance.PlanningHorizon,
86 cashVariables[financialHorizon].Id,
94 EconomicCriterion = economic,
95 FinancialCriterion = financial,
96 FinancialHorizon = financialHorizon
103 ArgumentNullException.ThrowIfNull(objective);
114 throw new InvalidOperationException(
115 "Economic objective sense must be Minimize or Maximize.");
121 private static Dictionary<int, MathematicalVariable>
122 AddCashBalanceVariables(
124 CashFlowPolicy policy,
125 int financialHorizon)
128 model.Variables.Count == 0
130 : model.
Variables.Max(variable => variable.Id) + 1;
133 new Dictionary<int, MathematicalVariable>();
136 period <= financialHorizon;
140 policy.EnforceMinimumCashBalance &&
141 period <= policy.PlanningHorizon
142 ? policy.MinimumCashBalance[period]
143 :
double.NegativeInfinity;
149 Name = $
"cashBalance_t{period}",
159 LowerBound = lowerBound,
160 UpperBound =
double.PositiveInfinity,
162 "Cash balance after all receipts/disbursements due in the period."
166 result.Add(period, variable);
172 private static void AddCashBalanceConstraints(
173 MathematicalModel model,
174 LinearExpression economic,
175 CashFlowPolicy policy,
177 int financialHorizon,
178 IReadOnlyDictionary<int, MathematicalVariable> cashVariables)
180 int nextConstraintId =
181 model.Constraints.Count == 0
183 : model.
Constraints.Max(constraint => constraint.Id) + 1;
186 new Dictionary<int, List<LinearTerm>>();
189 period <= financialHorizon;
192 dueTerms[period] =
new List<LinearTerm>();
195 foreach (LinearTerm term
in economic.
Terms)
197 MathematicalVariable variable =
199 ??
throw new InvalidOperationException(
200 $
"Unknown economic variable id {term.VariableId}.");
202 MathematicalDomainKey key =
203 MathematicalDomainKey.Parse(
207 MathematicalDomainKeySegment.Period,
208 out
int economicPeriod))
211 MathematicalDecisionCategory.InitialInventory)
217 throw new InvalidOperationException(
218 $
"Cash-flow timing is ambiguous for economic variable '{variable.Name}' because its domain key has no period.");
223 term.Coefficient < 0.0
224 ? policy.ReceiptDelayPeriods
225 : policy.DisbursementDelayPeriods;
228 economicPeriod + delay;
231 duePeriod > financialHorizon)
233 throw new InvalidOperationException(
234 $
"Cash-flow due period {duePeriod} is outside the financial horizon.");
237 dueTerms[duePeriod].Add(
244 period <= financialHorizon;
248 new LinearExpression();
251 cashVariables[period].Id,
257 cashVariables[period - 1].Id,
261 foreach (LinearTerm term
in dueTerms[period])
263 expression.AddTerm(term);
266 double fixedNetCashFlow =
267 period <= planningHorizon
268 ? policy.FixedNetCashFlow[period]
274 ? policy.InitialCashBalance
278 new LinearConstraint(
280 $
"cashBalance_t{period}",
286 $
"cashBalance|period={period}",
288 "Cash-flow balance distinct from OPEX budget."
293 private static void AddPeriodicOperatingExpenditureCoverage(
294 LotSizingInstance instance,
295 MathematicalModel model,
296 LinearExpression economic)
300 .PeriodicOperatingExpenditureBudget;
307 int nextConstraintId =
308 model.Constraints.Count == 0
310 : model.
Constraints.Max(constraint => constraint.Id) + 1;
313 period <= instance.PlanningHorizon;
317 new LinearExpression();
319 foreach (LinearTerm term
in economic.
Terms)
326 MathematicalVariable variable =
328 ??
throw new InvalidOperationException(
329 $
"Unknown economic variable id {term.VariableId}.");
331 MathematicalDomainKey key =
332 MathematicalDomainKey.
Parse(
336 MathematicalDomainKeySegment.Period,
340 MathematicalDecisionCategory.InitialInventory)
345 throw new InvalidOperationException(
346 $
"Periodic OPEX coverage cannot classify positive economic term '{variable.Name}' because it has no period.");
349 if (termPeriod == period)
351 expression.AddTerm(term);
356 new LinearConstraint(
358 $
"alpha33PeriodicOpexBudget_t{period}",
366 $
"periodicOperatingExpenditureBudget|period={period}",
368 "Complete positive economic-cost envelope, including scheduling setup/start-up/changeover costs."
373 private static double ReadPeriodValue(
383 BindingFlags.Instance | BindingFlags.Public,
385 types:
new[] { typeof(int) },
388 if (getter is not
null)
393 new object[] { period });
395 if (value is
double number)
401 PropertyInfo? indexer =
403 BindingFlags.Instance |
408 ParameterInfo[] indexes =
409 property.GetIndexParameters();
411 return indexes.Length == 1 &&
412 indexes[0].ParameterType ==
414 property.PropertyType ==
418 if (indexer is not
null)
423 new object[] { period });
425 if (value is
double number)
431 throw new InvalidOperationException(
432 $
"Cannot read period {period} from {type.FullName}.");
Builds stable business-domain keys for mathematical variables and constraints.
MathematicalDomainKeyBuilder Add(string name, string value)
Adds a text segment.
static MathematicalDomainKey Parse(string domainKey)
Parses a canonical mathematical domain key.
bool TryGetInt32(string name, out int value)
Attempts to get an integer segment.
Provides the canonical mathematical domain-key segment names used by lot-sizing variables and constra...