Coverage for moptipyapps / prodsched / mfc_generator.py: 89%
173 statements
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-30 03:25 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2025-12-30 03:25 +0000
1"""
2Methods for generating MFC instances.
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.
14But where does such instance data come from?
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.
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.
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.
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.
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)
44>>> inst.name
45'mfc_1_2_100_0x7b'
47>>> inst.n_demands
487
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))
66>>> len(inst.station_product_unit_times[0][0])
671600
69>>> len(inst.station_product_unit_times[1][0])
701600
72>>> inst.time_end_measure
73100.0
75>>> inst.time_end_warmup
7630.0
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)'}
99>>> inst = sample_mfc_instance(seed=23445)
100>>> inst.name
101'mfc_10_13_10000_0x5b95'
103>>> inst.n_demands
1049922
106>>> len([dem for dem in inst.demands if dem.product_id == 0])
107959
109>>> len([dem for dem in inst.demands if dem.product_id == 1])
1101055
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]
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
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
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__
152#: the "now" function
153__DTN: Final[Callable[[], datetime.datetime]] = datetime.datetime.now
156#: The information key for interarrival times
157INFO_PRODUCT_INTERARRIVAL_TIME_DIST: Final[str] = \
158 "info_product_interarrival_times"
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"
193@dataclass(order=True, frozen=True)
194class Product:
195 """The product sampling definition."""
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
204 def __init__(self, product_id: int, routing: Iterable[int],
205 interarrival_times: int | float | Distribution) -> None:
206 """
207 Create the product sampling instruction.
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())
226 def log_info(self, infos: dict[str, str]) -> None:
227 """
228 Log the sampling information of this product to the infos `dict`.
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
238@dataclass(order=True, frozen=True)
239class Station:
240 """The station sampling definition."""
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
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.
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())
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())
274 def log_info(self, infos: dict[str, str]) -> None:
275 """
276 Log the sampling information of this product to the infos `dict`.
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)
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.
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 }
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.")
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.")
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}.")
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.")
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)}.")
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)
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)
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)
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}.")
411 random: Final[Generator] = rand_generator(seed)
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
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])
445 # log the information
446 for product in products:
447 product.log_info(infos)
448 for station in stations:
449 station.log_info(infos)
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)
462def __s1t0(s: Iterable[int]) -> tuple[int, ...]:
463 """
464 Convert stations from 1 to 0-based index.
466 :param s: the stations
467 :return: the index
469 >>> __s1t0((1, 2, 3))
470 (0, 1, 2)
471 """
472 return tuple(x - 1 for x in s)
475def default_products() -> tuple[Product, ...]:
476 """
477 Create the default product sequence as used in [1].
479 :return: the default product sequence
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)))
503 1. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark
504 Stevenson. Material Flow Control in Make-to-Stock Production Systems:
505 An Assessment of Order Generation, Order Release and Production
506 Authorization by Simulation Flexible Services and Manufacturing
507 Journal. 37(1):1-37. March 2025.
508 doi: https://doi.org/10.1007/s10696-024-09532-2
509 """
510 return (
511 Product(0, __s1t0((1, 2, 4, 2, 9, 10, 11)),
512 Erlang.from_k_and_mean(3, 10)),
513 Product(1, __s1t0((1, 2, 5, 2, 8, 9, 10, 11)),
514 Erlang.from_k_and_mean(2, 10)),
515 Product(2, __s1t0((1, 2, 6, 4, 2, 9, 12, 11)),
516 Uniform(5, 15)),
517 Product(3, __s1t0((1, 2, 7, 4, 2, 9, 10, 11)),
518 Erlang.from_k_and_mean(3, 10)),
519 Product(4, __s1t0((1, 2, 4, 12, 2, 9, 2, 13)),
520 Erlang.from_k_and_mean(4, 10)),
521 Product(5, __s1t0((1, 2, 5, 12, 2, 9, 7, 13)),
522 Erlang.from_k_and_mean(2, 10)),
523 Product(6, __s1t0((1, 2, 6, 12, 2, 8, 2, 13)),
524 Erlang.from_k_and_mean(4, 10)),
525 Product(7, __s1t0((1, 2, 3, 7, 4, 12, 2, 8, 6, 9, 2, 13)),
526 Uniform(5, 15)),
527 Product(8, __s1t0((1, 2, 3, 5, 4, 6, 12, 2, 8, 2, 10, 6, 13)),
528 Erlang.from_k_and_mean(4, 10)),
529 Product(9, __s1t0((1, 2, 3, 6, 2, 4, 12, 7, 2, 9, 11, 5, 13)),
530 Erlang.from_k_and_mean(2, 10)))
533def default_stations() -> tuple[Station, ...]:
534 """
535 Create the default station sequence as used in [1].
537 :return: the default product station
539 >>> default_stations()
540 (Station(station_id=0, processing_time=Erlang(k=3, theta=0.26),\
541 processing_windows=Const(v=0.125)),\
542 Station(station_id=1, processing_time=Erlang(k=3, theta=0.12),\
543 processing_windows=Const(v=0.125)),\
544 Station(station_id=2, processing_time=Erlang(k=2, theta=1.33),\
545 processing_windows=Const(v=0.125)),\
546 Station(station_id=3,\
547 processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1)),\
548 processing_windows=Const(v=0.125)),\
549 Station(station_id=4, processing_time=Erlang(k=3, theta=0.67),\
550 processing_windows=Const(v=0.125)),\
551 Station(station_id=5, processing_time=Erlang(k=4, theta=0.35),\
552 processing_windows=Const(v=0.125)),\
553 Station(station_id=6, processing_time=Erlang(k=3, theta=0.59),\
554 processing_windows=Const(v=0.125)),\
555 Station(station_id=7, processing_time=Erlang(k=3, theta=0.63),\
556 processing_windows=Const(v=0.125)),\
557 Station(station_id=8, processing_time=Erlang(k=2, theta=0.59),\
558 processing_windows=Const(v=0.125)),\
559 Station(station_id=9, processing_time=Erlang(k=3, theta=0.6),\
560 processing_windows=Const(v=0.125)),\
561 Station(station_id=10,\
562 processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1)),\
563 processing_windows=Const(v=0.125)),\
564 Station(station_id=11, processing_time=Erlang(k=4, theta=0.29),\
565 processing_windows=Const(v=0.125)),\
566 Station(station_id=12, processing_time=Erlang(k=3, theta=0.48),\
567 processing_windows=Const(v=0.125)))
569 1. Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark
570 Stevenson. Material Flow Control in Make-to-Stock Production Systems:
571 An Assessment of Order Generation, Order Release and Production
572 Authorization by Simulation Flexible Services and Manufacturing
573 Journal. 37(1):1-37. March 2025.
574 doi: https://doi.org/10.1007/s10696-024-09532-2
575 """
576 return (
577 Station(0, Gamma(3, 0.26)),
578 Station(1, Gamma(3, 0.12)),
579 Station(2, Gamma(2, 1.33)),
580 Station(3, Gamma(1, 1.06)),
581 Station(4, Gamma(3, 0.67)),
582 Station(5, Gamma(4, 0.35)),
583 Station(6, Gamma(3, 0.59)),
584 Station(7, Gamma(3, 0.63)),
585 Station(8, Gamma(2, 0.59)),
586 Station(9, Gamma(3, 0.6)),
587 Station(10, Gamma(1, 1.44)),
588 Station(11, Gamma(4, 0.29)),
589 Station(12, Gamma(3, 0.48)))