|
|
|
""" |
|
Created on 4 February 2025. To submit to Annual Simulation Conference |
|
This version implements a horizontal, resource first approach to DES simulation. |
|
It implements other processes that request the same resources as the CaCx pathway |
|
@author: varad |
|
""" |
|
|
|
|
|
import numpy as np |
|
import simpy |
|
import gradio as gr |
|
import pandas as pd |
|
import random |
|
import csv |
|
import plotly.graph_objects as go |
|
import simpy.resources |
|
import os |
|
import plotly.subplots as sp |
|
|
|
|
|
class parameters (object): |
|
''' |
|
This class contains all the constant non-modifiable parameters that will go into the model |
|
These mostly include service times and the time for which the simulation is supposed to run and halt etc |
|
''' |
|
output_line_no = 0 |
|
|
|
experiment_no = 0 |
|
number_of_runs = 2 |
|
|
|
|
|
obs_gynae_pt_arr_time = 3 |
|
path_pt_arr_time = 1 |
|
cacx_pt_arr_time = 65 |
|
|
|
|
|
|
|
|
|
num_gynae_resident = 10 |
|
num_gynae_consulant = 3 |
|
num_pathologists = 15 |
|
num_cytotechnicians = 12 |
|
|
|
|
|
|
|
num_pap_kits = 50 |
|
num_pathology_consumables = 50 |
|
num_colposcopy_consumables = 50 |
|
num_thermal_consumables = 50 |
|
num_ot_consumables = 50 |
|
|
|
|
|
|
|
num_colposcopy_rooms = 2 |
|
num_ot_rooms = 2 |
|
|
|
|
|
history_exam_time = 10 |
|
|
|
path_processing_time = 5 |
|
path_reporting_time = 5 |
|
colposcopy_time = 25 |
|
thermal_time = 25 |
|
hysterectomy_time = 50 |
|
|
|
|
|
cacx_pt_prop = 0.10 |
|
gynae_pt_procedure_prop = 0.40 |
|
gynae_pt_sx_prop = 0.05 |
|
screen_positivity_rate = 0.1 |
|
biopsy_rate = 0.4 |
|
biopsy_cin_rate = 0.6 |
|
biopsy_cacx_rate = 0.02 |
|
follow_up_rate = 0.65 |
|
|
|
run_time = 131040 |
|
|
|
class scheduled_resource(simpy.Resource): |
|
''' |
|
Extends the simpy.Resource object to include a resource that is only available during certain time of day and day of week |
|
''' |
|
def __init__(self, env, schedule, capacity): |
|
super().__init__(env, capacity) |
|
self.schedule = schedule |
|
self.env = env |
|
|
|
def is_availeble (self): |
|
''' |
|
checks time of day and day of week and returns a boolean based on whether the resource is available at that time or not |
|
''' |
|
current_time = self.env.now |
|
week_minutes = 24 * 7 * 60 |
|
day_minutes = 24 * 60 |
|
|
|
current_day = int((current_time % week_minutes)/day_minutes) |
|
return current_day in self.schedule |
|
|
|
def request (self, *args, **kwargs): |
|
if self.is_availeble == False: |
|
self.env.process(self.wait_for_availability(*args, **kwargs)) |
|
return super().request(*args, **kwargs) |
|
|
|
def wait_for_availability(self, *args, **kwargs): |
|
''' |
|
Creates a waiting process that waits for the resource to be available and then executes the request function |
|
''' |
|
while not self.is_availeble(): |
|
|
|
current_minutes = self.env.now |
|
day_minutes = 24 * 60 |
|
minutes_till_next_day = day_minutes - (current_minutes/day_minutes) |
|
|
|
yield self.env.timeout(minutes_till_next_day) |
|
|
|
request = super().request(*args, **kwargs) |
|
|
|
yield request |
|
return request |
|
|
|
class ca_cx_patient (object): |
|
''' |
|
This class creates patients and declares their individual parameters that explains how they spent their time at the hospital |
|
These individual parameters will then be combined with others in the simulation to get overall estimates |
|
''' |
|
def __init__(self, pt_id): |
|
''' |
|
defines a patient and declares patient level variables to be recorded and written in a dataframe |
|
''' |
|
self.id = pt_id |
|
|
|
|
|
|
|
self.time_at_entered = 0 |
|
self.time_at_hist_exam = 0 |
|
self.time_at_screen_result = 0 |
|
self.time_at_colposcopy = 0 |
|
self.time_at_treatment = 0 |
|
self.time_at_exit = 0 |
|
|
|
self.history_examination_service_time = 0 |
|
self.colposcopy_service_time = 0 |
|
self.treatment_service_time = 0 |
|
self.screen_sample_processing_time = 0 |
|
self.screen_sample_reporting_time = 0 |
|
self.biopsy_sample_processing_time = 0 |
|
self.biopsy_sample_reporting_time = 0 |
|
|
|
|
|
self.hist_exam_wait_time = 0 |
|
self.screen_wait_time = 0 |
|
self.colpo_wait_time = 0 |
|
self.treatment_wait_time = 0 |
|
|
|
|
|
self.hist_exam_q_length = 0 |
|
self.colposcopy_q_length =0 |
|
self.treatment_q_length = 0 |
|
self.screen_processing_q_length = 0 |
|
self.screen_reporting_q_length = 0 |
|
|
|
self.biopsy_processing_q_length = 0 |
|
self.biopsy_reporting_q_length = 0 |
|
|
|
class obs_gynae_pt(object): |
|
''' |
|
This class implements a generic patient that arrives at the Obs Gynae OPD. |
|
A small percentage of them require procedures and a small percent of them require surgery |
|
Hence they request all the same resources as the cacx patient other than the path services |
|
''' |
|
def __init__(self, id): |
|
self.id = id |
|
|
|
class path_patient(object): |
|
''' |
|
This class implements all the other instances of all patients that generate a pathological sample and request the same resources as the |
|
cervical cancer screening and biopsy sample |
|
''' |
|
|
|
def __init__(self, id): |
|
self.id = id |
|
|
|
class Ca_Cx_pathway (object): |
|
''' |
|
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 |
|
results from 100 simulations |
|
''' |
|
def __init__(self, run_number, num_gynae_residents = 4, num_gynae_consultants = 2, num_pathologists = 4, num_cytotechnicians = 2, num_colposcopy_room = 3, num_ot_rooms = 2): |
|
|
|
self.env = simpy.Environment() |
|
|
|
|
|
self.num_gynae_residents = num_gynae_residents |
|
self.num_gynae_consultants = num_gynae_consultants |
|
self.num_pathologists = num_pathologists |
|
self.num_cytotechnicians = num_cytotechnicians |
|
self.num_colposcopy_rooms = num_colposcopy_room |
|
self.num_ot_rooms = num_ot_rooms |
|
self.run_number = run_number |
|
|
|
self.colposcopy_schedule = [0,2,4] |
|
self.ot_schedule = [0,2,4] |
|
|
|
self.gen_pt_counter = 0 |
|
self.cacx_pt_counter = 0 |
|
self.path_pt_counter = 0 |
|
|
|
self.run_number = self.run_number + 1 |
|
|
|
|
|
self.gynae_residents = simpy.Resource(self.env, capacity=num_gynae_residents) |
|
self.gynae_consultants = simpy.Resource(self.env, capacity=num_gynae_consultants) |
|
self.pathologist = simpy.Resource(self.env, capacity=num_pathologists) |
|
self.cytotechnician = simpy.Resource(self.env, capacity=num_cytotechnicians) |
|
|
|
|
|
self.pap_kit = simpy.Resource(self.env, capacity=parameters.num_pap_kits) |
|
self.pathology_consumables = simpy.Resource(self.env, capacity=parameters.num_pathology_consumables) |
|
self.colposcopy_consumables = simpy.Resource(self.env, capacity=parameters.num_colposcopy_consumables) |
|
self.thermal_consumables = simpy.Resource(self.env, capacity=parameters.num_thermal_consumables) |
|
self.ot_consumables = simpy.Resource(self.env, capacity=parameters.num_ot_consumables) |
|
|
|
|
|
self.colposcopy_room = scheduled_resource(self.env, self.colposcopy_schedule, capacity=parameters.num_colposcopy_rooms, ) |
|
self.ot_room = scheduled_resource(self.env, self.ot_schedule, capacity = parameters.num_ot_rooms) |
|
|
|
|
|
self.individual_results = pd.DataFrame({ |
|
"UHID" : [], |
|
"Time_Entered_in_System":[], |
|
|
|
"Hist_Exam_Q_Length":[], |
|
"Screen_Processing_Q_Length" : [], |
|
"Screen_reporting_Q_Length" : [], |
|
"Colposcopy_Q_Length" :[], |
|
"Biopsy_Processing_Q_Length" : [], |
|
"Biopsy_Reporting_Q_Length" : [], |
|
"Treatment_Q_length" :[], |
|
|
|
"Time_at_Hist_Exam" :[], |
|
"Time_at_colpo" : [], |
|
"Time_at_treatment" : [], |
|
"Time_at_screening_result":[], |
|
|
|
"Hist_Exam_Wait_time":[], |
|
"Screen_wait_time":[], |
|
"Colpo_Wait_time":[], |
|
|
|
"History_and_Examination_time": [], |
|
"Screen_processing_time":[], |
|
"Screen_reporting_time":[], |
|
"Biopsy_processing_time":[], |
|
"Biopsy_reporting_time":[], |
|
"Colposcopy_time":[], |
|
"Treatment_time":[], |
|
|
|
"Exit_time":[] |
|
}) |
|
|
|
|
|
|
|
self.time_to_screen_result = 0 |
|
self.time_to_colposcopy = 0 |
|
self.time_to_treatment = 0 |
|
self.total_time_in_system = 0 |
|
|
|
|
|
|
|
|
|
|
|
self.max_q_len_screen_processing = 0 |
|
self.max_q_len_screen_reporting = 0 |
|
self.max_q_len_colposcopy = 0 |
|
self.max_q_len_biopsy_processing = 0 |
|
self.max_q_len_biopsy_reporting = 0 |
|
self.max_q_len_treatment = 0 |
|
|
|
|
|
self.gynae_residents_utilisation = 0 |
|
self.gynae_consultants_utlisation = 0 |
|
self.cytotechnician_utilisation = 0 |
|
self.pathologist_utilisation = 0 |
|
|
|
|
|
|
|
def is_within_working_hours(self): |
|
''' |
|
Checks whether the current simulation time is within working hours and returns a boolean. |
|
''' |
|
current_sim_mins = self.env.now % (24 * 60) |
|
start_mins = 9 * 60 |
|
end_mins = 17 * 60 |
|
return start_mins <= current_sim_mins < end_mins |
|
|
|
def gen_obs_gynae_pt(self): |
|
''' |
|
Generates a fictional patient that either goes through the normal pathway or |
|
through the cacx pathway according to a distribution, they undergo and OPD, this generates a sample which undergoes processing, after results are |
|
conveyed, if positive, patient only then moves on to the next step i.e. colposcopy. |
|
''' |
|
while True: |
|
self.gen_pt_counter += 1 |
|
generic_pt = obs_gynae_pt(self.gen_pt_counter) |
|
self.env.process(self.obs_gynae_hist_exam(generic_pt)) |
|
wait_for_next_pt = random.expovariate(1/parameters.obs_gynae_pt_arr_time) |
|
yield self.env.timeout(wait_for_next_pt) |
|
|
|
def gen_cacx_pt(self): |
|
''' |
|
generates a cancer cervix patient and has its own interarrival rate |
|
''' |
|
while True: |
|
|
|
self.cacx_pt_counter += 1 |
|
screening_patient = ca_cx_patient(self.cacx_pt_counter) |
|
|
|
|
|
|
|
|
|
|
|
|
|
screening_patient.time_at_entered = self.env.now |
|
|
|
self.env.process(self.cacx_hist_exam(screening_patient)) |
|
parameters.output_line_no += 1 |
|
|
|
|
|
wait_for_next_cacx_pt = random.expovariate(1/parameters.cacx_pt_arr_time) |
|
yield self.env.timeout(wait_for_next_cacx_pt) |
|
self.add_to_individual_results(screening_patient) |
|
def gen_path_pt(self): |
|
''' |
|
Generates patients that generate a pathological sample and request path resources |
|
''' |
|
while True: |
|
|
|
self.path_pt_counter += 1 |
|
path_pt = path_patient(self.path_pt_counter) |
|
self.env.process(self.gen_path_process(path_pt)) |
|
wait_for_path_pt = random.expovariate(1/parameters.path_pt_arr_time) |
|
yield self.env.timeout(wait_for_path_pt) |
|
|
|
def obs_gynae_hist_exam(self, patient): |
|
''' |
|
All non tracked patients go through a general history and examination |
|
''' |
|
with self.gynae_residents.request() as gynae_res: |
|
yield gynae_res |
|
|
|
|
|
history_examination_time = random.triangular(parameters.history_exam_time/2, parameters.history_exam_time, parameters.history_exam_time *2 ) |
|
yield self.env.timeout(history_examination_time) |
|
|
|
if random.random() < parameters.gynae_pt_procedure_prop: |
|
self.env.process(self.obs_gynae_procedure(patient)) |
|
|
|
def cacx_hist_exam(self, patient): |
|
''' |
|
Patient undergoes history and examination and in the process also generates the screening sample |
|
''' |
|
|
|
start_q_time = self.env.now |
|
|
|
patient.hist_exam_q_length = len(self.gynae_residents.queue) |
|
with self.gynae_residents.request() as gynae_res, self.pap_kit.request() as pap, self.pathology_consumables.request() as path_consum : |
|
yield gynae_res and pap and path_consum |
|
|
|
|
|
end_q_time = self.env.now |
|
patient.hist_exam_wait_time = end_q_time - start_q_time |
|
|
|
patient.time_at_hist_exam = self.env.now |
|
history_examination_time = random.triangular(parameters.history_exam_time/2, parameters.history_exam_time, parameters.history_exam_time *2 ) |
|
patient.history_examination_service_time = history_examination_time |
|
yield self.env.timeout(history_examination_time) |
|
|
|
|
|
|
|
screening_sample_gen = self.env.process(self.screening(patient)) |
|
|
|
screen_result = yield screening_sample_gen |
|
|
|
|
|
if screen_result: |
|
self.env.process(self.call_for_follow_up(patient)) |
|
else: |
|
|
|
patient.time_at_exit = self.env.now |
|
self.add_to_individual_results(( patient)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def gen_path_process(self, patient): |
|
''' |
|
A generic path sample that requests path resources |
|
''' |
|
with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as scr_proc_consum: |
|
yield cytotec and scr_proc_consum |
|
|
|
path_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time *2) |
|
yield self.env.timeout(path_sample_processing_time) |
|
|
|
with self.pathologist.request() as path: |
|
yield path |
|
path_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time * 2) |
|
yield self.env.timeout(path_sample_reporting_time) |
|
|
|
def screening (self, patient): |
|
''' |
|
This function simulation the processing and reporting of screen samples and returns a boolean whether the result is positive or negative |
|
''' |
|
patient.screen_processing_q_length = len(self.cytotechnician.queue) |
|
|
|
start_q_time = self.env.now |
|
with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as scr_proc_consum: |
|
yield cytotec and scr_proc_consum |
|
|
|
screen_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time *2) |
|
patient.screen_sample_processing_time = screen_sample_processing_time |
|
yield self.env.timeout(screen_sample_processing_time) |
|
|
|
|
|
patient.screen_reporting_q_length = len(self.pathologist.queue) |
|
with self.pathologist.request() as path: |
|
yield path |
|
screen_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time * 2) |
|
patient.screen_sample_reporting_time = screen_sample_reporting_time |
|
yield self.env.timeout(screen_sample_reporting_time) |
|
|
|
end_q_time = self.env.now |
|
patient.screen_wait_time = end_q_time - start_q_time |
|
|
|
patient.time_at_screen_result = self.env.now |
|
|
|
if random.random() < parameters.screen_positivity_rate: |
|
return True |
|
else: |
|
return False |
|
|
|
|
|
def obs_gynae_procedure(self, patient): |
|
''' |
|
A generic procedure such as EA/ECC, other procedures done in the procedure room, part of the untracked pathway |
|
''' |
|
with self.gynae_consultants.request() as gynae_consul, self.colposcopy_room.request() as colpo_room: |
|
yield gynae_consul and colpo_room |
|
|
|
|
|
|
|
procedure_service_time = random.triangular(parameters.colposcopy_time/2, parameters.colposcopy_time, parameters.colposcopy_time *2) |
|
yield self.env.timeout(procedure_service_time) |
|
|
|
if random.random() < parameters.gynae_pt_sx_prop: |
|
self.env.process(self.gynae_sx(patient)) |
|
|
|
def call_for_follow_up (self, patient): |
|
''' |
|
Gynaecology residents |
|
''' |
|
|
|
|
|
with self.gynae_residents.request() as gynae_res: |
|
yield gynae_res |
|
|
|
|
|
if random.random() < parameters.follow_up_rate: |
|
|
|
self.env.process(self.colposcopy(patient)) |
|
|
|
else: |
|
|
|
patient.time_at_exit = self.env.now |
|
|
|
self.add_to_individual_results(patient) |
|
|
|
def colposcopy(self, patient): |
|
''' |
|
Patient that was generated undergoes colposcopy |
|
''' |
|
|
|
|
|
|
|
colpo_q_len_list = [len(self.gynae_consultants.queue), len(self.colposcopy_room.queue) ] |
|
patient.colposcopy_q_length = max(colpo_q_len_list) |
|
|
|
start_q_time = self.env.now |
|
|
|
with self.gynae_consultants.request() as gynae_consul, self.colposcopy_consumables.request() as gynae_consumables, self.colposcopy_room.request() as colpo_room: |
|
yield gynae_consul and gynae_consumables and colpo_room |
|
|
|
|
|
end_q_time = self.env.now |
|
patient.colpo_wait_time = end_q_time - start_q_time |
|
patient.time_at_colposcopy = self.env.now |
|
|
|
|
|
colposcopy_service_time = random.triangular(parameters.colposcopy_time/2, parameters.colposcopy_time, parameters.colposcopy_time *2) |
|
patient.colposcopy_service_time = colposcopy_service_time |
|
yield self.env.timeout(colposcopy_service_time) |
|
|
|
|
|
|
|
biopsy_sample_gen = self.env.process(self.biopsy(patient)) |
|
|
|
biopsy_result = yield biopsy_sample_gen |
|
|
|
if biopsy_result == 1: |
|
self.env.process(self.thermal_ablation(patient)) |
|
elif biopsy_result == 2: |
|
self.env.process(self.hysterectomy(patient)) |
|
else: |
|
patient.time_at_exit = self.env.now |
|
self.add_to_individual_results((patient)) |
|
|
|
def biopsy(self, patient): |
|
''' |
|
implementation is very similar to the screening function |
|
''' |
|
patient.biopsy_processing_q_length = len(self.cytotechnician.queue) |
|
with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as path_consum: |
|
yield cytotec and path_consum |
|
|
|
biopsy_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time * 2) |
|
patient.biopsy_sample_processing_time = biopsy_sample_processing_time |
|
yield self.env.timeout(biopsy_sample_processing_time) |
|
|
|
patient.biopsy_reporting_q_length = len(self.pathologist.queue) |
|
with self.pathologist.request() as path: |
|
yield path |
|
|
|
biopsy_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time *2) |
|
patient.biopsy_sample_reporting_time = biopsy_sample_reporting_time |
|
yield self.env.timeout(biopsy_sample_reporting_time) |
|
|
|
if random.random() < parameters.biopsy_cin_rate: |
|
return 1 |
|
elif parameters.biopsy_cin_rate < random.random() < parameters.biopsy_cacx_rate: |
|
return 2 |
|
else: |
|
return 3 |
|
|
|
def biopsy_sample_processing(self): |
|
''' |
|
Biopsy sample if prepared undergoes processing |
|
''' |
|
|
|
self.pt_biopsy_sample.biopsy_processing_q_length = len(self.cytotechnician.queue) |
|
|
|
with self.cytotechnician.request() as cytotec, self.pathology_consumables.request() as path_consum: |
|
yield cytotec and path_consum |
|
|
|
|
|
biopsy_sample_processing_time = random.triangular(parameters.path_processing_time/2, parameters.path_processing_time, parameters.path_processing_time * 2) |
|
self.pt_biopsy_sample.biopsy_sample_processing_time = biopsy_sample_processing_time |
|
yield self.env.timeout(biopsy_sample_processing_time) |
|
|
|
|
|
self.env.process(self.biopsy_sample_reporting()) |
|
|
|
def biopsy_sample_reporting(self): |
|
''' |
|
Biopsy sample if taken undergoes reporting after processing |
|
''' |
|
|
|
self.pt_biopsy_sample.biopsy_reporting_q_length = len(self.pathologist.queue) |
|
|
|
with self.pathologist.request() as path: |
|
yield path |
|
|
|
|
|
biopsy_sample_reporting_time = random.triangular(parameters.path_reporting_time/2, parameters.path_reporting_time, parameters.path_reporting_time *2) |
|
self.pt_biopsy_sample.biopsy_sample_reporting_time = biopsy_sample_reporting_time |
|
yield self.env.timeout(biopsy_sample_reporting_time) |
|
|
|
|
|
biopsy_result = random.random() |
|
if biopsy_result < parameters.biopsy_cin_rate: |
|
self.env.process(self.thermal_ablation()) |
|
|
|
elif parameters.biopsy_cin_rate < biopsy_result < parameters.biopsy_cacx_rate: |
|
self.env.process(self.hysterectomy()) |
|
|
|
else: |
|
self.patient.time_at_exit = self.env.now |
|
|
|
Ca_Cx_pathway.add_to_individual_results(self) |
|
|
|
def thermal_ablation(self, patient): |
|
''' |
|
If indicated, pt undergoes thermal ablation |
|
''' |
|
thermal_q_len_list = [len(self.gynae_consultants.queue), len(self.colposcopy_room.queue) ] |
|
patient.treatment_q_length = max(thermal_q_len_list) |
|
|
|
|
|
with self.gynae_consultants.request() as gynae_consul, self.thermal_consumables.request() as thermal_consum, self.colposcopy_room.request() as colpo_room: |
|
yield gynae_consul and thermal_consum and colpo_room |
|
|
|
patient.time_at_treatment = self.env.now |
|
|
|
thermal_ablation_time = random.triangular(parameters.thermal_time/2, parameters.thermal_time, parameters.thermal_time *2) |
|
patient.treatment_service_time = thermal_ablation_time |
|
yield self.env.timeout(thermal_ablation_time) |
|
|
|
|
|
|
|
patient.time_at_exit = self.env.now |
|
|
|
self.add_to_individual_results(patient) |
|
|
|
def leep (self): |
|
''' |
|
if indicated, patient undergoes LEEP |
|
''' |
|
|
|
pass |
|
|
|
def gynae_sx(self, patient): |
|
''' |
|
All surgeries other than CaCx surgery, part of the untracked pathway |
|
''' |
|
with self.gynae_consultants.request() as gynae_consul, self.ot_consumables.request() as ot_consum, self.ot_room.request() as ot_room: |
|
yield gynae_consul and ot_consum and ot_room |
|
|
|
|
|
sx_time = random.triangular(parameters.hysterectomy_time/2, parameters.hysterectomy_time, parameters.hysterectomy_time *2) |
|
yield self.env.timeout(sx_time) |
|
|
|
def hysterectomy (self, patient): |
|
''' |
|
if indicated, patient undergoes hysterectomy |
|
''' |
|
hyst_q_len_list = [len(self.gynae_consultants.queue), len(self.ot_room.queue)] |
|
patient.treatment_q_length = max(hyst_q_len_list) |
|
|
|
|
|
with self.gynae_consultants.request() as gynae_consul, self.ot_consumables.request() as ot_consum, self.ot_room as ot_room: |
|
yield gynae_consul and ot_consum and ot_room |
|
|
|
self.patient.time_at_treatment = self.env.now |
|
|
|
hysterectomy_time = random.triangular(parameters.hysterectomy_time/2, parameters.hysterectomy_time, parameters.hysterectomy_time *2) |
|
self.patient.treatment_service_time = hysterectomy_time |
|
yield self.env.timeout(hysterectomy_time) |
|
|
|
|
|
|
|
patient.time_at_exit = self.env.now |
|
|
|
self.add_to_individual_results(patient) |
|
|
|
def add_to_individual_results (self, patient): |
|
''' |
|
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 |
|
Hence to make my job easier, writing a function that does this in every function without having to write too much. |
|
''' |
|
df_to_add = pd.DataFrame({ |
|
"UHID" : [patient.id], |
|
"Time_Entered_in_System":[patient.time_at_entered], |
|
|
|
"Hist_Exam_Q_Length":[patient.hist_exam_q_length], |
|
"Screen_Processing_Q_Length" : [patient.screen_processing_q_length], |
|
"Screen_reporting_Q_Length" : [patient.screen_reporting_q_length], |
|
"Colposcopy_Q_Length":[patient.colposcopy_q_length], |
|
"Biopsy_Processing_Q_Length" : [patient.biopsy_processing_q_length], |
|
"Biopsy_Reporting_Q_Length" : [patient.biopsy_reporting_q_length], |
|
"Treatment_Q_length":[patient.treatment_q_length], |
|
|
|
"Time_at_Hist_Exam" :[patient.time_at_hist_exam], |
|
"Time_at_colposcopy" : [patient.time_at_colposcopy], |
|
"Time_at_treatment" : [patient.time_at_treatment], |
|
"Time_at_screening_result":[patient.time_at_screen_result], |
|
|
|
"Hist_Exam_Wait_time":[patient.hist_exam_wait_time], |
|
"Screen_wait_time":[patient.screen_wait_time], |
|
"Colpo_Wait_time":[patient.colpo_wait_time], |
|
|
|
"History_and_Examination_time": [patient.history_examination_service_time], |
|
"Screen_processing_time":[patient.screen_sample_processing_time], |
|
"Screen_reporting_time":[patient.screen_sample_reporting_time], |
|
"Biopsy_processing_time":[patient.biopsy_sample_processing_time], |
|
"Biopsy_reporting_time":[patient.biopsy_sample_reporting_time], |
|
"Colposcopy_time":[patient.colposcopy_service_time], |
|
"Treatment_time":[patient.treatment_service_time], |
|
"Exit_time":[patient.time_at_exit] |
|
|
|
}) |
|
df_to_add.set_index('UHID', inplace= True) |
|
self.individual_results = pd.concat([self.individual_results, df_to_add]) |
|
|
|
def individual_results_processor(self): |
|
''' |
|
Processes the individual results dataframe by adding columns from which KPI's can be calculated |
|
''' |
|
|
|
self.individual_results['Time_to_screen_results'] = self.individual_results['Time_at_screening_result'] - self.individual_results['Time_Entered_in_System'] |
|
self.individual_results['Time_to_Colposcopy'] = self.individual_results['Time_at_colposcopy'] - self.individual_results['Time_Entered_in_System'] |
|
self.individual_results['Time_to_Treatment'] = self.individual_results['Time_at_treatment'] - self.individual_results['Time_Entered_in_System'] |
|
self.individual_results['Total_time_in_system'] = self.individual_results['Exit_time'] - self.individual_results['Time_Entered_in_System'] |
|
|
|
|
|
|
|
self.individual_results['Gynae_res_busy_time'] = self.individual_results['History_and_Examination_time'] |
|
self.individual_results['Cytotech_busy_time'] = self.individual_results['Screen_processing_time'] + self.individual_results['Biopsy_processing_time'] |
|
self.individual_results['Pathologist_busy_time'] = self.individual_results['Screen_reporting_time'] + self.individual_results['Biopsy_reporting_time'] |
|
self.individual_results['Gynae_consul_busy_time'] = self.individual_results['Colposcopy_time'] + self.individual_results['Treatment_time'] |
|
|
|
def KPI_calculator(self): |
|
''' |
|
Function that calculates the various KPIs from an individual run from the different columns of the individual results dataframe |
|
These are KPIs for a signle run |
|
''' |
|
|
|
self.max_q_len_hist_exam = self.individual_results['Hist_Exam_Q_Length'].max() |
|
self.max_q_len_screen_processing = self.individual_results['Screen_Processing_Q_Length'].max() |
|
self.max_q_len_screen_reporting = self.individual_results['Screen_reporting_Q_Length'].max() |
|
self.max_q_len_colposcopy = self.individual_results['Colposcopy_Q_Length'].max() |
|
self.max_q_len_biopsy_processing = self.individual_results['Biopsy_Processing_Q_Length'].max() |
|
self.max_q_len_biopsy_reporting = self.individual_results['Biopsy_Reporting_Q_Length'].max() |
|
self.max_q_len_treatment = self.individual_results['Treatment_Q_length'].max() |
|
|
|
|
|
self.gynae_residents_utilisation = self.individual_results['Gynae_res_busy_time'].sum()/(parameters.run_time * self.num_gynae_residents) |
|
self.cytotechnician_utilisation = self.individual_results['Cytotech_busy_time'].sum()/(parameters.run_time * self.num_cytotechnicians) |
|
self.gynae_consultants_utlisation = self.individual_results['Gynae_consul_busy_time'].sum() / (parameters.run_time * self.num_gynae_consultants) |
|
self.pathologist_utilisation = self.individual_results['Pathologist_busy_time'].sum() / (parameters.run_time * self.num_pathologists) |
|
|
|
|
|
self.med_hist_exam_wait_time = self.individual_results['Hist_Exam_Wait_time'].median() |
|
self.med_screen_wait_time = self.individual_results['Screen_wait_time'].median() |
|
self.med_colpo_wait_time = self.individual_results['Colpo_Wait_time'].median() |
|
|
|
|
|
|
|
temp_colpo_time_df = self.individual_results['Time_to_Colposcopy'][self.individual_results['Time_to_Colposcopy'] >0] |
|
temp_treatmet_time_df = self.individual_results['Time_to_Treatment'][self.individual_results['Time_to_Treatment'] >0] |
|
|
|
self.med_time_to_scr_res = self.individual_results['Time_to_screen_results'].median() |
|
self.med_time_to_colpo = temp_colpo_time_df.median() |
|
self.med_time_to_treatment = temp_treatmet_time_df.median() |
|
self.med_tot_time_in_system = self.individual_results['Total_time_in_system'].median() |
|
|
|
def export_row_to_csv(self): |
|
''' |
|
Creates a new dataframe with trial results and exports a single row to that dataframe after each run |
|
''' |
|
with open ('kpi_trial_results.csv', 'a')as f: |
|
writer = csv.writer(f, delimiter= ',') |
|
row_to_add = [ |
|
self.run_number, |
|
self.max_q_len_hist_exam, |
|
self.max_q_len_screen_processing, |
|
self.max_q_len_screen_reporting, |
|
self.max_q_len_colposcopy, |
|
self.max_q_len_biopsy_processing, |
|
self.max_q_len_biopsy_reporting, |
|
self.max_q_len_treatment, |
|
|
|
self.gynae_residents_utilisation, |
|
self.gynae_consultants_utlisation, |
|
self.pathologist_utilisation, |
|
self.cytotechnician_utilisation, |
|
|
|
self.med_hist_exam_wait_time, |
|
self.med_screen_wait_time, |
|
self.med_colpo_wait_time, |
|
|
|
self.med_time_to_scr_res, |
|
self.med_time_to_colpo, |
|
self.med_time_to_treatment, |
|
self.med_tot_time_in_system |
|
] |
|
writer.writerow(row_to_add) |
|
|
|
def run(self): |
|
''' |
|
Runs the simulation and calls the generator function. |
|
''' |
|
self.env.process(self.gen_obs_gynae_pt()) |
|
self.env.process(self.gen_cacx_pt()) |
|
self.env.process(self.gen_path_pt()) |
|
self.env.run(until= parameters.run_time) |
|
self.individual_results_processor() |
|
self.individual_results.drop_duplicates(subset='Time_Entered_in_System', keep='last') |
|
self.individual_results.to_csv('individual_results.csv') |
|
self.KPI_calculator() |
|
self.export_row_to_csv() |
|
print(f"Completed {self.run_number} run") |
|
|
|
|
|
class summary_statistics(object): |
|
''' |
|
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 |
|
from 100 simulations for different number of independent variables (such as patients) |
|
''' |
|
def __init__(self): |
|
pass |
|
|
|
|
|
|
|
def gen_final_summary_table (self): |
|
''' |
|
Generates a table, essentially a row of summary statistics for 100 runs with a particular initial setting. |
|
''' |
|
with open ('final_summary_table.csv', 'w') as f: |
|
writer = csv.writer(f, delimiter= ',') |
|
column_headers = [ |
|
"Experiment_No" , |
|
"Max_Hist_Exam_Q_len", |
|
"Max_Scr_Proc_Q_len" , |
|
'Max_Scr_Rep_Q_len' , |
|
'Max_Colpo_Q_len' , |
|
"Max_Biop_Proc_Q_Len" , |
|
"Max_Biop_Rep_Q_Len" , |
|
"Max_T/t_Q_len" , |
|
|
|
|
|
'Gynae_Res_%_util', |
|
'Gynae_consul_%_util', |
|
'Path_%_util', |
|
'Cytotec_%_util', |
|
|
|
|
|
'Hist_Exam_Wait_Time', |
|
'Screening_Wait_Time', |
|
"Colpo_Wait_Time", |
|
|
|
|
|
'Time_to_screening_results', |
|
'Time_to_colposcopy', |
|
'Time_to_treatment', |
|
'Total_time_in_system' ] |
|
|
|
writer.writerow(column_headers) |
|
|
|
def calculate_summary_statistics(self): |
|
''' |
|
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 |
|
onto the final_summary_table csv |
|
''' |
|
filepath = 'kpi_trial_results.csv' |
|
df_to_read = pd.read_csv(filepath) |
|
self.max_hist_exam_q_len = df_to_read['Max_Hist_Exam_Q_len'].median() |
|
self.max_scr_proc_q_len = df_to_read['Max_Scr_Proc_Q_len'].median() |
|
self.max_scr_rep_q_len = df_to_read['Max_Scr_Rep_Q_len'].median() |
|
self.max_colpo_q_len = df_to_read['Max_Colpo_Q_len'].median() |
|
self.max_biop_proc_q_len = df_to_read['Max_Biop_Proc_Q_Len'].median() |
|
self.max_biop_rep_q_len = df_to_read['Max_Biop_Rep_Q_Len'].median() |
|
self.max_treatment_q_len = df_to_read['Max_T/t_Q_len'].median() |
|
|
|
self.med_gynae_res_util = df_to_read['Gynae_Res_%_util'].median() |
|
self.med_gynae_consul_util = df_to_read['Gynae_consul_%_util'].median() |
|
self.med_path_util = df_to_read['Path_%_util'].median() |
|
self.med_cytotec_util = df_to_read['Cytotec_%_util'].median() |
|
|
|
self.med_hist_exam_wait_time = df_to_read['Hist_Exam_Wait_Time'].median() |
|
self.med_scr_wait_time = df_to_read['Screening_Wait_Time'].median() |
|
self.med_colpo_wait_time = df_to_read['Colpo_Wait_Time'].median() |
|
|
|
self.med_time_to_scr = df_to_read['Time_to_screening_results'].median() |
|
self.med_time_to_colpo = df_to_read['Time_to_colposcopy'].median() |
|
self.med_time_to_tt = df_to_read['Time_to_treatment'].median() |
|
self.med_tot_time_in_sys = df_to_read['Total_time_in_system'].median() |
|
|
|
|
|
|
|
def populate_final_summary_table(self): |
|
''' |
|
Updates the final summary table one row whenever it is called. |
|
''' |
|
with open ('final_summary_table.csv', 'a') as f: |
|
writer = csv.writer(f, delimiter= ',') |
|
row_to_add = [parameters.experiment_no, |
|
self.max_hist_exam_q_len, |
|
self.max_scr_proc_q_len, |
|
self.max_scr_rep_q_len, |
|
self.max_colpo_q_len, |
|
self.max_biop_proc_q_len, |
|
self.max_biop_rep_q_len, |
|
self.max_treatment_q_len, |
|
|
|
self.med_gynae_res_util, |
|
self.med_gynae_consul_util, |
|
self.med_path_util, |
|
self.med_cytotec_util, |
|
|
|
self.med_hist_exam_wait_time, |
|
self.med_scr_wait_time, |
|
self.med_colpo_wait_time, |
|
|
|
self.med_time_to_scr, |
|
self.med_time_to_colpo, |
|
self.med_time_to_tt, |
|
self.med_tot_time_in_sys |
|
|
|
] |
|
writer.writerow(row_to_add) |
|
|
|
def clear_csv_file(): |
|
'''f |
|
Erases all the contents of a csv file. Used in the refresh button of the gradio app to start fresh |
|
''' |
|
|
|
parameters.experiment_no = 0 |
|
with open ('final_summary_table.csv', 'w') as f: |
|
pass |
|
open_final_table = summary_statistics() |
|
open_final_table.gen_final_summary_table() |
|
|
|
|
|
def plotly_plotter(): |
|
filepath = 'final_summary_table.csv' |
|
df_to_plot = pd.read_csv(filepath) |
|
fig = sp.make_subplots(rows = 1, cols= 3, subplot_titles= ("Max Queue Length", |
|
'HR % Utilisation','Time to important events')) |
|
|
|
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) |
|
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) |
|
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) |
|
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) |
|
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) |
|
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) |
|
|
|
|
|
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) |
|
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) |
|
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) |
|
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) |
|
|
|
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) |
|
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) |
|
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) |
|
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) |
|
|
|
return fig |
|
|
|
|
|
def gen_kpi_table(): |
|
|
|
|
|
with open('kpi_trial_results.csv', 'w') as f: |
|
writer = csv.writer(f, delimiter= ',') |
|
column_headers = [ |
|
"Run_Number" , |
|
"Max_Hist_Exam_Q_len", |
|
"Max_Scr_Proc_Q_len" , |
|
'Max_Scr_Rep_Q_len' , |
|
'Max_Colpo_Q_len' , |
|
"Max_Biop_Proc_Q_Len" , |
|
"Max_Biop_Rep_Q_Len" , |
|
"Max_T/t_Q_len" , |
|
|
|
|
|
'Gynae_Res_%_util', |
|
'Gynae_consul_%_util', |
|
'Path_%_util', |
|
'Cytotec_%_util', |
|
|
|
'Hist_Exam_Wait_Time', |
|
'Screening_Wait_Time', |
|
"Colpo_Wait_Time", |
|
|
|
|
|
'Time_to_screening_results', |
|
'Time_to_colposcopy', |
|
'Time_to_treatment', |
|
'Total_time_in_system' ] |
|
|
|
writer.writerow(column_headers) |
|
|
|
open_final_table = summary_statistics() |
|
open_final_table.gen_final_summary_table() |
|
|
|
def main(pt_per_year = 2000, hist_exam_time = 10, num_gynae_res = 1, num_gynae_consul = 8, num_cytotec = 9, num_path = 11): |
|
''' |
|
This function will run the simulation for different independent variables that we need. |
|
''' |
|
parameters.cacx_pt_arr_time = int(parameters.run_time / pt_per_year) |
|
parameters.experiment_no += 1 |
|
parameters.history_exam_time = hist_exam_time |
|
print (f'Experiment Number: {parameters.experiment_no}') |
|
|
|
sum_stats = summary_statistics() |
|
|
|
gen_kpi_table() |
|
for run in range (parameters.number_of_runs): |
|
print(f'Run {run+1} in {parameters.number_of_runs}') |
|
my_sim_model = Ca_Cx_pathway(run, num_gynae_res, num_gynae_consul, num_cytotec, num_path) |
|
my_sim_model.run() |
|
sum_stats.calculate_summary_statistics() |
|
sum_stats.populate_final_summary_table() |
|
return plotly_plotter() |
|
|
|
|
|
|
|
with gr.Blocks() as app: |
|
gr.HTML( |
|
''' |
|
<h1>Cervical Cancer DES App</h1> |
|
''' |
|
) |
|
|
|
with gr.Row(equal_height= True): |
|
|
|
with gr.Column(scale = 1): |
|
gr.HTML( |
|
''' |
|
<h2>Parameter Table</h2> |
|
<Table> |
|
<tr> |
|
<th>Parameter name</th> |
|
<th>Value</th> |
|
<th>Reference/Justification</th> |
|
</tr> |
|
<tr> |
|
<th>Simulation Parameters</th> |
|
</tr> |
|
<tr> |
|
<td>Number of Runs</td> |
|
<td>3</td> |
|
<td>For better run time</td> |
|
</tr> |
|
<tr> |
|
<td>Number of Gynae Pt in a year</td> |
|
<td>46,000</td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
</tr> |
|
<tr> |
|
<td>Number of Pathology Samples</td> |
|
<td>250,000</td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
</tr> |
|
<tr> |
|
<td>Total Run Time</td> |
|
<td>131040</td> |
|
<td>Total working hours in a year</td> |
|
</tr> |
|
<tr> |
|
<th>Service Times</th> |
|
</tr> |
|
<tr> |
|
<td>History and Examination</td> |
|
<td>10 mins</td> |
|
<td>Personal Experience</td> |
|
</tr> |
|
<tr> |
|
<td>Path Sample Processing time</td> |
|
<td>30 mins</td> |
|
<td>Pap smear staining process manual</td> |
|
</tr> |
|
<tr> |
|
<td>Path sample reporting time</td> |
|
<td>30 mins</td> |
|
<td>BMJ Report</td> |
|
</tr> |
|
<tr> |
|
<td>Colposcopy</td> |
|
<td>25 mins</td> |
|
<td>BMJ Report</td> |
|
</tr> |
|
<tr> |
|
<td>Thermal Ablation</td> |
|
<td>25 mins</td> |
|
<td>BMJ Report</td> |
|
</tr> |
|
<tr> |
|
<td>Hysterectomy</td> |
|
<td>50 mins</td> |
|
<td>BMJ Report</td> |
|
</tr> |
|
<tr> |
|
<th>Epidemiological Parameters</th> |
|
</tr> |
|
<tr> |
|
<td>Screen positivity</td> |
|
<td>10 %</td> |
|
<td>Self Meta Analysis</td> |
|
</tr> |
|
<tr> |
|
<td>Biopsy positivity rate</td> |
|
<td>40%</td> |
|
<td>Dummy value</td> |
|
</tr> |
|
<tr> |
|
<td>Biopsy CIN Ratio</td> |
|
<td>60%</td> |
|
<td>Dummy Value</td> |
|
</tr> |
|
<tr> |
|
<td>Biopsy CaCx Ratio</td> |
|
<td>2 %</td> |
|
<td>Dummy Value</td> |
|
</tr> |
|
<tr> |
|
<td>Follow up rate after positive screen</td> |
|
<td>65 %</td> |
|
<td>Self Meta-Analysis</td> |
|
</tr> |
|
<tr> |
|
<th>Hospital Resources</th> |
|
</tr> |
|
|
|
<tr> |
|
<td>Num Gynae Residents</td> |
|
<td> 10</td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
</tr> |
|
<tr> |
|
<td>Num Gynae Consultants</td> |
|
<td> 3 </td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
</tr> |
|
<tr> |
|
<td>Num Cytotechnicians</td> |
|
<td> 12</td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
</tr> |
|
<tr> |
|
<td>Num Pathologists</td> |
|
<td> 15 </td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
<tr> |
|
<td>Num Colposcopy/Procedure Rooms</td> |
|
<td> 2</td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
</tr> |
|
<tr> |
|
<td>Num Operation Theatres</td> |
|
<td> 2 </td> |
|
<td>AIIMS Bhopal Annual Report</td> |
|
|
|
</Table> |
|
|
|
''' |
|
) |
|
with gr.Column(scale=1): |
|
gr.HTML(''' |
|
<h2>AIIMS Bhopal Cervical Cancer pathways and other interacting pathways</h2> |
|
''') |
|
gr.Image('Pathways_modelled.png') |
|
|
|
|
|
with gr.Row(): |
|
gr.HTML( |
|
''' |
|
<h2>Modifiable Parameters</h2> |
|
Limited to different HR and Procedure rooms for this implementation |
|
''' |
|
|
|
) |
|
pt_per_year = gr.Slider(minimum= 1000, maximum = 10000, label= 'Patients Visiting per day', value= 2000, step = 1000) |
|
hist_exam_time = gr.Slider(minimum= 5, maximum = 40, label= 'Time taken for History and Examination (mins)', value= 10, step = 5) |
|
|
|
|
|
with gr.Row(): |
|
btn = gr.Button(value= "Run the Simulation") |
|
|
|
with gr.Row(equal_height=True): |
|
output = gr.Plot(label= 'Simulation Results') |
|
btn.click(main, [pt_per_year, hist_exam_time], output) |
|
|
|
with gr.Row(): |
|
btn_ref = gr.Button(value = "Refresh the plots") |
|
btn_ref.click (clear_csv_file) |
|
|
|
app.launch() |
|
|
|
|
|
|
|
|
|
|