Coverage for moptipyapps/prodsched/mfc_generator.py: 89%

173 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-05-28 09:42 +0000

1""" 

2Methods for generating MFC instances. 

3 

4In the module :mod:`~moptipyapps.prodsched.instance`, we provide the class 

5:class:`~moptipyapps.prodsched.instance.Instance`. Objects of this class 

6represent a fully deterministic production scheduling scenario. They prescribe 

7demands (:class:`~moptipyapps.prodsched.instance.Demand`) arriving at fixed 

8points in time in the system and work stations that need fixed amounts of 

9work times per product during certain time periods. 

10This allows us to create fully reproducible simulations 

11(:mod:`~moptipyapps.prodsched.simulation`) that show what a factory would do 

12to satisfy the demands. 

13 

14But where does such instance data come from? 

15 

16In the existing research on material flow control, no such fixed instances 

17exist. We invented them. Instead, the existing research [1] uses fixed numbers 

18of products and machines, fixed routes of products through machines, and 

19random distributions to generate demands and work times. 

20 

21So we create the function :func:`~default_stations` that creates the standard 

22work time distributions for the standard work stations. We also create the 

23function :func:`~default_products` that creates the default distributions 

24for the default products. 

25 

26The function :func:`sample_mfc_instance` then creates a material flow instance 

27following these distributions based on a given random seed. 

28This allows us to create scenarios that follow the same structure and random 

29distributions as prescribed in the paper [1] by Thürer et al. 

30However, our instances are fully deterministic. 

31 

32Once could not create a certain number of such instances and average 

33performance metrics over simulations on them. This would likely yield metrics 

34of reasonable accuracy, while allowing us to reproduce, analyze, and trace 

35every single production decision if need be. 

36 

37>>> from moptipyapps.utils.sampling import Gamma 

38>>> inst = sample_mfc_instance([ 

39... Product(0, (0, 1), Gamma.from_alpha_beta(3, 0.26))], [ 

40... Station(0, Gamma.from_k_and_mean(3, 10)), 

41... Station(1, Gamma.from_k_and_mean(2, 10))], 

42... time_end_measure=100, seed=123) 

43 

44>>> inst.name 

45'mfc_1_2_100_0x7b' 

46 

47>>> inst.n_demands 

487 

49 

50>>> inst.demands 

51(Demand(arrival=5.213885878801001, deadline=5.213885878801001, demand_id=0,\ 

52 customer_id=0, product_id=0, amount=1, measure=False),\ 

53 Demand(arrival=25.872387132411287, deadline=25.872387132411287, demand_id=1,\ 

54 customer_id=1, product_id=0, amount=1, measure=False),\ 

55 Demand(arrival=43.062182155666896, deadline=43.062182155666896, demand_id=2,\ 

56 customer_id=2, product_id=0, amount=1, measure=True),\ 

57 Demand(arrival=49.817978344678004, deadline=49.817978344678004, demand_id=3,\ 

58 customer_id=3, product_id=0, amount=1, measure=True),\ 

59 Demand(arrival=58.21166922638016, deadline=58.21166922638016, demand_id=4,\ 

60 customer_id=4, product_id=0, amount=1, measure=True),\ 

61 Demand(arrival=69.09054693162531, deadline=69.09054693162531, demand_id=5,\ 

62 customer_id=5, product_id=0, amount=1, measure=True),\ 

63 Demand(arrival=88.804579148131, deadline=88.804579148131, demand_id=6,\ 

64 customer_id=6, product_id=0, amount=1, measure=True)) 

65 

66>>> len(inst.station_product_unit_times[0][0]) 

671600 

68 

69>>> len(inst.station_product_unit_times[1][0]) 

701600 

71 

72>>> inst.time_end_measure 

73100.0 

74 

75>>> inst.time_end_warmup 

7630.0 

77 

78>>> d = dict(inst.infos) 

79>>> del d["info_generated_on"] 

80>>> del d["info_generator_version"] 

81>>> d 

82{'info_generator': 'moptipyapps.prodsched.mfc_generator',\ 

83 'info_rand_seed_src': 'USER_PROVIDED',\ 

84 'info_rand_seed': '0x7b',\ 

85 'info_time_end_measure_src': 'USER_PROVIDED',\ 

86 'info_time_end_measure': '100',\ 

87 'info_time_end_warmup_src': 'SAMPLED',\ 

88 'info_time_end_warmup': '30',\ 

89 'info_name_src': 'SAMPLED',\ 

90 'info_product_interarrival_times[0]':\ 

91 'Erlang(k=3, theta=3.846153846153846)',\ 

92 'info_product_route[0]': 'USER_PROVIDED',\ 

93 'info_station_processing_time[0]':\ 

94 'Erlang(k=3, theta=3.3333333333333335)',\ 

95 'info_station_processing_time_window_length[0]': 'Const(v=0.125)',\ 

96 'info_station_processing_time[1]': 'Erlang(k=2, theta=5)',\ 

97 'info_station_processing_time_window_length[1]': 'Const(v=0.125)'} 

98 

99>>> inst = sample_mfc_instance(seed=23445) 

100>>> inst.name 

101'mfc_10_13_10000_0x5b95' 

102 

103>>> inst.n_demands 

1049922 

105 

106>>> len([dem for dem in inst.demands if dem.product_id == 0]) 

107959 

108 

109>>> len([dem for dem in inst.demands if dem.product_id == 1]) 

1101055 

111 

112>>> [len(k[0]) for k in inst.station_product_unit_times] 

113[160000, 160000, 0, 160000, 0, 0, 0, 0, 160000, 160000, 160000, 0, 0] 

114 

1151. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson. 

116 Material Flow Control in Make-to-Stock Production Systems: An Assessment of 

117 Order Generation, Order Release and Production Authorization by Simulation 

118 Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025. 

119 doi:<https://doi.org/10.1007/s10696-024-09532-2> 

120""" 

