LotSizingDataModel.Solution 2.0.1
Solution objects for production, setup, inventory and related decisions.
Loading...
Searching...
No Matches
InventoryDecision.cs
Go to the documentation of this file.
1using System;
2using System.ComponentModel;
3using System.Linq;
4using System.Xml.Serialization;
5using LotSizingDataModel.Core.Common;
6using LotSizingDataModel.Core.PhysicalModel;
7
9
10/// <summary>
11/// Stores the inventory decisions associated with one item
12/// and one warehouse over the complete planning horizon.
13/// </summary>
14/// <remarks>
15/// Period numbers are one-based.
16///
17/// This class stores item-specific inventory decisions.
18/// Global warehouse activation and global additional capacity
19/// are stored separately in a warehouse-capacity decision.
20/// </remarks>
21[Serializable]
22[XmlType(TypeName = "inventoryDecision")]
23public sealed partial class InventoryDecision :
24 ModelObject,
25 IPlanningHorizonAware
26{
27 private int _itemId;
28
29 private WarehouseReference _warehouse =
30 new();
31
32 private DoubleTimeSeries _levels =
33 new();
34
35 private DoubleTimeSeries _safetyStockViolations =
36 new();
37
38 private IntegerTimeSeries _setups =
39 new();
40
41 private DoubleTimeSeries _additionalCapacityUsed =
42 new();
43
44 /// <summary>
45 /// Initializes an empty inventory decision.
46 /// </summary>
47 /// <remarks>
48 /// This constructor is required by
49 /// <see cref="XmlSerializer"/>.
50 /// </remarks>
52 {
53 SubscribeToObject(_warehouse);
54 SubscribeToObject(_levels);
55 SubscribeToObject(_safetyStockViolations);
56 SubscribeToObject(_setups);
57 SubscribeToObject(_additionalCapacityUsed);
58 }
59
60 /// <summary>
61 /// Initializes an inventory decision for an item,
62 /// a warehouse and a planning horizon.
63 /// </summary>
64 /// <param name="itemId">
65 /// Identifier of the stored item.
66 /// </param>
67 /// <param name="warehouse">
68 /// Reference to the warehouse in which the item is stored.
69 /// </param>
70 /// <param name="planningHorizon">
71 /// Strictly positive number of planning periods.
72 /// </param>
74 int itemId,
75 WarehouseReference warehouse,
76 int planningHorizon)
77 : this()
78 {
79 if (itemId <= 0)
80 {
81 throw new ArgumentOutOfRangeException(
82 nameof(itemId),
83 itemId,
84 "The item identifier must be strictly positive.");
85 }
86
87 ArgumentNullException.ThrowIfNull(warehouse);
88
89 if (planningHorizon <= 0)
90 {
91 throw new ArgumentOutOfRangeException(
92 nameof(planningHorizon),
93 planningHorizon,
94 "The planning horizon must be strictly positive.");
95 }
96
97 ItemId = itemId;
98 Warehouse = warehouse;
99
100 ResizeTimeSeries(planningHorizon);
101 }
102
103 /// <summary>
104 /// Gets or sets the identifier of the stored item.
105 /// </summary>
106 [XmlAttribute("itemId")]
107 public int ItemId
108 {
109 get => _itemId;
110 set
111 {
112 /*
113 * Zero is tolerated for an empty object created
114 * by XmlSerializer. The solution validator will
115 * require a strictly positive identifier.
116 */
117 if (value < 0)
118 {
119 throw new ArgumentOutOfRangeException(
120 nameof(value),
121 value,
122 "The item identifier cannot be negative.");
123 }
124
125 SetProperty(
126 ref _itemId,
127 value);
128 }
129 }
130
131 /// <summary>
132 /// Gets or sets the reference to the warehouse
133 /// associated with the inventory decision.
134 /// </summary>
135 [XmlElement("warehouse")]
136 public WarehouseReference Warehouse
137 {
138 get => _warehouse;
139 set
140 {
141 ArgumentNullException.ThrowIfNull(value);
142
143 if (ReferenceEquals(
144 _warehouse,
145 value))
146 {
147 return;
148 }
149
150 UnsubscribeFromObject(_warehouse);
151
152 SetProperty(
153 ref _warehouse,
154 value);
155
156 SubscribeToObject(_warehouse);
157
158 NotifyDerivedProperties();
159 }
160 }
161
162 /// <summary>
163 /// Gets or sets the inventory levels recorded
164 /// during each planning period.
165 /// </summary>
166 /// <remarks>
167 /// Values must be finite and non-negative.
168 /// </remarks>
169 [XmlElement("levels")]
170 public DoubleTimeSeries Levels
171 {
172 get => _levels;
173 set
174 {
175 ArgumentNullException.ThrowIfNull(value);
176
177 if (ReferenceEquals(
178 _levels,
179 value))
180 {
181 return;
182 }
183
184 UnsubscribeFromObject(_levels);
185
186 SetProperty(
187 ref _levels,
188 value);
189
190 SubscribeToObject(_levels);
191
192 NotifyDerivedProperties();
193 }
194 }
195
196 /// <summary>
197 /// Gets or sets the quantities by which the safety-stock
198 /// requirement is violated during each planning period.
199 /// </summary>
200 /// <remarks>
201 /// Values must be finite and non-negative.
202 ///
203 /// A zero value means that no safety-stock violation
204 /// is recorded for the corresponding period.
205 /// </remarks>
206 [XmlElement("safetyStockViolations")]
207 public DoubleTimeSeries SafetyStockViolations
208 {
209 get => _safetyStockViolations;
210 set
211 {
212 ArgumentNullException.ThrowIfNull(value);
213
214 if (ReferenceEquals(
215 _safetyStockViolations,
216 value))
217 {
218 return;
219 }
220
221 UnsubscribeFromObject(
222 _safetyStockViolations);
223
224 SetProperty(
225 ref _safetyStockViolations,
226 value);
227
228 SubscribeToObject(
229 _safetyStockViolations);
230
231 NotifyDerivedProperties();
232 }
233 }
234
235 /// <summary>
236 /// Gets or sets the binary item-specific
237 /// inventory-activation decisions.
238 /// </summary>
239 /// <remarks>
240 /// Each value must be zero or one.
241 /// </remarks>
242 [XmlElement("setups")]
243 public IntegerTimeSeries Setups
244 {
245 get => _setups;
246 set
247 {
248 ArgumentNullException.ThrowIfNull(value);
249
250 if (ReferenceEquals(
251 _setups,
252 value))
253 {
254 return;
255 }
256
257 UnsubscribeFromObject(_setups);
258
259 SetProperty(
260 ref _setups,
261 value);
262
263 SubscribeToObject(_setups);
264
265 NotifyDerivedProperties();
266 }
267 }
268
269 /// <summary>
270 /// Gets or sets the item-specific additional capacity
271 /// used during each planning period.
272 /// </summary>
273 /// <remarks>
274 /// Values must be finite and non-negative.
275 ///
276 /// Global additional capacity used by the complete warehouse
277 /// is not stored in this series.
278 /// </remarks>
279 [XmlElement("additionalCapacityUsed")]
280 public DoubleTimeSeries AdditionalCapacityUsed
281 {
282 get => _additionalCapacityUsed;
283 set
284 {
285 ArgumentNullException.ThrowIfNull(value);
286
287 if (ReferenceEquals(
288 _additionalCapacityUsed,
289 value))
290 {
291 return;
292 }
293
294 UnsubscribeFromObject(
295 _additionalCapacityUsed);
296
297 SetProperty(
298 ref _additionalCapacityUsed,
299 value);
300
301 SubscribeToObject(
302 _additionalCapacityUsed);
303
304 NotifyDerivedProperties();
305 }
306 }
307
308 /// <summary>
309 /// Gets the number of planning periods represented
310 /// by the inventory-level series.
311 /// </summary>
312 [XmlIgnore]
313 public int PlanningHorizon =>
314 Levels.PeriodCount;
315
316 /// <summary>
317 /// Gets a value indicating whether every decision series
318 /// uses the same planning horizon.
319 /// </summary>
320 [XmlIgnore]
322 SafetyStockViolations.PeriodCount ==
324 Setups.PeriodCount ==
326 AdditionalCapacityUsed.PeriodCount ==
328
329 /// <summary>
330 /// Gets a value indicating whether every inventory level
331 /// is finite and non-negative.
332 /// </summary>
333 [XmlIgnore]
334 public bool HasValidLevels =>
335 Levels.All(
336 level =>
337 double.IsFinite(level) &&
338 level >= 0.0);
339
340 /// <summary>
341 /// Gets a value indicating whether every safety-stock
342 /// violation is finite and non-negative.
343 /// </summary>
344 [XmlIgnore]
347 violation =>
348 double.IsFinite(violation) &&
349 violation >= 0.0);
350
351 /// <summary>
352 /// Gets a value indicating whether every setup value
353 /// is equal to zero or one.
354 /// </summary>
355 [XmlIgnore]
356 public bool HasValidSetupValues =>
357 Setups.All(
358 setup =>
359 setup is 0 or 1);
360
361 /// <summary>
362 /// Gets a value indicating whether every additional-capacity
363 /// value is finite and non-negative.
364 /// </summary>
365 [XmlIgnore]
368 capacity =>
369 double.IsFinite(capacity) &&
370 capacity >= 0.0);
371
372 /// <summary>
373 /// Gets a value indicating whether the inventory decision
374 /// is internally consistent.
375 /// </summary>
376 /// <remarks>
377 /// This property does not verify that the item and warehouse
378 /// exist in a particular supply-chain instance.
379 /// </remarks>
380 [XmlIgnore]
381 public bool IsInternallyValid =>
382 ItemId > 0 &&
383 PlanningHorizon > 0 &&
389
390 /// <summary>
391 /// Gets the inventory level for a planning period.
392 /// </summary>
393 /// <param name="period">
394 /// One-based planning period.
395 /// </param>
396 /// <returns>
397 /// Inventory level recorded for the period.
398 /// </returns>
399 public double GetLevel(int period)
400 {
401 return Levels[period];
402 }
403
404 /// <summary>
405 /// Sets the inventory level for a planning period.
406 /// </summary>
407 /// <param name="period">
408 /// One-based planning period.
409 /// </param>
410 /// <param name="level">
411 /// Finite and non-negative inventory level.
412 /// </param>
413 public void SetLevel(
414 int period,
415 double level)
416 {
417 ValidateNonNegativeFiniteValue(
418 level,
419 nameof(level));
420
421 Levels[period] = level;
422 }
423
424 /// <summary>
425 /// Gets the safety-stock violation for a planning period.
426 /// </summary>
427 /// <param name="period">
428 /// One-based planning period.
429 /// </param>
430 /// <returns>
431 /// Non-negative safety-stock violation quantity.
432 /// </returns>
434 int period)
435 {
436 return SafetyStockViolations[period];
437 }
438
439 /// <summary>
440 /// Sets the safety-stock violation for a planning period.
441 /// </summary>
442 /// <param name="period">
443 /// One-based planning period.
444 /// </param>
445 /// <param name="violation">
446 /// Finite and non-negative violation quantity.
447 /// </param>
449 int period,
450 double violation)
451 {
452 ValidateNonNegativeFiniteValue(
453 violation,
454 nameof(violation));
455
456 SafetyStockViolations[period] =
457 violation;
458 }
459
460 /// <summary>
461 /// Determines whether the item-specific inventory activity
462 /// is activated during a planning period.
463 /// </summary>
464 /// <param name="period">
465 /// One-based planning period.
466 /// </param>
467 /// <returns>
468 /// True when the setup value is one; otherwise, false.
469 /// </returns>
470 public bool IsSetupActivated(int period)
471 {
472 return Setups[period] == 1;
473 }
474
475 /// <summary>
476 /// Sets the item-specific inventory activation
477 /// for a planning period.
478 /// </summary>
479 /// <param name="period">
480 /// One-based planning period.
481 /// </param>
482 /// <param name="isActivated">
483 /// True to activate the inventory activity;
484 /// otherwise, false.
485 /// </param>
486 public void SetSetupActivated(
487 int period,
488 bool isActivated)
489 {
490 Setups[period] =
491 isActivated ? 1 : 0;
492 }
493
494 /// <summary>
495 /// Gets the item-specific additional capacity used
496 /// during a planning period.
497 /// </summary>
498 /// <param name="period">
499 /// One-based planning period.
500 /// </param>
501 /// <returns>
502 /// Non-negative additional-capacity quantity.
503 /// </returns>
505 int period)
506 {
507 return AdditionalCapacityUsed[period];
508 }
509
510 /// <summary>
511 /// Sets the item-specific additional capacity used
512 /// during a planning period.
513 /// </summary>
514 /// <param name="period">
515 /// One-based planning period.
516 /// </param>
517 /// <param name="capacity">
518 /// Finite and non-negative additional-capacity quantity.
519 /// </param>
521 int period,
522 double capacity)
523 {
524 ValidateNonNegativeFiniteValue(
525 capacity,
526 nameof(capacity));
527
528 AdditionalCapacityUsed[period] =
529 capacity;
530 }
531
532 /// <summary>
533 /// Resizes every decision series to the specified
534 /// planning horizon.
535 /// </summary>
536 /// <param name="periodCount">
537 /// Non-negative number of planning periods.
538 /// </param>
539 /// <remarks>
540 /// Existing values are preserved whenever possible.
541 /// New periods are initialized with zero.
542 /// </remarks>
543 public void ResizeTimeSeries(int periodCount)
544 {
545 if (periodCount < 0)
546 {
547 throw new ArgumentOutOfRangeException(
548 nameof(periodCount),
549 periodCount,
550 "The period count cannot be negative.");
551 }
552
553 Levels.Resize(
554 periodCount,
555 defaultValue: 0.0);
556
558 periodCount,
559 defaultValue: 0.0);
560
561 Setups.Resize(
562 periodCount,
563 defaultValue: 0);
564
566 periodCount,
567 defaultValue: 0.0);
568
569 NotifyDerivedProperties();
570 }
571
572 /// <summary>
573 /// Resets every inventory decision value to zero.
574 /// </summary>
575 public void Clear()
576 {
577 Levels.Fill(0.0);
579 SafetyStockViolations.Fill(0.0);
580 Setups.Fill(0);
581 AdditionalCapacityUsed.Fill(0.0);
582
583 NotifyDerivedProperties();
584 }
585
586 /// <inheritdoc/>
587 public override string ToString()
588 {
589 double maximumLevel =
590 Levels.Any()
591 ? Levels.Max()
592 : 0.0;
593
594 double totalSafetyStockViolation =
596
597 return
598 $"Item {ItemId}, warehouse " +
599 $"{Warehouse.Kind}:{Warehouse.ReferenceId}: " +
600 $"maximum level {maximumLevel}; " +
601 $"safety-stock violation " +
602 $"{totalSafetyStockViolation}";
603 }
604
605 private void SubscribeToObject(
606 ModelObject modelObject)
607 {
608 modelObject.PropertyChanged +=
609 OnNestedPropertyChanged;
610 }
611
612 private void UnsubscribeFromObject(
613 ModelObject modelObject)
614 {
615 modelObject.PropertyChanged -=
616 OnNestedPropertyChanged;
617 }
618
619 private void OnNestedPropertyChanged(
620 object? sender,
621 PropertyChangedEventArgs eventArgs)
622 {
623 NotifyDerivedProperties();
624 }
625
626 private void NotifyDerivedProperties()
627 {
628 OnPropertyChanged(
629 nameof(PlanningHorizon));
630
631 OnPropertyChanged(
633
634 OnPropertyChanged(
635 nameof(HasValidLevels));
636
637 OnPropertyChanged(
639
640 OnPropertyChanged(
641 nameof(HasValidSetupValues));
642
643 OnPropertyChanged(
645
646 OnPropertyChanged(
647 nameof(IsInternallyValid));
648 }
649
650 private static void ValidateNonNegativeFiniteValue(
651 double value,
652 string parameterName)
653 {
654 if (!double.IsFinite(value) ||
655 value < 0.0)
656 {
657 throw new ArgumentOutOfRangeException(
658 parameterName,
659 value,
660 "The value must be finite and non-negative.");
661 }
662 }
663}
int ItemId
Gets or sets the identifier of the stored item.
DoubleTimeSeries SafetyStockViolations
Gets or sets the quantities by which the safety-stock requirement is violated during each planning pe...
bool HasValidLevels
Gets a value indicating whether every inventory level is finite and non-negative.
bool HasConsistentPlanningHorizon
Gets a value indicating whether every decision series uses the same planning horizon.
double GetAdditionalCapacityUsed(int period)
Gets the item-specific additional capacity used during a planning period.
DoubleTimeSeries Levels
Gets or sets the inventory levels recorded during each planning period.
double InitialInventoryLevel
Gets or sets the optimized stock available before period 1.
void SetAdditionalCapacityUsed(int period, double capacity)
Sets the item-specific additional capacity used during a planning period.
void SetSetupActivated(int period, bool isActivated)
Sets the item-specific inventory activation for a planning period.
InventoryDecision()
Initializes an empty inventory decision.
void SetSafetyStockViolation(int period, double violation)
Sets the safety-stock violation for a planning period.
bool HasValidSetupValues
Gets a value indicating whether every setup value is equal to zero or one.
void Clear()
Resets every inventory decision value to zero.
double GetLevel(int period)
Gets the inventory level for a planning period.
bool HasValidAdditionalCapacityValues
Gets a value indicating whether every additional-capacity value is finite and non-negative.
void SetLevel(int period, double level)
Sets the inventory level for a planning period.
double GetSafetyStockViolation(int period)
Gets the safety-stock violation for a planning period.
WarehouseReference Warehouse
Gets or sets the reference to the warehouse associated with the inventory decision.
bool HasValidSafetyStockViolations
Gets a value indicating whether every safety-stock violation is finite and non-negative.
IntegerTimeSeries Setups
Gets or sets the binary item-specific inventory-activation decisions.
void ResizeTimeSeries(int periodCount)
Resizes every decision series to the specified planning horizon.
bool IsSetupActivated(int period)
Determines whether the item-specific inventory activity is activated during a planning period.
InventoryDecision(int itemId, WarehouseReference warehouse, int planningHorizon)
Initializes an inventory decision for an item, a warehouse and a planning horizon.
int PlanningHorizon
Gets the number of planning periods represented by the inventory-level series.
DoubleTimeSeries AdditionalCapacityUsed
Gets or sets the item-specific additional capacity used during each planning period.
bool IsInternallyValid
Gets a value indicating whether the inventory decision is internally consistent.