moptipyapps.prodsched package¶
Examples of production scheduling. Methods for generating instances. Bases: A customer represents purchase orders sequences. the time between orders the products that the customer orders and the corresponding amount distributions the base time given until the deadline an aggregation of the giving sampling method the time jitter added to the days between orders the source for the customers the base amount that the customer usually orders the distribution for the number of days between orders per customer the size of the custmer the time offset of the customer the normal time that the customer provides until the deadline the key for the day time unit distribution the key for the day time unit distribution generator the distribution for the number of demands per customer a deterministic structure the machines per product, if any the machines order offset the source for the number of customers the source for the number of demands the source for the number of machines the source for the number of products the jitter applied to the orders the key for the production time unit distribution the key for the production time unit distribution generator the distribution for the number of products per customer the distribution of the amount that is usually ordered per product the source for the product base demands the random seed of the generator the jitter applied until the deadline Bases: The class for generating instances. Get the day time units distribution. the day time units distribution Get the product base demand unit. the base demand distributions Get the production time units base distribution. the production time units base distribution Set the customers. You can either set the customers directly, by providing customers. Or they can be randomly sampled. customers ( time_offset ( demands_per_customer ( products_per_customer ( customer_size ( between_order_days ( between_order_jitter ( to_deadline_days ( to_deadline_jitter ( Set the distribution of the time units per day. tu ( Set the demands. You can either set the demands directly, by providing demands. Or they can be randomly sampled. Set the number of customers. n_customers ( Set the number of demands. n_demands ( Set the number of machines. n_machines ( Set the number of products. n_products ( Set the base unit distributions for the products. base_demands ( base_demand ( Set the distribution of the base time units per production step. tu ( Set the routes on which the products pass through machines. The routes can either be set directly as a constance, by providing routes. Otherwise, they can be sampled randomly. If you specify routes, then this parameter provides a sequence of sequences: For each product, it specifies the machines through which the product must pass. No product can pass through the same machine twice. No machine must be left unused. Each product passes through at least one machine. If you want to randomly sample the machines, you can provide a distribution from which the number of machines through which each product needs to go is drawn via n_machines_per_product. Now, in the real world, even if we have different products with different production steps, the order in which the products pass through machines is not completely “random”. If we randomly sample machines, then our idea to realize a reasonable order is as follows: If a product goes through a set of machines {a, b, c}, then it tends to go through the machine with the lower indices first. In some cases, the order may be different, but generally lower-index machines tend to come earlier in production cycles. To realize this, once we have the machines for a product selected, each machine gets base priority p = 10*i, where i is the machine index. To this base priority, we add a random number distributed as machine_order_offset. This offset can thus change the order priority of the machines. The machines are then used by the product based on the order priority. routes ( n_machines_per_product ( machine_order_offset ( A production scheduling instance. Production instances have names Notice that production times are used in a cycling fashion. The time when a certain product is finished can be computed via The number of products be 3. >>> n_products = 3 The number of customers be 5. >>> n_customers = 5 The number of machines be 4. >>> n_machines = 4 There will be 6 customer demands. >>> n_demands = 6 Each product may take a different route through different machines. >>> 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 machine requires a certain working time for each unit of each product. This production time may vary over time. For example, maybe machine 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 machines that a product is actually routed through. >>> m0_p0 = [10, 20, 11, 40, 8, 60] >>> m0_p1 = [12, 20, 7, 40, 11, 70] >>> m0_p2 = [] >>> m1_p0 = [] >>> m1_p1 = [20, 50, 30, 120, 7, 200] >>> m1_p2 = [21, 50, 29, 130, 8, 190] >>> m2_p0 = [ 8, 20, 9, 60] >>> m2_p1 = [10, 90] >>> m2_p2 = [12, 70, 30, 120] >>> m3_p0 = [70, 200, 3, 220] >>> m3_p1 = [60, 220, 5, 260] >>> m3_p2 = [30, 210, 10, 300] >>> machine_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_machines, n_demands, … routes, demands, warehous_at_t0, … machine_product_unit_times, infos) >>> instance.name ‘my_instance’ 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 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_machines == instance.n_machines 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.warehous_at_t0 == instance.warehous_at_t0 True >>> i2.machine_product_unit_times == instance.machine_product_unit_times True >>> i2.infos == instance.infos True The record for demands. Bases: An instance of the Production Scheduling Problem. The demands: Each demand is a tuple of demand_id, customer_id, product_id, amount, release_time, and deadline. The customer makes their order at time step release_time. They expect to receive their product by the deadline. The demands are sorted by release time and then deadline. The release time is always > 0. The deadline is always >= release time. Demand ids are unique. 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 per-machine unit production times for each product. Each machine 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 machine, 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. The amount of products in the warehouse at time step 0. the key for the number of customers the number of demands in the scenario the key for the number of machines the key for the number of products the first part of the production time the first part of the product route key Compute the time when one job is finished. The production times are cyclic intervals of unit production times and interval ends. the end time Here, the production time is 10 time units / 1 product unit, valid until end time 100. >>> compute_finish_time(0, 1, (10, 100)) 10 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, 1, (10, 100)) 260 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, 1, (10, 100)) 100 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 cannot be completed until the end of the period. So only 0.5 units are produced, needing 5 time units and finishing at period 100. Then the production time window begins again. Since there is only 1 production time, in the second production cycle there will still be 10 time units per production unit. The remaining 0.5 units will be produced in 5 time units, meaning that the overall production is finished at time step 105. >>> compute_finish_time(95, 1, (10, 100)) 105 Now we have two production periods. The production begins again at time step 95. 0.5 units are finished until the production period ends at time step 100. Then, the second production period begins, requiring 20 time units per product unit. We thus need 10 time units to finish the remaining 0.5 product unit, completing the job at time unit 110. >>> compute_finish_time(95, 1, (10, 100, 20, 200)) 110 Now things get more complex. We want to do 10 units of product. We can finish 0.5 units until the first period ends after 5 time steps. Then, in the second period, we can do exactly 2 units of product until the period ends at time 140. This leaves 7.5 units of product to be done in the third period, where each unit needs 50 time units. Thus, we end up needing 5 + 40 + 7.5*50 time units, which add to the starting time. >>> compute_finish_time(95, 10, (10, 100, 20, 140, 50, 5000)) 515 >>> 95 + (0.5*10 + 2*20 + 7.5*50) 515.0 This is the same as the example before, except that the third period now ends at time step 200. Thus, we can only do 1.2=(200-140)/50 units of product in the 200 - 140 time units this period lasts. Then the production cycle wraps over. Luckily, we can complete the remaining 6.3 units of product in the first period, where each product unit needs only 10 time units. >>> compute_finish_time(95, 10, (10, 100, 20, 140, 50, 200)) 263 >>> 95 + (0.5*10 + 2*20 + 1.2*50 + 6.3*10) 263.0 Here we illustrate that production times are rounded to the nearest larger integer. If we encounter a situation such that the production would need 246.6666 time units, we return 247. The only exception for this “rounding up” strategy is if the result is very very close to the nearest integer. For example, 10.00006103515625 would be rounded to 11, whereas 10.0000610351562 will be rounded to 10. (0.00006103515625 = 1/16384 = 2**⁻14). >>> compute_finish_time(95, 10, (10, 100, 20, 140, 50, 200, 3, 207)) 247 >>> 95 + (0.5*10 + 2*20 + 1.2*50 + (7/3)*3 + (6.3-(7/3))*10) 246.66666666666666 A simulator for production scheduling. For simulation a production system, we can build on the class 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 machines what to do. ## We have the following ctrl_* methods:** ## We have the following event_* methods:** 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. ## Examples Here we have a very easy production scheduling instance. There is 1 product that passes through 2 machines. First it passes through machine 0, then through machine 1. The per-unit production time is always 10 time units on machine 0 and 30 time units on machine 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_machines=2, n_demands=1, … routes=[[0, 1]], … demands=[[0, 0, 0, 10, 20, 100]], … warehous_at_t0=[0], … machine_product_unit_times=[[[10, 10000]], … [[30, 10000]]]) 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 machine 0. Since machine 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 machine 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()) >>> simulation.ctrl_run() start T=20: EVENT product=0, amount=0, in_warehouse=0, in_production=0, 1 pending demands T=20: EVENT machine=0, 1 jobs queued T=20: beginning to produce 10 units of product 0 on machine 0 T=120: finished producing 10 units of product 0 on machine 0 T=120: EVENT machine=1, 1 jobs queued T=120: beginning to produce 10 units of product 0 on machine 1 T=420: finished producing 10 units of product 0 on machine 1 T=420: EVENT product=0, amount=10, in_warehouse=0, in_production=0, 1 pending demands T=420: demand 0 for 10 units of product 0 satisfied T=420: finished Bases: The record for a production job. Bases: A listener for simulation events. Get notified right before If this event happens, the machine 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 Get notified right before time ( product_id ( amount ( in_warehouse ( in_production ( pending_demands ( Report the start of the production of a certain product at a machine. Report the completion of the production of a product at a machine. Bases: A listener that just prints simulation events. Print the prouct event. Print that the production at a given machine begun. Print that the production at a given machine ended. Bases: A simulator for production scheduling. Add a certain amount of product to the warehouse. Remove a certain amount of product to the warehouse. Reset the simulation. This function sets the time to 0, clears the event queue, clears the pending orders list, clears the warehouse. 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. Process an event for a given machine. If this event happens, the machine 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 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: time ( product_id ( amount ( in_warehouse ( in_production ( pending_demands (Submodules¶
moptipyapps.prodsched.generator module¶
>>> from moptipyapps.prodsched.instance import to_stream
>>> def receive(inst: Instance) -> None:
... for z in to_stream(inst):
... print(z)
>>> with InstanceGenerator(receive) as ig:
... ig.set_seed(11323)
objectIntDistribution¶tuple[tuple[int, IntDistribution], ...]¶IntDistribution¶AbstractContextManagerOptional[Iterable[Customer]], default: None) – the customersint | IntDistribution | None, default: None) – the distribution for the time offset when customers begin orderingint | IntDistribution | None, default: None) – an optional distribution of the number of demands that each customer may issue. In total, there will always be exactly the number of expected demands. The distributions are sampled again and again, until that number is reached.int | IntDistribution | None, default: None) – the products to be sampled per customerint | IntDistribution | None, default: None) – the multiplier of the order amountint | IntDistribution | None, default: None) – a distribution for the number of days between customer ordersint | IntDistribution | None, default: None) – the jitter for the time between ordersint | IntDistribution | None, default: None) – the normal amount of days that a customer gives as deadlineint | IntDistribution | None, default: None) – the jitter for the aboveint | IntDistribution) – the time units per dayint | IntDistribution) – the number of customers, either explicitly or implicitly set.int | IntDistribution) – the number of demands, either explicitly or implicitly set.int | IntDistribution) – the number of machines, either explicitly or implicitly set.int | IntDistribution) – the number of products, either explicitly or implicitly set.Optional[Iterable[IntDistribution]], default: None) – the base demand units per productint | IntDistribution | None, default: None) – the distribution from which we draw the base demandsint | IntDistribution) – the time units per dayOptional[Iterable[Iterable[int]]], default: None) – the routesIntDistribution | None, default: None) – the distribution for machines per productIntDistribution | None, default: None) – the machine order offsetmoptipyapps.prodsched.instance module¶
Instance.name.compute_finish_time() in an efficient way.>>> name = "my_instance"
>>> instance.n_customers
5
>>> instance.n_machines
4
>>> instance.n_demands
6
>>> instance.n_products
3
>>> instance.routes
((0, 3, 2), (0, 2, 1, 3), (1, 2, 3))
>>> instance.demands
(Demand(release_time=1240, deadline=3000, demand_id=0, customer_id=0, product_id=1, amount=20), Demand(release_time=2300, deadline=4000, demand_id=1, customer_id=1, product_id=0, amount=10), Demand(release_time=4234, deadline=27080, demand_id=5, customer_id=3, product_id=0, amount=19), Demand(release_time=5410, deadline=16720, demand_id=4, customer_id=4, product_id=2, amount=23), Demand(release_time=7300, deadline=9000, demand_id=3, customer_id=3, product_id=1, amount=12), Demand(release_time=8300, deadline=11000, demand_id=2, customer_id=2, product_id=2, amount=7))
>>> instance.warehous_at_t0
(10, 0, 6)
>>> instance.machine_product_unit_times
(((10, 20, 11, 40, 8, 60), (12, 20, 7, 40, 11, 70), ()), ((), (20, 50, 30, 120, 7, 200), (21, 50, 29, 130, 8, 190)), ((8, 20, 9, 60), (10, 90), (12, 70, 30, 120)), ((70, 200, 3, 220), (60, 220, 5, 260), (30, 210, 10, 300)))
>>> dict(instance.infos)
{'source': 'manually created', 'creation_date': '2025-11-09'}
ComponentFinal[tuple[Demand, ...]]¶Final[Mapping[str, str]]¶Final[tuple[tuple[tuple[int, ...], ...], ...]]¶moptipyapps.prodsched.simulation module¶
Simulation. This base class offers the support to implement almost arbitrarily complex production system scheduling logic. There are three groups of methods: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 invoke ctrl_reset() to clear the internal state.event_product() is invoked by the simulation if one of the following three things happened:>>> instance = Instance(
... name="test2", n_products=2, n_customers=1, n_machines=2, n_demands=2,
... routes=[[0, 1], [1, 0]],
... demands=[[0, 0, 1, 10, 20, 90], [1, 0, 0, 5, 22, 200]],
... warehous_at_t0=[2, 1],
... machine_product_unit_times=[[[10, 50, 15, 100], [5, 20, 7, 35, 4, 50]],
... [[ 5, 24, 7, 80], [3, 21, 6, 50,]]])
>>> instance.name
'test2'
>>> simulation = Simulation(instance, PrintingListener())
>>> simulation.ctrl_run()
start
T=0: EVENT product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0: 2 units of product 0 in warehouse
T=0: EVENT product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0: 1 units of product 1 in warehouse
T=20: EVENT product=1, amount=0, in_warehouse=1, in_production=0, 1 pending demands
T=20: EVENT machine=1, 1 jobs queued
T=20: beginning to produce 9 units of product 1 on machine 1
T=22: EVENT product=0, amount=0, in_warehouse=2, in_production=0, 1 pending demands
T=22: EVENT machine=0, 1 jobs queued
T=22: beginning to produce 3 units of product 0 on machine 0
T=53: finished producing 3 units of product 0 on machine 0
T=62: finished producing 9 units of product 1 on machine 1
T=62: EVENT machine=1, 1 jobs queued
T=62: beginning to produce 3 units of product 0 on machine 1
T=62: EVENT machine=0, 1 jobs queued
T=62: beginning to produce 9 units of product 1 on machine 0
T=83: finished producing 3 units of product 0 on machine 1
T=83: EVENT product=0, amount=3, in_warehouse=2, in_production=0, 1 pending demands
T=83: demand 1 for 5 units of product 0 satisfied
T=83: 0 units of product 0 in warehouse
T=108: finished producing 9 units of product 1 on machine 0
T=108: EVENT product=1, amount=9, in_warehouse=1, in_production=0, 1 pending demands
T=108: demand 0 for 10 units of product 1 satisfied
T=108: 0 units of product 1 in warehouse
T=108: finished
>>> simulation.ctrl_reset()
>>> simulation.ctrl_run()
start
T=0: EVENT product=0, amount=2, in_warehouse=0, in_production=0, 0 pending demands
T=0: 2 units of product 0 in warehouse
T=0: EVENT product=1, amount=1, in_warehouse=0, in_production=0, 0 pending demands
T=0: 1 units of product 1 in warehouse
T=20: EVENT product=1, amount=0, in_warehouse=1, in_production=0, 1 pending demands
T=20: EVENT machine=1, 1 jobs queued
T=20: beginning to produce 9 units of product 1 on machine 1
T=22: EVENT product=0, amount=0, in_warehouse=2, in_production=0, 1 pending demands
T=22: EVENT machine=0, 1 jobs queued
T=22: beginning to produce 3 units of product 0 on machine 0
T=53: finished producing 3 units of product 0 on machine 0
T=62: finished producing 9 units of product 1 on machine 1
T=62: EVENT machine=1, 1 jobs queued
T=62: beginning to produce 3 units of product 0 on machine 1
T=62: EVENT machine=0, 1 jobs queued
T=62: beginning to produce 9 units of product 1 on machine 0
T=83: finished producing 3 units of product 0 on machine 1
T=83: EVENT product=0, amount=3, in_warehouse=2, in_production=0, 1 pending demands
T=83: demand 1 for 5 units of product 0 satisfied
T=83: 0 units of product 0 in warehouse
T=108: finished producing 9 units of product 1 on machine 0
T=108: EVENT product=1, amount=9, in_warehouse=1, in_production=0, 1 pending demands
T=108: demand 0 for 10 units of product 1 satisfied
T=108: 0 units of product 1 in warehouse
T=108: finished
objectobjectSimulation.event_machine().act_exec_job().Simulation.event_product().int) – the current system timeint) – the id of the productint) – the amount of the product that appearsint) – the amount of the product currently in the warehouseint) – the amounf of product currently under productiontuple[Demand, ...]) – the pending orders for the productListenerobjectact_exec_job().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 with act_take_from_warehouse().int) – the current system timeint) – the id of the productint) – the amount of the product that appearsint) – the amount of the product currently in the warehouseint) – the amounf of product currently under productiontuple[Demand, ...]) – the pending orders for the product