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

1""" 

2Methods for generating MFC instances. 

3 

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. 

6 

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) 

13 

14>>> inst.name 

15'mfc_1_2_100_0x7b' 

16 

17>>> inst.n_demands 

187 

19 

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)) 

35 

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

371600 

38 

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

401600 

41 

42>>> inst.time_end_measure 

43100.0 

44 

45>>> inst.time_end_warmup 

4630.0 

47 

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)'} 

68 

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

70>>> inst.name 

71'mfc_10_13_10000_0x5b95' 

72 

73>>> inst.n_demands 

749922 

75 

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

77959 

78 

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

801055 

81 

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] 

84 

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 

94 

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 

104 

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__ 

121 

122#: the "now" function 

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

124 

125 

126#: The information key for interarrival times 

127INFO_PRODUCT_INTERARRIVAL_TIME_DIST: Final[str] = \ 

128 "info_product_interarrival_times" 

129 

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" 

161 

162 

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

164class Product: 

165 """The product sampling definition.""" 

166 

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 

173 

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

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

176 """ 

177 Create the product sampling instruction. 

178 

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()) 

195 

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

197 """ 

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

199 

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 

206 

207 

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

209class Station: 

210 """The station sampling definition.""" 

211 

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 

218 

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. 

226 

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()) 

237 

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()) 

243 

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

245 """ 

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

247 

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) 

255 

256 

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. 

266 

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 } 

286 

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.") 

293 

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.") 

304 

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}.") 

315 

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.") 

322 

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)}.") 

337 

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) 

346 

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) 

359 

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) 

370 

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}.") 

380 

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

382 

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 

398 

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]) 

414 

415 # log the information 

416 for product in products: 

417 product.log_info(infos) 

418 for station in stations: 

419 station.log_info(infos) 

420 

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) 

430 

431 

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

433 """ 

434 Convert stations from 1 to 0-based index. 

435 

436 :param s: the stations 

437 :return: the index 

438 

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

440 (0, 1, 2) 

441 """ 

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

443 

444 

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

446 """ 

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

448 

449 :return: the default product sequence 

450 

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))) 

472 

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))) 

501 

502 

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

504 """ 

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

506 

507 :return: the default product station 

508 

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))) 

538 

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)))