# ... code setting up the inputs and defining the classes
= Trial().run_trial()
results_df
st.dataframe(results_df)
48 Adding a run button
48.2 Adding a spinner
For long-running calculations, it can be helpful to provide a spinner that indicates clearly that something is happening.
We just make a minor additional tweak to include a with
clause referring to st.spinner
.
# A user must press a streamlit button to run the model
= st.button("Run simulation")
button_run_pressed
if button_run_pressed:
with st.spinner('Simulating the system...'):
= Trial().run_trial()
results_df
st.dataframe(results_df)
Warning
If deploying your app using stlite, you will need to modify the spinner code very slightly.
import asyncio
### ... additional code for app
# A user must press a streamlit button to run the model
= st.button("Run simulation")
button_run_pressed
if button_run_pressed:
with st.spinner('Simulating the system...'):
await asyncio.sleep(0.1)
= Trial().run_trial()
results_df
st.dataframe(results_df)
48.2.1 The full code
Click here to view the full code
import streamlit as st
import simpy
import random
import pandas as pd
"Simple One-Step DES")
st.title(
"In this discrete event simulation, patients arrive")
st.write(
= st.slider("What is the average length of time between patients arriving?",
patient_iat_slider =1, max_value=30, value=5)
min_value
= st.slider("What is the mean length of time (in minutes) for a consultation?",
patient_consult_slider = 3, max_value=60, value=6)
min_value
= st.slider("What is the number of nurses in the system?",
num_nurses_slider =1, max_value=10, value=1)
min_value
= st.number_input("How long should the simulation run for (minutes)?",
sim_duration_input =60, max_value=480, value=480) ## CHANGED - LONGER DEFAULT
min_value
= st.number_input("How many runs of the simulation should be done?",
num_runs_input =1, max_value=100, value=100) ## CHANGED - HIGHER DEFAULT
min_value
# Class to store global parameter values
class g:
= patient_iat_slider
patient_inter = patient_consult_slider
mean_n_consult_time = num_nurses_slider
number_of_nurses = sim_duration_input
sim_duration = num_runs_input
number_of_runs
# Class representing patients coming in to the clinic.
# Here, patients have two attributes that they carry with them -
# their ID, and the amount of time they spent queuing for the nurse.
# The ID is passed in when a new patient is created.
class Patient:
def __init__(self, p_id):
self.id = p_id
self.q_time_nurse = 0
# Class representing our model of the clinic.
class Model:
# Constructor to set up the model for a run. We pass in a run number when
# we create a new model.
def __init__(self, run_number):
# Create a SimPy environment in which everything will live
self.env = simpy.Environment()
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
# Create a SimPy resource to represent a nurse, that will live in the
# environment created above. The number of this resource we have is
# specified by the capacity, and we grab this value from our g class.
self.nurse = simpy.Resource(self.env, capacity=g.number_of_nurses)
# Store the passed in run number
self.run_number = run_number
# Create a new Pandas DataFrame that will store some results against
# the patient ID (which we'll use as the index).
self.results_df = pd.DataFrame()
self.results_df["Patient ID"] = [1]
self.results_df["Q Time Nurse"] = [0.0]
self.results_df["Time with Nurse"] = [0.0]
self.results_df.set_index("Patient ID", inplace=True)
# Create an attribute to store the mean queuing time for the nurse
# across this run of the model
self.mean_q_time_nurse = 0
# A generator function that represents the DES generator for patient
# arrivals
def generator_patient_arrivals(self):
# We use an infinite loop here to keep doing this indefinitely whilst
# the simulation runs
while True:
# Increment the patient counter by 1 (this means our first patient
# will have an ID of 1)
self.patient_counter += 1
# Create a new patient - an instance of the Patient Class we
# defined above. Remember, we pass in the ID when creating a
# patient - so here we pass the patient counter to use as the ID.
= Patient(self.patient_counter)
p
# Tell SimPy to start up the attend_clinic generator function with
# this patient (the generator function that will model the
# patient's journey through the system)
self.env.process(self.attend_clinic(p))
# Randomly sample the time to the next patient arriving. Here, we
# sample from an exponential distribution (common for inter-arrival
# times), and pass in a lambda value of 1 / mean. The mean
# inter-arrival time is stored in the g class.
= random.expovariate(1.0 / g.patient_inter)
sampled_inter
# Freeze this instance of this function in place until the
# inter-arrival time we sampled above has elapsed.
yield self.env.timeout(sampled_inter)
# A generator function that represents the pathway for a patient going
# through the clinic.
def attend_clinic(self, patient):
# Record the time the patient started queuing for a nurse
= self.env.now
start_q_nurse
# Request a nurse resource, and do all of the following block of code with
# that nurse resource held in place (and therefore not usable by another patient)
with self.nurse.request() as req:
# Freeze the function until the request for a nurse can be met.
# The patient is currently queuing.
yield req
# When we get to this bit of code, control has been passed back to the generator
# function, and therefore the request for a nurse has been met.
# We now have the nurse, and have stopped queuing, so we can record the current time
# as the time we finished queuing.
= self.env.now
end_q_nurse
# Calculate the time this patient was queuing for the nurse, and
# record it in the patient's attribute for this.
= end_q_nurse - start_q_nurse
patient.q_time_nurse
# Now we'll randomly sample the time this patient with the nurse.
# Here, we use an Exponential distribution for simplicity
# As with sampling the inter-arrival times, we grab the mean from the g class,
# and pass in 1 / mean as the lambda value.
= random.expovariate(1.0 /
sampled_nurse_act_time
g.mean_n_consult_time)
# Here we'll store the queuing time for the nurse and the sampled
# time to spend with the nurse in the results DataFrame against the
# ID for this patient.
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
# Freeze this function in place for the activity time we sampled
# above. This is the patient spending time with the nurse.
yield self.env.timeout(sampled_nurse_act_time)
# When the time above elapses, the generator function will return
# here. As there's nothing more that we've written, the function
# will simply end. This is a sink. We could choose to add
# something here if we wanted to record something - e.g. a counter
# for number of patients that left, recording something about the
# patients that left at a particular sink etc.
def calculate_run_results(self):
# Take the mean of the queuing times for the nurse across patients in
# this run of the model.
self.mean_q_time_nurse = self.results_df["Q Time Nurse"].mean()
# The run method starts up the DES entity generators, runs the simulation,
# and in turns calls anything we need to generate results for the run
def run(self):
# Start up our DES entity generators that create new patients.
# We've only got one in this model, but we'd need
# to do this for each one if we had multiple generators.
self.env.process(self.generator_patient_arrivals())
# Run the model for the duration specified in g class
self.env.run(until=g.sim_duration)
# Now the simulation run has finished, call the method that calculates
# run results
self.calculate_run_results()
# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
# The constructor sets up a pandas dataframe that will store the key
# results from each run (just the mean queuing time for the nurse here)
# against run number, with run number as the index.
def __init__(self):
self.df_trial_results = pd.DataFrame()
self.df_trial_results["Run Number"] = [0]
self.df_trial_results["Mean Q Time Nurse"] = [0.0]
self.df_trial_results.set_index("Run Number", inplace=True)
# Method to run a trial
def run_trial(self):
# Run the simulation for the number of runs specified in g class.
# For each run, we create a new instance of the Model class and call its
# run method. Once the run has completed, we grab out the stored run results
# (just mean queuing time here) and store it against the run number
# in the trial results dataframe.
for run in range(g.number_of_runs):
= Model(run)
my_model
my_model.run()
self.df_trial_results.loc[run] = [my_model.mean_q_time_nurse]
# Once the trial (ie all runs) has completed, return the final results
return self.df_trial_results
############
# NEW #
############
# A user must press a streamlit button to run the model
= st.button("Run simulation")
button_run_pressed
if button_run_pressed:
with st.spinner('Simulating the system...'):
= Trial().run_trial()
results_df
st.dataframe(results_df)############
# END NEW #
############