LotSizingDataModel.Core 2.0.1
Core domain model, shared abstractions and XML-serializable entities.
Loading...
Searching...
No Matches
BillOfMaterialsAnalyzer.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
6
8
9/// <summary>
10/// Provides analysis operations for the bill of materials.
11///
12/// The bill of materials is represented by directed relationships
13/// from a parent item to its component items.
14/// </summary>
15public sealed class BillOfMaterialsAnalyzer
16{
17 private readonly Dictionary<int, List<ComponentRequirement>>
18 _requirementsByParent = new();
19
20 private readonly Dictionary<int, List<ComponentRequirement>>
21 _requirementsByComponent = new();
22
23 /// <summary>
24 /// Initializes an analyzer and creates a new entity index.
25 /// </summary>
26 /// <param name="supplyChain">
27 /// Supply chain containing the bill of materials.
28 /// </param>
30 : this(
32 supplyChain ??
33 throw new ArgumentNullException(
34 nameof(supplyChain))))
35 {
36 }
37
38 /// <summary>
39 /// Initializes an analyzer using an existing entity index.
40 /// </summary>
41 /// <param name="index">
42 /// Index used to resolve item references.
43 /// </param>
45 SupplyChainIndex index)
46 {
47 Index = index ??
48 throw new ArgumentNullException(nameof(index));
49
51
52 Rebuild();
53 }
54
55 /// <summary>
56 /// Gets the analyzed supply chain.
57 /// </summary>
58 public SupplyChain SupplyChain { get; }
59
60 /// <summary>
61 /// Gets the entity index used by the analyzer.
62 /// </summary>
63 public SupplyChainIndex Index { get; }
64
65 /// <summary>
66 /// Rebuilds the entity index and the bill-of-material indexes.
67 ///
68 /// Call this method after adding, removing or modifying
69 /// component requirements.
70 /// </summary>
71 public void Rebuild()
72 {
73 Index.Rebuild();
74
75 _requirementsByParent.Clear();
76 _requirementsByComponent.Clear();
77
78 foreach (ComponentRequirement requirement
79 in SupplyChain.ComponentRequirements)
80 {
81 /*
82 * These calls ensure that both referenced items exist.
83 */
84 Index.GetRequiredItem(
85 requirement.ParentItemId);
86
87 Index.GetRequiredItem(
88 requirement.ComponentItemId);
89
90 AddRequirement(
91 _requirementsByParent,
92 requirement.ParentItemId,
93 requirement);
94
95 AddRequirement(
96 _requirementsByComponent,
97 requirement.ComponentItemId,
98 requirement);
99 }
100 }
101
102 #region Direct relationships
103
104 /// <summary>
105 /// Gets the direct component requirements of a parent item.
106 /// </summary>
107 public IReadOnlyList<ComponentRequirement>
108 GetDirectRequirements(int parentItemId)
109 {
110 Index.GetRequiredItem(parentItemId);
111
112 if (!_requirementsByParent.TryGetValue(
113 parentItemId,
114 out List<ComponentRequirement>? requirements))
115 {
116 return Array.Empty<ComponentRequirement>();
117 }
118
119 return requirements.ToArray();
120 }
121
122 /// <summary>
123 /// Gets the direct components of a parent item.
124 /// </summary>
125 public IReadOnlyList<Item> GetDirectComponents(
126 int parentItemId)
127 {
128 return GetDirectRequirements(parentItemId)
129 .Select(
130 requirement =>
131 Index.GetRequiredItem(
132 requirement.ComponentItemId))
133 .ToArray();
134 }
135
136 /// <summary>
137 /// Gets the requirements in which an item is used
138 /// as a direct component.
139 /// </summary>
140 public IReadOnlyList<ComponentRequirement>
141 GetDirectParentRequirements(int componentItemId)
142 {
143 Index.GetRequiredItem(componentItemId);
144
145 if (!_requirementsByComponent.TryGetValue(
146 componentItemId,
147 out List<ComponentRequirement>? requirements))
148 {
149 return Array.Empty<ComponentRequirement>();
150 }
151
152 return requirements.ToArray();
153 }
154
155 /// <summary>
156 /// Gets the direct parent items using a component.
157 /// </summary>
158 public IReadOnlyList<Item> GetDirectParents(
159 int componentItemId)
160 {
161 return GetDirectParentRequirements(componentItemId)
162 .Select(
163 requirement =>
164 Index.GetRequiredItem(
165 requirement.ParentItemId))
166 .ToArray();
167 }
168
169 /// <summary>
170 /// Determines whether an item has no component.
171 /// </summary>
172 public bool IsLeafItem(int itemId)
173 {
174 Index.GetRequiredItem(itemId);
175
176 return !_requirementsByParent.ContainsKey(itemId);
177 }
178
179 /// <summary>
180 /// Determines whether an item is not used as a component
181 /// of another item.
182 /// </summary>
183 public bool IsRootItem(int itemId)
184 {
185 Index.GetRequiredItem(itemId);
186
187 return !_requirementsByComponent.ContainsKey(itemId);
188 }
189
190 /// <summary>
191 /// Gets all root items of the bill of materials.
192 /// </summary>
193 public IReadOnlyList<Item> GetRootItems()
194 {
195 return SupplyChain.Items
196 .Where(item => IsRootItem(item.Id))
197 .OrderBy(item => item.Id)
198 .ToArray();
199 }
200
201 /// <summary>
202 /// Gets all leaf items of the bill of materials.
203 /// </summary>
204 public IReadOnlyList<Item> GetLeafItems()
205 {
206 return SupplyChain.Items
207 .Where(item => IsLeafItem(item.Id))
208 .OrderBy(item => item.Id)
209 .ToArray();
210 }
211
212 #endregion
213
214 #region Topological order and cycle detection
215
216 /// <summary>
217 /// Determines whether the bill of materials contains
218 /// at least one circular dependency.
219 /// </summary>
220 public bool HasCycle =>
222
223 /// <summary>
224 /// Attempts to calculate a topological order in which
225 /// every parent item appears before its components.
226 /// </summary>
227 /// <param name="items">
228 /// Calculated order, or an empty collection when a cycle exists.
229 /// </param>
231 out IReadOnlyList<Item> items)
232 {
233 Dictionary<int, int> incomingEdgeCounts =
234 SupplyChain.Items.ToDictionary(
235 item => item.Id,
236 _ => 0);
237
238 foreach (ComponentRequirement requirement
239 in SupplyChain.ComponentRequirements)
240 {
241 incomingEdgeCounts[
242 requirement.ComponentItemId]++;
243 }
244
245 var availableItemIds =
246 new SortedSet<int>(
247 incomingEdgeCounts
248 .Where(pair => pair.Value == 0)
249 .Select(pair => pair.Key));
250
251 var orderedItems =
252 new List<Item>(
253 SupplyChain.Items.Count);
254
255 while (availableItemIds.Count > 0)
256 {
257 int currentItemId =
258 availableItemIds.First();
259
260 availableItemIds.Remove(currentItemId);
261
262 orderedItems.Add(
263 Index.GetRequiredItem(currentItemId));
264
265 if (!_requirementsByParent.TryGetValue(
266 currentItemId,
267 out List<ComponentRequirement>? requirements))
268 {
269 continue;
270 }
271
272 foreach (ComponentRequirement requirement
273 in requirements)
274 {
275 int componentItemId =
276 requirement.ComponentItemId;
277
278 incomingEdgeCounts[componentItemId]--;
279
280 if (incomingEdgeCounts[componentItemId] == 0)
281 {
282 availableItemIds.Add(componentItemId);
283 }
284 }
285 }
286
287 if (orderedItems.Count != SupplyChain.Items.Count)
288 {
289 items = Array.Empty<Item>();
290 return false;
291 }
292
293 items = orderedItems;
294 return true;
295 }
296
297 /// <summary>
298 /// Gets a topological order in which parent items appear
299 /// before their components.
300 /// </summary>
301 /// <exception cref="InvalidOperationException">
302 /// Thrown when the bill of materials contains a cycle.
303 /// </exception>
304 public IReadOnlyList<Item> GetTopologicalOrder()
305 {
307 out IReadOnlyList<Item> items))
308 {
309 return items;
310 }
311
312 throw new InvalidOperationException(
313 "The bill of materials contains a circular dependency.");
314 }
315
316 /// <summary>
317 /// Gets an order in which components appear before
318 /// their parent items.
319 ///
320 /// This order is useful for bottom-up calculations.
321 /// </summary>
322 public IReadOnlyList<Item> GetReverseTopologicalOrder()
323 {
324 return GetTopologicalOrder()
325 .Reverse()
326 .ToArray();
327 }
328
329 #endregion
330
331 #region Level calculation
332
333 /// <summary>
334 /// Calculates the bill-of-material level of every item.
335 ///
336 /// Root items receive level zero. The level of a component
337 /// is the maximum parent level plus one.
338 /// </summary>
339 public IReadOnlyDictionary<int, int> CalculateLevels()
340 {
341 IReadOnlyList<Item> topologicalOrder =
343
344 Dictionary<int, int> levels =
345 SupplyChain.Items.ToDictionary(
346 item => item.Id,
347 _ => 0);
348
349 foreach (Item parentItem in topologicalOrder)
350 {
351 if (!_requirementsByParent.TryGetValue(
352 parentItem.Id,
353 out List<ComponentRequirement>? requirements))
354 {
355 continue;
356 }
357
358 int componentLevel =
359 checked(levels[parentItem.Id] + 1);
360
361 foreach (ComponentRequirement requirement
362 in requirements)
363 {
364 int currentLevel =
365 levels[requirement.ComponentItemId];
366
367 if (componentLevel > currentLevel)
368 {
369 levels[requirement.ComponentItemId] =
370 componentLevel;
371 }
372 }
373 }
374
375 return levels;
376 }
377
378 /// <summary>
379 /// Recalculates and updates the BillOfMaterialsLevel property
380 /// of every item.
381 /// </summary>
383 {
384 IReadOnlyDictionary<int, int> levels =
386
387 foreach (Item item in SupplyChain.Items)
388 {
389 item.BillOfMaterialsLevel =
390 levels[item.Id];
391 }
392 }
393
394 /// <summary>
395 /// Determines whether the levels currently stored in the items
396 /// match the levels calculated from the bill of materials.
397 /// </summary>
399 {
400 IReadOnlyDictionary<int, int> calculatedLevels =
402
403 return SupplyChain.Items.All(
404 item =>
406 calculatedLevels[item.Id]);
407 }
408
409 #endregion
410
411 #region Cumulative requirements
412
413 /// <summary>
414 /// Calculates the cumulative component coefficients required
415 /// to manufacture one unit of a parent item.
416 ///
417 /// When a component is reached through several branches,
418 /// all contributions are added.
419 /// </summary>
420 /// <param name="parentItemId">
421 /// Identifier of the manufactured parent item.
422 /// </param>
423 /// <returns>
424 /// Component identifiers and cumulative integer coefficients.
425 /// The parent item itself is excluded.
426 /// </returns>
427 public IReadOnlyDictionary<int, long>
429 int parentItemId)
430 {
431 Index.GetRequiredItem(parentItemId);
432
433 IReadOnlyList<Item> topologicalOrder =
435
436 var quantities =
437 new Dictionary<int, long>
438 {
439 [parentItemId] = 1L
440 };
441
442 foreach (Item currentItem in topologicalOrder)
443 {
444 if (!quantities.TryGetValue(
445 currentItem.Id,
446 out long currentQuantity))
447 {
448 continue;
449 }
450
451 if (!_requirementsByParent.TryGetValue(
452 currentItem.Id,
453 out List<ComponentRequirement>? requirements))
454 {
455 continue;
456 }
457
458 foreach (ComponentRequirement requirement
459 in requirements)
460 {
461 long contribution;
462
463 try
464 {
465 contribution = checked(
466 currentQuantity *
467 requirement.Quantity);
468 }
469 catch (OverflowException exception)
470 {
471 throw new InvalidOperationException(
472 "The cumulative bill-of-material coefficient " +
473 "exceeds the supported integer range.",
474 exception);
475 }
476
477 quantities.TryGetValue(
478 requirement.ComponentItemId,
479 out long existingQuantity);
480
481 try
482 {
483 quantities[requirement.ComponentItemId] =
484 checked(
485 existingQuantity +
486 contribution);
487 }
488 catch (OverflowException exception)
489 {
490 throw new InvalidOperationException(
491 "The cumulative bill-of-material coefficient " +
492 "exceeds the supported integer range.",
493 exception);
494 }
495 }
496 }
497
498 quantities.Remove(parentItemId);
499
500 return quantities;
501 }
502
503 /// <summary>
504 /// Calculates the gross component requirements for a given
505 /// production quantity of a parent item.
506 /// </summary>
507 /// <param name="parentItemId">
508 /// Identifier of the manufactured parent item.
509 /// </param>
510 /// <param name="parentQuantity">
511 /// Non-negative finite production quantity.
512 /// </param>
513 public IReadOnlyDictionary<int, double>
515 int parentItemId,
516 double parentQuantity)
517 {
518 if (!double.IsFinite(parentQuantity) ||
519 parentQuantity < 0.0)
520 {
521 throw new ArgumentOutOfRangeException(
522 nameof(parentQuantity),
523 parentQuantity,
524 "The parent-item quantity must be finite " +
525 "and non-negative.");
526 }
527
528 IReadOnlyDictionary<int, long> coefficients =
530 parentItemId);
531
532 var requirements =
533 new Dictionary<int, double>();
534
535 foreach (KeyValuePair<int, long> pair
536 in coefficients)
537 {
538 double quantity =
539 pair.Value * parentQuantity;
540
541 if (!double.IsFinite(quantity))
542 {
543 throw new InvalidOperationException(
544 "A gross component requirement exceeds " +
545 "the supported numerical range.");
546 }
547
548 requirements[pair.Key] = quantity;
549 }
550
551 return requirements;
552 }
553
554 #endregion
555
556 private static void AddRequirement(
557 IDictionary<int, List<ComponentRequirement>> dictionary,
558 int itemId,
559 ComponentRequirement requirement)
560 {
561 if (!dictionary.TryGetValue(
562 itemId,
563 out List<ComponentRequirement>? requirements))
564 {
565 requirements =
566 new List<ComponentRequirement>();
567
568 dictionary.Add(
569 itemId,
570 requirements);
571 }
572
573 requirements.Add(requirement);
574 }
575}
BillOfMaterialsAnalyzer(SupplyChain supplyChain)
Initializes an analyzer and creates a new entity index.
IReadOnlyList< Item > GetReverseTopologicalOrder()
Gets an order in which components appear before their parent items.
IReadOnlyDictionary< int, double > CalculateGrossComponentRequirements(int parentItemId, double parentQuantity)
Calculates the gross component requirements for a given production quantity of a parent item.
void ApplyCalculatedLevels()
Recalculates and updates the BillOfMaterialsLevel property of every item.
bool IsRootItem(int itemId)
Determines whether an item is not used as a component of another item.
IReadOnlyList< Item > GetLeafItems()
Gets all leaf items of the bill of materials.
bool HasCycle
Determines whether the bill of materials contains at least one circular dependency.
IReadOnlyDictionary< int, int > CalculateLevels()
Calculates the bill-of-material level of every item.
IReadOnlyList< Item > GetDirectParents(int componentItemId)
Gets the direct parent items using a component.
bool IsLeafItem(int itemId)
Determines whether an item has no component.
bool HasConsistentStoredLevels()
Determines whether the levels currently stored in the items match the levels calculated from the bill...
IReadOnlyList< Item > GetTopologicalOrder()
Gets a topological order in which parent items appear before their components.
bool TryGetTopologicalOrder(out IReadOnlyList< Item > items)
Attempts to calculate a topological order in which every parent item appears before its components.
IReadOnlyDictionary< int, long > GetCumulativeRequirementCoefficients(int parentItemId)
Calculates the cumulative component coefficients required to manufacture one unit of a parent item.
IReadOnlyList< Item > GetDirectComponents(int parentItemId)
Gets the direct components of a parent item.
void Rebuild()
Rebuilds the entity index and the bill-of-material indexes.
SupplyChain SupplyChain
Gets the analyzed supply chain.
IReadOnlyList< ComponentRequirement > GetDirectRequirements(int parentItemId)
Gets the direct component requirements of a parent item.
IReadOnlyList< Item > GetRootItems()
Gets all root items of the bill of materials.
IReadOnlyList< ComponentRequirement > GetDirectParentRequirements(int componentItemId)
Gets the requirements in which an item is used as a direct component.
BillOfMaterialsAnalyzer(SupplyChainIndex index)
Initializes an analyzer using an existing entity index.
SupplyChainIndex Index
Gets the entity index used by the analyzer.
int Id
Gets or sets the numerical identifier of the entity.
Provides fast access to the entities contained in a supply chain.
SupplyChain SupplyChain
Gets the indexed supply chain.
Represents a bill-of-materials relationship between two items.
int ParentItemId
Gets or sets the identifier of the item being manufactured.
int ComponentItemId
Gets or sets the identifier of the required component.
int Quantity
Gets or sets the quantity of the component required to manufacture one unit of the parent item.
Represents a finished or semi-finished item handled by the supply chain.
Definition Item.cs:15
int BillOfMaterialsLevel
Gets or sets the item's bill-of-materials level.
Definition Item.cs:61
List< Item > Items
Gets the items present in the supply chain.