LotSizingDataModel.Solution 2.0.1
Solution objects for production, setup, inventory and related decisions.
Loading...
Searching...
No Matches
LotSizingSolutionValidator.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using LotSizingDataModel.Core;
5using LotSizingDataModel.Core.PhysicalModel;
9
11
12/// <summary>
13/// Identifies the severity of a solution-validation issue.
14/// </summary>
16{
17 /// <summary>
18 /// The issue does not necessarily make the solution invalid,
19 /// but should be reviewed.
20 /// </summary>
22
23 /// <summary>
24 /// The issue makes the solution structurally invalid
25 /// or incompatible with its associated instance.
26 /// </summary>
28}
29
30/// <summary>
31/// Describes one issue detected while validating
32/// a lot-sizing solution.
33/// </summary>
34public sealed class SolutionValidationIssue
35{
36 /// <summary>
37 /// Initializes a solution-validation issue.
38 /// </summary>
39 /// <param name="severity">
40 /// Severity of the issue.
41 /// </param>
42 /// <param name="code">
43 /// Stable validation code.
44 /// </param>
45 /// <param name="path">
46 /// Logical path of the affected value.
47 /// </param>
48 /// <param name="message">
49 /// Human-readable description of the issue.
50 /// </param>
53 string code,
54 string path,
55 string message)
56 {
57 ArgumentException.ThrowIfNullOrWhiteSpace(code);
58 ArgumentException.ThrowIfNullOrWhiteSpace(message);
59
60 Severity = severity;
61 Code = code.Trim();
62 Path = path?.Trim() ?? string.Empty;
63 Message = message.Trim();
64 }
65
66 /// <summary>
67 /// Gets the severity of the issue.
68 /// </summary>
70
71 /// <summary>
72 /// Gets the stable validation code.
73 /// </summary>
74 public string Code { get; }
75
76 /// <summary>
77 /// Gets the logical path of the affected value.
78 /// </summary>
79 public string Path { get; }
80
81 /// <summary>
82 /// Gets the human-readable description of the issue.
83 /// </summary>
84 public string Message { get; }
85
86 /// <inheritdoc/>
87 public override string ToString()
88 {
89 string pathDescription =
90 string.IsNullOrWhiteSpace(Path)
91 ? string.Empty
92 : $" {Path}:";
93
94 return
95 $"[{Severity}] {Code}{pathDescription} {Message}";
96 }
97}
98
99/// <summary>
100/// Validates the structure and instance compatibility
101/// of a lot-sizing solution.
102/// </summary>
103/// <remarks>
104/// This validator checks identifiers, planning horizons,
105/// duplicate decision keys, decision-value domains,
106/// evaluation consistency and references to a
107/// <see cref="SupplyChain"/> instance.
108///
109/// It does not independently recompute material balances,
110/// capacity consumption or objective values.
111/// </remarks>
112public sealed class LotSizingSolutionValidator
113{
114 private readonly double _numericalTolerance;
115
116 /// <summary>
117 /// Initializes a solution validator.
118 /// </summary>
119 /// <param name="numericalTolerance">
120 /// Non-negative finite tolerance used when comparing
121 /// numerical evaluation values with zero.
122 /// </param>
124 double numericalTolerance = 1e-9)
125 {
126 if (!double.IsFinite(numericalTolerance) ||
127 numericalTolerance < 0.0)
128 {
129 throw new ArgumentOutOfRangeException(
130 nameof(numericalTolerance),
131 numericalTolerance,
132 "The numerical tolerance must be finite " +
133 "and non-negative.");
134 }
135
136 _numericalTolerance =
137 numericalTolerance;
138 }
139
140 /// <summary>
141 /// Gets the numerical tolerance used by the validator.
142 /// </summary>
143 public double NumericalTolerance =>
144 _numericalTolerance;
145
146 /// <summary>
147 /// Validates the internal structure of a solution.
148 /// </summary>
149 /// <param name="solution">
150 /// Solution to validate.
151 /// </param>
152 /// <returns>
153 /// Detected warnings and errors.
154 /// </returns>
155 public IReadOnlyList<SolutionValidationIssue> Validate(
156 LotSizingSolution solution)
157 {
158 ArgumentNullException.ThrowIfNull(solution);
159
160 var issues =
161 new List<SolutionValidationIssue>();
162
163 ValidateGeneralInformation(
164 solution,
165 issues);
166
167 ValidateGenerationMetadata(
168 solution,
169 issues);
170
171 ValidateEvaluation(
172 solution,
173 issues);
174
175 ValidateDecisions(
176 solution,
177 issues);
178
179 return issues;
180 }
181
182 /// <summary>
183 /// Validates a solution and its compatibility with
184 /// a supply-chain instance.
185 /// </summary>
186 /// <param name="solution">
187 /// Solution to validate.
188 /// </param>
189 /// <param name="supplyChain">
190 /// Supply-chain instance associated with the solution.
191 /// </param>
192 /// <returns>
193 /// Detected warnings and errors.
194 /// </returns>
195 public IReadOnlyList<SolutionValidationIssue> Validate(
196 LotSizingSolution solution,
197 SupplyChain supplyChain)
198 {
199 ArgumentNullException.ThrowIfNull(solution);
200 ArgumentNullException.ThrowIfNull(supplyChain);
201
202 var issues =
203 Validate(solution).ToList();
204
205 ValidateAgainstSupplyChain(
206 solution,
207 supplyChain,
208 issues);
209
210 return issues;
211 }
212
213 /// <summary>
214 /// Determines whether a solution contains no
215 /// internal validation errors.
216 /// </summary>
217 /// <param name="solution">
218 /// Solution to validate.
219 /// </param>
220 /// <returns>
221 /// True when no error is detected; otherwise, false.
222 /// </returns>
223 public bool IsValid(
224 LotSizingSolution solution)
225 {
226 return !Validate(solution).Any(
227 issue =>
228 issue.Severity ==
230 }
231
232 /// <summary>
233 /// Determines whether a solution is structurally valid
234 /// and compatible with a supply-chain instance.
235 /// </summary>
236 /// <param name="solution">
237 /// Solution to validate.
238 /// </param>
239 /// <param name="supplyChain">
240 /// Associated supply-chain instance.
241 /// </param>
242 /// <returns>
243 /// True when no error is detected; otherwise, false.
244 /// </returns>
245 public bool IsValid(
246 LotSizingSolution solution,
247 SupplyChain supplyChain)
248 {
249 return !Validate(
250 solution,
251 supplyChain)
252 .Any(
253 issue =>
254 issue.Severity ==
256 }
257
258 /// <summary>
259 /// Validates a solution and throws an exception
260 /// when at least one error is detected.
261 /// </summary>
262 /// <param name="solution">
263 /// Solution to validate.
264 /// </param>
265 /// <exception cref="InvalidOperationException">
266 /// Thrown when the solution contains at least one error.
267 /// </exception>
268 public void ThrowIfInvalid(
269 LotSizingSolution solution)
270 {
271 ThrowIfErrors(
272 Validate(solution));
273 }
274
275 /// <summary>
276 /// Validates a solution against a supply-chain instance
277 /// and throws an exception when an error is detected.
278 /// </summary>
279 /// <param name="solution">
280 /// Solution to validate.
281 /// </param>
282 /// <param name="supplyChain">
283 /// Associated supply-chain instance.
284 /// </param>
285 /// <exception cref="InvalidOperationException">
286 /// Thrown when the solution contains at least one error.
287 /// </exception>
288 public void ThrowIfInvalid(
289 LotSizingSolution solution,
290 SupplyChain supplyChain)
291 {
292 ThrowIfErrors(
293 Validate(
294 solution,
295 supplyChain));
296 }
297
298 private static void ValidateGeneralInformation(
299 LotSizingSolution solution,
300 ICollection<SolutionValidationIssue> issues)
301 {
302 if (solution.Id == Guid.Empty)
303 {
304 AddError(
305 issues,
306 "SOL001",
307 "id",
308 "The solution identifier cannot be empty.");
309 }
310
311 if (solution.PlanningHorizon <= 0)
312 {
313 AddError(
314 issues,
315 "SOL002",
316 "planningHorizon",
317 "The planning horizon must be strictly positive.");
318 }
319
320 if (string.IsNullOrWhiteSpace(
321 solution.InstanceIdentifier))
322 {
323 AddWarning(
324 issues,
325 "SOL003",
326 "instanceIdentifier",
327 "No instance identifier is recorded.");
328 }
329
330 if (!solution.HasDecisions)
331 {
332 AddError(
333 issues,
334 "SOL004",
335 "decisions",
336 "The solution does not contain any " +
337 "decision object.");
338 }
339
340 if (solution.Completeness ==
341 SolutionCompleteness.Unknown)
342 {
343 AddWarning(
344 issues,
345 "SOL005",
346 "completeness",
347 "The completeness of the solution " +
348 "has not been evaluated.");
349 }
350
351 if (solution.Completeness ==
352 SolutionCompleteness.Partial &&
353 solution.Evaluation.FeasibilityStatus ==
354 FeasibilityStatus.Feasible)
355 {
356 AddError(
357 issues,
358 "SOL006",
359 "evaluation.feasibilityStatus",
360 "A partial solution cannot be declared " +
361 "globally feasible.");
362 }
363
364 if (solution.Completeness !=
365 SolutionCompleteness.Complete &&
366 solution.Evaluation.OptimalityStatus ==
367 OptimalityStatus.ProvenOptimal)
368 {
369 AddError(
370 issues,
371 "SOL007",
372 "evaluation.optimalityStatus",
373 "A solution cannot be declared proven optimal " +
374 "unless it is complete.");
375 }
376 }
377
378 private static void ValidateGenerationMetadata(
379 LotSizingSolution solution,
380 ICollection<SolutionValidationIssue> issues)
381 {
382 if (solution.GenerationMetadata.MethodKind ==
383 SolutionMethodKind.Unknown)
384 {
385 AddWarning(
386 issues,
387 "GEN001",
388 "generationMetadata.methodKind",
389 "The solution-generation method is unknown.");
390 }
391
392 if (solution.GenerationMetadata.TerminationReason ==
393 TerminationReason.Unknown)
394 {
395 AddWarning(
396 issues,
397 "GEN002",
398 "generationMetadata.terminationReason",
399 "The execution termination reason is unknown.");
400 }
401
402 var duplicateParameterNames =
403 solution.GenerationMetadata.Parameters
404 .Where(
405 parameter =>
406 !string.IsNullOrWhiteSpace(
407 parameter.Name))
408 .GroupBy(
409 parameter =>
410 parameter.Name,
411 StringComparer.OrdinalIgnoreCase)
412 .Where(
413 group =>
414 group.Count() > 1)
415 .Select(
416 group =>
417 group.Key);
418
419 foreach (string parameterName
420 in duplicateParameterNames)
421 {
422 AddError(
423 issues,
424 "GEN003",
425 "generationMetadata.parameters",
426 $"The algorithm parameter " +
427 $"'{parameterName}' is duplicated.");
428 }
429
430 for (int index = 0;
431 index <
432 solution.GenerationMetadata.Parameters.Count;
433 index++)
434 {
435 if (string.IsNullOrWhiteSpace(
436 solution.GenerationMetadata
437 .Parameters[index]
438 .Name))
439 {
440 AddError(
441 issues,
442 "GEN004",
443 $"generationMetadata.parameters[{index}]",
444 "The algorithm parameter name is empty.");
445 }
446 }
447
448 if (solution.GenerationMetadata.TerminationReason ==
449 TerminationReason.OptimalityProven &&
450 solution.Evaluation.OptimalityStatus !=
451 OptimalityStatus.ProvenOptimal)
452 {
453 AddWarning(
454 issues,
455 "GEN005",
456 "generationMetadata.terminationReason",
457 "The generation metadata indicates that " +
458 "optimality was proven, but the evaluation " +
459 "does not declare the solution proven optimal.");
460 }
461 }
462
463 private void ValidateEvaluation(
464 LotSizingSolution solution,
465 ICollection<SolutionValidationIssue> issues)
466 {
467 SolutionEvaluation evaluation =
468 solution.Evaluation;
469
470 if (evaluation.OptimalityStatus ==
471 OptimalityStatus.ProvenOptimal &&
472 evaluation.FeasibilityStatus !=
473 FeasibilityStatus.Feasible)
474 {
475 AddError(
476 issues,
477 "EVA001",
478 "evaluation.optimalityStatus",
479 "A proven-optimal solution must be feasible.");
480 }
481
482 if (evaluation.OptimalityStatus ==
483 OptimalityStatus.ProvenOptimal &&
484 !evaluation.ObjectiveValue.HasValue)
485 {
486 AddError(
487 issues,
488 "EVA002",
489 "evaluation.objectiveValue",
490 "A proven-optimal solution must have an " +
491 "objective value.");
492 }
493
494 if (evaluation.OptimalityStatus ==
495 OptimalityStatus.ProvenOptimal &&
496 evaluation.AbsoluteGap.HasValue &&
497 evaluation.AbsoluteGap.Value >
499 {
500 AddError(
501 issues,
502 "EVA003",
503 "evaluation.absoluteGap",
504 "The absolute gap of a proven-optimal solution " +
505 "must be zero within the numerical tolerance.");
506 }
507
508 if (evaluation.OptimalityStatus ==
509 OptimalityStatus.ProvenOptimal &&
510 evaluation.RelativeGap.HasValue &&
511 evaluation.RelativeGap.Value >
513 {
514 AddError(
515 issues,
516 "EVA004",
517 "evaluation.relativeGap",
518 "The relative gap of a proven-optimal solution " +
519 "must be zero within the numerical tolerance.");
520 }
521
522 if ((evaluation.AbsoluteGap.HasValue ||
523 evaluation.RelativeGap.HasValue) &&
524 !evaluation.BestBound.HasValue)
525 {
526 AddError(
527 issues,
528 "EVA005",
529 "evaluation.bestBound",
530 "An optimality gap is recorded without " +
531 "an objective bound.");
532 }
533
534 if (evaluation.BestBound.HasValue &&
535 !evaluation.ObjectiveValue.HasValue)
536 {
537 AddError(
538 issues,
539 "EVA006",
540 "evaluation.objectiveValue",
541 "An objective bound is recorded without " +
542 "an objective value.");
543 }
544
545 if (evaluation.MaximumConstraintViolation.HasValue &&
546 evaluation.TotalConstraintViolation.HasValue &&
547 evaluation.MaximumConstraintViolation.Value >
548 evaluation.TotalConstraintViolation.Value +
550 {
551 AddError(
552 issues,
553 "EVA007",
554 "evaluation.maximumConstraintViolation",
555 "The maximum constraint violation cannot " +
556 "exceed the total constraint violation.");
557 }
558
559 bool hasPositiveViolation =
560 evaluation.MaximumConstraintViolation >
562 evaluation.TotalConstraintViolation >
564 evaluation.ViolatedConstraintCount > 0;
565
566 if (evaluation.FeasibilityStatus ==
567 FeasibilityStatus.Feasible &&
568 hasPositiveViolation)
569 {
570 AddError(
571 issues,
572 "EVA008",
573 "evaluation.feasibilityStatus",
574 "A solution with recorded constraint " +
575 "violations cannot be declared feasible.");
576 }
577
578 if (evaluation.FeasibilityStatus ==
579 FeasibilityStatus.Feasible &&
580 solution.Completeness ==
581 SolutionCompleteness.Unknown)
582 {
583 AddWarning(
584 issues,
585 "EVA009",
586 "completeness",
587 "The solution is declared feasible while " +
588 "its completeness remains unknown.");
589 }
590 }
591
592 private static void ValidateDecisions(
593 LotSizingSolution solution,
594 ICollection<SolutionValidationIssue> issues)
595 {
596 ValidateProductionDecisions(
597 solution,
598 issues);
599
600 ValidateInventoryDecisions(
601 solution,
602 issues);
603
604 ValidateTransportDecisions(
605 solution,
606 issues);
607
608 ValidatePurchaseDecisions(
609 solution,
610 issues);
611
612 ValidateDistributionDecisions(
613 solution,
614 issues);
615
616 ValidateWorkCenterCapacityDecisions(
617 solution,
618 issues);
619
620 ValidateWarehouseCapacityDecisions(
621 solution,
622 issues);
623
624 ValidateTransportResourceCapacityDecisions(
625 solution,
626 issues);
627 }
628
629 private static void ValidateProductionDecisions(
630 LotSizingSolution solution,
631 ICollection<SolutionValidationIssue> issues)
632 {
633 var seenRoutingIds =
634 new HashSet<int>();
635
636 for (int index = 0;
637 index < solution.ProductionDecisions.Count;
638 index++)
639 {
640 ProductionDecision decision =
641 solution.ProductionDecisions[index];
642
643 string path =
644 $"productionDecisions[{index}]";
645
646 ValidateDecisionHorizon(
647 decision.PlanningHorizon,
648 solution.PlanningHorizon,
649 path,
650 issues);
651
652 if (!decision.IsInternallyValid)
653 {
654 AddError(
655 issues,
656 "DEC101",
657 path,
658 "The production decision is not " +
659 "internally valid.");
660 }
661
662 if (!seenRoutingIds.Add(
663 decision.RoutingId))
664 {
665 AddError(
666 issues,
667 "DEC102",
668 path,
669 $"A production decision for routing " +
670 $"{decision.RoutingId} is duplicated.");
671 }
672 }
673 }
674
675 private static void ValidateInventoryDecisions(
676 LotSizingSolution solution,
677 ICollection<SolutionValidationIssue> issues)
678 {
679 var seenKeys =
680 new HashSet<(
681 int ItemId,
682 WarehouseReferenceKind Kind,
683 int ReferenceId)>();
684
685 for (int index = 0;
686 index < solution.InventoryDecisions.Count;
687 index++)
688 {
689 InventoryDecision decision =
690 solution.InventoryDecisions[index];
691
692 string path =
693 $"inventoryDecisions[{index}]";
694
695 ValidateDecisionHorizon(
696 decision.PlanningHorizon,
697 solution.PlanningHorizon,
698 path,
699 issues);
700
701 if (!decision.IsInternallyValid)
702 {
703 AddError(
704 issues,
705 "DEC201",
706 path,
707 "The inventory decision is not " +
708 "internally valid.");
709 }
710
711 var key =
712 (
713 decision.ItemId,
714 decision.Warehouse.Kind,
715 decision.Warehouse.ReferenceId
716 );
717
718 if (!seenKeys.Add(key))
719 {
720 AddError(
721 issues,
722 "DEC202",
723 path,
724 "The item-and-warehouse inventory " +
725 "decision key is duplicated.");
726 }
727 }
728 }
729
730 private static void ValidateTransportDecisions(
731 LotSizingSolution solution,
732 ICollection<SolutionValidationIssue> issues)
733 {
734 var seenKeys =
735 new HashSet<(
736 int ItemId,
737 int ResourceId,
738 WarehouseReferenceKind OriginKind,
739 int OriginId,
740 WarehouseReferenceKind DestinationKind,
741 int DestinationId)>();
742
743 for (int index = 0;
744 index < solution.TransportDecisions.Count;
745 index++)
746 {
747 TransportDecision decision =
748 solution.TransportDecisions[index];
749
750 string path =
751 $"transportDecisions[{index}]";
752
753 ValidateDecisionHorizon(
754 decision.PlanningHorizon,
755 solution.PlanningHorizon,
756 path,
757 issues);
758
759 if (!decision.IsInternallyValid)
760 {
761 AddError(
762 issues,
763 "DEC301",
764 path,
765 "The transport decision is not " +
766 "internally valid.");
767 }
768
769 var key =
770 (
771 decision.ItemId,
772 decision.TransportResourceId,
773 decision.Origin.Kind,
774 decision.Origin.ReferenceId,
775 decision.Destination.Kind,
776 decision.Destination.ReferenceId
777 );
778
779 if (!seenKeys.Add(key))
780 {
781 AddError(
782 issues,
783 "DEC302",
784 path,
785 "The item-resource-lane transport " +
786 "decision key is duplicated.");
787 }
788 }
789 }
790
791 private static void ValidatePurchaseDecisions(
792 LotSizingSolution solution,
793 ICollection<SolutionValidationIssue> issues)
794 {
795 var seenKeys =
796 new HashSet<(
797 int SupplierId,
798 int ItemId,
799 WarehouseReferenceKind Kind,
800 int ReferenceId)>();
801
802 for (int index = 0;
803 index < solution.PurchaseDecisions.Count;
804 index++)
805 {
806 PurchaseDecision decision =
807 solution.PurchaseDecisions[index];
808
809 string path =
810 $"purchaseDecisions[{index}]";
811
812 ValidateDecisionHorizon(
813 decision.PlanningHorizon,
814 solution.PlanningHorizon,
815 path,
816 issues);
817
818 if (!decision.IsInternallyValid)
819 {
820 AddError(
821 issues,
822 "DEC401",
823 path,
824 "The purchase decision is not " +
825 "internally valid.");
826 }
827
828 var key =
829 (
830 decision.SupplierId,
831 decision.ItemId,
832 decision.DestinationWarehouse.Kind,
833 decision.DestinationWarehouse.ReferenceId
834 );
835
836 if (!seenKeys.Add(key))
837 {
838 AddError(
839 issues,
840 "DEC402",
841 path,
842 "The supplier-item-warehouse purchase " +
843 "decision key is duplicated.");
844 }
845 }
846 }
847
848 private static void ValidateDistributionDecisions(
849 LotSizingSolution solution,
850 ICollection<SolutionValidationIssue> issues)
851 {
852 var seenKeys =
853 new HashSet<(
854 int DistributionCenterId,
855 int ItemId,
856 WarehouseReferenceKind Kind,
857 int ReferenceId)>();
858
859 for (int index = 0;
860 index < solution.DistributionDecisions.Count;
861 index++)
862 {
863 DistributionDecision decision =
864 solution.DistributionDecisions[index];
865
866 string path =
867 $"distributionDecisions[{index}]";
868
869 ValidateDecisionHorizon(
870 decision.PlanningHorizon,
871 solution.PlanningHorizon,
872 path,
873 issues);
874
875 if (!decision.IsInternallyValid)
876 {
877 AddError(
878 issues,
879 "DEC501",
880 path,
881 "The distribution decision is not " +
882 "internally valid.");
883 }
884
885 var key =
886 (
887 decision.DistributionCenterId,
888 decision.ItemId,
889 decision.Warehouse.Kind,
890 decision.Warehouse.ReferenceId
891 );
892
893 if (!seenKeys.Add(key))
894 {
895 AddError(
896 issues,
897 "DEC502",
898 path,
899 "The distribution-center-item-warehouse " +
900 "decision key is duplicated.");
901 }
902 }
903 }
904
905 private static void ValidateWorkCenterCapacityDecisions(
906 LotSizingSolution solution,
907 ICollection<SolutionValidationIssue> issues)
908 {
909 var seenKeys =
910 new HashSet<(int PlantId, int WorkCenterId)>();
911
912 for (int index = 0;
913 index <
914 solution.WorkCenterCapacityDecisions.Count;
915 index++)
916 {
917 WorkCenterCapacityDecision decision =
918 solution.WorkCenterCapacityDecisions[index];
919
920 string path =
921 $"workCenterCapacityDecisions[{index}]";
922
923 ValidateDecisionHorizon(
924 decision.PlanningHorizon,
925 solution.PlanningHorizon,
926 path,
927 issues);
928
929 if (!decision.IsInternallyValid)
930 {
931 AddError(
932 issues,
933 "DEC601",
934 path,
935 "The work-center capacity decision " +
936 "is not internally valid.");
937 }
938
939 var key =
940 (
941 decision.WorkCenter.PlantId,
942 decision.WorkCenter.WorkCenterId
943 );
944
945 if (!seenKeys.Add(key))
946 {
947 AddError(
948 issues,
949 "DEC602",
950 path,
951 "The work-center capacity decision " +
952 "key is duplicated.");
953 }
954 }
955 }
956
957 private static void ValidateWarehouseCapacityDecisions(
958 LotSizingSolution solution,
959 ICollection<SolutionValidationIssue> issues)
960 {
961 var seenKeys =
962 new HashSet<(
963 WarehouseReferenceKind Kind,
964 int ReferenceId)>();
965
966 for (int index = 0;
967 index <
968 solution.WarehouseCapacityDecisions.Count;
969 index++)
970 {
971 WarehouseCapacityDecision decision =
972 solution.WarehouseCapacityDecisions[index];
973
974 string path =
975 $"warehouseCapacityDecisions[{index}]";
976
977 ValidateDecisionHorizon(
978 decision.PlanningHorizon,
979 solution.PlanningHorizon,
980 path,
981 issues);
982
983 if (!decision.IsInternallyValid)
984 {
985 AddError(
986 issues,
987 "DEC701",
988 path,
989 "The warehouse-capacity decision " +
990 "is not internally valid.");
991 }
992
993 var key =
994 (
995 decision.Warehouse.Kind,
996 decision.Warehouse.ReferenceId
997 );
998
999 if (!seenKeys.Add(key))
1000 {
1001 AddError(
1002 issues,
1003 "DEC702",
1004 path,
1005 "The warehouse-capacity decision " +
1006 "key is duplicated.");
1007 }
1008 }
1009 }
1010
1011 private static void
1012 ValidateTransportResourceCapacityDecisions(
1013 LotSizingSolution solution,
1014 ICollection<SolutionValidationIssue> issues)
1015 {
1016 var seenResourceIds =
1017 new HashSet<int>();
1018
1019 for (int index = 0;
1020 index <
1022 index++)
1023 {
1024 TransportResourceCapacityDecision decision =
1026 index];
1027
1028 string path =
1029 $"transportResourceCapacityDecisions[{index}]";
1030
1031 ValidateDecisionHorizon(
1032 decision.PlanningHorizon,
1033 solution.PlanningHorizon,
1034 path,
1035 issues);
1036
1037 if (!decision.IsInternallyValid)
1038 {
1039 AddError(
1040 issues,
1041 "DEC801",
1042 path,
1043 "The transport-resource capacity decision " +
1044 "is not internally valid.");
1045 }
1046
1047 if (!seenResourceIds.Add(
1048 decision.TransportResourceId))
1049 {
1050 AddError(
1051 issues,
1052 "DEC802",
1053 path,
1054 "The transport-resource capacity decision " +
1055 "key is duplicated.");
1056 }
1057 }
1058 }
1059
1060 private static void ValidateAgainstSupplyChain(
1061 LotSizingSolution solution,
1062 SupplyChain supplyChain,
1063 ICollection<SolutionValidationIssue> issues)
1064 {
1065 if (solution.PlanningHorizon !=
1066 supplyChain.PlanningHorizon)
1067 {
1068 AddError(
1069 issues,
1070 "INS001",
1071 "planningHorizon",
1072 $"The solution horizon " +
1073 $"({solution.PlanningHorizon}) differs from " +
1074 $"the instance horizon " +
1075 $"({supplyChain.PlanningHorizon}).");
1076 }
1077
1078 ValidateProductionReferences(
1079 solution,
1080 supplyChain,
1081 issues);
1082
1083 ValidateInventoryReferences(
1084 solution,
1085 supplyChain,
1086 issues);
1087
1088 ValidateTransportReferences(
1089 solution,
1090 supplyChain,
1091 issues);
1092
1093 ValidatePurchaseReferences(
1094 solution,
1095 supplyChain,
1096 issues);
1097
1098 ValidateDistributionReferences(
1099 solution,
1100 supplyChain,
1101 issues);
1102
1103 ValidateCapacityReferences(
1104 solution,
1105 supplyChain,
1106 issues);
1107 }
1108
1109 private static void ValidateProductionReferences(
1110 LotSizingSolution solution,
1111 SupplyChain supplyChain,
1112 ICollection<SolutionValidationIssue> issues)
1113 {
1114 for (int index = 0;
1115 index < solution.ProductionDecisions.Count;
1116 index++)
1117 {
1118 ProductionDecision decision =
1119 solution.ProductionDecisions[index];
1120
1121 bool routingExists =
1122 supplyChain.ProductionRoutings.Any(
1123 routing =>
1124 routing.Id ==
1125 decision.RoutingId);
1126
1127 if (!routingExists)
1128 {
1129 AddError(
1130 issues,
1131 "REF101",
1132 $"productionDecisions[{index}].routingId",
1133 $"Production routing " +
1134 $"{decision.RoutingId} does not exist " +
1135 "in the supply-chain instance.");
1136 }
1137 }
1138 }
1139
1140 private static void ValidateInventoryReferences(
1141 LotSizingSolution solution,
1142 SupplyChain supplyChain,
1143 ICollection<SolutionValidationIssue> issues)
1144 {
1145 for (int index = 0;
1146 index < solution.InventoryDecisions.Count;
1147 index++)
1148 {
1149 InventoryDecision decision =
1150 solution.InventoryDecisions[index];
1151
1152 string path =
1153 $"inventoryDecisions[{index}]";
1154
1155 ValidateItemReference(
1156 decision.ItemId,
1157 supplyChain,
1158 $"{path}.itemId",
1159 issues);
1160
1161 ValidateWarehouseReference(
1162 decision.Warehouse,
1163 supplyChain,
1164 $"{path}.warehouse",
1165 issues);
1166
1167 bool inventoryExists =
1168 supplyChain.Inventories.Any(
1169 inventory =>
1170 inventory.ItemId ==
1171 decision.ItemId &&
1172 SameWarehouse(
1173 inventory.Warehouse,
1174 decision.Warehouse));
1175
1176 if (!inventoryExists)
1177 {
1178 AddError(
1179 issues,
1180 "REF201",
1181 path,
1182 "No inventory relationship exists " +
1183 "for this item and warehouse.");
1184 }
1185 }
1186 }
1187
1188 private static void ValidateTransportReferences(
1189 LotSizingSolution solution,
1190 SupplyChain supplyChain,
1191 ICollection<SolutionValidationIssue> issues)
1192 {
1193 for (int index = 0;
1194 index < solution.TransportDecisions.Count;
1195 index++)
1196 {
1197 TransportDecision decision =
1198 solution.TransportDecisions[index];
1199
1200 string path =
1201 $"transportDecisions[{index}]";
1202
1203 ValidateItemReference(
1204 decision.ItemId,
1205 supplyChain,
1206 $"{path}.itemId",
1207 issues);
1208
1209 ValidateWarehouseReference(
1210 decision.Origin,
1211 supplyChain,
1212 $"{path}.origin",
1213 issues);
1214
1215 ValidateWarehouseReference(
1216 decision.Destination,
1217 supplyChain,
1218 $"{path}.destination",
1219 issues);
1220
1221 var resource =
1222 supplyChain.TransportResources
1223 .FirstOrDefault(
1224 current =>
1225 current.Id ==
1226 decision.TransportResourceId);
1227
1228 if (resource is null)
1229 {
1230 AddError(
1231 issues,
1232 "REF301",
1233 $"{path}.transportResourceId",
1234 $"Transport resource " +
1235 $"{decision.TransportResourceId} " +
1236 "does not exist.");
1237
1238 continue;
1239 }
1240
1241 bool characteristicExists =
1242 supplyChain.TransportCharacteristics.Any(
1243 characteristic =>
1244 characteristic.ItemId ==
1245 decision.ItemId &&
1246 characteristic.TransportResourceId ==
1247 decision.TransportResourceId);
1248
1249 if (!characteristicExists)
1250 {
1251 AddError(
1252 issues,
1253 "REF302",
1254 path,
1255 "No transport characteristic exists " +
1256 "for this item and transport resource.");
1257 }
1258
1259 bool laneExists =
1260 supplyChain.GetTransportLanes(resource.Id).Any(
1261 lane =>
1262 SameWarehouse(
1263 lane.Origin,
1264 decision.Origin) &&
1265 SameWarehouse(
1266 lane.Destination,
1267 decision.Destination));
1268
1269 if (!laneExists)
1270 {
1271 AddError(
1272 issues,
1273 "REF303",
1274 path,
1275 "The directed transport lane does not " +
1276 "exist for this transport resource.");
1277 }
1278 }
1279 }
1280
1281 private static void ValidatePurchaseReferences(
1282 LotSizingSolution solution,
1283 SupplyChain supplyChain,
1284 ICollection<SolutionValidationIssue> issues)
1285 {
1286 for (int index = 0;
1287 index < solution.PurchaseDecisions.Count;
1288 index++)
1289 {
1290 PurchaseDecision decision =
1291 solution.PurchaseDecisions[index];
1292
1293 string path =
1294 $"purchaseDecisions[{index}]";
1295
1296 ValidateItemReference(
1297 decision.ItemId,
1298 supplyChain,
1299 $"{path}.itemId",
1300 issues);
1301
1302 ValidateWarehouseReference(
1303 decision.DestinationWarehouse,
1304 supplyChain,
1305 $"{path}.destinationWarehouse",
1306 issues);
1307
1308 bool supplierExists =
1309 supplyChain.Suppliers.Any(
1310 supplier =>
1311 supplier.Id ==
1312 decision.SupplierId);
1313
1314 if (!supplierExists)
1315 {
1316 AddError(
1317 issues,
1318 "REF401",
1319 $"{path}.supplierId",
1320 $"Supplier {decision.SupplierId} " +
1321 "does not exist.");
1322 }
1323
1324 bool deliveryExists =
1325 supplyChain.SupplierDeliveries.Any(
1326 delivery =>
1327 delivery.SupplierId ==
1328 decision.SupplierId &&
1329 delivery.ItemId ==
1330 decision.ItemId &&
1331 SameWarehouse(
1332 delivery.Warehouse,
1333 decision.DestinationWarehouse));
1334
1335 if (!deliveryExists)
1336 {
1337 AddError(
1338 issues,
1339 "REF402",
1340 path,
1341 "No supplier-delivery relationship exists " +
1342 "for this supplier, item and warehouse.");
1343 }
1344 }
1345 }
1346
1347 private static void ValidateDistributionReferences(
1348 LotSizingSolution solution,
1349 SupplyChain supplyChain,
1350 ICollection<SolutionValidationIssue> issues)
1351 {
1352 for (int index = 0;
1353 index < solution.DistributionDecisions.Count;
1354 index++)
1355 {
1356 DistributionDecision decision =
1357 solution.DistributionDecisions[index];
1358
1359 string path =
1360 $"distributionDecisions[{index}]";
1361
1362 ValidateItemReference(
1363 decision.ItemId,
1364 supplyChain,
1365 $"{path}.itemId",
1366 issues);
1367
1368 ValidateWarehouseReference(
1369 decision.Warehouse,
1370 supplyChain,
1371 $"{path}.warehouse",
1372 issues);
1373
1374 bool distributionCenterExists =
1375 supplyChain.DistributionCenters.Any(
1376 distributionCenter =>
1377 distributionCenter.Id ==
1378 decision.DistributionCenterId);
1379
1380 if (!distributionCenterExists)
1381 {
1382 AddError(
1383 issues,
1384 "REF501",
1385 $"{path}.distributionCenterId",
1386 $"Distribution center " +
1387 $"{decision.DistributionCenterId} " +
1388 "does not exist.");
1389 }
1390
1391 bool sourcingExists =
1392 supplyChain.DistributionCenterSourcings.Any(
1393 sourcing =>
1394 sourcing.DistributionCenterId ==
1395 decision.DistributionCenterId &&
1396 sourcing.ItemId ==
1397 decision.ItemId &&
1398 SameWarehouse(
1399 sourcing.Warehouse,
1400 decision.Warehouse));
1401
1402 if (!sourcingExists)
1403 {
1404 AddError(
1405 issues,
1406 "REF502",
1407 path,
1408 "No distribution-center sourcing " +
1409 "relationship exists for this center, " +
1410 "item and warehouse.");
1411 }
1412 }
1413 }
1414
1415 private static void ValidateCapacityReferences(
1416 LotSizingSolution solution,
1417 SupplyChain supplyChain,
1418 ICollection<SolutionValidationIssue> issues)
1419 {
1420 for (int index = 0;
1421 index <
1422 solution.WorkCenterCapacityDecisions.Count;
1423 index++)
1424 {
1425 WorkCenterCapacityDecision decision =
1426 solution.WorkCenterCapacityDecisions[index];
1427
1428 bool workCenterExists =
1429 supplyChain.Plants.Any(
1430 plant =>
1431 plant.Id ==
1432 decision.WorkCenter.PlantId &&
1433 plant.WorkCenters.Any(
1434 workCenter =>
1435 workCenter.Id ==
1436 decision.WorkCenter
1437 .WorkCenterId));
1438
1439 if (!workCenterExists)
1440 {
1441 AddError(
1442 issues,
1443 "REF601",
1444 $"workCenterCapacityDecisions[{index}]" +
1445 ".workCenter",
1446 $"Work center " +
1447 $"{decision.WorkCenter.PlantId}:" +
1448 $"{decision.WorkCenter.WorkCenterId} " +
1449 "does not exist.");
1450 }
1451 }
1452
1453 for (int index = 0;
1454 index <
1455 solution.WarehouseCapacityDecisions.Count;
1456 index++)
1457 {
1458 WarehouseCapacityDecision decision =
1459 solution.WarehouseCapacityDecisions[index];
1460
1461 ValidateWarehouseReference(
1462 decision.Warehouse,
1463 supplyChain,
1464 $"warehouseCapacityDecisions[{index}]" +
1465 ".warehouse",
1466 issues);
1467 }
1468
1469 for (int index = 0;
1470 index <
1472 index++)
1473 {
1474 TransportResourceCapacityDecision decision =
1476 index];
1477
1478 bool resourceExists =
1479 supplyChain.TransportResources.Any(
1480 resource =>
1481 resource.Id ==
1482 decision.TransportResourceId);
1483
1484 if (!resourceExists)
1485 {
1486 AddError(
1487 issues,
1488 "REF701",
1489 $"transportResourceCapacityDecisions" +
1490 $"[{index}].transportResourceId",
1491 $"Transport resource " +
1492 $"{decision.TransportResourceId} " +
1493 "does not exist.");
1494 }
1495 }
1496 }
1497
1498 private static void ValidateItemReference(
1499 int itemId,
1500 SupplyChain supplyChain,
1501 string path,
1502 ICollection<SolutionValidationIssue> issues)
1503 {
1504 bool itemExists =
1505 supplyChain.Items.Any(
1506 item =>
1507 item.Id == itemId);
1508
1509 if (!itemExists)
1510 {
1511 AddError(
1512 issues,
1513 "REF001",
1514 path,
1515 $"Item {itemId} does not exist.");
1516 }
1517 }
1518
1519 private static void ValidateWarehouseReference(
1520 WarehouseReference warehouse,
1521 SupplyChain supplyChain,
1522 string path,
1523 ICollection<SolutionValidationIssue> issues)
1524 {
1525 if (!WarehouseExists(
1526 warehouse,
1527 supplyChain))
1528 {
1529 AddError(
1530 issues,
1531 "REF002",
1532 path,
1533 $"Warehouse {warehouse.Kind}:" +
1534 $"{warehouse.ReferenceId} does not exist.");
1535 }
1536 }
1537
1538 private static bool WarehouseExists(
1539 WarehouseReference warehouse,
1540 SupplyChain supplyChain)
1541 {
1542 return warehouse.Kind switch
1543 {
1544 WarehouseReferenceKind.StandaloneWarehouse =>
1545 supplyChain.StandaloneWarehouses.Any(
1546 current =>
1547 current.Id ==
1548 warehouse.ReferenceId),
1549
1550 WarehouseReferenceKind.PlantWarehouse =>
1551 supplyChain.Plants.Any(
1552 plant =>
1553 plant.Id ==
1554 warehouse.ReferenceId),
1555
1556 _ => false
1557 };
1558 }
1559
1560 private static void ValidateDecisionHorizon(
1561 int decisionHorizon,
1562 int solutionHorizon,
1563 string path,
1564 ICollection<SolutionValidationIssue> issues)
1565 {
1566 if (decisionHorizon != solutionHorizon)
1567 {
1568 AddError(
1569 issues,
1570 "DEC001",
1571 path,
1572 $"The decision horizon ({decisionHorizon}) " +
1573 $"differs from the solution horizon " +
1574 $"({solutionHorizon}).");
1575 }
1576 }
1577
1578 private static bool SameWarehouse(
1579 WarehouseReference first,
1580 WarehouseReference second)
1581 {
1582 return first.Kind ==
1583 second.Kind &&
1584 first.ReferenceId ==
1585 second.ReferenceId;
1586 }
1587
1588 private static void ThrowIfErrors(
1589 IEnumerable<SolutionValidationIssue> issues)
1590 {
1591 SolutionValidationIssue[] errors =
1592 issues
1593 .Where(
1594 issue =>
1595 issue.Severity ==
1597 .ToArray();
1598
1599 if (errors.Length == 0)
1600 {
1601 return;
1602 }
1603
1604 string message =
1605 "The lot-sizing solution is invalid:" +
1606 Environment.NewLine +
1607 string.Join(
1608 Environment.NewLine,
1609 errors.Select(
1610 error =>
1611 $"- {error}"));
1612
1613 throw new InvalidOperationException(
1614 message);
1615 }
1616
1617 private static void AddError(
1618 ICollection<SolutionValidationIssue> issues,
1619 string code,
1620 string path,
1621 string message)
1622 {
1623 issues.Add(
1624 new SolutionValidationIssue(
1626 code,
1627 path,
1628 message));
1629 }
1630
1631 private static void AddWarning(
1632 ICollection<SolutionValidationIssue> issues,
1633 string code,
1634 string path,
1635 string message)
1636 {
1637 issues.Add(
1638 new SolutionValidationIssue(
1640 code,
1641 path,
1642 message));
1643 }
1644}
int PlanningHorizon
Gets the number of planning periods represented by the delivered-quantity series.
WarehouseReference Warehouse
Gets or sets the warehouse from which the item is delivered.
int ItemId
Gets or sets the identifier of the distributed item.
int DistributionCenterId
Gets or sets the identifier of the distribution center.
bool IsInternallyValid
Gets a value indicating whether the distribution decision is internally consistent.
int ItemId
Gets or sets the identifier of the stored item.
WarehouseReference Warehouse
Gets or sets the reference to the warehouse associated with the inventory decision.
int PlanningHorizon
Gets the number of planning periods represented by the inventory-level series.
bool IsInternallyValid
Gets a value indicating whether the inventory decision is internally consistent.
int PlanningHorizon
Gets the number of planning periods represented by the production-quantity series.
int RoutingId
Gets or sets the identifier of the production routing associated with the decision.
bool IsInternallyValid
Gets a value indicating whether the production decision is internally consistent.
int PlanningHorizon
Gets the number of planning periods represented by the purchased-quantity series.
WarehouseReference DestinationWarehouse
Gets or sets the warehouse to which the purchased item is delivered.
int ItemId
Gets or sets the identifier of the purchased item.
int SupplierId
Gets or sets the identifier of the supplier associated with the purchase decision.
bool IsInternallyValid
Gets a value indicating whether the purchase decision is internally consistent.
WarehouseReference Destination
Gets or sets the destination warehouse reference.
bool IsInternallyValid
Gets a value indicating whether the transport decision is internally consistent.
int TransportResourceId
Gets or sets the identifier of the transport resource used by the decision.
int PlanningHorizon
Gets the number of planning periods represented by the transported-quantity series.
WarehouseReference Origin
Gets or sets the origin warehouse reference.
int ItemId
Gets or sets the identifier of the transported item.
int PlanningHorizon
Gets the number of planning periods represented by the activation series.
bool IsInternallyValid
Gets a value indicating whether the transport-resource capacity decision is internally consistent.
int TransportResourceId
Gets or sets the identifier of the transport resource associated with the capacity decision.
WarehouseReference Warehouse
Gets or sets the warehouse associated with the capacity decision.
bool IsInternallyValid
Gets a value indicating whether the warehouse-capacity decision is internally consistent.
int PlanningHorizon
Gets the number of planning periods represented by the activation series.
int PlanningHorizon
Gets the number of planning periods represented by the activation series.
WorkCenterReference WorkCenter
Gets or sets the reference to the work center associated with the capacity decision.
bool IsInternallyValid
Gets a value indicating whether the work-center capacity decision is internally consistent.
double? AbsoluteGap
Gets or sets the absolute optimality gap.
double? BestBound
Gets or sets a valid bound on the optimal objective value.
double? MaximumConstraintViolation
Gets or sets the largest absolute constraint violation detected in the solution.
double? RelativeGap
Gets or sets the relative optimality gap.
OptimalityStatus OptimalityStatus
Gets or sets the available information about the optimality of the solution.
FeasibilityStatus FeasibilityStatus
Gets or sets the evaluated feasibility status of the solution.
double? TotalConstraintViolation
Gets or sets the sum of the absolute constraint violations detected in the solution.
double? ObjectiveValue
Gets or sets the evaluated objective value.
Represents one candidate solution for a lot-sizing supply-chain instance.
List< ProductionDecision > ProductionDecisions
Gets the production decisions contained in the solution.
List< WorkCenterCapacityDecision > WorkCenterCapacityDecisions
Gets the global work-center capacity decisions contained in the solution.
SolutionCompleteness Completeness
Gets or sets whether the solution contains all decisions expected for the associated instance.
SolutionGenerationMetadata GenerationMetadata
Gets or sets the metadata describing how the solution was generated.
int PlanningHorizon
Gets or sets the number of planning periods represented by the solution.
List< TransportDecision > TransportDecisions
Gets the transport decisions contained in the solution.
string InstanceIdentifier
Gets or sets the identifier of the supply-chain instance associated with the solution.
Guid Id
Gets or sets the unique identifier of the solution.
List< PurchaseDecision > PurchaseDecisions
Gets the purchase decisions contained in the solution.
List< DistributionDecision > DistributionDecisions
Gets the distribution decisions contained in the solution.
List< WarehouseCapacityDecision > WarehouseCapacityDecisions
Gets the global warehouse-capacity decisions contained in the solution.
SolutionEvaluation Evaluation
Gets or sets the independent evaluation of the solution.
bool HasDecisions
Gets a value indicating whether the solution contains at least one decision object.
List< TransportResourceCapacityDecision > TransportResourceCapacityDecisions
Gets the global transport-resource capacity decisions contained in the solution.
List< InventoryDecision > InventoryDecisions
Gets the inventory decisions contained in the solution.
IReadOnlyList< SolutionValidationIssue > Validate(LotSizingSolution solution, SupplyChain supplyChain)
Validates a solution and its compatibility with a supply-chain instance.
double NumericalTolerance
Gets the numerical tolerance used by the validator.
void ThrowIfInvalid(LotSizingSolution solution)
Validates a solution and throws an exception when at least one error is detected.
IReadOnlyList< SolutionValidationIssue > Validate(LotSizingSolution solution)
Validates the internal structure of a solution.
void ThrowIfInvalid(LotSizingSolution solution, SupplyChain supplyChain)
Validates a solution against a supply-chain instance and throws an exception when an error is detecte...
bool IsValid(LotSizingSolution solution, SupplyChain supplyChain)
Determines whether a solution is structurally valid and compatible with a supply-chain instance.
bool IsValid(LotSizingSolution solution)
Determines whether a solution contains no internal validation errors.
LotSizingSolutionValidator(double numericalTolerance=1e-9)
Initializes a solution validator.
string Message
Gets the human-readable description of the issue.
SolutionValidationIssue(SolutionValidationSeverity severity, string code, string path, string message)
Initializes a solution-validation issue.
SolutionValidationSeverity Severity
Gets the severity of the issue.
string Path
Gets the logical path of the affected value.
SolutionCompleteness
Indicates whether a lot-sizing solution contains all decision values expected for its associated inst...
TerminationReason
Identifies the reason why a solution-generation execution was terminated.
SolutionMethodKind
Identifies the general type of method used to generate a lot-sizing solution.
FeasibilityStatus
Indicates the evaluated feasibility status of a lot-sizing solution candidate.
OptimalityStatus
Indicates the available information about the optimality of a lot-sizing solution.
SolutionValidationSeverity
Identifies the severity of a solution-validation issue.
@ Warning
The issue does not necessarily make the solution invalid, but should be reviewed.
@ Error
The issue makes the solution structurally invalid or incompatible with its associated instance.