Coverage for moptipyapps/prodsched/multistatistics.py: 69%
91 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-12 08:11 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-12 08:11 +0000
1"""
2A statistics record for multiple simulations.
4:class:`~MultiStatistics` are records that hold multiple simulation
5:class:`~moptipyapps.prodsched.statistics.Statistics`, each of which computed
6over a separate :class:`~moptipyapps.prodsched.simulation.Simulation` based on
7a separate :mod:`~moptipyapps.prodsched.instance` of the material flow
8problem.
9These records are filled with data by via the
10:class:`moptipyapps.prodsched.rop_multisimulation.ROPMultiSimulation`
11mechanism, which performs the multiple simulations.
13We cn use this record as the solution space when optimizing for the MFC
14scenario.
15Such a record holds comprehensive statistics across several simulation runs.
16This makes it suitable as source of data for objective functions
17(:class:`~moptipy.api.objective.Objective`).
18The objective functions can then access these statistics.
20Since we use :class:`~MultiStatistics` as solution space, we also need an
21implementation of moptipy's :class:`~moptipy.api.space.Space`-API to plug it
22into the optimization process.
23Sucha space implementation is provided as class
24:class:`~MultiStatisticsSpace`.
25It can create, copy, and serialize these objects to text, so that they can
26appear in the log files.
27"""
29from dataclasses import dataclass
30from typing import Final, Generator, Iterable, Self
32from moptipy.api.space import Space
33from moptipy.utils.logger import (
34 KeyValueLogSection,
35)
36from pycommons.types import type_error
38from moptipyapps.prodsched.instance import Instance
39from moptipyapps.prodsched.statistics import Statistics
40from moptipyapps.prodsched.statistics import to_stream as stat_to_stream
43@dataclass(order=False, frozen=True)
44class MultiStatistics:
45 """A set of statistics gathered over multiple instances."""
47 #: the per-instance statistics
48 per_instance: tuple[Statistics, ...]
49 #: the instance names
50 inst_names: tuple[str, ...]
52 def __init__(self, instances: tuple[Instance, ...],
53 names: tuple[str, ...] | None = None) -> None:
54 """
55 Create the multi-statistics object.
57 :param instances: the instances for which we create the statistics
58 """
59 object.__setattr__(self, "per_instance", tuple(
60 Statistics(inst.n_products, inst.n_stations)
61 for inst in instances))
62 if names is None:
63 names = tuple(inst.name for inst in instances)
64 elif tuple.__len__(names) != tuple.__len__(instances):
65 raise ValueError(f"names {names} do not fit")
66 object.__setattr__(self, "inst_names", names)
68 def from_stream(self, stream: Iterable[str]) -> Self:
69 """
70 Convert a stream of text to a multi-statistics object.
72 Warning: This method cannot restore the numbers `n` in the single
73 stream statistics.
75 :param stream: the stream
76 :return: the object itself
77 """
78 pi: Final[tuple[Statistics, ...]] = self.per_instance
79 n: Final[int] = tuple.__len__(pi)
80 for statistics in pi:
81 statistics.clear()
82 source = iter(stream)
83 needs: Final[set[int]] = set(range(n))
84 for srow in source:
85 row = str.strip(srow)
86 if not row.startswith("-"):
87 continue
88 space: int = row.index(" ", row.index(" ") + 1)
89 colon: int = row.index(":", space + 1)
90 prime_1: int = row.index("'", colon + 1)
91 prime_2: int = row.index("'", prime_1 + 1)
92 inst_name: str = str.strip(row[prime_1 + 1: prime_2])
93 inst_idx: int = int(row[space + 1:colon])
94 if inst_idx not in needs:
95 raise ValueError(f"Instance data {inst_idx} / "
96 f"{inst_name!r} already loaded.")
97 if self.inst_names[inst_idx] != inst_name:
98 raise ValueError(
99 f"Name {inst_name!r} of instance {inst_idx} should "
100 f"be {self.inst_names[inst_idx]!r}.")
101 pi[inst_idx].from_stream(source)
102 needs.remove(inst_idx)
104 if set.__len__(needs) != 0:
105 raise ValueError(f"Data for instances {needs} is missing.")
106 return self
109def to_stream(multi: MultiStatistics) -> Generator[str, None, None]:
110 """
111 Convert a multi-statistics object to a stream.
113 :param multi: the multi-statistics object
114 :return: the stream of strings
115 """
116 if not isinstance(multi, MultiStatistics):
117 raise type_error(multi, "multi", MultiStatistics)
118 for i, ss in enumerate(multi.per_instance):
119 yield f"-------- Instance {i}: {multi.inst_names[i]!r} -------"
120 yield from stat_to_stream(ss)
123class MultiStatisticsSpace(Space):
124 """An implementation of the `Space` API of for multiple statistics."""
126 def __init__(self, instances: tuple[Instance, ...]) -> None:
127 """
128 Create a multi-statistics space.
130 :param instances: the instances
131 """
132 if not isinstance(instances, tuple):
133 raise type_error(instances, "instances", tuple)
134 for inst in instances:
135 if not isinstance(inst, Instance):
136 raise type_error(inst, "instance", Instance)
137 #: The instance to which the statistics and simulations belong.
138 self.instances: Final[tuple[Instance, ...]] = instances
139 #: the instance names
140 self.__inst_names: Final[tuple[str, ...]] = tuple(
141 inst.name for inst in instances)
143 def copy(self, dest: MultiStatistics, source: MultiStatistics) -> None:
144 """
145 Copy one multi-statistics to another one.
147 :param dest: the destination multi-statistics
148 :param source: the source multi-statistics
149 """
150 for i, d in enumerate(dest.per_instance):
151 d.copy_from(source.per_instance[i])
153 def create(self) -> MultiStatistics:
154 """
155 Create an empty multi-statistics record.
157 :return: the empty multi-statistics record
158 """
159 return MultiStatistics(self.instances, self.__inst_names)
161 def to_str(self, x: MultiStatistics) -> str:
162 """
163 Convert a multi-statistics to a string.
165 :param x: the packing
166 :return: a string corresponding to the multi-statistics
167 """
168 return "\n".join(to_stream(x))
170 def is_equal(self, x1: MultiStatistics, x2: MultiStatistics) -> bool:
171 """
172 Check if two multi-statistics have the same contents.
174 :param x1: the first multi-statistics
175 :param x2: the second multi-statistics
176 :return: `True` if both multi-statistics have the same content
177 """
178 return (x1 is x2) or ((x1.per_instance == x2.per_instance) and (
179 x1.inst_names == x2.inst_names))
181 def from_str(self, text: str) -> MultiStatistics:
182 """
183 Convert a string to a multi-statistics.
185 Warning: This method cannot restore the numbers `n` in the single
186 stream statistics. Therefore, the returned objects cannot be identical
187 to the stored objects...
189 :param text: the string
190 :return: the multi-statistics
191 """
192 result: Final[MultiStatistics] = self.create().from_stream(
193 text.split())
194 self.validate(result)
195 return result
197 def validate(self, x: MultiStatistics) -> None:
198 """
199 Check if a multi-statistics is valid.
201 :param x: the multi-statistics
202 :raises TypeError: if any component of the multi-statistics is of the
203 wrong type
204 :raises ValueError: if the multi-statistics is not feasible
205 """
206 if not isinstance(x, MultiStatistics):
207 raise type_error(x, "x", MultiStatistics)
208 if not isinstance(x.per_instance, tuple):
209 raise type_error(x.per_instance, "x.per_instance", tuple)
210 for s in x.per_instance:
211 if not isinstance(s, Statistics):
212 raise type_error(s, "x.per_instance[i]", Statistics)
213 if x.inst_names != self.__inst_names:
214 raise ValueError("Wrong instance names.")
216 def n_points(self) -> int:
217 """
218 Get the number of possible multi-statistics.
220 :return: just some arbitrary very large number
221 """
222 return 100 ** tuple.__len__(self.instances)
224 def __str__(self) -> str:
225 """
226 Get the name of the multi-statistics space.
228 :return: the name
229 """
230 return f"multistats_{tuple.__len__(self.instances)}"
232 def log_parameters_to(self, logger: KeyValueLogSection) -> None:
233 """
234 Log the parameters of the space to the given logger.
236 :param logger: the logger for the parameters
237 """
238 super().log_parameters_to(logger)
239 for i, inst in enumerate(self.__inst_names):
240 logger.key_value(f"inst_{i}", inst)