Simulating and Visualizing Real-Life Events in Python with SimPy
By: Kevin Brown
Tags: Discrete Event Simulation, A Frame, Data Science, Matplotlib, Python

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.

SimPy Simulation Set Up

The repository with the complete runnable source can be found at https://github.com/dattivo/gate-simulation with the following snippets lifted from the simpy example.py file. In this section we will step through the SimPy-specific set up; however, note that the parts that connect to Tkinter for visualization are omitted to focus on the DES features of SimPy.

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).

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).

Note that we are creating a RealtimeEnvironment which is intended for running a simulation in near real-time, particularly for our intentions of visualizing this as it runs. With the environment set up, we generate our seller and scanner line resources (queues) that we will then in turn pass to our “master event” of the bus arriving. The env.process() command will begin the process as described in the bus_arrival() function depicted below. This function is the top-level event from which all other events are dispatched. It simulates a bus arriving every BUS_ARRIVAL_MEAN minutes with BUS_OCCUPANCY_MEAN people on board and then triggers the selling and scanning processes accordingly.

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.

The purchasing_customer() function simulates three key events: walking to the line, waiting in line, and then passing control to the scanning_customer() event (the same function that is called by bus_arrival() for those bypassing the sellers and going straight to the scanners). This function picks its line based on what is shortest at the time of selection.

Finally, we need to implement the behaviour for the scanning_customer(). This is very similar to the purchasing_customer() function with one key difference: although visitors may arrive and walk together in groups, each person must have their ticket scanned individually. Consequently, you will see the scan timeout repeated for each scanned customer.

We pass the walk duration and standard deviation to the scanning_customer() function since those values will vary depending on whether the visitors walked directly to the scanners or if they stopped at the sellers first.

Visualizing the Data using Tkinter (Native Python UI)

In order to visualize the data, we added a few global lists and dictionaries to track key metrics. For example, the arrivals dictionary tracks the number of arrivals by minute and the seller_waits and scan_waits dictionaries map the minute of the simulation to a list of waits times for those exiting the queues in those minutes. There is also an event_log list that we will use in the HTML5 Canvas animation in the next section. As key events take place (e.g., a visitor exiting a queue), the functions under the ANALYTICAL_GLOBALS heading in simpy example.py file are called to keep these dictionaries and lists up to date.

We used an ancillary SimPy event to send a tick event to the UI in order to update a clock, update the current wait averages and redraw the Matplotlib charts. The complete code can be found in the GitHub repository (https://github.com/dattivo/gate-simulation/blob/master/simpy%20example.py); however, the following snippet provides a skeleton view of how these updates are dispatched from SimPy.

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.

Animating the Data Using HTML5 Canvas

As an alternate visualization, we wanted to export the events from the SimPy simulation and pull them into a simple HTML5 web application to visualize the scenario on a 2D canvas. We accomplished this by appending to an event_log list as SimPy events take place. In particular, the bus arrival, walk to seller, wait in seller line, buy tickets, walk to scanner, wait in scanner line, and scan tickets events are each logged as individual dictionaries that are then exported to JSON at the end of the simulation. You can see some sample outputs of this here: https://github.com/dattivo/gate-simulation/tree/master/output

We developed a quick proof-of-concept to show how these events can be translated into a 2D animation which you can experiment with at https://dattivo.github.io/gate-simulation/visualize.html. You can see the source code for the animation logic in https://github.com/dattivo/gate-simulation/blob/master/visualize.ts.