121import datetime 

122from dataclasses import dataclass 

123from typing import Callable, Final, Iterable 

124 

125from moptipy.utils.nputils import ( 

126 rand_generator, 

127 rand_seed_generate, 

128) 

129from moptipy.utils.strings import sanitize_name 

130from numpy.random import Generator 

131from pycommons.math.int_math import try_int 

132from pycommons.strings.string_conv import num_to_str 

133from pycommons.types import check_int_range, type_error 

134 

135from moptipyapps.prodsched.instance import ( 

136 KEY_IDX_END, 

137 KEY_IDX_START, 

138 MAX_VALUE, 

139 Demand, 

140 Instance, 

141) 

142from moptipyapps.utils.sampling import ( 

143 AtLeast, 

144 Const, 

145 Distribution, 

146 Erlang, 

147 Gamma, 

148 Uniform, 

149) 

150from moptipyapps.version import __version__ 

151 

152#: the "now" function 

153__DTN: Final[Callable[[], datetime.datetime]] = datetime.datetime.now 

154 

155 

156#: The information key for interarrival times 

157INFO_PRODUCT_INTERARRIVAL_TIME_DIST: Final[str] = \ 

158 "info_product_interarrival_times" 

159 

160#: The generator key 

161INFO_GENERATOR: Final[str] = "info_generator" 

162#: The generator version 

163INFO_GENERATOR_VERSION: Final[str] = "info_generator_version" 

164#: When was the instance generated? 

165INFO_GENERATED_ON: Final[str] = "info_generated_on" 

166#: The information key for interarrival times 

167INFO_PRODUCT_ROUTE: Final[str] = "info_product_route" 

168#: a fixed structure 

169INFO_USER_PROVIDED: Final[str] = "USER_PROVIDED" 

170#: a sampled structure 

