LotSizingDataModel.Instance 2.0.1
Lot-sizing instance representation, descriptors and problem characterization.
Loading...
Searching...
No Matches
SupplyNetworkAnalyzer.cs
Go to the documentation of this file.
1using LotSizingDataModel.Core;
2using LotSizingDataModel.Core.PhysicalModel;
3
5
6/// <summary>
7/// Extracts the physical forward supply-flow graph encoded by Core.
8/// </summary>
9/// <remarks>
10/// BOM relationships are intentionally excluded. Forward arcs are induced by
11/// SupplierDelivery, TransportLane and DistributionCenterSourcing.
12/// </remarks>
13public sealed class SupplyNetworkAnalyzer
14{
15 public SupplyNetworkDescriptor Analyze(SupplyChain supplyChain)
16 {
17 ArgumentNullException.ThrowIfNull(supplyChain);
18
19 var nodeSeeds =
20 new Dictionary<string, NodeSeed>(StringComparer.Ordinal);
21
22 foreach (Supplier supplier in supplyChain.Suppliers)
23 {
24 AddDeclaredNode(
25 nodeSeeds,
26 SupplierKey(supplier.Id),
27 SupplyNetworkNodeKind.Supplier,
28 supplier.Id);
29 }
30
31 foreach (Plant plant in supplyChain.Plants)
32 {
33 AddDeclaredNode(
34 nodeSeeds,
35 PlantWarehouseKey(plant.Id),
36 SupplyNetworkNodeKind.PlantWarehouse,
37 plant.Id);
38 }
39
40 foreach (StandaloneWarehouse warehouse
41 in supplyChain.StandaloneWarehouses)
42 {
43 AddDeclaredNode(
44 nodeSeeds,
45 StandaloneWarehouseKey(warehouse.Id),
46 SupplyNetworkNodeKind.StandaloneWarehouse,
47 warehouse.Id);
48 }
49
50 foreach (DistributionCenter center
51 in supplyChain.DistributionCenters)
52 {
53 AddDeclaredNode(
54 nodeSeeds,
55 DistributionCenterKey(center.Id),
56 SupplyNetworkNodeKind.DistributionCenter,
57 center.Id);
58 }
59
60 var arcMultiplicity =
61 new Dictionary<ArcKey, int>();
62
63 foreach (var delivery in supplyChain.SupplierDeliveries)
64 {
65 string from = SupplierKey(delivery.SupplierId);
66 string to = WarehouseKey(delivery.Warehouse);
67
68 EnsureReferencedNode(
69 nodeSeeds,
70 from,
71 SupplyNetworkNodeKind.Supplier,
72 delivery.SupplierId);
73
74 EnsureWarehouseNode(nodeSeeds, delivery.Warehouse);
75
76 AddArc(
77 arcMultiplicity,
78 from,
79 to,
80 SupplyNetworkArcKind.SupplierDelivery);
81 }
82
83 foreach (TransportResource resource
84 in supplyChain.TransportResources)
85 {
86 foreach (AssignedTransportLane lane in supplyChain.GetTransportLanes(resource.Id))
87 {
88 string from = WarehouseKey(lane.Origin);
89 string to = WarehouseKey(lane.Destination);
90
91 EnsureWarehouseNode(nodeSeeds, lane.Origin);
92 EnsureWarehouseNode(nodeSeeds, lane.Destination);
93
94 AddArc(
95 arcMultiplicity,
96 from,
97 to,
98 SupplyNetworkArcKind.TransportLane);
99 }
100 }
101
102 foreach (var sourcing
103 in supplyChain.DistributionCenterSourcings)
104 {
105 string from = WarehouseKey(sourcing.Warehouse);
106 string to =
107 DistributionCenterKey(
108 sourcing.DistributionCenterId);
109
110 EnsureWarehouseNode(nodeSeeds, sourcing.Warehouse);
111
112 EnsureReferencedNode(
113 nodeSeeds,
114 to,
115 SupplyNetworkNodeKind.DistributionCenter,
116 sourcing.DistributionCenterId);
117
118 AddArc(
119 arcMultiplicity,
120 from,
121 to,
122 SupplyNetworkArcKind.DistributionCenterSourcing);
123 }
124
125 var adjacency =
126 nodeSeeds.Keys.ToDictionary(
127 key => key,
128 _ => new HashSet<string>(StringComparer.Ordinal),
129 StringComparer.Ordinal);
130
131 var reverseAdjacency =
132 nodeSeeds.Keys.ToDictionary(
133 key => key,
134 _ => new HashSet<string>(StringComparer.Ordinal),
135 StringComparer.Ordinal);
136
137 foreach (ArcKey arc in arcMultiplicity.Keys)
138 {
139 adjacency[arc.From].Add(arc.To);
140 reverseAdjacency[arc.To].Add(arc.From);
141 }
142
143 bool hasCycles =
144 HasDirectedCycle(
145 nodeSeeds.Keys,
146 adjacency,
147 reverseAdjacency);
148
150 ClassifyTopology(
151 nodeSeeds.Keys,
152 adjacency,
153 reverseAdjacency,
154 hasCycles);
155
156 int? echelonCount =
157 hasCycles
158 ? null
159 : ComputeEchelonCount(
160 nodeSeeds.Keys,
161 adjacency,
162 reverseAdjacency);
163
165 nodeSeeds.Values
166 .OrderBy(seed => seed.Key, StringComparer.Ordinal)
167 .Select(
168 seed =>
170 {
171 Key = seed.Key,
172 Kind = seed.Kind,
173 ReferenceId = seed.ReferenceId,
174 IsDeclared = seed.IsDeclared,
175 InDegree =
176 reverseAdjacency[seed.Key].Count,
177 OutDegree =
178 adjacency[seed.Key].Count
179 })
180 .ToArray();
181
183 arcMultiplicity
184 .OrderBy(pair => pair.Key.From, StringComparer.Ordinal)
185 .ThenBy(pair => pair.Key.To, StringComparer.Ordinal)
186 .ThenBy(pair => pair.Key.Kind)
187 .Select(
188 pair =>
190 {
191 FromKey = pair.Key.From,
192 ToKey = pair.Key.To,
193 Kind = pair.Key.Kind,
194 RelationshipMultiplicity = pair.Value
195 })
196 .ToArray();
197
198 var forward =
200 {
201 Nodes = nodes,
202 Arcs = arcs,
203 Topology = topology,
204 HasCycles = hasCycles,
205 EchelonCount = echelonCount
206 };
207
208 bool hasMultiSourcing =
209 HasSupplierMultiSourcing(supplyChain) ||
210 HasDistributionCenterMultiSourcing(supplyChain);
211
212 bool hasTransshipment =
213 supplyChain.TransportResources.Any(
214 resource => supplyChain.GetTransportLanes(resource.Id).Any());
215
216 bool hasDistributionNetwork =
217 supplyChain.DistributionCenterSourcings.Count > 0;
218
219 bool hasExternalDemandAtDistributionCenters =
220 supplyChain.Demands.Count > 0;
221
222 return new SupplyNetworkDescriptor
223 {
224 ForwardNetwork = forward,
225 ReverseNetwork = null,
226 Coupling = NetworkCouplingType.ForwardOnly,
227 HasMultiSourcing = hasMultiSourcing,
228 HasTransshipment = hasTransshipment,
229 HasDistributionNetwork =
230 hasDistributionNetwork,
231 HasExternalDemandAtDistributionCenters =
232 hasExternalDemandAtDistributionCenters
233 };
234 }
235
236 private static bool HasSupplierMultiSourcing(
237 SupplyChain supplyChain)
238 {
239 return supplyChain.SupplierDeliveries
240 .GroupBy(
241 delivery =>
242 $"{delivery.ItemId}|" +
243 $"{delivery.Warehouse.Kind}|" +
244 $"{delivery.Warehouse.ReferenceId}",
245 StringComparer.Ordinal)
246 .Any(
247 group =>
248 group
249 .Select(delivery => delivery.SupplierId)
250 .Distinct()
251 .Skip(1)
252 .Any());
253 }
254
255 private static bool HasDistributionCenterMultiSourcing(
256 SupplyChain supplyChain)
257 {
258 return supplyChain.DistributionCenterSourcings
259 .GroupBy(
260 sourcing =>
261 $"{sourcing.ItemId}|" +
262 $"{sourcing.DistributionCenterId}",
263 StringComparer.Ordinal)
264 .Any(
265 group =>
266 group
267 .Select(
268 sourcing =>
269 $"{sourcing.Warehouse.Kind}|" +
270 $"{sourcing.Warehouse.ReferenceId}")
271 .Distinct(StringComparer.Ordinal)
272 .Skip(1)
273 .Any());
274 }
275
276 private static SupplyNetworkTopologyType ClassifyTopology(
277 IEnumerable<string> keys,
278 IReadOnlyDictionary<string, HashSet<string>> adjacency,
279 IReadOnlyDictionary<string, HashSet<string>> reverseAdjacency,
280 bool hasCycles)
281 {
282 string[] nodes = keys.ToArray();
283
284 if (nodes.Length == 0)
285 {
286 return SupplyNetworkTopologyType.Unknown;
287 }
288
289 int physicalEdgeCount =
290 adjacency.Values.Sum(targets => targets.Count);
291
292 if (physicalEdgeCount == 0)
293 {
294 return SupplyNetworkTopologyType.Independent;
295 }
296
297 if (hasCycles)
298 {
299 return SupplyNetworkTopologyType.General;
300 }
301
302 int maxIn =
303 nodes.Max(key => reverseAdjacency[key].Count);
304
305 int maxOut =
306 nodes.Max(key => adjacency[key].Count);
307
308 if (maxIn <= 1 && maxOut <= 1)
309 {
310 return SupplyNetworkTopologyType.Serial;
311 }
312
313 if (maxIn > 1 && maxOut <= 1)
314 {
315 return SupplyNetworkTopologyType.Convergent;
316 }
317
318 if (maxOut > 1 && maxIn <= 1)
319 {
320 return SupplyNetworkTopologyType.Divergent;
321 }
322
323 if (IsUndirectedForest(nodes, adjacency))
324 {
325 return SupplyNetworkTopologyType.Tree;
326 }
327
328 return SupplyNetworkTopologyType.General;
329 }
330
331 private static bool HasDirectedCycle(
332 IEnumerable<string> keys,
333 IReadOnlyDictionary<string, HashSet<string>> adjacency,
334 IReadOnlyDictionary<string, HashSet<string>> reverseAdjacency)
335 {
336 var indegree =
337 keys.ToDictionary(
338 key => key,
339 key => reverseAdjacency[key].Count,
340 StringComparer.Ordinal);
341
342 var queue =
343 new Queue<string>(
344 indegree
345 .Where(pair => pair.Value == 0)
346 .Select(pair => pair.Key));
347
348 int visited = 0;
349
350 while (queue.Count > 0)
351 {
352 string current = queue.Dequeue();
353 visited++;
354
355 foreach (string target in adjacency[current])
356 {
357 indegree[target]--;
358
359 if (indegree[target] == 0)
360 {
361 queue.Enqueue(target);
362 }
363 }
364 }
365
366 return visited != indegree.Count;
367 }
368
369 private static int ComputeEchelonCount(
370 IEnumerable<string> keys,
371 IReadOnlyDictionary<string, HashSet<string>> adjacency,
372 IReadOnlyDictionary<string, HashSet<string>> reverseAdjacency)
373 {
374 string[] nodes = keys.ToArray();
375
376 if (nodes.Length == 0)
377 {
378 return 0;
379 }
380
381 var indegree =
382 nodes.ToDictionary(
383 key => key,
384 key => reverseAdjacency[key].Count,
385 StringComparer.Ordinal);
386
387 var depth =
388 nodes.ToDictionary(
389 key => key,
390 _ => 1,
391 StringComparer.Ordinal);
392
393 var queue =
394 new Queue<string>(
395 indegree
396 .Where(pair => pair.Value == 0)
397 .Select(pair => pair.Key));
398
399 while (queue.Count > 0)
400 {
401 string current = queue.Dequeue();
402
403 foreach (string target in adjacency[current])
404 {
405 depth[target] =
406 Math.Max(
407 depth[target],
408 depth[current] + 1);
409
410 indegree[target]--;
411
412 if (indegree[target] == 0)
413 {
414 queue.Enqueue(target);
415 }
416 }
417 }
418
419 return depth.Values.Max();
420 }
421
422 private static bool IsUndirectedForest(
423 IReadOnlyCollection<string> nodes,
424 IReadOnlyDictionary<string, HashSet<string>> adjacency)
425 {
426 var undirected =
427 nodes.ToDictionary(
428 key => key,
429 _ => new HashSet<string>(StringComparer.Ordinal),
430 StringComparer.Ordinal);
431
432 int edgeCount = 0;
433
434 foreach (string from in nodes)
435 {
436 foreach (string to in adjacency[from])
437 {
438 if (undirected[from].Add(to))
439 {
440 undirected[to].Add(from);
441 edgeCount++;
442 }
443 }
444 }
445
446 int components = 0;
447 var visited =
448 new HashSet<string>(StringComparer.Ordinal);
449
450 foreach (string start in nodes)
451 {
452 if (!visited.Add(start))
453 {
454 continue;
455 }
456
457 components++;
458
459 var stack = new Stack<string>();
460 stack.Push(start);
461
462 while (stack.Count > 0)
463 {
464 string current = stack.Pop();
465
466 foreach (string neighbor in undirected[current])
467 {
468 if (visited.Add(neighbor))
469 {
470 stack.Push(neighbor);
471 }
472 }
473 }
474 }
475
476 return edgeCount == nodes.Count - components;
477 }
478
479 private static void AddArc(
480 IDictionary<ArcKey, int> arcs,
481 string from,
482 string to,
484 {
485 var key = new ArcKey(from, to, kind);
486
487 if (arcs.TryGetValue(key, out int count))
488 {
489 arcs[key] = count + 1;
490 }
491 else
492 {
493 arcs.Add(key, 1);
494 }
495 }
496
497 private static void AddDeclaredNode(
498 IDictionary<string, NodeSeed> nodes,
499 string key,
501 int referenceId)
502 {
503 nodes[key] =
504 new NodeSeed(
505 key,
506 kind,
507 referenceId,
508 true);
509 }
510
511 private static void EnsureReferencedNode(
512 IDictionary<string, NodeSeed> nodes,
513 string key,
515 int referenceId)
516 {
517 if (!nodes.ContainsKey(key))
518 {
519 nodes.Add(
520 key,
521 new NodeSeed(
522 key,
523 kind,
524 referenceId,
525 false));
526 }
527 }
528
529 private static void EnsureWarehouseNode(
530 IDictionary<string, NodeSeed> nodes,
531 WarehouseReference warehouse)
532 {
534 warehouse.Kind ==
535 WarehouseReferenceKind.PlantWarehouse
536 ? SupplyNetworkNodeKind.PlantWarehouse
537 : SupplyNetworkNodeKind.StandaloneWarehouse;
538
539 EnsureReferencedNode(
540 nodes,
541 WarehouseKey(warehouse),
542 kind,
543 warehouse.ReferenceId);
544 }
545
546 private static string SupplierKey(int id) =>
547 $"supplier:{id}";
548
549 private static string DistributionCenterKey(int id) =>
550 $"distributionCenter:{id}";
551
552 private static string PlantWarehouseKey(int plantId) =>
553 $"plantWarehouse:{plantId}";
554
555 private static string StandaloneWarehouseKey(int warehouseId) =>
556 $"warehouse:{warehouseId}";
557
558 private static string WarehouseKey(
559 WarehouseReference warehouse)
560 {
561 return warehouse.Kind ==
562 WarehouseReferenceKind.PlantWarehouse
563 ? PlantWarehouseKey(warehouse.ReferenceId)
564 : StandaloneWarehouseKey(warehouse.ReferenceId);
565 }
566
567 private sealed record NodeSeed(
568 string Key,
570 int ReferenceId,
571 bool IsDeclared);
572
573 private readonly record struct ArcKey(
574 string From,
575 string To,
577}
Immutable analysis result for one directed physical supply network.
Extracts the physical forward supply-flow graph encoded by Core.
Describes one aggregated physical forward-flow relationship.
Describes the physical supply-flow network independently from the BOM and independently from any plan...
SupplyNetworkNodeKind
Identifies one physical facility category in the supply-flow graph.
NetworkCouplingType
Describes coupling between forward and reverse physical networks.
SupplyNetworkArcKind
Identifies the Core relationship that induces a physical forward-flow arc.
SupplyNetworkTopologyType
Classifies the directed physical topology of a supply-flow network.