Manufacturing simulation using SimPy | by Juan Horgan | Towards Data Science

Hi there! In this tutorial we are going to build a guitar factory using SimPy! Even though this is a toy example, we are going to cover some pretty cool things that you can use for your own simulations. We are going to go throw the code here, but if you want to see the final version, it’s on my GitHub.
First of all, what is SimPy? In the documentation, they define it as: “SimPy is a process-based discrete-event simulation framework based on standard Python”. You can see the full documentation here, which I strongly recommend. There you can find not only the things you can do, but also a lot of simple but very helpful tutorials. If you don’t have SimPy installed, a simple pip will do the trick:
pip install simpy
We are going to build a guitar factory from scratch, starting from something very simple into a more complex system. In this example, we are building one type of guitar. The body and the neck are build separately, but from the same type of wood. Then they are pass to a painting area. After that, a body, a neck and the electronic components are combined, so the guitar is made.
Let’s have a look:
Let me explain that flowchart:
There are 2 main containers. Wood and Electronic. Those containers have an N amount of wood/electronic components that going to be use in the process.The body make get a piece of wood from the container and transform it into a body, which will be stored in the Body storage. The same thing happen with the neck builder, but from one piece of wood, he will get two necks. The necks are stored in the Neck storage.Painter pick necks and bodies, paint them, and store them in Body storage 2 and Neck storage 2.Assembler picks a body, a neck and one piece of electronics, and assemble the guitar, which will be store in the Dispatch container.After an N amount of guitars are made, store send someone to pick them up.When Wood or Electronic are bellow a certain level of raw material, a call to the Supplier is made. After T days, supplier arrives to the factory and refill the container with raw material.
There are 2 main containers. Wood and Electronic. Those containers have an N amount of wood/electronic components that going to be use in the process.
The body make get a piece of wood from the container and transform it into a body, which will be stored in the Body storage. The same thing happen with the neck builder, but from one piece of wood, he will get two necks. The necks are stored in the Neck storage.
Painter pick necks and bodies, paint them, and store them in Body storage 2 and Neck storage 2.
Assembler picks a body, a neck and one piece of electronics, and assemble the guitar, which will be store in the Dispatch container.
After an N amount of guitars are made, store send someone to pick them up.
When Wood or Electronic are bellow a certain level of raw material, a call to the Supplier is made. After T days, supplier arrives to the factory and refill the container with raw material.
Part 1: Simple model
Let’s start our model from the simplest form:
We start importing SimPy. After that we will create the class Guitar_Factory and add the first two containers. Notice that we set the capacity of our containers as the variables shown above. Of course, dispatch will start empty. If we run out of wood, process will not stop, but it won’t create any more guitars either. The same thing happen if dispatch is full. Note the env argument. This is simply the SimPy environment where this are happening. We will define this later in our code.
Now we will add the Body maker and the Neck maker:
We start creating our first two employees. The function take two arguments: The SimPy environment and the guitar_factory class (note that guitar_factory is different from the Guitar_Factory we define as our class in the previous Gist). What is happening there is pretty straight forward:
While the simulation is running, the maker will take one piece of wood (yield guitar_factory.wood.get(1)).
It will keep that wood piece for one time period (that’s what the env.timeout function does). This simulate the time it takes to transform a piece of wood into a guitar body/neck.
After that time unit (1 in our case) has pass, the maker will put that piece into a container named dispatch. Please note that from one piece of wood, the body maker will make one guitar body. Instead, the neck maker, will make two necks from one piece of wood. So, you have the flexibility to take the number of raw material you want, and deliver as much other thing as you want, simply by changing that numbers.
And finally:
Our simulation will run for 40 hours (8 hours per business day * 5 business days), which is defined in the until = total_time argument passed to the env.run function. The body_maker_process and neck_maker_process will create our “employees”. And finally, we added a print so we can know how much bodies and neck’s we’ve make. Note that we get that result with the guitar_factory.dispatch.level. That’s equivalent to say “return the amount of things that are in the dispatch container”:
If you want the full code of part 1, is here.
Now we are going to add paint and assembling to our model. For that purpose, we will add:
Pre paint and post paint containers, and the painter.
Electronic container and assembler.
Nothing new here. We add electronic, pre_paint and post_paint containers, and set their capacities.
As we’ve done with body and neck makers, we create the painter and the assembler. Note that painting takes 4 hours, but it works over 10 pieces at the same time, while assembling use 2 post paint pieces (1 body and 1 neck) plus 1 electronic piece, and deliver a guitar after an hour of work.
Then we add some prints, environment process creation and run the simulation! Again, part 2 full code is here.
Now we are going to add some very cool stuff. Fasten your seat belts!
Until now, we have set a fixed time for our employees activity. For example, we have said that assembler takes 1 hour to make a guitar. With that parameter, it will always takes one hour, and not a minute more and not a minute less. We all now that’s not true. Activities takes different times, even do they are repeated along time. Here we will asume that the distribution of time is normal, but you can use any distribution you want. Also note that we are defining how many employees of each kind we want. We will start this part by importing the random library and defining the parameters needed to make the changes:
Lets explain this:
num_body set the number of body makers we are going to use (in this case, 2)
mean_body its the mean time needed for a body maker to make a body out of a piece of wood.
std_body is the standard error.
Now we are going to make a change to the function that creates our employees:
It is simple to note that now, body_time will not always be 1. In fact it will be a random number taken from a normal distribution with (in this case) mean 1 and standard error 0.1. Also note the assembling_time: we are taking the maximum number between the random one and 1. In other words, we are saying that assembling a guitar will never take less than one hour.
We have to change the creating process of our employees, by creating a new function that allow us to create more than one employee for each kind:
Of course, this will be the same code for neck_maker_gen, paint_maker_gen and assembler_maker_gen. With that for loop we will be creating 2 body makers (remember that we define the variable num_body = 2). So, we are going to have 2 body makers, 1 neck maker, 1 painter and 4 assemblers.
Now we are going to create stock alarms and suppliers calls. This is how it will work:
Our alarm will be constantly monitoring the amount of stock (level) of a container.
If the current level is bellow a critical level that we’ll define, it will make a call to the supplier.
After a T amount of time, supplier will arrive to our factory and refill our container with an N amount of raw material.
First, we need to define our critical levels:
As we can see, wood supplier takes 2 business days to arrive. Then for critical level I define depends on the mean time it takes to create a body or a neck, the amount of body makers and neck makers, the 2 business days it takes to the supplier to arrive, and 1 margin day just to be sure. We do not want to be over stocked. Stock is money that just sit there, and that’s not good. But we don’t want to run out of stock either, because if that happens we are going to keep paying salaries for people that cannot work. The critical level is a trade off between that two things that you will need to define by your self. Each business if different.
Let’s add the alarm:
We made some changes in the __init__ function, by adding the wood_control and electronic_control processes. Before we take a look at the wood_stock_control, please notice that the electronic_stock_control function is not there. That function is pretty much the same as the wood_stock_control, so you can try to create it yourself. If you can’t or don’t want to, the complete code will be at the end of this part.
So, the first yield env.timeout(0) means that this process will start executing as soon as the simulation start. We can change that value (0) to, for example, an 8. That way, the process will start 8 time units later.
The while True, as we explain earlier, mean that the process will execute for all the time the simulation es running.
Then, it would check if the stock level is equal or minor than the critical level defined earlier. If stock es greater than that level, it would shut down for 1 time unit (see the else statement).
Of the stock level is equal or lower than the critical level, the print will be executed, informing that the level is N at a certain day and hour and the supplier call has been made.
After 2 days (16 hours), supplier arrives and it would refill the wood container with 300 units. That is what the yield self.wood.put(300) means.
Finally, the new stock level is printed and the alarm will be off for 1 day (yield env.timeout(8)).
We run this simulation for 5 days:
Part 3 complete code can be found here.
To be honest, until now we have been cheating somehow. After bodies and necks were made, they were store in the same container (prepaint) and we were treating them as the some thing. Now we are going to implement separe container for each piece, so we can treat them properly:
We already now how to make a container. Just notice two things here: the guitars_made variable, and the dispatch_control. Of course, we need to modify the body_maker and neck_maker processes:
Now we are storing the bodies in the body_pre_paint container. The same thing happens with the neck_maker. Painter also needs a modification:
We are now going to build a control process as we did on electronic or wood. This process will track the stock level of guitars ready to ship, and call the shop to let them now they can come to pick them up. For tracking purpose, we create a global variable call guitars_made, so we can store the number of guitars the shop has taken:
The first thing we do here is call the global variable guitars_made, with the statement global. If dispatch stock level is equal or higher than 50 guitars, we call the shop. After 4 hours, they come to pick the guitars and they take all the guitars available, not only the, for example, 50 that were ready when we call. So, when they take the guitars, we sum than level to our guitars_made variable with the statement guitars_made += self.dispatch.level. Then, the control process goes off for 8 hours. Assembler will need to be modified too, you can try to do it yourself, or take it from the full code.
Finally, add some prints and run the simulation:
And we are done! Of course, part 4 full code is in the link at the beginning. Thank you so much for reading my articule, hope you enjoy it and you could get something useful from it.
Until next time!