171INFO_SAMPLED: Final[str] = "SAMPLED" 

172#: The information key for the processing time distribution 

173INFO_STATION_PROCESSING_TIME: Final[str] = "info_station_processing_time" 

174#: The information key for the processing time window length distribution 

175INFO_STATION_PROCESSING_WINDOW_LENGTH: Final[str] = \ 

176 "info_station_processing_time_window_length" 

177#: the random seed 

178INFO_RAND_SEED: Final[str] = "info_rand_seed" 

179#: the random seed source 

180INFO_RAND_SEED_SRC: Final[str] = f"{INFO_RAND_SEED}_src" 

181#: the name source 

182INFO_NAME_SRC: Final[str] = "info_name_src" 

183#: the warmup time end 

184INFO_TIME_END_WARMUP: Final[str] = "info_time_end_warmup" 

185#: the source of the warmup time end 

186INFO_TIME_END_WARMUP_SRC: Final[str] = f"{INFO_TIME_END_WARMUP}_src" 

187#: the measurement time end 

188INFO_TIME_END_MEASURE: Final[str] = "info_time_end_measure" 

189#: the source of the measurement time end 

190INFO_TIME_END_MEASURE_SRC: Final[str] = f"{INFO_TIME_END_MEASURE}_src" 

191 

192 

193@dataclass(order=True, frozen=True) 

194class Product: 

195 """The product sampling definition.""" 

196 

197 #: the product ID 

198 product_id: int 

199 #: the routing of the product 

200 routing: tuple[int, ...] 

201 #: the interarrival distribution 

202 interarrival_times: Distribution 

203 

204 def __init__(self, product_id: int, routing: Iterable[int], 

205 interarrival_times: int | float | Distribution) -> None: 

206 """ 

207 Create the product sampling instruction. 

208 

209 :param product_id: the product id 

210 :param routing: the routing information 

211 :param interarrival_times: the interarrival time distribution 

212 """ 

213 object.__setattr__(self, "product_id", check_int_range( 

214 product_id, "product_id", 0, 1_000_000)) 

215 route: tuple[int, ...] = tuple(routing) 

216 n_route: int = tuple.__len__(route) 

217 if n_route <= 0: 

218 raise ValueError("Route cannot be empty!") 

219 for k in route: 

220 check_int_range(k, "station", 0, 1_000_000) 

221 object.__setattr__(self, "routing", route) 

222 object.__setattr__( 

223 self, "interarrival_times", AtLeast.greater_than_zero( 

224 interarrival_times).simplify()) 

225 

226 def log_info(self, infos: dict[str, str]) -> None: 

227 """ 

228 Log the sampling information of this product to the infos `dict`. 

229 

230 :param infos: the information dictionary 

231 """ 

232 key: Final[str] = f"{KEY_IDX_START}{self.product_id}{KEY_IDX_END}" 

233 infos[f"{INFO_PRODUCT_INTERARRIVAL_TIME_DIST}{key}"] = repr( 

234 self.interarrival_times) 

235 infos[f"{INFO_PRODUCT_ROUTE}{key}"] = INFO_USER_PROVIDED 

236 

237 

238@dataclass(order=True, frozen=True) 

239class Station: 

240 """The station sampling definition.""" 

241 

242 #: the product ID 

243 station_id: int 

244 #: the processing time distribution 

245 processing_time: Distribution 

246 #: the processing window distribution 

247 processing_windows: Distribution 

248 

249 def __init__( 

250 self, station_id: int, 

251 processing_time: int | float | Distribution, 

252 processing_windows: int | float | Distribution | None = None) \ 

253 -> None: 

254 """ 

255 Create the station sampling instruction. 

256 

257 :param station_id: the station id 

258 :param processing_time: the processing time distribution 

259 :param processing_windows: the processing time window length 

260 distribution 

261 """ 

262 object.__setattr__(self, "station_id", check_int_range( 

263 station_id, "station_id", 0, 1_000_000)) 

