33 MathematicalModel model,
34 LotSizingSolution solution)
36 ArgumentNullException.ThrowIfNull(model);
37 ArgumentNullException.ThrowIfNull(solution);
45 MathematicalVariable variable
50 if (
string.IsNullOrWhiteSpace(
53 throw new InvalidOperationException(
54 "The mathematical variable has no domain key.");
57 MathematicalDomainKey domainKey =
58 MathematicalDomainKey.Parse(
70 catch (Exception exception)
90 private static double ResolveValue(
91 LotSizingSolution solution,
92 MathematicalDomainKey key)
94 return key.Category
switch
96 MathematicalDecisionCategory.Production =>
101 production.GetQuantity(
104 MathematicalDecisionCategory.Setup =>
109 production.IsSetupActivated(
114 MathematicalDecisionCategory.AuxiliaryLotSizeMultiplier =>
119 production.GetLotMultipleCount(
122 MathematicalDecisionCategory
123 .AuxiliarySmallBucketProductionActivation =>
129 production.GetQuantity(
130 GetPeriod(key))) > 1e-9
134 MathematicalDecisionCategory
135 .AuxiliarySchedulingSetupStart =>
140 IsSchedulingSetupStart(
146 MathematicalDecisionCategory.MicroPeriodProduction =>
147 ResolveMicroProduction(solution, key),
149 MathematicalDecisionCategory.MicroPeriodSetupState =>
150 ResolveMicroSetupState(solution, key),
152 MathematicalDecisionCategory.AuxiliaryMicroPeriodChangeover =>
153 ResolveMicroChangeover(solution, key),
155 MathematicalDecisionCategory.AuxiliaryMicroPeriodSetupStart =>
156 ResolveMicroSetupStart(solution, key),
158 MathematicalDecisionCategory.AuxiliaryMacroProductionActivation =>
159 ResolveProduction(solution, key, production => Math.Abs(production.GetQuantity(GetPeriod(key))) > 1e-9 ? 1.0 : 0.0),
161 MathematicalDecisionCategory.AuxiliaryProductionStartUp =>
162 ResolveProductionStartUp(solution, key),
164 MathematicalDecisionCategory.InitialInventory =>
165 ResolveInitialInventory(solution, key),
167 MathematicalDecisionCategory.CashBalance =>
168 ResolveCashBalance(solution, key),
170 MathematicalDecisionCategory.Inventory =>
178 MathematicalDecisionCategory.InventorySetup =>
183 inventory.IsSetupActivated(
188 MathematicalDecisionCategory.InventorySafetyStockViolation =>
193 inventory.GetSafetyStockViolation(
196 MathematicalDecisionCategory.InventoryAdditionalCapacity =>
201 inventory.GetAdditionalCapacityUsed(
204 MathematicalDecisionCategory.Delivery =>
209 decision.GetDeliveredQuantity(
212 MathematicalDecisionCategory.Backlog =>
217 decision.GetBacklogLevel(
220 MathematicalDecisionCategory.Shortage =>
225 decision.GetShortageQuantity(
228 MathematicalDecisionCategory.Transport =>
233 decision.GetTransportedQuantity(
236 MathematicalDecisionCategory.TransportSetup =>
241 decision.IsSetupActivated(
246 MathematicalDecisionCategory.TransportAdditionalCapacity =>
251 decision.GetAdditionalCapacityUsed(
254 MathematicalDecisionCategory.Procurement =>
259 MathematicalDecisionCategory.WorkCenterActivation =>
260 ResolveWorkCenterCapacity(
264 decision.IsActivated(
269 MathematicalDecisionCategory.WorkCenterAdditionalCapacity =>
270 ResolveWorkCenterCapacity(
274 decision.GetAdditionalCapacityUsed(
277 MathematicalDecisionCategory.WarehouseActivation =>
278 ResolveWarehouseCapacity(
282 decision.IsActivated(
287 MathematicalDecisionCategory.WarehouseAdditionalCapacity =>
288 ResolveWarehouseCapacity(
292 decision.GetAdditionalCapacityUsed(
295 MathematicalDecisionCategory.TransportResourceActivation =>
296 ResolveTransportResourceCapacity(
300 decision.IsActivated(
305 MathematicalDecisionCategory.TransportResourceAdditionalCapacity =>
306 ResolveTransportResourceCapacity(
310 decision.GetAdditionalCapacityUsed(
313 MathematicalDecisionCategory.Subcontracting =>
314 throw new NotSupportedException(
315 "The current normalized LotSizingSolution model " +
316 "does not expose a subcontracting decision family."),
319 throw new NotSupportedException(
320 $
"Mathematical decision category " +
321 $
"'{key.Category}' is not supported by the " +
322 "solution value projector.")
326 private static double ResolveMicroProduction(
327 LotSizingSolution solution,
328 MathematicalDomainKey key)
330 int routingId = key.GetRequiredInt32(MathematicalDomainKeySegment.Routing);
331 ProductionMicroPeriodDecision decision = ResolveMicroDecision(solution, key);
332 return decision.RoutingId == routingId ? decision.Quantity : 0.0;
335 private static double ResolveMicroSetupState(
336 LotSizingSolution solution,
337 MathematicalDomainKey key)
339 int itemId = key.GetRequiredInt32(MathematicalDomainKeySegment.Item);
340 ProductionMicroPeriodDecision decision = ResolveMicroDecision(solution, key);
341 return decision.SetupItemId == itemId ? 1.0 : 0.0;
344 private static double ResolveMicroChangeover(
345 LotSizingSolution solution,
346 MathematicalDomainKey key)
348 int fromItemId = key.GetRequiredInt32(MathematicalDomainKeySegment.FromItem);
349 int toItemId = key.GetRequiredInt32(MathematicalDomainKeySegment.ToItem);
350 int plantId = key.GetRequiredInt32(MathematicalDomainKeySegment.Plant);
351 int workCenterId = key.GetRequiredInt32(MathematicalDomainKeySegment.WorkCenter);
352 int period = key.GetRequiredInt32(MathematicalDomainKeySegment.Period);
353 int micro = key.GetRequiredInt32(MathematicalDomainKeySegment.MicroPeriod);
354 WorkCenterSchedulingDecision schedule = solution.WorkCenterSchedulingDecisions.SingleOrDefault(candidate => candidate.WorkCenter.PlantId == plantId && candidate.WorkCenter.WorkCenterId == workCenterId) ??
throw new InvalidOperationException(
"No candidate macro/micro schedule matches the mathematical domain key.");
355 ProductionMicroPeriodDecision[] ordered = schedule.MicroPeriods.OrderBy(candidate => candidate.MicroPeriod.MacroPeriod).ThenBy(candidate => candidate.MicroPeriod.MicroPeriodIndex).ToArray();
356 int currentIndex = Array.FindIndex(ordered, candidate => candidate.MicroPeriod.MacroPeriod == period && candidate.MicroPeriod.MicroPeriodIndex == micro);
357 if (currentIndex < 0) {
throw new InvalidOperationException(
"No candidate micro-period decision matches the changeover domain key."); }
358 if (currentIndex == 0) {
return ordered[0].SetupItemId == toItemId ? 1.0 : 0.0; }
359 return ordered[currentIndex - 1].SetupItemId == fromItemId && ordered[currentIndex].SetupItemId == toItemId ? 1.0 : 0.0;
362 private static double ResolveCashBalance(
363 LotSizingSolution solution,
364 MathematicalDomainKey key)
367 key.GetRequiredInt32(
368 MathematicalDomainKeySegment.Period);
370 return solution.TryGetCashBalance(
376 private static double ResolveInitialInventory(
377 LotSizingSolution solution,
378 MathematicalDomainKey key)
381 key.GetRequiredInt32(
382 MathematicalDomainKeySegment.Item);
386 MathematicalDomainKeySegment.Warehouse,
387 out
int warehouseId);
391 MathematicalDomainKeySegment.Plant,
394 if (hasWarehouse == hasPlant)
396 throw new InvalidOperationException(
397 "An initial-inventory key must identify exactly one warehouse.");
401 solution.InventoryDecisions.SingleOrDefault(
403 candidate.ItemId == itemId &&
405 ? candidate.Warehouse.Kind ==
406 WarehouseReferenceKind.StandaloneWarehouse &&
407 candidate.Warehouse.ReferenceId == warehouseId
408 : candidate.Warehouse.Kind ==
409 WarehouseReferenceKind.PlantWarehouse &&
410 candidate.Warehouse.ReferenceId == plantId));
412 return decision?.InitialInventoryLevel ?? 0.0;
414 private static double ResolveProductionStartUp(
415 LotSizingSolution solution,
416 MathematicalDomainKey key)
418 if (key.TryGetInt32(MathematicalDomainKeySegment.MicroPeriod, out _))
420 return ResolveMicroSetupStart(solution, key);
423 return ResolveProduction(
427 IsSchedulingSetupStart(production, key)
432 private static double ResolveMicroSetupStart(
433 LotSizingSolution solution,
434 MathematicalDomainKey key)
436 int itemId = key.GetRequiredInt32(MathematicalDomainKeySegment.Item);
437 int plantId = key.GetRequiredInt32(MathematicalDomainKeySegment.Plant);
438 int workCenterId = key.GetRequiredInt32(MathematicalDomainKeySegment.WorkCenter);
439 int period = key.GetRequiredInt32(MathematicalDomainKeySegment.Period);
440 int micro = key.GetRequiredInt32(MathematicalDomainKeySegment.MicroPeriod);
441 WorkCenterSchedulingDecision schedule = solution.WorkCenterSchedulingDecisions.SingleOrDefault(candidate => candidate.WorkCenter.PlantId == plantId && candidate.WorkCenter.WorkCenterId == workCenterId) ??
throw new InvalidOperationException(
"No candidate macro/micro schedule matches the mathematical domain key.");
442 ProductionMicroPeriodDecision[] ordered = schedule.MicroPeriods.OrderBy(candidate => candidate.MicroPeriod.MacroPeriod).ThenBy(candidate => candidate.MicroPeriod.MicroPeriodIndex).ToArray();
443 int currentIndex = Array.FindIndex(ordered, candidate => candidate.MicroPeriod.MacroPeriod == period && candidate.MicroPeriod.MicroPeriodIndex == micro);
444 if (currentIndex < 0) {
throw new InvalidOperationException(
"No candidate micro-period decision matches the setup-start domain key."); }
445 if (ordered[currentIndex].SetupItemId != itemId) {
return 0.0; }
446 if (key.TryGetInt32(MathematicalDomainKeySegment.SetupReset, out
int reset) && reset != 0) {
return 1.0; }
447 if (key.TryGetInt32(MathematicalDomainKeySegment.FromItem, out
int fromItem)) {
return itemId == fromItem ? 0.0 : 1.0; }
448 if (currentIndex == 0) {
return 1.0; }
449 return ordered[currentIndex - 1].SetupItemId == itemId ? 0.0 : 1.0;
452 private static ProductionMicroPeriodDecision ResolveMicroDecision(
453 LotSizingSolution solution,
454 MathematicalDomainKey key)
456 int plantId = key.GetRequiredInt32(MathematicalDomainKeySegment.Plant);
457 int workCenterId = key.GetRequiredInt32(MathematicalDomainKeySegment.WorkCenter);
458 int period = key.GetRequiredInt32(MathematicalDomainKeySegment.Period);
459 int micro = key.GetRequiredInt32(MathematicalDomainKeySegment.MicroPeriod);
460 WorkCenterSchedulingDecision schedule = solution.WorkCenterSchedulingDecisions.SingleOrDefault(candidate => candidate.WorkCenter.PlantId == plantId && candidate.WorkCenter.WorkCenterId == workCenterId) ??
throw new InvalidOperationException(
"No candidate macro/micro schedule matches the mathematical domain key.");
461 return schedule.MicroPeriods.SingleOrDefault(candidate => candidate.MicroPeriod.MacroPeriod == period && candidate.MicroPeriod.MicroPeriodIndex == micro) ??
throw new InvalidOperationException(
"No candidate micro-period decision matches the mathematical domain key.");
464 private static bool IsSchedulingSetupStart(
465 ProductionDecision production,
466 MathematicalDomainKey key)
468 int period = GetPeriod(key);
469 if (!production.IsSetupActivated(period)) {
return false; }
470 if (key.TryGetInt32(MathematicalDomainKeySegment.SetupReset, out
int reset) && reset != 0) {
return true; }
471 if (period == 1 && key.TryGetInt32(MathematicalDomainKeySegment.FromItem, out
int fromItem) && key.TryGetInt32(MathematicalDomainKeySegment.Item, out
int itemId)) {
return itemId != fromItem; }
472 return period == 1 || !production.IsSetupActivated(period - 1);
475 private static double ResolveProduction(
476 LotSizingSolution solution,
477 MathematicalDomainKey key,
478 Func<ProductionDecision, double> selector)
481 key.GetRequiredInt32(
482 MathematicalDomainKeySegment.Routing);
484 ProductionDecision decision =
485 solution.ProductionDecisions
488 candidate.RoutingId ==
490 ??
throw MissingDecision(
492 $
"production routing {routingId}");
494 return selector(decision);
497 private static double ResolveInventory(
498 LotSizingSolution solution,
499 MathematicalDomainKey key,
500 Func<InventoryDecision, double> selector)
503 key.GetRequiredInt32(
504 MathematicalDomainKeySegment.Item);
506 WarehouseReference warehouse =
509 MathematicalDomainKeySegment.Warehouse,
510 MathematicalDomainKeySegment.Plant,
513 InventoryDecision decision =
514 solution.InventoryDecisions
522 ??
throw MissingDecision(
524 $
"inventory item {itemId}");
526 return selector(decision);
529 private static double ResolveDistribution(
530 LotSizingSolution solution,
531 MathematicalDomainKey key,
532 Func<DistributionDecision, double> selector)
534 int distributionCenterId =
535 key.GetRequiredInt32(
536 MathematicalDomainKeySegment.DistributionCenter);
539 key.GetRequiredInt32(
540 MathematicalDomainKeySegment.Item);
542 WarehouseReference warehouse =
545 MathematicalDomainKeySegment.Warehouse,
546 MathematicalDomainKeySegment.Plant,
547 "distribution source");
549 DistributionDecision decision =
550 solution.DistributionDecisions
554 distributionCenterId,
557 ??
throw MissingDecision(
559 $
"distribution center {distributionCenterId}, " +
562 return selector(decision);
565 private static double ResolveTransport(
566 LotSizingSolution solution,
567 MathematicalDomainKey key,
568 Func<TransportDecision, double> selector)
571 key.GetRequiredInt32(
572 MathematicalDomainKeySegment.Item);
574 int transportResourceId =
575 key.GetRequiredInt32(
576 MathematicalDomainKeySegment.TransportResource);
578 WarehouseReference origin =
581 MathematicalDomainKeySegment.OriginWarehouse,
582 MathematicalDomainKeySegment.OriginPlant,
585 WarehouseReference destination =
588 MathematicalDomainKeySegment.DestinationWarehouse,
589 MathematicalDomainKeySegment.DestinationPlant,
590 "transport destination");
592 TransportDecision decision =
593 solution.TransportDecisions
601 ??
throw MissingDecision(
603 $
"transport item {itemId}, resource " +
604 $
"{transportResourceId}");
606 return selector(decision);
609 private static double ResolveProcurement(
610 LotSizingSolution solution,
611 MathematicalDomainKey key)
614 key.GetRequiredInt32(
615 MathematicalDomainKeySegment.Supplier);
618 key.GetRequiredInt32(
619 MathematicalDomainKeySegment.Item);
621 WarehouseReference destination =
624 MathematicalDomainKeySegment.DestinationWarehouse,
625 MathematicalDomainKeySegment.DestinationPlant,
626 "procurement destination");
628 PurchaseDecision decision =
629 solution.PurchaseDecisions
636 ??
throw MissingDecision(
638 $
"supplier {supplierId}, item {itemId}");
640 return decision.GetPurchasedQuantity(
644 private static double ResolveWorkCenterCapacity(
645 LotSizingSolution solution,
646 MathematicalDomainKey key,
647 Func<WorkCenterCapacityDecision, double> selector)
650 key.GetRequiredInt32(
651 MathematicalDomainKeySegment.Plant);
654 key.GetRequiredInt32(
655 MathematicalDomainKeySegment.WorkCenter);
657 WorkCenterCapacityDecision decision =
658 solution.WorkCenterCapacityDecisions
661 candidate.WorkCenter.PlantId ==
663 candidate.WorkCenter.WorkCenterId ==
665 ??
throw MissingDecision(
667 $
"work center {plantId}/{workCenterId}");
669 return selector(decision);
672 private static double ResolveWarehouseCapacity(
673 LotSizingSolution solution,
674 MathematicalDomainKey key,
675 Func<WarehouseCapacityDecision, double> selector)
677 WarehouseReference warehouse =
680 MathematicalDomainKeySegment.Warehouse,
681 MathematicalDomainKeySegment.Plant,
682 "warehouse capacity");
684 WarehouseCapacityDecision decision =
685 solution.WarehouseCapacityDecisions
690 ??
throw MissingDecision(
692 "warehouse capacity");
694 return selector(decision);
697 private static double ResolveTransportResourceCapacity(
698 LotSizingSolution solution,
699 MathematicalDomainKey key,
700 Func<TransportResourceCapacityDecision, double> selector)
702 int transportResourceId =
703 key.GetRequiredInt32(
704 MathematicalDomainKeySegment.TransportResource);
706 TransportResourceCapacityDecision decision =
707 solution.TransportResourceCapacityDecisions
711 transportResourceId))
712 ??
throw MissingDecision(
714 $
"transport resource {transportResourceId}");
716 return selector(decision);
719 private static int GetPeriod(
720 MathematicalDomainKey key)
722 return key.GetRequiredInt32(
723 MathematicalDomainKeySegment.Period);
726 private static WarehouseReference ResolveWarehouse(
727 MathematicalDomainKey key,
728 string standaloneSegment,
735 out
int warehouseId);
742 if (hasStandalone == hasPlant)
744 throw new InvalidOperationException(
745 $
"The domain key must identify exactly one {role} " +
746 $
"warehouse using either '{standaloneSegment}' or " +
747 $
"'{plantSegment}'.");
751 ? WarehouseReference.ForStandaloneWarehouse(
753 : WarehouseReference.ForPlantWarehouse(
757 private static bool SameWarehouse(
758 WarehouseReference left,
759 WarehouseReference right)
767 private static InvalidOperationException MissingDecision(
768 MathematicalDomainKey key,
769 string decisionDescription)
771 return new InvalidOperationException(
772 $
"No candidate {decisionDescription} decision matches " +
773 $
"mathematical domain key '{FormatKey(key)}'.");
776 private static string FormatKey(
777 MathematicalDomainKey key)
787 $
"{pair.Key}={pair.Value}")));