LotSizingDataModel.Instance 2.0.1
Lot-sizing instance representation, descriptors and problem characterization.
Loading...
Searching...
No Matches
ProductStructureAnalyzer.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using LotSizingDataModel.Core;
7
9
10/// <summary>
11/// Analyzes the bill-of-materials graph of a supply-chain
12/// instance and determines its product-structure type.
13/// </summary>
14/// <remarks>
15/// Each directed graph arc goes from a component item to
16/// an immediate parent item that consumes the component.
17///
18/// The analyzer does not modify the supplied
19/// <see cref="SupplyChain"/>.
20/// </remarks>
21public static class ProductStructureAnalyzer
22{
23 /// <summary>
24 /// Gets the current version of the product-structure
25 /// classification rules.
26 /// </summary>
27 public const string CurrentVersion = "1.0";
28
29 /// <summary>
30 /// Analyzes the product structure of a supply-chain
31 /// instance.
32 /// </summary>
33 /// <param name="supplyChain">
34 /// Supply-chain instance to analyze.
35 /// </param>
36 /// <returns>
37 /// Detailed product-structure analysis.
38 /// </returns>
40 SupplyChain supplyChain)
41 {
42 ArgumentNullException.ThrowIfNull(supplyChain);
43
44 var errors =
45 new List<string>();
46
47 var warnings =
48 new List<string>();
49
50 ValidateItemIdentifiers(
51 supplyChain,
52 errors);
53
54 int[] itemIds =
55 supplyChain.Items
56 .Select(item => item.Id)
57 .Where(itemId => itemId > 0)
58 .Distinct()
59 .OrderBy(itemId => itemId)
60 .ToArray();
61
62 var itemIdSet =
63 itemIds.ToHashSet();
64
65 if (itemIds.Length == 0)
66 {
67 errors.Add(
68 "The supply-chain instance does not contain " +
69 "any valid item.");
70 }
71
72 /*
73 * Directed graph orientation:
74 *
75 * component item -> immediate parent item
76 */
77 Dictionary<int, HashSet<int>>
78 parentItemsByComponent =
79 itemIds.ToDictionary(
80 itemId => itemId,
81 _ => new HashSet<int>());
82
83 Dictionary<int, HashSet<int>>
84 componentItemsByParent =
85 itemIds.ToDictionary(
86 itemId => itemId,
87 _ => new HashSet<int>());
88
89 BuildGraph(
90 supplyChain,
91 itemIdSet,
92 parentItemsByComponent,
93 componentItemsByParent,
94 errors,
95 warnings);
96
97 int relationshipCount =
98 parentItemsByComponent.Values.Sum(
99 parentIds => parentIds.Count);
100
101 int[] rootItemIds =
102 itemIds
103 .Where(
104 itemId =>
105 parentItemsByComponent[itemId]
106 .Count == 0)
107 .ToArray();
108
109 int[] leafItemIds =
110 itemIds
111 .Where(
112 itemId =>
113 componentItemsByParent[itemId]
114 .Count == 0)
115 .ToArray();
116
117 int[] isolatedItemIds =
118 itemIds
119 .Where(
120 itemId =>
121 parentItemsByComponent[itemId]
122 .Count == 0 &&
123 componentItemsByParent[itemId]
124 .Count == 0)
125 .ToArray();
126
127 int[] sharedComponentItemIds =
128 itemIds
129 .Where(
130 itemId =>
131 parentItemsByComponent[itemId]
132 .Count > 1)
133 .ToArray();
134
135 int maximumImmediateComponentCount =
136 itemIds.Length == 0
137 ? 0
138 : itemIds.Max(
139 itemId =>
140 componentItemsByParent[itemId]
141 .Count);
142
143 int maximumImmediateParentCount =
144 itemIds.Length == 0
145 ? 0
146 : itemIds.Max(
147 itemId =>
148 parentItemsByComponent[itemId]
149 .Count);
150
151 int connectedComponentCount =
152 CountConnectedComponents(
153 itemIds,
154 parentItemsByComponent,
155 componentItemsByParent);
156
157 int[] cyclicItemIds =
158 FindCyclicItemIds(
159 itemIds,
160 parentItemsByComponent);
161
162 if (cyclicItemIds.Length > 0)
163 {
164 errors.Add(
165 "The bill-of-materials graph contains at " +
166 "least one directed cycle involving item " +
167 "identifiers: " +
168 string.Join(", ", cyclicItemIds) +
169 ".");
170 }
171
172 int maximumDepth =
173 cyclicItemIds.Length == 0
174 ? CalculateMaximumDepth(
175 itemIds,
176 parentItemsByComponent,
177 componentItemsByParent)
178 : 0;
179
180 ProductStructureType detectedType =
181 DetermineStructureType(
182 relationshipCount,
183 maximumImmediateComponentCount,
184 maximumImmediateParentCount,
185 errors,
186 cyclicItemIds);
187
188 return new ProductStructureAnalysis(
189 detectedType:
190 detectedType,
191
192 itemCount:
193 itemIds.Length,
194
195 relationshipCount:
196 relationshipCount,
197
198 connectedComponentCount:
199 connectedComponentCount,
200
201 maximumDepth:
202 maximumDepth,
203
204 maximumImmediateComponentCount:
205 maximumImmediateComponentCount,
206
207 maximumImmediateParentCount:
208 maximumImmediateParentCount,
209
210 rootItemIds:
211 rootItemIds,
212
213 leafItemIds:
214 leafItemIds,
215
216 isolatedItemIds:
217 isolatedItemIds,
218
219 sharedComponentItemIds:
220 sharedComponentItemIds,
221
222 cyclicItemIds:
223 cyclicItemIds,
224
225 errors:
226 errors,
227
228 warnings:
229 warnings);
230 }
231
232 /// <summary>
233 /// Analyzes a supply-chain product structure and applies
234 /// the result to a persistent descriptor.
235 /// </summary>
236 /// <param name="supplyChain">
237 /// Supply-chain instance to analyze.
238 /// </param>
239 /// <param name="descriptor">
240 /// Descriptor to update with the analysis result.
241 /// </param>
242 /// <param name="supplyChainFingerprint">
243 /// Optional fingerprint of the analyzed supply chain.
244 /// </param>
245 /// <returns>
246 /// Detailed product-structure analysis.
247 /// </returns>
249 SupplyChain supplyChain,
251 string supplyChainFingerprint = "")
252 {
253 ArgumentNullException.ThrowIfNull(supplyChain);
254 ArgumentNullException.ThrowIfNull(descriptor);
255
256 ProductStructureAnalysis analysis =
257 Analyze(supplyChain);
258
260 analysis,
261 descriptor,
262 supplyChainFingerprint);
263
264 return analysis;
265 }
266
267 /// <summary>
268 /// Applies an existing analysis result to a persistent
269 /// product-structure descriptor.
270 /// </summary>
271 /// <param name="analysis">
272 /// Analysis result to apply.
273 /// </param>
274 /// <param name="descriptor">
275 /// Descriptor to update.
276 /// </param>
277 /// <param name="supplyChainFingerprint">
278 /// Optional fingerprint of the analyzed supply chain.
279 /// </param>
280 public static void ApplyAnalysis(
283 string supplyChainFingerprint = "")
284 {
285 ArgumentNullException.ThrowIfNull(analysis);
286 ArgumentNullException.ThrowIfNull(descriptor);
287
288 descriptor.DetectedType =
289 analysis.DetectedType;
290
291 descriptor.HasCycle =
292 analysis.HasCycle;
293
294 descriptor.MaximumDepth =
295 analysis.MaximumDepth;
296
297 descriptor.ReplaceAnalyzedItemSets(
298 analysis.RootItemIds,
299 analysis.LeafItemIds,
300 analysis.SharedComponentItemIds);
301
302 descriptor.AnalyzedAtUtc =
303 DateTime.UtcNow;
304
305 descriptor.AnalyzerVersion =
307
308 descriptor.SupplyChainFingerprint =
309 supplyChainFingerprint?.Trim() ??
310 string.Empty;
311
312 descriptor.AnalysisComment =
313 BuildAnalysisComment(analysis);
314
315 descriptor.CheckStatus =
316 DetermineCheckStatus(
317 descriptor.DeclaredType,
318 analysis);
319 }
320
321 private static void ValidateItemIdentifiers(
322 SupplyChain supplyChain,
323 ICollection<string> errors)
324 {
325 for (int index = 0;
326 index < supplyChain.Items.Count;
327 index++)
328 {
329 int itemId =
330 supplyChain.Items[index].Id;
331
332 if (itemId <= 0)
333 {
334 errors.Add(
335 $"Item at index {index} has an invalid " +
336 $"identifier ({itemId}).");
337 }
338 }
339
340 int[] duplicateItemIds =
341 supplyChain.Items
342 .GroupBy(item => item.Id)
343 .Where(group => group.Count() > 1)
344 .Select(group => group.Key)
345 .OrderBy(itemId => itemId)
346 .ToArray();
347
348 foreach (int duplicateItemId
349 in duplicateItemIds)
350 {
351 errors.Add(
352 $"Item identifier {duplicateItemId} " +
353 "is duplicated.");
354 }
355 }
356
357 private static void BuildGraph(
358 SupplyChain supplyChain,
359 IReadOnlySet<int> itemIds,
360 IDictionary<int, HashSet<int>>
361 parentItemsByComponent,
362 IDictionary<int, HashSet<int>>
363 componentItemsByParent,
364 ICollection<string> errors,
365 ICollection<string> warnings)
366 {
367 for (int index = 0;
368 index <
369 supplyChain.ComponentRequirements.Count;
370 index++)
371 {
372 var requirement =
373 supplyChain.ComponentRequirements[index];
374
375 int parentItemId =
376 requirement.ParentItemId;
377
378 int componentItemId =
379 requirement.ComponentItemId;
380
381 string relationshipDescription =
382 $"component requirement at index {index}";
383
384 bool hasValidIdentifiers =
385 true;
386
387 if (parentItemId <= 0)
388 {
389 errors.Add(
390 $"The {relationshipDescription} has an " +
391 $"invalid parent-item identifier " +
392 $"({parentItemId}).");
393
394 hasValidIdentifiers = false;
395 }
396 else if (!itemIds.Contains(parentItemId))
397 {
398 errors.Add(
399 $"The {relationshipDescription} refers " +
400 $"to unknown parent item " +
401 $"{parentItemId}.");
402
403 hasValidIdentifiers = false;
404 }
405
406 if (componentItemId <= 0)
407 {
408 errors.Add(
409 $"The {relationshipDescription} has an " +
410 $"invalid component-item identifier " +
411 $"({componentItemId}).");
412
413 hasValidIdentifiers = false;
414 }
415 else if (!itemIds.Contains(componentItemId))
416 {
417 errors.Add(
418 $"The {relationshipDescription} refers " +
419 $"to unknown component item " +
420 $"{componentItemId}.");
421
422 hasValidIdentifiers = false;
423 }
424
425 if (requirement.Quantity <= 0)
426 {
427 errors.Add(
428 $"The {relationshipDescription} has a " +
429 $"non-positive requirement quantity " +
430 $"({requirement.Quantity}).");
431 }
432
433 if (!hasValidIdentifiers)
434 {
435 continue;
436 }
437
438 bool relationshipAdded =
439 parentItemsByComponent[
440 componentItemId]
441 .Add(parentItemId);
442
443 if (!relationshipAdded)
444 {
445 warnings.Add(
446 $"The relationship from component " +
447 $"{componentItemId} to parent " +
448 $"{parentItemId} is duplicated.");
449
450 continue;
451 }
452
453 componentItemsByParent[
454 parentItemId]
455 .Add(componentItemId);
456 }
457 }
458
459 private static int CountConnectedComponents(
460 IEnumerable<int> itemIds,
461 IReadOnlyDictionary<int, HashSet<int>>
462 parentItemsByComponent,
463 IReadOnlyDictionary<int, HashSet<int>>
464 componentItemsByParent)
465 {
466 var visited =
467 new HashSet<int>();
468
469 int connectedComponentCount =
470 0;
471
472 foreach (int startItemId
473 in itemIds)
474 {
475 if (!visited.Add(startItemId))
476 {
477 continue;
478 }
479
480 connectedComponentCount++;
481
482 var pendingItemIds =
483 new Stack<int>();
484
485 pendingItemIds.Push(
486 startItemId);
487
488 while (pendingItemIds.Count > 0)
489 {
490 int currentItemId =
491 pendingItemIds.Pop();
492
493 IEnumerable<int> adjacentItemIds =
494 parentItemsByComponent[
495 currentItemId]
496 .Concat(
497 componentItemsByParent[
498 currentItemId]);
499
500 foreach (int adjacentItemId
501 in adjacentItemIds)
502 {
503 if (visited.Add(
504 adjacentItemId))
505 {
506 pendingItemIds.Push(
507 adjacentItemId);
508 }
509 }
510 }
511 }
512
513 return connectedComponentCount;
514 }
515
516 private static int[] FindCyclicItemIds(
517 IEnumerable<int> itemIds,
518 IReadOnlyDictionary<int, HashSet<int>>
519 parentItemsByComponent)
520 {
521 /*
522 * Tarjan's strongly connected component algorithm.
523 *
524 * An item belongs to a cycle when:
525 * - its strongly connected component contains
526 * several items; or
527 * - it has a self-loop.
528 */
529 int nextIndex =
530 0;
531
532 var indices =
533 new Dictionary<int, int>();
534
535 var lowLinks =
536 new Dictionary<int, int>();
537
538 var stack =
539 new Stack<int>();
540
541 var itemsOnStack =
542 new HashSet<int>();
543
544 var cyclicItemIds =
545 new HashSet<int>();
546
547 void Visit(int itemId)
548 {
549 indices[itemId] =
550 nextIndex;
551
552 lowLinks[itemId] =
553 nextIndex;
554
555 nextIndex++;
556
557 stack.Push(itemId);
558 itemsOnStack.Add(itemId);
559
560 foreach (int parentItemId
561 in parentItemsByComponent[itemId])
562 {
563 if (!indices.ContainsKey(
564 parentItemId))
565 {
566 Visit(parentItemId);
567
568 lowLinks[itemId] =
569 Math.Min(
570 lowLinks[itemId],
571 lowLinks[parentItemId]);
572 }
573 else if (itemsOnStack.Contains(
574 parentItemId))
575 {
576 lowLinks[itemId] =
577 Math.Min(
578 lowLinks[itemId],
579 indices[parentItemId]);
580 }
581 }
582
583 if (lowLinks[itemId] !=
584 indices[itemId])
585 {
586 return;
587 }
588
589 var stronglyConnectedComponent =
590 new List<int>();
591
592 while (stack.Count > 0)
593 {
594 int currentItemId =
595 stack.Pop();
596
597 itemsOnStack.Remove(
598 currentItemId);
599
600 stronglyConnectedComponent.Add(
601 currentItemId);
602
603 if (currentItemId == itemId)
604 {
605 break;
606 }
607 }
608
609 bool isCycle =
610 stronglyConnectedComponent.Count > 1;
611
612 if (!isCycle &&
613 stronglyConnectedComponent.Count == 1)
614 {
615 int singleItemId =
616 stronglyConnectedComponent[0];
617
618 isCycle =
619 parentItemsByComponent[
620 singleItemId]
621 .Contains(singleItemId);
622 }
623
624 if (!isCycle)
625 {
626 return;
627 }
628
629 foreach (int cyclicItemId
630 in stronglyConnectedComponent)
631 {
632 cyclicItemIds.Add(
633 cyclicItemId);
634 }
635 }
636
637 foreach (int itemId
638 in itemIds.OrderBy(
639 currentItemId =>
640 currentItemId))
641 {
642 if (!indices.ContainsKey(itemId))
643 {
644 Visit(itemId);
645 }
646 }
647
648 return cyclicItemIds
649 .OrderBy(itemId => itemId)
650 .ToArray();
651 }
652
653 private static int CalculateMaximumDepth(
654 IEnumerable<int> itemIds,
655 IReadOnlyDictionary<int, HashSet<int>>
656 parentItemsByComponent,
657 IReadOnlyDictionary<int, HashSet<int>>
658 componentItemsByParent)
659 {
660 /*
661 * Kahn topological traversal.
662 *
663 * Because arcs go from components to parents,
664 * items without components form the initial queue.
665 */
666 int[] normalizedItemIds =
667 itemIds.ToArray();
668
669 Dictionary<int, int> remainingComponentCounts =
670 normalizedItemIds.ToDictionary(
671 itemId => itemId,
672 itemId =>
673 componentItemsByParent[itemId]
674 .Count);
675
676 Dictionary<int, int> depths =
677 normalizedItemIds.ToDictionary(
678 itemId => itemId,
679 _ => 0);
680
681 var readyItemIds =
682 new SortedSet<int>(
683 normalizedItemIds.Where(
684 itemId =>
685 remainingComponentCounts[itemId]
686 == 0));
687
688 int processedItemCount =
689 0;
690
691 int maximumDepth =
692 0;
693
694 while (readyItemIds.Count > 0)
695 {
696 int itemId =
697 readyItemIds.Min;
698
699 readyItemIds.Remove(itemId);
700 processedItemCount++;
701
702 maximumDepth =
703 Math.Max(
704 maximumDepth,
705 depths[itemId]);
706
707 foreach (int parentItemId
708 in parentItemsByComponent[itemId])
709 {
710 depths[parentItemId] =
711 Math.Max(
712 depths[parentItemId],
713 depths[itemId] + 1);
714
715 remainingComponentCounts[
716 parentItemId]--;
717
718 if (remainingComponentCounts[
719 parentItemId] == 0)
720 {
721 readyItemIds.Add(
722 parentItemId);
723 }
724 }
725 }
726
727 /*
728 * This should only happen if a cycle was not detected
729 * before calling this method.
730 */
731 return processedItemCount ==
732 normalizedItemIds.Length
733 ? maximumDepth
734 : 0;
735 }
736
737 private static ProductStructureType
738 DetermineStructureType(
739 int relationshipCount,
740 int maximumImmediateComponentCount,
741 int maximumImmediateParentCount,
742 IReadOnlyCollection<string> errors,
743 IReadOnlyCollection<int> cyclicItemIds)
744 {
745 if (errors.Count > 0 ||
746 cyclicItemIds.Count > 0)
747 {
748 return ProductStructureType.Unknown;
749 }
750
751 if (relationshipCount == 0)
752 {
753 return
754 ProductStructureType.IndependentItems;
755 }
756
757 bool hasAssemblyNode =
758 maximumImmediateComponentCount > 1;
759
760 bool hasDivergentComponent =
761 maximumImmediateParentCount > 1;
762
763 if (!hasAssemblyNode &&
764 !hasDivergentComponent)
765 {
766 return ProductStructureType.Serial;
767 }
768
769 if (hasAssemblyNode &&
770 !hasDivergentComponent)
771 {
772 return ProductStructureType.Assembly;
773 }
774
775 if (!hasAssemblyNode &&
776 hasDivergentComponent)
777 {
778 return
779 ProductStructureType.Arborescent;
780 }
781
782 return ProductStructureType.General;
783 }
784
785 private static ProductStructureCheckStatus
786 DetermineCheckStatus(
787 ProductStructureType declaredType,
788 ProductStructureAnalysis analysis)
789 {
790 if (!analysis.IsValid)
791 {
792 return
794 }
795
796 if (declaredType ==
797 ProductStructureType.Unknown)
798 {
799 return
800 ProductStructureCheckStatus.DetectedOnly;
801 }
802
803 if (declaredType ==
804 analysis.DetectedType)
805 {
807 .DeclaredAndConfirmed;
808 }
809
811 .DeclaredAndContradicted;
812 }
813
814 private static string BuildAnalysisComment(
815 ProductStructureAnalysis analysis)
816 {
817 var sections =
818 new List<string>();
819
820 if (analysis.Errors.Count > 0)
821 {
822 sections.Add(
823 "Errors: " +
824 string.Join(
825 " | ",
826 analysis.Errors));
827 }
828
829 if (analysis.Warnings.Count > 0)
830 {
831 sections.Add(
832 "Warnings: " +
833 string.Join(
834 " | ",
835 analysis.Warnings));
836 }
837
838 return string.Join(
839 Environment.NewLine,
840 sections);
841 }
842}
Represents the detailed result of an automatic analysis of a product bill-of-materials graph.
IReadOnlyList< int > LeafItemIds
Gets the identifiers of leaf items.
bool HasCycle
Gets a value indicating whether the product-structure graph contains at least one directed cycle.
int MaximumDepth
Gets the maximum number of relationships on a directed path from a leaf item to a root item.
IReadOnlyList< int > SharedComponentItemIds
Gets the identifiers of components consumed by more than one immediate parent item.
ProductStructureType DetectedType
Gets the product-structure type detected by the analyzer.
IReadOnlyList< int > RootItemIds
Gets the identifiers of root items.
Analyzes the bill-of-materials graph of a supply-chain instance and determines its product-structure ...
static void ApplyAnalysis(ProductStructureAnalysis analysis, ProductStructureDescriptor descriptor, string supplyChainFingerprint="")
Applies an existing analysis result to a persistent product-structure descriptor.
const string CurrentVersion
Gets the current version of the product-structure classification rules.
static ProductStructureAnalysis Analyze(SupplyChain supplyChain)
Analyzes the product structure of a supply-chain instance.
static ProductStructureAnalysis AnalyzeAndUpdate(SupplyChain supplyChain, ProductStructureDescriptor descriptor, string supplyChainFingerprint="")
Analyzes a supply-chain product structure and applies the result to a persistent descriptor.
Describes the declared and automatically detected structure of the product bill-of-materials graph of...
ProductStructureType DeclaredType
Gets or sets the product-structure type declared by an author, publication or data provider.
void ReplaceAnalyzedItemSets(IEnumerable< int > rootItemIds, IEnumerable< int > leafItemIds, IEnumerable< int > sharedComponentItemIds)
Replaces the item sets generated by the automatic product-structure analysis.
ProductStructureCheckStatus
Indicates the current status of the declaration, automatic detection and verification of a product bi...
ProductStructureType
Identifies the structural category of a product bill-of-materials graph.