264 object.__setattr__( 

265 self, "processing_time", 

266 AtLeast.greater_than_zero(processing_time).simplify()) 

267 

268 if processing_windows is None: 

269 processing_windows = Const(1 / 8) 

270 object.__setattr__( 

271 self, "processing_windows", 

272 AtLeast.greater_than_zero(processing_windows).simplify()) 

273 

274 def log_info(self, infos: dict[str, str]) -> None: 

275 """ 

276 Log the sampling information of this product to the infos `dict`. 

277 

278 :param infos: the information dictionary 

279 """ 

280 key: Final[str] = f"{KEY_IDX_START}{self.station_id}{KEY_IDX_END}" 

281 infos[f"{INFO_STATION_PROCESSING_TIME}{key}"] = repr( 

282 self.processing_time) 

283 infos[f"{INFO_STATION_PROCESSING_WINDOW_LENGTH}{key}"] = repr( 

284 self.processing_windows) 

285 

286 

287# pylint: disable=R0914,R0912,R0915 

288def sample_mfc_instance(products: Iterable[Product] | None = None, 

289 stations: Iterable[Station] | None = None, 

290 time_end_warmup: int | float | None = None, 

291 time_end_measure: int | float | None = None, 

292 name: str | None = None, 

293 seed: int | None = None) -> Instance: 

294 """ 

295 Sample an MFC instance. 

296 

297 :param products: the products 

298 :param stations: the work stations 

299 :param time_end_warmup: the end of the warmup period 

300 :param time_end_measure: the end of the measurement period 

301 :param name: the instance name 

302 :param seed: the random seed, if any 

303 :return: the instance 

304 """ 

305 generator: str = str(__file__) 

306 idx: int = str.rfind(generator, "moptipyapps") 

307 if idx >= 0: 

308 generator = str.removesuffix(generator[idx:].replace("/", "."), ".py") 

309 else: 

310 generator = "mfc_generator" 

311 infos: Final[dict[str, str]] = { 

312 INFO_GENERATOR: generator, 

313 INFO_GENERATOR_VERSION: __version__, 

314 INFO_GENERATED_ON: str(__DTN()), 

315 } 

316 

317 if products is None: 

318 products = default_products() 

319 products = sorted(products) 

320 n_products: Final[int] = list.__len__(products) 

321 if n_products <= 0: 

322 raise ValueError(f"Cannot have {n_products} products.") 

323 

324 ids: Final[set[int]] = set() 

325 used_stations: Final[set[int]] = set() 

326 for product in products: 

327 if not isinstance(product, Product): 

328 raise type_error(product, "product", Product) 

329 ids.add(product.product_id) 

330 used_stations.update(product.routing) 

331 if (set.__len__(ids) != n_products) or ( 

332 max(ids) - min(ids) + 1 != n_products): 

333 raise ValueError("Inconsistent product ids.") 

334 

335 n_stations: Final[int] = set.__len__(used_stations) 

336 if not 0 < n_stations < 1_000_000: 

337 raise ValueError(f"Invalid number {n_stations} of stations.") 

338 if stations is None: 

339 if n_stations == 13: 

340 stations = default_stations() 

341 else: 

342 raise ValueError( 

343 "Can only use default settings with 13 stations, " 

344 f"but got {n_stations}.") 

345 

346 stations = sorted(stations) 

347 n_stations_real: int = list.__len__(stations) 

348 if n_stations_real != n_stations: 

349 raise ValueError( 

350 f"Products use {n_stations} stations," 

351 f" but {n_stations_real} are provided.") 

352 

353 ids.clear() 

354 for station in stations: 

355 if not isinstance(station, Station): 

356 raise type_error(station, "station", Station) 

357 ids.add(station.station_id) 

358 min_id: int = min(ids) 

359 max_id: int = max(ids) 

