LotSizingDataModel.Instance 2.0.1
Lot-sizing instance representation, descriptors and problem characterization.
Loading...
Searching...
No Matches
LotSizingProblemClassifier.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using LotSizingDataModel.Core;
7
9
10/// <summary>
11/// Classifies lot-sizing supply-chain instances using a
12/// catalog of known problem-family definitions.
13/// </summary>
14/// <remarks>
15/// The classifier first extracts a factual feature profile
16/// from the supplied instance.
17///
18/// It then evaluates every enabled catalog definition and
19/// produces:
20/// <list type="bullet">
21/// <item>
22/// <description>exact known-family matches;</description>
23/// </item>
24/// <item>
25/// <description>known extensions;</description>
26/// </item>
27/// <item>
28/// <description>closest-known-family matches;</description>
29/// </item>
30/// <item>
31/// <description>a primary classification when one can be selected unambiguously.</description>
32/// </item>
33/// </list>
34///
35/// This standard classifier evaluates the complete problem.
36/// Specialized relaxation and subproblem recognizers may add
37/// further matches to the resulting classification later.
38/// </remarks>
39public static class LotSizingProblemClassifier
40{
41 /// <summary>
42 /// Gets the current version of the automatic
43 /// problem-classification algorithm.
44 /// </summary>
45 public const string CurrentVersion = "1.0";
46
47 private const double ScoreComparisonTolerance =
48 1e-12;
49
50 /// <summary>
51 /// Classifies a supply-chain instance using a known
52 /// problem-type catalog.
53 /// </summary>
54 /// <param name="supplyChain">
55 /// Supply-chain instance to classify.
56 /// </param>
57 /// <param name="catalog">
58 /// Catalog containing known problem families and
59 /// classification rules.
60 /// </param>
61 /// <param name="supplyChainFingerprint">
62 /// Optional fingerprint identifying the analyzed
63 /// supply-chain state.
64 /// </param>
65 /// <param name="numericalTolerance">
66 /// Non-negative finite tolerance used while extracting
67 /// numerical features.
68 /// </param>
69 /// <returns>
70 /// Persistent lot-sizing problem classification.
71 /// </returns>
72 /// <exception cref="ArgumentNullException">
73 /// Thrown when <paramref name="supplyChain"/> or
74 /// <paramref name="catalog"/> is
75 /// <see langword="null"/>.
76 /// </exception>
77 /// <exception cref="InvalidOperationException">
78 /// Thrown when the supplied catalog is structurally
79 /// invalid.
80 /// </exception>
82 SupplyChain supplyChain,
84 string supplyChainFingerprint = "",
85 double numericalTolerance =
87 .DefaultNumericalTolerance)
88 {
89 ArgumentNullException.ThrowIfNull(supplyChain);
90 ArgumentNullException.ThrowIfNull(catalog);
91
92 catalog.EnsureValid();
93
94 ProductStructureAnalysis productStructureAnalysis =
96 supplyChain);
97
100 supplyChain,
101 productStructureAnalysis,
102 numericalTolerance);
103
104 return ClassifyCore(
105 features,
106 catalog,
107 supplyChainFingerprint,
108 productStructureAnalysis.Warnings,
109 productStructureAnalysis.Errors);
110 }
111
112 /// <summary>
113 /// Classifies an existing lot-sizing problem-feature
114 /// profile using a known problem-type catalog.
115 /// </summary>
116 /// <param name="features">
117 /// Factual feature profile to classify.
118 /// </param>
119 /// <param name="catalog">
120 /// Catalog containing known problem families and
121 /// classification rules.
122 /// </param>
123 /// <param name="supplyChainFingerprint">
124 /// Optional fingerprint identifying the supply-chain
125 /// state from which the features were extracted.
126 /// </param>
127 /// <returns>
128 /// Persistent lot-sizing problem classification.
129 /// </returns>
130 /// <exception cref="ArgumentNullException">
131 /// Thrown when <paramref name="features"/> or
132 /// <paramref name="catalog"/> is
133 /// <see langword="null"/>.
134 /// </exception>
135 /// <exception cref="InvalidOperationException">
136 /// Thrown when the supplied catalog is structurally
137 /// invalid.
138 /// </exception>
142 string supplyChainFingerprint = "")
143 {
144 ArgumentNullException.ThrowIfNull(features);
145 ArgumentNullException.ThrowIfNull(catalog);
146
147 catalog.EnsureValid();
148
149 return ClassifyCore(
150 features,
151 catalog,
152 supplyChainFingerprint,
153 Array.Empty<string>(),
154 Array.Empty<string>());
155 }
156
157 private static LotSizingProblemClassification ClassifyCore(
160 string supplyChainFingerprint,
161 IEnumerable<string> initialWarnings,
162 IEnumerable<string> initialErrors)
163 {
164 var classification =
166 {
167 ClassifierVersion =
169
170 CatalogName =
171 catalog.CatalogName,
172
173 CatalogVersion =
174 catalog.CatalogVersion,
175
176 ClassifiedAtUtc =
177 DateTime.UtcNow,
178
179 SupplyChainFingerprint =
180 supplyChainFingerprint?.Trim() ??
181 string.Empty
182 };
183
184 var warnings =
185 NormalizeMessages(initialWarnings);
186
187 var errors =
188 NormalizeMessages(initialErrors);
189
190 if (!features.IsStructurallyUsable)
191 {
192 errors.Add(
193 "The extracted feature profile does not " +
194 "contain the minimum structural information " +
195 "required for problem classification.");
196 }
197
198 if (errors.Count > 0)
199 {
200 classification.ReplaceWarnings(warnings);
201 classification.ReplaceErrors(errors);
202
203 classification.Status =
205
206 return classification;
207 }
208
209 var evaluatedMatches =
210 new List<EvaluatedProblemTypeMatch>();
211
212 foreach (KnownProblemTypeDefinition definition
214 {
215 EvaluatedProblemTypeMatch? evaluatedMatch =
216 EvaluateDefinition(
217 definition,
218 catalog,
219 features,
220 errors);
221
222 if (evaluatedMatch is not null)
223 {
224 evaluatedMatches.Add(
225 evaluatedMatch);
226 }
227 }
228
229 if (errors.Count > 0)
230 {
231 classification.ReplaceWarnings(warnings);
232 classification.ReplaceErrors(errors);
233
234 classification.Status =
236
237 return classification;
238 }
239
240 KnownProblemTypeMatch[] orderedMatches =
241 evaluatedMatches
242 .Select(
243 evaluatedMatch =>
244 evaluatedMatch.Match)
245 .OrderBy(
246 match =>
247 GetMatchDisplayOrder(
248 match.MatchKind))
249 .ThenByDescending(
250 match =>
251 match.Score)
252 .ThenBy(
253 match =>
254 match.ProblemTypeCode,
255 StringComparer.OrdinalIgnoreCase)
256 .ToArray();
257
258 classification.ReplaceMatches(
259 orderedMatches);
260
261 SelectPrimaryMatchAndStatus(
262 classification,
263 evaluatedMatches);
264
265 classification.ReplaceWarnings(
266 warnings);
267
268 classification.ReplaceErrors(
269 errors);
270
271 return classification;
272 }
273
274 private static EvaluatedProblemTypeMatch?
275 EvaluateDefinition(
276 KnownProblemTypeDefinition definition,
277 KnownProblemTypeCatalog catalog,
278 LotSizingProblemFeatures features,
279 ICollection<string> errors)
280 {
281 var evidence =
282 new List<ClassificationEvidence>();
283
284 var additionalFeatureCodes =
285 new HashSet<string>(
286 StringComparer.OrdinalIgnoreCase);
287
288 bool evaluationFailed =
289 false;
290
291 EvaluateStandardRuleCategory(
292 definition.RequiredRuleCodes,
293 catalog,
294 features,
295 isRequired: true,
296 evidence,
297 errors,
298 ref evaluationFailed);
299
300 EvaluateStandardRuleCategory(
301 definition.OptionalRuleCodes,
302 catalog,
303 features,
304 isRequired: false,
305 evidence,
306 errors,
307 ref evaluationFailed);
308
309 EvaluateExtensionRules(
310 definition.ExtensionRuleCodes,
311 catalog,
312 features,
313 evidence,
314 additionalFeatureCodes,
315 errors,
316 ref evaluationFailed);
317
318 EvaluateExclusionRules(
319 definition.ExclusionRuleCodes,
320 catalog,
321 features,
322 evidence,
323 errors,
324 ref evaluationFailed);
325
326 if (evaluationFailed)
327 {
328 return null;
329 }
330
331 var match =
332 new KnownProblemTypeMatch(
333 problemTypeCode:
334 definition.Code,
335
336 problemTypeName:
337 definition.Name,
338
339 matchKind:
340 ProblemMatchKind.Unknown,
341
342 scope:
343 definition.DefaultScope)
344 {
345 DefinitionVersion =
346 definition.DefinitionVersion
347 };
348
349 match.ReplaceEvidence(
350 evidence);
351
352 match.ReplaceAdditionalFeatureCodes(
353 additionalFeatureCodes);
354
355 match.UpdateScoreFromEvidence();
356
357 if (!match.HasBlockingMismatches)
358 {
359 match.MatchKind =
360 match.HasAdditionalFeatures
361 ? ProblemMatchKind.KnownExtension
362 : ProblemMatchKind.Exact;
363
364 return new EvaluatedProblemTypeMatch(
365 definition,
366 match);
367 }
368
369 if (match.Score + ScoreComparisonTolerance <
370 definition.ClosestMatchThreshold)
371 {
372 return null;
373 }
374
375 match.MatchKind =
376 ProblemMatchKind.ClosestKnownFamily;
377
378 return new EvaluatedProblemTypeMatch(
379 definition,
380 match);
381 }
382
383 private static void EvaluateStandardRuleCategory(
384 IEnumerable<string> ruleCodes,
385 KnownProblemTypeCatalog catalog,
386 LotSizingProblemFeatures features,
387 bool isRequired,
388 ICollection<ClassificationEvidence> evidence,
389 ICollection<string> errors,
390 ref bool evaluationFailed)
391 {
392 foreach (string ruleCode in ruleCodes)
393 {
394 KnownProblemRuleDefinition? rule =
395 catalog.FindRule(ruleCode);
396
397 if (rule is null)
398 {
399 errors.Add(
400 $"Classification rule '{ruleCode}' " +
401 "could not be resolved.");
402
403 evaluationFailed = true;
404 continue;
405 }
406
407 bool succeeded =
408 KnownProblemRuleEvaluator.TryEvaluate(
409 rule,
410 features,
411 isRequired,
412 out ClassificationEvidence?
413 evaluatedEvidence,
414 out string errorMessage);
415
416 if (!succeeded ||
417 evaluatedEvidence is null)
418 {
419 errors.Add(
420 $"Rule '{ruleCode}' could not be " +
421 $"evaluated: {errorMessage}");
422
423 evaluationFailed = true;
424 continue;
425 }
426
427 evidence.Add(
428 evaluatedEvidence);
429 }
430 }
431
432 private static void EvaluateExtensionRules(
433 IEnumerable<string> ruleCodes,
434 KnownProblemTypeCatalog catalog,
435 LotSizingProblemFeatures features,
436 ICollection<ClassificationEvidence> evidence,
437 ISet<string> additionalFeatureCodes,
438 ICollection<string> errors,
439 ref bool evaluationFailed)
440 {
441 foreach (string ruleCode in ruleCodes)
442 {
443 KnownProblemRuleDefinition? rule =
444 catalog.FindRule(ruleCode);
445
446 if (rule is null)
447 {
448 errors.Add(
449 $"Extension rule '{ruleCode}' could not " +
450 "be resolved.");
451
452 evaluationFailed = true;
453 continue;
454 }
455
456 bool succeeded =
457 KnownProblemRuleEvaluator.TryEvaluate(
458 rule,
459 features,
460 isRequired: false,
461 out ClassificationEvidence?
462 extensionEvidence,
463 out string errorMessage);
464
465 if (!succeeded ||
466 extensionEvidence is null)
467 {
468 errors.Add(
469 $"Extension rule '{ruleCode}' could not " +
470 $"be evaluated: {errorMessage}");
471
472 evaluationFailed = true;
473 continue;
474 }
475
476 /*
477 * The absence of an extension must not reduce
478 * the similarity score of the classical family.
479 */
480 extensionEvidence.Weight = 0.0;
481
482 if (extensionEvidence.IsSatisfied)
483 {
484 additionalFeatureCodes.Add(
485 rule.FeatureCode);
486
487 extensionEvidence.Comment =
488 "The condition identifies an additional " +
489 "feature not included in the classical " +
490 "problem-family definition.";
491 }
492 else
493 {
494 extensionEvidence.Comment =
495 "The possible extension is not present " +
496 "in the analyzed instance.";
497 }
498
499 evidence.Add(
500 extensionEvidence);
501 }
502 }
503
504 private static void EvaluateExclusionRules(
505 IEnumerable<string> ruleCodes,
506 KnownProblemTypeCatalog catalog,
507 LotSizingProblemFeatures features,
508 ICollection<ClassificationEvidence> evidence,
509 ICollection<string> errors,
510 ref bool evaluationFailed)
511 {
512 foreach (string ruleCode in ruleCodes)
513 {
514 KnownProblemRuleDefinition? rule =
515 catalog.FindRule(ruleCode);
516
517 if (rule is null)
518 {
519 errors.Add(
520 $"Exclusion rule '{ruleCode}' could not " +
521 "be resolved.");
522
523 evaluationFailed = true;
524 continue;
525 }
526
527 bool succeeded =
528 KnownProblemRuleEvaluator.TryEvaluate(
529 rule,
530 features,
531 isRequired: true,
532 out ClassificationEvidence?
533 rawEvidence,
534 out string errorMessage);
535
536 if (!succeeded ||
537 rawEvidence is null)
538 {
539 errors.Add(
540 $"Exclusion rule '{ruleCode}' could not " +
541 $"be evaluated: {errorMessage}");
542
543 evaluationFailed = true;
544 continue;
545 }
546
547 /*
548 * An exclusion rule describes a condition whose
549 * presence contradicts the family.
550 *
551 * Therefore the evidence supports the match only
552 * when the exclusion condition is absent.
553 */
554 var exclusionEvidence =
555 new ClassificationEvidence(
556 featureCode:
557 rawEvidence.FeatureCode,
558
559 expectedValue:
560 "exclusion condition absent",
561
562 observedValue:
563 rawEvidence.IsSatisfied
564 ? "exclusion condition present"
565 : "exclusion condition absent",
566
567 isSatisfied:
568 !rawEvidence.IsSatisfied,
569
570 isRequired:
571 true,
572
573 description:
574 string.IsNullOrWhiteSpace(
575 rawEvidence.Description)
576 ? "The exclusion condition must " +
577 "be absent."
578 : rawEvidence.Description)
579 {
580 RuleCode =
581 rawEvidence.RuleCode,
582
583 Weight =
584 0.0,
585
586 Comment =
587 $"Evaluated exclusion condition: " +
588 $"expected '{rawEvidence.ExpectedValue}', " +
589 $"observed '{rawEvidence.ObservedValue}'."
590 };
591
592 evidence.Add(
593 exclusionEvidence);
594 }
595 }
596
597 private static void SelectPrimaryMatchAndStatus(
598 LotSizingProblemClassification classification,
599 IReadOnlyCollection<EvaluatedProblemTypeMatch>
600 evaluatedMatches)
601 {
602 EvaluatedProblemTypeMatch[] directCandidates =
603 evaluatedMatches
604 .Where(
605 evaluatedMatch =>
606 evaluatedMatch
607 .Definition
608 .CanBePrimaryMatch &&
609 evaluatedMatch
610 .Match
611 .IsDirectMatch &&
612 evaluatedMatch
613 .Match
614 .AppliesToCompleteProblem)
615 .OrderByDescending(
616 evaluatedMatch =>
617 GetDirectMatchQuality(
618 evaluatedMatch.Match))
619 .ThenByDescending(
620 evaluatedMatch =>
621 evaluatedMatch.Match.Score)
622 .ThenByDescending(
623 evaluatedMatch =>
624 evaluatedMatch
625 .Definition
626 .RequiredRuleCodes
627 .Count)
628 .ThenByDescending(
629 evaluatedMatch =>
630 evaluatedMatch
631 .Definition
632 .Priority)
633 .ThenBy(
634 evaluatedMatch =>
635 evaluatedMatch.Match
636 .ProblemTypeCode,
637 StringComparer.OrdinalIgnoreCase)
638 .ToArray();
639
640 if (directCandidates.Length == 0)
641 {
642 bool hasRecognizedPartialStructure =
643 classification.Matches.Any(
644 match =>
645 match.MatchKind ==
647 .RecognizedRelaxation ||
648 match.MatchKind ==
650 .RecognizedSubproblem);
651
652 classification.Status =
653 hasRecognizedPartialStructure
655 .PartiallyClassified
657 .Unclassified;
658
659 return;
660 }
661
662 EvaluatedProblemTypeMatch bestCandidate =
663 directCandidates[0];
664
665 EvaluatedProblemTypeMatch[] equivalentCandidates =
666 directCandidates
667 .Where(
668 candidate =>
669 AreEquivalentPrimaryCandidates(
670 bestCandidate,
671 candidate))
672 .ToArray();
673
674 if (equivalentCandidates.Length > 1)
675 {
676 classification.Status =
678
679 classification.Comment =
680 "Several known problem families have the " +
681 "same primary-selection rank: " +
682 string.Join(
683 ", ",
684 equivalentCandidates.Select(
685 candidate =>
686 candidate.Match
687 .ProblemTypeCode)) +
688 ".";
689
690 return;
691 }
692
693 classification.SetPrimaryMatch(
694 bestCandidate.Match);
695
696 classification.ReplaceUnclassifiedFeatureCodes(
697 bestCandidate.Match
698 .AdditionalFeatureCodes);
699
700 classification.Status =
701 bestCandidate.Match.MatchKind ==
702 ProblemMatchKind.Exact &&
703 !classification.HasUnclassifiedFeatures
704 ? ProblemClassificationStatus.Classified
706 .PartiallyClassified;
707 }
708
709 private static bool AreEquivalentPrimaryCandidates(
710 EvaluatedProblemTypeMatch first,
711 EvaluatedProblemTypeMatch second)
712 {
713 return
714 GetDirectMatchQuality(first.Match) ==
715 GetDirectMatchQuality(second.Match) &&
716
717 Math.Abs(
718 first.Match.Score -
719 second.Match.Score) <=
720 ScoreComparisonTolerance &&
721
722 first.Definition.RequiredRuleCodes.Count ==
723 second.Definition.RequiredRuleCodes.Count &&
724
725 first.Definition.Priority ==
726 second.Definition.Priority;
727 }
728
729 private static int GetDirectMatchQuality(
730 KnownProblemTypeMatch match)
731 {
732 return match.MatchKind switch
733 {
734 ProblemMatchKind.Exact =>
735 2,
736
737 ProblemMatchKind.KnownExtension =>
738 1,
739
740 _ =>
741 0
742 };
743 }
744
745 private static int GetMatchDisplayOrder(
746 ProblemMatchKind matchKind)
747 {
748 return matchKind switch
749 {
750 ProblemMatchKind.Exact =>
751 0,
752
753 ProblemMatchKind.KnownExtension =>
754 1,
755
757 .RecognizedSubproblem =>
758 2,
759
761 .RecognizedRelaxation =>
762 3,
763
765 .ClosestKnownFamily =>
766 4,
767
768 _ =>
769 5
770 };
771 }
772
773 private static List<string> NormalizeMessages(
774 IEnumerable<string> messages)
775 {
776 ArgumentNullException.ThrowIfNull(messages);
777
778 return messages
779 .Where(
780 message =>
781 !string.IsNullOrWhiteSpace(
782 message))
783 .Select(
784 message =>
785 message.Trim())
786 .Distinct(
787 StringComparer.Ordinal)
788 .OrderBy(
789 message =>
790 message,
791 StringComparer.Ordinal)
792 .ToList();
793 }
794
795 private sealed class EvaluatedProblemTypeMatch
796 {
797 public EvaluatedProblemTypeMatch(
798 KnownProblemTypeDefinition definition,
799 KnownProblemTypeMatch match)
800 {
801 Definition =
802 definition ??
803 throw new ArgumentNullException(
804 nameof(definition));
805
806 Match =
807 match ??
808 throw new ArgumentNullException(
809 nameof(match));
810 }
811
812 public KnownProblemTypeDefinition Definition
813 {
814 get;
815 }
816
817 public KnownProblemTypeMatch Match
818 {
819 get;
820 }
821 }
822}
Represents the detailed result of an automatic analysis of a product bill-of-materials graph.
IReadOnlyList< string > Errors
Gets the errors detected during the analysis.
IReadOnlyList< string > Warnings
Gets the non-fatal warnings detected during the analysis.
Analyzes the bill-of-materials graph of a supply-chain instance and determines its product-structure ...
static ProductStructureAnalysis Analyze(SupplyChain supplyChain)
Analyzes the product structure of a supply-chain instance.
Stores known lot-sizing problem-family definitions and the reusable rules used to recognize them.
string CatalogName
Gets or sets the human-readable name of the catalog.
IReadOnlyList< KnownProblemTypeDefinition > GetDefinitionsForClassification()
Returns the enabled and valid problem-family definitions in classification order.
string CatalogVersion
Gets or sets the version identifying the catalog contents and classification semantics.
void EnsureValid()
Validates the catalog and throws an exception when at least one structural error is detected.
Stores the persistent result of the automatic classification of a lot-sizing problem instance.
Classifies lot-sizing supply-chain instances using a catalog of known problem-family definitions.
static LotSizingProblemClassification Classify(SupplyChain supplyChain, KnownProblemTypeCatalog catalog, string supplyChainFingerprint="", double numericalTolerance=LotSizingProblemFeatureExtractor .DefaultNumericalTolerance)
Classifies a supply-chain instance using a known problem-type catalog.
const string CurrentVersion
Gets the current version of the automatic problem-classification algorithm.
static LotSizingProblemClassification Classify(LotSizingProblemFeatures features, KnownProblemTypeCatalog catalog, string supplyChainFingerprint="")
Classifies an existing lot-sizing problem-feature profile using a known problem-type catalog.
Extracts factual lot-sizing problem features from a supply-chain instance.
static LotSizingProblemFeatures Extract(SupplyChain supplyChain, double numericalTolerance=DefaultNumericalTolerance)
Extracts lot-sizing problem features and automatically analyzes the product structure.
Describes the factual structural and modeling features detected in a lot-sizing supply-chain instance...
bool IsStructurallyUsable
Gets a value indicating whether the extracted feature profile contains the minimum structural informa...
ProblemMatchKind
Identifies how a supply-chain instance matches a known lot-sizing problem family.
ProblemClassificationStatus
Indicates the current status of the automatic classification of a lot-sizing problem instance.