Coverage for moptipyapps / prodsched / mfc_generator.py: 89%
173 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"""
2Methods for generating MFC instances.
4Here we provide some basic utilities for generating deterministic variants of
5instances such as those used in the paper [1] by Thürer et al.
7>>> from moptipyapps.utils.sampling import Gamma
8>>> inst = sample_mfc_instance([
9... Product(0, (0, 1), Gamma.from_alpha_beta(3, 0.26))], [
10... Station(0, Gamma.from_k_and_mean(3, 10)),
11... Station(1, Gamma.from_k_and_mean(2, 10))],
12... time_end_measure=100, seed=123)
14>>> inst.name
15'mfc_1_2_100_0x7b'
17>>> inst.n_demands
187
20>>> inst.demands
21(Demand(arrival=5.213885878801001, deadline=5.213885878801001, demand_id=0,\
22 customer_id=0, product_id=0, amount=1, measure=False),\
23 Demand(arrival=25.872387132411287, deadline=25.872387132411287, demand_id=1,\
24 customer_id=1, product_id=0, amount=1, measure=False),\
25 Demand(arrival=43.062182155666896, deadline=43.062182155666896, demand_id=2,\
26 customer_id=2, product_id=0, amount=1, measure=True),\
27 Demand(arrival=49.817978344678004, deadline=49.817978344678004, demand_id=3,\
28 customer_id=3, product_id=0, amount=1, measure=True),\
29 Demand(arrival=58.21166922638016, deadline=58.21166922638016, demand_id=4,\
30 customer_id=4, product_id=0, amount=1, measure=True),\
31 Demand(arrival=69.09054693162531, deadline=69.09054693162531, demand_id=5,\
32 customer_id=5, product_id=0, amount=1, measure=True),\
33 Demand(arrival=88.804579148131, deadline=88.804579148131, demand_id=6,\
34 customer_id=6, product_id=0, amount=1, measure=True))
36>>> len(inst.station_product_unit_times[0][0])
371600
39>>> len(inst.station_product_unit_times[1][0])
401600
42>>> inst.time_end_measure
43100.0
45>>> inst.time_end_warmup
4630.0
48>>> d = dict(inst.infos)
49>>> del d["info_generated_on"]
50>>> del d["info_generator_version"]
51>>> d
52{'info_generator': 'moptipyapps.prodsched.mfc_generator',\
53 'info_rand_seed_src': 'USER_PROVIDED',\
54 'info_rand_seed': '0x7b',\
55 'info_time_end_measure_src': 'USER_PROVIDED',\
56 'info_time_end_measure': '100',\
57 'info_time_end_warmup_src': 'SAMPLED',\
58 'info_time_end_warmup': '30',\
59 'info_name_src': 'SAMPLED',\
60 'info_product_interarrival_times[0]':\
61 'Erlang(k=3, theta=3.846153846153846)',\
62 'info_product_route[0]': 'USER_PROVIDED',\
63 'info_station_processing_time[0]':\
64 'Erlang(k=3, theta=3.3333333333333335)',\
65 'info_station_processing_time_window_length[0]': 'Const(v=0.125)',\
66 'info_station_processing_time[1]': 'Erlang(k=2, theta=5)',\
67 'info_station_processing_time_window_length[1]': 'Const(v=0.125)'}
69>>> inst = sample_mfc_instance(seed=23445)
70>>> inst.name
71'mfc_10_13_10000_0x5b95'
73>>> inst.n_demands
749922
76>>> len([dem for dem in inst.demands if dem.product_id == 0])
77959
79>>> len([dem for dem in inst.demands if dem.product_id == 1])
801055
82>>> [len(k[0]) for k in inst.station_product_unit_times]
83[160000, 160000, 0, 160000, 0, 0, 0, 0, 160000, 160000, 160000, 0, 0]
851. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson.
86 Material Flow Control in Make-to-Stock Production Systems: An Assessment of
87 Order Generation, Order Release and Production Authorization by Simulation
88 Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025.
89 doi:https://doi.org/10.1007/s10696-024-09532-2
90"""
91import datetime
92from dataclasses import dataclass
93from typing import Callable, Final, Iterable
95from moptipy.utils.nputils import (
96 rand_generator,
97 rand_seed_generate,
98)
99from moptipy.utils.strings import sanitize_name
100from numpy.random import Generator
101from pycommons.math.int_math import try_int
102from pycommons.strings.string_conv import num_to_str
103from pycommons.types import check_int_range, type_error
105from moptipyapps.prodsched.instance import (
106 KEY_IDX_END,
107 KEY_IDX_START,
108 MAX_VALUE,
109 Demand,
110 Instance,
111)
112from moptipyapps.utils.sampling import (
113 AtLeast,
114 Const,
115 Distribution,
116 Erlang,
117 Gamma,
118 Uniform,
119)
120from moptipyapps.version import __version__
122#: the "now" function
123__DTN: Final[Callable[[], datetime.datetime]] = datetime.datetime.now
126#: The information key for interarrival times
127INFO_PRODUCT_INTERARRIVAL_TIME_DIST: Final[str] = \
128 "info_product_interarrival_times"
130#: The generator key
131INFO_GENERATOR: Final[str] = "info_generator"
132#: The generator version
133INFO_GENERATOR_VERSION: Final[str] = "info_generator_version"
134#: When was the instance generated?
135INFO_GENERATED_ON: Final[str] = "info_generated_on"
136#: The information key for interarrival times
137INFO_PRODUCT_ROUTE: Final[str] = "info_product_route"
138#: a fixed structure
139INFO_USER_PROVIDED: Final[str] = "USER_PROVIDED"
140#: a sampled structure
141INFO_SAMPLED: Final[str] = "SAMPLED"
142#: The information key for the processing time distribution
143INFO_STATION_PROCESSING_TIME: Final[str] = "info_station_processing_time"
144#: The information key for the processing time window length distribution
145INFO_STATION_PROCESSING_WINDOW_LENGTH: Final[str] = \
146 "info_station_processing_time_window_length"
147#: the random seed
148INFO_RAND_SEED: Final[str] = "info_rand_seed"
149#: the random seed source
150INFO_RAND_SEED_SRC: Final[str] = f"{INFO_RAND_SEED}_src"
151#: the name source
152INFO_NAME_SRC: Final[str] = "info_name_src"
153#: the warmup time end
154INFO_TIME_END_WARMUP: Final[str] = "info_time_end_warmup"
155#: the source of the warmup time end
156INFO_TIME_END_WARMUP_SRC: Final[str] = f"{INFO_TIME_END_WARMUP}_src"
157#: the measurement time end
158INFO_TIME_END_MEASURE: Final[str] = "info_time_end_measure"
159#: the source of the measurement time end
160INFO_TIME_END_MEASURE_SRC: Final[str] = f"{INFO_TIME_END_MEASURE}_src"
163@dataclass(order=True, frozen=True)
164class Product:
165 """The product sampling definition."""
167 #: the product ID
168 product_id: int
169 #: the routing of the product
170 routing: tuple[int, ...]
171 #: the interarrival distribution
172 interarrival_times: Distribution
174 def __init__(self, product_id: int, routing: Iterable[int],
175 interarrival_times: int | float | Distribution) -> None:
176 """
177 Create the product sampling instruction.
179 :param product_id: the product id
180 :param routing: the routing information
181 :param interarrival_times: the interarrival time distribution
182 """
183 object.__setattr__(self, "product_id", check_int_range(
184 product_id, "product_id", 0, 1_000_000))
185 route: tuple[int, ...] = tuple(routing)
186 n_route: int = tuple.__len__(route)
187 if n_route <= 0:
188 raise ValueError("Route cannot be empty!")
189 for k in route:
190 check_int_range(k, "station", 0, 1_000_000)
191 object.__setattr__(self, "routing", route)
192 object.__setattr__(
193 self, "interarrival_times", AtLeast.greater_than_zero(
194 interarrival_times).simplify())
196 def log_info(self, infos: dict[str, str]) -> None:
197 """
198 Log the sampling information of this product to the infos `dict`.
200 :param infos: the information dictionary
201 """
202 key: Final[str] = f"{KEY_IDX_START}{self.product_id}{KEY_IDX_END}"
203 infos[f"{INFO_PRODUCT_INTERARRIVAL_TIME_DIST}{key}"] = repr(
204 self.interarrival_times)
205 infos[f"{INFO_PRODUCT_ROUTE}{key}"] = INFO_USER_PROVIDED
208@dataclass(order=True, frozen=True)
209class Station:
210 """The station sampling definition."""
212 #: the product ID
213 station_id: int
214 #: the processing time distribution
215 processing_time: Distribution
216 #: the processing window distribution
217 processing_windows: Distribution
219 def __init__(
220 self, station_id: int,
221 processing_time: int | float | Distribution,
222 processing_windows: int | float | Distribution | None = None) \
223 -> None:
224 """
225 Create the station sampling instruction.
227 :param station_id: the station id
228 :param processing_time: the processing time distribution
229 :param processing_windows: the processing time window length
230 distribution
231 """
232 object.__setattr__(self, "station_id", check_int_range(
233 station_id, "station_id", 0, 1_000_000))
234 object.__setattr__(
235 self, "processing_time",
236 AtLeast.greater_than_zero(processing_time).simplify())
238 if processing_windows is None:
239 processing_windows = Const(1 / 8)
240 object.__setattr__(
241 self, "processing_windows",
242 AtLeast.greater_than_zero(processing_windows).simplify())
244 def log_info(self, infos: dict[str, str]) -> None:
245 """
246 Log the sampling information of this product to the infos `dict`.
248 :param infos: the information dictionary
249 """
250 key: Final[str] = f"{KEY_IDX_START}{self.station_id}{KEY_IDX_END}"
251 infos[f"{INFO_STATION_PROCESSING_TIME}{key}"] = repr(
252 self.processing_time)
253 infos[f"{INFO_STATION_PROCESSING_WINDOW_LENGTH}{key}"] = repr(
254 self.processing_windows)
257# pylint: disable=R0914,R0912,R0915
258def sample_mfc_instance(products: Iterable[Product] | None = None,
259 stations: Iterable[Station] | None = None,
260 time_end_warmup: int | float | None = None,
261 time_end_measure: int | float | None = None,
262 name: str | None = None,
263 seed: int | None = None) -> Instance:
264 """
265 Sample an MFC instance.
267 :param products: the products
268 :param stations: the work stations
269 :param time_end_warmup: the end of the warmup period
270 :param time_end_measure: the end of the measurement period
271 :param name: the instance name
272 :param seed: the random seed, if any
273 :return: the instance
274 """
275 generator: str = str(__file__)
276 idx: int = str.rfind(generator, "moptipyapps")
277 if idx >= 0:
278 generator = str.removesuffix(generator[idx:].replace("/", "."), ".py")
279 else:
280 generator = "mfc_generator"
281 infos: Final[dict[str, str]] = {
282 INFO_GENERATOR: generator,
283 INFO_GENERATOR_VERSION: __version__,
284 INFO_GENERATED_ON: str(__DTN()),
285 }
287 if products is None:
288 products = default_products()
289 products = sorted(products)
290 n_products: Final[int] = list.__len__(products)
291 if n_products <= 0:
292 raise ValueError(f"Cannot have {n_products} products.")
294 ids: Final[set[int]] = set()
295 used_stations: Final[set[int]] = set()
296 for product in products:
297 if not isinstance(product, Product):
298 raise type_error(product, "product", Product)
299 ids.add(product.product_id)
300 used_stations.update(product.routing)
301 if (set.__len__(ids) != n_products) or (
302 max(ids) - min(ids) + 1 != n_products):
303 raise ValueError("Inconsistent product ids.")
305 n_stations: Final[int] = set.__len__(used_stations)
306 if not 0 < n_stations < 1_000_000:
307 raise ValueError(f"Invalid number {n_stations} of stations.")
308 if stations is None:
309 if n_stations == 13:
310 stations = default_stations()
311 else:
312 raise ValueError(
313 "Can only use default settings with 13 stations, "
314 f"but got {n_stations}.")
316 stations = sorted(stations)
317 n_stations_real: int = list.__len__(stations)
318 if n_stations_real != n_stations:
319 raise ValueError(
320 f"Products use {n_stations} stations,"
321 f" but {n_stations_real} are provided.")
323 ids.clear()
324 for station in stations:
325 if not isinstance(station, Station):
326 raise type_error(station, "station", Station)
327 ids.add(station.station_id)
328 min_id: int = min(ids)
329 max_id: int = max(ids)
330 if (set.__len__(ids) != n_stations) or (
331 max_id - min_id + 1 != n_stations):
332 raise ValueError("Inconsistent station ids.")
333 if ids != used_stations:
334 raise ValueError(
335 f"Station ids are {min_id}...{max_id}, but products use "
336 f"stations {sorted(used_stations)}.")
338 if seed is None:
339 infos[INFO_RAND_SEED_SRC] = INFO_SAMPLED
340 seed = rand_seed_generate()
341 else:
342 infos[INFO_RAND_SEED_SRC] = INFO_USER_PROVIDED
343 if not isinstance(seed, int):
344 raise type_error(seed, "seed", int)
345 infos[INFO_RAND_SEED] = hex(seed)
347 if time_end_measure is None:
348 time_end_measure = 10_000 if (time_end_warmup is None) or (
349 time_end_warmup <= 0) else max(
350 time_end_warmup + 1, (10 * time_end_warmup) / 3)
351 infos[INFO_TIME_END_MEASURE_SRC] = INFO_SAMPLED
352 else:
353 infos[INFO_TIME_END_MEASURE_SRC] = INFO_USER_PROVIDED
354 time_end_measure = try_int(time_end_measure)
355 if not 0 < time_end_measure < MAX_VALUE:
356 raise ValueError(
357 f"Invalid time_end_measure={time_end_measure}.")
358 infos[INFO_TIME_END_MEASURE] = num_to_str(time_end_measure)
360 if time_end_warmup is None:
361 time_end_warmup = (3 * time_end_measure) / 10
362 infos[INFO_TIME_END_WARMUP_SRC] = INFO_SAMPLED
363 else:
364 infos[INFO_TIME_END_WARMUP_SRC] = INFO_USER_PROVIDED
365 time_end_warmup = try_int(time_end_warmup)
366 if not 0 <= time_end_warmup < time_end_measure:
367 raise ValueError(f"Invalid time_end_warmup={time_end_warmup} "
368 f"for time_end_measure={time_end_measure}.")
369 infos[INFO_TIME_END_WARMUP] = num_to_str(time_end_warmup)
371 if name is None:
372 infos[INFO_NAME_SRC] = INFO_SAMPLED
373 s: str = num_to_str(time_end_measure).replace(".", "d")
374 name = f"mfc_{n_products}_{n_stations}_{s}_{seed:#x}"
375 else:
376 infos[INFO_NAME_SRC] = INFO_USER_PROVIDED
377 uname: str = sanitize_name(name)
378 if uname != name:
379 raise ValueError(f"Invalid name {name!r}.")
381 random: Final[Generator] = rand_generator(seed)
383 # sample the demands
384 demands: Final[list[Demand]] = []
385 current_id: int = 0
386 for product in products:
387 time: float = 0.0
388 while True:
389 until = product.interarrival_times.sample(random)
390 time += until
391 if time >= time_end_measure:
392 break
393 demands.append(Demand(
394 arrival=time, deadline=time, demand_id=current_id,
395 customer_id=current_id, product_id=product.product_id,
396 amount=1, measure=time_end_warmup <= time))
397 current_id += 1
399 #: sample the working times
400 production_times: Final[list[list[list[float]]]] = []
401 for station in stations:
402 times: list[float] = []
403 time = 0.0
404 while True:
405 processing = station.processing_time.sample(random)
406 window = station.processing_windows.sample(random)
407 time += window
408 times.extend((processing, time))
409 if time >= time_end_measure:
410 break
411 production_times.append([
412 times if station.station_id in product.routing else []
413 for product in products])
415 # log the information
416 for product in products:
417 product.log_info(infos)
418 for station in stations:
419 station.log_info(infos)
421 return Instance(
422 name=name, n_products=n_products,
423 n_customers=current_id, n_stations=n_stations,
424 n_demands=current_id,
425 time_end_warmup=time_end_warmup, time_end_measure=time_end_measure,
426 routes=(product.routing for product in products),
427 demands=demands, warehous_at_t0=[0] * n_products,
428 station_product_unit_times=production_times,
429 infos=infos)
432def __s1t0(s: Iterable[int]) -> tuple[int, ...]:
433 """
434 Convert stations from 1 to 0-based index.
436 :param s: the stations
437 :return: the index
439 >>> __s1t0((1, 2, 3))
440 (0, 1, 2)
441 """
442 return tuple(x - 1 for x in s)
445def default_products() -> tuple[Product, ...]:
446 """
447 Create the default product sequence as used in [1].
449 :return: the default product sequence
451 >>> default_products()
452 (Product(product_id=0, routing=(0, 1, 3, 1, 8, 9, 10), \
453interarrival_times=Erlang(k=3, theta=3.3333333333333335)), \
454Product(product_id=1, routing=(0, 1, 4, 1, 7, 8, 9, 10), \
455interarrival_times=Erlang(k=2, theta=5)), \
456Product(product_id=2, routing=(0, 1, 5, 3, 1, 8, 11, 10), \
457interarrival_times=Uniform(low=5, high=15)), \
458Product(product_id=3, routing=(0, 1, 6, 3, 1, 8, 9, 10), \
459interarrival_times=Erlang(k=3, theta=3.3333333333333335)), \
460Product(product_id=4, routing=(0, 1, 3, 11, 1, 8, 1, 12), \
461interarrival_times=Erlang(k=4, theta=2.5)), \
462Product(product_id=5, routing=(0, 1, 4, 11, 1, 8, 6, 12), \
463interarrival_times=Erlang(k=2, theta=5)), \
464Product(product_id=6, routing=(0, 1, 5, 11, 1, 7, 1, 12), \
465interarrival_times=Erlang(k=4, theta=2.5)), \
466Product(product_id=7, routing=(0, 1, 2, 6, 3, 11, 1, 7, 5, 8, 1, 12), \
467interarrival_times=Uniform(low=5, high=15)), \
468Product(product_id=8, routing=(0, 1, 2, 4, 3, 5, 11, 1, 7, 1, 9, 5, 12), \
469interarrival_times=Erlang(k=4, theta=2.5)), \
470Product(product_id=9, routing=(0, 1, 2, 5, 1, 3, 11, 6, 1, 8, 10, 4, 12), \
471interarrival_times=Erlang(k=2, theta=5)))
473 1. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark
474 Stevenson. Material Flow Control in Make-to-Stock Production Systems:
475 An Assessment of Order Generation, Order Release and Production
476 Authorization by Simulation Flexible Services and Manufacturing
477 Journal. 37(1):1-37. March 2025.
478 doi:https://doi.org/10.1007/s10696-024-09532-2
479 """
480 return (
481 Product(0, __s1t0((1, 2, 4, 2, 9, 10, 11)),
482 Erlang.from_k_and_mean(3, 10)),
483 Product(1, __s1t0((1, 2, 5, 2, 8, 9, 10, 11)),
484 Erlang.from_k_and_mean(2, 10)),
485 Product(2, __s1t0((1, 2, 6, 4, 2, 9, 12, 11)),
486 Uniform(5, 15)),
487 Product(3, __s1t0((1, 2, 7, 4, 2, 9, 10, 11)),
488 Erlang.from_k_and_mean(3, 10)),
489 Product(4, __s1t0((1, 2, 4, 12, 2, 9, 2, 13)),
490 Erlang.from_k_and_mean(4, 10)),
491 Product(5, __s1t0((1, 2, 5, 12, 2, 9, 7, 13)),
492 Erlang.from_k_and_mean(2, 10)),
493 Product(6, __s1t0((1, 2, 6, 12, 2, 8, 2, 13)),
494 Erlang.from_k_and_mean(4, 10)),
495 Product(7, __s1t0((1, 2, 3, 7, 4, 12, 2, 8, 6, 9, 2, 13)),
496 Uniform(5, 15)),
497 Product(8, __s1t0((1, 2, 3, 5, 4, 6, 12, 2, 8, 2, 10, 6, 13)),
498 Erlang.from_k_and_mean(4, 10)),
499 Product(9, __s1t0((1, 2, 3, 6, 2, 4, 12, 7, 2, 9, 11, 5, 13)),
500 Erlang.from_k_and_mean(2, 10)))
503def default_stations() -> tuple[Station, ...]:
504 """
505 Create the default station sequence as used in [1].
507 :return: the default product station
509 >>> default_stations()
510 (Station(station_id=0, processing_time=Erlang(k=3, theta=0.26),\
511 processing_windows=Const(v=0.125)),\
512 Station(station_id=1, processing_time=Erlang(k=3, theta=0.12),\
513 processing_windows=Const(v=0.125)),\
514 Station(station_id=2, processing_time=Erlang(k=2, theta=1.33),\
515 processing_windows=Const(v=0.125)),\
516 Station(station_id=3,\
517 processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1)),\
518 processing_windows=Const(v=0.125)),\
519 Station(station_id=4, processing_time=Erlang(k=3, theta=0.67),\
520 processing_windows=Const(v=0.125)),\
521 Station(station_id=5, processing_time=Erlang(k=4, theta=0.35),\
522 processing_windows=Const(v=0.125)),\
523 Station(station_id=6, processing_time=Erlang(k=3, theta=0.59),\
524 processing_windows=Const(v=0.125)),\
525 Station(station_id=7, processing_time=Erlang(k=3, theta=0.63),\
526 processing_windows=Const(v=0.125)),\
527 Station(station_id=8, processing_time=Erlang(k=2, theta=0.59),\
528 processing_windows=Const(v=0.125)),\
529 Station(station_id=9, processing_time=Erlang(k=3, theta=0.6),\
530 processing_windows=Const(v=0.125)),\
531 Station(station_id=10,\
532 processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1)),\
533 processing_windows=Const(v=0.125)),\
534 Station(station_id=11, processing_time=Erlang(k=4, theta=0.29),\
535 processing_windows=Const(v=0.125)),\
536 Station(station_id=12, processing_time=Erlang(k=3, theta=0.48),\
537 processing_windows=Const(v=0.125)))
539 1. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark
540 Stevenson. Material Flow Control in Make-to-Stock Production Systems:
541 An Assessment of Order Generation, Order Release and Production
542 Authorization by Simulation Flexible Services and Manufacturing
543 Journal. 37(1):1-37. March 2025.
544 doi:https://doi.org/10.1007/s10696-024-09532-2
545 """
546 return (
547 Station(0, Gamma(3, 0.26)),
548 Station(1, Gamma(3, 0.12)),
549 Station(2, Gamma(2, 1.33)),
550 Station(3, Gamma(1, 1.06)),
551 Station(4, Gamma(3, 0.67)),
552 Station(5, Gamma(4, 0.35)),
553 Station(6, Gamma(3, 0.59)),
554 Station(7, Gamma(3, 0.63)),
555 Station(8, Gamma(2, 0.59)),
556 Station(9, Gamma(3, 0.6)),
557 Station(10, Gamma(1, 1.44)),
558 Station(11, Gamma(4, 0.29)),
559 Station(12, Gamma(3, 0.48)))