360 if (set.__len__(ids) != n_stations) or ( 

361 max_id - min_id + 1 != n_stations): 

362 raise ValueError("Inconsistent station ids.") 

363 if ids != used_stations: 

364 raise ValueError( 

365 f"Station ids are {min_id}...{max_id}, but products use " 

366 f"stations {sorted(used_stations)}.") 

367 

368 if seed is None: 

369 infos[INFO_RAND_SEED_SRC] = INFO_SAMPLED 

370 seed = rand_seed_generate() 

371 else: 

372 infos[INFO_RAND_SEED_SRC] = INFO_USER_PROVIDED 

373 if not isinstance(seed, int): 

374 raise type_error(seed, "seed", int) 

375 infos[INFO_RAND_SEED] = hex(seed) 

376 

377 if time_end_measure is None: 

378 time_end_measure = 10_000 if (time_end_warmup is None) or ( 

379 time_end_warmup <= 0) else max( 

380 time_end_warmup + 1, (10 * time_end_warmup) / 3) 

381 infos[INFO_TIME_END_MEASURE_SRC] = INFO_SAMPLED 

382 else: 

383 infos[INFO_TIME_END_MEASURE_SRC] = INFO_USER_PROVIDED 

384 time_end_measure = try_int(time_end_measure) 

385 if not 0 < time_end_measure < MAX_VALUE: 

386 raise ValueError( 

387 f"Invalid time_end_measure={time_end_measure}.") 

388 infos[INFO_TIME_END_MEASURE] = num_to_str(time_end_measure) 

389 

390 if time_end_warmup is None: 

391 time_end_warmup = (3 * time_end_measure) / 10 

392 infos[INFO_TIME_END_WARMUP_SRC] = INFO_SAMPLED 

393 else: 

394 infos[INFO_TIME_END_WARMUP_SRC] = INFO_USER_PROVIDED 

395 time_end_warmup = try_int(time_end_warmup) 

396 if not 0 <= time_end_warmup < time_end_measure: 

397 raise ValueError(f"Invalid time_end_warmup={time_end_warmup} " 

398 f"for time_end_measure={time_end_measure}.") 

399 infos[INFO_TIME_END_WARMUP] = num_to_str(time_end_warmup) 

400 

401 if name is None: 

402 infos[INFO_NAME_SRC] = INFO_SAMPLED 

403 s: str = num_to_str(time_end_measure).replace(".", "d") 

404 name = f"mfc_{n_products}_{n_stations}_{s}_{seed:#x}" 

405 else: 

406 infos[INFO_NAME_SRC] = INFO_USER_PROVIDED 

407 uname: str = sanitize_name(name) 

408 if uname != name: 

409 raise ValueError(f"Invalid name {name!r}.") 

410 

411 random: Final[Generator] = rand_generator(seed) 

412 

413 # sample the demands 

414 demands: Final[list[Demand]] = [] 

415 current_id: int = 0 

416 for product in products: 

417 time: float = 0.0 

418 while True: 

419 until = product.interarrival_times.sample(random) 

420 time += until 

421 if time >= time_end_measure: 

422 break 

423 demands.append(Demand( 

424 arrival=time, deadline=time, demand_id=current_id, 

425 customer_id=current_id, product_id=product.product_id, 

426 amount=1, measure=time_end_warmup <= time)) 

427 current_id += 1 

428 

429 #: sample the working times 

430 production_times: Final[list[list[list[float]]]] = [] 

431 for station in stations: 

432 times: list[float] = [] 

433 time = 0.0 

434 while True: 

435 processing = station.processing_time.sample(random) 

436 window = station.processing_windows.sample(random) 

437 time += window 

438 times.extend((processing, time)) 

439 if time >= time_end_measure: 

440 break 

441 production_times.append([ 

442 times if station.station_id in product.routing else [] 

443 for product in products]) 

444 

445 # log the information 

446 for product in products: 

