LotSizingDataModel.Core 2.0.1
Core domain model, shared abstractions and XML-serializable entities.
Loading...
Searching...
No Matches
TransportPathFinder.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
7
9
10/// <summary>
11/// Finds transport paths between warehouses.
12///
13/// Transport lanes are directed. The path finder minimizes
14/// the sum of the transport lead times of the selected lanes.
15/// </summary>
16public sealed class TransportPathFinder
17{
18 /// <summary>
19 /// Initializes a path finder and creates a new entity index.
20 /// </summary>
21 /// <param name="supplyChain">
22 /// Supply chain whose transport network must be explored.
23 /// </param>
24 public TransportPathFinder(SupplyChain supplyChain)
25 : this(
27 supplyChain ??
28 throw new ArgumentNullException(
29 nameof(supplyChain))))
30 {
31 }
32
33 /// <summary>
34 /// Initializes a path finder using an existing entity index.
35 /// </summary>
36 /// <param name="index">
37 /// Index used to resolve warehouse and item references.
38 /// </param>
40 {
41 Index = index ??
42 throw new ArgumentNullException(nameof(index));
43
45 }
46
47 /// <summary>
48 /// Gets the explored supply chain.
49 /// </summary>
50 public SupplyChain SupplyChain { get; }
51
52 /// <summary>
53 /// Gets the entity index used by the path finder.
54 /// </summary>
55 public SupplyChainIndex Index { get; }
56
57 /// <summary>
58 /// Rebuilds the underlying entity index.
59 ///
60 /// Call this method after directly modifying the supply-chain
61 /// entity collections.
62 /// </summary>
63 public void RebuildIndex()
64 {
65 Index.Rebuild();
66 }
67
68 /// <summary>
69 /// Finds the fastest transport path between two warehouses.
70 ///
71 /// Every transport resource and every valid transport lane
72 /// may be used.
73 /// </summary>
74 /// <param name="origin">
75 /// Origin warehouse.
76 /// </param>
77 /// <param name="destination">
78 /// Destination warehouse.
79 /// </param>
80 /// <returns>
81 /// The fastest path, or null when no path exists.
82 /// </returns>
84 WarehouseReference origin,
85 WarehouseReference destination)
86 {
87 return FindFastestPathCore(
88 origin,
89 destination,
90 allowedTransportResourceIds: null);
91 }
92
93 /// <summary>
94 /// Finds the fastest transport path compatible with
95 /// a specified item.
96 ///
97 /// A transport resource is considered compatible when a
98 /// TransportCharacteristic exists for the item-resource pair.
99 /// </summary>
100 /// <param name="itemId">
101 /// Identifier of the transported item.
102 /// </param>
103 /// <param name="origin">
104 /// Origin warehouse.
105 /// </param>
106 /// <param name="destination">
107 /// Destination warehouse.
108 /// </param>
109 /// <returns>
110 /// The fastest compatible path, or null when no path exists.
111 /// </returns>
113 int itemId,
114 WarehouseReference origin,
115 WarehouseReference destination)
116 {
117 Index.GetRequiredItem(itemId);
118
119 // Build the set of transport resources compatible with the item.
120 HashSet<int> allowedTransportResourceIds =
122 .Where(
123 characteristic =>
124 characteristic.ItemId == itemId)
125 .Select(
126 characteristic =>
127 characteristic.TransportResourceId)
128 .ToHashSet();
129
130 return FindFastestPathCore(
131 origin,
132 destination,
133 allowedTransportResourceIds);
134 }
135
136 /// <summary>
137 /// Gets the fastest transport path between two warehouses.
138 /// </summary>
139 /// <exception cref="KeyNotFoundException">
140 /// Thrown when no path exists.
141 /// </exception>
143 WarehouseReference origin,
144 WarehouseReference destination)
145 {
146 return FindFastestPath(
147 origin,
148 destination) ??
149 throw new KeyNotFoundException(
150 "No transport path exists between " +
151 $"{FormatWarehouse(origin)} and " +
152 $"{FormatWarehouse(destination)}.");
153 }
154
155 /// <summary>
156 /// Gets the fastest item-compatible transport path
157 /// between two warehouses.
158 /// </summary>
159 /// <exception cref="KeyNotFoundException">
160 /// Thrown when no compatible path exists.
161 /// </exception>
163 int itemId,
164 WarehouseReference origin,
165 WarehouseReference destination)
166 {
168 itemId,
169 origin,
170 destination) ??
171 throw new KeyNotFoundException(
172 $"No transport path exists for item {itemId} " +
173 $"between {FormatWarehouse(origin)} and " +
174 $"{FormatWarehouse(destination)}.");
175 }
176
177 /// <summary>
178 /// Determines whether a transport path exists between
179 /// two warehouses.
180 /// </summary>
181 public bool HasPath(
182 WarehouseReference origin,
183 WarehouseReference destination)
184 {
185 return FindFastestPath(
186 origin,
187 destination) is not null;
188 }
189
190 /// <summary>
191 /// Determines whether an item-compatible transport path
192 /// exists between two warehouses.
193 /// </summary>
194 public bool HasPathForItem(
195 int itemId,
196 WarehouseReference origin,
197 WarehouseReference destination)
198 {
200 itemId,
201 origin,
202 destination) is not null;
203 }
204
205 private TransportPath? FindFastestPathCore(
206 WarehouseReference origin,
207 WarehouseReference destination,
208 IReadOnlySet<int>? allowedTransportResourceIds)
209 {
210 ArgumentNullException.ThrowIfNull(origin);
211 ArgumentNullException.ThrowIfNull(destination);
212
213 /*
214 * These calls also ensure that both references identify
215 * warehouses that exist in the current supply chain.
216 */
218 Index.GetRequiredWarehouse(destination);
219
220 WarehouseKey originKey =
221 WarehouseKey.FromReference(origin);
222
223 WarehouseKey destinationKey =
224 WarehouseKey.FromReference(destination);
225
226 // Early return for same-warehouse case: empty path with zero lead time.
227 if (originKey == destinationKey)
228 {
229 return new TransportPath(
230 origin,
231 destination,
232 Array.Empty<TransportLeg>());
233 }
234
235 Dictionary<WarehouseKey, List<TransportLeg>>
236 adjacency = BuildAdjacency(
237 allowedTransportResourceIds);
238
239 // Initialize Dijkstra's algorithm: distance to origin is zero.
240 var distances =
241 new Dictionary<WarehouseKey, int>
242 {
243 [originKey] = 0
244 };
245
246 var predecessors =
247 new Dictionary<WarehouseKey, TransportLeg>();
248
249 var queue =
250 new PriorityQueue<WarehouseKey, int>();
251
252 queue.Enqueue(
253 originKey,
254 priority: 0);
255
256 while (queue.TryDequeue(
257 out WarehouseKey current,
258 out int currentDistance))
259 {
260 if (!distances.TryGetValue(
261 current,
262 out int bestKnownDistance) ||
263 currentDistance != bestKnownDistance)
264 {
265 /*
266 * A better route to this warehouse has already
267 * been inserted into the priority queue.
268 */
269 continue;
270 }
271
272 // Stop when destination is reached; Dijkstra guarantees this is the shortest path.
273 if (current == destinationKey)
274 {
275 break;
276 }
277
278 if (!adjacency.TryGetValue(
279 current,
280 out List<TransportLeg>? outgoingLegs))
281 {
282 continue;
283 }
284
285 foreach (TransportLeg leg in outgoingLegs)
286 {
287 WarehouseKey next =
288 WarehouseKey.FromReference(
289 leg.Destination);
290
291 int candidateDistance;
292
293 // Protect against overflow when summing lead times.
294 try
295 {
296 candidateDistance = checked(
297 currentDistance +
298 leg.LeadTime);
299 }
300 catch (OverflowException exception)
301 {
302 throw new InvalidOperationException(
303 "The accumulated transport lead time " +
304 "exceeds the supported integer range.",
305 exception);
306 }
307
308 if (distances.TryGetValue(
309 next,
310 out int existingDistance) &&
311 candidateDistance >= existingDistance)
312 {
313 continue;
314 }
315
316 distances[next] = candidateDistance;
317 predecessors[next] = leg;
318
319 queue.Enqueue(
320 next,
321 candidateDistance);
322 }
323 }
324
325 if (!distances.ContainsKey(destinationKey))
326 {
327 return null;
328 }
329
330 // Reconstruct the path backward from destination to origin.
331 List<TransportLeg> reversedLegs =
332 ReconstructPath(
333 originKey,
334 destinationKey,
335 predecessors);
336
337 reversedLegs.Reverse();
338
339 return new TransportPath(
340 origin,
341 destination,
342 reversedLegs);
343 }
344
345 private Dictionary<WarehouseKey, List<TransportLeg>>
346 BuildAdjacency(
347 IReadOnlySet<int>? allowedTransportResourceIds)
348 {
349 var adjacency =
350 new Dictionary<WarehouseKey, List<TransportLeg>>();
351
352 foreach (TransportResource resource
353 in SupplyChain.TransportResources)
354 {
355 // Skip resources not in the allowed set (if filtering by item compatibility).
356 if (allowedTransportResourceIds is not null &&
357 !allowedTransportResourceIds.Contains(
358 resource.Id))
359 {
360 continue;
361 }
362
363 foreach (AssignedTransportLane lane in SupplyChain.GetTransportLanes(resource.Id))
364 {
365 if (lane.Origin is null ||
366 lane.Destination is null)
367 {
368 continue;
369 }
370
371 /*
372 * Invalid references are ignored here. They are
373 * reported separately by SupplyChainValidator.
374 */
375 if (!Index.TryGetWarehouse(
376 lane.Origin,
377 out _) ||
378 !Index.TryGetWarehouse(
379 lane.Destination,
380 out _))
381 {
382 continue;
383 }
384
385 if (lane.LeadTime < 0)
386 {
387 throw new InvalidOperationException(
388 "A transport path cannot be calculated " +
389 "because a lane has a negative lead time.");
390 }
391
392 WarehouseKey originKey =
393 WarehouseKey.FromReference(
394 lane.Origin);
395
396 if (!adjacency.TryGetValue(
397 originKey,
398 out List<TransportLeg>? legs))
399 {
400 legs = new List<TransportLeg>();
401 adjacency.Add(originKey, legs);
402 }
403
404 legs.Add(
405 new TransportLeg(
406 resource,
407 lane));
408 }
409 }
410
411 return adjacency;
412 }
413
414 private static List<TransportLeg> ReconstructPath(
415 WarehouseKey origin,
416 WarehouseKey destination,
417 IReadOnlyDictionary<
418 WarehouseKey,
419 TransportLeg> predecessors)
420 {
421 var reversedLegs =
422 new List<TransportLeg>();
423
424 WarehouseKey current = destination;
425
426 while (current != origin)
427 {
428 if (!predecessors.TryGetValue(
429 current,
430 out TransportLeg? leg))
431 {
432 throw new InvalidOperationException(
433 "The transport path cannot be reconstructed.");
434 }
435
436 reversedLegs.Add(leg);
437
438 current =
439 WarehouseKey.FromReference(
440 leg.Origin);
441 }
442
443 return reversedLegs;
444 }
445
446 private static string FormatWarehouse(
447 WarehouseReference reference)
448 {
449 ArgumentNullException.ThrowIfNull(reference);
450
451 return
452 $"{reference.Kind}:{reference.ReferenceId}";
453 }
454
455 /// <summary>
456 /// Internal value-based identifier used by the
457 /// shortest-path algorithm.
458 /// </summary>
459 private readonly record struct WarehouseKey(
461 int ReferenceId)
462 {
463 public static WarehouseKey FromReference(
464 WarehouseReference reference)
465 {
466 ArgumentNullException.ThrowIfNull(reference);
467
468 return new WarehouseKey(
469 reference.Kind,
470 reference.ReferenceId);
471 }
472 }
473
474 /// <summary>
475 /// Represents one leg of a transport path.
476 /// </summary>
477 public sealed class TransportLeg
478 {
479 /// <summary>
480 /// Initializes one leg of a transport path.
481 /// </summary>
482 /// <param name="transportResource">
483 /// Transport resource used for this leg.
484 /// </param>
485 /// <param name="lane">
486 /// Directed transport lane used for this leg.
487 /// </param>
489 TransportResource transportResource,
491 {
492 TransportResource = transportResource ??
493 throw new ArgumentNullException(
494 nameof(transportResource));
495
496 Lane = lane ??
497 throw new ArgumentNullException(nameof(lane));
498 }
499
500 /// <summary>
501 /// Gets the transport resource used for this leg.
502 /// </summary>
504
505 /// <summary>
506 /// Gets the transport lane used for this leg.
507 /// </summary>
509
510 /// <summary>
511 /// Gets the origin warehouse reference.
512 /// </summary>
514
515 /// <summary>
516 /// Gets the destination warehouse reference.
517 /// </summary>
519 Lane.Destination;
520
521 /// <summary>
522 /// Gets the transport lead time of this leg.
523 /// </summary>
524 public int LeadTime => Lane.LeadTime;
525
526 /// <inheritdoc/>
527 public override string ToString()
528 {
529 return
530 $"{TransportResource.Name}: " +
531 $"{FormatWarehouse(Origin)} -> " +
532 $"{FormatWarehouse(Destination)} " +
533 $"({LeadTime} period(s))";
534 }
535 }
536
537 /// <summary>
538 /// Represents a complete transport path between
539 /// two warehouses.
540 /// </summary>
541 public sealed class TransportPath
542 {
543 private readonly TransportLeg[] _legs;
544
545 /// <summary>
546 /// Initializes a complete transport path between
547 /// two warehouses.
548 /// </summary>
549 /// <param name="origin">
550 /// Origin warehouse reference.
551 /// </param>
552 /// <param name="destination">
553 /// Destination warehouse reference.
554 /// </param>
555 /// <param name="legs">
556 /// Ordered sequence of transport legs forming the path.
557 /// </param>
559 WarehouseReference origin,
560 WarehouseReference destination,
561 IEnumerable<TransportLeg> legs)
562 {
563 Origin = origin ??
564 throw new ArgumentNullException(nameof(origin));
565
566 Destination = destination ??
567 throw new ArgumentNullException(
568 nameof(destination));
569
570 ArgumentNullException.ThrowIfNull(legs);
571
572 _legs = legs.ToArray();
573
574 TotalLeadTime = _legs.Sum(
575 leg => leg.LeadTime);
576 }
577
578 /// <summary>
579 /// Gets the path origin.
580 /// </summary>
582
583 /// <summary>
584 /// Gets the path destination.
585 /// </summary>
587
588 /// <summary>
589 /// Gets the ordered transport legs.
590 /// </summary>
591 public IReadOnlyList<TransportLeg> Legs =>
592 _legs;
593
594 /// <summary>
595 /// Gets the number of transport legs.
596 /// </summary>
597 public int LegCount => _legs.Length;
598
599 /// <summary>
600 /// Gets the sum of the lead times of all transport legs.
601 /// </summary>
602 public int TotalLeadTime { get; }
603
604 /// <summary>
605 /// Gets a value indicating whether the origin and
606 /// destination are the same warehouse.
607 /// </summary>
608 public bool IsEmpty => _legs.Length == 0;
609
610 /// <inheritdoc/>
611 public override string ToString()
612 {
613 if (IsEmpty)
614 {
615 return
616 $"{FormatWarehouse(Origin)} " +
617 "(same origin and destination)";
618 }
619
620 string route = string.Join(
621 " | ",
622 _legs.Select(
623 leg => leg.ToString()));
624
625 return
626 $"{route} — total lead time: " +
627 $"{TotalLeadTime} period(s)";
628 }
629 }
630}
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.
Warehouse GetRequiredWarehouse(WarehouseReference reference)
Resolves a warehouse from a warehouse reference.
A runtime join of the central lane and assignment; stores no copied business data.
Represents a serializable reference to a warehouse.
int ReferenceId
Gets or sets the referenced identifier.
WarehouseReferenceKind Kind
Gets or sets the kind of warehouse being referenced.
TransportResource TransportResource
Gets the transport resource used for this leg.
AssignedTransportLane Lane
Gets the transport lane used for this leg.
WarehouseReference Origin
Gets the origin warehouse reference.
WarehouseReference Destination
Gets the destination warehouse reference.
TransportLeg(TransportResource transportResource, AssignedTransportLane lane)
Initializes one leg of a transport path.
Represents a complete transport path between two warehouses.
int TotalLeadTime
Gets the sum of the lead times of all transport legs.
TransportPath(WarehouseReference origin, WarehouseReference destination, IEnumerable< TransportLeg > legs)
Initializes a complete transport path between two warehouses.
bool IsEmpty
Gets a value indicating whether the origin and destination are the same warehouse.
IReadOnlyList< TransportLeg > Legs
Gets the ordered transport legs.
SupplyChain SupplyChain
Gets the explored supply chain.
TransportPathFinder(SupplyChain supplyChain)
Initializes a path finder and creates a new entity index.
TransportPath? FindFastestPathForItem(int itemId, WarehouseReference origin, WarehouseReference destination)
Finds the fastest transport path compatible with a specified item.
SupplyChainIndex Index
Gets the entity index used by the path finder.
void RebuildIndex()
Rebuilds the underlying entity index.
TransportPath? FindFastestPath(WarehouseReference origin, WarehouseReference destination)
Finds the fastest transport path between two warehouses.
bool HasPath(WarehouseReference origin, WarehouseReference destination)
Determines whether a transport path exists between two warehouses.
TransportPath GetRequiredFastestPathForItem(int itemId, WarehouseReference origin, WarehouseReference destination)
Gets the fastest item-compatible transport path between two warehouses.
TransportPath GetRequiredFastestPath(WarehouseReference origin, WarehouseReference destination)
Gets the fastest transport path between two warehouses.
bool HasPathForItem(int itemId, WarehouseReference origin, WarehouseReference destination)
Determines whether an item-compatible transport path exists between two warehouses.
TransportPathFinder(SupplyChainIndex index)
Initializes a path finder using an existing entity index.
List< TransportCharacteristic > TransportCharacteristics
Gets the item-transport-resource characteristics.
WarehouseReferenceKind
Identifies the type of warehouse referenced by a relationship.