75 bool validateCurrentFingerprint =
true)
77 ArgumentNullException.ThrowIfNull(instance);
82 ValidateInstanceMetadata(
86 ValidateProductStructure(
90 ValidateProblemClassification(
94 ValidateRecommendationReport(
98 Dictionary<string, KnownResult> knownResultsById =
103 ValidateBestKnownResult(
108 ValidateSupersedingReferences(
113 ValidateSupersedingCycles(
117 if (validateCurrentFingerprint)
119 ValidateFingerprints(
124 return NormalizeMessages(
150 bool validateCurrentFingerprint =
true)
152 ArgumentNullException.ThrowIfNull(instance);
160 "The instance does not have a human-readable " +
167 "The instance creation date has not been " +
174 "No source information has been recorded for " +
178 AddProductStructureWarnings(
182 AddClassificationWarnings(
186 AddRecommendationWarnings(
191 in instance.KnownResults
192 .Where(result => result is not
null))
194 AddKnownResultWarnings(
199 if (instance.HasEligibleBestKnownResult)
202 instance.BestKnownResult;
204 if (bestKnownResult is not
null &&
209 $
"Best known result " +
210 $
"'{bestKnownResult.ResultId}' has not " +
214 if (bestKnownResult is not
null &&
219 $
"Best known result " +
220 $
"'{bestKnownResult.ResultId}' is based " +
221 "only on information reported by its " +
226 if (validateCurrentFingerprint)
228 AddFingerprintWarnings(
233 return NormalizeMessages(
254 bool validateCurrentFingerprint =
true)
258 validateCurrentFingerprint).Count == 0;
277 bool validateCurrentFingerprint =
true)
279 IReadOnlyList<string> errors =
282 validateCurrentFingerprint);
284 if (errors.Count == 0)
289 throw new InvalidOperationException(
290 "The lot-sizing instance is invalid:" +
291 Environment.NewLine +
299 private static void ValidateInstanceMetadata(
301 ICollection<string> errors)
306 "The instance format version is missing.");
312 "The instance identifier is missing.");
318 "The supply-chain model is missing.");
324 "The product-structure descriptor is " +
331 "The problem-classification object is " +
341 "The instance modification date precedes its " +
346 private static void ValidateProductStructure(
347 ProductStructureDescriptor descriptor,
348 ICollection<string> errors)
350 if (descriptor is
null)
355 ValidateItemIdentifierCollection(
357 "product-structure root-item",
360 ValidateItemIdentifierCollection(
362 "product-structure leaf-item",
365 ValidateItemIdentifierCollection(
367 "shared-component item",
375 "The product structure contains a cycle but " +
376 "is not marked as invalid.");
384 "A cyclic product structure cannot have a " +
385 "valid detected product-structure type.");
391 ValidateNotAnalyzedProductStructure(
397 ValidateDeclaredOnlyProductStructure(
403 ValidateDetectedOnlyProductStructure(
409 .DeclaredAndConfirmed:
410 ValidateConfirmedProductStructure(
416 .DeclaredAndContradicted:
417 ValidateContradictedProductStructure(
423 ValidateInvalidProductStructure(
429 ValidateOutdatedProductStructure(
436 $
"Unsupported product-structure check " +
437 $
"status '{descriptor.CheckStatus}'.");
442 private static void ValidateNotAnalyzedProductStructure(
443 ProductStructureDescriptor descriptor,
444 ICollection<string> errors)
449 "A declared product-structure type exists but " +
450 "the descriptor status is NotAnalyzed instead " +
457 "A product-structure type has been detected " +
458 "but the descriptor status is NotAnalyzed.");
464 "A product-structure analysis date exists but " +
465 "the descriptor status is NotAnalyzed.");
469 private static void ValidateDeclaredOnlyProductStructure(
470 ProductStructureDescriptor descriptor,
471 ICollection<string> errors)
476 "The product-structure status is DeclaredOnly " +
477 "but no declared type is available.");
483 "The product-structure status is DeclaredOnly " +
484 "but an automatically detected type exists.");
490 "The product-structure status is DeclaredOnly " +
491 "but an automatic analysis date exists.");
495 private static void ValidateDetectedOnlyProductStructure(
496 ProductStructureDescriptor descriptor,
497 ICollection<string> errors)
502 "The product-structure status is DetectedOnly " +
503 "but a declared type also exists.");
509 "The product-structure status is DetectedOnly " +
510 "but no detected type is available.");
513 ValidateCompletedProductStructureAnalysis(
518 private static void ValidateConfirmedProductStructure(
519 ProductStructureDescriptor descriptor,
520 ICollection<string> errors)
526 "A confirmed product structure requires both " +
527 "declared and detected types.");
534 "The product structure is marked as confirmed " +
535 "but the declared and detected types differ.");
538 ValidateCompletedProductStructureAnalysis(
543 private static void ValidateContradictedProductStructure(
544 ProductStructureDescriptor descriptor,
545 ICollection<string> errors)
551 "A contradicted product structure requires " +
552 "both declared and detected types.");
559 "The product structure is marked as " +
560 "contradicted but the declared and detected " +
561 "types are identical.");
564 ValidateCompletedProductStructureAnalysis(
569 private static void ValidateInvalidProductStructure(
570 ProductStructureDescriptor descriptor,
571 ICollection<string> errors)
577 "An invalid product structure must not retain " +
578 "a valid detected structure type.");
584 "The product structure is marked as invalid " +
585 "but no analysis date has been recorded.");
589 private static void ValidateOutdatedProductStructure(
590 ProductStructureDescriptor descriptor,
591 ICollection<string> errors)
597 "The product structure is marked as outdated " +
598 "but no previous automatic analysis exists.");
603 ValidateCompletedProductStructureAnalysis(
604 ProductStructureDescriptor descriptor,
605 ICollection<string> errors)
610 "A completed product-structure analysis must " +
611 "have an analysis date.");
614 if (
string.IsNullOrWhiteSpace(
618 "A completed product-structure analysis must " +
619 "record its analyzer version.");
623 private static void ValidateProblemClassification(
624 LotSizingProblemClassification classification,
625 ICollection<string> errors)
627 if (classification is
null)
632 if (classification.
Features is
null)
635 "The problem classification does not contain " +
636 "a feature profile.");
641 ValidateClassificationMatches(
645 switch (classification.
Status)
648 ValidateNotAnalyzedClassification(
654 ValidateClassifiedClassification(
660 .PartiallyClassified:
661 ValidatePartiallyClassifiedClassification(
667 ValidateAmbiguousClassification(
673 ValidateUnclassifiedClassification(
679 ValidateInvalidClassification(
685 ValidateOutdatedClassification(
692 $
"Unsupported problem-classification " +
693 $
"status '{classification.Status}'.");
698 private static void ValidateClassificationMatches(
699 LotSizingProblemClassification classification,
700 ICollection<string> errors)
703 index < classification.
Matches.Count;
706 KnownProblemTypeMatch? match =
712 $
"Problem-classification match at index " +
713 $
"{index} is null.");
721 $
"Problem-classification match at index " +
722 $
"{index} has no problem-family code.");
725 if (!
double.IsFinite(match.
Score) ||
730 $
"Problem-classification match " +
731 $
"'{DisplayProblemTypeCode(match)}' has " +
732 "an invalid score.");
740 $
"Exact problem-family match " +
741 $
"'{DisplayProblemTypeCode(match)}' " +
742 "contains at least one blocking " +
750 $
"Problem-family match " +
751 $
"'{DisplayProblemTypeCode(match)}' has " +
752 "an unknown match kind.");
759 $
"Problem-family match " +
760 $
"'{DisplayProblemTypeCode(match)}' has " +
761 "an unknown classification scope.");
764 ValidateStringCollection(
766 $
"affected entity key of match " +
767 $
"'{DisplayProblemTypeCode(match)}'",
770 ValidateStringCollection(
772 $
"additional feature code of match " +
773 $
"'{DisplayProblemTypeCode(match)}'",
785 $
"Primary problem-family code " +
786 $
"'{classification.PrimaryProblemTypeCode}' " +
787 "does not reference a reported match.");
791 private static void ValidateEvidence(
792 KnownProblemTypeMatch match,
793 ICollection<string> errors)
799 ClassificationEvidence? evidence =
802 if (evidence is
null)
805 $
"Evidence at index {index} of match " +
806 $
"'{DisplayProblemTypeCode(match)}' is " +
812 if (
string.IsNullOrWhiteSpace(
816 $
"Evidence at index {index} of match " +
817 $
"'{DisplayProblemTypeCode(match)}' has " +
821 if (!
double.IsFinite(evidence.
Weight) ||
825 $
"Evidence at index {index} of match " +
826 $
"'{DisplayProblemTypeCode(match)}' has " +
827 "an invalid weight.");
832 private static void ValidateNotAnalyzedClassification(
833 LotSizingProblemClassification classification,
834 ICollection<string> errors)
839 "The problem classification is NotAnalyzed " +
840 "but a classification date is present.");
846 "The problem classification is NotAnalyzed " +
847 "but problem-family matches are present.");
853 "The problem classification is NotAnalyzed " +
854 "but a primary problem family is present.");
858 private static void ValidateClassifiedClassification(
859 LotSizingProblemClassification classification,
860 ICollection<string> errors)
862 ValidateCompletedClassificationMetadata(
869 "A classified problem must have a primary " +
870 "problem-family code.");
876 "A classified problem must have a valid " +
877 "primary problem-family match.");
882 "A fully classified problem must use an exact " +
883 "primary problem-family match.");
889 "A fully classified problem cannot contain " +
890 "globally unclassified features.");
896 "A fully classified problem cannot contain " +
897 "classification errors.");
902 ValidatePartiallyClassifiedClassification(
903 LotSizingProblemClassification classification,
904 ICollection<string> errors)
906 ValidateCompletedClassificationMetadata(
913 "A partially classified problem must contain " +
914 "at least one problem-family match.");
924 .RecognizedRelaxation ||
927 .RecognizedSubproblem
931 "A partially classified problem must either " +
932 "have a primary family or contain a " +
933 "recognized relaxation or subproblem.");
937 private static void ValidateAmbiguousClassification(
938 LotSizingProblemClassification classification,
939 ICollection<string> errors)
941 ValidateCompletedClassificationMetadata(
948 "An ambiguous classification must not select " +
949 "a primary problem family.");
959 if (candidateCount < 2)
962 "An ambiguous classification must contain at " +
963 "least two direct complete-problem matches.");
967 private static void ValidateUnclassifiedClassification(
968 LotSizingProblemClassification classification,
969 ICollection<string> errors)
971 ValidateCompletedClassificationMetadata(
978 "An unclassified problem must not have a " +
979 "primary problem-family code.");
982 if (classification.
Matches.Any(
989 "An unclassified problem cannot contain a " +
990 "direct complete-problem match.");
994 private static void ValidateInvalidClassification(
995 LotSizingProblemClassification classification,
996 ICollection<string> errors)
1001 "An invalid problem classification must " +
1002 "contain at least one explanatory error.");
1006 private static void ValidateOutdatedClassification(
1007 LotSizingProblemClassification classification,
1008 ICollection<string> errors)
1014 "The problem classification is marked as " +
1015 "outdated but no previous classification " +
1021 ValidateCompletedClassificationMetadata(
1022 LotSizingProblemClassification classification,
1023 ICollection<string> errors)
1028 "A completed problem classification must " +
1029 "record its classification date.");
1035 "A completed problem classification must " +
1036 "record its classifier version.");
1042 "A completed problem classification must " +
1043 "record its catalog name and version.");
1047 private static void ValidateRecommendationReport(
1048 LotSizingInstance instance,
1049 ICollection<string> errors)
1056 SolutionMethodRecommendationReport report =
1059 IReadOnlyList<string> reportErrors =
1062 foreach (
string reportError
in reportErrors)
1065 "Solution-method recommendation report: " +
1070 private static void AddRecommendationWarnings(
1071 LotSizingInstance instance,
1072 ICollection<string> warnings)
1079 SolutionMethodRecommendationReport report =
1086 "The solution-method recommendation report " +
1087 "was generated but contains no " +
1095 "The solution-method recommendation report " +
1096 "does not identify an applicable method.");
1099 foreach (SolutionMethodRecommendation recommendation
1102 recommendation is not
null))
1107 $
"Method recommendation " +
1108 $
"'{recommendation.MethodCode}' has not " +
1112 foreach (
string recommendationWarning
1115 if (!
string.IsNullOrWhiteSpace(
1116 recommendationWarning))
1119 $
"Method '{recommendation.MethodCode}': " +
1120 recommendationWarning.Trim());
1126 private static Dictionary<string, KnownResult>
1127 ValidateKnownResults(
1128 LotSizingInstance instance,
1129 ICollection<string> errors)
1131 var knownResultsById =
1132 new Dictionary<string, KnownResult>(
1133 StringComparer.OrdinalIgnoreCase);
1139 KnownResult? result =
1145 $
"Known result at index {index} is null.");
1153 $
"Known result at index {index} has no " +
1154 "stable identifier.");
1159 if (!knownResultsById.TryAdd(
1164 $
"Known-result identifier " +
1165 $
"'{result.ResultId}' is used more than " +
1169 ValidateKnownResult(
1174 return knownResultsById;
1177 private static void ValidateKnownResult(
1179 ICollection<string> errors)
1181 bool hasMeaningfulInformation =
1182 result.HasReportedObjectiveValue ||
1183 result.HasDetailedSolution ||
1184 result.HasFeasibilityInformation ||
1185 result.HasOptimalityInformation ||
1186 result.HasMethodInformation ||
1187 result.HasSourceInformation ||
1190 if (!hasMeaningfulInformation)
1193 $
"Known result '{result.ResultId}' does not " +
1194 "contain any result, claim, source or " +
1195 "descriptive information.");
1202 StringComparison.OrdinalIgnoreCase))
1205 $
"Known result '{result.ResultId}' cannot " +
1206 "supersede itself.");
1214 $
"Known result '{result.ResultId}' contains " +
1215 "a non-finite objective value.");
1219 private static void ValidateBestKnownResult(
1220 LotSizingInstance instance,
1221 IReadOnlyDictionary<string, KnownResult>
1223 ICollection<string> errors)
1230 if (!knownResultsById.TryGetValue(
1232 out KnownResult? bestKnownResult))
1235 $
"Best-known-result identifier " +
1236 $
"'{instance.BestKnownResultId}' does not " +
1237 "reference a known result.");
1242 if (!bestKnownResult
1243 .CanBeSelectedAsBestKnownResult)
1246 $
"Known result '{bestKnownResult.ResultId}' " +
1247 "is not eligible to be selected as the best " +
1252 private static void ValidateSupersedingReferences(
1253 IEnumerable<KnownResult> knownResults,
1254 IReadOnlyDictionary<string, KnownResult>
1256 ICollection<string> errors)
1258 foreach (KnownResult result
1259 in knownResults.Where(
1261 result is not
null &&
1264 if (!knownResultsById.ContainsKey(
1268 $
"Known result '{result.ResultId}' is " +
1269 $
"superseded by unknown result " +
1270 $
"'{result.SupersededByResultId}'.");
1275 private static void ValidateSupersedingCycles(
1276 IReadOnlyDictionary<string, KnownResult>
1278 ICollection<string> errors)
1280 var reportedCycles =
1281 new HashSet<string>(
1282 StringComparer.OrdinalIgnoreCase);
1284 foreach (KnownResult startResult
1285 in knownResultsById.Values)
1287 var positionByResultId =
1288 new Dictionary<string, int>(
1289 StringComparer.OrdinalIgnoreCase);
1294 KnownResult? currentResult =
1297 while (currentResult is not
null &&
1300 if (positionByResultId.TryGetValue(
1302 out
int cycleStartIndex))
1304 string[] cycleResultIds =
1306 .Skip(cycleStartIndex)
1310 currentResult.ResultId
1314 string cycleDescription =
1319 if (reportedCycles.Add(
1323 "The known-result supersession " +
1324 $
"graph contains a cycle: " +
1325 $
"{cycleDescription}.");
1331 positionByResultId.Add(
1338 if (!knownResultsById.TryGetValue(
1341 out KnownResult? nextResult))
1352 private static void ValidateFingerprints(
1353 LotSizingInstance instance,
1354 ICollection<string> errors)
1356 string currentFingerprint;
1360 currentFingerprint =
1361 LotSizingInstanceFactory
1362 .ComputeSupplyChainFingerprint(
1365 catch (InvalidOperationException exception)
1368 "The current supply-chain fingerprint could " +
1369 $
"not be calculated: {exception.Message}");
1374 ProductStructureDescriptor descriptor =
1377 bool productStructureShouldBeCurrent =
1378 descriptor.CheckStatus !=
1379 ProductStructureCheckStatus.NotAnalyzed &&
1380 descriptor.CheckStatus !=
1381 ProductStructureCheckStatus.DeclaredOnly &&
1382 descriptor.CheckStatus !=
1385 if (productStructureShouldBeCurrent &&
1386 !
string.IsNullOrWhiteSpace(
1391 StringComparison.Ordinal))
1394 "The product-structure analysis does not " +
1395 "match the current supply-chain fingerprint " +
1396 "and should be marked as outdated.");
1399 LotSizingProblemClassification classification =
1402 bool classificationShouldBeCurrent =
1403 classification.Status !=
1404 ProblemClassificationStatus.NotAnalyzed &&
1405 classification.Status !=
1408 if (classificationShouldBeCurrent &&
1412 currentFingerprint))
1415 "The problem classification does not match " +
1416 "the current supply-chain fingerprint and " +
1417 "should be marked as outdated.");
1420 SolutionMethodRecommendationReport recommendationReport =
1425 !recommendationReport
1427 currentFingerprint))
1430 "The solution-method recommendation report " +
1431 "does not match the current supply-chain " +
1432 "fingerprint and must be regenerated.");
1436 private static void AddProductStructureWarnings(
1437 ProductStructureDescriptor descriptor,
1438 ICollection<string> warnings)
1442 .DeclaredAndContradicted)
1445 $
"The declared product-structure type " +
1446 $
"'{descriptor.DeclaredType}' differs from " +
1447 $
"the detected type " +
1448 $
"'{descriptor.DetectedType}'.");
1455 "The stored product-structure analysis is " +
1460 string.IsNullOrWhiteSpace(
1464 "The product-structure analysis does not " +
1465 "record a supply-chain fingerprint.");
1469 private static void AddClassificationWarnings(
1470 LotSizingProblemClassification classification,
1471 ICollection<string> warnings)
1473 if (classification.
Status ==
1477 "The stored problem classification is " +
1481 if (classification.
Status ==
1483 .PartiallyClassified)
1486 "The problem is only partially classified.");
1489 if (classification.
Status ==
1493 "The problem classification is ambiguous.");
1501 $
"Primary problem family " +
1502 $
"'{classification.PrimaryProblemTypeCode}' " +
1503 "does not have a human-readable name.");
1511 "The problem classification does not record " +
1512 "a supply-chain fingerprint.");
1515 foreach (
string warning
1518 if (!
string.IsNullOrWhiteSpace(warning))
1527 private static void AddKnownResultWarnings(
1529 ICollection<string> warnings)
1535 $
"Known result '{result.ResultId}' has an " +
1536 "objective value but no objective name.");
1543 $
"Known result '{result.ResultId}' has an " +
1544 "objective value but no objective unit.");
1553 $
"Known result '{result.ResultId}' is marked " +
1554 "as source-reported but has no source " +
1563 $
"Known result '{result.ResultId}' contains " +
1564 "a detailed solution that has not been " +
1571 $
"Known result '{result.ResultId}' does not " +
1572 "record a supply-chain fingerprint.");
1576 private static void AddFingerprintWarnings(
1577 LotSizingInstance instance,
1578 ICollection<string> warnings)
1580 string currentFingerprint;
1584 currentFingerprint =
1585 LotSizingInstanceFactory
1586 .ComputeSupplyChainFingerprint(
1589 catch (InvalidOperationException)
1594 foreach (KnownResult result
1597 result is not
null &&
1601 currentFingerprint))
1604 $
"Known result '{result.ResultId}' was " +
1605 "recorded for a different supply-chain " +
1610 SolutionMethodRecommendationReport recommendationReport =
1615 !recommendationReport
1617 currentFingerprint))
1620 "The solution-method recommendation report " +
1621 "was generated for a different supply-chain " +
1626 private static void ValidateItemIdentifierCollection(
1627 IEnumerable<int> itemIds,
1628 string collectionDescription,
1629 ICollection<string> errors)
1631 int[] normalizedItemIds =
1634 if (normalizedItemIds.Any(
1639 $
"Every {collectionDescription} identifier " +
1640 "must be strictly positive.");
1643 int[] duplicateItemIds =
1659 if (duplicateItemIds.Length > 0)
1662 $
"Duplicate {collectionDescription} " +
1671 private static void ValidateStringCollection(
1672 IEnumerable<string> values,
1673 string valueDescription,
1674 ICollection<string> errors)
1676 string[] normalizedValues =
1679 if (normalizedValues.Any(
1681 string.IsNullOrWhiteSpace(value)))
1684 $
"Every {valueDescription} must be " +
1688 string[] duplicateValues =
1692 !
string.IsNullOrWhiteSpace(value))
1696 StringComparer.OrdinalIgnoreCase)
1706 StringComparer.OrdinalIgnoreCase)
1709 if (duplicateValues.Length > 0)
1712 $
"Duplicate {valueDescription} values: " +
1720 private static IReadOnlyList<string> NormalizeMessages(
1721 IEnumerable<string> messages)
1726 !
string.IsNullOrWhiteSpace(message))
1731 StringComparer.Ordinal)
1735 StringComparer.Ordinal)
1739 private static string DisplayProblemTypeCode(
1740 KnownProblemTypeMatch match)
1742 return string.IsNullOrWhiteSpace(