varadpuntambekar commited on
Commit
4c6a838
·
verified ·
1 Parent(s): d054312

creating the app.py file that runs the entire program

Browse files

This file contains the source code for the program. This is the file that runs on huggingface.

Files changed (1) hide show
  1. app.py +939 -0
app.py ADDED
@@ -0,0 +1,939 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on 26 January 2025 14:57 (HSC DPhil students Room, Oxford)
4
+
5
+ @author: varad
6
+ """
7
+ #all libraries that I will be using in the simulation
8
+
9
+ import numpy as np
10
+ import simpy
11
+ import gradio as gr
12
+ import pandas as pd
13
+ import random
14
+ import csv
15
+ import matplotlib
16
+ import plotly.graph_objects as go
17
+ import matplotlib.pyplot as plt
18
+ import simpy.resources
19
+ import os
20
+ import plotly.subplots as sp
21
+
22
+ #Non Modifiable variables
23
+ class parameters (object):
24
+ '''
25
+ This class contains all the constant non-modifiable parameters that will go into the model
26
+ These mostly include service times and the time for which the simulation is supposed to run and halt etc
27
+ '''
28
+
29
+ experiment_no = 0 #incremented every time the main function is called
30
+ number_of_runs = 3 #Total number of times the simulation will run for 1 experiment
31
+
32
+ pt_per_day = 10 #Number of patients that visit the Gynae OPD every day (derived from AIIMS Bhopal Annual Report)(This could also be capped for a day if there are limited spots)
33
+ pt_interarrival_time = 50 #Number of minutes in a working day / number of total patients expected during the day
34
+ #modifiable factors, will be defined again in the relevant class
35
+ #resources
36
+ #Staff
37
+ #Since these are modifiable parameters, they are not implemented but only defined here, they are implemented to be inputted from the gradio app
38
+ gynae_resident = None #Gynaecological residents that perform history and examination and pap smear collection in the routine OPDs in AIIMS Bhopal
39
+ gynae_consulant = None #Gynaecological consultants that perform procedures such as LEEP (LEETZ) or hysterectomy etc
40
+ pathologists = None #No of pathologists that interpret the pathology findings
41
+ cytotechnicians = None #No of cytotechnicians that process the sample generated
42
+
43
+
44
+ #Stuff
45
+ num_pap_kits = 50 #number of kits that the hospital has to perform a pap smear (ayre's spatula, glass slide, preservative and box)
46
+ num_pathology_consumables = 50 #consumables required for processing the pathological specimen
47
+ num_colposcopy_consumables = 50 #consumables required for conducting colposcopy
48
+ num_thermal_consumables = 50 #consumables required for LEEP
49
+ num_ot_consumables = 50 #consumables required for hysterectomy
50
+
51
+
52
+ #rooms (scheduled resource)
53
+ num_colposcopy_rooms = 3 #number of colposcopy rooms
54
+ num_ot_rooms = 3 #number of OT rooms
55
+
56
+ #service times
57
+ history_exam_time = 25 #time taken to complete history and examination per patient (Imp thing to remember here would be that this might change as the system adapts
58
+ #to an excess load)
59
+ path_processing_time = 30 #time it takes from the sample is generated to the sample is prepared by cytotechnicians and ready for interpretation
60
+ path_reporting_time = 30 # time taken by pathologists to report the results of a processed sample
61
+ colposcopy_time = 25 #time taken to perform 1 colposcopy
62
+ thermal_time = 25 #time taken for 1 Loop Electrosurgical Excision procedure
63
+ hysterectomy_time = 50 #time taken for 1 hysterectomy
64
+
65
+ #Epidemiological parameters
66
+ screen_positivity_rate = 0.02 #% of positive samples (True positive + false positive / total samples)
67
+ biopsy_rate = 0.4 # % of all colposcopies that undergo a biopsy
68
+ biopsy_cin_rate = 0.6 # % of biopsies that are CIN
69
+ biopsy_cacx_rate = 0.02 # % of biopsies that are CaCx
70
+ follow_up_rate = 0.65 # % of women who follow up after a positive screen result (My own meta analysis + local data)
71
+
72
+ run_time = 100000 #Time for which the entire simulation will run (in minutes)
73
+ class scheduled_resource(simpy.Resource):
74
+ '''
75
+ Extends the simpy.Resource object to include a resource that is only available during certain time of day and day of week
76
+ '''
77
+ def __init__(self, env, schedule, capacity):
78
+ super().__init__(env, capacity)
79
+ self.schedule = schedule # and integer list [0-6] for days of the week
80
+ self.env = env
81
+
82
+ def is_availeble (self):
83
+ '''
84
+ checks time of day and day of week and returns a boolean based on whether the resource is available at that time or not
85
+ '''
86
+ current_time = self.env.now
87
+ week_minutes = 24 * 7 * 60 #minutes in a week
88
+ day_minutes = 24 * 60 # minutes in a day
89
+
90
+ current_day = int((current_time % week_minutes)/day_minutes) #first checks the number of minutes left in th week then checks number of day of the week
91
+ return current_day in self.schedule # returns a boolean whether the int current day is in schedule
92
+
93
+ def request (self, *args, **kwargs):
94
+ if self.is_availeble == False:
95
+ self.env.process(self.wait_for_availability(*args, **kwargs))
96
+ return super().request(*args, **kwargs)
97
+
98
+ def wait_for_availability(self, *args, **kwargs):
99
+ '''
100
+ Creates a waiting process that waits for the resource to be available and then executes the request function
101
+ '''
102
+ while not self.is_availeble():
103
+ #sees how much time is left for new day
104
+ current_minutes = self.env.now
105
+ day_minutes = 24 * 60
106
+ minutes_till_next_day = day_minutes - (current_minutes/day_minutes)
107
+ #wait for that much time
108
+ yield self.env.timeout(minutes_till_next_day)
109
+ #when it's the right time, execute the request
110
+ request = super().request(*args, **kwargs)
111
+
112
+ yield request
113
+ return request
114
+
115
+
116
+ class ca_cx_patient (object):
117
+ '''
118
+ This class creates patients and declares their individual parameters that explains how they spent their time at the hospital
119
+ These individual parameters will then be combined with others in the simulation to get overall estimates
120
+ '''
121
+ def __init__(self, pt_id):
122
+ '''
123
+ defines a patient and declares patient level variables to be recorded and written in a dataframe
124
+ '''
125
+ self.id = pt_id
126
+
127
+ #declaring the variables to be recorded
128
+ #putting them as zero to try an
129
+ self.time_at_entered = 0 #time when the patient entered into the OPD room
130
+ self.time_at_screen_result = 0 #time when the patient first received the screening result
131
+ self.time_at_colposcopy = 0 #time when the patient attended the colposcopy clinic
132
+ self.time_at_treatment = 0 #time when patient got the treatment, either admission or surgery or LEEP or thermal/cryo
133
+ self.time_at_exit = 0 #time when patient exits the system
134
+ #need these values to calculate resource utilisation percentage
135
+ self.history_examination_service_time = 0
136
+ self.colposcopy_service_time = 0
137
+ self.treatment_service_time = 0
138
+ self.screen_sample_processing_time = 0
139
+ self.screen_sample_reporting_time = 0
140
+ self.biopsy_sample_processing_time = 0
141
+ self.biopsy_sample_reporting_time = 0
142
+
143
+
144
+ #need these values to calculate queue lengths
145
+ self.colposcopy_q_length =0
146
+ self.treatment_q_length = 0
147
+ self.screen_processing_q_length = 0
148
+ self.screen_reporting_q_length = 0
149
+
150
+ self.biopsy_processing_q_length = 0
151
+ self.biopsy_reporting_q_length = 0
152
+
153
+
154
+ class Ca_Cx_pathway (object):
155
+ '''
156
+ This is the fake hospital. Defines all the processes that the patients will go through. Will record statistics for 1 simulation that will be later analyzed and clubbed with
157
+ results from 100 simulations
158
+ '''
159
+ def __init__(self, run_number, num_gynae_residents, num_gynae_consultants, num_pathologists, num_cytotechnicians, num_colposcopy_room = 3, num_ot_rooms = 2):
160
+ self.env = simpy.Environment()
161
+
162
+ #declaring number of modifiable resource capacity, non modifiable resources to be imported from the parameters class
163
+ self.num_gynae_residents = num_gynae_residents
164
+ self.num_gynae_consultants = num_gynae_consultants
165
+ self.num_pathologists = num_pathologists
166
+ self.num_cytotechnicians = num_cytotechnicians
167
+ self.num_colposcopy_rooms = num_colposcopy_room
168
+ self.num_ot_rooms = num_ot_rooms
169
+ self.run_number = run_number
170
+
171
+ self.colposcopy_schedule = [0,2,4] #list of integers form 0-6 for each day of the week that resource is available
172
+ self.ot_schedule = [0,2,4] #list of integers from 0-6 for each day of the week that resource is available
173
+
174
+ self.pt_counter = 0 #acts as the UHID of the 0th patient
175
+ self.run_number = self.run_number + 1
176
+ #declaring resources
177
+ #staff
178
+ self.gynae_residents = simpy.Resource(self.env, capacity=num_gynae_residents)
179
+ self.gynae_consultants = simpy.Resource(self.env, capacity=num_gynae_consultants)
180
+ self.pathologist = simpy.Resource(self.env, capacity=num_pathologists)
181
+ self.cytotechnician = simpy.Resource(self.env, capacity=num_cytotechnicians)
182
+
183
+ #stuff
184
+ self.pap_kit = simpy.Resource(self.env, capacity=parameters.num_pap_kits)
185
+ self.pathology_consumables = simpy.Resource(self.env, capacity=parameters.num_pathology_consumables)
186
+ self.colposcopy_consumables = simpy.Resource(self.env, capacity=parameters.num_colposcopy_consumables)
187
+ self.thermal_consumables = simpy.Resource(self.env, capacity=parameters.num_thermal_consumables)
188
+ self.ot_consumables = simpy.Resource(self.env, capacity=parameters.num_ot_consumables)
189
+
190
+ #rooms (scheduled resource)
191
+ self.colposcopy_room = scheduled_resource(self.env, self.colposcopy_schedule, capacity=parameters.num_colposcopy_rooms, )
192
+ self.ot_room = scheduled_resource(self.env, self.ot_schedule, capacity = parameters.num_ot_rooms)
193
+
194
+ #declaring a patient level dataframe to record patient KPIs - This is recorded at the individual level
195
+ self.individual_results = pd.DataFrame({
196
+ "UHID" : [],
197
+ "Time_Entered_in System":[],
198
+ "Screen_Processing_Q_Length" : [],
199
+ "Screen_reporting_Q_Length" : [],
200
+ "Time_at_screening_result":[],
201
+ "Colposcopy_Q_Length" :[],
202
+ "Time_at_colposcopy" : [],
203
+ "Biopsy_Processing_Q_Length" : [],
204
+ "Biopsy_Reporting_Q_Length" : [],
205
+ "Treatment_Q_length" :[],
206
+ "Time_at_treatment" : [],
207
+ "History_and_Examination_time": [], #also recording service times as they will ultimately be added up to calculate resource utilisation percentage
208
+ "Screen_processing_time":[],
209
+ "Screen_reporting_time":[],
210
+ "Biopsy_processing_time":[],
211
+ "Biopsy_reporting_time":[],
212
+ "Colposcopy_time":[],
213
+ "Treatment_time":[],
214
+ "Exit_time":[]
215
+ })
216
+
217
+ #Declaring individual results processing variables
218
+ #time intervals between important points
219
+ self.time_to_screen_result = 0 #during analysis, need to only consider those patients who actually did undergo these procedures
220
+ self.time_to_colposcopy = 0 #as not all patients will undergo all the processes. might include some drop na function, but shouldn't be too much of a problem
221
+ self.time_to_treatment = 0
222
+ self.total_time_in_system = 0
223
+
224
+
225
+
226
+ #declaring system KPIs to be measured at the run level.
227
+ #Queue lengths for different processes
228
+ self.max_q_len_screen_processing = 0
229
+ self.max_q_len_screen_reporting = 0
230
+ self.max_q_len_colposcopy = 0
231
+ self.max_q_len_biopsy_processing = 0
232
+ self.max_q_len_biopsy_reporting = 0
233
+ self.max_q_len_treatment = 0
234
+
235
+ #Resource utilization percentages
236
+ self.gynae_residents_utilisation = 0 #by adding service times of all the processes where these resources are required.
237
+ self.gynae_consultants_utlisation = 0
238
+ self.cytotechnician_utilisation = 0
239
+ self.pathologist_utilisation = 0
240
+
241
+
242
+
243
+ def is_within_working_hours(self):
244
+ '''
245
+ checks whether the current simulation time is within working hours and returns a boolean
246
+ '''
247
+ current_sim_mins = self.env.now
248
+ day_mins = 24*60
249
+ current_sim_hour = int((current_sim_mins%day_mins)/60)
250
+ return 8 < current_sim_hour < 17
251
+
252
+
253
+
254
+ def gen_patient_arrival(self):
255
+ '''
256
+ Generates a fictional patient according to a distribution, they undergo and OPD, this generates a sample which undergoes processing, after results are
257
+ conveyed, if positive, patient only then moves on to the next step i.e. colposcopy.
258
+ '''
259
+ while True:
260
+ #check time of day,
261
+ #if self.is_within_working_hours:
262
+ #if time of day is appropriate then generate the patient
263
+
264
+ self.pt_counter += 1
265
+ screening_patient = ca_cx_patient(self.pt_counter)
266
+
267
+
268
+ #print("Patient generates", self.patient.pt_id)
269
+ #here we will need to generate all the samples for the patient, even if they don't get created later on
270
+ #reason for that is it's okay if the reading is 0 or NaN, the code just won't work if there is no object to begin with
271
+
272
+ #record necessary timepoints
273
+ screening_patient.time_at_entered = self.env.now
274
+ #patient moves to the OPD
275
+ self.env.process(self.history_examination(screening_patient))
276
+ #time for next patient arrival
277
+ wait_time_for_next_pt = random.expovariate(1/parameters.pt_interarrival_time)
278
+ yield self.env.timeout(wait_time_for_next_pt)
279
+
280
+ def history_examination(self, patient):
281
+ '''
282
+ Patient undergoes history and examination and in the process also generates the screening sample
283
+ '''
284
+ #request for a resident and consumables for sample collection and wait for them to be available
285
+ with self.gynae_residents.request() as gynae_res, self.pap_kit.request() as pap, self.pathology_consumables.request() as path_consum :
286
+ yield gynae_res and pap and path_consum
287
+
288
+ #patient undergoes history, examination and sample collection
289
+ history_examination_time = random.triangular(parameters.history_exam_time/2, parameters.history_exam_time, parameters.history_exam_time *2 )
290
+ patient.history_examination_service_time = history_examination_time
291
+ yield self.env.timeout(history_examination_time)
292
+
293
+ #New implementation different than the previous one
294
+ #The sample goes to processing and reporting function which generates a boolean which decides whether the patient moves on or not
295
+ screening_sample_gen = self.env.process(self.screening(patient))
296
+
297
+ screen_result = yield screening_sample_gen
298
+
299
+
300
+ if screen_result:
301
+ self.env.process(self.call_for_follow_up(patient))
302
+ else:
303
+ #patient exits the system
304
+ patient.time_at_exit = self.env.now
305
+ self.add_to_individual_results(( patient))
306
+
307
+ #generate a screening sample
308
+ #self.pt_screening_sample = screen_sample(self.patient.pt_id) #screen sample id is the same as the patient id
309
+ #print("Screen Sample generated", self.pt_screening_sample.screen_sample_id)
310
+ #here we will need to generate all the samples for the patient, even if they don't get created later on
311
+ #reason for that is it's okay if the reading is 0 or NaN, the code just won't work if there is no object to begin with
312
+ #self.pt_biopsy_sample = biopsy_sample(self.patient.pt_id) #generate a biopsy sample that will go for processing
313
+ #print("biopsy sample generated", self.pt_biopsy_sample.biopsy_sample_id)
314
+
315
+ #sample goes on for processing
316
+ #self.env.process(self.screen_sample_processing())
317
+
318
+ def screening (self, patient):
319
+ '''
320
+ This function simulation the processing and reporting of screen samples and returns a boolean whether the result is positive or negative
321
+ '''
322
+ patient.screen_processing_q_length = len(self.cytotechnician.queue)
323
+
324
+ with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as scr_proc_consum:
325
+ yield cytotec and scr_proc_consum
326
+
327
+ screen_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time *2)
328
+ patient.screen_sample_processing_time = screen_sample_processing_time
329
+ yield self.env.timeout(screen_sample_processing_time)
330
+
331
+
332
+ patient.screen_reporting_q_length = len(self.pathologist.queue)
333
+ with self.pathologist.request() as path:
334
+ yield path
335
+ screen_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time * 2)
336
+ patient.screen_sample_reporting_time = screen_sample_reporting_time #record this for resource utilisation %
337
+ yield self.env.timeout(screen_sample_reporting_time)
338
+
339
+ patient.time_at_screen_result = self.env.now
340
+
341
+ if random.random() < parameters.screen_positivity_rate:
342
+ return True #if sample is positive
343
+ else:
344
+ return False # if sample is negative
345
+
346
+ def screen_sample_processing(self):
347
+ '''
348
+ Sample undergoes processing
349
+ '''
350
+ #queue length for processing
351
+ self.pt_screening_sample.screen_processing_q_length = len(self.cytotechnician.queue)
352
+ #request resources and wait for them to be available
353
+ with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as scr_proc_consum:
354
+ yield cytotec and scr_proc_consum
355
+
356
+ #sample undergoes processing
357
+ screen_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time *2)
358
+ self.pt_screening_sample.screen_sample_processing_time = screen_sample_processing_time
359
+ yield self.env.timeout(screen_sample_processing_time)
360
+ #sample goes for reporting
361
+ self.env.process(self.screen_sample_reporting())
362
+
363
+ def screen_sample_reporting(self):
364
+ '''
365
+ Processed sample is interpreted and reported by pathologist
366
+ '''
367
+ #measure queue length for every patient that comes (max of this column will be the max queue length)
368
+ self.pt_screening_sample.screen_reporting_q_length = len(self.pathologist.queue)
369
+ #request for a pathologist and wait until
370
+ with self.pathologist.request() as path:
371
+ yield path
372
+
373
+ #record the current time as an important milestone
374
+ self.patient.time_at_screen_result = self.env.now
375
+
376
+ #sample undergoes reporting
377
+ screen_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time * 2)
378
+ self.pt_screening_sample.screen_sample_reporting_time = screen_sample_reporting_time #record this for resource utilisation %
379
+ yield self.env.timeout(screen_sample_reporting_time)
380
+
381
+ #if sample is positive, move on to follow up, otherwise terminate
382
+ if random.random() < parameters.screen_positivity_rate:
383
+ self.env.process(self.call_for_follow_up())
384
+ else:
385
+ #patient exits the system
386
+ self.patient.time_at_exit = self.env.now
387
+ #add data to the df
388
+ Ca_Cx_pathway.add_to_individual_results(self)
389
+
390
+ def call_for_follow_up (self, patient):
391
+ '''
392
+ Gynaecology residents
393
+ '''
394
+ #no waiting time for this as it is quite instant.
395
+ #request a gynae_res (later on could modify to include a receptionist or another health cadre)
396
+ with self.gynae_residents.request() as gynae_res:
397
+ yield gynae_res
398
+
399
+ # whether the patient returns or not
400
+ if random.random() < parameters.follow_up_rate:
401
+ #patient goes on for colposcopy
402
+ self.env.process(self.colposcopy(patient))
403
+ #instantaneous process so no timeout really and also not a service
404
+ else:
405
+ #patient exits the system
406
+ patient.time_at_exit = self.env.now
407
+ #add to df
408
+ self.add_to_individual_results(patient)
409
+
410
+ def colposcopy(self, patient):
411
+ '''
412
+ Patient that was generated undergoes colposcopy
413
+ '''
414
+ #here, the entity requests two different resources, it's waiting time or queue length will be decided by whatever is less available.
415
+ # 1 small caveat here is that service times for different resources are different, so a larger queue doesn't necessarily mean a longer waiting time
416
+ # we're not measuring waiting time but only time between events as that is a much more relevant indicator for implementation decisions.
417
+ colpo_q_len_list = [len(self.gynae_consultants.queue), len(self.colposcopy_room.queue) ]
418
+ patient.colposcopy_q_length = max(colpo_q_len_list)
419
+
420
+ #requests for a consultant, consumables and a room
421
+ with self.gynae_consultants.request() as gynae_consul, self.colposcopy_consumables.request() as gynae_consumables, self.colposcopy_room.request() as colpo_room:
422
+ yield gynae_consul and gynae_consumables and colpo_room
423
+
424
+ #Record time at colposcopy
425
+ patient.time_at_colposcopy = self.env.now
426
+
427
+ #patient undergoes colposcopy
428
+ colposcopy_service_time = random.triangular(parameters.colposcopy_time/2, parameters.colposcopy_time, parameters.colposcopy_time *2)
429
+ patient.colposcopy_service_time = colposcopy_service_time
430
+ yield self.env.timeout(colposcopy_service_time)
431
+
432
+
433
+
434
+ biopsy_sample_gen = self.env.process(self.biopsy(patient))
435
+
436
+ biopsy_result = yield biopsy_sample_gen
437
+
438
+ if biopsy_result == 1:
439
+ self.env.process(self.thermal_ablation(patient))
440
+ elif biopsy_result == 2:
441
+ self.env.process(self.hysterectomy(patient))
442
+ else:
443
+ patient.time_at_exit = self.env.now
444
+ self.add_to_individual_results((patient))
445
+
446
+ def biopsy(self, patient):
447
+ '''
448
+ implementation is very similar to the screening function
449
+ '''
450
+ patient.biopsy_processing_q_length = len(self.cytotechnician.queue)
451
+ with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as path_consum:
452
+ yield cytotec and path_consum
453
+
454
+ biopsy_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time * 2)
455
+ patient.biopsy_sample_processing_time = biopsy_sample_processing_time
456
+ yield self.env.timeout(biopsy_sample_processing_time)
457
+
458
+ patient.biopsy_reporting_q_length = len(self.pathologist.queue)
459
+ with self.pathologist.request() as path:
460
+ yield path
461
+
462
+ biopsy_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time *2)
463
+ patient.biopsy_sample_reporting_time = biopsy_sample_reporting_time
464
+ yield self.env.timeout(biopsy_sample_reporting_time)
465
+
466
+ if random.random() < parameters.biopsy_cin_rate:
467
+ return 1
468
+ elif parameters.biopsy_cin_rate < random.random() < parameters.biopsy_cacx_rate:
469
+ return 2
470
+ else:
471
+ return 3
472
+
473
+ def biopsy_sample_processing(self):
474
+ '''
475
+ Biopsy sample if prepared undergoes processing
476
+ '''
477
+ #queue length for processing
478
+ self.pt_biopsy_sample.biopsy_processing_q_length = len(self.cytotechnician.queue)
479
+ #requests a cytotechnicians and consumables
480
+ with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as path_consum:
481
+ yield cytotec and path_consum
482
+
483
+ #biopsy sample undergoes processing
484
+ biopsy_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time * 2)
485
+ self.pt_biopsy_sample.biopsy_sample_processing_time = biopsy_sample_processing_time
486
+ yield self.env.timeout(biopsy_sample_processing_time)
487
+
488
+ #biopsy sample goes for reporting
489
+ self.env.process(self.biopsy_sample_reporting())
490
+
491
+ def biopsy_sample_reporting(self):
492
+ '''
493
+ Biopsy sample if taken undergoes reporting after processing
494
+ '''
495
+ #queue length for reporting
496
+ self.pt_biopsy_sample.biopsy_reporting_q_length = len(self.pathologist.queue)
497
+ #requests a pathologist
498
+ with self.pathologist.request() as path:
499
+ yield path
500
+
501
+ #biopsy sample undergoes reporting
502
+ biopsy_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time *2)
503
+ self.pt_biopsy_sample.biopsy_sample_reporting_time = biopsy_sample_reporting_time
504
+ yield self.env.timeout(biopsy_sample_reporting_time)
505
+
506
+ #depending on the diagnosis, patient either goes for thermal ablation or hysterectomy (currently, only making 2 options available, have the option of adding more on later)
507
+ biopsy_result = random.random()
508
+ if biopsy_result < parameters.biopsy_cin_rate:
509
+ self.env.process(self.thermal_ablation()) #diagnosed with CIN
510
+
511
+ elif parameters.biopsy_cin_rate < biopsy_result < parameters.biopsy_cacx_rate:
512
+ self.env.process(self.hysterectomy()) #diagnosed with cervical cancer
513
+
514
+ else:
515
+ self.patient.time_at_exit = self.env.now #patient exits the system
516
+ #add data to the df
517
+ Ca_Cx_pathway.add_to_individual_results(self)
518
+
519
+ def thermal_ablation(self, patient):
520
+ '''
521
+ If indicated, pt undergoes thermal ablation
522
+ '''
523
+ thermal_q_len_list = [len(self.gynae_consultants.queue), len(self.colposcopy_room.queue) ]
524
+ patient.treatment_q_length = max(thermal_q_len_list)
525
+
526
+ #requests resources required for thermal ablation
527
+ with self.gynae_consultants.request() as gynae_consul, self.thermal_consumables.request() as thermal_consum, self.colposcopy_room.request() as colpo_room:
528
+ yield gynae_consul and thermal_consum and colpo_room
529
+
530
+ patient.time_at_treatment = self.env.now
531
+ #patient undergoes thermal ablation
532
+ thermal_ablation_time = random.triangular(parameters.thermal_time/2, parameters.thermal_time, parameters.thermal_time *2)
533
+ patient.treatment_service_time = thermal_ablation_time
534
+ yield self.env.timeout(thermal_ablation_time)
535
+
536
+ #patient exits the system
537
+
538
+ patient.time_at_exit = self.env.now
539
+ #add to df
540
+ self.add_to_individual_results(patient)
541
+
542
+ def leep (self):
543
+ '''
544
+ if indicated, patient undergoes LEEP
545
+ '''
546
+ #Not being implemented in this first version of the model
547
+ pass
548
+ def hysterectomy (self, patient):
549
+ '''
550
+ if indicated, patient undergoes hysterectomy
551
+ '''
552
+ hyst_q_len_list = [len(self.gynae_consultants.queue), len(self.ot_room.queue)]
553
+ patient.treatment_q_length = max(hyst_q_len_list)
554
+
555
+ #request for a ot room and other equipment
556
+ with self.gynae_consultants.request() as gynae_consul, self.ot_consumables.request() as ot_consum, self.ot_room as ot_room:
557
+ yield gynae_consul and ot_consum and ot_room
558
+
559
+ self.patient.time_at_treatment = self.env.now
560
+ #patient undergoes surgery
561
+ hysterectomy_time = random.triangular(parameters.hysterectomy_time/2, parameters.hysterectomy_time, parameters.hysterectomy_time *2)
562
+ self.patient.treatment_service_time = hysterectomy_time
563
+ yield self.env.timeout(hysterectomy_time)
564
+
565
+ #patient exits the system
566
+
567
+ patient.time_at_exit = self.env.now
568
+ #adding everything to the dataframe
569
+ self.add_to_individual_results(patient)
570
+
571
+
572
+ def add_to_individual_results (self, patient):
573
+ '''
574
+ To add a row to a df, we need to pass an argument that adds in all 10-12 columns together even if we want to add just one cell
575
+ Hence to make my job easier, writing a function that does this in every function without having to write too much.
576
+ '''
577
+ df_to_add = pd.DataFrame({
578
+ "UHID" : [patient.id],
579
+ "Time_Entered_in System":[patient.time_at_entered],
580
+ "Screen_Processing_Q_Length" : [patient.screen_processing_q_length],
581
+ "Screen_reporting_Q_Length" : [patient.screen_reporting_q_length],
582
+ "Time_at_screening_result":[patient.time_at_screen_result],
583
+ "Colposcopy_Q_Length":[patient.colposcopy_q_length],
584
+ "Time_at_colposcopy" : [patient.time_at_colposcopy],
585
+ "Biopsy_Processing_Q_Length" : [patient.biopsy_processing_q_length],
586
+ "Biopsy_Reporting_Q_Length" : [patient.biopsy_reporting_q_length],
587
+ "Treatment_Q_length":[patient.treatment_q_length],
588
+ "Time_at_treatment" : [patient.time_at_treatment],
589
+ "History_and_Examination_time": [patient.history_examination_service_time], #also recording service times as they will ultimately be added up to calculate resource utilisation percentage
590
+ "Screen_processing_time":[patient.screen_sample_processing_time],
591
+ "Screen_reporting_time":[patient.screen_sample_reporting_time],
592
+ "Biopsy_processing_time":[patient.biopsy_sample_processing_time],
593
+ "Biopsy_reporting_time":[patient.biopsy_sample_reporting_time],
594
+ "Colposcopy_time":[patient.colposcopy_service_time],
595
+ "Treatment_time":[patient.treatment_service_time],
596
+ "Exit_time":[patient.time_at_exit]
597
+
598
+ })
599
+ df_to_add.set_index('UHID', inplace= True)
600
+ self.individual_results = pd.concat([self.individual_results, df_to_add]) #throws syntax error that I should not use the _ sign, we'll see
601
+
602
+
603
+ def individual_results_processor(self):
604
+ '''
605
+ Processes the individual results dataframe by adding columns from which KPI's can be calculated
606
+ '''
607
+ #Calculating time between important events
608
+ self.individual_results['Time_to_screen_results'] = self.individual_results['Time_at_screening_result'] - self.individual_results['Time_Entered_in System']
609
+ self.individual_results['Time_to_Colposcopy'] = self.individual_results['Time_at_colposcopy'] - self.individual_results['Time_Entered_in System']
610
+ self.individual_results['Time_to_Treatment'] = self.individual_results['Time_at_treatment'] - self.individual_results['Time_Entered_in System']
611
+ self.individual_results['Total_time_in_system'] = self.individual_results['Exit_time'] - self.individual_results['Time_Entered_in System']
612
+
613
+ #Calculating service times for different resources
614
+
615
+ self.individual_results['Gynae_res_busy_time'] = self.individual_results['History_and_Examination_time']
616
+ self.individual_results['Cytotech_busy_time'] = self.individual_results['Screen_processing_time'] + self.individual_results['Biopsy_processing_time']
617
+ self.individual_results['Pathologist_busy_time'] = self.individual_results['Screen_reporting_time'] + self.individual_results['Biopsy_reporting_time']
618
+ self.individual_results['Gynae_consul_busy_time'] = self.individual_results['Colposcopy_time'] + self.individual_results['Treatment_time']
619
+
620
+
621
+
622
+ def KPI_calculator(self):
623
+ '''
624
+ Function that calculates the various KPIs from an individual run from the different columns of the individual results dataframe
625
+ These are KPIs for a signle run
626
+ '''
627
+ #max q lengths
628
+ self.max_q_len_screen_processing = self.individual_results['Screen_Processing_Q_Length'].max()
629
+ self.max_q_len_screen_reporting = self.individual_results['Screen_reporting_Q_Length'].max()
630
+ self.max_q_len_colposcopy = self.individual_results['Colposcopy_Q_Length'].max()
631
+ self.max_q_len_biopsy_processing = self.individual_results['Biopsy_Processing_Q_Length'].max()
632
+ self.max_q_len_biopsy_reporting = self.individual_results['Biopsy_Reporting_Q_Length'].max()
633
+ self.max_q_len_treatment = self.individual_results['Treatment_Q_length'].max()
634
+
635
+ #resource utilisation percentages
636
+ self.gynae_residents_utilisation = self.individual_results['Gynae_res_busy_time'].sum()/(parameters.run_time * self.num_gynae_residents)
637
+ self.cytotechnician_utilisation = self.individual_results['Cytotech_busy_time'].sum()/(parameters.run_time * self.num_cytotechnicians)
638
+ self.gynae_consultants_utlisation = self.individual_results['Gynae_consul_busy_time'].sum() / (parameters.run_time * self.num_gynae_consultants)
639
+ self.pathologist_utilisation = self.individual_results['Pathologist_busy_time'].sum() / (parameters.run_time * self.num_pathologists)
640
+
641
+ #median time to important events
642
+ #creating temp df and dropping rows with negative values for specific columns
643
+ temp_colpo_time_df = self.individual_results['Time_to_Colposcopy'][self.individual_results['Time_to_Colposcopy'] >0]
644
+ temp_treatmet_time_df = self.individual_results['Time_to_Treatment'][self.individual_results['Time_to_Treatment'] >0]
645
+ #now putting the median method onto that limited dataset
646
+ self.med_time_to_scr_res = self.individual_results['Time_to_screen_results'].median()
647
+ self.med_time_to_colpo = temp_colpo_time_df.median()
648
+ self.med_time_to_treatment = temp_treatmet_time_df.median()
649
+ self.med_tot_time_in_system = self.individual_results['Total_time_in_system'].median()
650
+
651
+
652
+ def export_row_to_csv(self):
653
+ '''
654
+ Creates a new dataframe with trial results and exports a single row to that dataframe after each run
655
+ '''
656
+ with open ('kpi_trial_results.csv', 'a')as f:
657
+ writer = csv.writer(f, delimiter= ',')
658
+ row_to_add = [
659
+ self.run_number,
660
+ self.max_q_len_screen_processing,
661
+ self.max_q_len_screen_reporting,
662
+ self.max_q_len_colposcopy,
663
+ self.max_q_len_biopsy_processing,
664
+ self.max_q_len_biopsy_reporting,
665
+ self.max_q_len_treatment,
666
+
667
+ self.gynae_residents_utilisation,
668
+ self.gynae_consultants_utlisation,
669
+ self.pathologist_utilisation,
670
+ self.cytotechnician_utilisation,
671
+
672
+ self.med_time_to_scr_res,
673
+ self.med_time_to_colpo,
674
+ self.med_time_to_treatment,
675
+ self.med_tot_time_in_system
676
+ ]
677
+ writer.writerow(row_to_add)
678
+
679
+ def run(self):
680
+ '''
681
+ Runs the simulation and calls the generator function.
682
+ '''
683
+ self.env.process(self.gen_patient_arrival())
684
+ self.env.run(until= parameters.run_time)
685
+ #print(self.individual_results)
686
+ self.individual_results_processor()
687
+ self.individual_results.to_csv('individual_results.csv')
688
+ self.KPI_calculator()
689
+ self.export_row_to_csv()
690
+
691
+
692
+ class summary_statistics(object):
693
+ '''
694
+ This class will define methods that will calculate aggregate statistics from 100 simulations and append the results onto a new spreadsheet which will be used to append results
695
+ from 100 simulations for different number of independent variables (such as patients)
696
+ '''
697
+ def __init__(self):
698
+ pass
699
+
700
+
701
+
702
+ def gen_final_summary_table (self):
703
+ '''
704
+ Generates a table, essentially a row of summary statistics for 100 runs with a particular initial setting.
705
+ '''
706
+ with open ('final_summary_table.csv', 'w') as f:
707
+ writer = csv.writer(f, delimiter= ',')
708
+ column_headers = [
709
+ "Experiment_No" ,
710
+ "Max_Scr_Proc_Q_len" ,
711
+ 'Max_Scr_Rep_Q_len' ,
712
+ 'Max_Colpo_Q_len' ,
713
+ "Max_Biop_Proc_Q_Len" ,
714
+ "Max_Biop_Rep_Q_Len" ,
715
+ "Max_T/t_Q_len" ,
716
+
717
+ #resource utilisation %
718
+ 'Gynae_Res_%_util',
719
+ 'Gynae_consul_%_util',
720
+ 'Path_%_util',
721
+ 'Cytotec_%_util',
722
+
723
+ #Time between important events
724
+ 'Time_to_screening_results',
725
+ 'Time_to_colposcopy',
726
+ 'Time_to_treatment',
727
+ 'Total_time_in_system' ]
728
+
729
+ writer.writerow(column_headers)
730
+
731
+ def calculate_summary_statistics(self):
732
+ '''
733
+ Calculates summary statistic from 100 runs (or whatever the number of runs is specified) from the kpi_trial_results table which will then later on be added
734
+ onto the final_summary_table csv
735
+ '''
736
+ filepath = 'kpi_trial_results.csv'
737
+ df_to_read = pd.read_csv(filepath)
738
+ self.max_scr_proc_q_len = df_to_read['Max_Scr_Proc_Q_len'].median()
739
+ self.max_scr_rep_q_len = df_to_read['Max_Scr_Rep_Q_len'].median()
740
+ self.max_colpo_q_len = df_to_read['Max_Colpo_Q_len'].median()
741
+ self.max_biop_proc_q_len = df_to_read['Max_Biop_Proc_Q_Len'].median()
742
+ self.max_biop_rep_q_len = df_to_read['Max_Biop_Rep_Q_Len'].median()
743
+ self.max_treatment_q_len = df_to_read['Max_T/t_Q_len'].median()
744
+
745
+ self.med_gynae_res_util = df_to_read['Gynae_Res_%_util'].median()
746
+ self.med_gynae_consul_util = df_to_read['Gynae_consul_%_util'].median()
747
+ self.med_path_util = df_to_read['Path_%_util']
748
+ self.med_cytotec_util = df_to_read['Cytotec_%_util']
749
+
750
+ self.med_time_to_scr = df_to_read['Time_to_screening_results'].median()
751
+ self.med_time_to_colpo = df_to_read['Time_to_colposcopy'].median()
752
+ self.med_time_to_tt = df_to_read['Time_to_treatment'].median()
753
+ self.med_tot_time_in_sys = df_to_read['Total_time_in_system'].median()
754
+
755
+
756
+
757
+ def populate_final_summary_table(self):
758
+ '''
759
+ Updates the final summary table one row whenever it is called.
760
+ '''
761
+ with open ('final_summary_table.csv', 'a') as f:
762
+ writer = csv.writer(f, delimiter= ',')
763
+ row_to_add = [parameters.experiment_no,
764
+ self.max_scr_proc_q_len,
765
+ self.max_scr_rep_q_len,
766
+ self.max_colpo_q_len,
767
+ self.max_biop_proc_q_len,
768
+ self.max_biop_rep_q_len,
769
+ self.max_treatment_q_len,
770
+
771
+ self.med_gynae_res_util,
772
+ self.med_gynae_consul_util,
773
+ self.med_path_util,
774
+ self.med_cytotec_util,
775
+
776
+ self.med_time_to_scr,
777
+ self.med_time_to_colpo,
778
+ self.med_time_to_tt,
779
+ self.med_tot_time_in_sys
780
+
781
+ ]
782
+ writer.writerow(row_to_add)
783
+
784
+ def clear_csv_file():
785
+ '''f
786
+ Erases all the contents of a csv file. Used in the refresh button of the gradio app to start fresh
787
+ '''
788
+
789
+ parameters.experiment_no = 0
790
+ with open ('final_summary_table.csv', 'w') as f:
791
+ pass
792
+ open_final_table = summary_statistics()
793
+ open_final_table.gen_final_summary_table()
794
+
795
+
796
+ def plotly_plotter():
797
+ filepath = 'final_summary_table.csv'
798
+ df_to_plot = pd.read_csv(filepath)
799
+ fig = sp.make_subplots(rows = 1, cols= 3, subplot_titles= ("Max Queue Length for Different Processes",
800
+ 'Percent Utilisation for different Professionals','Time to important events'))
801
+
802
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Max_Scr_Proc_Q_len'], name = "Q len for Screen Processing"), row = 1, col = 1)
803
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Max_Scr_Rep_Q_len'], name = "Q len for Screen Reporting"),row = 1, col = 1)
804
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Max_Colpo_Q_len'], name = "Q len for Colposcopy"),row = 1, col = 1)
805
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Max_Biop_Proc_Q_Len'], name = "Q len for Biopsy Processing"),row = 1, col = 1)
806
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Max_Biop_Rep_Q_Len'], name = "Q len for Biopsy Reporting"),row = 1, col = 1)
807
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Max_T/t_Q_len'], name = "Q len for Treatment"),row = 1, col = 1)
808
+
809
+
810
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Gynae_Res_%_util'], name = "% Utilisation for Gynae Residents"),row = 1, col = 2)
811
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Gynae_consul_%_util'], name = "% Utilisation for Gynae Consultants"),row = 1, col = 2)
812
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Path_%_util'], name = "% Utilisation for Pathologists"),row = 1, col = 2)
813
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Cytotec_%_util'], name = "% Utilisation for Cytotechnicians"),row = 1, col = 2)
814
+
815
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Time_to_screening_results'], name = "Time to screening results"),row = 1, col = 3)
816
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Time_to_colposcopy'], name = "Time to Colposcopy"),row = 1, col = 3)
817
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Time_to_treatment'], name = "Time to Treatment"),row = 1, col = 3)
818
+ fig.add_trace(go.Scatter(x = df_to_plot['Experiment_No'], y = df_to_plot['Total_time_in_system'], name = "Total Time in the System"),row = 1, col = 3)
819
+
820
+ return fig
821
+
822
+
823
+ def gen_kpi_table():
824
+ #defining the KPI Results Table for one run, the export to row function in the cacx pathway class
825
+ # will add one row at a time
826
+ with open('kpi_trial_results.csv', 'w') as f:
827
+ writer = csv.writer(f, delimiter= ',')
828
+ column_headers = [
829
+ "Run_Number" ,
830
+ "Max_Scr_Proc_Q_len" ,
831
+ 'Max_Scr_Rep_Q_len' ,
832
+ 'Max_Colpo_Q_len' ,
833
+ "Max_Biop_Proc_Q_Len" ,
834
+ "Max_Biop_Rep_Q_Len" ,
835
+ "Max_T/t_Q_len" ,
836
+
837
+ #resource utilisation %
838
+ 'Gynae_Res_%_util',
839
+ 'Gynae_consul_%_util',
840
+ 'Path_%_util',
841
+ 'Cytotec_%_util',
842
+
843
+ #Time between important events
844
+ 'Time_to_screening_results',
845
+ 'Time_to_colposcopy',
846
+ 'Time_to_treatment',
847
+ 'Total_time_in_system' ]
848
+
849
+ writer.writerow(column_headers)
850
+
851
+ open_final_table = summary_statistics()
852
+ open_final_table.gen_final_summary_table()
853
+
854
+ def main(pt_per_day, num_gynae_res, num_gynae_consul, num_cytotec, num_path):
855
+ '''
856
+ This function will run the simulation for different independent variables that we need.
857
+ '''
858
+ parameters.experiment_no += 1
859
+ print (f'Experiment Number: {parameters.experiment_no}')
860
+ #print('For this experiment, Pt interarrival time = 480/patients per day = 480/{pt_per_day}')
861
+ parameters.pt_per_day = pt_per_day
862
+ sum_stats = summary_statistics()
863
+
864
+ gen_kpi_table()
865
+ for run in range (parameters.number_of_runs):
866
+ print(f'Run {run+1} in {parameters.number_of_runs}')
867
+ my_sim_model = Ca_Cx_pathway(run, num_gynae_res, num_gynae_consul, num_cytotec, num_path)
868
+ my_sim_model.run()
869
+ sum_stats.calculate_summary_statistics()
870
+ sum_stats.populate_final_summary_table()
871
+ return plotly_plotter()
872
+
873
+
874
+
875
+ with gr.Blocks() as app:
876
+ gr.HTML(
877
+ '''
878
+ <h1>Cervical Cancer DES App</h1>
879
+ '''
880
+ )
881
+
882
+ with gr.Row(equal_height= True):
883
+
884
+ with gr.Column(scale = 1):
885
+ gr.HTML(
886
+ '''
887
+ <h2>List of Assumptions</h2>
888
+ Discrete Event Simulation Models take in a lot of assumptions into consideration. Here is a list of assumptions made
889
+ while building this model
890
+ <ul>
891
+ <li> 50 Patients for cervical cancer screening arrive every day, making pt interrarrival time 480/50, but can be modified
892
+ <li> One working day is from 9 am to 5 pm so total 480 minutes in a day
893
+ <li> Procedure rooms such as Colposcopy and OT rooms are only available on Monday, Wednenday and Friday (3 days a week)
894
+ <li> Cytotechnicians and Pathologists process and report both Screening and Biopsy specimens
895
+ <li> Screening only happens through conventional pap smear
896
+ <li> Gynae residents and Gynae consultants essentially mean two different levels of health cadres, with Consultants being more
897
+ experienced than residents. Doesn't necessarily mean professionals with specialised training in Obstetrics and Gynaecology
898
+ <li> In the demo app, one run is for 1000 minutes i.e. 3 days
899
+ <li> Medians are calculated from 100 iterations of the simulation
900
+ </ul>
901
+ '''
902
+ )
903
+ with gr.Column(scale=1):
904
+ gr.HTML('''
905
+ <h2>AIIMS Bhopal Cervical Cancer Pathway</h2>
906
+ ''')
907
+ gr.Image('AIIMS_Bhopal_baseline_process_map.png')
908
+
909
+
910
+ with gr.Row():
911
+ gr.HTML(
912
+ '''
913
+ <h2>Modifiable Parameters</h2>
914
+ Limited to different HR and Procedure rooms for this implementation
915
+ '''
916
+ #num_gynae_residents, num_gynae_consultants, num_pathologists, num_cytotechnicians, num_colposcopy_room, num_ot_rooms
917
+ )
918
+ pt_per_day = gr.Slider(minimum= 1, maximum = 100, label= 'Patients Visiting per day', value= 50, step = int)
919
+ num_gynae_res = gr.Slider(minimum= 1, maximum = 10, label= 'No of Gynae Residents', value= 1, step = int)
920
+ num_gynae_consul = gr.Slider(minimum= 1, maximum = 10, label= 'No of Gynae Consultants', value= 1, step = int)
921
+ num_cytotec = gr.Slider(minimum= 1, maximum = 10, label= 'No of Cytotechnicians', value= 1, step = int)
922
+ num_path = gr.Slider(minimum= 1, maximum = 10, label= 'No of Pathologists', value= 1, step = int)
923
+
924
+ with gr.Row():
925
+ btn = gr.Button(value= "Run the Simulation")
926
+
927
+ with gr.Row(equal_height=True):
928
+ output = gr.Plot(label= 'Simulation Results')
929
+ btn.click(main, [pt_per_day, num_gynae_res, num_gynae_consul, num_cytotec, num_path], output)
930
+
931
+ with gr.Row():
932
+ btn_ref = gr.Button(value = "Refresh the plots")
933
+ btn_ref.click (clear_csv_file)
934
+
935
+ app.launch(share = True)
936
+
937
+
938
+
939
+