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.
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.
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.
With 5 of each, it gets quite better.
With 10, you start thinking: is it supposed to be a quadratic?
With 100, you realise that it's just a linear relationship after all.
With 10000 (yes this is crazy), it's confirmed.
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.
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.