447 product.log_info(infos) 

448 for station in stations: 

449 station.log_info(infos) 

450 

451 return Instance( 

452 name=name, n_products=n_products, 

453 n_customers=current_id, n_stations=n_stations, 

454 n_demands=current_id, 

455 time_end_warmup=time_end_warmup, time_end_measure=time_end_measure, 

456 routes=(product.routing for product in products), 

457 demands=demands, warehous_at_t0=[0] * n_products, 

458 station_product_unit_times=production_times, 

459 infos=infos) 

460 

461 

462def __s1t0(s: Iterable[int]) -> tuple[int, ...]: 

463 """ 

464 Convert stations from 1 to 0-based index. 

465 

466 :param s: the stations 

467 :return: the index 

468 

469 >>> __s1t0((1, 2, 3)) 

470 (0, 1, 2) 

471 """ 

472 return tuple(x - 1 for x in s) 

473 

474 

475def default_products() -> tuple[Product, ...]: 

476 """ 

477 Create the default product sequence as used in [1]. 

478 

479 :return: the default product sequence 

480 

481 >>> default_products() 

482 (Product(product_id=0, routing=(0, 1, 3, 1, 8, 9, 10), \ 

483interarrival_times=Erlang(k=3, theta=3.3333333333333335)), \ 

484Product(product_id=1, routing=(0, 1, 4, 1, 7, 8, 9, 10), \ 

485interarrival_times=Erlang(k=2, theta=5)), \ 

486Product(product_id=2, routing=(0, 1, 5, 3, 1, 8, 11, 10), \ 

487interarrival_times=Uniform(low=5, high=15)), \ 

488Product(product_id=3, routing=(0, 1, 6, 3, 1, 8, 9, 10), \ 

489interarrival_times=Erlang(k=3, theta=3.3333333333333335)), \ 

490Product(product_id=4, routing=(0, 1, 3, 11, 1, 8, 1, 12), \ 

491interarrival_times=Erlang(k=4, theta=2.5)), \ 

492Product(product_id=5, routing=(0, 1, 4, 11, 1, 8, 6, 12), \ 

493interarrival_times=Erlang(k=2, theta=5)), \ 

494Product(product_id=6, routing=(0, 1, 5, 11, 1, 7, 1, 12), \ 

495interarrival_times=Erlang(k=4, theta=2.5)), \ 

496Product(product_id=7, routing=(0, 1, 2, 6, 3, 11, 1, 7, 5, 8, 1, 12), \ 

497interarrival_times=Uniform(low=5, high=15)), \ 

498Product(product_id=8, routing=(0, 1, 2, 4, 3, 5, 11, 1, 7, 1, 9, 5, 12), \ 

499interarrival_times=Erlang(k=4, theta=2.5)), \ 

500Product(product_id=9, routing=(0, 1, 2, 5, 1, 3, 11, 6, 1, 8, 10, 4, 12), \ 

501interarrival_times=Erlang(k=2, theta=5))) 

502 

503 Now we reproduce the data from Table 2 of the paper: 

504 

505 >>> for i, p in enumerate(default_products()): 

506 ... itat = p.interarrival_times 

507 ... s = str(itat) 

508 ... s = f"{i + 1}: {s[:s.index(',')]}, mean={itat.mean()})" 

509 ... s += f"; routing: {','.join(str(j + 1) for j in p.routing)}" 

510 ... print(s) 

511 1: Erlang(k=3, mean=10); routing: 1,2,4,2,9,10,11 

512 2: Erlang(k=2, mean=10); routing: 1,2,5,2,8,9,10,11 

513 3: Uniform(low=5, mean=10); routing: 1,2,6,4,2,9,12,11 

514 4: Erlang(k=3, mean=10); routing: 1,2,7,4,2,9,10,11 

515 5: Erlang(k=4, mean=10); routing: 1,2,4,12,2,9,2,13 

516 6: Erlang(k=2, mean=10); routing: 1,2,5,12,2,9,7,13 

517 7: Erlang(k=4, mean=10); routing: 1,2,6,12,2,8,2,13 

518 8: Uniform(low=5, mean=10); routing: 1,2,3,7,4,12,2,8,6,9,2,13 

519 9: Erlang(k=4, mean=10); routing: 1,2,3,5,4,6,12,2,8,2,10,6,13 

520 10: Erlang(k=2, mean=10); routing: 1,2,3,6,2,4,12,7,2,9,11,5,13 

521 

522 1. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark 

523 Stevenson. Material Flow Control in Make-to-Stock Production Systems: 

524 An Assessment of Order Generation, Order Release and Production 

525 Authorization by Simulation Flexible Services and Manufacturing 

526 Journal. 37(1):1-37. March 2025. 

527 doi: https://doi.org/10.1007/s10696-024-09532-2 

528 """ 

