Exploring Python's simpy module: Real Life Simulation of a Movie Theatre

0 likes

Yesterday, I was scrolling through articles on Real Python when I came across this particular one: SimPy: Simulating Real-World Processes With Python. As I am very much interested in how mathematics and computer science can be used to model and solve real world problems, I had to try this one out.

The Code

Here is the code in full

#20/02/21
import random
import statistics
import simpy
import tkinter as tk
import matplotlib.pyplot as plt
import time

wait_times = []

class Theater(object):
    def __init__(self, env, num_cashiers, num_servers, num_ushers):
        self.env = env
        self.cashier = simpy.Resource(env, num_cashiers)
        self.server = simpy.Resource(env, num_servers)
        self.usher = simpy.Resource(env, num_ushers)

    def purchase_ticket(self, moviegoer):
        yield self.env.timeout(random.randint(1, 3)) #takes anywhere from 1 - 3 minutes to purchase a ticket

    def check_ticket(self, moviegoer):
        yield self.env.timeout(3 / 60) #3 seconds

    def sell_food(self, moviegoer):
        yield self.env.timeout(random.randint(1, 5)) #takes anywhere from 1 - 5 minutes to purchase a ticket

def go_to_movies(env, moviegoer, theater, food_prob):
    # Moviegoer arrives at the theater
    arrival_time = env.now

    with theater.cashier.request() as request:
        yield request
        yield env.process(theater.purchase_ticket(moviegoer))

    with theater.usher.request() as request:
        yield request
        yield env.process(theater.check_ticket(moviegoer))

    #if random.choice([True, False]):
    if random.random() < food_prob:
        with theater.server.request() as request:
            yield request
            yield env.process(theater.sell_food(moviegoer))

    # Moviegoer heads into the theater
    wait_times.append(env.now - arrival_time)


def run_theater(env, num_cashiers, num_servers, num_ushers, food_probability):
    theater = Theater(env, num_cashiers, num_servers, num_ushers)

    for moviegoer in range(3): #3 people will already be in line when the theatre opens 
        env.process(go_to_movies(env, moviegoer, theater, food_probability))

    while True:
        yield env.timeout(12/60)  # Wait 12 seconds before generating a new person

        moviegoer += 1
        env.process(go_to_movies(env, moviegoer, theater, food_probability))

def main():
  # Setup
  random.seed(time.time())
  all_waits = []
  for i in range(11):
    global wait_times
    wait_times = []
    num_cashiers, num_servers, num_ushers, get_food_probability = int(num_cashiers_entry.get()), int(num_servers_entry.get()), int(num_ushers_entry.get()), i/10
    print(get_food_probability)

    # Run the simulation
    env = simpy.Environment()
    env.process(run_theater(env, num_cashiers, num_servers, num_ushers, get_food_probability))
    env.run(until=90) #run the simulation for 90 minutes

    # View the results
    average_wait = statistics.mean(wait_times)
    minutes, frac_minutes = divmod(average_wait, 1) #divide average wait time in minutes by 1 to separate mins and secs
    seconds = frac_minutes * 60 #convert from decimal secs to 60 seconds
    mins, secs = round(minutes), round(seconds)
    print(
        "Running simulation...",
        f"\nThe average wait time is {mins} minutes and {secs} seconds.",
    )

    all_waits.append(average_wait)

  print(all_waits)
  plt.plot([i/10 for i in range(11)], all_waits, color="g", label=f"{num_cashiers} cashiers, {num_servers} servers and {num_ushers} ushers")
  plt.xlabel("Food probability")
  plt.xticks([i/10 for i in range(11)])
  plt.ylabel("Average wait time")
  plt.title(f"Relationship between food probability and average movie wait time")
  plt.legend()
  plt.show()


