LotSizingDataModel.Instance 2.0.1
Lot-sizing instance representation, descriptors and problem characterization.
Loading...
Searching...
No Matches
SolutionMethodRecommendationReport.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Globalization;
4using System.Linq;
5using System.Xml.Serialization;
6using LotSizingDataModel.Core.Common;
7
9
10/// <summary>
11/// Stores the solution-method recommendations generated for
12/// one lot-sizing problem instance.
13/// </summary>
14/// <remarks>
15/// The report preserves:
16/// <list type="bullet">
17/// <item>
18/// <description>
19/// the advisor and catalog versions used during evaluation;
20/// </description>
21/// </item>
22/// <item>
23/// <description>
24/// the fingerprint of the evaluated supply chain;
25/// </description>
26/// </item>
27/// <item>
28/// <description>
29/// the recommendation score threshold;
30/// </description>
31/// </item>
32/// <item>
33/// <description>
34/// the ranked solution-method recommendations.
35/// </description>
36/// </item>
37/// </list>
38///
39/// A report becomes potentially outdated when the current
40/// supply-chain fingerprint differs from
41/// <see cref="SupplyChainFingerprint"/>.
42/// </remarks>
43[Serializable]
44[XmlType(TypeName = "solutionMethodRecommendationReport")]
46 ModelObject
47{
48 private string _advisorVersion =
49 string.Empty;
50
51 private string _catalogName =
52 string.Empty;
53
54 private string _catalogVersion =
55 string.Empty;
56
57 private string _supplyChainFingerprint =
58 string.Empty;
59
60 private DateTime? _generatedAtUtc;
61
62 private double _recommendedScoreThreshold =
65
66 private string _comment =
67 string.Empty;
68
69 /// <summary>
70 /// Initializes an empty solution-method recommendation
71 /// report.
72 /// </summary>
73 /// <remarks>
74 /// This constructor is required for XML serialization.
75 /// </remarks>
79
80 /// <summary>
81 /// Initializes a solution-method recommendation report.
82 /// </summary>
83 /// <param name="advisorVersion">
84 /// Version of the advisor used to produce the report.
85 /// </param>
86 /// <param name="catalogName">
87 /// Name of the solution-method catalog.
88 /// </param>
89 /// <param name="catalogVersion">
90 /// Version of the solution-method catalog.
91 /// </param>
92 /// <param name="supplyChainFingerprint">
93 /// Fingerprint of the evaluated supply chain.
94 /// </param>
95 /// <param name="recommendedScoreThreshold">
96 /// Score threshold used to mark directly compatible
97 /// methods as recommended.
98 /// </param>
99 /// <exception cref="ArgumentException">
100 /// Thrown when a required textual value is empty.
101 /// </exception>
102 /// <exception cref="ArgumentOutOfRangeException">
103 /// Thrown when
104 /// <paramref name="recommendedScoreThreshold"/> does not
105 /// lie between zero and one.
106 /// </exception>
108 string advisorVersion,
109 string catalogName,
110 string catalogVersion,
111 string supplyChainFingerprint,
112 double recommendedScoreThreshold =
114 .DefaultRecommendedScoreThreshold)
115 {
116 if (string.IsNullOrWhiteSpace(advisorVersion))
117 {
118 throw new ArgumentException(
119 "An advisor version is required.",
120 nameof(advisorVersion));
121 }
122
123 if (string.IsNullOrWhiteSpace(catalogName))
124 {
125 throw new ArgumentException(
126 "A solution-method catalog name is required.",
127 nameof(catalogName));
128 }
129
130 if (string.IsNullOrWhiteSpace(catalogVersion))
131 {
132 throw new ArgumentException(
133 "A solution-method catalog version is " +
134 "required.",
135 nameof(catalogVersion));
136 }
137
138 if (string.IsNullOrWhiteSpace(
139 supplyChainFingerprint))
140 {
141 throw new ArgumentException(
142 "A supply-chain fingerprint is required.",
143 nameof(supplyChainFingerprint));
144 }
145
147 advisorVersion;
148
150 catalogName;
151
153 catalogVersion;
154
156 supplyChainFingerprint;
157
159 recommendedScoreThreshold;
160
162 DateTime.UtcNow;
163 }
164
165 /// <summary>
166 /// Gets or sets the version of the advisor used to produce
167 /// the recommendations.
168 /// </summary>
169 [XmlAttribute("advisorVersion")]
170 public string AdvisorVersion
171 {
172 get => _advisorVersion;
173 set
174 {
175 if (SetProperty(
176 ref _advisorVersion,
177 value?.Trim() ?? string.Empty))
178 {
179 OnPropertyChanged(
180 nameof(HasAdvisorVersion));
181
182 NotifyValidityProperties();
183 }
184 }
185 }
186
187 /// <summary>
188 /// Gets or sets the name of the solution-method catalog
189 /// used during evaluation.
190 /// </summary>
191 [XmlAttribute("catalogName")]
192 public string CatalogName
193 {
194 get => _catalogName;
195 set
196 {
197 if (SetProperty(
198 ref _catalogName,
199 value?.Trim() ?? string.Empty))
200 {
201 OnPropertyChanged(
202 nameof(HasCatalogInformation));
203
204 NotifyValidityProperties();
205 }
206 }
207 }
208
209 /// <summary>
210 /// Gets or sets the version of the solution-method catalog
211 /// used during evaluation.
212 /// </summary>
213 [XmlAttribute("catalogVersion")]
214 public string CatalogVersion
215 {
216 get => _catalogVersion;
217 set
218 {
219 if (SetProperty(
220 ref _catalogVersion,
221 value?.Trim() ?? string.Empty))
222 {
223 OnPropertyChanged(
224 nameof(HasCatalogInformation));
225
226 NotifyValidityProperties();
227 }
228 }
229 }
230
231 /// <summary>
232 /// Gets or sets the fingerprint of the supply chain that
233 /// was evaluated.
234 /// </summary>
235 [XmlAttribute("supplyChainFingerprint")]
237 {
238 get => _supplyChainFingerprint;
239 set
240 {
241 if (SetProperty(
242 ref _supplyChainFingerprint,
243 value?.Trim() ?? string.Empty))
244 {
245 OnPropertyChanged(
247
248 NotifyValidityProperties();
249 }
250 }
251 }
252
253 /// <summary>
254 /// Gets or sets the UTC date and time at which the report
255 /// was generated.
256 /// </summary>
257 [XmlElement("generatedAtUtc", IsNullable = true)]
258 public DateTime? GeneratedAtUtc
259 {
260 get => _generatedAtUtc;
261 set
262 {
263 DateTime? normalizedValue =
264 value.HasValue
265 ? ConvertToUtc(value.Value)
266 : null;
267
268 if (SetProperty(
269 ref _generatedAtUtc,
270 normalizedValue))
271 {
272 OnPropertyChanged(
273 nameof(HasGenerationDate));
274
275 OnPropertyChanged(
276 nameof(HasBeenGenerated));
277
278 NotifyValidityProperties();
279 }
280 }
281 }
282
283 /// <summary>
284 /// Gets or sets the score threshold used to mark a
285 /// directly compatible method as recommended.
286 /// </summary>
287 /// <exception cref="ArgumentOutOfRangeException">
288 /// Thrown when the supplied value is not finite or does
289 /// not lie between zero and one.
290 /// </exception>
291 [XmlAttribute("recommendedScoreThreshold")]
293 {
294 get => _recommendedScoreThreshold;
295 set
296 {
297 if (!double.IsFinite(value) ||
298 value < 0.0 ||
299 value > 1.0)
300 {
301 throw new ArgumentOutOfRangeException(
302 nameof(value),
303 value,
304 "The recommended-score threshold must " +
305 "be finite and lie between zero and one.");
306 }
307
308 if (SetProperty(
309 ref _recommendedScoreThreshold,
310 value))
311 {
312 NotifyValidityProperties();
313 }
314 }
315 }
316
317 /// <summary>
318 /// Gets the ranked solution-method recommendations.
319 /// </summary>
320 [XmlArray("recommendations")]
321 [XmlArrayItem("recommendation")]
322 public List<SolutionMethodRecommendation>
324 { get; } =
325 new();
326
327 /// <summary>
328 /// Gets or sets an optional explanatory comment.
329 /// </summary>
330 [XmlElement("comment")]
331 public string Comment
332 {
333 get => _comment;
334 set
335 {
336 if (SetProperty(
337 ref _comment,
338 value?.Trim() ?? string.Empty))
339 {
340 OnPropertyChanged(
341 nameof(HasComment));
342 }
343 }
344 }
345
346 /// <summary>
347 /// Gets a value indicating whether an advisor version has
348 /// been recorded.
349 /// </summary>
350 [XmlIgnore]
351 public bool HasAdvisorVersion =>
352 !string.IsNullOrWhiteSpace(
354
355 /// <summary>
356 /// Gets a value indicating whether complete catalog
357 /// information has been recorded.
358 /// </summary>
359 [XmlIgnore]
361 !string.IsNullOrWhiteSpace(
362 CatalogName) &&
363 !string.IsNullOrWhiteSpace(
365
366 /// <summary>
367 /// Gets a value indicating whether a supply-chain
368 /// fingerprint has been recorded.
369 /// </summary>
370 [XmlIgnore]
372 !string.IsNullOrWhiteSpace(
374
375 /// <summary>
376 /// Gets a value indicating whether a generation date has
377 /// been recorded.
378 /// </summary>
379 [XmlIgnore]
380 public bool HasGenerationDate =>
381 GeneratedAtUtc.HasValue;
382
383 /// <summary>
384 /// Gets a value indicating whether this report has been
385 /// generated.
386 /// </summary>
387 [XmlIgnore]
388 public bool HasBeenGenerated =>
393
394 /// <summary>
395 /// Gets a value indicating whether at least one
396 /// recommendation has been recorded.
397 /// </summary>
398 [XmlIgnore]
399 public bool HasRecommendations =>
400 Recommendations.Count > 0;
401
402 /// <summary>
403 /// Gets the total number of recommendations.
404 /// </summary>
405 [XmlIgnore]
407 Recommendations.Count;
408
409 /// <summary>
410 /// Gets the number of evaluated recommendations.
411 /// </summary>
412 [XmlIgnore]
414 Recommendations.Count(
415 recommendation =>
416 recommendation is not null &&
417 recommendation.HasBeenEvaluated);
418
419 /// <summary>
420 /// Gets the number of methods marked as recommended.
421 /// </summary>
422 [XmlIgnore]
424 Recommendations.Count(
425 recommendation =>
426 recommendation is not null &&
427 recommendation.IsRecommended);
428
429 /// <summary>
430 /// Gets the number of directly compatible methods,
431 /// including methods marked as recommended.
432 /// </summary>
433 [XmlIgnore]
435 Recommendations.Count(
436 recommendation =>
437 recommendation is not null &&
438 recommendation.IsCompatible);
439
440 /// <summary>
441 /// Gets the number of partially compatible methods.
442 /// </summary>
443 [XmlIgnore]
445 Recommendations.Count(
446 recommendation =>
447 recommendation is not null &&
448 recommendation.IsPartiallyCompatible);
449
450 /// <summary>
451 /// Gets the number of incompatible methods.
452 /// </summary>
453 [XmlIgnore]
455 Recommendations.Count(
456 recommendation =>
457 recommendation is not null &&
458 recommendation.IsIncompatible);
459
460 /// <summary>
461 /// Gets a value indicating whether at least one directly
462 /// compatible method exists.
463 /// </summary>
464 [XmlIgnore]
467
468 /// <summary>
469 /// Gets a value indicating whether at least one method is
470 /// directly recommended.
471 /// </summary>
472 [XmlIgnore]
473 public bool HasRecommendedMethod =>
475
476 /// <summary>
477 /// Gets a value indicating whether at least one method can
478 /// be applied directly or after adaptation.
479 /// </summary>
480 [XmlIgnore]
481 public bool HasApplicableMethod =>
484
485 /// <summary>
486 /// Gets the best applicable recommendation.
487 /// </summary>
488 /// <remarks>
489 /// Directly recommended and compatible methods are
490 /// preferred to partially compatible methods.
491 ///
492 /// Incompatible and non-evaluated methods are never
493 /// returned by this property.
494 /// </remarks>
495 [XmlIgnore]
499 .Where(
500 recommendation =>
501 recommendation is not null &&
502 (
503 recommendation.IsCompatible ||
504 recommendation
505 .IsPartiallyCompatible
506 ))
507 .OrderBy(
508 recommendation =>
509 recommendation.Rank ??
510 int.MaxValue)
511 .ThenByDescending(
512 recommendation =>
513 recommendation
514 .CompatibilityLevel)
515 .ThenByDescending(
516 recommendation =>
517 recommendation.Score)
518 .ThenBy(
519 recommendation =>
520 recommendation.MethodCode,
521 StringComparer.OrdinalIgnoreCase)
522 .FirstOrDefault();
523
524 /// <summary>
525 /// Gets a value indicating whether a best applicable
526 /// recommendation exists.
527 /// </summary>
528 [XmlIgnore]
530 BestRecommendation is not null;
531
532 /// <summary>
533 /// Gets a value indicating whether an explanatory comment
534 /// has been recorded.
535 /// </summary>
536 [XmlIgnore]
537 public bool HasComment =>
538 !string.IsNullOrWhiteSpace(
539 Comment);
540
541 /// <summary>
542 /// Gets a value indicating whether the report is
543 /// structurally valid.
544 /// </summary>
545 [XmlIgnore]
546 public bool IsValidReport =>
547 Validate().Count == 0;
548
549 /// <summary>
550 /// Adds a recommendation to the report.
551 /// </summary>
552 /// <param name="recommendation">
553 /// Recommendation to add.
554 /// </param>
555 /// <exception cref="ArgumentNullException">
556 /// Thrown when <paramref name="recommendation"/> is
557 /// <see langword="null"/>.
558 /// </exception>
559 /// <exception cref="ArgumentException">
560 /// Thrown when the recommendation is invalid or another
561 /// recommendation uses the same method code.
562 /// </exception>
563 public void AddRecommendation(
564 SolutionMethodRecommendation recommendation)
565 {
566 ArgumentNullException.ThrowIfNull(
567 recommendation);
568
569 if (!recommendation.IsValidRecommendation)
570 {
571 throw new ArgumentException(
572 "The solution-method recommendation is " +
573 "invalid.",
574 nameof(recommendation));
575 }
576
578 recommendation.MethodCode))
579 {
580 throw new ArgumentException(
581 $"A recommendation already exists for method " +
582 $"'{recommendation.MethodCode}'.",
583 nameof(recommendation));
584 }
585
586 Recommendations.Add(
587 recommendation);
588
589 RefreshRanks();
590
591 NotifyRecommendationProperties();
592 }
593
594 /// <summary>
595 /// Replaces all recommendations contained in the report.
596 /// </summary>
597 /// <param name="recommendations">
598 /// New recommendation collection.
599 /// </param>
600 /// <exception cref="ArgumentNullException">
601 /// Thrown when <paramref name="recommendations"/> is
602 /// <see langword="null"/>.
603 /// </exception>
604 /// <exception cref="ArgumentException">
605 /// Thrown when the collection contains a null or invalid
606 /// recommendation, or duplicate method codes.
607 /// </exception>
609 IEnumerable<SolutionMethodRecommendation>
610 recommendations)
611 {
612 ArgumentNullException.ThrowIfNull(
613 recommendations);
614
615 SolutionMethodRecommendation[] materialized =
616 recommendations.ToArray();
617
618 if (materialized.Any(
619 recommendation =>
620 recommendation is null))
621 {
622 throw new ArgumentException(
623 "The recommendation collection cannot " +
624 "contain a null element.",
625 nameof(recommendations));
626 }
627
628 SolutionMethodRecommendation? invalidRecommendation =
629 materialized.FirstOrDefault(
630 recommendation =>
631 !recommendation
632 .IsValidRecommendation);
633
634 if (invalidRecommendation is not null)
635 {
636 throw new ArgumentException(
637 $"Recommendation for method " +
638 $"'{invalidRecommendation.MethodCode}' is " +
639 "invalid.",
640 nameof(recommendations));
641 }
642
643 string[] duplicateMethodCodes =
644 materialized
645 .GroupBy(
646 recommendation =>
647 recommendation.MethodCode,
648 StringComparer.OrdinalIgnoreCase)
649 .Where(
650 group =>
651 group.Count() > 1)
652 .Select(
653 group =>
654 group.Key)
655 .OrderBy(
656 methodCode =>
657 methodCode,
658 StringComparer.OrdinalIgnoreCase)
659 .ToArray();
660
661 if (duplicateMethodCodes.Length > 0)
662 {
663 throw new ArgumentException(
664 "Duplicate recommendation method codes: " +
665 string.Join(
666 ", ",
667 duplicateMethodCodes) +
668 ".",
669 nameof(recommendations));
670 }
671
672 Recommendations.Clear();
673
674 Recommendations.AddRange(
675 materialized);
676
677 RefreshRanks();
678
679 NotifyRecommendationProperties();
680 }
681
682 /// <summary>
683 /// Finds the recommendation associated with a method code.
684 /// </summary>
685 /// <param name="methodCode">
686 /// Method code to search for.
687 /// </param>
688 /// <returns>
689 /// Matching recommendation, or <see langword="null"/> when
690 /// no recommendation uses the supplied code.
691 /// </returns>
692 /// <exception cref="InvalidOperationException">
693 /// Thrown when several recommendations use the same
694 /// method code.
695 /// </exception>
697 string methodCode)
698 {
699 if (string.IsNullOrWhiteSpace(methodCode))
700 {
701 return null;
702 }
703
704 string normalizedMethodCode =
705 methodCode.Trim();
706
709 .Where(
710 recommendation =>
711 recommendation is not null &&
712 string.Equals(
713 recommendation.MethodCode,
714 normalizedMethodCode,
715 StringComparison.OrdinalIgnoreCase))
716 .Take(2)
717 .ToArray();
718
719 return matches.Length switch
720 {
721 0 =>
722 null,
723
724 1 =>
725 matches[0],
726
727 _ =>
728 throw new InvalidOperationException(
729 $"Method code '{methodCode}' is " +
730 "ambiguous in the recommendation report.")
731 };
732 }
733
734 /// <summary>
735 /// Determines whether a recommendation exists for a
736 /// method code.
737 /// </summary>
738 /// <param name="methodCode">
739 /// Method code to search for.
740 /// </param>
741 /// <returns>
742 /// <see langword="true"/> when a matching recommendation
743 /// exists; otherwise, <see langword="false"/>.
744 /// </returns>
746 string methodCode)
747 {
748 return FindRecommendation(
749 methodCode) is not null;
750 }
751
752 /// <summary>
753 /// Removes a recommendation from the report.
754 /// </summary>
755 /// <param name="methodCode">
756 /// Code of the method whose recommendation must be
757 /// removed.
758 /// </param>
759 /// <returns>
760 /// <see langword="true"/> when a recommendation was
761 /// removed; otherwise, <see langword="false"/>.
762 /// </returns>
764 string methodCode)
765 {
766 SolutionMethodRecommendation? recommendation =
768 methodCode);
769
770 if (recommendation is null)
771 {
772 return false;
773 }
774
775 bool removed =
776 Recommendations.Remove(
777 recommendation);
778
779 if (removed)
780 {
781 RefreshRanks();
782
783 NotifyRecommendationProperties();
784 }
785
786 return removed;
787 }
788
789 /// <summary>
790 /// Removes every recommendation while preserving the
791 /// report-generation metadata.
792 /// </summary>
794 {
795 if (Recommendations.Count == 0)
796 {
797 return;
798 }
799
800 Recommendations.Clear();
801
802 NotifyRecommendationProperties();
803 }
804
805 /// <summary>
806 /// Clears the recommendations and all generation metadata.
807 /// </summary>
808 public void ClearReport()
809 {
810 Recommendations.Clear();
811
813 string.Empty;
814
816 string.Empty;
817
819 string.Empty;
820
822 string.Empty;
823
825 null;
826
830
831 Comment =
832 string.Empty;
833
834 NotifyRecommendationProperties();
835 }
836
837 /// <summary>
838 /// Reorders the recommendations and assigns sequential
839 /// ranks starting at one.
840 /// </summary>
841 public void RefreshRanks()
842 {
845 .Where(
846 recommendation =>
847 recommendation is not null)
848 .OrderByDescending(
849 recommendation =>
850 recommendation
851 .CompatibilityLevel)
852 .ThenByDescending(
853 recommendation =>
854 recommendation.Score)
855 .ThenBy(
856 recommendation =>
857 recommendation.MethodName,
858 StringComparer.OrdinalIgnoreCase)
859 .ThenBy(
860 recommendation =>
861 recommendation.MethodCode,
862 StringComparer.OrdinalIgnoreCase)
863 .ToArray();
864
865 Recommendations.Clear();
866
867 for (int index = 0;
868 index < ordered.Length;
869 index++)
870 {
871 ordered[index].Rank =
872 index + 1;
873
874 Recommendations.Add(
875 ordered[index]);
876 }
877
878 NotifyRecommendationProperties();
879 }
880
881 /// <summary>
882 /// Determines whether the report was generated for the
883 /// supplied supply-chain fingerprint.
884 /// </summary>
885 /// <param name="currentFingerprint">
886 /// Current supply-chain fingerprint.
887 /// </param>
888 /// <returns>
889 /// <see langword="true"/> when both fingerprints are
890 /// present and equal; otherwise, <see langword="false"/>.
891 /// </returns>
893 string currentFingerprint)
894 {
896 string.IsNullOrWhiteSpace(
897 currentFingerprint))
898 {
899 return false;
900 }
901
902 return string.Equals(
904 currentFingerprint.Trim(),
905 StringComparison.Ordinal);
906 }
907
908 /// <summary>
909 /// Validates the structural consistency of the report.
910 /// </summary>
911 /// <returns>
912 /// Ordered validation-error collection. An empty
913 /// collection indicates that the report is valid.
914 /// </returns>
915 public IReadOnlyList<string> Validate()
916 {
917 var errors =
918 new List<string>();
919
920 if (HasRecommendations ||
922 {
924 {
925 errors.Add(
926 "The recommendation report advisor " +
927 "version is missing.");
928 }
929
931 {
932 errors.Add(
933 "The recommendation report catalog " +
934 "information is incomplete.");
935 }
936
938 {
939 errors.Add(
940 "The recommendation report supply-chain " +
941 "fingerprint is missing.");
942 }
943
945 {
946 errors.Add(
947 "The recommendation report generation " +
948 "date is missing.");
949 }
950 }
951
952 ValidateRecommendations(
953 errors);
954
955 ValidateMethodCodeUniqueness(
956 errors);
957
958 ValidateRanks(
959 errors);
960
961 ValidateRecommendationMetadata(
962 errors);
963
964 return errors
965 .Where(
966 error =>
967 !string.IsNullOrWhiteSpace(error))
968 .Select(
969 error =>
970 error.Trim())
971 .Distinct(
972 StringComparer.Ordinal)
973 .OrderBy(
974 error =>
975 error,
976 StringComparer.Ordinal)
977 .ToArray();
978 }
979
980 /// <summary>
981 /// Validates the report and throws an exception when at
982 /// least one error is found.
983 /// </summary>
984 /// <exception cref="InvalidOperationException">
985 /// Thrown when the report is invalid.
986 /// </exception>
987 public void EnsureValid()
988 {
989 IReadOnlyList<string> errors =
990 Validate();
991
992 if (errors.Count == 0)
993 {
994 return;
995 }
996
997 throw new InvalidOperationException(
998 "The solution-method recommendation report is " +
999 "invalid:" +
1000 Environment.NewLine +
1001 string.Join(
1002 Environment.NewLine,
1003 errors.Select(
1004 error =>
1005 "- " + error)));
1006 }
1007
1008 /// <inheritdoc/>
1009 public override string ToString()
1010 {
1011 string generationDescription =
1012 GeneratedAtUtc is DateTime generatedAtUtc
1013 ? generatedAtUtc.ToString(
1014 "O",
1015 CultureInfo.InvariantCulture)
1016 : "not generated";
1017
1018 return
1019 $"{RecommendationCount} recommendation(s); " +
1020 $"{RecommendedMethodCount} recommended; " +
1021 $"{CompatibleMethodCount} compatible; " +
1022 $"{generationDescription}";
1023 }
1024
1025 private void ValidateRecommendations(
1026 ICollection<string> errors)
1027 {
1028 for (int index = 0;
1029 index < Recommendations.Count;
1030 index++)
1031 {
1032 SolutionMethodRecommendation? recommendation =
1033 Recommendations[index];
1034
1035 if (recommendation is null)
1036 {
1037 errors.Add(
1038 $"Recommendation at index {index} is " +
1039 "null.");
1040
1041 continue;
1042 }
1043
1044 if (!recommendation.IsValidRecommendation)
1045 {
1046 errors.Add(
1047 $"Recommendation for method " +
1048 $"'{DisplayMethodCode(recommendation)}' " +
1049 "is invalid.");
1050 }
1051 }
1052 }
1053
1054 private void ValidateMethodCodeUniqueness(
1055 ICollection<string> errors)
1056 {
1057 string[] duplicateMethodCodes =
1059 .Where(
1060 recommendation =>
1061 recommendation is not null &&
1062 recommendation.HasMethodCode)
1063 .GroupBy(
1064 recommendation =>
1065 recommendation.MethodCode,
1066 StringComparer.OrdinalIgnoreCase)
1067 .Where(
1068 group =>
1069 group.Count() > 1)
1070 .Select(
1071 group =>
1072 group.Key)
1073 .OrderBy(
1074 methodCode =>
1075 methodCode,
1076 StringComparer.OrdinalIgnoreCase)
1077 .ToArray();
1078
1079 if (duplicateMethodCodes.Length > 0)
1080 {
1081 errors.Add(
1082 "Duplicate recommendation method codes: " +
1083 string.Join(
1084 ", ",
1085 duplicateMethodCodes) +
1086 ".");
1087 }
1088 }
1089
1090 private void ValidateRanks(
1091 ICollection<string> errors)
1092 {
1093 SolutionMethodRecommendation[] rankedRecommendations =
1095 .Where(
1096 recommendation =>
1097 recommendation is not null &&
1098 recommendation.Rank.HasValue)
1099 .ToArray();
1100
1101 int[] duplicateRanks =
1102 rankedRecommendations
1103 .GroupBy(
1104 recommendation =>
1105 recommendation.Rank!.Value)
1106 .Where(
1107 group =>
1108 group.Count() > 1)
1109 .Select(
1110 group =>
1111 group.Key)
1112 .OrderBy(
1113 rank =>
1114 rank)
1115 .ToArray();
1116
1117 if (duplicateRanks.Length > 0)
1118 {
1119 errors.Add(
1120 "Duplicate recommendation ranks: " +
1121 string.Join(
1122 ", ",
1123 duplicateRanks) +
1124 ".");
1125 }
1126
1127 if (rankedRecommendations.Length > 0 &&
1128 rankedRecommendations.Length !=
1129 Recommendations.Count)
1130 {
1131 errors.Add(
1132 "Recommendation ranks are only partially " +
1133 "defined.");
1134 }
1135 }
1136
1137 private void ValidateRecommendationMetadata(
1138 ICollection<string> errors)
1139 {
1140 foreach (SolutionMethodRecommendation recommendation
1141 in Recommendations.Where(
1142 recommendation =>
1143 recommendation is not null))
1144 {
1145 if (recommendation
1148 !string.Equals(
1149 recommendation
1152 StringComparison.Ordinal))
1153 {
1154 errors.Add(
1155 $"Recommendation for method " +
1156 $"'{DisplayMethodCode(recommendation)}' " +
1157 "uses a different supply-chain " +
1158 "fingerprint.");
1159 }
1160
1161 if (recommendation
1164 !string.Equals(
1165 recommendation.AdvisorVersion,
1167 StringComparison.Ordinal))
1168 {
1169 errors.Add(
1170 $"Recommendation for method " +
1171 $"'{DisplayMethodCode(recommendation)}' " +
1172 "uses a different advisor version.");
1173 }
1174
1175 if (recommendation
1176 .HasMethodCatalogInformation &&
1178 (
1179 !string.Equals(
1180 recommendation.MethodCatalogName,
1182 StringComparison.Ordinal) ||
1183 !string.Equals(
1184 recommendation.MethodCatalogVersion,
1186 StringComparison.Ordinal)
1187 ))
1188 {
1189 errors.Add(
1190 $"Recommendation for method " +
1191 $"'{DisplayMethodCode(recommendation)}' " +
1192 "uses different catalog information.");
1193 }
1194 }
1195 }
1196
1197 private static string DisplayMethodCode(
1198 SolutionMethodRecommendation recommendation)
1199 {
1200 return recommendation.HasMethodCode
1201 ? recommendation.MethodCode
1202 : "<missing code>";
1203 }
1204
1205 private static DateTime ConvertToUtc(
1206 DateTime value)
1207 {
1208 return value.Kind switch
1209 {
1210 DateTimeKind.Utc =>
1211 value,
1212
1213 DateTimeKind.Local =>
1214 value.ToUniversalTime(),
1215
1216 _ =>
1217 DateTime.SpecifyKind(
1218 value,
1219 DateTimeKind.Utc)
1220 };
1221 }
1222
1223 private void NotifyRecommendationProperties()
1224 {
1225 OnPropertyChanged(
1226 nameof(Recommendations));
1227
1228 OnPropertyChanged(
1229 nameof(HasRecommendations));
1230
1231 OnPropertyChanged(
1232 nameof(RecommendationCount));
1233
1234 OnPropertyChanged(
1236
1237 OnPropertyChanged(
1238 nameof(RecommendedMethodCount));
1239
1240 OnPropertyChanged(
1241 nameof(CompatibleMethodCount));
1242
1243 OnPropertyChanged(
1245
1246 OnPropertyChanged(
1247 nameof(IncompatibleMethodCount));
1248
1249 OnPropertyChanged(
1251
1252 OnPropertyChanged(
1253 nameof(HasRecommendedMethod));
1254
1255 OnPropertyChanged(
1256 nameof(HasApplicableMethod));
1257
1258 OnPropertyChanged(
1259 nameof(BestRecommendation));
1260
1261 OnPropertyChanged(
1262 nameof(HasBestRecommendation));
1263
1264 NotifyValidityProperties();
1265 }
1266
1267 private void NotifyValidityProperties()
1268 {
1269 OnPropertyChanged(
1270 nameof(IsValidReport));
1271 }
1272}
Evaluates and ranks solution methods for lot-sizing problem instances.
const double DefaultRecommendedScoreThreshold
Gets the default score above which a directly compatible method may be marked as recommended.
Represents the evaluated compatibility of one solution method with a specific lot-sizing problem inst...
bool HasMethodCode
Gets a value indicating whether a stable method code has been recorded.
bool IsValidRecommendation
Gets a value indicating whether the recommendation contains the minimum coherent information required...
string MethodCode
Gets or sets the stable code identifying the evaluated solution method.
void EnsureValid()
Validates the report and throws an exception when at least one error is found.
void ClearRecommendations()
Removes every recommendation while preserving the report-generation metadata.
bool HasCatalogInformation
Gets a value indicating whether complete catalog information has been recorded.
bool HasComment
Gets a value indicating whether an explanatory comment has been recorded.
SolutionMethodRecommendation? BestRecommendation
Gets the best applicable recommendation.
DateTime? GeneratedAtUtc
Gets or sets the UTC date and time at which the report was generated.
bool HasBeenGenerated
Gets a value indicating whether this report has been generated.
bool HasRecommendedMethod
Gets a value indicating whether at least one method is directly recommended.
SolutionMethodRecommendationReport(string advisorVersion, string catalogName, string catalogVersion, string supplyChainFingerprint, double recommendedScoreThreshold=SolutionMethodAdvisor .DefaultRecommendedScoreThreshold)
Initializes a solution-method recommendation report.
double RecommendedScoreThreshold
Gets or sets the score threshold used to mark a directly compatible method as recommended.
bool RemoveRecommendation(string methodCode)
Removes a recommendation from the report.
bool HasBestRecommendation
Gets a value indicating whether a best applicable recommendation exists.
List< SolutionMethodRecommendation > Recommendations
Gets the ranked solution-method recommendations.
bool HasAdvisorVersion
Gets a value indicating whether an advisor version has been recorded.
bool HasDirectlyCompatibleMethod
Gets a value indicating whether at least one directly compatible method exists.
bool HasSupplyChainFingerprint
Gets a value indicating whether a supply-chain fingerprint has been recorded.
int CompatibleMethodCount
Gets the number of directly compatible methods, including methods marked as recommended.
void AddRecommendation(SolutionMethodRecommendation recommendation)
Adds a recommendation to the report.
bool HasRecommendations
Gets a value indicating whether at least one recommendation has been recorded.
bool HasGenerationDate
Gets a value indicating whether a generation date has been recorded.
string CatalogName
Gets or sets the name of the solution-method catalog used during evaluation.
string AdvisorVersion
Gets or sets the version of the advisor used to produce the recommendations.
bool MatchesSupplyChainFingerprint(string currentFingerprint)
Determines whether the report was generated for the supplied supply-chain fingerprint.
bool ContainsMethodCode(string methodCode)
Determines whether a recommendation exists for a method code.
void ReplaceRecommendations(IEnumerable< SolutionMethodRecommendation > recommendations)
Replaces all recommendations contained in the report.
bool HasApplicableMethod
Gets a value indicating whether at least one method can be applied directly or after adaptation.
bool IsValidReport
Gets a value indicating whether the report is structurally valid.
void RefreshRanks()
Reorders the recommendations and assigns sequential ranks starting at one.
string CatalogVersion
Gets or sets the version of the solution-method catalog used during evaluation.
SolutionMethodRecommendationReport()
Initializes an empty solution-method recommendation report.
SolutionMethodRecommendation? FindRecommendation(string methodCode)
Finds the recommendation associated with a method code.
IReadOnlyList< string > Validate()
Validates the structural consistency of the report.
string SupplyChainFingerprint
Gets or sets the fingerprint of the supply chain that was evaluated.