moptipyapps.prodsched package¶
Examples of production scheduling.
In this package, we provide instance data structures, instance generators, and a simulator for production scheduling problems. The goal is to emulate the works of Thürer et al. [1] and beyond, in a way that can be plugged into optimization procedures.
Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson. Material Flow Control in Make-to-Stock Production Systems: An Assessment of Order Generation, Order Release and Production Authorization by Simulation Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025. doi: https://doi.org/10.1007/s10696-024-09532-2
Subpackages¶
Submodules¶
moptipyapps.prodsched.instance module¶
A production scheduling instance.
Each production scheduling instance has a given name. It represents once concrete scenario of a material flow / factory production scenario. The factory has n_stations work stations, e.g., machines that perform a certain production step. The factory also produces a set of n_products different products. Each product passes through a set of work stations in a certain, pre-defined order (and may even pass through the same machine multiple times). The route each product takes is defined by the routes matrix. Each product unit requires a certain time at each work station. These times follow a certain random distribution.
Ther are customer demands (Demand) that appear in the system at certain arrival times. The demand is not known to the system before its arrival time, so we cannot really anticipate it. However, when it arrives at the arrival time, it has a certain deadline by which it should be completed. Normally, this is the same as the arrival time, but it could also be later in some scenarios. Each demand has a unique demand_id and is issued by a certain customer with a certain customer_id. In many MFC scenarios, the customers do not matter. What always matters is the product_id, though, which identifies the product that the customer ordered, as well as the amount of that product that the customer ordered (which is normally 1, but could be an arbitary positive integer).
To summarize so far: We have a factory that can produce n_products different products, which have IDs from 0 to n_products-1. The factory uses n_stations work stations (with IDs from 0 to n_stations-1) for that purpose. The routes matrix defines which product is processed by which work station and in which order. The product with ID i passes through the work stations routes[i], which is tuple of work station IDs.
The n_customers customers issue n_demands demands. Each demand has a unique ID demand_id and appears in the system at the arrival time. It is for amount units of the product with ID product_id. It should be satisfied until deadline.
So the factory is producing the products with the goal to satisfy the demands as soon as possible. This leaves the last piece of the puzzle: How long does it take to manufacture a unit of a given product? This is regulated by the three-dimensional matrix station_product_unit_times. If we want to produce one unit of a product with a given ID p, we can use the routes matrix (routes[p]) to determine through which machines this unit needs to pass. If it arrives at a machine with ID m, then we look up the ndarray of prodcution slots and times under station_product_unit_times[m][p].
This array stores (flat) pairs of time window ends and unit production times. The function compute_finish_time() then tells us when the unit of the production would be finished on this machine. It would pass to the next machine prescribed in routes[p] until it has passed all machines and is finished.
Notice that all the elements of an Instance are deterministic. The demands come in at specific, fixed times and are for fixed amounts of fixed products. They are stored in demands. Of course, any realistic simulation would only get to see them at their arrival times, but they are from a deterministic sequence nonetheless. Also, the production times are fixed and stored in the 3D array station_product_unit_times.
Even the initial amount warehous_at_t0 of products that is available in the warehouse at time step 0 is fixed. Each instance also prescribes a fixed warm-up time time_end_warmup (that must be ignored during the performance metric computation) and end time time_end_measure for the simulation.
Everything is specified, deterministic, and fixed.
Of course, that being said, you can still generate instances randomly. Indeed, in mfc_generator, we do exactly that. So you get the best of both worlds: You can generate many different instances where work times and demands follow the distributions of your liking. And you can run fully-reproducible simulations (using the base class Simulation).
Because if all events and times that would normally be “random” are hard-coded in an Instance, then two simulations with the same “factory operating system” will yield the exactly same behavior.
Instances can be converted to a text stream that you can store in a text file by using the function to_stream(). You can load them from a text stream using function from_stream(). If you want to store multiple instances in a directory as text files, you can use store_instances(). To load a set of instances from a directory, you can use load_instances(). The class Simulation in module simulation offers the ability to run a fully reproducible simulation based on an Instance and to pipe out events and data via a Listener interface.
>>> name = "my_instance"
The number of products be 3.
>>> n_products = 3
The number of customers be 5.
>>> n_customers = 5
The number of stations be 4.
>>> n_stations = 4
There will be 6 customer demands.
>>> n_demands = 6
The end of the warmup period.
>>> time_end_warmup = 10
The end of the measurement period.
>>> time_end_measure = 10000
Each product may take a different route through different stations.
>>> route_p0 = [0, 3, 2]
>>> route_p1 = [0, 2, 1, 3]
>>> route_p2 = [1, 2, 3]
>>> routes = [route_p0, route_p1, route_p2]
Each demand is a tuple of demand_id, customer_id, product_id, amount, release time, and deadline.
>>> d0 = [0, 0, 1, 20, 1240, 3000]
>>> d1 = [1, 1, 0, 10, 2300, 4000]
>>> d2 = [2, 2, 2, 7, 8300, 11000]
>>> d3 = [3, 3, 1, 12, 7300, 9000]
>>> d4 = [4, 4, 2, 23, 5410, 16720]
>>> d5 = [5, 3, 0, 19, 4234, 27080]
>>> demands = [d0, d1, d2, d3, d4, d5]
There is a fixed amount of each product in the warehouse at time step 0.
>>> warehous_at_t0 = [10, 0, 6]
Each station requires a certain working time for each unit of each product. This production time may vary over time. For example, maybe station 0 needs 10 time units for 1 unit of product 0 from time step 0 to time step 19, then 11 time units from time step 20 to 39, then 8 time units from time step 40 to 59. These times are cyclic, meaning that at time step 60 to 79, it will again need 10 time units, and so on. Of course, production times are only specified for stations that a product is actually routed through.
>>> m0_p0 = [10.0, 20.0, 11.0, 40.0, 8.0, 60.0]
>>> m0_p1 = [12.0, 20.0, 7.0, 40.0, 11.0, 70.0]
>>> m0_p2 = []
>>> m1_p0 = []
>>> m1_p1 = [20.0, 50.0, 30.0, 120.0, 7.0, 200.0]
>>> m1_p2 = [21.0, 50.0, 29.0, 130.0, 8.0, 190.0]
>>> m2_p0 = [ 8.0, 20.0, 9.0, 60.0]
>>> m2_p1 = [10.0, 90.0]
>>> m2_p2 = [12.0, 70.0, 30.0, 120.0]
>>> m3_p0 = [70.0, 200.0, 3.0, 220.0]
>>> m3_p1 = [60.0, 220.0, 5.0, 260.0]
>>> m3_p2 = [30.0, 210.0, 10.0, 300.0]
>>> station_product_unit_times = [[m0_p0, m0_p1, m0_p2],
... [m1_p0, m1_p1, m1_p2],
... [m2_p0, m2_p1, m2_p2],
... [m3_p0, m3_p1, m3_p2]]
We can (but do not need to) provide additional information as key-value pairs.
>>> infos = {"source": "manually created",
... "creation_date": "2025-11-09"}
From all of this data, we can create the instance.
>>> instance = Instance(name, n_products, n_customers, n_stations, n_demands,
... time_end_warmup, time_end_measure,
... routes, demands, warehous_at_t0,
... station_product_unit_times, infos)
>>> instance.name
'my_instance'
>>> instance.n_customers
5
>>> instance.n_stations
4
>>> instance.n_demands
6
>>> instance.n_products
3
>>> instance.routes
((0, 3, 2), (0, 2, 1, 3), (1, 2, 3))
>>> instance.time_end_warmup
10.0
>>> instance.time_end_measure
10000.0
>>> instance.demands
(Demand(arrival=1240.0, deadline=3000.0, demand_id=0, customer_id=0, product_id=1, amount=20, measure=True), Demand(arrival=2300.0, deadline=4000.0, demand_id=1, customer_id=1, product_id=0, amount=10, measure=True), Demand(arrival=4234.0, deadline=27080.0, demand_id=5, customer_id=3, product_id=0, amount=19, measure=True), Demand(arrival=5410.0, deadline=16720.0, demand_id=4, customer_id=4, product_id=2, amount=23, measure=True), Demand(arrival=7300.0, deadline=9000.0, demand_id=3, customer_id=3, product_id=1, amount=12, measure=True), Demand(arrival=8300.0, deadline=11000.0, demand_id=2, customer_id=2, product_id=2, amount=7, measure=True))
>>> instance.warehous_at_t0
(10, 0, 6)
>>> instance.station_product_unit_times
((array([10., 20., 11., 40., 8., 60.]), array([12., 20., 7., 40., 11., 70.]), array([], dtype=float64)), (array([], dtype=float64), array([ 20., 50., 30., 120., 7., 200.]), array([ 21., 50., 29., 130., 8., 190.])), (array([ 8., 20., 9., 60.]), array([10., 90.]), array([ 12., 70., 30., 120.])), (array([ 70., 200., 3., 220.]), array([ 60., 220., 5., 260.]), array([ 30., 210., 10., 300.])))
>>> instance.n_measurable_demands
6
>>> instance.n_measurable_demands_per_product
(2, 2, 2)
>>> dict(instance.infos)
{'source': 'manually created', 'creation_date': '2025-11-09'}
We can serialize instances to a stream of strings and also load them back from a stream of strings. Here, we store instance to a stream. We then load the independent instance i2 from that stream.
>>> i2 = from_stream(to_stream(instance))
>>> i2 is instance
False
>>> i2 == instance
True
You can see that the loaded instance has the same data as the stored one.
>>> i2.name == instance.name
True
>>> i2.n_customers == instance.n_customers
True
>>> i2.n_stations == instance.n_stations
True
>>> i2.n_demands == instance.n_demands
True
>>> i2.n_products == instance.n_products
True
>>> i2.routes == instance.routes
True
>>> i2.demands == instance.demands
True
>>> i2.time_end_warmup == instance.time_end_warmup
True
>>> i2.time_end_measure == instance.time_end_measure
True
>>> i2.warehous_at_t0 == instance.warehous_at_t0
True
>>> eq: bool = True
>>> for i in range(i2.n_stations):
... ma1 = i2.station_product_unit_times[i]
... ma2 = instance.station_product_unit_times[i]
... for j in range(i2.n_products):
... pr1 = ma1[j]
... pr2 = ma2[j]
... if not np.array_equal(pr1, pr2):
... eq = False
>>> eq
True
True >>> i2.infos == instance.infos True
- class moptipyapps.prodsched.instance.Demand(arrival, deadline, demand_id, customer_id, product_id, amount, measure)[source]¶
-
The record for demands.
Each demand has an
arrivaltime at which point it enters the system. It has a uniquedemand_id. It has adeadline, at which point the customercustomer_idwho issued the demand expects to receive theamountunits of the productproduct_idthat they ordered.Demands with the
measureflag set fall into the simulation time window where performance metrics are gathered. Demands wheremeasureis False will be irgnored during the performance evaluation, because they fall into the setup time. Their processing must still be simulated, though.>>> Demand(arrival=0.6, deadline=0.8, demand_id=1, ... customer_id=2, product_id=6, amount=12, measure=True) Demand(arrival=0.6, deadline=0.8, demand_id=1, customer_id=2, product_id=6, amount=12, measure=True) >>> Demand(arrival=16, deadline=28, demand_id=1, ... customer_id=2, product_id=6, amount=12, measure=False) Demand(arrival=16.0, deadline=28.0, demand_id=1, customer_id=2, product_id=6, amount=12, measure=False)
- class moptipyapps.prodsched.instance.Instance(name, n_products, n_customers, n_stations, n_demands, time_end_warmup, time_end_measure, routes, demands, warehous_at_t0, station_product_unit_times, infos=None)[source]¶
Bases:
ComponentAn instance of the Production Scheduling Problem.
- demands:
Final[tuple[Demand,...]]¶ The demands: Each demand stores the
demand_id,customer_id,product_id,amount,arrivaltime, anddeadline, as well as whether it should be measured during the simulation (measure). The customer makes their order at time steparrival. They expect to receive their product by thedeadline. The demands are sorted byarrivaland thendeadline. The release time is always > 0. Thearrivalis always >=deadline. Demand ids are unique.
- infos:
Final[Mapping[str,str]]¶ Additional information about the nature of the instance can be stored here. This has no impact on the behavior of the instance, but it may explain, e.g., settings of an instance generator. The module
mfc_generatorwhich is used to randomly generate instances, for example, makes use of this data to store the random number seed as well as the distributions that were used to create the instances.
- n_measurable_demands:
Final[int]¶ the number of demands that actually fall into the time measured window
- n_measurable_demands_per_product:
Final[tuple[int,...]]¶ the measurable demands on a per-product basis
- routes:
Final[tuple[tuple[int,...],...]]¶ the product routes, i.e., the stations through which each product must pass
- station_product_unit_times:
Final[tuple[tuple[ndarray,...],...]]¶ The per-station unit production times for each product. Each station can have different production times per product. Let’s say that this is tuple A. For each product, it has a tuple B at the index of the product id. If the product does not pass through the station, B is empty. Otherwise, it holds one or multiple tuples C. Each tuple C consists of two numbers: A per-unit-production time for the product. An end time index for this production time. Once the real time surpasses the end time of the last of these production specs, the production specs are recycled and begin again.
- demands:
- moptipyapps.prodsched.instance.KEY_IN_WAREHOUSE: Final[str] = 'products_in_warehouse_at_t0'¶
The amount of products in the warehouse at time step 0.
- moptipyapps.prodsched.instance.KEY_N_CUSTOMERS: Final[str] = 'n_customers'¶
the key for the number of customers
- moptipyapps.prodsched.instance.KEY_N_DEMANDS: Final[str] = 'n_demands'¶
the number of demands in the scenario
- moptipyapps.prodsched.instance.KEY_N_PRODUCTS: Final[str] = 'n_products'¶
the key for the number of products
- moptipyapps.prodsched.instance.KEY_N_STATIONS: Final[str] = 'n_stations'¶
the key for the number of stations
- moptipyapps.prodsched.instance.KEY_PRODUCTION_TIME: Final[str] = 'production_time'¶
the first part of the production time
- moptipyapps.prodsched.instance.KEY_ROUTE: Final[str] = 'product_route'¶
the first part of the product route key
- moptipyapps.prodsched.instance.KEY_TIME_END_MEASURE: Final[str] = 'time_end_measure'¶
the end of the measure period
- moptipyapps.prodsched.instance.KEY_TIME_END_WARMUP: Final[str] = 'time_end_warmup'¶
the end of the warmup period
- moptipyapps.prodsched.instance.MAX_ID: Final[int] = 1000000000¶
The maximum for the number of stations, products, or customers.
- moptipyapps.prodsched.instance.MAX_VALUE: Final[int] = 2147483647¶
No value bigger than this is permitted in any tuple anywhere.
- moptipyapps.prodsched.instance.compute_finish_time(start_time, amount, production_times)[source]¶
Compute the time when one job is finished.
The production times are cyclic intervals of unit production times and interval ends.
- Parameters:
- Return type:
- Returns:
the end time
Here, the production time is 10 time units / 1 product unit, valid until end time 100.
>>> compute_finish_time(0.0, 1, np.array((10.0, 100.0), np.float64)) 10.0
Here, the production time is 10 time units / 1 product unit, valid until end time 100. We begin producing at time unit 250. Since the production periods are cyclic, this is OK: we would be halfway through the third production period when the request comes in. It will consume 10 time units and be done at time unit 260.
>>> compute_finish_time(250.0, 1, np.array((10.0, 100.0))) 260.0
Here, the end time of the production time validity is at time unit 100. However, we begin producing 1 product unit at time step 90. This unit will use 10 time units, meaning that its production is exactly finished when the production time validity ends. It will be finished at time step 100.
>>> compute_finish_time(90.0, 1, np.array((10.0, 100.0))) 100.0
Here, the end time of the production time validity is at time unit 100. However, we begin producing 1 product unit at time step 95. This unit would use 10 time units. It will use these units, even though this extends beyond the end of the production time window.
>>> compute_finish_time(95.0, 1, np.array((10.0, 100.0))) 105.0
Now we have two production periods. The production begins again at time step 95. It will use 10 time units, even though this extends into the second period.
>>> compute_finish_time(95.0, 1, np.array((10.0, 100.0, 20.0, 200.0))) 105.0
Now things get more complex. We want to do 10 units of product. We start in the first period, so one unit will be completed there. This takes the starting time for the next job to 105, which is in the second period. Here, one unit of product takes 20 time units. We can finish producing one unit until time 125 and start the production of a second one, taking until 145. Now the remaining three units are produced until time 495 >>> compute_finish_time(95.0, 10, np.array(( … 10.0, 100.0, 20.0, 140.0, 50.0, 5000.0))) 495.0 >>> 95 + (1*10 + 2*20 + 7*50) 495
We again produce 10 product units starting at time step 95. The first one takes 10 time units, taking us into the second production interval at time 105. Then we can again do two units here, which consume 40 time units, taking us over the edge into the third interval at time unit 145. Here we do two units using 50 time units. We ahen are at time 245, which wraps back to 45. So the remaining 5 units take 10 time units each.
>>> compute_finish_time(95.0, 10, np.array(( ... 10.0, 100.0, 20.0, 140.0, 50.0, 200.0))) 295.0 >>> 95 + (1*10 + 2*20 + 2*50 + 5*10) 295
This is the same as the last example, but this time, the last interval (3 time units until 207) is skipped over by the long production of the second 50-time-unit product.
>>> compute_finish_time(95.0, 10, np.array(( ... 10.0, 100.0, 20.0, 140.0, 50.0, 200.0, 3.0, 207.0))) 295.0 >>> 95 + (1*10 + 2*20 + 2*50 + 5*10) 295
Production unit times may extend beyond the intervals.
>>> compute_finish_time(0.0, 5, np.array((1000.0, 100.0, 10.0, 110.0))) 5000.0 >>> 5 * 1000
- moptipyapps.prodsched.instance.load_instances(source, file_filter=<function <lambda>>)[source]¶
Load the instances from a given irectory.
This function iterates over the files in a directory and applies
from_stream()to each text file (ends with .txt) that it finds and that is accepted by the filter file_filter. (The default file filter always returns True, i.e., accepts all files.)- Parameters:
source (
str) – the source directoryfile_filter (
Callable[[Path],bool], default:<function <lambda> at 0x7f80627f59e0>) – a filter for files. Here you can provide a function or lambda that returns True if a file should be loaded and False otherwise. Notice that only .txt files are considered either way.
- Return type:
- Returns:
the tuple of instances
>>> inst1 = Instance( ... name="test1", n_products=1, n_customers=1, n_stations=2, ... n_demands=1, time_end_warmup=10, time_end_measure=4000, ... routes=[[0, 1]], ... demands=[[0, 0, 0, 10, 20, 100]], ... warehous_at_t0=[0], ... station_product_unit_times=[[[10.0, 10000.0]], ... [[30.0, 10000.0]]])
>>> inst2 = Instance( ... name="test2", n_products=2, n_customers=1, n_stations=2, ... n_demands=3, time_end_warmup=21, time_end_measure=10000, ... routes=[[0, 1], [1, 0]], ... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200], ... [2, 0, 1, 7, 30, 200]], ... warehous_at_t0=[2, 1], ... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0], ... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]], ... [[ 5.0, 24.0, 7.0, 80.0], ... [ 3.0, 21.0, 6.0, 50.0,]]])
>>> from pycommons.io.temp import temp_dir >>> with temp_dir() as td: ... store_instances(td, [inst2, inst1]) ... res = load_instances(td) >>> res == (inst1, inst2) True
moptipyapps.prodsched.instances module¶
Generate instances for training and testing.
In this package, we provide a function for generating instances, i.e., objects of type Instance, in a deterministic fashion for training and testing of MFC scenarios.
The function get_instances() will return a fixed set of instances for a given instance number. It allows you to store and retrieve compatible instance sets of different sizes from a given directory.
This is necessary when doing repeatable experiments that average performance metrics over multiple Instance objects. We do not just want to be able to generate the instances, but we also need to store them and to re-load them. Storing them is easy, function store_instances() can do it. Loading instances is easy, too, because for that we have function load_instances().
However, what do you do if you generated 10 instances in a deterministic fashion, but for your next experiment you only want to use five of them? How do you decide which to use? Or what if you want to use 15 now. How do you make sure that the previous ten instances are part of the set of 15 instances? get_instances() does all of that for you. It creates the random seeds for the instance creation in the good old deterministic “moptipy” style, using rand_seeds_from_str(). It then checks the instance directory for instances to use that comply with the seeds and generates (and stores) additional instances if need be. For this, we use the Thürer-style instance synthesis implemented as sample_mfc_instance(). Thus, we have a consistent way of generating, storing, and loading instances in a transparent way.
(The implementation is not overly efficient, but it will do.)
>>> from pycommons.io.temp import temp_dir
>>> with temp_dir() as td:
... inst_1 = get_instances(3, td)
... inst_2 = get_instances(1, td)
... inst_3 = get_instances(5, td)
>>> len(inst_1)
3
>>> len(inst_2)
1
>>> len(inst_3)
5
>>> all(ix in inst_1 for ix in inst_2)
True
>>> all(ix in inst_3 for ix in inst_1)
True
moptipyapps.prodsched.mfc_generator module¶
Methods for generating MFC instances.
In the module instance, we provide the class Instance. Objects of this class represent a fully deterministic production scheduling scenario. They prescribe demands (Demand) arriving at fixed points in time in the system and work stations that need fixed amounts of work times per product during certain time periods. This allows us to create fully reproducible simulations (simulation) that show what a factory would do to satisfy the demands.
But where does such instance data come from?
In the existing research on material flow control, no such fixed instances exist. We invented them. Instead, the existing research [1] uses fixed numbers of products and machines, fixed routes of products through machines, and random distributions to generate demands and work times.
So we create the function default_stations() that creates the standard work time distributions for the standard work stations. We also create the function default_products() that creates the default distributions for the default products.
The function sample_mfc_instance() then creates a material flow instance following these distributions based on a given random seed. This allows us to create scenarios that follow the same structure and random distributions as prescribed in the paper [1] by Thürer et al. However, our instances are fully deterministic.
Once could not create a certain number of such instances and average performance metrics over simulations on them. This would likely yield metrics of reasonable accuracy, while allowing us to reproduce, analyze, and trace every single production decision if need be.
>>> from moptipyapps.utils.sampling import Gamma
>>> inst = sample_mfc_instance([
... Product(0, (0, 1), Gamma.from_alpha_beta(3, 0.26))], [
... Station(0, Gamma.from_k_and_mean(3, 10)),
... Station(1, Gamma.from_k_and_mean(2, 10))],
... time_end_measure=100, seed=123)
>>> inst.name
'mfc_1_2_100_0x7b'
>>> inst.n_demands
7
>>> inst.demands
(Demand(arrival=5.213885878801001, deadline=5.213885878801001, demand_id=0, customer_id=0, product_id=0, amount=1, measure=False), Demand(arrival=25.872387132411287, deadline=25.872387132411287, demand_id=1, customer_id=1, product_id=0, amount=1, measure=False), Demand(arrival=43.062182155666896, deadline=43.062182155666896, demand_id=2, customer_id=2, product_id=0, amount=1, measure=True), Demand(arrival=49.817978344678004, deadline=49.817978344678004, demand_id=3, customer_id=3, product_id=0, amount=1, measure=True), Demand(arrival=58.21166922638016, deadline=58.21166922638016, demand_id=4, customer_id=4, product_id=0, amount=1, measure=True), Demand(arrival=69.09054693162531, deadline=69.09054693162531, demand_id=5, customer_id=5, product_id=0, amount=1, measure=True), Demand(arrival=88.804579148131, deadline=88.804579148131, demand_id=6, customer_id=6, product_id=0, amount=1, measure=True))
>>> len(inst.station_product_unit_times[0][0])
1600
>>> len(inst.station_product_unit_times[1][0])
1600
>>> inst.time_end_measure
100.0
>>> inst.time_end_warmup
30.0
>>> d = dict(inst.infos)
>>> del d["info_generated_on"]
>>> del d["info_generator_version"]
>>> d
{'info_generator': 'moptipyapps.prodsched.mfc_generator', 'info_rand_seed_src': 'USER_PROVIDED', 'info_rand_seed': '0x7b', 'info_time_end_measure_src': 'USER_PROVIDED', 'info_time_end_measure': '100', 'info_time_end_warmup_src': 'SAMPLED', 'info_time_end_warmup': '30', 'info_name_src': 'SAMPLED', 'info_product_interarrival_times[0]': 'Erlang(k=3, theta=3.846153846153846)', 'info_product_route[0]': 'USER_PROVIDED', 'info_station_processing_time[0]': 'Erlang(k=3, theta=3.3333333333333335)', 'info_station_processing_time_window_length[0]': 'Const(v=0.125)', 'info_station_processing_time[1]': 'Erlang(k=2, theta=5)', 'info_station_processing_time_window_length[1]': 'Const(v=0.125)'}
>>> inst = sample_mfc_instance(seed=23445)
>>> inst.name
'mfc_10_13_10000_0x5b95'
>>> inst.n_demands
9922
>>> len([dem for dem in inst.demands if dem.product_id == 0])
959
>>> len([dem for dem in inst.demands if dem.product_id == 1])
1055
>>> [len(k[0]) for k in inst.station_product_unit_times]
[160000, 160000, 0, 160000, 0, 0, 0, 0, 160000, 160000, 160000, 0, 0]
Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson. Material Flow Control in Make-to-Stock Production Systems: An Assessment of Order Generation, Order Release and Production Authorization by Simulation Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025. doi:<https://doi.org/10.1007/s10696-024-09532-2>
- moptipyapps.prodsched.mfc_generator.INFO_GENERATED_ON: Final[str] = 'info_generated_on'¶
When was the instance generated?
- moptipyapps.prodsched.mfc_generator.INFO_GENERATOR_VERSION: Final[str] = 'info_generator_version'¶
The generator version
- moptipyapps.prodsched.mfc_generator.INFO_PRODUCT_INTERARRIVAL_TIME_DIST: Final[str] = 'info_product_interarrival_times'¶
The information key for interarrival times
- moptipyapps.prodsched.mfc_generator.INFO_PRODUCT_ROUTE: Final[str] = 'info_product_route'¶
The information key for interarrival times
- moptipyapps.prodsched.mfc_generator.INFO_RAND_SEED_SRC: Final[str] = 'info_rand_seed_src'¶
the random seed source
- moptipyapps.prodsched.mfc_generator.INFO_STATION_PROCESSING_TIME: Final[str] = 'info_station_processing_time'¶
The information key for the processing time distribution
- moptipyapps.prodsched.mfc_generator.INFO_STATION_PROCESSING_WINDOW_LENGTH: Final[str] = 'info_station_processing_time_window_length'¶
The information key for the processing time window length distribution
- moptipyapps.prodsched.mfc_generator.INFO_TIME_END_MEASURE: Final[str] = 'info_time_end_measure'¶
the measurement time end
- moptipyapps.prodsched.mfc_generator.INFO_TIME_END_MEASURE_SRC: Final[str] = 'info_time_end_measure_src'¶
the source of the measurement time end
- moptipyapps.prodsched.mfc_generator.INFO_TIME_END_WARMUP: Final[str] = 'info_time_end_warmup'¶
the warmup time end
- moptipyapps.prodsched.mfc_generator.INFO_TIME_END_WARMUP_SRC: Final[str] = 'info_time_end_warmup_src'¶
the source of the warmup time end
- moptipyapps.prodsched.mfc_generator.INFO_USER_PROVIDED: Final[str] = 'USER_PROVIDED'¶
a fixed structure
- class moptipyapps.prodsched.mfc_generator.Product(product_id, routing, interarrival_times)[source]¶
Bases:
objectThe product sampling definition.
- interarrival_times:
Distribution¶ the interarrival distribution
- interarrival_times:
- class moptipyapps.prodsched.mfc_generator.Station(station_id, processing_time, processing_windows=None)[source]¶
Bases:
objectThe station sampling definition.
- processing_time:
Distribution¶ the processing time distribution
- processing_windows:
Distribution¶ the processing window distribution
- processing_time:
- moptipyapps.prodsched.mfc_generator.default_products()[source]¶
Create the default product sequence as used in [1].
>>> default_products() (Product(product_id=0, routing=(0, 1, 3, 1, 8, 9, 10), interarrival_times=Erlang(k=3, theta=3.3333333333333335)), Product(product_id=1, routing=(0, 1, 4, 1, 7, 8, 9, 10), interarrival_times=Erlang(k=2, theta=5)), Product(product_id=2, routing=(0, 1, 5, 3, 1, 8, 11, 10), interarrival_times=Uniform(low=5, high=15)), Product(product_id=3, routing=(0, 1, 6, 3, 1, 8, 9, 10), interarrival_times=Erlang(k=3, theta=3.3333333333333335)), Product(product_id=4, routing=(0, 1, 3, 11, 1, 8, 1, 12), interarrival_times=Erlang(k=4, theta=2.5)), Product(product_id=5, routing=(0, 1, 4, 11, 1, 8, 6, 12), interarrival_times=Erlang(k=2, theta=5)), Product(product_id=6, routing=(0, 1, 5, 11, 1, 7, 1, 12), interarrival_times=Erlang(k=4, theta=2.5)), Product(product_id=7, routing=(0, 1, 2, 6, 3, 11, 1, 7, 5, 8, 1, 12), interarrival_times=Uniform(low=5, high=15)), Product(product_id=8, routing=(0, 1, 2, 4, 3, 5, 11, 1, 7, 1, 9, 5, 12), interarrival_times=Erlang(k=4, theta=2.5)), Product(product_id=9, routing=(0, 1, 2, 5, 1, 3, 11, 6, 1, 8, 10, 4, 12), interarrival_times=Erlang(k=2, theta=5)))
Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson. Material Flow Control in Make-to-Stock Production Systems: An Assessment of Order Generation, Order Release and Production Authorization by Simulation Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025. doi: https://doi.org/10.1007/s10696-024-09532-2
- moptipyapps.prodsched.mfc_generator.default_stations()[source]¶
Create the default station sequence as used in [1].
>>> default_stations() (Station(station_id=0, processing_time=Erlang(k=3, theta=0.26), processing_windows=Const(v=0.125)), Station(station_id=1, processing_time=Erlang(k=3, theta=0.12), processing_windows=Const(v=0.125)), Station(station_id=2, processing_time=Erlang(k=2, theta=1.33), processing_windows=Const(v=0.125)), Station(station_id=3, processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1)), processing_windows=Const(v=0.125)), Station(station_id=4, processing_time=Erlang(k=3, theta=0.67), processing_windows=Const(v=0.125)), Station(station_id=5, processing_time=Erlang(k=4, theta=0.35), processing_windows=Const(v=0.125)), Station(station_id=6, processing_time=Erlang(k=3, theta=0.59), processing_windows=Const(v=0.125)), Station(station_id=7, processing_time=Erlang(k=3, theta=0.63), processing_windows=Const(v=0.125)), Station(station_id=8, processing_time=Erlang(k=2, theta=0.59), processing_windows=Const(v=0.125)), Station(station_id=9, processing_time=Erlang(k=3, theta=0.6), processing_windows=Const(v=0.125)), Station(station_id=10, processing_time=AtLeast(lb=5e-324, d=Exponential(eta=1)), processing_windows=Const(v=0.125)), Station(station_id=11, processing_time=Erlang(k=4, theta=0.29), processing_windows=Const(v=0.125)), Station(station_id=12, processing_time=Erlang(k=3, theta=0.48), processing_windows=Const(v=0.125)))
Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson. Material Flow Control in Make-to-Stock Production Systems: An Assessment of Order Generation, Order Release and Production Authorization by Simulation Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025. doi: https://doi.org/10.1007/s10696-024-09532-2
- moptipyapps.prodsched.mfc_generator.sample_mfc_instance(products=None, stations=None, time_end_warmup=None, time_end_measure=None, name=None, seed=None)[source]¶
Sample an MFC instance.
- Parameters:
products (
Optional[Iterable[Product]], default:None) – the productsstations (
Optional[Iterable[Station]], default:None) – the work stationstime_end_warmup (
int|float|None, default:None) – the end of the warmup periodtime_end_measure (
int|float|None, default:None) – the end of the measurement period
- Return type:
- Returns:
the instance
moptipyapps.prodsched.multistatistics module¶
A statistics record for multiple simulations.
MultiStatistics are records that hold multiple simulation Statistics, each of which computed over a separate Simulation based on a separate instance of the material flow problem. These records are filled with data by via the moptipyapps.prodsched.rop_multisimulation.ROPMultiSimulation mechanism, which performs the multiple simulations.
We cn use this record as the solution space when optimizing for the MFC scenario. Such a record holds comprehensive statistics across several simulation runs. This makes it suitable as source of data for objective functions (Objective). The objective functions can then access these statistics.
Since we use MultiStatistics as solution space, we also need an implementation of moptipy’s Space-API to plug it into the optimization process. Sucha space implementation is provided as class MultiStatisticsSpace. It can create, copy, and serialize these objects to text, so that they can appear in the log files.
- class moptipyapps.prodsched.multistatistics.MultiStatistics(instances, names=None)[source]¶
Bases:
objectA set of statistics gathered over multiple instances.
- per_instance:
tuple[Statistics,...]¶ the per-instance statistics
- per_instance:
- class moptipyapps.prodsched.multistatistics.MultiStatisticsSpace(instances)[source]¶
Bases:
SpaceAn implementation of the Space API of for multiple statistics.
- copy(dest, source)[source]¶
Copy one multi-statistics to another one.
- Parameters:
dest (
MultiStatistics) – the destination multi-statisticssource (
MultiStatistics) – the source multi-statistics
- Return type:
- create()[source]¶
Create an empty multi-statistics record.
- Return type:
- Returns:
the empty multi-statistics record
- from_str(text)[source]¶
Convert a string to a multi-statistics.
- Parameters:
text (
str) – the string- Return type:
- Returns:
the multi-statistics
- is_equal(x1, x2)[source]¶
Check if two multi-statistics have the same contents.
- Parameters:
x1 (
MultiStatistics) – the first multi-statisticsx2 (
MultiStatistics) – the second multi-statistics
- Return type:
- Returns:
True if both multi-statistics have the same content
- log_parameters_to(logger)[source]¶
Log the parameters of the space to the given logger.
- Parameters:
logger (
KeyValueLogSection) – the logger for the parameters- Return type:
- n_points()[source]¶
Get the number of possible multi-statistics.
- Return type:
- Returns:
just some arbitrary very large number
- to_str(x)[source]¶
Convert a multi-statistics to a string.
- Parameters:
x (
MultiStatistics) – the packing- Return type:
- Returns:
a string corresponding to the multi-statistics
- validate(x)[source]¶
Check if a multi-statistics is valid.
- Parameters:
x (
MultiStatistics) – the multi-statistics- Raises:
TypeError – if any component of the multi-statistics is of the wrong type
ValueError – if the multi-statistics is not feasible
- Return type:
moptipyapps.prodsched.rop_experiment module¶
A small template for ROP-based experiments.
This experiment uses the NSGA-II algorithm to optimize Re-Order-Points (ROPs) to achieve both a high worst-case fillrate and a low worst-case average stock level.
It is just a preliminary idea.
In manufacturing systems, the concept of re-order points (ROPs) can be used to schedule production. The ROP basically provides one limit X per product. If there are less or equal (<=) X units of the product in the warehouse, a production order is issued so that one new unit is produced.
The question is how should X be set so that we can
satisfy as many customer demands as possible directly when they come in, i.e., maximize the fill rate (
immediate_rates, represented byworst_and_mean_fill_rate) andhave a low total average number of product units sitting around in our warehouse, i.e., minimize the stock level (
stock_levels, represented bymax_stocklevel).
Since we have n=10 products in the Thürer [1] base scenario, we also have 10 such ROP values X.
Now how can we know the fill rate and the stock level? This is done by simulating the whole system in action. We therefore use instances generated using the Thürer-style distributions as defined in mfc_generator [1].
Our simulations (see simulation) are not randomized. Instead, each of them is based on a fixed instance. Each Instance defines exactly when a customer Demand comes in and, for many time windows, the production time needed by a machine to produce one unit of product. Of course, all of these values follow the random distributions (Erlang, Gamma, with respective parameters) given in Tables 2 and 3 of the original Thürer paper [1] and implemented in mfc_generator. But apart from this, they are fixed per instance.
This means that we can take the same ROP and run the simulation twice for a given instance and will get exactly the same results and statistics (fill rates, stock levels, etc.).
Of course, using a single fixed instance may be misleading. Maybe we would think that a certain ROP is very good … but it is only good on the specific instance we tried.
So as a second step, we here generate 11 instances (via get_instances()). And then we look at the worst fill rate and the worst stock level over all 11 instances (more or less). And we use that to judge whether an ROP is good or not.
This leaves the question: Where do these ROPs come from?
They come from an optimization process. First, we define that ROPs be integer vectors (i.e., from an IntSpace) where each element comes from the range 0..63. We sample the initial solutions randomly from that interval (via Op0Random).
As unary search operator, we take an existing ROP and, for a number of elements, sample a new value normally distributed around it (with standard deviation 2.5) but rounded to integer. We do this for a binomially distributed number of elements, exactly like the bit-string based (1+1) EA would do it, but implemented for integers by operator Op1MNormal.
As binary operator, we use uniform crossover, given as Op2Uniform. This operator takes two existing solutions and creates a new solution by element-wise copying elements of the parents. For each element, it randomly decides from which parent solution it should be copied.
As optimization algorithm, we use NSGA-II implemented by class NSGA2. This is a multi-objective optimization algorithm. We do this because we have two goals:
Maximize the worst-case fill rates,
Minimize the worst-case average stock level.
Regarding the first objective, we have a small tweak: Assume that, over all 11 instances, PM be the worst fill rate per product (in [0,1], 0 being worst) and AM be the worst average fill rate over all products (0 worst, 1 best). Then our objective value – subject to minimization – is (1 - PM) * 100 + (1 - AM). This is implemented in module worst_and_mean_fill_rate.
The objective function minimizing the stock level is implemented in module max_stocklevel.
In summary, what we do is this:
The optimization algorithm proposes ROPs, each of which being an integer vector with 10 values (1 value per product). (
IntSpace) These integer vectors are the elements of the search space.The ROP is evaluated by simulating it 11 times (using 10000 time units per instance, 3000 of which are used for warmup). This is implemented as an
encoding,ROPMultiSimulation.This
simulation-based decoding procedure maps the ROP vectors to the solution space. In our case, this solution space are just multi-statistics records, as implemented in modulemultistatistics, where a correspondingspaceimplementation is also provided.Each of the 11 simulations is based on one fixed
instance, where all customer demands and machine work times (at certain time intervals) are fixed (but were sampled based on the distributions given in the Thürer paper [1]) using modulemfc_generator.Each of the 11 simulations has per-product fill rates PM_i,j, one average fill rate AM_i, one average stock level SL_i stored in the
multistatisticsrecords.As first objective, we use the smallest PM_i_j as PM and the smallest AM_i as AM and compute (1 - AM) * 100 + (1 - PM) in
worst_and_mean_fill_rate.As second objective, we use the largest SL_i in
max_stocklevel.NSGA-II, given in
nsga2, maintains a population of solutions which it evaluates like that.NSGA-II decides which solutions to keep based on the current Pareto front and crowding distance.
The retained solutions are reproduced, either via unary or binary search operators.
The unary search operator changes a random number (binomial distribution) of elements of a ROP vector (via normal distribution). It is given in
Op1MNormal.The binary search operator is simple uniform crossover, i.e., fills a new solution with elements of either of the two parent solutions. It is defined in
Op2Uniform.
The core idea is that we do not use randomized ARENA-like simulation. Instead, we use a simulator that is based on deterministic, fully pre-defined instances. These instances are still randomly generated according to the distributions given by Thürer in [1]. However, they have all values pre-determined. This allows us to run a simulation with the same ROP twice and get the exactly same result. If two different ROPs are simulated, but both of them decide to produce 1 unit of product A on machine V at time unit T, then for both of them this will take exactly the same amount of time.
Simulation results are thus less noisy.
Of course, there is a danger of overfitting. This is why we need to use multiple simulations to check a ROP. And then we take the worst-case results.
So this is the idea.
Matthias Thürer, Nuno O. Fernandes, Hermann Lödding, and Mark Stevenson. Material Flow Control in Make-to-Stock Production Systems: An Assessment of Order Generation, Order Release and Production Authorization by Simulation Flexible Services and Manufacturing Journal. 37(1):1-37. March 2025. doi: https://doi.org/10.1007/s10696-024-09532-2
moptipyapps.prodsched.rop_multisimulation module¶
A simulator for multiple runs of the ROP scenario.
Re-Order-Point (ROP) scenarios are such that for each product, a value X is provided. Once there are no more than X elements of that product in the warehouse, one new unit is ordered to be produced. Therefore, we have n_products such X values. A simulation in this scenario is implemented in rop_simulation as class ROPSimulation.
This module here provides the functionality to simulate this ROP-approach over multiple instances (Instance). It acts as an Encoding that converts a re-order point into an instance of MultiStatistics which can then be used as basis to compute the values of (potentially multiple) objective functions, such as those given in package objectives.
Now, doing a multi-simulation is costly. It takes from half a second to several seconds. It is unlikely that we can do more than a million in any run of an optimization algorithm. Therefore, we use an internal caching mechanism to store all the input vectors and output statistics. This may consume quite some memory, but it might be faster.
>>> from moptipyapps.prodsched.mfc_generator import sample_mfc_instance
>>> from moptipyapps.prodsched.mfc_generator import Product
>>> from moptipyapps.prodsched.mfc_generator import Station
>>> from moptipyapps.utils.sampling import Gamma
>>> from moptipyapps.prodsched.multistatistics import to_stream
>>> inst1 = sample_mfc_instance(seed=100)
>>> inst2 = sample_mfc_instance(seed=200)
>>> instances = (inst1, inst2)
>>> space = MultiStatisticsSpace(instances)
>>> ms = ROPMultiSimulation(space)
>>> x1 = np.array([4, 6, 3, 4, 4, 6, 3, 5, 6, 10])
>>> y = space.create()
>>> ms.decode(x1, y)
>>> for s in to_stream(y):
... if "time" not in s:
... print(s)
-------- Instance 0: 'mfc_10_13_10000_0x64' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;5.2184145757182705;5.2184145757182705;8.901258396714184;9.297175556856018;7.298761505635412;8.93985302431065;12.606621860129053;7.656178022299173;20.904266810249283;19.5732602761791;20.522703426695443
trp.mean;32.405411220867336;22.45718865883824;29.222031978321247;25.136275651698107;25.884250065648388;27.37383814423524;32.10269885414455;21.905835255853667;45.11627023942567;44.78387873021794;49.465651426661886
trp.max;101.42283298962138;56.30942379913131;74.17811849023519;58.955465373336665;60.209164983262326;60.66621167329777;74.4357130671624;47.944841355481;83.29062031809917;96.5279603056706;101.42283298962138
trp.sd;14.352456423754838;8.966715472534952;10.260311480773648;8.824583884361449;9.375043161957333;9.776116378207652;10.962890512009123;6.691465594834237;12.613587720623373;12.30244975508726;13.52381481631245
cwt.min;0.026454757136889384;0.026454757136889384;2.30483638259102;0.054286282554130594;0.16648942892061314;0.1385205442725237;2.2444075501571206;0.16694429707604286;0.2360807359827959;0.2894682031374032;0.7020815078112719
cwt.mean;8.296470547393602;4.437533958350662;12.615320110639914;4.7164029806433545;6.611310521050654;8.350460109042185;7.55452574755307;5.82304931358363;9.068938196877168;17.21719619156937;6.710247622764048
cwt.max;37.76460269749805;11.383554877750612;25.02207987420161;16.22657205109499;24.355088852856625;23.499199642416897;24.507644884798083;27.834403782263507;29.068687291493006;37.76460269749805;12.188275706294007
cwt.sd;8.072214752396842;3.8478370296451603;9.569679438559595;4.197433045217076;7.324722986150041;6.142260031783497;6.213481379280693;7.142777751769366;7.629569041217826;11.995928244823393;5.761419012759379
fill.rate;0.9475719810915342;0.9673295454545454;0.9869565217391304;0.9121037463976945;0.9595808383233533;0.93033381712627;0.9775280898876404;0.9477401129943502;0.8531073446327684;0.9452449567723343;0.9957983193277311
stocklevel.mean;29.106301317812118;2.760696025360742;4.137393715988243;1.5507610335648132;2.5500355305055966;2.3659654563794095;3.7559706390001586;1.811228677305335;1.5668982789103876;2.657874734996926;5.949477225800507
fulfilled.rate;1;1;1;1;1;1;1;1;1;1;1
-------- Instance 1: 'mfc_10_13_10000_0xc8' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;6.530138861169689;6.530138861169689;9.628902017961991;8.559079500931148;9.008831732480758;8.17345928487157;10.857776143797764;7.1989408754006945;20.221688833546978;20.764475417566246;18.683929092542712
trp.mean;31.011119751546097;20.636089799689874;27.67447485260846;23.897005119242046;24.208477302980572;25.25418917347286;29.761302044694876;23.04493364417017;43.39775607053671;45.37848659776272;46.77244686997264
trp.max;82.04580382289168;45.2064686844069;55.755010710267015;47.241916696964836;46.46518160018422;56.86270345999128;58.305458902948885;52.54891087334727;80.98768370429207;80.62246465566932;82.04580382289168
trp.sd;12.96227404463328;6.684031626500774;7.995431160774385;7.132422793760531;7.239745166217043;8.109774006790918;8.249688118912145;7.915621083543569;10.500873112173776;10.750658149592526;10.934157433002026
cwt.min;0.011249992165176081;0.5337058348654864;0.8014497560352538;0.2827261678148716;1.4241222500077129;0.011249992165176081;7.901938969392177;0.041148101390717784;0.08459428467904218;0.10726475210685749;
cwt.mean;7.183224986008803;1.9535417603494485;0.8014497560352538;4.619753737796203;9.370441335605987;7.744917661655692;7.901938969392177;8.052111019031262;8.505184343659167;5.922113521223626;
cwt.max;31.446190263142853;5.290086057329063;0.8014497560352538;10.883639956720799;25.608040164921476;24.178558590047942;7.901938969392177;31.446190263142853;21.35684648685401;16.312679904455308;
cwt.sd;6.15364337829274;1.7473126971982065;;3.440991075922831;6.485559248771155;6.715154696592823;;7.703968965710594;6.633960138504885;3.8259446158453017;
fill.rate;0.9628522630230573;0.9901408450704225;0.9985569985569985;0.9544159544159544;0.9635343618513323;0.9702127659574468;0.998546511627907;0.9175108538350217;0.9149855907780979;0.919831223628692;1
stocklevel.mean;30.090461520030797;2.907712049060156;4.261495763524114;1.6231331746350672;2.565318363579915;2.472060544282644;4.079218485848546;1.786007080448915;1.7664180248890007;2.4348085700118425;6.194289463750593
fulfilled.rate;1;1;1;1;1;1;1;1;1;1;1
>>> x2 = np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
>>> ms.decode(x2, y)
>>> for s in to_stream(y):
... if "time" not in s:
... print(s)
-------- Instance 0: 'mfc_10_13_10000_0x64' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;7.663287715103252;7.663287715103252;9.095457148523565;7.766240497990111;8.562181846958993;9.316040128631357;10.985466232731596;7.918294809653162;21.60147394131036;17.547777378904357;21.955240879517987
trp.mean;32.182938074849496;22.140701244808632;28.47300655868274;25.03367526681401;25.540081292675946;27.45355145426555;31.790509174952756;22.848381852082664;44.52135481443732;44.628532680799054;48.780078255964575
trp.max;90.86353698620042;59.31124925844961;67.52985535834341;53.98557871595767;59.61194121202789;59.45241944832833;67.31898313910006;56.0614850141219;81.02729637887933;80.10102521519639;90.86353698620042
trp.sd;13.55080270389196;8.168678848316368;9.338600023998156;8.450479951599732;8.617636871353389;9.348284373344056;10.208637939870723;7.518466597403155;10.930234897788171;10.544143459268716;12.157006455407252
cwt.min;0.011910352755876374;0.052322628985166375;0.011910352755876374;0.10146378360695962;0.0524298188602188;0.025453817918787536;0.05183870049131656;0.019666362641146407;0.08505119605797518;0.041104098391770094;0.35634199392734445
cwt.mean;17.582567285448004;9.404399139690337;13.569942297813908;9.604699962010896;10.98311819636398;13.02578680799254;16.36199335629276;9.05433728777719;24.801083270074987;24.958902762745797;30.451958550810023
cwt.max;77.9385378023253;38.626600188006705;53.98496252245468;38.07134509071511;42.09266935074265;47.99381408201043;48.82502650356764;44.910304177168655;61.19671763574479;61.32131436097461;77.9385378023253
cwt.sd;12.680595891084934;6.984547071390763;9.43383821564051;7.012918522038577;7.891903498485814;9.033444211571616;9.757435017677162;7.598286359665955;11.356538993918976;11.731098098924933;14.099099363785335
fill.rate;0.2175906030654634;0.40767045454545453;0.2492753623188406;0.3314121037463977;0.3158682634730539;0.28156748911465895;0.17134831460674158;0.3714689265536723;0.002824858757062147;0.018731988472622477;0.0350140056022409
stocklevel.mean;1.6302006108805502;0.3367466779925779;0.19929696858529114;0.15581052878690257;0.27567206398796995;0.2210582880452966;0.14995708383534714;0.261766524058465;0.00048013843604374936;0.006324013813991379;0.02308832333866472
fulfilled.rate;0.9982810485603781;0.9985795454545454;1;1;0.9985029940119761;0.9985486211901307;0.9929775280898876;1;0.9971751412429378;0.9985590778097982;0.9985994397759104
-------- Instance 1: 'mfc_10_13_10000_0xc8' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;6.456526072982342;6.456526072982342;10.280447092151007;6.8487732247867825;7.0104482929637015;6.928763679217354;10.310002410918969;6.5546482603685945;18.98639893585596;20.305760878712135;19.684816736973517
trp.mean;30.62233294764437;20.55618285379736;26.886775958390697;23.309369645366303;23.990410121167177;25.071330781848836;29.668457645437183;22.39107965050359;43.25662277895463;44.56350867653903;46.348319919246514
trp.max;78.42550095496154;50.8966871369812;54.1070436589971;48.669180978890836;52.42258290758127;55.73600798325697;57.290713516881624;53.713961902238225;78.26718994789553;78.42550095496154;76.74843649876311
trp.sd;13.071194037915653;7.013910120253621;8.186009510157717;7.519377794772064;7.702542096043028;8.526420667544688;8.8970364463244;7.682875953156183;10.548987944489301;10.828886793958864;10.898005970008226
cwt.min;0.0004850386339967372;0.00456283385574352;0.056539264354796614;0.005326168571627932;0.008960725403085235;0.08762150885104347;0.01698932920680818;0.0004850386339967372;0.2800049970701366;0.31679782128776424;0.12185953505104408
cwt.mean;16.459929680370593;8.156568809736969;12.374646871831668;8.300740034967504;10.371317340036168;10.931842774336376;14.222659911890325;9.16345905328603;23.137826511457558;25.434148525686762;27.66379761065284
cwt.max;60.919637988946306;29.480300784184692;35.16766467176876;36.653507062645986;39.00025180235298;45.453680537177206;43.580941370786604;45.28946949821329;56.024945551713245;59.28160733189452;60.919637988946306
cwt.sd;12.118506999914882;6.044408962707157;7.728932055062374;6.268950876605313;7.854789647395754;7.732399117687471;8.863583544578608;7.7525558824150504;11.241315012712777;12.118136175485233;12.596354952532373
fill.rate;0.24110446911471678;0.44084507042253523;0.2712842712842713;0.36182336182336183;0.3394109396914446;0.32340425531914896;0.21075581395348839;0.4196816208393632;0.002881844380403458;0.01969057665260197;0.025034770514603615
stocklevel.mean;1.8113476672429796;0.3802859636659473;0.23439669327549575;0.19451906423091544;0.2559188646462229;0.2206691076289149;0.1935442874513864;0.3124656055438941;0.00017487014444265826;0.008074900603352099;0.011298310052408007
fulfilled.rate;0.9987190435525192;1;0.9985569985569985;1;1;1;1;1;0.9985590778097982;0.9929676511954993;0.9972183588317107
Now the caching kicks in:
>>> x2 = np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
>>> ms.decode(x2, y)
>>> for s in to_stream(y):
... if "time" not in s:
... print(s)
-------- Instance 0: 'mfc_10_13_10000_0x64' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;7.663287715103252;7.663287715103252;9.095457148523565;7.766240497990111;8.562181846958993;9.316040128631357;10.985466232731596;7.918294809653162;21.60147394131036;17.547777378904357;21.955240879517987
trp.mean;32.182938074849496;22.140701244808632;28.47300655868274;25.03367526681401;25.540081292675946;27.45355145426555;31.790509174952756;22.848381852082664;44.52135481443732;44.628532680799054;48.780078255964575
trp.max;90.86353698620042;59.31124925844961;67.52985535834341;53.98557871595767;59.61194121202789;59.45241944832833;67.31898313910006;56.0614850141219;81.02729637887933;80.10102521519639;90.86353698620042
trp.sd;13.55080270389196;8.168678848316368;9.338600023998156;8.450479951599732;8.617636871353389;9.348284373344056;10.208637939870723;7.518466597403155;10.930234897788171;10.544143459268716;12.157006455407252
cwt.min;0.011910352755876374;0.052322628985166375;0.011910352755876374;0.10146378360695962;0.0524298188602188;0.025453817918787536;0.05183870049131656;0.019666362641146407;0.08505119605797518;0.041104098391770094;0.35634199392734445
cwt.mean;17.582567285448004;9.404399139690337;13.569942297813908;9.604699962010896;10.98311819636398;13.02578680799254;16.36199335629276;9.05433728777719;24.801083270074987;24.958902762745797;30.451958550810023
cwt.max;77.9385378023253;38.626600188006705;53.98496252245468;38.07134509071511;42.09266935074265;47.99381408201043;48.82502650356764;44.910304177168655;61.19671763574479;61.32131436097461;77.9385378023253
cwt.sd;12.680595891084934;6.984547071390763;9.43383821564051;7.012918522038577;7.891903498485814;9.033444211571616;9.757435017677162;7.598286359665955;11.356538993918976;11.731098098924933;14.099099363785335
fill.rate;0.2175906030654634;0.40767045454545453;0.2492753623188406;0.3314121037463977;0.3158682634730539;0.28156748911465895;0.17134831460674158;0.3714689265536723;0.002824858757062147;0.018731988472622477;0.0350140056022409
stocklevel.mean;1.6302006108805502;0.3367466779925779;0.19929696858529114;0.15581052878690257;0.27567206398796995;0.2210582880452966;0.14995708383534714;0.261766524058465;0.00048013843604374936;0.006324013813991379;0.02308832333866472
fulfilled.rate;0.9982810485603781;0.9985795454545454;1;1;0.9985029940119761;0.9985486211901307;0.9929775280898876;1;0.9971751412429378;0.9985590778097982;0.9985994397759104
-------- Instance 1: 'mfc_10_13_10000_0xc8' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;6.456526072982342;6.456526072982342;10.280447092151007;6.8487732247867825;7.0104482929637015;6.928763679217354;10.310002410918969;6.5546482603685945;18.98639893585596;20.305760878712135;19.684816736973517
trp.mean;30.62233294764437;20.55618285379736;26.886775958390697;23.309369645366303;23.990410121167177;25.071330781848836;29.668457645437183;22.39107965050359;43.25662277895463;44.56350867653903;46.348319919246514
trp.max;78.42550095496154;50.8966871369812;54.1070436589971;48.669180978890836;52.42258290758127;55.73600798325697;57.290713516881624;53.713961902238225;78.26718994789553;78.42550095496154;76.74843649876311
trp.sd;13.071194037915653;7.013910120253621;8.186009510157717;7.519377794772064;7.702542096043028;8.526420667544688;8.8970364463244;7.682875953156183;10.548987944489301;10.828886793958864;10.898005970008226
cwt.min;0.0004850386339967372;0.00456283385574352;0.056539264354796614;0.005326168571627932;0.008960725403085235;0.08762150885104347;0.01698932920680818;0.0004850386339967372;0.2800049970701366;0.31679782128776424;0.12185953505104408
cwt.mean;16.459929680370593;8.156568809736969;12.374646871831668;8.300740034967504;10.371317340036168;10.931842774336376;14.222659911890325;9.16345905328603;23.137826511457558;25.434148525686762;27.66379761065284
cwt.max;60.919637988946306;29.480300784184692;35.16766467176876;36.653507062645986;39.00025180235298;45.453680537177206;43.580941370786604;45.28946949821329;56.024945551713245;59.28160733189452;60.919637988946306
cwt.sd;12.118506999914882;6.044408962707157;7.728932055062374;6.268950876605313;7.854789647395754;7.732399117687471;8.863583544578608;7.7525558824150504;11.241315012712777;12.118136175485233;12.596354952532373
fill.rate;0.24110446911471678;0.44084507042253523;0.2712842712842713;0.36182336182336183;0.3394109396914446;0.32340425531914896;0.21075581395348839;0.4196816208393632;0.002881844380403458;0.01969057665260197;0.025034770514603615
stocklevel.mean;1.8113476672429796;0.3802859636659473;0.23439669327549575;0.19451906423091544;0.2559188646462229;0.2206691076289149;0.1935442874513864;0.3124656055438941;0.00017487014444265826;0.008074900603352099;0.011298310052408007
fulfilled.rate;0.9987190435525192;1;0.9985569985569985;1;1;1;1;1;0.9985590778097982;0.9929676511954993;0.9972183588317107
>>> x1 = np.array([4, 6, 3, 4, 4, 6, 3, 5, 6, 10])
>>> y = space.create()
>>> ms.decode(x1, y)
>>> for s in to_stream(y):
... if "time" not in s:
... print(s)
-------- Instance 0: 'mfc_10_13_10000_0x64' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;5.2184145757182705;5.2184145757182705;8.901258396714184;9.297175556856018;7.298761505635412;8.93985302431065;12.606621860129053;7.656178022299173;20.904266810249283;19.5732602761791;20.522703426695443
trp.mean;32.405411220867336;22.45718865883824;29.222031978321247;25.136275651698107;25.884250065648388;27.37383814423524;32.10269885414455;21.905835255853667;45.11627023942567;44.78387873021794;49.465651426661886
trp.max;101.42283298962138;56.30942379913131;74.17811849023519;58.955465373336665;60.209164983262326;60.66621167329777;74.4357130671624;47.944841355481;83.29062031809917;96.5279603056706;101.42283298962138
trp.sd;14.352456423754838;8.966715472534952;10.260311480773648;8.824583884361449;9.375043161957333;9.776116378207652;10.962890512009123;6.691465594834237;12.613587720623373;12.30244975508726;13.52381481631245
cwt.min;0.026454757136889384;0.026454757136889384;2.30483638259102;0.054286282554130594;0.16648942892061314;0.1385205442725237;2.2444075501571206;0.16694429707604286;0.2360807359827959;0.2894682031374032;0.7020815078112719
cwt.mean;8.296470547393602;4.437533958350662;12.615320110639914;4.7164029806433545;6.611310521050654;8.350460109042185;7.55452574755307;5.82304931358363;9.068938196877168;17.21719619156937;6.710247622764048
cwt.max;37.76460269749805;11.383554877750612;25.02207987420161;16.22657205109499;24.355088852856625;23.499199642416897;24.507644884798083;27.834403782263507;29.068687291493006;37.76460269749805;12.188275706294007
cwt.sd;8.072214752396842;3.8478370296451603;9.569679438559595;4.197433045217076;7.324722986150041;6.142260031783497;6.213481379280693;7.142777751769366;7.629569041217826;11.995928244823393;5.761419012759379
fill.rate;0.9475719810915342;0.9673295454545454;0.9869565217391304;0.9121037463976945;0.9595808383233533;0.93033381712627;0.9775280898876404;0.9477401129943502;0.8531073446327684;0.9452449567723343;0.9957983193277311
stocklevel.mean;29.106301317812118;2.760696025360742;4.137393715988243;1.5507610335648132;2.5500355305055966;2.3659654563794095;3.7559706390001586;1.811228677305335;1.5668982789103876;2.657874734996926;5.949477225800507
fulfilled.rate;1;1;1;1;1;1;1;1;1;1;1
-------- Instance 1: 'mfc_10_13_10000_0xc8' -------
stat;total;product_0;product_1;product_2;product_3;product_4;product_5;product_6;product_7;product_8;product_9
trp.min;6.530138861169689;6.530138861169689;9.628902017961991;8.559079500931148;9.008831732480758;8.17345928487157;10.857776143797764;7.1989408754006945;20.221688833546978;20.764475417566246;18.683929092542712
trp.mean;31.011119751546097;20.636089799689874;27.67447485260846;23.897005119242046;24.208477302980572;25.25418917347286;29.761302044694876;23.04493364417017;43.39775607053671;45.37848659776272;46.77244686997264
trp.max;82.04580382289168;45.2064686844069;55.755010710267015;47.241916696964836;46.46518160018422;56.86270345999128;58.305458902948885;52.54891087334727;80.98768370429207;80.62246465566932;82.04580382289168
trp.sd;12.96227404463328;6.684031626500774;7.995431160774385;7.132422793760531;7.239745166217043;8.109774006790918;8.249688118912145;7.915621083543569;10.500873112173776;10.750658149592526;10.934157433002026
cwt.min;0.011249992165176081;0.5337058348654864;0.8014497560352538;0.2827261678148716;1.4241222500077129;0.011249992165176081;7.901938969392177;0.041148101390717784;0.08459428467904218;0.10726475210685749;
cwt.mean;7.183224986008803;1.9535417603494485;0.8014497560352538;4.619753737796203;9.370441335605987;7.744917661655692;7.901938969392177;8.052111019031262;8.505184343659167;5.922113521223626;
cwt.max;31.446190263142853;5.290086057329063;0.8014497560352538;10.883639956720799;25.608040164921476;24.178558590047942;7.901938969392177;31.446190263142853;21.35684648685401;16.312679904455308;
cwt.sd;6.15364337829274;1.7473126971982065;;3.440991075922831;6.485559248771155;6.715154696592823;;7.703968965710594;6.633960138504885;3.8259446158453017;
fill.rate;0.9628522630230573;0.9901408450704225;0.9985569985569985;0.9544159544159544;0.9635343618513323;0.9702127659574468;0.998546511627907;0.9175108538350217;0.9149855907780979;0.919831223628692;1
stocklevel.mean;30.090461520030797;2.907712049060156;4.261495763524114;1.6231331746350672;2.565318363579915;2.472060544282644;4.079218485848546;1.786007080448915;1.7664180248890007;2.4348085700118425;6.194289463750593
fulfilled.rate;1;1;1;1;1;1;1;1;1;1;1
- class moptipyapps.prodsched.rop_multisimulation.ROPMultiSimulation(space)[source]¶
Bases:
EncodingA multi-simulation that caches the results for reuse.
- decode(x, y)[source]¶
Map a ROP setting to a multi-statistics.
This method uses an internal cache: The same re-order points will yield the same statistics.
- Parameters:
x (
ndarray) – the arrayy (
MultiStatistics) – the Gantt chart
- Return type:
moptipyapps.prodsched.rop_simulation module¶
A re-order-point-based simulation.
Re-Order-Point (ROP) scenarios are such that for each product, a value X is provided. Once there are no more than X elements of that product in the warehouse, one new unit is ordered to be produced. Therefore, we have n_products such X values.
ROP-based simulations extend the basic behavior of the class Simulation to re-order production based on ROPs.
>>> from moptipyapps.prodsched.simulation import PrintingListener
>>> instance = Instance(
... name="test1", n_products=2, n_customers=4, n_stations=2, n_demands=4,
... time_end_warmup=3000, time_end_measure=10000,
... routes=[[0, 1], [1, 0]],
... demands=[Demand(arrival=100, deadline=100, demand_id=0,
... customer_id=0, product_id=0, amount=1, measure=False),
... Demand(arrival=3100, deadline=3100, demand_id=1,
... customer_id=1, product_id=0, amount=1, measure=True),
... Demand(arrival=500, deadline=500, demand_id=2,
... customer_id=2, product_id=1, amount=1, measure=False),
... Demand(arrival=4000, deadline=5000, demand_id=3,
... customer_id=3, product_id=1, amount=1, measure=True)],
... warehous_at_t0=[0, 0],
... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0],
... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]],
... [[ 5.0, 24.0, 7.0, 80.0],
... [ 3.0, 21.0, 6.0, 50.0,]]])
>>> rop_sim = ROPSimulation(instance, PrintingListener(print_time=False))
>>> rop_sim.set_rop((10, 20))
>>> rop_sim.ctrl_run()
start
T=0.0: product=0, amount=0, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: station=0, 1 jobs queued
T=0.0: start j(id: 0, p: 0, am: 11, ar: 0, me: F, c: F, st: 0, sp: 0) at station 0
T=0.0: product=1, amount=0, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: station=1, 1 jobs queued
T=0.0: start j(id: 1, p: 1, am: 21, ar: 0, me: F, c: F, st: 0, sp: 0) at station 1
T=84.0: finished j(id: 1, p: 1, am: 21, ar: 0, me: F, c: F, st: 0, sp: 0) at station 1
T=100.0: product=0, amount=0, in_warehouse=0, in_production=11, 1 pending demands
T=130.0: finished j(id: 0, p: 0, am: 11, ar: 0, me: F, c: F, st: 0, sp: 0) at station 0
T=130.0: station=0, 2 jobs queued
T=130.0: start j(id: 1, p: 1, am: 21, ar: 0, me: F, c: F, st: 84, sp: 1) at station 0
T=130.0: station=1, 1 jobs queued
T=130.0: start j(id: 0, p: 0, am: 11, ar: 0, me: F, c: F, st: 130, sp: 1) at station 1
T=197.0: finished j(id: 0, p: 0, am: 11, ar: 0, me: F, c: T, st: 130, sp: 1) at station 1
T=197.0: product=0, amount=11, in_warehouse=0, in_production=1, 1 pending demands
T=197.0: d(id: 0, p: 0, c: 0, am: 1, ar: 100, dl: 100, me: F) statisfied
T=197.0: 10 units of product 0 in warehouse
T=240.0: finished j(id: 1, p: 1, am: 21, ar: 0, me: F, c: T, st: 84, sp: 1) at station 0
T=240.0: station=0, 1 jobs queued
T=240.0: start j(id: 2, p: 0, am: 1, ar: 100, me: F, c: F, st: 100, sp: 0) at station 0
T=240.0: product=1, amount=21, in_warehouse=0, in_production=0, 0 pending demands
T=240.0: 21 units of product 1 in warehouse
T=250.0: finished j(id: 2, p: 0, am: 1, ar: 100, me: F, c: F, st: 100, sp: 0) at station 0
T=250.0: station=1, 1 jobs queued
T=250.0: start j(id: 2, p: 0, am: 1, ar: 100, me: F, c: F, st: 250, sp: 1) at station 1
T=255.0: finished j(id: 2, p: 0, am: 1, ar: 100, me: F, c: T, st: 250, sp: 1) at station 1
T=255.0: product=0, amount=1, in_warehouse=10, in_production=0, 0 pending demands
T=255.0: 11 units of product 0 in warehouse
T=500.0: product=1, amount=0, in_warehouse=21, in_production=0, 1 pending demands
T=500.0: d(id: 2, p: 1, c: 2, am: 1, ar: 500, dl: 500, me: F) statisfied
T=500.0: 20 units of product 1 in warehouse
T=500.0: station=1, 1 jobs queued
T=500.0: start j(id: 3, p: 1, am: 1, ar: 500, me: F, c: F, st: 500, sp: 0) at station 1
T=503.0: finished j(id: 3, p: 1, am: 1, ar: 500, me: F, c: F, st: 500, sp: 0) at station 1
T=503.0: station=0, 1 jobs queued
T=503.0: start j(id: 3, p: 1, am: 1, ar: 500, me: F, c: F, st: 503, sp: 1) at station 0
T=508.0: finished j(id: 3, p: 1, am: 1, ar: 500, me: F, c: T, st: 503, sp: 1) at station 0
T=508.0: product=1, amount=1, in_warehouse=20, in_production=0, 0 pending demands
T=508.0: 21 units of product 1 in warehouse
T=3100.0! product=0, amount=0, in_warehouse=11, in_production=0, 1 pending demands
T=3100.0! d(id: 1, p: 0, c: 1, am: 1, ar: 3100, dl: 3100, me: T) statisfied
T=3100.0! 10 units of product 0 in warehouse
T=3100.0! station=0, 1 jobs queued
T=3100.0! start j(id: 4, p: 0, am: 1, ar: 3100, me: T, c: F, st: 3100, sp: 0) at station 0
T=3110.0! finished j(id: 4, p: 0, am: 1, ar: 3100, me: T, c: F, st: 3100, sp: 0) at station 0
T=3110.0! station=1, 1 jobs queued
T=3110.0! start j(id: 4, p: 0, am: 1, ar: 3100, me: T, c: F, st: 3110, sp: 1) at station 1
T=3117.0! finished j(id: 4, p: 0, am: 1, ar: 3100, me: T, c: T, st: 3110, sp: 1) at station 1
T=3117.0! product=0, amount=1, in_warehouse=10, in_production=0, 0 pending demands
T=3117.0! 11 units of product 0 in warehouse
T=4000.0! product=1, amount=0, in_warehouse=21, in_production=0, 1 pending demands
T=4000.0! d(id: 3, p: 1, c: 3, am: 1, ar: 4000, dl: 5000, me: T) statisfied
T=4000.0! 20 units of product 1 in warehouse
T=4000.0! station=1, 1 jobs queued
T=4000.0! start j(id: 5, p: 1, am: 1, ar: 4000, me: T, c: F, st: 4000, sp: 0) at station 1
T=4003.0! finished j(id: 5, p: 1, am: 1, ar: 4000, me: T, c: F, st: 4000, sp: 0) at station 1
T=4003.0! station=0, 1 jobs queued
T=4003.0! start j(id: 5, p: 1, am: 1, ar: 4000, me: T, c: F, st: 4003, sp: 1) at station 0
T=4008.0! finished j(id: 5, p: 1, am: 1, ar: 4000, me: T, c: T, st: 4003, sp: 1) at station 0
T=4008.0! product=1, amount=1, in_warehouse=20, in_production=0, 0 pending demands
T=4008.0! 21 units of product 1 in warehouse
T=4008.0 -- finished
>>> instance = Instance(
... name="test2", n_products=2, n_customers=1, n_stations=2, n_demands=5,
... time_end_warmup=21, time_end_measure=10000,
... routes=[[0, 1], [1, 0]],
... demands=[[0, 0, 1, 4, 20, 90], [1, 0, 0, 5, 22, 200],
... [2, 0, 1, 4, 30, 200], [3, 0, 1, 6, 60, 200],
... [4, 0, 0, 3, 5, 2000]],
... warehous_at_t0=[2, 1],
... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0],
... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]],
... [[ 5.0, 24.0, 7.0, 80.0],
... [ 3.0, 21.0, 6.0, 50.0,]]])
>>> rop_sim = ROPSimulation(instance, PrintingListener(print_time=False))
>>> rop_sim.set_rop((10, 10))
>>> rop_sim.ctrl_run()
start
T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 2 units of product 0 in warehouse
T=0.0: station=0, 1 jobs queued
T=0.0: start j(id: 0, p: 0, am: 9, ar: 0, me: F, c: F, st: 0, sp: 0) at station 0
T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 1 units of product 1 in warehouse
T=0.0: station=1, 1 jobs queued
T=0.0: start j(id: 1, p: 1, am: 10, ar: 0, me: F, c: F, st: 0, sp: 0) at station 1
T=5.0: product=0, amount=0, in_warehouse=2, in_production=9, 1 pending demands
T=20.0: product=1, amount=0, in_warehouse=1, in_production=10, 1 pending demands
T=22.0! product=0, amount=0, in_warehouse=2, in_production=12, 2 pending demands
T=30.0! product=1, amount=0, in_warehouse=1, in_production=14, 2 pending demands
T=39.0: finished j(id: 1, p: 1, am: 10, ar: 0, me: F, c: F, st: 0, sp: 0) at station 1
T=39.0! station=1, 2 jobs queued
T=39.0: start j(id: 3, p: 1, am: 4, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=57.0: finished j(id: 3, p: 1, am: 4, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=57.0! station=1, 1 jobs queued
T=57.0! start j(id: 5, p: 1, am: 4, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=60.0! product=1, amount=0, in_warehouse=1, in_production=18, 3 pending demands
T=69.0! finished j(id: 5, p: 1, am: 4, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=69.0! station=1, 1 jobs queued
T=69.0! start j(id: 6, p: 1, am: 6, ar: 60, me: T, c: F, st: 60, sp: 0) at station 1
T=102.0! finished j(id: 6, p: 1, am: 6, ar: 60, me: T, c: F, st: 60, sp: 0) at station 1
T=110.0: finished j(id: 0, p: 0, am: 9, ar: 0, me: F, c: F, st: 0, sp: 0) at station 0
T=110.0! station=0, 6 jobs queued
T=110.0: start j(id: 2, p: 0, am: 3, ar: 5, me: F, c: F, st: 5, sp: 0) at station 0
T=110.0! station=1, 1 jobs queued
T=110.0: start j(id: 0, p: 0, am: 9, ar: 0, me: F, c: F, st: 110, sp: 1) at station 1
T=140.0: finished j(id: 2, p: 0, am: 3, ar: 5, me: F, c: F, st: 5, sp: 0) at station 0
T=140.0! station=0, 5 jobs queued
T=140.0! start j(id: 4, p: 0, am: 5, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=171.0: finished j(id: 0, p: 0, am: 9, ar: 0, me: F, c: T, st: 110, sp: 1) at station 1
T=171.0! station=1, 1 jobs queued
T=171.0: start j(id: 2, p: 0, am: 3, ar: 5, me: F, c: F, st: 140, sp: 1) at station 1
T=171.0! product=0, amount=9, in_warehouse=2, in_production=8, 2 pending demands
T=171.0: d(id: 4, p: 0, c: 0, am: 3, ar: 5, dl: 2000, me: F) statisfied
T=171.0! d(id: 1, p: 0, c: 0, am: 5, ar: 22, dl: 200, me: T) statisfied
T=171.0! 3 units of product 0 in warehouse
T=186.0: finished j(id: 2, p: 0, am: 3, ar: 5, me: F, c: T, st: 140, sp: 1) at station 1
T=186.0! product=0, amount=3, in_warehouse=3, in_production=5, 0 pending demands
T=186.0! 6 units of product 0 in warehouse
T=210.0! finished j(id: 4, p: 0, am: 5, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=210.0! station=0, 4 jobs queued
T=210.0: start j(id: 1, p: 1, am: 10, ar: 0, me: F, c: F, st: 39, sp: 1) at station 0
T=210.0! station=1, 1 jobs queued
T=210.0! start j(id: 4, p: 0, am: 5, ar: 22, me: T, c: F, st: 210, sp: 1) at station 1
T=245.0! finished j(id: 4, p: 0, am: 5, ar: 22, me: T, c: T, st: 210, sp: 1) at station 1
T=245.0! product=0, amount=5, in_warehouse=6, in_production=0, 0 pending demands
T=245.0! 11 units of product 0 in warehouse
T=263.0: finished j(id: 1, p: 1, am: 10, ar: 0, me: F, c: T, st: 39, sp: 1) at station 0
T=263.0! station=0, 3 jobs queued
T=263.0: start j(id: 3, p: 1, am: 4, ar: 20, me: F, c: F, st: 57, sp: 1) at station 0
T=263.0! product=1, amount=10, in_warehouse=1, in_production=14, 3 pending demands
T=263.0: d(id: 0, p: 1, c: 0, am: 4, ar: 20, dl: 90, me: F) statisfied
T=263.0! d(id: 2, p: 1, c: 0, am: 4, ar: 30, dl: 200, me: T) statisfied
T=263.0! 3 units of product 1 in warehouse
T=287.0: finished j(id: 3, p: 1, am: 4, ar: 20, me: F, c: T, st: 57, sp: 1) at station 0
T=287.0! station=0, 2 jobs queued
T=287.0! start j(id: 5, p: 1, am: 4, ar: 30, me: T, c: F, st: 69, sp: 1) at station 0
T=287.0! product=1, amount=4, in_warehouse=3, in_production=10, 1 pending demands
T=287.0! d(id: 3, p: 1, c: 0, am: 6, ar: 60, dl: 200, me: T) statisfied
T=287.0! 1 units of product 1 in warehouse
T=303.0! finished j(id: 5, p: 1, am: 4, ar: 30, me: T, c: T, st: 69, sp: 1) at station 0
T=303.0! station=0, 1 jobs queued
T=303.0! start j(id: 6, p: 1, am: 6, ar: 60, me: T, c: F, st: 102, sp: 1) at station 0
T=303.0! product=1, amount=4, in_warehouse=1, in_production=6, 0 pending demands
T=303.0! 5 units of product 1 in warehouse
T=337.0! finished j(id: 6, p: 1, am: 6, ar: 60, me: T, c: T, st: 102, sp: 1) at station 0
T=337.0! product=1, amount=6, in_warehouse=5, in_production=0, 0 pending demands
T=337.0! 11 units of product 1 in warehouse
T=337.0 -- finished
- class moptipyapps.prodsched.rop_simulation.ROPSimulation(instance, listener)[source]¶
Bases:
SimulationCreate the re-order point-based simulation.
moptipyapps.prodsched.simulation module¶
A simulator for production scheduling.
For simulation a production system, we can build on the class Simulation. This base class offers the support to implement almost arbitrarily complex production system scheduling logic. The simulations here a fully deterministic and execute a given MFC scenario given as an instance, i.e., an object of type Instance.
The Simulation class offers a core backbone of a priority-queue based discrete event simulation. These events are strictly related to the production scheduling task, which allows for an efficient implementation. The Simulation is driven by the data of an instance. This instance prescribes when new demands (Demand) enter the system, how long certain production steps take, and which route each product type takes through the system, i.e., by which work stations it is processed in which order. This is the core logic that drives the simulation.
A simulation is executed by invoking the method ctrl_run(). Then, the event loop begins and it invokes the event_* methods as need be. For example, when a customer demand for a certain product comes in or if some units of a given product become available, the method event_product() is invoked. You can overwrite this method to decide what to do in such cases. For example, you could invoke act_demand_satisfied() to mark a Demand as satisfied, you could invoke act_produce() to tell the factory to produce some units of a given product, you could invoke act_store_in_warehouse() to store some units of a product in the warehouse or use act_take_from_warehouse() to take some out. In other words, in the event_* methods, you implement the logic, the operating system for your factory. Their default implementations in this class just produce product units on demand and do not really perform predictive production.
Each Simulation also needs an instance of Listener. The Listener is informed about what happens and sees all the production events. It is used to gather statistics and information – independently of how you implement the event_* methods. This allows us to compare factories that run on very different logic.
Simulations have three groups of methods:
Methods starting with ctrl_* are for starting and resetting the simulation so that it can be started again. You may override them if you have additional need for initialization or clean-up.
Methods that start with event_* are methods that are invoked by the simulator to notify you about an event in the simulation. You can overwrite these methods to implement the logic of your production scheduling method.
Methods that start with act_* are actions that you can invoke inside the event_* methods. The tell the simulator or stations what to do.
An example of such specialized simulations is the ROPSimulation, which simulates the behavior of a system that uses re-order points (ROPs) to decide what to produce and when. In such a simulation, the event_*-methods are overwritten to invoke the act_*-methods according to their needs. Here, in the base class Simulation, they are implemented such to order the production of product units directly upon the arrival of customer demands. In the ROPSimulation on the other hand, products are produced base on re-order points.
`ctrl_*` Methods:
We have the following ctrl_* methods, which are invoked from outside to start, stop, or reset the simulation.
ctrl_run()runs the simulation.ctrl_reset()resets the simulator so that we can start it again. If you want to re-use a simulation, you need to first invokectrl_reset()to clear the internal state.
`event_*` Methods:
We have the following event_* methods, which implement the core logic of the factory. They can be overwritten to ralize different production scenarios.
event_product()is invoked by the simulation if one of the following three things happened:An amount of a product has been produced (amount > 0).
An amount of a product has been made available at the start of the simulation to form the initial amount in the warehouse (amount > 0).
A customer demand for the product has appeared in the system.
In this method, you can store product into the warehouse, remove product from the warehouse, and/or mark a demand as completed.
event_station()is invoked by the simulation if a work station became idle and at least one productionJobis queued at the station. Now you can decide which of the queued to jobs to execute next by invokingact_exec_job(). The job you pass into this method will then immediately begin production at the work station.
`act_*` Methods:
The act_* methods are invoked from inside the event_* methods. They cause the production system to perform certain actions. The following act_* methods exist:
act_store_in_warehouse()can be invoked from insideevent_product(). It tells the system to add a certain amount of units of a given product to the warehouse. Notice that these units of product must have come from somewhere. They could be the result of a completed production job or could have occurred at the simulation startup as initial warehouse contents. You cannot just “make up” new product units without violating the integrity of the simulation.act_take_from_warehouse()can be invoked from insideevent_product(). It tells the system to take a certain amount of units out of the warehouse. You cannot take out more units from the warehouse than currently stored inside it nor can you take out a negative amount of units.act_demand_satisfied()can be invoked from insideevent_product(). It tells the system that a certainDemandhas been fulfilled. If you do that, you must make sure to remove the corresponding amount of product units from the system. They could have just been produced or they could have been taken from the warehouse. However, demands can only be satisfied using actually existing units of product. If you just “make up” product units, you will destroy the integrity of the simulation.act_produce()can be invoked from insideevent_product(). This instructs the system to begin producing a certain amount of a given product. This will lead to the creation of aJobrecord. This record will enter the queue of the first work station that the product should pass through. It will appear inevent_station()once this work station gets idle (or right away, if it currently is idle).act_exec_job()is invoked from insideevent_station(). Basically,event_station()gets called if there are one or multiple productionJobrecords queued at a work station and the work station is idle (or becomes idle). Then, you can decide which of the jobs to begin working on next on that work station. You will pass the correspondingJobrecord toact_exec_job(). Once that job is completed on the current work station, it will re-appear in theevent_station()invocation for the next work station it needs to pass through. Once it completes at its last work station, its producedamountof productproduct_idwill appear in aevent_product()invocation for the corresponding product.
Through this simple interface, we can control a relatively complex material flow simulation. In rop_simulation, we extend this simulation class by implementing a re-order point based approach. Matter of fact, we can extend this basic simulation using all kinds of production logic to drive our simulated factory.
The question then is: If all these approaches overwrite the event_* methods in different ways, how can we get a clear picture of their performance? No problem: For this purpose, the class Listener exists. Its methods are automatically invoked by the simulation and allow us to track exactly what happens and when, independently of the actual simulation logic. The class PrintingListener implements these methods to print the events to the standard output. In statistics_collector, we implement the class StatisticsCollector which instead uses the events to fill a Statistics record (see module statistics). This record collects all the performance data that is relevant for judging the efficiency of a production scheduling approach.
Examples:
Let’s now look at some basic examples of the production scheduling / material flow control simulation.
Here we have a very easy production scheduling instance. There is 1 product that passes through 2 stations. First it passes through station 0, then through station 1. The per-unit production time is always 10 time units on station 0 and 30 time units on station 2. There is one customer demand, for 10 units of this product, which enters the system at time unit 20. The warehouse is initially empty.
>>> instance = Instance(
... name="test1", n_products=1, n_customers=1, n_stations=2, n_demands=1,
... time_end_warmup=10, time_end_measure=4000,
... routes=[[0, 1]],
... demands=[[0, 0, 0, 10, 20, 100]],
... warehous_at_t0=[0],
... station_product_unit_times=[[[10.0, 10000.0]],
... [[30.0, 10000.0]]])
The simulation will see that the customer demand for 10 units of product 0 appears at time unit 20. It will issue a production order for these 10 units at station 0. Since station 0 is not occupied, it can immediately begin with the production. It will finish the production after 10*10 time units, i.e., at time unit 120. The product is then routed to station 1, which is also idle and can immediately begin producing. It needs 10*30 time units, meaning that it finishes after 300 time units. The demanded product amount is completed after 420 time units and the demand 0 can be fulfilled.
>>> simulation = Simulation(instance, PrintingListener(print_time=False))
>>> simulation.ctrl_run()
start
T=0.0: product=0, amount=0, in_warehouse=0, in_production=0, 0 pending demands
T=20.0! product=0, amount=0, in_warehouse=0, in_production=0, 1 pending demands
T=20.0! station=0, 1 jobs queued
T=20.0! start j(id: 0, p: 0, am: 10, ar: 20, me: T, c: F, st: 20, sp: 0) at station 0
T=120.0! finished j(id: 0, p: 0, am: 10, ar: 20, me: T, c: F, st: 20, sp: 0) at station 0
T=120.0! station=1, 1 jobs queued
T=120.0! start j(id: 0, p: 0, am: 10, ar: 20, me: T, c: F, st: 120, sp: 1) at station 1
T=420.0! finished j(id: 0, p: 0, am: 10, ar: 20, me: T, c: T, st: 120, sp: 1) at station 1
T=420.0! product=0, amount=10, in_warehouse=0, in_production=0, 1 pending demands
T=420.0! d(id: 0, p: 0, c: 0, am: 10, ar: 20, dl: 100, me: T) statisfied
T=420.0 -- finished
>>> instance = Instance(
... name="test2", n_products=2, n_customers=1, n_stations=2, n_demands=3,
... time_end_warmup=21, time_end_measure=10000,
... routes=[[0, 1], [1, 0]],
... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200],
... [2, 0, 1, 7, 30, 200]],
... warehous_at_t0=[2, 1],
... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0],
... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]],
... [[ 5.0, 24.0, 7.0, 80.0],
... [ 3.0, 21.0, 6.0, 50.0,]]])
>>> instance.name
'test2'
>>> simulation = Simulation(instance, PrintingListener(print_time=False))
>>> simulation.ctrl_run()
start
T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 2 units of product 0 in warehouse
T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 1 units of product 1 in warehouse
T=20.0: product=1, amount=0, in_warehouse=1, in_production=0, 1 pending demands
T=20.0: station=1, 1 jobs queued
T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=22.0! product=0, amount=0, in_warehouse=2, in_production=0, 1 pending demands
T=22.0! station=0, 1 jobs queued
T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=30.0! product=1, amount=0, in_warehouse=1, in_production=9, 2 pending demands
T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=62.0! station=1, 2 jobs queued
T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=62.0! station=0, 1 jobs queued
T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1) at station 0
T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=95.0! station=1, 1 jobs queued
T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1) at station 1
T=107.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: T, st: 62, sp: 1) at station 0
T=107.0! station=0, 1 jobs queued
T=107.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 95, sp: 1) at station 0
T=107.0! product=1, amount=9, in_warehouse=1, in_production=7, 2 pending demands
T=107.0: d(id: 0, p: 1, c: 0, am: 10, ar: 20, dl: 90, me: F) statisfied
T=107.0! 0 units of product 1 in warehouse
T=112.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: T, st: 52, sp: 1) at station 1
T=112.0! product=0, amount=3, in_warehouse=2, in_production=0, 1 pending demands
T=112.0! d(id: 1, p: 0, c: 0, am: 5, ar: 22, dl: 200, me: T) statisfied
T=112.0! 0 units of product 0 in warehouse
T=144.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: T, st: 95, sp: 1) at station 0
T=144.0! product=1, amount=7, in_warehouse=0, in_production=0, 1 pending demands
T=144.0! d(id: 2, p: 1, c: 0, am: 7, ar: 30, dl: 200, me: T) statisfied
T=144.0 -- finished
>>> simulation.ctrl_reset()
>>> simulation.ctrl_run()
start
T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 2 units of product 0 in warehouse
T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 1 units of product 1 in warehouse
T=20.0: product=1, amount=0, in_warehouse=1, in_production=0, 1 pending demands
T=20.0: station=1, 1 jobs queued
T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=22.0! product=0, amount=0, in_warehouse=2, in_production=0, 1 pending demands
T=22.0! station=0, 1 jobs queued
T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=30.0! product=1, amount=0, in_warehouse=1, in_production=9, 2 pending demands
T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=62.0! station=1, 2 jobs queued
T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=62.0! station=0, 1 jobs queued
T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1) at station 0
T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=95.0! station=1, 1 jobs queued
T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1) at station 1
T=107.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: T, st: 62, sp: 1) at station 0
T=107.0! station=0, 1 jobs queued
T=107.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 95, sp: 1) at station 0
T=107.0! product=1, amount=9, in_warehouse=1, in_production=7, 2 pending demands
T=107.0: d(id: 0, p: 1, c: 0, am: 10, ar: 20, dl: 90, me: F) statisfied
T=107.0! 0 units of product 1 in warehouse
T=112.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: T, st: 52, sp: 1) at station 1
T=112.0! product=0, amount=3, in_warehouse=2, in_production=0, 1 pending demands
T=112.0! d(id: 1, p: 0, c: 0, am: 5, ar: 22, dl: 200, me: T) statisfied
T=112.0! 0 units of product 0 in warehouse
T=144.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: T, st: 95, sp: 1) at station 0
T=144.0! product=1, amount=7, in_warehouse=0, in_production=0, 1 pending demands
T=144.0! d(id: 2, p: 1, c: 0, am: 7, ar: 30, dl: 200, me: T) statisfied
T=144.0 -- finished
Now we want to stop the simulation measurement period before the last job completes. Notice that the last production jobs after time unit 81 are no longer performed, because their end falls outside of the measurement period.
>>> instance = Instance(
... name="test3", n_products=2, n_customers=1, n_stations=2, n_demands=3,
... time_end_warmup=21, time_end_measure=100,
... routes=[[0, 1], [1, 0]],
... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200],
... [2, 0, 1, 7, 30, 200]],
... warehous_at_t0=[2, 1],
... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0],
... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]],
... [[ 5.0, 24.0, 7.0, 80.0],
... [ 3.0, 21.0, 6.0, 50.0,]]])
>>> instance.name
'test3'
>>> simulation = Simulation(instance, PrintingListener(print_time=False))
>>> simulation.ctrl_run()
start
T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 2 units of product 0 in warehouse
T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 1 units of product 1 in warehouse
T=20.0: product=1, amount=0, in_warehouse=1, in_production=0, 1 pending demands
T=20.0: station=1, 1 jobs queued
T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=22.0! product=0, amount=0, in_warehouse=2, in_production=0, 1 pending demands
T=22.0! station=0, 1 jobs queued
T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=30.0! product=1, amount=0, in_warehouse=1, in_production=9, 2 pending demands
T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=62.0! station=1, 2 jobs queued
T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=62.0! station=0, 1 jobs queued
T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1) at station 0
T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=95.0! station=1, 1 jobs queued
T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1) at station 1
T=95.0 -- finished
>>> simulation.ctrl_reset()
>>> simulation.ctrl_run()
start
T=0.0: product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 2 units of product 0 in warehouse
T=0.0: product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0.0: 1 units of product 1 in warehouse
T=20.0: product=1, amount=0, in_warehouse=1, in_production=0, 1 pending demands
T=20.0: station=1, 1 jobs queued
T=20.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=22.0! product=0, amount=0, in_warehouse=2, in_production=0, 1 pending demands
T=22.0! station=0, 1 jobs queued
T=22.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=30.0! product=1, amount=0, in_warehouse=1, in_production=9, 2 pending demands
T=52.0! finished j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 22, sp: 0) at station 0
T=62.0: finished j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 20, sp: 0) at station 1
T=62.0! station=1, 2 jobs queued
T=62.0! start j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=62.0! station=0, 1 jobs queued
T=62.0: start j(id: 0, p: 1, am: 9, ar: 20, me: F, c: F, st: 62, sp: 1) at station 0
T=95.0! finished j(id: 2, p: 1, am: 7, ar: 30, me: T, c: F, st: 30, sp: 0) at station 1
T=95.0! station=1, 1 jobs queued
T=95.0! start j(id: 1, p: 0, am: 3, ar: 22, me: T, c: F, st: 52, sp: 1) at station 1
T=95.0 -- finished
- class moptipyapps.prodsched.simulation.Job(job_id, product_id, amount, arrival, measure, completed=False, station_time=-1.0, step=-1)[source]¶
Bases:
objectThe record for a production job.
- class moptipyapps.prodsched.simulation.Listener[source]¶
Bases:
objectA listener for simulation events.
- event_product(time, product_id, amount, in_warehouse, in_production, pending_demands, is_in_measure_period)[source]¶
Get notified right before
Simulation.event_product().- Parameters:
time (
float) – the current system timeproduct_id (
int) – the id of the productamount (
int) – the amount of the product that appearsin_warehouse (
int) – the amount of the product currently in the warehousein_production (
int) – the amounf of product currently under productionpending_demands (
tuple[Demand,...]) – the pending orders for the productis_in_measure_period (
bool) – is this event inside the measurement period?
- Return type:
- event_station(time, station_id, queue, is_in_measure_period)[source]¶
Get notified right before
Simulation.event_station().If this event happens, the station is not busy. It could process a job and there is at least one job that it could process. You can now select the job to be executed from the queue and pass it to
act_exec_job().
- produce_at_begin(time, station_id, job)[source]¶
Report the start of the production of a certain product at a station.
- produce_at_end(time, station_id, job)[source]¶
Report the completion of the production of a product at a station.
- class moptipyapps.prodsched.simulation.PrintingListener(output=<built-in function print>, print_time=True)[source]¶
Bases:
ListenerA listener that just prints simulation events.
- event_product(time, product_id, amount, in_warehouse, in_production, pending_demands, is_in_measure_period)[source]¶
Print the prouct event.
- Return type:
- event_station(time, station_id, queue, is_in_measure_period)[source]¶
Print the station event.
- Return type:
- produce_at_begin(time, station_id, job)[source]¶
Print that the production at a given station begun.
- Return type:
- produce_at_end(time, station_id, job)[source]¶
Print that the production at a given station ended.
- Return type:
- class moptipyapps.prodsched.simulation.Simulation(instance, listener)[source]¶
Bases:
objectA simulator for production scheduling.
- act_store_in_warehouse(product_id, amount)[source]¶
Add a certain amount of product to the warehouse.
- act_take_from_warehouse(product_id, amount)[source]¶
Remove a certain amount of product to the warehouse.
- ctrl_reset()[source]¶
Reset the simulation.
This function sets the time to 0, clears the event queue, clears the pending orders list, clears the warehouse.
- Return type:
- ctrl_run()[source]¶
Run the simulation.
This function executes the main loop of the simulation. It runs the central event pump, which is a priority queue. It processes the simulation events one by one.
- Return type:
- event_product(time, product_id, amount, in_warehouse, in_production, pending_demands)[source]¶
Take actions when an event regarding a product or demand occurred.
The following events may have occurred:
An amount of a product has been produced (amount > 0).
An amount of a product has been made available at the start of the simulation to form the initial amount in the warehouse (amount > 0).
A customer demand for the product has appeared in the system. If there is any demand to be fulfilled, then pending_demands is not empty.
You can choose to execute one or multiple of the following actions:
act_store_in_warehouse()to store a positive amount of product in the warehouse.act_take_from_warehouse()to take a positive amount of product out of the warehouse (must be <= in_warehouse.act_produce()to order the production of a positive amount of the product.act_demand_satisfied()to mark one of the demands from queue as satisfied. Notice that in this case, you must make sure to remove the corresponding amount of product units from the system. If sufficient units are in amount, you would simply not store these in the warehouse. You could also simply take some units out of the warehouse withact_take_from_warehouse().
- Parameters:
time (
float) – the current system timeproduct_id (
int) – the id of the productamount (
int) – the amount of the product that appearsin_warehouse (
int) – the amount of the product currently in the warehousein_production (
int) – the amounf of product currently under productionpending_demands (
tuple[Demand,...]) – the pending orders for the product
- Return type:
- event_station(time, station_id, queue)[source]¶
Process an event for a given station.
If this event happens, the station is not busy. It could process a job and there is at least one job that it could process. You can now select the job to be executed from the queue and pass it to
act_exec_job().
moptipyapps.prodsched.statistics module¶
A statistics record for the simulation.
This module provides a record with statistics derived from one single MFC simulation. It can store values such as the mean fill rate or the mean stock level. Such statistics records are filled in by instances of the StatisticsCollector plugged into the Simulation.
- moptipyapps.prodsched.statistics.COL_PRODUCT_PREFIX: Final[str] = 'product_'¶
the product column prefix
- moptipyapps.prodsched.statistics.ROW_FULFILLED_RATE: Final[str] = 'fulfilled.rate'¶
the fulfilled rate
- moptipyapps.prodsched.statistics.ROW_SIMULATION_TIME: Final[str] = 'time.s: '¶
the simulation time getter
- moptipyapps.prodsched.statistics.ROW_STOCK_LEVEL_MEAN: Final[str] = 'stocklevel.mean'¶
the mean stock level row
- class moptipyapps.prodsched.statistics.Statistics(n_products)[source]¶
Bases:
objectA statistics record based on production scheduling.
It provides the following statistics:
immediate_rates: The per-product-fillrate, i.e., the fraction of demands of a given product that were immediately fulfilled when arriving in the system (i.e., that were fulfilled by using product that was available in the warehouse/in stock). Higher values are good.immediate_rate: The overall fillrate, i.e., the total fraction of demands that were immediately fulfilled upon arrival in the system over all demands. That is, this is the fraction of demands that were fulfilled by using product that was available in the warehouse/in stock. Higher values are good.waiting_times: The per-product waiting times (“CWT”) for the demands that came in but could not immediately be fulfilled. These are the demands for a given product that were, so to say, not covered by the fillrate/immediate_rate. If all demands of a product could immediately be satisfied, then this is None. Otherwise, smaller values are good.waiting_time: The overall waiting times (“CWT”) for the demands that came in but could not immediately be fulfilled. These are all the demands for a given product that were, so to say, not covered by the fillrate/immediate_rate. If all demands could immediately be satisfied, then this is None. Otherwise, smaller values are good.production_times: The per-product times that producing one unit of the product takes from the moment that a production job is created until it is completed. Smaller values of this “TRP” are better.production_time: The overall statistics on the times that producing one unit of any product takes from the moment that a production job is created until it is completed. Smaller values this “TRP” are better.fulfilled_rates: The per-product fraction of demands that were satisfied. Demands for a product may remain unsatisfied if they have not been satisfied by the end of the simulation period. Larger values are better.fulfilled_rate: The fraction of demands that were satisfied. Demands may remain unsatisfied if they have not been satisfied by the end of the simulation period. Larger values are better.stock_levels: The average amount of a given product in the warehouse averaged over the simulation time. Smaller values are better.stock_level: The total average amount units of any product in the warehouse averaged over the simulation time. Smaller values are better.simulation_time_nanos: The total time that the simulation took, measured in nanoseconds.
Instances of this class are filled by
StatisticsCollectorobjects plugged into theSimulation.- copy_from(stat)[source]¶
Copy the contents of another statistics record.
- Parameters:
stat (
Statistics) – the other statistics record- Return type:
- fulfilled_rates:
Final[list[int|float|None]]¶ the fraction of demands that were fulfilled, on a per-product basis
- immediate_rate:
int|float|None¶ the overall fraction of immediately satisfied demands, i.e., the fillrate
- immediate_rates:
Final[list[int|float|None]]¶ the fraction of demands that were immediately satisfied, on a per-product basis, i.e., the fillrate
- production_time:
StreamStatistics|None¶ the overall production time (TRP) statistics
- production_times:
Final[list[StreamStatistics|None]]¶ the production time (TRP) statistics per-product
- waiting_time:
StreamStatistics|None¶ the overall waiting time for all demands that were not immediately satisfied – only counting demands that were actually satisfied, i.e., the CWT
- waiting_times:
Final[list[StreamStatistics|None]]¶ the average waiting time for all demands that were not immediately satisfied – only counting demands that were actually satisfied, i.e., the CWT
moptipyapps.prodsched.statistics_collector module¶
A tool for collecting statistics from an MFC simulation.
A statistics collector StatisticsCollector is a special Listener that can be plugged into a Simulation. During the execution of the simulation, it gathers statistics about what is going on. It finally stores these into a Statistics record. Such a record can then be used to understand the key characteristics of the behavior of the simulation in on a given Instance.
The simulation listeners (Listener) offer a method to pipe out data from arbitrary subclasses of Simulation. This means that they allow us to access data in a unified way, regardless of which manufacturing logic or production scheduling we actually implement.
In the case of the StatisticsCollector implemented here, we implement the Listener-API to fill a Statistics record with data. Such records offer the standard statistics that Thürer et al. used in their works. In other words, we can make such statistics available and accessible, regardless of how our simulation schedules the production.
Moreover, multiple such Statistics records, filled with data from simulations over multiple different instances (instance) can be combined in a MultiStatistics record. This record, which will comprehensively represent performance over several independent instances, then can be used as basis for objective functions (Objective) such as those given in objectives.
>>> instance = Instance(
... name="test2", n_products=2, n_customers=1, n_stations=2, n_demands=5,
... time_end_warmup=21, time_end_measure=10000,
... routes=[[0, 1], [1, 0]],
... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200],
... [2, 0, 1, 7, 30, 200], [3, 0, 1, 6, 60, 200],
... [4, 0, 0, 125, 5, 2000]],
... warehous_at_t0=[2, 1],
... station_product_unit_times=[[[10.0, 50.0, 15.0, 100.0],
... [ 5.0, 20.0, 7.0, 35.0, 4.0, 50.0]],
... [[ 5.0, 24.0, 7.0, 80.0],
... [ 3.0, 21.0, 6.0, 50.0,]]])
>>> from moptipyapps.prodsched.simulation import Simulation
>>> statistics = Statistics(instance.n_products)
>>> collector = StatisticsCollector(instance)
>>> collector.set_dest(statistics)
>>> simulation = Simulation(instance, collector)
>>> simulation.ctrl_run()
>>> print("\n".join(str(statistics).split("\n")[:-1]))
stat;total;product_0;product_1
trp.min;229.57142857142858;455.6;229.57142857142858
trp.mean;304.8888888888889;455.6;246.92307692307693
trp.max;455.6;455.6;267.1666666666667
trp.sd;97.56326157594913;0;19.50721118346466
cwt.min;1603;2278;1603
cwt.mean;1829.3333333333333;2278;1605
cwt.max;2278;2278;1607
cwt.sd;388.56187838403986;;2.8284271247461903
fill.rate;0;0;0
stocklevel.mean;0.6078765407355446;0.4497444633730835;0.15813207736246118
fulfilled.rate;1;1;1
- class moptipyapps.prodsched.statistics_collector.StatisticsCollector(instance)[source]¶
Bases:
ListenerA listener for simulation events that collects basic statistics.
- produce_at_end(time, station_id, job)[source]¶
Report the completion of the production of a product at a station.
- product_in_warehouse(time, product_id, amount, is_in_measure_period)[source]¶
Report a change of the amount of products in the warehouse.
- set_dest(dest)[source]¶
Set the statistics record to fill.
- Parameters:
dest (
Statistics) – the destination- Return type: