Simulating and Visualizing Real-Life Events in Python With SimPy - by Kevin Brown - Towards Data Science
Simulating and Visualizing Real-Life Events in Python With SimPy - by Kevin Brown - Towards Data Science
195 2
DES is a way to model real-life events using statistical functions, typically for
queues and resource usage with applications in health care, manufacturing,
logistics and others [3]. The end goal is to arrive at key operational metrics
such as resource usage and average wait times in order to evaluate and
optimize various real-life configurations. SIMUL8 has a video depicting how
emergency room wait times can be modelled [4], and MathWorks has a
number of educational videos to provide an overview of the topic [5], in
addition to a case study on automotive manufacturing [6]. The SimPy [7]
library provides support for describing and running DES models in Python.
Unlike a package such as SIMUL8, SimPy is not a complete graphical
environment for building, executing and reporting upon simulations;
however, it does provide the fundamental components to perform
simulations and output data for visualization and analysis.
This article will first walk through a scenario and show how to it can be
implemented in SimPy. It will then look at three different approaches to
visualizing the results: a Python-native solution (with Matplotlib [8] and
Tkinter [9]), an HTML5 canvas-based approach, and an interactive AR/VR
visualization. We will conclude by using our SimPy model to evaluate
alternative configurations.
The Scenario
For our demonstration, I will use an example from some of my previous
work: the entrance queue at an event. However, other examples that follow a
similar pattern could be a queue at a grocery store or a restaurant that takes
online orders, a movie theatre, a pharmacy, or a train station.
Each bus will contain 100 +/- 30 visitors determined using a normal
distribution (μ = 100, σ = 30)
Visitors will form groups of 2.25 +/– 0.5 people using a normal
distribution (μ = 2.25, σ = 0.5). We will round this to the closest whole
number
We’ll assume that a fixed ratio of 40% of visitors will need to purchase
tickets at the seller booths, another 40% will arrive with a ticket already
purchased online, and 20% will arrive with staff credentials
Visitors will take one minute on average to exit the bus and walk to the
seller booth (normal, μ = 1, σ = 0.25), and another half minute to walk
from the sellers to the scanners (normal, μ = 0.5, σ = 0.1). For those
skipping the sellers (tickets pre-purchased or staff with badges), we’ll
assume an average walk of 1.5 minutes (normal, μ = 1.5, σ = 0.35)
Visitors will select the shortest line when they arrive, where each line has
one seller or scanner
With that in mind, let’s start with the output and work backwards from there.
Image by Author
The graph on the left-hand side represents the number visitors arriving per
minute and the graphs on the right-hand side represent the average time the
visitors exiting the queue at that moment needed to wait before being
served.
To begin, let’s start with the parameters of the simulation. The variables that
will be most interesting to analyze are the number of seller lines
(SELLER_LINES) and the number of sellers per line (SELLERS_PER_LINE) as
well as their equivalents for the scanners (SCANNER_LINES and
SCANNERS_PER_LINE). Also, note the distinction between the two possible
queue/seller configurations: although the most prevalent configuration is to
have multiple distinct queues that a visitor will select and stay at until they’re
served, it has also become more mainstream in retail to see multiple sellers
for one single line (e.g., quick checkout lines at general merchandise big box
retailers).
1 BUS_ARRIVAL_MEAN = 3
2 BUS_OCCUPANCY_MEAN = 100
3 BUS_OCCUPANCY_STD = 30
4
5 PURCHASE_RATIO_MEAN = 0.4
6 PURCHASE_GROUP_SIZE_MEAN = 2.25
7 PURCHASE_GROUP_SIZE_STD = 0.50
8
9 TIME_TO_WALK_TO_SELLERS_MEAN = 1
10 TIME_TO_WALK_TO_SELLERS_STD = 0.25
11 TIME_TO_WALK_TO_SCANNERS_MEAN = 0.5
12 TIME_TO_WALK_TO_SCANNERS_STD = 0.1
13
14 SELLER_LINES = 6
15 SELLERS_PER_LINE = 1
16 SELLER_MEAN = 1
17 SELLER_STD = 0.2
18
19 SCANNER_LINES = 4
20 SCANNERS_PER_LINE = 1
21 SCANNER_MEAN = 1 / 20
22 SCANNER_STD = 0.01
With the configuration complete, let’s start the SimPy process by first
creating an “environment”, all the queues (Resources), and run the
simulation (in this case, until the 60-minute mark).
1 env = simpy.rt.RealtimeEnvironment(factor = 0.1, strict = False)
2
3 seller_lines = [ simpy.Resource(env, capacity = SELLERS_PER_LINE) for _ in range(SELLER_LINES) ]
4 scanner_lines = [ simpy.Resource(env, capacity = SCANNERS_PER_LINE) for _ in range(SCANNER_LINES)
5
6 env.process(bus_arrival(env, seller_lines, scanner_lines))
7
8 env.run(until = 60)
Since this is the top-level event function, we see that all the work in this
function is taking place within an endless while loop. Within the loop, we are
“yielding” our wait time with env.timeout(). SimPy makes extensive use of
generator functions which will return an iterator of the yielded values. More
information on Python generators can be found in [10].
At the end of the loop, we are dispatching one of two events depending on
whether we’re going directly to the scanners or if we’ve randomly decided
that this group needs to purchase tickets first. Note that we are not yielding
to these processes as that would instruct SimPy to complete each of these
operations in sequence; instead, all those visitors exiting the bus will be
proceeding to the queues concurrently.
Note that the people_ids list is being used is so that each person is assigned a
unique ID for visualization purposes. We are using the people_ids list as a
queue of people remaining to be processed; as visitors are dispatched to
their destinations, they are removed from the people_ids queue.
1 class ClockAndData:
2 def __init__(self, canvas, x1, y1, x2, y2, time):
3 # Draw the initial state of the clock and data on the canvas
4 self.canvas.update()
5
6 def tick(self, time):
7 # Re-draw the the clock and data fields on the canvas. Also update the Matplotlib charts
8
9 # ...
10
11 clock = ClockAndData(canvas, 1100, 320, 1290, 400, 0)
12
13 # ...
14
15 def create_clock(env):
16 while True:
17 yield env.timeout(0.1)
18 clock.tick(env.now)
19
20 # ...
21
22 env.process(create_clock(env))
The visualization of the users moving to and from seller and scanner queues
is represented using standard Tkinter logic. We created the QueueGraphics
class to abstract the common parts of the seller and scanner queues.
Methods from this class are coded into the SimPy event functions described
in the previous section to update the canvas (e.g., sellers.add_to_line(1)
where 1 is the seller number, and sellers.remove_from_line(1)). As future
work, we could use an event handler at key points in the process so the
SimPy simulation logic is not tightly coupled to the UI logic specific to this
analysis.
Let’s begin with the case demonstrated in the animations above: six sellers
and four scanners with one seller and scanner per line (6/4). After 60
minutes, we see the average seller wait was 1.8 minutes and the average
scanner wait was 0.1 minutes. From the chart below, we see that the seller
time peaks at almost a 6-minute wait.
We can see that the sellers are consistently backed up (although 3.3 minutes
may not be too unreasonable); so, let’s see what happens if we add an extra
four sellers bumping the total up to 10.
As expected, the average seller wait is reduced to 0.7 minutes and the
maximum wait is reduced to be just over three minutes.
Now, let’s say that by reducing the price of online tickets, we’re able to boost
the number of people arriving with a ticket by 35%. Initially, we assumed
that 40% of all visitors need to buy a ticket, 40% have pre-purchased online,
and 20% are staff and vendors entering with credentials. Therefore, with
35% more people arriving with tickets, we reduce the number of people
needing to purchase down to 26%. Let’s simulate this with our initial 6/4
configuration.
In this scenario, the average seller wait is reduced to 1.0 minutes with a
maximum wait of just over 4-minutes. In this circumstance, increasing
online sales by 35% had a similar effect to adding more seller queues to the
average wait; if waiting time is the metric that we were most interested in
reducing, then at that point we could consider which of these two options
would have a stronger business case.
Open in app
the queues is only one component of the analysis as the percentage of the Sign up Sign in
time that the sellers and scanners are sitting idle should also be considered
Search
in arriving at the most optimal solution. Additionally, it would also be Write
References
[1] https://www.simul8.com/
[2] https://www.mathworks.com/solutions/discrete-event-simulation.html
[3] https://en.wikipedia.org/wiki/Discrete-event_simulation
[4] https://www.simul8.com/videos/
[5] https://www.mathworks.com/videos/series/understanding-discrete-event-
simulation.html
[6] https://www.mathworks.com/company/newsletters/articles/optimizing-
automotive-manufacturing-processes-with-discrete-event-simulation.html
[7] https://simpy.readthedocs.io/en/latest/
[8] https://matplotlib.org/
[9] https://docs.python.org/3/library/tkinter.html
[10] https://wiki.python.org/moin/Generators
[11] https://reactjs.org/
[12] https://aframe.io/
(This post has been adapted from two previously published articles at:
https://dattivo.com/simulating-real-life-events-in-python-with-simpy/ and
https://dattivo.com/3d-visualization-of-a-simulated-real-life-event-in-virtual-
reality/)
100 1 1.7K 26
Cristian Leo in Towards Data Science Kevin Brown in Management Matters
16 min read · Jan 31, 2024 8 min read · Jun 15, 2021
2.5K 20 21
See all from Kevin Brown See all from Towards Data Science
66 1 12.7K 151
Lists
1.7K 26 4.6K 55
46 76 1