529 return ( 

530 Product(0, __s1t0((1, 2, 4, 2, 9, 10, 11)), 

531 Erlang.from_k_and_mean(3, 10)), 

532 Product(1, __s1t0((1, 2, 5, 2, 8, 9, 10, 11)), 

533 Erlang.from_k_and_mean(2, 10)), 

534 Product(2, __s1t0((1, 2, 6, 4, 2, 9, 12, 11)), 

535 Uniform(5, 15)), 

536 Product(3, __s1t0((1, 2, 7, 4, 2, 9, 10, 11)), 

537 Erlang.from_k_and_mean(3, 10)), 

538 Product(4, __s1t0((1, 2, 4, 12, 2, 9, 2, 13)), 

539 Erlang.from_k_and_mean(4, 10)), 

540 Product(5, __s1t0((1, 2, 5, 12, 2, 9, 7, 13)), 

541 Erlang.from_k_and_mean(2, 10)), 

542 Product(6, __s1t0((1, 2, 6, 12, 2, 8, 2, 13)), 

543 Erlang.from_k_and_mean(4, 10)), 

544 Product(7, __s1t0((1, 2, 3, 7, 4, 12, 2, 8, 6, 9, 2, 13)), 

545 Uniform(5, 15)), 

546 Product(8, __s1t0((1, 2, 3, 5, 4, 6, 12, 2, 8, 2, 10, 6, 13)), 

547 Erlang.from_k_and_mean(4, 10)), 

548 Product(9, __s1t0((1, 2, 3, 6, 2, 4, 12, 7, 2, 9, 11, 5, 13)), 

549 Erlang.from_k_and_mean(2, 10))) 

550 

551 

552def default_stations() -> tuple[Station, ...]: 