if __name__ == '__main__':
    window=tk.Tk()
    window.title("Movie Average Wait Time Simulation")
    window.geometry("400x400")

    for i in range(3):
	    window.columnconfigure(index=i, weight=1)
	    window.rowconfigure(index=i,weight=1)

    ushers_label = tk.Label(window, text="Number of ushers", fg="white", bg="black").grid(row=0, column=0, columnspan=2, sticky="ew")
    num_ushers_entry = tk.Entry(fg="gray", bg="white", width=20)
    num_ushers_entry.grid(row=0, column=2, columnspan=2, sticky="ew")

    servers_label = tk.Label(window, text="Number of servers", fg="white", bg="black").grid(row=1, column=0, columnspan=2, sticky="ew")
    num_servers_entry = tk.Entry(fg="gray", bg="white", width=20)
    num_servers_entry.grid(row=1, column=2, columnspan=2, sticky="ew")

    cashiers_label = tk.Label(window, text="Number of cashiers", fg="white", bg="black").grid(row=2, column=0, columnspan=2, sticky="ew")
    num_cashiers_entry = tk.Entry(fg="gray", bg="white", width=20)
    num_cashiers_entry.grid(row=2, column=2, columnspan=2, sticky="ew")

    start_button = tk.Button(
        text="Start Simulation",
        width=15,
        height=3,
        bg="white",
        fg="black",
        command=main,
    )
    start_button.grid(row=3, column=0, columnspan=2, sticky="ew") #columns 1 and 2, the middle 2 columns

    window.mainloop()
As you can see, I took Real Python's programme and added the food probability system, a graph and a rather basic graphical user interface using Tkinter. In the simulation, as you can see in the function run_theatre, it is assumed that 3 people are already at the theatre before it opens. After that, a new moviegoer arrives every 12 seconds. Then, the go_to_movies function is called. Each moviegoer must request to use a cashier, usher and a server if they decided to order food. If there is no worker available, then they must wait until someone becomes free: this is conveyed via yield request. As you can see in the Theatre class, purchasing a ticket (cashier) has a randomised waiting time of 1 to 3 minutes, checking the ticket (usher) takes only 3 seconds, and selling food (server) takes anywhere from 1 to 5 minutes. After that, we go back to main() where we calculate the average time and plot a graph. The simulation is supposed to run for 90 minutes, but don't worry, it only takes about 5 seconds when actually running the programme. A screenshot of the GUI

Results & Conclusions

I decided to change my programme from the Real Python example by focusing on how the probability of a moviegoer purchasing food affects waiting time. Originally, it was set to if random.choice([True, False]). This means that the probability of getting food is always 50%. However, this is not so realistic depending on certain times of the year. Perhaps people are more inclined to purchase food and spend money during the holidays, and less so during workdays. Perhaps the coronavirus is deterring people away from eating food in the public. Whatever the case, it is always interesting to observe what happens as certain parameters are changed.

With just 1 usher, 1 server and 1 cashier, the result is quite a mess. Resulting graph with 1 usher, 1 server and 1 cashier With 5 of each, it gets quite better. Resulting graph with 5 ushers, 5 servers and 5 cashiesr With 10, you start thinking: is it supposed to be a quadratic? Resulting graph with 10 ushers, 10 servers and 10 cashiers With 100, you realise that it's just a linear relationship after all. Resulting graph with 100 ushers, 100 servers and 100 cashiers With 10000 (yes this is crazy), it's confirmed. Resulting graph with 10000 ushers, 10000 servers and 10000 cashiers As the number of workers increases, increasing the probability of a moviegoer getting food only linearly increases the total wait time. Moreover, as you can see in the graphs above, there is a limit to the minimum wait time: no matter how many workers you employ, the minimum is always around 5 minutes if everyone buys food.

Takeaways

By using a computer simulation to model real life situations such as these, producers can improve their allocative efficiency and optimise the use of their resources. In this example, the constraint, based on a study of some sort, was that the wait time had to be less than 10 minutes in order for consumer satisfaction to be acceptable (who wants to wait more than 10 minutes to watch a movie anyway?). The manager of this movie theatre can then find the minimum number of ushers, cashiers and servers they would need to employ to meet the constraint. An extension then, would be to implement a cost function to let the computer calculate the optimal combination of workers. Of course, some jobs are more costly for the producer (in the form of salaries) than others: perhaps cashiers require more skill than ushers, and therefore are paid more. By attaching a cost function to each worker, the computer can then run through the different possibilities and find the combination that meets the constraint whilst minimising cost. But this is something for another time.


Leave a Comment
/200 Characters