114 private readonly
double _numericalTolerance;
124 double numericalTolerance = 1e-9)
126 if (!
double.IsFinite(numericalTolerance) ||
127 numericalTolerance < 0.0)
129 throw new ArgumentOutOfRangeException(
130 nameof(numericalTolerance),
132 "The numerical tolerance must be finite " +
133 "and non-negative.");
136 _numericalTolerance =
155 public IReadOnlyList<SolutionValidationIssue>
Validate(
158 ArgumentNullException.ThrowIfNull(solution);
161 new List<SolutionValidationIssue>();
163 ValidateGeneralInformation(
167 ValidateGenerationMetadata(
195 public IReadOnlyList<SolutionValidationIssue>
Validate(
197 SupplyChain supplyChain)
199 ArgumentNullException.ThrowIfNull(solution);
200 ArgumentNullException.ThrowIfNull(supplyChain);
205 ValidateAgainstSupplyChain(
247 SupplyChain supplyChain)
290 SupplyChain supplyChain)
298 private static void ValidateGeneralInformation(
300 ICollection<SolutionValidationIssue> issues)
302 if (solution.
Id == Guid.Empty)
308 "The solution identifier cannot be empty.");
317 "The planning horizon must be strictly positive.");
320 if (
string.IsNullOrWhiteSpace(
326 "instanceIdentifier",
327 "No instance identifier is recorded.");
336 "The solution does not contain any " +
347 "The completeness of the solution " +
348 "has not been evaluated.");
359 "evaluation.feasibilityStatus",
360 "A partial solution cannot be declared " +
361 "globally feasible.");
372 "evaluation.optimalityStatus",
373 "A solution cannot be declared proven optimal " +
374 "unless it is complete.");
378 private static void ValidateGenerationMetadata(
379 LotSizingSolution solution,
380 ICollection<SolutionValidationIssue> issues)
388 "generationMetadata.methodKind",
389 "The solution-generation method is unknown.");
398 "generationMetadata.terminationReason",
399 "The execution termination reason is unknown.");
402 var duplicateParameterNames =
403 solution.GenerationMetadata.Parameters
406 !
string.IsNullOrWhiteSpace(
411 StringComparer.OrdinalIgnoreCase)
419 foreach (
string parameterName
420 in duplicateParameterNames)
425 "generationMetadata.parameters",
426 $
"The algorithm parameter " +
427 $
"'{parameterName}' is duplicated.");
435 if (
string.IsNullOrWhiteSpace(
443 $
"generationMetadata.parameters[{index}]",
444 "The algorithm parameter name is empty.");
456 "generationMetadata.terminationReason",
457 "The generation metadata indicates that " +
458 "optimality was proven, but the evaluation " +
459 "does not declare the solution proven optimal.");
463 private void ValidateEvaluation(
464 LotSizingSolution solution,
465 ICollection<SolutionValidationIssue> issues)
467 SolutionEvaluation evaluation =
478 "evaluation.optimalityStatus",
479 "A proven-optimal solution must be feasible.");
489 "evaluation.objectiveValue",
490 "A proven-optimal solution must have an " +
503 "evaluation.absoluteGap",
504 "The absolute gap of a proven-optimal solution " +
505 "must be zero within the numerical tolerance.");
517 "evaluation.relativeGap",
518 "The relative gap of a proven-optimal solution " +
519 "must be zero within the numerical tolerance.");
529 "evaluation.bestBound",
530 "An optimality gap is recorded without " +
531 "an objective bound.");
540 "evaluation.objectiveValue",
541 "An objective bound is recorded without " +
542 "an objective value.");
554 "evaluation.maximumConstraintViolation",
555 "The maximum constraint violation cannot " +
556 "exceed the total constraint violation.");
559 bool hasPositiveViolation =
560 evaluation.MaximumConstraintViolation >
562 evaluation.TotalConstraintViolation >
564 evaluation.ViolatedConstraintCount > 0;
568 hasPositiveViolation)
573 "evaluation.feasibilityStatus",
574 "A solution with recorded constraint " +
575 "violations cannot be declared feasible.");
587 "The solution is declared feasible while " +
588 "its completeness remains unknown.");
592 private static void ValidateDecisions(
593 LotSizingSolution solution,
594 ICollection<SolutionValidationIssue> issues)
596 ValidateProductionDecisions(
600 ValidateInventoryDecisions(
604 ValidateTransportDecisions(
608 ValidatePurchaseDecisions(
612 ValidateDistributionDecisions(
616 ValidateWorkCenterCapacityDecisions(
620 ValidateWarehouseCapacityDecisions(
624 ValidateTransportResourceCapacityDecisions(
629 private static void ValidateProductionDecisions(
630 LotSizingSolution solution,
631 ICollection<SolutionValidationIssue> issues)
640 ProductionDecision decision =
644 $
"productionDecisions[{index}]";
646 ValidateDecisionHorizon(
658 "The production decision is not " +
659 "internally valid.");
662 if (!seenRoutingIds.Add(
669 $
"A production decision for routing " +
670 $
"{decision.RoutingId} is duplicated.");
675 private static void ValidateInventoryDecisions(
676 LotSizingSolution solution,
677 ICollection<SolutionValidationIssue> issues)
682 WarehouseReferenceKind Kind,
689 InventoryDecision decision =
693 $
"inventoryDecisions[{index}]";
695 ValidateDecisionHorizon(
707 "The inventory decision is not " +
708 "internally valid.");
715 decision.Warehouse.ReferenceId
718 if (!seenKeys.Add(key))
724 "The item-and-warehouse inventory " +
725 "decision key is duplicated.");
730 private static void ValidateTransportDecisions(
731 LotSizingSolution solution,
732 ICollection<SolutionValidationIssue> issues)
738 WarehouseReferenceKind OriginKind,
740 WarehouseReferenceKind DestinationKind,
741 int DestinationId)>();
747 TransportDecision decision =
751 $
"transportDecisions[{index}]";
753 ValidateDecisionHorizon(
765 "The transport decision is not " +
766 "internally valid.");
774 decision.
Origin.ReferenceId,
776 decision.Destination.ReferenceId
779 if (!seenKeys.Add(key))
785 "The item-resource-lane transport " +
786 "decision key is duplicated.");
791 private static void ValidatePurchaseDecisions(
792 LotSizingSolution solution,
793 ICollection<SolutionValidationIssue> issues)
799 WarehouseReferenceKind Kind,
806 PurchaseDecision decision =
810 $
"purchaseDecisions[{index}]";
812 ValidateDecisionHorizon(
824 "The purchase decision is not " +
825 "internally valid.");
833 decision.DestinationWarehouse.ReferenceId
836 if (!seenKeys.Add(key))
842 "The supplier-item-warehouse purchase " +
843 "decision key is duplicated.");
848 private static void ValidateDistributionDecisions(
849 LotSizingSolution solution,
850 ICollection<SolutionValidationIssue> issues)
854 int DistributionCenterId,
856 WarehouseReferenceKind Kind,
863 DistributionDecision decision =
867 $
"distributionDecisions[{index}]";
869 ValidateDecisionHorizon(
881 "The distribution decision is not " +
882 "internally valid.");
890 decision.Warehouse.ReferenceId
893 if (!seenKeys.Add(key))
899 "The distribution-center-item-warehouse " +
900 "decision key is duplicated.");
905 private static void ValidateWorkCenterCapacityDecisions(
906 LotSizingSolution solution,
907 ICollection<SolutionValidationIssue> issues)
910 new HashSet<(int PlantId, int WorkCenterId)>();
917 WorkCenterCapacityDecision decision =
921 $
"workCenterCapacityDecisions[{index}]";
923 ValidateDecisionHorizon(
935 "The work-center capacity decision " +
936 "is not internally valid.");
942 decision.WorkCenter.WorkCenterId
945 if (!seenKeys.Add(key))
951 "The work-center capacity decision " +
952 "key is duplicated.");
957 private static void ValidateWarehouseCapacityDecisions(
958 LotSizingSolution solution,
959 ICollection<SolutionValidationIssue> issues)
963 WarehouseReferenceKind Kind,
971 WarehouseCapacityDecision decision =
975 $
"warehouseCapacityDecisions[{index}]";
977 ValidateDecisionHorizon(
989 "The warehouse-capacity decision " +
990 "is not internally valid.");
996 decision.Warehouse.ReferenceId
999 if (!seenKeys.Add(key))
1005 "The warehouse-capacity decision " +
1006 "key is duplicated.");
1012 ValidateTransportResourceCapacityDecisions(
1013 LotSizingSolution solution,
1014 ICollection<SolutionValidationIssue> issues)
1016 var seenResourceIds =
1024 TransportResourceCapacityDecision decision =
1029 $
"transportResourceCapacityDecisions[{index}]";
1031 ValidateDecisionHorizon(
1043 "The transport-resource capacity decision " +
1044 "is not internally valid.");
1047 if (!seenResourceIds.Add(
1054 "The transport-resource capacity decision " +
1055 "key is duplicated.");
1060 private static void ValidateAgainstSupplyChain(
1061 LotSizingSolution solution,
1062 SupplyChain supplyChain,
1063 ICollection<SolutionValidationIssue> issues)
1066 supplyChain.PlanningHorizon)
1072 $
"The solution horizon " +
1073 $
"({solution.PlanningHorizon}) differs from " +
1074 $
"the instance horizon " +
1075 $
"({supplyChain.PlanningHorizon}).");
1078 ValidateProductionReferences(
1083 ValidateInventoryReferences(
1088 ValidateTransportReferences(
1093 ValidatePurchaseReferences(
1098 ValidateDistributionReferences(
1103 ValidateCapacityReferences(
1109 private static void ValidateProductionReferences(
1110 LotSizingSolution solution,
1111 SupplyChain supplyChain,
1112 ICollection<SolutionValidationIssue> issues)
1118 ProductionDecision decision =
1121 bool routingExists =
1122 supplyChain.ProductionRoutings.Any(
1132 $
"productionDecisions[{index}].routingId",
1133 $
"Production routing " +
1134 $
"{decision.RoutingId} does not exist " +
1135 "in the supply-chain instance.");
1140 private static void ValidateInventoryReferences(
1141 LotSizingSolution solution,
1142 SupplyChain supplyChain,
1143 ICollection<SolutionValidationIssue> issues)
1149 InventoryDecision decision =
1153 $
"inventoryDecisions[{index}]";
1155 ValidateItemReference(
1161 ValidateWarehouseReference(
1164 $
"{path}.warehouse",
1167 bool inventoryExists =
1168 supplyChain.Inventories.Any(
1173 inventory.Warehouse,
1176 if (!inventoryExists)
1182 "No inventory relationship exists " +
1183 "for this item and warehouse.");
1188 private static void ValidateTransportReferences(
1189 LotSizingSolution solution,
1190 SupplyChain supplyChain,
1191 ICollection<SolutionValidationIssue> issues)
1197 TransportDecision decision =
1201 $
"transportDecisions[{index}]";
1203 ValidateItemReference(
1209 ValidateWarehouseReference(
1215 ValidateWarehouseReference(
1218 $
"{path}.destination",
1222 supplyChain.TransportResources
1228 if (resource is
null)
1233 $
"{path}.transportResourceId",
1234 $
"Transport resource " +
1235 $
"{decision.TransportResourceId} " +
1241 bool characteristicExists =
1242 supplyChain.TransportCharacteristics.Any(
1244 characteristic.ItemId ==
1246 characteristic.TransportResourceId ==
1249 if (!characteristicExists)
1255 "No transport characteristic exists " +
1256 "for this item and transport resource.");
1260 supplyChain.GetTransportLanes(resource.Id).Any(
1275 "The directed transport lane does not " +
1276 "exist for this transport resource.");
1281 private static void ValidatePurchaseReferences(
1282 LotSizingSolution solution,
1283 SupplyChain supplyChain,
1284 ICollection<SolutionValidationIssue> issues)
1290 PurchaseDecision decision =
1294 $
"purchaseDecisions[{index}]";
1296 ValidateItemReference(
1302 ValidateWarehouseReference(
1305 $
"{path}.destinationWarehouse",
1308 bool supplierExists =
1309 supplyChain.Suppliers.Any(
1314 if (!supplierExists)
1319 $
"{path}.supplierId",
1320 $
"Supplier {decision.SupplierId} " +
1324 bool deliveryExists =
1325 supplyChain.SupplierDeliveries.Any(
1327 delivery.SupplierId ==
1335 if (!deliveryExists)
1341 "No supplier-delivery relationship exists " +
1342 "for this supplier, item and warehouse.");
1347 private static void ValidateDistributionReferences(
1348 LotSizingSolution solution,
1349 SupplyChain supplyChain,
1350 ICollection<SolutionValidationIssue> issues)
1356 DistributionDecision decision =
1360 $
"distributionDecisions[{index}]";
1362 ValidateItemReference(
1368 ValidateWarehouseReference(
1371 $
"{path}.warehouse",
1374 bool distributionCenterExists =
1375 supplyChain.DistributionCenters.Any(
1376 distributionCenter =>
1377 distributionCenter.Id ==
1380 if (!distributionCenterExists)
1385 $
"{path}.distributionCenterId",
1386 $
"Distribution center " +
1387 $
"{decision.DistributionCenterId} " +
1391 bool sourcingExists =
1392 supplyChain.DistributionCenterSourcings.Any(
1394 sourcing.DistributionCenterId ==
1402 if (!sourcingExists)
1408 "No distribution-center sourcing " +
1409 "relationship exists for this center, " +
1410 "item and warehouse.");
1415 private static void ValidateCapacityReferences(
1416 LotSizingSolution solution,
1417 SupplyChain supplyChain,
1418 ICollection<SolutionValidationIssue> issues)
1425 WorkCenterCapacityDecision decision =
1428 bool workCenterExists =
1429 supplyChain.Plants.Any(
1433 plant.WorkCenters.Any(
1439 if (!workCenterExists)
1444 $
"workCenterCapacityDecisions[{index}]" +
1447 $
"{decision.WorkCenter.PlantId}:" +
1448 $
"{decision.WorkCenter.WorkCenterId} " +
1458 WarehouseCapacityDecision decision =
1461 ValidateWarehouseReference(
1464 $
"warehouseCapacityDecisions[{index}]" +
1474 TransportResourceCapacityDecision decision =
1478 bool resourceExists =
1479 supplyChain.TransportResources.Any(
1484 if (!resourceExists)
1489 $
"transportResourceCapacityDecisions" +
1490 $
"[{index}].transportResourceId",
1491 $
"Transport resource " +
1492 $
"{decision.TransportResourceId} " +
1498 private static void ValidateItemReference(
1500 SupplyChain supplyChain,
1502 ICollection<SolutionValidationIssue> issues)
1505 supplyChain.Items.Any(
1515 $
"Item {itemId} does not exist.");
1519 private static void ValidateWarehouseReference(
1520 WarehouseReference warehouse,
1521 SupplyChain supplyChain,
1523 ICollection<SolutionValidationIssue> issues)
1525 if (!WarehouseExists(
1533 $
"Warehouse {warehouse.Kind}:" +
1534 $
"{warehouse.ReferenceId} does not exist.");
1538 private static bool WarehouseExists(
1539 WarehouseReference warehouse,
1540 SupplyChain supplyChain)
1542 return warehouse.Kind
switch
1544 WarehouseReferenceKind.StandaloneWarehouse =>
1545 supplyChain.StandaloneWarehouses.Any(
1548 warehouse.ReferenceId),
1550 WarehouseReferenceKind.PlantWarehouse =>
1551 supplyChain.Plants.Any(
1554 warehouse.ReferenceId),
1560 private static void ValidateDecisionHorizon(
1561 int decisionHorizon,
1562 int solutionHorizon,
1564 ICollection<SolutionValidationIssue> issues)
1566 if (decisionHorizon != solutionHorizon)
1572 $
"The decision horizon ({decisionHorizon}) " +
1573 $
"differs from the solution horizon " +
1574 $
"({solutionHorizon}).");
1578 private static bool SameWarehouse(
1579 WarehouseReference first,
1580 WarehouseReference second)
1582 return first.Kind ==
1584 first.ReferenceId ==
1588 private static void ThrowIfErrors(
1589 IEnumerable<SolutionValidationIssue> issues)
1591 SolutionValidationIssue[] errors =
1599 if (errors.Length == 0)
1605 "The lot-sizing solution is invalid:" +
1606 Environment.NewLine +
1608 Environment.NewLine,
1613 throw new InvalidOperationException(
1617 private static void AddError(
1618 ICollection<SolutionValidationIssue> issues,
1624 new SolutionValidationIssue(
1631 private static void AddWarning(
1632 ICollection<SolutionValidationIssue> issues,
1638 new SolutionValidationIssue(