40 SupplyChain supplyChain)
42 ArgumentNullException.ThrowIfNull(supplyChain);
50 ValidateItemIdentifiers(
56 .Select(item => item.Id)
57 .Where(itemId => itemId > 0)
59 .OrderBy(itemId => itemId)
65 if (itemIds.Length == 0)
68 "The supply-chain instance does not contain " +
77 Dictionary<int, HashSet<int>>
78 parentItemsByComponent =
81 _ =>
new HashSet<int>());
83 Dictionary<int, HashSet<int>>
84 componentItemsByParent =
87 _ =>
new HashSet<int>());
92 parentItemsByComponent,
93 componentItemsByParent,
97 int relationshipCount =
98 parentItemsByComponent.Values.Sum(
99 parentIds => parentIds.Count);
105 parentItemsByComponent[itemId]
113 componentItemsByParent[itemId]
117 int[] isolatedItemIds =
121 parentItemsByComponent[itemId]
123 componentItemsByParent[itemId]
127 int[] sharedComponentItemIds =
131 parentItemsByComponent[itemId]
135 int maximumImmediateComponentCount =
140 componentItemsByParent[itemId]
143 int maximumImmediateParentCount =
148 parentItemsByComponent[itemId]
151 int connectedComponentCount =
152 CountConnectedComponents(
154 parentItemsByComponent,
155 componentItemsByParent);
157 int[] cyclicItemIds =
160 parentItemsByComponent);
162 if (cyclicItemIds.Length > 0)
165 "The bill-of-materials graph contains at " +
166 "least one directed cycle involving item " +
168 string.Join(
", ", cyclicItemIds) +
173 cyclicItemIds.Length == 0
174 ? CalculateMaximumDepth(
176 parentItemsByComponent,
177 componentItemsByParent)
181 DetermineStructureType(
183 maximumImmediateComponentCount,
184 maximumImmediateParentCount,
198 connectedComponentCount:
199 connectedComponentCount,
204 maximumImmediateComponentCount:
205 maximumImmediateComponentCount,
207 maximumImmediateParentCount:
208 maximumImmediateParentCount,
219 sharedComponentItemIds:
220 sharedComponentItemIds,
249 SupplyChain supplyChain,
251 string supplyChainFingerprint =
"")
253 ArgumentNullException.ThrowIfNull(supplyChain);
254 ArgumentNullException.ThrowIfNull(descriptor);
262 supplyChainFingerprint);
283 string supplyChainFingerprint =
"")
285 ArgumentNullException.ThrowIfNull(analysis);
286 ArgumentNullException.ThrowIfNull(descriptor);
288 descriptor.DetectedType =
291 descriptor.HasCycle =
294 descriptor.MaximumDepth =
302 descriptor.AnalyzedAtUtc =
305 descriptor.AnalyzerVersion =
308 descriptor.SupplyChainFingerprint =
309 supplyChainFingerprint?.Trim() ??
312 descriptor.AnalysisComment =
313 BuildAnalysisComment(analysis);
315 descriptor.CheckStatus =
316 DetermineCheckStatus(
321 private static void ValidateItemIdentifiers(
322 SupplyChain supplyChain,
323 ICollection<string> errors)
326 index < supplyChain.Items.Count;
330 supplyChain.Items[index].Id;
335 $
"Item at index {index} has an invalid " +
336 $
"identifier ({itemId}).");
340 int[] duplicateItemIds =
342 .GroupBy(item => item.Id)
343 .Where(group => group.Count() > 1)
344 .Select(group => group.Key)
345 .OrderBy(itemId => itemId)
348 foreach (
int duplicateItemId
352 $
"Item identifier {duplicateItemId} " +
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)
369 supplyChain.ComponentRequirements.Count;
373 supplyChain.ComponentRequirements[index];
376 requirement.ParentItemId;
378 int componentItemId =
379 requirement.ComponentItemId;
381 string relationshipDescription =
382 $
"component requirement at index {index}";
384 bool hasValidIdentifiers =
387 if (parentItemId <= 0)
390 $
"The {relationshipDescription} has an " +
391 $
"invalid parent-item identifier " +
392 $
"({parentItemId}).");
394 hasValidIdentifiers =
false;
396 else if (!itemIds.Contains(parentItemId))
399 $
"The {relationshipDescription} refers " +
400 $
"to unknown parent item " +
403 hasValidIdentifiers =
false;
406 if (componentItemId <= 0)
409 $
"The {relationshipDescription} has an " +
410 $
"invalid component-item identifier " +
411 $
"({componentItemId}).");
413 hasValidIdentifiers =
false;
415 else if (!itemIds.Contains(componentItemId))
418 $
"The {relationshipDescription} refers " +
419 $
"to unknown component item " +
420 $
"{componentItemId}.");
422 hasValidIdentifiers =
false;
425 if (requirement.Quantity <= 0)
428 $
"The {relationshipDescription} has a " +
429 $
"non-positive requirement quantity " +
430 $
"({requirement.Quantity}).");
433 if (!hasValidIdentifiers)
438 bool relationshipAdded =
439 parentItemsByComponent[
443 if (!relationshipAdded)
446 $
"The relationship from component " +
447 $
"{componentItemId} to parent " +
448 $
"{parentItemId} is duplicated.");
453 componentItemsByParent[
455 .Add(componentItemId);
459 private static int CountConnectedComponents(
460 IEnumerable<int> itemIds,
461 IReadOnlyDictionary<
int, HashSet<int>>
462 parentItemsByComponent,
463 IReadOnlyDictionary<
int, HashSet<int>>
464 componentItemsByParent)
469 int connectedComponentCount =
472 foreach (
int startItemId
475 if (!visited.Add(startItemId))
480 connectedComponentCount++;
488 while (pendingItemIds.Count > 0)
491 pendingItemIds.Pop();
493 IEnumerable<int> adjacentItemIds =
494 parentItemsByComponent[
497 componentItemsByParent[
500 foreach (
int adjacentItemId
513 return connectedComponentCount;
516 private static int[] FindCyclicItemIds(
517 IEnumerable<int> itemIds,
518 IReadOnlyDictionary<
int, HashSet<int>>
519 parentItemsByComponent)
533 new Dictionary<int, int>();
536 new Dictionary<int, int>();
547 void Visit(
int itemId)
558 itemsOnStack.Add(itemId);
560 foreach (
int parentItemId
561 in parentItemsByComponent[itemId])
563 if (!indices.ContainsKey(
571 lowLinks[parentItemId]);
573 else if (itemsOnStack.Contains(
579 indices[parentItemId]);
583 if (lowLinks[itemId] !=
589 var stronglyConnectedComponent =
592 while (stack.Count > 0)
600 stronglyConnectedComponent.Add(
603 if (currentItemId == itemId)
610 stronglyConnectedComponent.Count > 1;
613 stronglyConnectedComponent.Count == 1)
616 stronglyConnectedComponent[0];
619 parentItemsByComponent[
621 .Contains(singleItemId);
629 foreach (
int cyclicItemId
630 in stronglyConnectedComponent)
642 if (!indices.ContainsKey(itemId))
649 .OrderBy(itemId => itemId)
653 private static int CalculateMaximumDepth(
654 IEnumerable<int> itemIds,
655 IReadOnlyDictionary<
int, HashSet<int>>
656 parentItemsByComponent,
657 IReadOnlyDictionary<
int, HashSet<int>>
658 componentItemsByParent)
666 int[] normalizedItemIds =
669 Dictionary<int, int> remainingComponentCounts =
670 normalizedItemIds.ToDictionary(
673 componentItemsByParent[itemId]
676 Dictionary<int, int> depths =
677 normalizedItemIds.ToDictionary(
683 normalizedItemIds.Where(
685 remainingComponentCounts[itemId]
688 int processedItemCount =
694 while (readyItemIds.Count > 0)
699 readyItemIds.Remove(itemId);
700 processedItemCount++;
707 foreach (
int parentItemId
708 in parentItemsByComponent[itemId])
710 depths[parentItemId] =
712 depths[parentItemId],
715 remainingComponentCounts[
718 if (remainingComponentCounts[
731 return processedItemCount ==
732 normalizedItemIds.Length
738 DetermineStructureType(
739 int relationshipCount,
740 int maximumImmediateComponentCount,
741 int maximumImmediateParentCount,
742 IReadOnlyCollection<string> errors,
743 IReadOnlyCollection<int> cyclicItemIds)
745 if (errors.Count > 0 ||
746 cyclicItemIds.Count > 0)
751 if (relationshipCount == 0)
757 bool hasAssemblyNode =
758 maximumImmediateComponentCount > 1;
760 bool hasDivergentComponent =
761 maximumImmediateParentCount > 1;
763 if (!hasAssemblyNode &&
764 !hasDivergentComponent)
769 if (hasAssemblyNode &&
770 !hasDivergentComponent)
775 if (!hasAssemblyNode &&
776 hasDivergentComponent)
786 DetermineCheckStatus(
788 ProductStructureAnalysis analysis)
790 if (!analysis.IsValid)
804 analysis.DetectedType)
807 .DeclaredAndConfirmed;
811 .DeclaredAndContradicted;
814 private static string BuildAnalysisComment(
815 ProductStructureAnalysis analysis)
820 if (analysis.Errors.Count > 0)
829 if (analysis.Warnings.Count > 0)