553 """ 

554 Create the default station sequence as used in [1]. 

555 

556 :return: the default product station 

557 

558 >>> default_stations() 

559 (Station(station_id=0, processing_time=Erlang(k=3, theta=0.26),\ 

560 processing_windows=Const(v=0.125)),\ 

561 Station(station_id=1, processing_time=Erlang(k=3, theta=0.12),\ 

562 processing_windows=Const(v=0.125)),\ 

563 Station(station_id=2, processing_time=Erlang(k=2, theta=1.33),\ 

564 processing_windows=Const(v=0.125)),\ 

565 Station(station_id=3,\ 

566 processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1.06)),\ 

567 processing_windows=Const(v=0.125)),\ 

568 Station(station_id=4, processing_time=Erlang(k=3, theta=0.67),\ 

569 processing_windows=Const(v=0.125)),\ 

570 Station(station_id=5, processing_time=Erlang(k=4, theta=0.35),\ 

571 processing_windows=Const(v=0.125)),\ 

572 Station(station_id=6, processing_time=Erlang(k=3, theta=0.59),\ 

573 processing_windows=Const(v=0.125)),\ 

574 Station(station_id=7, processing_time=Erlang(k=3, theta=0.63),\ 

575 processing_windows=Const(v=0.125)),\ 

576 Station(station_id=8, processing_time=Erlang(k=2, theta=0.59),\ 

577 processing_windows=Const(v=0.125)),\ 

578 Station(station_id=9, processing_time=Erlang(k=3, theta=0.6),\ 

579 processing_windows=Const(v=0.125)),\ 

580 Station(station_id=10,\ 

581 processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1.44)),\ 

582 processing_windows=Const(v=0.125)),\ 

583 Station(station_id=11, processing_time=Erlang(k=4, theta=0.29),\ 

584 processing_windows=Const(v=0.125)),\ 

585 Station(station_id=12, processing_time=Erlang(k=3, theta=0.48),\ 

586 processing_windows=Const(v=0.125))) 

587 

588 Now we reproduce the data from Table 3 of the paper: 

589 

590 >>> from moptipyapps.utils.sampling import Exponential, AtLeast 

591 >>> from numpy import array 

592 >>> from numpy.random import default_rng 

593 >>> g = default_rng(seed=11) 

594 >>> for i, p in enumerate(default_stations()): 

595 ... pti = p.processing_time 

596 ... if isinstance(pti, AtLeast): 

597 ... pti = pti.d 

598 ... use_pti = pti 

599 ... if isinstance(use_pti, Exponential): 

600 ... use_pti = Gamma(1, use_pti.eta) 

601 ... elif isinstance(use_pti, Erlang): 

602 ... use_pti = Gamma(use_pti.k, use_pti.theta) 

603 ... s = str(use_pti) 

604 ... vals = array([pti.sample(g) for _ in range(100000)]) 

605 ... cov = vals.std() / vals.mean() 

606 ... s = f"{i + 1}: {s}, {cov:.2f}; {use_pti.simplify() == pti}" 

607 ... print(s) 

608 1: Gamma(k=3, theta=0.26), 0.58; True 

609 2: Gamma(k=3, theta=0.12), 0.58; True 

610 3: Gamma(k=2, theta=1.33), 0.71; True 

611 4: Gamma(k=1, theta=1.06), 1.00; True 

612 5: Gamma(k=3, theta=0.67), 0.58; True 

613 6: Gamma(k=4, theta=0.35), 0.50; True 

614 7: Gamma(k=3, theta=0.59), 0.58; True 

615 8: Gamma(k=3, theta=0.63), 0.58; True 

616 9: Gamma(k=2, theta=0.59), 0.71; True 

617 10: Gamma(k=3, theta=0.6), 0.58; True 

618 11: Gamma(k=1, theta=1.44), 1.00; True 

619 12: Gamma(k=4, theta=0.29), 0.50; True 

620 13: Gamma(k=3, theta=0.48), 0.58; True 

621 

622 1. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark 

623 Stevenson. Material Flow Control in Make-to-Stock Production Systems: 

624 An Assessment of Order Generation, Order Release and Production 

625 Authorization by Simulation Flexible Services and Manufacturing 

626 Journal. 37(1):1-37. March 2025. 

627 doi: https://doi.org/10.1007/s10696-024-09532-2 

628 """ 

629 return ( 

630 Station(0, Gamma(3, 0.26)), 

631 Station(1, Gamma(3, 0.12)), 

632 Station(2, Gamma(2, 1.33)), 

633 Station(3, Gamma(1, 1.06)), 

634 Station(4, Gamma(3, 0.67)), 

635 Station(5, Gamma(4, 0.35)), 

636 Station(6, Gamma(3, 0.59)), 

637 Station(7, Gamma(3, 0.63)), 

638 Station(8, Gamma(2, 0.59)), 

639 Station(9, Gamma(3, 0.6)), 

640 Station(10, Gamma(1, 1.44)), 

641 Station(11, Gamma(4, 0.29)), 

642 Station(12, Gamma(3, 0.48)))