|
| 1 | +import streamlit as st |
| 2 | +import simpy |
| 3 | +import numpy as np |
| 4 | +import pandas as pd |
| 5 | + |
| 6 | +class Experiment: |
| 7 | + def __init__(self, |
| 8 | + interarrival_means=[22.72, 26.0, 37.0, 47.2, 575.0, 17.91], |
| 9 | + stay_distributions=[(128.79, 267.51), (177.89, 276.54), (140.15, 218.02), (212.86, 457.67), (87.53, 108.67), 57.34], |
| 10 | + elective_treatment_mean=57.34, |
| 11 | + num_critical_care_beds=24, |
| 12 | + intensive_cleaning_duration=5, |
| 13 | + warm_up_period=30*24, |
| 14 | + results_collection_period=12*30*24, |
| 15 | + trace=False, |
| 16 | + random_number_set=0): |
| 17 | + self.interarrival_means = interarrival_means |
| 18 | + self.stay_distributions = stay_distributions |
| 19 | + self.elective_treatment_mean = elective_treatment_mean |
| 20 | + self.num_critical_care_beds = num_critical_care_beds |
| 21 | + self.intensive_cleaning_duration = intensive_cleaning_duration |
| 22 | + self.warm_up_period = warm_up_period |
| 23 | + self.results_collection_period = results_collection_period |
| 24 | + self.total_treatment_time = 0 |
| 25 | + self.cancelled_elective_count = 0 |
| 26 | + self.mean_waiting_time_unplanned = 0 |
| 27 | + self.total_unplanned_admissions = 0 |
| 28 | + self.bed_occupancy = 0 |
| 29 | + self.trace = trace |
| 30 | + self.patient_count = 0 |
| 31 | + self.random_number_set = random_number_set |
| 32 | + self.streams = [] |
| 33 | + |
| 34 | + self.setup_streams(random_number_set) |
| 35 | + |
| 36 | + def reset_kpi(self): |
| 37 | + self.total_treatment_time = 0 |
| 38 | + self.cancelled_elective_count = 0 |
| 39 | + self.mean_waiting_time_unplanned = 0 |
| 40 | + self.total_unplanned_admissions = 0 |
| 41 | + self.bed_occupancy = 0 |
| 42 | + self.patient_count = 0 |
| 43 | + |
| 44 | + def setup_streams(self, random_number_set): |
| 45 | + self.streams = [] |
| 46 | + rng = np.random.default_rng(random_number_set) |
| 47 | + seeds = rng.integers(0, np.iinfo(np.int64).max, size=12) |
| 48 | + for seed in seeds: |
| 49 | + self.streams.append(np.random.default_rng(seed)) |
| 50 | + |
| 51 | +class CCUModel: |
| 52 | + def __init__(self, env, experiment): |
| 53 | + self.env = env |
| 54 | + self.experiment = experiment |
| 55 | + self.patient_count = 0 |
| 56 | + self.critical_care_beds = simpy.Resource(env, capacity=experiment.num_critical_care_beds) |
| 57 | + |
| 58 | + def patient_arrival_AE(self): |
| 59 | + while True: |
| 60 | + yield self.env.timeout(self.experiment.streams[0].exponential(self.experiment.interarrival_means[0])) |
| 61 | + self.patient_count += 1 |
| 62 | + if self.experiment.trace: |
| 63 | + print(f"Patient {self.patient_count} arrived from Accident and Emergency at time {self.env.now}") |
| 64 | + self.env.process(self.unplanned_admission(self.experiment.stay_distributions[0])) |
| 65 | + |
| 66 | + def patient_arrival_wards(self): |
| 67 | + while True: |
| 68 | + yield self.env.timeout(self.experiment.streams[1].exponential(self.experiment.interarrival_means[1])) |
| 69 | + self.patient_count += 1 |
| 70 | + if self.experiment.trace: |
| 71 | + print(f"Patient {self.patient_count} arrived from the Wards at time {self.env.now}") |
| 72 | + self.env.process(self.unplanned_admission(self.experiment.stay_distributions[1])) |
| 73 | + |
| 74 | + def patient_arrival_surgery(self): |
| 75 | + while True: |
| 76 | + yield self.env.timeout(self.experiment.streams[2].exponential(self.experiment.interarrival_means[2])) |
| 77 | + self.patient_count += 1 |
| 78 | + if self.experiment.trace: |
| 79 | + print(f"Patient {self.patient_count} arrived from Emergency surgery at time {self.env.now}") |
| 80 | + self.env.process(self.unplanned_admission(self.experiment.stay_distributions[2])) |
| 81 | + |
| 82 | + def patient_arrival_other_hospitals(self): |
| 83 | + while True: |
| 84 | + yield self.env.timeout(self.experiment.streams[3].exponential(self.experiment.interarrival_means[3])) |
| 85 | + self.patient_count += 1 |
| 86 | + if self.experiment.trace: |
| 87 | + print(f"Patient {self.patient_count} arrived from other hospitals at time {self.env.now}") |
| 88 | + self.env.process(self.unplanned_admission(self.experiment.stay_distributions[3])) |
| 89 | + |
| 90 | + def patient_arrival_X_ray(self): |
| 91 | + while True: |
| 92 | + yield self.env.timeout(self.experiment.streams[4].exponential(self.experiment.interarrival_means[4])) |
| 93 | + self.patient_count += 1 |
| 94 | + if self.experiment.trace: |
| 95 | + print(f"Patient {self.patient_count} arrived from the X-Ray department at time {self.env.now}") |
| 96 | + self.env.process(self.unplanned_admission(self.experiment.stay_distributions[4])) |
| 97 | + |
| 98 | + def patient_arrival_elective_surgery(self): |
| 99 | + while True: |
| 100 | + yield self.env.timeout(self.experiment.streams[5].normal(self.experiment.interarrival_means[5], 3.16)) |
| 101 | + self.patient_count += 1 |
| 102 | + if self.experiment.trace: |
| 103 | + print(f"Elective surgery patient {self.patient_count} arrived at time {self.env.now}") |
| 104 | + if len(self.critical_care_beds.users) == self.critical_care_beds.capacity: |
| 105 | + if self.experiment.trace: |
| 106 | + print(f"Elective surgery for patient {self.patient_count} cancelled due to no available critical care beds at time {self.env.now}") |
| 107 | + if self.env.now > self.experiment.warm_up_period: |
| 108 | + self.experiment.cancelled_elective_count += 1 |
| 109 | + else: |
| 110 | + self.env.process(self.elective_surgery_process(self.experiment.elective_treatment_mean)) |
| 111 | + |
| 112 | + def unplanned_admission(self, stay_distribution): |
| 113 | + arrival_time = self.env.now |
| 114 | + with self.critical_care_beds.request() as req: |
| 115 | + yield req |
| 116 | + wait_time = self.env.now - arrival_time |
| 117 | + if self.experiment.trace: |
| 118 | + print(f"Patient {self.patient_count} admitted to critical care bed at time {self.env.now}") |
| 119 | + treatment_time = self.experiment.streams[6].lognormal(np.log(stay_distribution[0]) - 0.5 * np.log(1 + (stay_distribution[1] / stay_distribution[0])**2), |
| 120 | + np.sqrt(np.log(1 + (stay_distribution[1] / stay_distribution[0])**2))) |
| 121 | + yield self.env.timeout(treatment_time) |
| 122 | + if self.experiment.trace: |
| 123 | + print(f"Patient {self.patient_count} discharged from critical care bed at time {self.env.now}") |
| 124 | + if self.env.now > self.experiment.warm_up_period: |
| 125 | + self.experiment.total_treatment_time += treatment_time |
| 126 | + self.experiment.mean_waiting_time_unplanned += wait_time |
| 127 | + self.experiment.total_unplanned_admissions += 1 |
| 128 | + yield self.env.timeout(self.experiment.intensive_cleaning_duration) |
| 129 | + if self.experiment.trace: |
| 130 | + print(f"Critical care bed is available for next patient at time {self.env.now}") |
| 131 | + |
| 132 | + def elective_surgery_process(self, treatment_mean): |
| 133 | + with self.critical_care_beds.request() as req: |
| 134 | + yield req |
| 135 | + if self.experiment.trace: |
| 136 | + print(f"Elective surgery patient {self.patient_count} admitted to critical care bed at time {self.env.now}") |
| 137 | + treatment_time = self.experiment.streams[7].exponential(treatment_mean) |
| 138 | + yield self.env.timeout(treatment_time) |
| 139 | + if self.experiment.trace: |
| 140 | + print(f"Elective surgery patient {self.patient_count} discharged from critical care bed at time {self.env.now}") |
| 141 | + if self.env.now > self.experiment.warm_up_period: |
| 142 | + self.experiment.total_treatment_time += treatment_time |
| 143 | + yield self.env.timeout(self.experiment.intensive_cleaning_duration) |
| 144 | + if self.experiment.trace: |
| 145 | + print(f"Critical care bed is available for next patient at time {self.env.now}") |
| 146 | + |
| 147 | + def warmup_complete(self): |
| 148 | + yield self.env.timeout(self.experiment.warm_up_period) |
| 149 | + self.patient_count = 0 |
| 150 | + if self.experiment.trace: |
| 151 | + print("Warm-up complete") |
| 152 | + |
| 153 | + def run(self): |
| 154 | + self.env.process(self.patient_arrival_AE()) |
| 155 | + self.env.process(self.patient_arrival_wards()) |
| 156 | + self.env.process(self.patient_arrival_surgery()) |
| 157 | + self.env.process(self.patient_arrival_other_hospitals()) |
| 158 | + self.env.process(self.patient_arrival_X_ray()) |
| 159 | + self.env.process(self.patient_arrival_elective_surgery()) |
| 160 | + self.env.process(self.warmup_complete()) |
| 161 | + self.env.run(until=self.experiment.results_collection_period + self.experiment.warm_up_period) |
| 162 | + if self.env.now > self.experiment.warm_up_period: |
| 163 | + mean_waiting_time_unplanned = self.experiment.mean_waiting_time_unplanned / self.experiment.total_unplanned_admissions |
| 164 | + bed_utilization = self.experiment.total_treatment_time / (self.experiment.num_critical_care_beds * self.experiment.results_collection_period) |
| 165 | + bed_occupancy = bed_utilization * self.experiment.num_critical_care_beds |
| 166 | + performance_measures = pd.DataFrame({'Cancelled Elective Operations': [self.experiment.cancelled_elective_count], 'Bed Utilization': [bed_utilization], 'Mean Waiting Time Unplanned': [mean_waiting_time_unplanned], 'Bed Occupancy': [bed_occupancy], 'Patient Count': [self.patient_count]}) |
| 167 | + return performance_measures |
| 168 | + |
| 169 | + |
| 170 | +import pandas as pd |
| 171 | + |
| 172 | +def multiple_replications(experiment, num_replications=5): |
| 173 | + all_results = [] |
| 174 | + |
| 175 | + for i in range(num_replications): |
| 176 | + experiment.setup_streams(i) # Call the setup_streams method and pass in the current replication number |
| 177 | + model = CCUModel(simpy.Environment(), experiment) |
| 178 | + experiment.reset_kpi() |
| 179 | + results = model.run() |
| 180 | + results.insert(0, 'Replication', i+1) |
| 181 | + all_results.append(results) |
| 182 | + |
| 183 | + return pd.concat(all_results, ignore_index=True) |
| 184 | + |
| 185 | + |
| 186 | +def results_summary(results): |
| 187 | + # Drop the 'Replication' column |
| 188 | + results = results.drop(columns='Replication') |
| 189 | + |
| 190 | + # Calculate the mean and standard deviation of each column |
| 191 | + mean = results.mean() |
| 192 | + std = results.std() |
| 193 | + |
| 194 | + # Create a summary dataframe |
| 195 | + summary = pd.DataFrame({'Mean': mean, 'Standard Deviation': std}) |
| 196 | + |
| 197 | + return summary |
| 198 | + |
| 199 | + |
| 200 | +def get_experiments(): |
| 201 | + experiments = {} |
| 202 | + for i in range(23, 29): |
| 203 | + exp = Experiment(num_critical_care_beds=i) |
| 204 | + experiments[f'Experiment_{i}'] = exp |
| 205 | + return experiments |
| 206 | + |
| 207 | +def run_all_experiments(experiments, num_replications): |
| 208 | + summaries = {} |
| 209 | + for name, exp in experiments.items(): |
| 210 | + print(f"Running experiment: {name}") |
| 211 | + model = CCUModel(simpy.Environment(), exp) |
| 212 | + results = multiple_replications(exp, num_replications) |
| 213 | + summary = results_summary(results) |
| 214 | + summaries[name] = summary |
| 215 | + return summaries |
| 216 | + |
| 217 | +def summary_of_experiments(experiment_summaries): |
| 218 | + return pd.concat(experiment_summaries, axis=1) |
| 219 | + |
| 220 | + |
| 221 | + |
| 222 | +import streamlit as st |
| 223 | +import pandas as pd |
| 224 | + |
| 225 | +def main(): |
| 226 | + st.title('A simulation model of bed-occupancy in a critical care unit') |
| 227 | + st.write('This model is a recreation of the model reported in a published academic study:') |
| 228 | + st.write('J D Griffiths, M Jones, M S Read & J E Williams (2010) A simulation model of bed-occupancy in a critical care unit, Journal of Simulation, 4:1, 52-59, DOI: 10.1057/jos.2009.22') |
| 229 | + st.write('Original Study: [Journal of Simulation](https://www.tandfonline.com/doi/full/10.1057/jos.2009.22)') |
| 230 | + |
| 231 | + with st.sidebar: |
| 232 | + st.subheader('Experiment Parameters') |
| 233 | + num_beds = st.slider('Number of Critical Care Beds', 23, 28, 23) |
| 234 | + cleaning_duration = st.slider('Intensive Cleaning Duration', 1, 10, 5) |
| 235 | + trace = st.checkbox('Enable Trace', False) |
| 236 | + num_replications = st.number_input('Number of Replications', 1, 10, 5) |
| 237 | + |
| 238 | + experiment = Experiment(num_critical_care_beds=num_beds, intensive_cleaning_duration=cleaning_duration, trace=trace) |
| 239 | + simulate_button = st.button('Simulate') |
| 240 | + |
| 241 | + if simulate_button: |
| 242 | + with st.spinner('Please wait for results...'): |
| 243 | + results = multiple_replications(experiment, num_replications) |
| 244 | + summary = results_summary(results) |
| 245 | + st.write(summary) |
| 246 | + |
| 247 | +if __name__ == '__main__': |
| 248 | + main() |
| 249 | + |
| 250 | + |
| 251 | + |
| 252 | + |
0 commit comments