LotSizingDataModel.Core 2.0.1
Core domain model, shared abstractions and XML-serializable entities.
Loading...
Searching...
No Matches
DemandAnalyzer.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
6
8
9/// <summary>
10/// Provides aggregate calculations over the demand relationships
11/// contained in a supply-chain model.
12///
13/// Demand values correspond to D[c,i,t], where:
14/// - c identifies a distribution center;
15/// - i identifies an item;
16/// - t identifies a planning period.
17/// </summary>
18public sealed class DemandAnalyzer
19{
20 private readonly Dictionary<int, List<Demand>>
21 _demandsByItem = new();
22
23 private readonly Dictionary<int, List<Demand>>
24 _demandsByDistributionCenter = new();
25
26 /// <summary>
27 /// Initializes a demand analyzer and creates an entity index.
28 /// </summary>
29 /// <param name="supplyChain">
30 /// Supply chain containing the demands to analyze.
31 /// </param>
32 public DemandAnalyzer(SupplyChain supplyChain)
33 : this(
35 supplyChain ??
36 throw new ArgumentNullException(
37 nameof(supplyChain))))
38 {
39 }
40
41 /// <summary>
42 /// Initializes a demand analyzer using an existing index.
43 /// </summary>
44 /// <param name="index">
45 /// Index used to resolve item and distribution-center
46 /// references.
47 /// </param>
49 {
50 Index = index ??
51 throw new ArgumentNullException(nameof(index));
52
54
55 Rebuild();
56 }
57
58 /// <summary>
59 /// Gets the analyzed supply chain.
60 /// </summary>
61 public SupplyChain SupplyChain { get; }
62
63 /// <summary>
64 /// Gets the entity index used by the analyzer.
65 /// </summary>
66 public SupplyChainIndex Index { get; }
67
68 /// <summary>
69 /// Rebuilds the entity index and the demand indexes.
70 ///
71 /// Call this method after adding, removing or modifying
72 /// demand relationships.
73 /// </summary>
74 public void Rebuild()
75 {
76 Index.Rebuild();
77
78 _demandsByItem.Clear();
79 _demandsByDistributionCenter.Clear();
80
81 foreach (Demand demand in SupplyChain.Demands)
82 {
83 /*
84 * These calls ensure that the referenced entities
85 * exist before the demand is indexed.
86 */
87 Index.GetRequiredItem(demand.ItemId);
88
89 Index.GetRequiredDistributionCenter(
91
92 ValidateDemandHorizon(demand);
93
94 AddDemand(
95 _demandsByItem,
96 demand.ItemId,
97 demand);
98
99 AddDemand(
100 _demandsByDistributionCenter,
102 demand);
103 }
104 }
105
106 #region Demand relationship queries
107
108 /// <summary>
109 /// Gets every demand defined for an item.
110 /// </summary>
111 public IReadOnlyList<Demand> GetDemandsForItem(
112 int itemId)
113 {
114 Index.GetRequiredItem(itemId);
115
116 if (!_demandsByItem.TryGetValue(
117 itemId,
118 out List<Demand>? demands))
119 {
120 return Array.Empty<Demand>();
121 }
122
123 return demands.ToArray();
124 }
125
126 /// <summary>
127 /// Gets every demand expressed by a distribution center.
128 /// </summary>
129 public IReadOnlyList<Demand>
131 int distributionCenterId)
132 {
133 Index.GetRequiredDistributionCenter(
134 distributionCenterId);
135
136 if (!_demandsByDistributionCenter.TryGetValue(
137 distributionCenterId,
138 out List<Demand>? demands))
139 {
140 return Array.Empty<Demand>();
141 }
142
143 return demands.ToArray();
144 }
145
146 /// <summary>
147 /// Finds the demand relationship defined for an item and
148 /// a distribution center.
149 ///
150 /// Returns null when no demand relationship exists.
151 /// </summary>
153 int itemId,
154 int distributionCenterId)
155 {
156 Index.GetRequiredItem(itemId);
157
158 Index.GetRequiredDistributionCenter(
159 distributionCenterId);
160
161 if (!_demandsByItem.TryGetValue(
162 itemId,
163 out List<Demand>? demands))
164 {
165 return null;
166 }
167
168 return demands.FirstOrDefault(
169 demand =>
170 demand.DistributionCenterId ==
171 distributionCenterId);
172 }
173
174 /// <summary>
175 /// Gets the demand relationship defined for an item and
176 /// a distribution center.
177 /// </summary>
179 int itemId,
180 int distributionCenterId)
181 {
182 return FindDemand(
183 itemId,
184 distributionCenterId) ??
185 throw new KeyNotFoundException(
186 $"No demand exists for item {itemId} and " +
187 $"distribution center {distributionCenterId}.");
188 }
189
190 #endregion
191
192 #region Period calculations
193
194 /// <summary>
195 /// Gets the demand of an item at a distribution center
196 /// during a planning period.
197 /// </summary>
198 public double GetDemandQuantity(
199 int itemId,
200 int distributionCenterId,
201 int period)
202 {
203 ValidatePeriod(period);
204
205 return GetRequiredDemand(
206 itemId,
207 distributionCenterId)
208 .GetQuantity(period);
209 }
210
211 /// <summary>
212 /// Calculates the total demand for an item during
213 /// a planning period.
214 /// </summary>
216 int itemId,
217 int period)
218 {
219 ValidatePeriod(period);
220
221 IReadOnlyList<Demand> demands =
222 GetDemandsForItem(itemId);
223
224 return SumDemands(
225 demands,
226 period);
227 }
228
229 /// <summary>
230 /// Calculates the total demand expressed by a distribution
231 /// center during a planning period.
232 /// </summary>
234 int distributionCenterId,
235 int period)
236 {
237 ValidatePeriod(period);
238
239 IReadOnlyList<Demand> demands =
241 distributionCenterId);
242
243 return SumDemands(
244 demands,
245 period);
246 }
247
248 /// <summary>
249 /// Calculates the total demand of the complete supply chain
250 /// during a planning period.
251 /// </summary>
252 public double GetGlobalDemand(int period)
253 {
254 ValidatePeriod(period);
255
256 return SumDemands(
257 SupplyChain.Demands,
258 period);
259 }
260
261 #endregion
262
263 #region Demand profiles
264
265 /// <summary>
266 /// Calculates the aggregate demand profile of an item.
267 /// </summary>
269 int itemId)
270 {
271 IReadOnlyList<Demand> demands =
272 GetDemandsForItem(itemId);
273
274 return CreateProfile(demands);
275 }
276
277 /// <summary>
278 /// Calculates the aggregate demand profile of a
279 /// distribution center.
280 /// </summary>
281 public DemandProfile
283 int distributionCenterId)
284 {
285 IReadOnlyList<Demand> demands =
287 distributionCenterId);
288
289 return CreateProfile(demands);
290 }
291
292 /// <summary>
293 /// Calculates the aggregate demand profile of the complete
294 /// supply chain.
295 /// </summary>
297 {
298 return CreateProfile(
299 SupplyChain.Demands);
300 }
301
302 /// <summary>
303 /// Calculates the demand profile of one item at one
304 /// distribution center.
305 /// </summary>
307 int itemId,
308 int distributionCenterId)
309 {
310 Demand demand =
312 itemId,
313 distributionCenterId);
314
315 double[] quantities =
316 new double[SupplyChain.PlanningHorizon];
317
318 for (int period = 1;
319 period <= SupplyChain.PlanningHorizon;
320 period++)
321 {
322 quantities[period - 1] =
323 demand.GetQuantity(period);
324 }
325
326 return new DemandProfile(quantities);
327 }
328
329 #endregion
330
331 #region Cumulative calculations
332
333 /// <summary>
334 /// Calculates the cumulative demand for an item over
335 /// the complete planning horizon.
336 /// </summary>
338 int itemId)
339 {
340 return GetDemandProfileForItem(itemId)
342 }
343
344 /// <summary>
345 /// Calculates the cumulative demand for an item over
346 /// an inclusive period interval.
347 /// </summary>
349 int itemId,
350 int startPeriod,
351 int endPeriod)
352 {
353 return GetDemandProfileForItem(itemId)
355 startPeriod,
356 endPeriod);
357 }
358
359 /// <summary>
360 /// Calculates the cumulative demand expressed by a
361 /// distribution center over the complete horizon.
362 /// </summary>
363 public double
365 int distributionCenterId)
366 {
368 distributionCenterId)
370 }
371
372 /// <summary>
373 /// Calculates the cumulative global demand over an
374 /// inclusive period interval.
375 /// </summary>
377 int startPeriod,
378 int endPeriod)
379 {
382 startPeriod,
383 endPeriod);
384 }
385
386 #endregion
387
388 #region Private helpers
389
390 private DemandProfile CreateProfile(
391 IEnumerable<Demand> demands)
392 {
393 ArgumentNullException.ThrowIfNull(demands);
394
395 Demand[] demandArray =
396 demands.ToArray();
397
398 double[] quantities =
399 new double[SupplyChain.PlanningHorizon];
400
401 for (int period = 1;
403 period++)
404 {
405 quantities[period - 1] =
406 SumDemands(
407 demandArray,
408 period);
409 }
410
411 return new DemandProfile(quantities);
412 }
413
414 private static double SumDemands(
415 IEnumerable<Demand> demands,
416 int period)
417 {
418 double total = 0.0;
419
420 foreach (Demand demand in demands)
421 {
422 double quantity =
423 demand.GetQuantity(period);
424
425 if (!double.IsFinite(quantity) ||
426 quantity < 0.0)
427 {
428 throw new InvalidOperationException(
429 "A demand quantity must be finite " +
430 "and non-negative.");
431 }
432
433 total += quantity;
434
435 if (!double.IsFinite(total))
436 {
437 throw new InvalidOperationException(
438 "The aggregated demand exceeds the " +
439 "supported numerical range.");
440 }
441 }
442
443 return total;
444 }
445
446 private void ValidateDemandHorizon(
447 Demand demand)
448 {
449 if (demand.PlanningHorizon !=
450 SupplyChain.PlanningHorizon)
451 {
452 throw new InvalidOperationException(
453 $"The demand for item {demand.ItemId} and " +
454 $"distribution center " +
455 $"{demand.DistributionCenterId} uses a " +
456 $"planning horizon of {demand.PlanningHorizon}, " +
457 $"whereas the global planning horizon is " +
458 $"{SupplyChain.PlanningHorizon}.");
459 }
460 }
461
462 private void ValidatePeriod(int period)
463 {
464 if (period < 1 ||
465 period > SupplyChain.PlanningHorizon)
466 {
467 throw new ArgumentOutOfRangeException(
468 nameof(period),
469 period,
470 $"The period must be between 1 and " +
471 $"{SupplyChain.PlanningHorizon}.");
472 }
473 }
474
475 private static void AddDemand(
476 IDictionary<int, List<Demand>> dictionary,
477 int key,
478 Demand demand)
479 {
480 if (!dictionary.TryGetValue(
481 key,
482 out List<Demand>? demands))
483 {
484 demands = new List<Demand>();
485
486 dictionary.Add(
487 key,
488 demands);
489 }
490
491 demands.Add(demand);
492 }
493
494 #endregion
495
496 /// <summary>
497 /// Represents an aggregated demand time series.
498 ///
499 /// Period numbers are one-based.
500 /// </summary>
501 public sealed class DemandProfile
502 {
503 private readonly double[] _quantities;
504
505 /// <summary>
506 /// Initializes a demand profile.
507 /// </summary>
509 IEnumerable<double> quantities)
510 {
511 ArgumentNullException.ThrowIfNull(quantities);
512
513 _quantities = quantities.ToArray();
514
515 for (int index = 0;
516 index < _quantities.Length;
517 index++)
518 {
519 double quantity =
520 _quantities[index];
521
522 if (!double.IsFinite(quantity) ||
523 quantity < 0.0)
524 {
525 throw new ArgumentOutOfRangeException(
526 nameof(quantities),
527 quantity,
528 "A demand-profile quantity must be " +
529 "finite and non-negative.");
530 }
531 }
532
534 CalculateTotalQuantity();
535
537 _quantities.Length == 0
538 ? 0.0
539 : TotalQuantity /
540 _quantities.Length;
541
543 CalculatePeak();
544 }
545
546 /// <summary>
547 /// Gets the number of periods in the profile.
548 /// </summary>
549 public int PlanningHorizon =>
550 _quantities.Length;
551
552 /// <summary>
553 /// Gets a copy of the quantities.
554 /// </summary>
555 public IReadOnlyList<double> Quantities =>
556 Array.AsReadOnly(
557 (double[])_quantities.Clone());
558
559 /// <summary>
560 /// Gets the demand quantity for a planning period.
561 /// </summary>
562 public double this[int period]
563 {
564 get
565 {
566 ValidateProfilePeriod(period);
567
568 return _quantities[period - 1];
569 }
570 }
571
572 /// <summary>
573 /// Gets the cumulative demand over the complete profile.
574 /// </summary>
575 public double TotalQuantity { get; }
576
577 /// <summary>
578 /// Gets the average demand per planning period.
579 /// </summary>
580 public double AverageQuantity { get; }
581
582 /// <summary>
583 /// Gets the first period having the highest demand.
584 ///
585 /// Returns zero when the profile contains no period.
586 /// </summary>
587 public int PeakPeriod { get; }
588
589 /// <summary>
590 /// Gets the highest period demand.
591 /// </summary>
592 public double PeakQuantity { get; }
593
594 /// <summary>
595 /// Calculates the cumulative demand over an inclusive
596 /// period interval.
597 /// </summary>
599 int startPeriod,
600 int endPeriod)
601 {
602 ValidateProfilePeriod(startPeriod);
603 ValidateProfilePeriod(endPeriod);
604
605 if (startPeriod > endPeriod)
606 {
607 throw new ArgumentException(
608 "The start period cannot be greater " +
609 "than the end period.");
610 }
611
612 double total = 0.0;
613
614 for (int period = startPeriod;
615 period <= endPeriod;
616 period++)
617 {
618 total += this[period];
619
620 if (!double.IsFinite(total))
621 {
622 throw new InvalidOperationException(
623 "The cumulative demand exceeds the " +
624 "supported numerical range.");
625 }
626 }
627
628 return total;
629 }
630
631 private double CalculateTotalQuantity()
632 {
633 double total = 0.0;
634
635 foreach (double quantity in _quantities)
636 {
637 total += quantity;
638
639 if (!double.IsFinite(total))
640 {
641 throw new InvalidOperationException(
642 "The total demand exceeds the supported " +
643 "numerical range.");
644 }
645 }
646
647 return total;
648 }
649
650 private (int Period, double Quantity)
651 CalculatePeak()
652 {
653 if (_quantities.Length == 0)
654 {
655 return (0, 0.0);
656 }
657
658 int peakPeriod = 1;
659 double peakQuantity = _quantities[0];
660
661 for (int index = 1;
662 index < _quantities.Length;
663 index++)
664 {
665 if (_quantities[index] >
666 peakQuantity)
667 {
668 peakQuantity =
669 _quantities[index];
670
671 peakPeriod = index + 1;
672 }
673 }
674
675 return (
676 peakPeriod,
677 peakQuantity
678 );
679 }
680
681 private void ValidateProfilePeriod(int period)
682 {
683 if (period < 1 ||
684 period > PlanningHorizon)
685 {
686 throw new ArgumentOutOfRangeException(
687 nameof(period),
688 period,
689 $"The period must be between 1 and " +
690 $"{PlanningHorizon}.");
691 }
692 }
693 }
694}
Represents an aggregated demand time series.
double GetCumulativeQuantity(int startPeriod, int endPeriod)
Calculates the cumulative demand over an inclusive period interval.
double TotalQuantity
Gets the cumulative demand over the complete profile.
double AverageQuantity
Gets the average demand per planning period.
DemandProfile(IEnumerable< double > quantities)
Initializes a demand profile.
int PeakPeriod
Gets the first period having the highest demand.
int PlanningHorizon
Gets the number of periods in the profile.
IReadOnlyList< double > Quantities
Gets a copy of the quantities.
double GetTotalDemandForDistributionCenter(int distributionCenterId, int period)
Calculates the total demand expressed by a distribution center during a planning period.
IReadOnlyList< Demand > GetDemandsForItem(int itemId)
Gets every demand defined for an item.
double GetCumulativeDemandForItem(int itemId)
Calculates the cumulative demand for an item over the complete planning horizon.
Demand GetRequiredDemand(int itemId, int distributionCenterId)
Gets the demand relationship defined for an item and a distribution center.
DemandProfile GetDemandProfileForItem(int itemId)
Calculates the aggregate demand profile of an item.
SupplyChain SupplyChain
Gets the analyzed supply chain.
DemandProfile GetDemandProfileForDistributionCenter(int distributionCenterId)
Calculates the aggregate demand profile of a distribution center.
double GetDemandQuantity(int itemId, int distributionCenterId, int period)
Gets the demand of an item at a distribution center during a planning period.
double GetGlobalDemand(int period)
Calculates the total demand of the complete supply chain during a planning period.
Demand? FindDemand(int itemId, int distributionCenterId)
Finds the demand relationship defined for an item and a distribution center.
void Rebuild()
Rebuilds the entity index and the demand indexes.
DemandAnalyzer(SupplyChain supplyChain)
Initializes a demand analyzer and creates an entity index.
DemandProfile GetGlobalDemandProfile()
Calculates the aggregate demand profile of the complete supply chain.
double GetTotalDemandForItem(int itemId, int period)
Calculates the total demand for an item during a planning period.
DemandAnalyzer(SupplyChainIndex index)
Initializes a demand analyzer using an existing index.
double GetCumulativeDemandForDistributionCenter(int distributionCenterId)
Calculates the cumulative demand expressed by a distribution center over the complete horizon.
DemandProfile GetDemandProfile(int itemId, int distributionCenterId)
Calculates the demand profile of one item at one distribution center.
SupplyChainIndex Index
Gets the entity index used by the analyzer.
IReadOnlyList< Demand > GetDemandsForDistributionCenter(int distributionCenterId)
Gets every demand expressed by a distribution center.
double GetCumulativeDemandForItem(int itemId, int startPeriod, int endPeriod)
Calculates the cumulative demand for an item over an inclusive period interval.
double GetCumulativeGlobalDemand(int startPeriod, int endPeriod)
Calculates the cumulative global demand over an inclusive period interval.
Provides fast access to the entities contained in a supply chain.
SupplyChain SupplyChain
Gets the indexed supply chain.
Represents the demand for an item issued by a distribution center.
Definition Demand.cs:22
int ItemId
Gets or sets the identifier of the requested item.
Definition Demand.cs:70
int PlanningHorizon
Gets the number of periods represented by the demand.
Definition Demand.cs:159
int DistributionCenterId
Gets or sets the identifier of the distribution center issuing the demand.
Definition Demand.cs:96
double GetQuantity(int period)
Gets the demand quantity for a planning period.
Definition Demand.cs:168
Represents the complete supply-chain data model.
int PlanningHorizon
Gets or sets the number of periods in the global planning horizon.