Coverage for moptipyapps / prodsched / statistics.py: 78%
59 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 04:40 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-11 04:40 +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.
7"""
9from itertools import chain
10from typing import Callable, Final, Generator
12from moptipy.utils.logger import KEY_VALUE_SEPARATOR
13from pycommons.io.csv import CSV_SEPARATOR, SCOPE_SEPARATOR
14from pycommons.math.stream_statistics import (
15 KEY_MAXIMUM,
16 KEY_MEAN_ARITH,
17 KEY_MINIMUM,
18 KEY_STDDEV,
19 StreamStatistics,
20)
21from pycommons.strings.string_conv import num_or_none_to_str
22from pycommons.types import check_int_range, type_error
24#: the name of the statistics key
25COL_STAT: Final[str] = "stat"
26#: the total column name
27COL_TOTAL: Final[str] = "total"
28#: the statistics rate
29KEY_RATE: Final[str] = "rate"
30#: the product column prefix
31COL_PRODUCT_PREFIX: Final[str] = "product_"
32#: the mean TRP row
33ROW_TRP: Final[str] = "trp"
34#: the fill rate row
35ROW_FILL_RATE: Final[str] = f"fill{SCOPE_SEPARATOR}{KEY_RATE}"
36#: the CWT row
37ROW_CWT: Final[str] = "cwt"
38#: the mean stock level row
39ROW_STOCK_LEVEL_MEAN: Final[str] = \
40 f"stocklevel{SCOPE_SEPARATOR}{KEY_MEAN_ARITH}"
41#: the fulfilled rate
42ROW_FULFILLED_RATE: Final[str] = f"fulfilled{SCOPE_SEPARATOR}{KEY_RATE}"
43#: the simulation time getter
44ROW_SIMULATION_TIME: Final[str] = \
45 f"time{SCOPE_SEPARATOR}s{KEY_VALUE_SEPARATOR}"
47#: the statistics that we will print
48__STATS: tuple[tuple[str, Callable[[
49 StreamStatistics], int | float | None]], ...] = (
50 (KEY_MINIMUM, StreamStatistics.getter_or_none(KEY_MINIMUM)),
51 (KEY_MEAN_ARITH, StreamStatistics.getter_or_none(KEY_MEAN_ARITH)),
52 (KEY_MAXIMUM, StreamStatistics.getter_or_none(KEY_MAXIMUM)),
53 (KEY_STDDEV, StreamStatistics.getter_or_none(KEY_STDDEV)))
56class Statistics:
57 """A statistics record based on production scheduling."""
59 def __init__(self, n_products: int) -> None:
60 """
61 Create the statistics record.
63 :param n_products: the number of products
64 """
65 check_int_range(n_products, "n_products", 1, 1_000_000_000)
66 #: the production time statistics per-product
67 self.production_times: Final[list[
68 StreamStatistics | None]] = [None] * n_products
69 #: the overall production time statistics
70 self.production_time: StreamStatistics | None = None
71 #: the fraction of demands that were immediately satisfied,
72 #: on a per-product basis
73 self.immediate_rates: Final[list[int | float | None]] = (
74 [None] * n_products)
75 #: the overall fraction of immediately satisfied demands
76 self.immediate_rate: int | float | None = None
77 #: the average waiting time for all demands that were not immediately
78 #: satisfied -- only counting demands that were actually satisfied
79 self.waiting_times: Final[list[
80 StreamStatistics | None]] = [None] * n_products
81 #: the overall waiting time for all demands that were not immediately
82 #: satisfied -- only counting demands that were actually satisfied
83 self.waiting_time: StreamStatistics | None = None
84 #: the fraction of demands that were fulfilled, on a per-product basis
85 self.fulfilled_rates: Final[list[
86 int | float | None]] = [None] * n_products
87 #: the fraction of demands that were fulfilled overall
88 self.fulfilled_rate: int | float | None = None
89 #: the average stock level, on a per-product basis
90 self.stock_levels: Final[list[
91 int | float | None]] = [None] * n_products
92 #: the overall average stock level
93 self.stock_level: int | float | None = None
94 #: the nano seconds used by the simulation
95 self.simulation_time_nanos: int | float | None = None
97 def __str__(self) -> str:
98 """Convert this object to a string."""
99 return "\n".join(to_stream(self))
101 def copy_from(self, stat: "Statistics") -> None:
102 """
103 Copy the contents of another statistics record.
105 :param stat: the other statistics record
106 """
107 if not isinstance(stat, Statistics):
108 raise type_error(stat, "stat", Statistics)
109 self.production_times[:] = stat.production_times
110 self.production_time = stat.production_time
111 self.immediate_rates[:] = stat.immediate_rates
112 self.immediate_rate = stat.immediate_rate
113 self.waiting_times[:] = stat.waiting_times
114 self.waiting_time = stat.waiting_time
115 self.fulfilled_rates[:] = stat.fulfilled_rates
116 self.fulfilled_rate = stat.fulfilled_rate
117 self.stock_levels[:] = stat.stock_levels
118 self.stock_level = stat.stock_level
119 self.simulation_time_nanos = stat.simulation_time_nanos
122def to_stream(stats: Statistics) -> Generator[str, None, None]:
123 """
124 Write a statistics record to a stream.
126 :param stats: the statistics record
127 :return: the stream of data
128 """
129 n_products: Final[int] = list.__len__(stats.production_times)
130 nts: Final[Callable[[int | float | None], str]] = num_or_none_to_str
132 yield str.join(CSV_SEPARATOR, chain((
133 COL_STAT, COL_TOTAL), (f"{COL_PRODUCT_PREFIX}{i}" for i in range(
134 n_products))))
136 for key, alle, single in (
137 (ROW_TRP, stats.production_times, stats.production_time),
138 (ROW_CWT, stats.waiting_times, stats.waiting_time)):
139 for stat, call in __STATS:
140 yield str.join(CSV_SEPARATOR, chain((
141 f"{key}{SCOPE_SEPARATOR}{stat}", nts(call(single))), (
142 map(nts, map(call, alle)))))
143 yield str.join(CSV_SEPARATOR, chain((
144 ROW_FILL_RATE, nts(stats.immediate_rate)), (
145 map(nts, stats.immediate_rates))))
146 yield str.join(CSV_SEPARATOR, chain((
147 ROW_STOCK_LEVEL_MEAN, nts(stats.stock_level)), (
148 map(nts, stats.stock_levels))))
149 yield str.join(CSV_SEPARATOR, chain((
150 ROW_FULFILLED_RATE, nts(stats.fulfilled_rate)), (
151 map(nts, stats.fulfilled_rates))))
152 yield f"{ROW_SIMULATION_TIME}{nts(
153 stats.simulation_time_nanos / 1_000_000_000)}"