LotSizingDataModel.Instance 2.0.1
Lot-sizing instance representation, descriptors and problem characterization.
Loading...
Searching...
No Matches
SolutionMethodCatalog.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5using System.Xml.Serialization;
6using LotSizingDataModel.Core.Common;
8
10
11/// <summary>
12/// Represents an extensible catalog of solution methods that
13/// may be evaluated for lot-sizing problem instances.
14/// </summary>
15/// <remarks>
16/// A method catalog contains solver-independent descriptions
17/// of algorithms, formulations, heuristics and decomposition
18/// procedures.
19///
20/// The catalog does not execute the methods. It provides the
21/// capabilities and limitations used by the solution-method
22/// advisor to determine technical compatibility.
23/// </remarks>
24[Serializable]
25[XmlRoot("solutionMethodCatalog")]
26[XmlType(TypeName = "solutionMethodCatalog")]
27public sealed class SolutionMethodCatalog : ModelObject
28{
29 private string _catalogName =
30 string.Empty;
31
32 private string _catalogVersion =
33 string.Empty;
34
35 private string _description =
36 string.Empty;
37
38 private bool _allowUnknownFeatureCodes;
39
40 /// <summary>
41 /// Initializes an empty solution-method catalog.
42 /// </summary>
43 /// <remarks>
44 /// This constructor is required for XML serialization.
45 /// </remarks>
47 {
48 }
49
50 /// <summary>
51 /// Initializes a solution-method catalog.
52 /// </summary>
53 /// <param name="catalogName">
54 /// Human-readable catalog name.
55 /// </param>
56 /// <param name="catalogVersion">
57 /// Version of the catalog contents and semantics.
58 /// </param>
59 /// <exception cref="ArgumentException">
60 /// Thrown when <paramref name="catalogName"/> or
61 /// <paramref name="catalogVersion"/> is empty.
62 /// </exception>
64 string catalogName,
65 string catalogVersion)
66 {
67 if (string.IsNullOrWhiteSpace(catalogName))
68 {
69 throw new ArgumentException(
70 "A solution-method catalog name is required.",
71 nameof(catalogName));
72 }
73
74 if (string.IsNullOrWhiteSpace(catalogVersion))
75 {
76 throw new ArgumentException(
77 "A solution-method catalog version is required.",
78 nameof(catalogVersion));
79 }
80
82 catalogName;
83
85 catalogVersion;
86 }
87
88 /// <summary>
89 /// Gets or sets the human-readable name of the catalog.
90 /// </summary>
91 [XmlAttribute("catalogName")]
92 public string CatalogName
93 {
94 get => _catalogName;
95 set
96 {
97 if (SetProperty(
98 ref _catalogName,
99 value?.Trim() ?? string.Empty))
100 {
101 OnPropertyChanged(
102 nameof(HasCatalogName));
103
104 NotifyCatalogValidityProperties();
105 }
106 }
107 }
108
109 /// <summary>
110 /// Gets or sets the version of the catalog contents and
111 /// compatibility semantics.
112 /// </summary>
113 [XmlAttribute("catalogVersion")]
114 public string CatalogVersion
115 {
116 get => _catalogVersion;
117 set
118 {
119 if (SetProperty(
120 ref _catalogVersion,
121 value?.Trim() ?? string.Empty))
122 {
123 OnPropertyChanged(
124 nameof(HasCatalogVersion));
125
126 NotifyCatalogValidityProperties();
127 }
128 }
129 }
130
131 /// <summary>
132 /// Gets or sets a human-readable description of the
133 /// catalog.
134 /// </summary>
135 [XmlElement("description")]
136 public string Description
137 {
138 get => _description;
139 set
140 {
141 if (SetProperty(
142 ref _description,
143 value?.Trim() ?? string.Empty))
144 {
145 OnPropertyChanged(
146 nameof(HasDescription));
147 }
148 }
149 }
150
151 /// <summary>
152 /// Gets or sets a value indicating whether method
153 /// definitions may reference feature codes that are not
154 /// standard Boolean properties of
155 /// <see cref="LotSizingProblemFeatures"/>.
156 /// </summary>
157 /// <remarks>
158 /// The default value is <see langword="false"/>.
159 ///
160 /// Set this property to <see langword="true"/> when the
161 /// catalog is intended to be used with a customized
162 /// feature extractor or an extended advisor.
163 /// </remarks>
164 [XmlAttribute("allowUnknownFeatureCodes")]
166 {
167 get => _allowUnknownFeatureCodes;
168 set
169 {
170 if (SetProperty(
171 ref _allowUnknownFeatureCodes,
172 value))
173 {
174 NotifyCatalogValidityProperties();
175 }
176 }
177 }
178
179 /// <summary>
180 /// Gets the solution-method definitions contained in the
181 /// catalog.
182 /// </summary>
183 [XmlArray("methods")]
184 [XmlArrayItem("method")]
185 public List<SolutionMethodDefinition> Methods { get; } =
186 new();
187
188 /// <summary>
189 /// Gets a value indicating whether the catalog has a
190 /// human-readable name.
191 /// </summary>
192 [XmlIgnore]
193 public bool HasCatalogName =>
194 !string.IsNullOrWhiteSpace(
196
197 /// <summary>
198 /// Gets a value indicating whether the catalog has a
199 /// version.
200 /// </summary>
201 [XmlIgnore]
202 public bool HasCatalogVersion =>
203 !string.IsNullOrWhiteSpace(
205
206 /// <summary>
207 /// Gets a value indicating whether the catalog has a
208 /// description.
209 /// </summary>
210 [XmlIgnore]
211 public bool HasDescription =>
212 !string.IsNullOrWhiteSpace(
214
215 /// <summary>
216 /// Gets a value indicating whether the catalog contains
217 /// at least one method definition.
218 /// </summary>
219 [XmlIgnore]
220 public bool HasMethods =>
221 Methods.Count > 0;
222
223 /// <summary>
224 /// Gets the number of method definitions contained in the
225 /// catalog.
226 /// </summary>
227 [XmlIgnore]
228 public int MethodCount =>
229 Methods.Count;
230
231 /// <summary>
232 /// Gets the number of enabled method definitions.
233 /// </summary>
234 [XmlIgnore]
235 public int EnabledMethodCount =>
236 Methods.Count(
237 method =>
238 method is not null &&
239 method.IsEnabled);
240
241 /// <summary>
242 /// Gets the number of structurally valid method
243 /// definitions.
244 /// </summary>
245 [XmlIgnore]
246 public int ValidMethodCount =>
247 Methods.Count(
248 method =>
249 method is not null &&
250 method.IsValidDefinition);
251
252 /// <summary>
253 /// Gets the number of methods that may currently be
254 /// evaluated by the method advisor.
255 /// </summary>
256 [XmlIgnore]
258 Methods.Count(
259 method =>
260 method is not null &&
261 method.CanBeEvaluated);
262
263 /// <summary>
264 /// Gets a value indicating whether the catalog is
265 /// structurally valid.
266 /// </summary>
267 [XmlIgnore]
268 public bool IsValidCatalog =>
269 Validate().Count == 0;
270
271 /// <summary>
272 /// Gets a value indicating whether the catalog can be
273 /// used by the solution-method advisor.
274 /// </summary>
275 [XmlIgnore]
276 public bool CanRecommend =>
279
280 /// <summary>
281 /// Adds a solution-method definition to the catalog.
282 /// </summary>
283 /// <param name="method">
284 /// Method definition to add.
285 /// </param>
286 /// <exception cref="ArgumentNullException">
287 /// Thrown when <paramref name="method"/> is
288 /// <see langword="null"/>.
289 /// </exception>
290 /// <exception cref="ArgumentException">
291 /// Thrown when the method has no stable code or when the
292 /// code is already used by another method.
293 /// </exception>
294 public void AddMethod(
296 {
297 ArgumentNullException.ThrowIfNull(
298 method);
299
300 if (!method.HasMethodCode)
301 {
302 throw new ArgumentException(
303 "The solution method must have a stable code.",
304 nameof(method));
305 }
306
308 method.MethodCode))
309 {
310 throw new ArgumentException(
311 $"Solution-method code " +
312 $"'{method.MethodCode}' is already used.",
313 nameof(method));
314 }
315
316 Methods.Add(
317 method);
318
319 NotifyMethodCollectionProperties();
320 }
321
322 /// <summary>
323 /// Replaces all solution-method definitions in the
324 /// catalog.
325 /// </summary>
326 /// <param name="methods">
327 /// New collection of method definitions.
328 /// </param>
329 /// <exception cref="ArgumentNullException">
330 /// Thrown when <paramref name="methods"/> is
331 /// <see langword="null"/>.
332 /// </exception>
333 /// <exception cref="ArgumentException">
334 /// Thrown when the collection contains a null definition,
335 /// a method without a stable code or duplicate method
336 /// codes.
337 /// </exception>
338 public void ReplaceMethods(
339 IEnumerable<SolutionMethodDefinition> methods)
340 {
341 ArgumentNullException.ThrowIfNull(
342 methods);
343
344 SolutionMethodDefinition[] normalizedMethods =
345 methods.ToArray();
346
347 if (normalizedMethods.Any(
348 method =>
349 method is null))
350 {
351 throw new ArgumentException(
352 "The method collection cannot contain a null " +
353 "definition.",
354 nameof(methods));
355 }
356
357 if (normalizedMethods.Any(
358 method =>
359 !method.HasMethodCode))
360 {
361 throw new ArgumentException(
362 "Every solution method must have a stable " +
363 "code.",
364 nameof(methods));
365 }
366
367 string[] duplicateCodes =
368 normalizedMethods
369 .GroupBy(
370 method =>
371 method.MethodCode,
372 StringComparer.OrdinalIgnoreCase)
373 .Where(
374 group =>
375 group.Count() > 1)
376 .Select(
377 group =>
378 group.Key)
379 .OrderBy(
380 code =>
381 code,
382 StringComparer.OrdinalIgnoreCase)
383 .ToArray();
384
385 if (duplicateCodes.Length > 0)
386 {
387 throw new ArgumentException(
388 "Duplicate solution-method codes: " +
389 string.Join(
390 ", ",
391 duplicateCodes) +
392 ".",
393 nameof(methods));
394 }
395
396 Methods.Clear();
397
398 Methods.AddRange(
399 normalizedMethods);
400
401 NotifyMethodCollectionProperties();
402 }
403
404 /// <summary>
405 /// Finds a solution method from its stable code.
406 /// </summary>
407 /// <param name="methodCode">
408 /// Method code to search for.
409 /// </param>
410 /// <returns>
411 /// Matching method definition, or
412 /// <see langword="null"/> when no method uses the supplied
413 /// code.
414 /// </returns>
415 /// <exception cref="InvalidOperationException">
416 /// Thrown when several methods use the supplied code.
417 /// </exception>
419 string methodCode)
420 {
421 if (string.IsNullOrWhiteSpace(methodCode))
422 {
423 return null;
424 }
425
426 string normalizedCode =
427 methodCode.Trim();
428
429 SolutionMethodDefinition[] matches =
430 Methods
431 .Where(
432 method =>
433 method is not null &&
434 string.Equals(
435 method.MethodCode,
436 normalizedCode,
437 StringComparison.OrdinalIgnoreCase))
438 .Take(2)
439 .ToArray();
440
441 return matches.Length switch
442 {
443 0 =>
444 null,
445
446 1 =>
447 matches[0],
448
449 _ =>
450 throw new InvalidOperationException(
451 $"Solution-method code '{methodCode}' is " +
452 "ambiguous in the current catalog.")
453 };
454 }
455
456 /// <summary>
457 /// Determines whether the catalog contains a method with
458 /// the supplied code.
459 /// </summary>
460 /// <param name="methodCode">
461 /// Method code to search for.
462 /// </param>
463 /// <returns>
464 /// <see langword="true"/> when the code exists;
465 /// otherwise, <see langword="false"/>.
466 /// </returns>
468 string methodCode)
469 {
470 return FindMethod(
471 methodCode) is not null;
472 }
473
474 /// <summary>
475 /// Removes a solution method from the catalog.
476 /// </summary>
477 /// <param name="methodCode">
478 /// Code of the method to remove.
479 /// </param>
480 /// <returns>
481 /// <see langword="true"/> when a method was removed;
482 /// otherwise, <see langword="false"/>.
483 /// </returns>
484 public bool RemoveMethod(
485 string methodCode)
486 {
489 methodCode);
490
491 if (method is null)
492 {
493 return false;
494 }
495
496 bool removed =
497 Methods.Remove(
498 method);
499
500 if (removed)
501 {
502 NotifyMethodCollectionProperties();
503 }
504
505 return removed;
506 }
507
508 /// <summary>
509 /// Removes all method definitions from the catalog.
510 /// </summary>
511 public void ClearMethods()
512 {
513 if (Methods.Count == 0)
514 {
515 return;
516 }
517
518 Methods.Clear();
519
520 NotifyMethodCollectionProperties();
521 }
522
523 /// <summary>
524 /// Gets the enabled method definitions in deterministic
525 /// priority order.
526 /// </summary>
527 /// <returns>
528 /// Enabled method definitions ordered by decreasing
529 /// priority, then by name and method code.
530 /// </returns>
531 /// <remarks>
532 /// This method may return enabled definitions that are
533 /// structurally invalid. Use
534 /// <see cref="GetMethodsForEvaluation"/> to retrieve only
535 /// definitions that can be evaluated.
536 /// </remarks>
537 public IReadOnlyList<SolutionMethodDefinition>
539 {
540 return Methods
541 .Where(
542 method =>
543 method is not null &&
544 method.IsEnabled)
545 .OrderByDescending(
546 method =>
547 method.Priority)
548 .ThenBy(
549 method =>
550 method.Name,
551 StringComparer.OrdinalIgnoreCase)
552 .ThenBy(
553 method =>
554 method.MethodCode,
555 StringComparer.OrdinalIgnoreCase)
556 .ToArray();
557 }
558
559 /// <summary>
560 /// Gets the method definitions that can be evaluated by
561 /// the solution-method advisor.
562 /// </summary>
563 /// <returns>
564 /// Enabled and structurally valid method definitions in
565 /// deterministic priority order.
566 /// </returns>
567 public IReadOnlyList<SolutionMethodDefinition>
569 {
570 return Methods
571 .Where(
572 method =>
573 method is not null &&
574 method.CanBeEvaluated)
575 .OrderByDescending(
576 method =>
577 method.Priority)
578 .ThenBy(
579 method =>
580 method.Name,
581 StringComparer.OrdinalIgnoreCase)
582 .ThenBy(
583 method =>
584 method.MethodCode,
585 StringComparer.OrdinalIgnoreCase)
586 .ToArray();
587 }
588
589 /// <summary>
590 /// Gets all standard Boolean feature codes that may be
591 /// referenced by a solution-method definition.
592 /// </summary>
593 /// <returns>
594 /// Ordered collection of public Boolean property names
595 /// defined by <see cref="LotSizingProblemFeatures"/>.
596 /// </returns>
597 public static IReadOnlyList<string>
599 {
600 return typeof(LotSizingProblemFeatures)
601 .GetProperties(
602 BindingFlags.Instance |
603 BindingFlags.Public)
604 .Where(
605 property =>
606 property.CanRead &&
607 property.GetIndexParameters().Length == 0 &&
608 property.PropertyType ==
609 typeof(bool))
610 .Select(
611 property =>
612 property.Name)
613 .OrderBy(
614 featureCode =>
615 featureCode,
616 StringComparer.OrdinalIgnoreCase)
617 .ToArray();
618 }
619
620 /// <summary>
621 /// Validates the structural consistency of the catalog and
622 /// its method definitions.
623 /// </summary>
624 /// <returns>
625 /// Ordered validation-error collection. An empty
626 /// collection indicates that the catalog is valid.
627 /// </returns>
628 public IReadOnlyList<string> Validate()
629 {
630 var errors =
631 new List<string>();
632
633 if (!HasCatalogName)
634 {
635 errors.Add(
636 "The solution-method catalog name is missing.");
637 }
638
640 {
641 errors.Add(
642 "The solution-method catalog version is " +
643 "missing.");
644 }
645
646 if (!HasMethods)
647 {
648 errors.Add(
649 "The solution-method catalog does not contain " +
650 "any method definition.");
651 }
652
653 ValidateMethodDefinitions(
654 errors);
655
656 ValidateMethodCodeUniqueness(
657 errors);
658
660 {
661 ValidateReferencedFeatureCodes(
662 errors);
663 }
664
665 return errors
666 .Where(
667 error =>
668 !string.IsNullOrWhiteSpace(error))
669 .Select(
670 error =>
671 error.Trim())
672 .Distinct(
673 StringComparer.Ordinal)
674 .OrderBy(
675 error =>
676 error,
677 StringComparer.Ordinal)
678 .ToArray();
679 }
680
681 /// <summary>
682 /// Validates the catalog and throws an exception when at
683 /// least one error is found.
684 /// </summary>
685 /// <exception cref="InvalidOperationException">
686 /// Thrown when the catalog is invalid.
687 /// </exception>
688 public void EnsureValid()
689 {
690 IReadOnlyList<string> errors =
691 Validate();
692
693 if (errors.Count == 0)
694 {
695 return;
696 }
697
698 throw new InvalidOperationException(
699 "The solution-method catalog is invalid:" +
700 Environment.NewLine +
701 string.Join(
702 Environment.NewLine,
703 errors.Select(
704 error =>
705 "- " + error)));
706 }
707
708 /// <inheritdoc/>
709 public override string ToString()
710 {
711 return
712 $"{CatalogName}; version {CatalogVersion}; " +
713 $"{MethodCount} method(s); " +
714 $"{EvaluableMethodCount} evaluable";
715 }
716
717 private void ValidateMethodDefinitions(
718 ICollection<string> errors)
719 {
720 for (int index = 0;
721 index < Methods.Count;
722 index++)
723 {
725 Methods[index];
726
727 if (method is null)
728 {
729 errors.Add(
730 $"Solution-method definition at index " +
731 $"{index} is null.");
732
733 continue;
734 }
735
736 IReadOnlyList<string> methodErrors =
737 method.Validate();
738
739 foreach (string methodError in methodErrors)
740 {
741 errors.Add(
742 $"Method '{DisplayMethodCode(method)}': " +
743 methodError);
744 }
745 }
746 }
747
748 private void ValidateMethodCodeUniqueness(
749 ICollection<string> errors)
750 {
751 string[] missingCodeIndexes =
752 Methods
753 .Select(
754 (method, index) =>
755 new
756 {
757 Method =
758 method,
759
760 Index =
761 index
762 })
763 .Where(
764 entry =>
765 entry.Method is not null &&
766 !entry.Method.HasMethodCode)
767 .Select(
768 entry =>
769 entry.Index.ToString(
770 System.Globalization
771 .CultureInfo.InvariantCulture))
772 .ToArray();
773
774 if (missingCodeIndexes.Length > 0)
775 {
776 errors.Add(
777 "Method definitions without a stable code " +
778 "exist at indexes: " +
779 string.Join(
780 ", ",
781 missingCodeIndexes) +
782 ".");
783 }
784
785 string[] duplicateMethodCodes =
786 Methods
787 .Where(
788 method =>
789 method is not null &&
790 method.HasMethodCode)
791 .GroupBy(
792 method =>
793 method.MethodCode,
794 StringComparer.OrdinalIgnoreCase)
795 .Where(
796 group =>
797 group.Count() > 1)
798 .Select(
799 group =>
800 group.Key)
801 .OrderBy(
802 methodCode =>
803 methodCode,
804 StringComparer.OrdinalIgnoreCase)
805 .ToArray();
806
807 if (duplicateMethodCodes.Length > 0)
808 {
809 errors.Add(
810 "Duplicate solution-method codes: " +
811 string.Join(
812 ", ",
813 duplicateMethodCodes) +
814 ".");
815 }
816 }
817
818 private void ValidateReferencedFeatureCodes(
819 ICollection<string> errors)
820 {
821 HashSet<string> knownBooleanFeatureCodes =
823 .ToHashSet(
824 StringComparer.OrdinalIgnoreCase);
825
826 foreach (SolutionMethodDefinition method
827 in Methods.Where(
828 method =>
829 method is not null))
830 {
831 string[] unknownFeatureCodes =
832 method
833 .GetAllFeatureCodes()
834 .Where(
835 featureCode =>
836 !knownBooleanFeatureCodes.Contains(
837 featureCode))
838 .OrderBy(
839 featureCode =>
840 featureCode,
841 StringComparer.OrdinalIgnoreCase)
842 .ToArray();
843
844 if (unknownFeatureCodes.Length == 0)
845 {
846 continue;
847 }
848
849 errors.Add(
850 $"Method '{DisplayMethodCode(method)}' " +
851 "references feature codes that are not " +
852 "public Boolean properties of " +
853 $"{nameof(LotSizingProblemFeatures)}: " +
854 string.Join(
855 ", ",
856 unknownFeatureCodes) +
857 ".");
858 }
859 }
860
861 private static string DisplayMethodCode(
862 SolutionMethodDefinition method)
863 {
864 return method.HasMethodCode
865 ? method.MethodCode
866 : "<missing code>";
867 }
868
869 private void NotifyMethodCollectionProperties()
870 {
871 OnPropertyChanged(
872 nameof(Methods));
873
874 OnPropertyChanged(
875 nameof(HasMethods));
876
877 OnPropertyChanged(
878 nameof(MethodCount));
879
880 OnPropertyChanged(
881 nameof(EnabledMethodCount));
882
883 OnPropertyChanged(
884 nameof(ValidMethodCount));
885
886 OnPropertyChanged(
887 nameof(EvaluableMethodCount));
888
889 NotifyCatalogValidityProperties();
890 }
891
892 private void NotifyCatalogValidityProperties()
893 {
894 OnPropertyChanged(
895 nameof(IsValidCatalog));
896
897 OnPropertyChanged(
898 nameof(CanRecommend));
899 }
900}
Describes the factual structural and modeling features detected in a lot-sizing supply-chain instance...
int EnabledMethodCount
Gets the number of enabled method definitions.
static IReadOnlyList< string > GetKnownBooleanFeatureCodes()
Gets all standard Boolean feature codes that may be referenced by a solution-method definition.
IReadOnlyList< SolutionMethodDefinition > GetEnabledMethods()
Gets the enabled method definitions in deterministic priority order.
string Description
Gets or sets a human-readable description of the catalog.
List< SolutionMethodDefinition > Methods
Gets the solution-method definitions contained in the catalog.
bool CanRecommend
Gets a value indicating whether the catalog can be used by the solution-method advisor.
bool ContainsMethodCode(string methodCode)
Determines whether the catalog contains a method with the supplied code.
bool HasCatalogName
Gets a value indicating whether the catalog has a human-readable name.
void ReplaceMethods(IEnumerable< SolutionMethodDefinition > methods)
Replaces all solution-method definitions in the catalog.
bool IsValidCatalog
Gets a value indicating whether the catalog is structurally valid.
IReadOnlyList< SolutionMethodDefinition > GetMethodsForEvaluation()
Gets the method definitions that can be evaluated by the solution-method advisor.
void AddMethod(SolutionMethodDefinition method)
Adds a solution-method definition to the catalog.
void EnsureValid()
Validates the catalog and throws an exception when at least one error is found.
int ValidMethodCount
Gets the number of structurally valid method definitions.
int MethodCount
Gets the number of method definitions contained in the catalog.
string CatalogVersion
Gets or sets the version of the catalog contents and compatibility semantics.
SolutionMethodCatalog()
Initializes an empty solution-method catalog.
string CatalogName
Gets or sets the human-readable name of the catalog.
IReadOnlyList< string > Validate()
Validates the structural consistency of the catalog and its method definitions.
bool AllowUnknownFeatureCodes
Gets or sets a value indicating whether method definitions may reference feature codes that are not s...
bool HasCatalogVersion
Gets a value indicating whether the catalog has a version.
bool HasMethods
Gets a value indicating whether the catalog contains at least one method definition.
SolutionMethodDefinition? FindMethod(string methodCode)
Finds a solution method from its stable code.
SolutionMethodCatalog(string catalogName, string catalogVersion)
Initializes a solution-method catalog.
int EvaluableMethodCount
Gets the number of methods that may currently be evaluated by the method advisor.
void ClearMethods()
Removes all method definitions from the catalog.
bool RemoveMethod(string methodCode)
Removes a solution method from the catalog.
bool HasDescription
Gets a value indicating whether the catalog has a description.
Describes the applicability, capabilities and limitations of a solution method for lot-sizing problem...
bool HasMethodCode
Gets a value indicating whether a stable method code has been recorded.
string MethodCode
Gets or sets the stable code identifying the solution method.
IReadOnlyList< string > Validate()
Validates the structural consistency of this method definition.