Coverage for moptipyapps / prodsched / statistics.py: 98%
59 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-30 03:25 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-30 03:25 +0000
1"""
2A statistics record for the simulation.
4This module provides a record with statistics derived from one single
5MFC simulation. It can store values such as the mean fill rate or the
6mean stock level.
7Such statistics records are filled in by instances of the
8:class:`~moptipyapps.prodsched.statistics_collector.StatisticsCollector`
9plugged into the
10:class:`~moptipyapps.prodsched.simulation.Simulation`.
11"""
13from itertools import chain
14from typing import Callable, Final, Generator
16from moptipy.utils.logger import KEY_VALUE_SEPARATOR
17from pycommons.io.csv import CSV_SEPARATOR, SCOPE_SEPARATOR
18from pycommons.math.stream_statistics import (
19 KEY_MAXIMUM,
20 KEY_MEAN_ARITH,
21 KEY_MINIMUM,
22 KEY_STDDEV,
23 StreamStatistics,
24)
25from pycommons.strings.string_conv import num_or_none_to_str
26from pycommons.types import check_int_range, type_error
28#: the name of the statistics key
29COL_STAT: Final[str] = "stat"
30#: the total column name
31COL_TOTAL: Final[str] = "total"
32#: the statistics rate
33KEY_RATE: Final[str] = "rate"
34#: the product column prefix
35COL_PRODUCT_PREFIX: Final[str] = "product_"
36#: the mean TRP row
37ROW_TRP: Final[str] = "trp"
38#: the fill rate row
39ROW_FILL_RATE: Final[str] = f"fill{SCOPE_SEPARATOR}{KEY_RATE}"
40#: the CWT row
41ROW_CWT: Final[str] = "cwt"
42#: the mean stock level row
43ROW_STOCK_LEVEL_MEAN: Final[str] = \
44 f"stocklevel{SCOPE_SEPARATOR}{KEY_MEAN_ARITH}"
45#: the fulfilled rate
46ROW_FULFILLED_RATE: Final[str] = f"fulfilled{SCOPE_SEPARATOR}{KEY_RATE}"
47#: the simulation time getter
48ROW_SIMULATION_TIME: Final[str] = \
49 f"time{SCOPE_SEPARATOR}s{KEY_VALUE_SEPARATOR}"
51#: the statistics that we will print
52__STATS: tuple[tuple[str, Callable[[
53 StreamStatistics], int | float | None]], ...] = (
54 (KEY_MINIMUM, StreamStatistics.getter_or_none(KEY_MINIMUM)),
55 (KEY_MEAN_ARITH, StreamStatistics.getter_or_none(KEY_MEAN_ARITH)),
56 (KEY_MAXIMUM, StreamStatistics.getter_or_none(KEY_MAXIMUM)),
57 (KEY_STDDEV, StreamStatistics.getter_or_none(KEY_STDDEV)))
60class Statistics:
61 """
62 A statistics record based on production scheduling.
64 It provides the following statistics:
66 - :attr:`~immediate_rates`: The per-product-fillrate, i.e., the fraction
67 of demands of a given product that were immediately fulfilled when
68 arriving in the system (i.e., that were fulfilled by using product that
69 was available in the warehouse/in stock).
70 Higher values are good.
71 - :attr:`~immediate_rate`: The overall fillrate, i.e., the total fraction
72 of demands that were immediately fulfilled upon arrival in the system
73 over all demands. That is, this is the fraction of demands that were
74 fulfilled by using product that was available in the warehouse/in stock.
75 Higher values are good.
76 - :attr:`~waiting_times`: The per-product waiting times ("CWT") for the
77 demands that came in but could *not* immediately be fulfilled. These are
78 the demands for a given product that were, so to say, not covered by the
79 fillrate/:attr:`~immediate_rate`. If all demands of a product could
80 immediately be satisfied, then this is `None`.
81 Otherwise, smaller values are good.
82 - :attr:`~waiting_time`: The overall waiting times ("CWT") for the demands
83 that came in but could *not* immediately be fulfilled. These are all the
84 demands for a given product that were, so to say, not covered by the
85 fillrate/:attr:`~immediate_rate`. If all demands could immediately be
86 satisfied, then this is `None`.
87 Otherwise, smaller values are good.
88 - :attr:`~production_times`: The per-product times that producing one unit
89 of the product takes from the moment that a production job is created
90 until it is completed. Smaller values of this "TRP" are better.
91 - :attr:`~production_time`: The overall statistics on the times that
92 producing one unit of any product takes from the moment that a
93 production job is created until it is completed. Smaller values this
94 "TRP" are better.
95 - :attr:`~fulfilled_rates`: The per-product fraction of demands that were
96 satisfied. Demands for a product may remain unsatisfied if they have not
97 been satisfied by the end of the simulation period. Larger values are
98 better.
99 - :attr:`~fulfilled_rate`: The fraction of demands that were satisfied.
100 Demands may remain unsatisfied if they have not been satisfied by the
101 end of the simulation period. Larger values are better.
102 - :attr:`~stock_levels`: The average amount of a given product in the
103 warehouse averaged over the simulation time. Smaller values are better.
104 - :attr:`~stock_level`: The total average amount units of any product in
105 the warehouse averaged over the simulation time. Smaller values are
106 better.
107 - :attr:`~simulation_time_nanos`: The total time that the simulation took,
108 measured in nanoseconds.
110 Instances of this class are filled by
111 :class:`~moptipyapps.prodsched.statistics_collector.StatisticsCollector`
112 objects plugged into the
113 :class:`~moptipyapps.prodsched.simulation.Simulation`.
114 """
116 def __init__(self, n_products: int) -> None:
117 """
118 Create the statistics record for a given number of products.
120 :param n_products: the number of products
121 """
122 check_int_range(n_products, "n_products", 1, 1_000_000_000)
123 #: the production time (TRP) statistics per-product
124 self.production_times: Final[list[
125 StreamStatistics | None]] = [None] * n_products
126 #: the overall production time (TRP) statistics
127 self.production_time: StreamStatistics | None = None
128 #: the fraction of demands that were immediately satisfied,
129 #: on a per-product basis, i.e., the fillrate
130 self.immediate_rates: Final[list[int | float | None]] = (
131 [None] * n_products)
132 #: the overall fraction of immediately satisfied demands, i.e.,
133 #: the fillrate
134 self.immediate_rate: int | float | None = None
135 #: the average waiting time for all demands that were not immediately
136 #: satisfied -- only counting demands that were actually satisfied,
137 #: i.e., the CWT
138 self.waiting_times: Final[list[
139 StreamStatistics | None]] = [None] * n_products
140 #: the overall waiting time for all demands that were not immediately
141 #: satisfied -- only counting demands that were actually satisfied,
142 #: i.e., the CWT
143 self.waiting_time: StreamStatistics | None = None
144 #: the fraction of demands that were fulfilled, on a per-product basis
145 self.fulfilled_rates: Final[list[
146 int | float | None]] = [None] * n_products
147 #: the fraction of demands that were fulfilled overall
148 self.fulfilled_rate: int | float | None = None
149 #: the average stock level, on a per-product basis
150 self.stock_levels: Final[list[
151 int | float | None]] = [None] * n_products
152 #: the overall average stock level
153 self.stock_level: int | float | None = None
154 #: the nanoseconds used by the simulation
155 self.simulation_time_nanos: int | float | None = None
157 def __str__(self) -> str:
158 """Convert this object to a string."""
159 return "\n".join(to_stream(self))
161 def copy_from(self, stat: "Statistics") -> None:
162 """
163 Copy the contents of another statistics record.
165 :param stat: the other statistics record
166 """
167 if not isinstance(stat, Statistics):
168 raise type_error(stat, "stat", Statistics)
169 self.production_times[:] = stat.production_times
170 self.production_time = stat.production_time
171 self.immediate_rates[:] = stat.immediate_rates
172 self.immediate_rate = stat.immediate_rate
173 self.waiting_times[:] = stat.waiting_times
174 self.waiting_time = stat.waiting_time
175 self.fulfilled_rates[:] = stat.fulfilled_rates
176 self.fulfilled_rate = stat.fulfilled_rate
177 self.stock_levels[:] = stat.stock_levels
178 self.stock_level = stat.stock_level
179 self.simulation_time_nanos = stat.simulation_time_nanos
182def to_stream(stats: Statistics) -> Generator[str, None, None]:
183 """
184 Write a statistics record to a stream.
186 :param stats: the statistics record
187 :return: the stream of data
188 """
189 n_products: Final[int] = list.__len__(stats.production_times)
190 nts: Final[Callable[[int | float | None], str]] = num_or_none_to_str
192 yield str.join(CSV_SEPARATOR, chain((
193 COL_STAT, COL_TOTAL), (f"{COL_PRODUCT_PREFIX}{i}" for i in range(
194 n_products))))
196 for key, alle, single in (
197 (ROW_TRP, stats.production_times, stats.production_time),
198 (ROW_CWT, stats.waiting_times, stats.waiting_time)):
199 for stat, call in __STATS:
200 yield str.join(CSV_SEPARATOR, chain((
201 f"{key}{SCOPE_SEPARATOR}{stat}", nts(call(single))), (
202 map(nts, map(call, alle)))))
203 yield str.join(CSV_SEPARATOR, chain((
204 ROW_FILL_RATE, nts(stats.immediate_rate)), (
205 map(nts, stats.immediate_rates))))
206 yield str.join(CSV_SEPARATOR, chain((
207 ROW_STOCK_LEVEL_MEAN, nts(stats.stock_level)), (
208 map(nts, stats.stock_levels))))
209 yield str.join(CSV_SEPARATOR, chain((
210 ROW_FULFILLED_RATE, nts(stats.fulfilled_rate)), (
211 map(nts, stats.fulfilled_rates))))
212 yield f"{ROW_SIMULATION_TIME}{nts(
213 stats.simulation_time_nanos / 1_000_000_000)}"