creating the app.py file that runs the entire program
Browse filesThis file contains the source code for the program. This is the file that runs on huggingface.
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 |
+
|