h-adminsim 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- h_adminsim/__init__.py +5 -0
- h_adminsim/admin_staff.py +280 -0
- h_adminsim/assets/configs/data4primary.yaml +47 -0
- h_adminsim/assets/configs/data4secondary.yaml +47 -0
- h_adminsim/assets/configs/data4tertiary.yaml +47 -0
- h_adminsim/assets/country/address.json +141859 -0
- h_adminsim/assets/country/country_code.json +244 -0
- h_adminsim/assets/departments/department.json +85 -0
- h_adminsim/assets/departments/symptom.json +4530 -0
- h_adminsim/assets/fhir.schema.json +75253 -0
- h_adminsim/assets/names/firstname.txt +1219 -0
- h_adminsim/assets/names/lastname.txt +88799 -0
- h_adminsim/assets/prompts/cancel_patient_system.txt +38 -0
- h_adminsim/assets/prompts/intake_staff_task_user.txt +16 -0
- h_adminsim/assets/prompts/intake_supervisor_system.txt +8 -0
- h_adminsim/assets/prompts/intake_supervisor_user.txt +31 -0
- h_adminsim/assets/prompts/reschedule_patient_system.txt +38 -0
- h_adminsim/assets/prompts/schedule_patient_rejected_system.txt +42 -0
- h_adminsim/assets/prompts/schedule_patient_system.txt +36 -0
- h_adminsim/assets/prompts/schedule_staff_reasoning.txt +57 -0
- h_adminsim/assets/prompts/schedule_staff_sc_tool_calling.txt +13 -0
- h_adminsim/assets/prompts/schedule_staff_system.txt +10 -0
- h_adminsim/assets/prompts/schedule_staff_tool_calling.txt +41 -0
- h_adminsim/client/__init__.py +3 -0
- h_adminsim/client/google_client.py +209 -0
- h_adminsim/client/openai_client.py +199 -0
- h_adminsim/client/vllm_client.py +160 -0
- h_adminsim/environment/__init__.py +1 -0
- h_adminsim/environment/hospital.py +462 -0
- h_adminsim/environment/op_scheduling_simulation.py +1126 -0
- h_adminsim/pipeline/__init__.py +3 -0
- h_adminsim/pipeline/data_generator.py +192 -0
- h_adminsim/pipeline/evaluator.py +33 -0
- h_adminsim/pipeline/simulation.py +231 -0
- h_adminsim/registry/__init__.py +5 -0
- h_adminsim/registry/errors.py +89 -0
- h_adminsim/registry/models.py +126 -0
- h_adminsim/registry/phrases.py +10 -0
- h_adminsim/registry/pydantic_models.py +21 -0
- h_adminsim/registry/variables.py +9 -0
- h_adminsim/supervisor.py +182 -0
- h_adminsim/task/agent_task.py +900 -0
- h_adminsim/task/fhir_manager.py +222 -0
- h_adminsim/task/schedule_assign.py +151 -0
- h_adminsim/tools/__init__.py +5 -0
- h_adminsim/tools/agent_data_builder.py +124 -0
- h_adminsim/tools/data_converter.py +536 -0
- h_adminsim/tools/data_synthesizer.py +365 -0
- h_adminsim/tools/evaluator.py +258 -0
- h_adminsim/tools/sanity_checker.py +216 -0
- h_adminsim/tools/scheduling_rule.py +420 -0
- h_adminsim/utils/__init__.py +136 -0
- h_adminsim/utils/common_utils.py +698 -0
- h_adminsim/utils/fhir_utils.py +190 -0
- h_adminsim/utils/filesys_utils.py +135 -0
- h_adminsim/utils/image_preprocess_utils.py +188 -0
- h_adminsim/utils/random_utils.py +358 -0
- h_adminsim/version.txt +1 -0
- h_adminsim-1.0.0.dist-info/LICENSE +30 -0
- h_adminsim-1.0.0.dist-info/METADATA +494 -0
- h_adminsim-1.0.0.dist-info/RECORD +62 -0
- h_adminsim-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from decimal import getcontext
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Union, Tuple, Optional
|
|
7
|
+
|
|
8
|
+
from h_adminsim.task.fhir_manager import FHIRManager
|
|
9
|
+
from h_adminsim.utils import log, colorstr
|
|
10
|
+
from h_adminsim.utils.fhir_utils import get_all_doctor_info
|
|
11
|
+
from h_adminsim.utils.common_utils import (
|
|
12
|
+
iso_to_date,
|
|
13
|
+
iso_to_hour,
|
|
14
|
+
get_iso_time,
|
|
15
|
+
sort_schedule,
|
|
16
|
+
get_utc_offset,
|
|
17
|
+
str_to_datetime,
|
|
18
|
+
datetime_to_str,
|
|
19
|
+
compare_iso_time,
|
|
20
|
+
exponential_backoff,
|
|
21
|
+
generate_random_iso_time_between,
|
|
22
|
+
convert_time_list_to_merged_time,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HospitalEnvironment:
|
|
28
|
+
def __init__(self,
|
|
29
|
+
agent_test_data: dict,
|
|
30
|
+
fhir_url: Optional[str] = None,
|
|
31
|
+
fhir_max_connection_retries: int = 5,
|
|
32
|
+
start_day_before: float = 3):
|
|
33
|
+
|
|
34
|
+
# FHIR manager
|
|
35
|
+
self.fhir_manager = FHIRManager(fhir_url) if fhir_url else None
|
|
36
|
+
|
|
37
|
+
# Basic
|
|
38
|
+
getcontext().prec = 10
|
|
39
|
+
self._epsilon = 1e-6
|
|
40
|
+
self.max_retries = fhir_max_connection_retries
|
|
41
|
+
self._days_before = start_day_before
|
|
42
|
+
self.HOSPITAL_NAME = agent_test_data.get('metadata').get('hospital_name')
|
|
43
|
+
self._START_DATE = agent_test_data.get('metadata').get('start_date')
|
|
44
|
+
self._END_DATE = agent_test_data.get('metadata').get('end_date')
|
|
45
|
+
self._START_HOUR = agent_test_data.get('metadata').get('time').get('start_hour')
|
|
46
|
+
self._END_HOUR = agent_test_data.get('metadata').get('time').get('end_hour')
|
|
47
|
+
self._TIME_UNIT = agent_test_data.get('metadata').get('time').get('interval_hour')
|
|
48
|
+
self._PATIENT_NUM = len(agent_test_data.get('agent_data'))
|
|
49
|
+
_country_code = agent_test_data.get('metadata').get('country_code', 'KR')
|
|
50
|
+
self.booking_num = {k: 0 for k in agent_test_data.get('doctor')}
|
|
51
|
+
|
|
52
|
+
# Time setting
|
|
53
|
+
self._utc_offset = get_utc_offset(_country_code)
|
|
54
|
+
self.current_time = get_iso_time(
|
|
55
|
+
time_hour=random.uniform(max(0, self._START_HOUR - 6), max(0, self._START_HOUR - self._epsilon)),
|
|
56
|
+
date=datetime_to_str(str_to_datetime(self._START_DATE) - timedelta(days=self._days_before), "%Y-%m-%d"),
|
|
57
|
+
utc_offset=self._utc_offset
|
|
58
|
+
)
|
|
59
|
+
self.avg_gap = self.__calculate_max_time_increment()
|
|
60
|
+
|
|
61
|
+
# Misc.
|
|
62
|
+
self.patient_schedules = list()
|
|
63
|
+
self.waiting_list = list()
|
|
64
|
+
self.first_verbose_flag = True
|
|
65
|
+
|
|
66
|
+
# Cache variables
|
|
67
|
+
self._fhir_practitioner_cache = None
|
|
68
|
+
self._fhir_practitionerrole_cache = None
|
|
69
|
+
self._fhir_schedule_cache = None
|
|
70
|
+
self._fhir_slot_cache = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __calculate_max_time_increment(self) -> float:
|
|
74
|
+
"""
|
|
75
|
+
Calculate the maximum average time increment (gap) between patient booking within the defined scheduling period.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
float: The average time gap (in hours) between patients.
|
|
79
|
+
"""
|
|
80
|
+
st = str_to_datetime(get_iso_time(self._START_HOUR, self._START_DATE, self._utc_offset))
|
|
81
|
+
tr = str_to_datetime(get_iso_time(self._END_HOUR, self._END_DATE, self._utc_offset))
|
|
82
|
+
total_hours = (tr - st).total_seconds() / 3600
|
|
83
|
+
avg_gap = total_hours / self._PATIENT_NUM
|
|
84
|
+
return avg_gap
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_general_doctor_info_from_fhir(self, use_cache: bool = True) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
Build a doctor information dictionary from FHIR resources for simulation.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
use_cache (bool): If True, reuse cached FHIR resources if available. Defaults to True.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
dict: doctor_information (dict): Dictionary of doctor data including their existing schedules.
|
|
96
|
+
Each key is a doctor's name, and each value includes a 'schedule' field.
|
|
97
|
+
"""
|
|
98
|
+
if self.first_verbose_flag:
|
|
99
|
+
log('Build doctor information from the FHIR resources..')
|
|
100
|
+
self.first_verbose_flag = False
|
|
101
|
+
|
|
102
|
+
hospital_id = self.HOSPITAL_NAME.replace('_', '')
|
|
103
|
+
cache_ready = all([
|
|
104
|
+
self._fhir_practitioner_cache,
|
|
105
|
+
self._fhir_practitionerrole_cache,
|
|
106
|
+
self._fhir_schedule_cache,
|
|
107
|
+
self._fhir_slot_cache,
|
|
108
|
+
])
|
|
109
|
+
|
|
110
|
+
if not use_cache or not cache_ready:
|
|
111
|
+
self._fhir_practitioner_cache = [
|
|
112
|
+
x for x in self.fhir_manager.read_all('Practitioner', verbose=False)
|
|
113
|
+
if hospital_id in x['resource']['id']
|
|
114
|
+
]
|
|
115
|
+
self._fhir_practitionerrole_cache = [
|
|
116
|
+
x for x in self.fhir_manager.read_all('PractitionerRole', verbose=False)
|
|
117
|
+
if hospital_id in x['resource']['id']
|
|
118
|
+
]
|
|
119
|
+
self._fhir_schedule_cache = [
|
|
120
|
+
x for x in self.fhir_manager.read_all('Schedule', verbose=False)
|
|
121
|
+
if hospital_id in x['resource']['id']
|
|
122
|
+
]
|
|
123
|
+
self._fhir_slot_cache = [
|
|
124
|
+
x for x in self.fhir_manager.read_all('Slot', verbose=False)
|
|
125
|
+
if hospital_id in x['resource']['id']
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# Get Appointment resources from the FHIR server
|
|
129
|
+
# NOTE: Sometimes, a FHIR resource is accessed before it gets updated, so the operation is performed with a retry flag
|
|
130
|
+
retry_count = 0
|
|
131
|
+
while 1:
|
|
132
|
+
try:
|
|
133
|
+
self.fhir_appointment = [
|
|
134
|
+
x for x in self.fhir_manager.read_all('Appointment', verbose=False)
|
|
135
|
+
if hospital_id in x['resource']['id']
|
|
136
|
+
]
|
|
137
|
+
valid_len = len(list(filter(lambda x: x['status'] != 'cancelled', self.patient_schedules)))
|
|
138
|
+
assert len(self.fhir_appointment) == valid_len, f"Mismatch in appointment count: expected {valid_len}, got {len(self.fhir_appointment)}"
|
|
139
|
+
break
|
|
140
|
+
except AssertionError as e:
|
|
141
|
+
if retry_count >= self.max_retries:
|
|
142
|
+
log(f"\nMax retries reached. Last error: {e}", level='error')
|
|
143
|
+
raise e
|
|
144
|
+
wait_time = exponential_backoff(retry_count)
|
|
145
|
+
log(f"[{retry_count + 1}/{self.max_retries}] {type(e).__name__}: {e}. Retrying in {wait_time:.1f} seconds...", level='warning')
|
|
146
|
+
time.sleep(wait_time)
|
|
147
|
+
retry_count += 1
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
# Convert resources regardless of whether they came from cache or fresh read
|
|
151
|
+
doctor_information = get_all_doctor_info(
|
|
152
|
+
self._fhir_practitioner_cache,
|
|
153
|
+
self._fhir_practitionerrole_cache,
|
|
154
|
+
self._fhir_schedule_cache,
|
|
155
|
+
self._fhir_slot_cache,
|
|
156
|
+
self.fhir_appointment,
|
|
157
|
+
**{'start': self._START_HOUR, 'end': self._END_HOUR, 'interval': self._TIME_UNIT}
|
|
158
|
+
)
|
|
159
|
+
return doctor_information
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_doctor_schedule(self,
|
|
163
|
+
doctor_information: Optional[dict] = None,
|
|
164
|
+
*,
|
|
165
|
+
department: Optional[str] = None,
|
|
166
|
+
fhir_integration: bool = False,
|
|
167
|
+
express_detail: bool = False) -> dict:
|
|
168
|
+
"""
|
|
169
|
+
Build doctor schedules for a given department.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
doctor_information (Optional[dict], optional): Simulation doctor data (used when fhir_integration is False). Defaults to None.
|
|
173
|
+
department (Optional[str], optional): Target department name. Defaults to None.
|
|
174
|
+
fhir_integration (bool, optional): If True, build schedules from FHIR resources. Defaults to False.
|
|
175
|
+
express_detail (bool, optional): If True, express schedules with explicit start/end fields. Defualtsto False.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
dict: Filtered doctor scheduling information.
|
|
179
|
+
"""
|
|
180
|
+
def __build_single_doctor_schedule(practitioner_role: dict) -> Tuple[str, dict]:
|
|
181
|
+
"""
|
|
182
|
+
Build scheduling information for a single doctor.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
practitioner_role (dict): A FHIR PractitionerRole resource for a doctor, already filtered by department.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Tuple[str, dict]: Doctor name and his (or her) information dictionary containing the constructed scheduling information.
|
|
189
|
+
"""
|
|
190
|
+
schedule, appointments = dict(), list()
|
|
191
|
+
practitioner_id = practitioner_role['practitioner']['reference']
|
|
192
|
+
practitioner = self.fhir_manager.read('Practitioner', practitioner_id.split('/')[-1], verbose=False).json()
|
|
193
|
+
practitioner_schedule_id = self.fhir_manager.read_all('Schedule', params={'actor': practitioner_id}, verbose=False)[0]['resource']['id']
|
|
194
|
+
fixed_slots = [slot['resource'] for slot in self.fhir_manager.read_all('Slot', params={'schedule': f'Schedule/{practitioner_schedule_id}'}, verbose=False)]
|
|
195
|
+
|
|
196
|
+
# Append fixed schedules of a doctor
|
|
197
|
+
for slot in fixed_slots:
|
|
198
|
+
date = iso_to_date(slot['start'])
|
|
199
|
+
schedule.setdefault(date, [])
|
|
200
|
+
if slot['status'] != 'free':
|
|
201
|
+
schedule[date].append([iso_to_hour(slot['start']), iso_to_hour(slot['end'])])
|
|
202
|
+
|
|
203
|
+
# Get all appointments related to this slot
|
|
204
|
+
appointment_resources = self.fhir_manager.read_all('Appointment', params={'slot': f'Slot/{slot["id"]}'}, verbose=False)
|
|
205
|
+
if len(appointment_resources) > 0:
|
|
206
|
+
appointments.append(appointment_resources[0]['resource'])
|
|
207
|
+
|
|
208
|
+
# Merge fixed schedule times
|
|
209
|
+
for date, time_list in schedule.items():
|
|
210
|
+
schedule[date] = convert_time_list_to_merged_time(
|
|
211
|
+
time_list=sort_schedule(time_list),
|
|
212
|
+
start=self._START_HOUR,
|
|
213
|
+
end=self._END_HOUR,
|
|
214
|
+
interval=self._TIME_UNIT
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Append patient appointments of a doctor
|
|
218
|
+
for appointment in appointments:
|
|
219
|
+
date = iso_to_date(appointment['start'])
|
|
220
|
+
schedule.setdefault(date, [])
|
|
221
|
+
schedule[date].append([iso_to_hour(appointment['start']), iso_to_hour(appointment['end'])])
|
|
222
|
+
|
|
223
|
+
# Collect doctor's information
|
|
224
|
+
name = f"{practitioner['name'][0]['prefix'][0]} {practitioner['name'][0]['given'][0]} {practitioner['name'][0]['family']}"
|
|
225
|
+
department = practitioner_role['specialty'][0]['text']
|
|
226
|
+
specialty = {
|
|
227
|
+
'name': practitioner_role['specialty'][0]['coding'][0]['display'],
|
|
228
|
+
'code': practitioner_role['specialty'][0]['coding'][0]['code']
|
|
229
|
+
}
|
|
230
|
+
capacity_attributes = {attr['text']: attr['coding'][0]['display'] for attr in practitioner_role['characteristic']}
|
|
231
|
+
workload = f"{round(self.booking_num[name] / int(capacity_attributes['capacity']) * 100, 2)}%"
|
|
232
|
+
outpatient_duration = 1 / int(capacity_attributes['capacity_per_hour'])
|
|
233
|
+
information = {
|
|
234
|
+
'department': department,
|
|
235
|
+
'specialty': specialty,
|
|
236
|
+
'schedule': sort_schedule(schedule),
|
|
237
|
+
'workload': workload,
|
|
238
|
+
'outpatient_duration': outpatient_duration
|
|
239
|
+
}
|
|
240
|
+
return name, information
|
|
241
|
+
|
|
242
|
+
filtered_doctor_information = {'doctor': {}}
|
|
243
|
+
|
|
244
|
+
# Get filtered doctor information directly from FHIR
|
|
245
|
+
if fhir_integration:
|
|
246
|
+
if self.first_verbose_flag:
|
|
247
|
+
log('Build doctor information from the FHIR resources..')
|
|
248
|
+
self.first_verbose_flag = False
|
|
249
|
+
|
|
250
|
+
# Get doctors belonging to the department
|
|
251
|
+
params={"specialty:text": department}
|
|
252
|
+
practitioner_roles = [resource['resource'] for resource in self.fhir_manager.read_all('PractitionerRole', params=params, verbose=False)]
|
|
253
|
+
|
|
254
|
+
for practitioner_role in practitioner_roles:
|
|
255
|
+
doctor_name, doctor_schedule = __build_single_doctor_schedule(practitioner_role)
|
|
256
|
+
filtered_doctor_information['doctor'][doctor_name] = doctor_schedule
|
|
257
|
+
|
|
258
|
+
# Get filtered doctor information from the simulation data
|
|
259
|
+
else:
|
|
260
|
+
for k, v in doctor_information.items():
|
|
261
|
+
if v['department'] == department:
|
|
262
|
+
tmp_schedule = deepcopy(v)
|
|
263
|
+
del tmp_schedule['capacity_per_hour'], tmp_schedule['capacity'], tmp_schedule['gender'], tmp_schedule['telecom'], tmp_schedule['birthDate']
|
|
264
|
+
tmp_schedule['workload'] = f"{round(self.booking_num[k] / v['capacity'] * 100, 2)}%"
|
|
265
|
+
tmp_schedule['outpatient_duration'] = 1 / v['capacity_per_hour']
|
|
266
|
+
filtered_doctor_information['doctor'][k] = tmp_schedule
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# Whether express more details in the built schedules
|
|
270
|
+
if express_detail:
|
|
271
|
+
for _, info in filtered_doctor_information['doctor'].items():
|
|
272
|
+
info['schedule'] = {
|
|
273
|
+
date: [{'start': s[0], 'end': s[1]} for s in schedule]
|
|
274
|
+
for date, schedule in info['schedule'].items()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return filtered_doctor_information
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def resume(self, agent_results: dict):
|
|
281
|
+
"""
|
|
282
|
+
Resume the hospital environment from previously saved agent results.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
agent_test_data (dict): Input data containing static information
|
|
286
|
+
about doctors, patients, and other hospital resources.
|
|
287
|
+
agent_results (dict): Previously saved results from the agent's simulation.
|
|
288
|
+
"""
|
|
289
|
+
if 'schedule' in agent_results:
|
|
290
|
+
statuses = [x for y in agent_results['schedule']['status'] for x in (y if isinstance(y, list) or isinstance(y, tuple) else [y])]
|
|
291
|
+
preds = [x for y in agent_results['schedule']['pred'] for x in (y if isinstance(y, list) or isinstance(y, tuple) else [y])]
|
|
292
|
+
for status, pred in zip(statuses, preds):
|
|
293
|
+
if isinstance(status, bool) and status:
|
|
294
|
+
if 'patient' in pred:
|
|
295
|
+
self.patient_schedules.append(pred)
|
|
296
|
+
self.current_time = pred['last_updated_time']
|
|
297
|
+
|
|
298
|
+
if 'status' in pred and not pred['status'] == 'cancelled':
|
|
299
|
+
self.booking_num[pred['attending_physician']] += 1
|
|
300
|
+
|
|
301
|
+
self.waiting_list = sorted([(i, s) for i, s in enumerate(self.patient_schedules) if s['waiting_order'] >= 0], key=lambda x: x[1]['waiting_order'])
|
|
302
|
+
|
|
303
|
+
log(f"Resumed hospital time set to {self.current_time}.")
|
|
304
|
+
log(f"Resumed hospital environment with {len(self.patient_schedules)} patient schedules.")
|
|
305
|
+
log(f"Resumed waiting list with {len(self.waiting_list)} patient schedules.")
|
|
306
|
+
log(f"Current booking numbers per doctor: {self.booking_num}")
|
|
307
|
+
|
|
308
|
+
self.update_current_time()
|
|
309
|
+
self.update_patient_status()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def schedule_cancel_event(self, idx: int, verbose: bool = False):
|
|
313
|
+
"""
|
|
314
|
+
Cancel a scheduled event for the patient.
|
|
315
|
+
This method updates the status of a scheduled event at the given index
|
|
316
|
+
to 'cancelled'. Index values greater than or equal to 0 are allowed.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
idx (int): The index of the schedule to cancel. Must be 0 or a positive integer.
|
|
320
|
+
verbose (bool, optional): Whether logging the each result or not. Defaults to False.
|
|
321
|
+
"""
|
|
322
|
+
if idx >= 0:
|
|
323
|
+
for turn, (i, _) in enumerate(self.waiting_list):
|
|
324
|
+
if i == idx:
|
|
325
|
+
self.pop_waiting_list(turn, verbose)
|
|
326
|
+
break
|
|
327
|
+
self.patient_schedules[idx]['status'] = 'cancelled'
|
|
328
|
+
self.patient_schedules[idx]['last_updated_time'] = self.current_time
|
|
329
|
+
self.booking_num[self.patient_schedules[idx]['attending_physician']] -= 1
|
|
330
|
+
if verbose:
|
|
331
|
+
log(f'{colorstr("[CANCELLED]")}: {self.patient_schedules[idx]} schedule is cancelled.')
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def add_waiting_list(self, idx: int, verbose: bool = False):
|
|
335
|
+
"""
|
|
336
|
+
Add a schedule to the waiting list for an earlier appointment if needed.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
idx (int): The index of the schedule to add to the waiting list. Must be 0 or a positive integer.
|
|
340
|
+
verbose (bool, optional): Whether logging the each result or not. Defaults to False.
|
|
341
|
+
"""
|
|
342
|
+
if idx >= 0:
|
|
343
|
+
requested_schedule = self.patient_schedules[idx]
|
|
344
|
+
if all(requested_schedule != s[1] for s in self.waiting_list):
|
|
345
|
+
requested_schedule['waiting_order'] = len(self.waiting_list)
|
|
346
|
+
self.waiting_list.append((idx, requested_schedule))
|
|
347
|
+
if verbose:
|
|
348
|
+
log(f'{colorstr("[WAITING LIST ADDED]")}: {requested_schedule} schedule is appended to the waiting list.')
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def pop_waiting_list(self, idx: Union[list[int], int], verbose: bool = False):
|
|
352
|
+
"""
|
|
353
|
+
Pop a schedule to the waiting list.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
idx (Union[list[int], int]): The index list (or index) of the schedule to pop from the waiting list.
|
|
357
|
+
verbose (bool, optional): Whether logging the each result or not. Defaults to False.
|
|
358
|
+
"""
|
|
359
|
+
if isinstance(idx, int) and idx >= 0:
|
|
360
|
+
idx = [idx]
|
|
361
|
+
|
|
362
|
+
if len(idx):
|
|
363
|
+
idx = sorted(idx, reverse=True)
|
|
364
|
+
for _id in idx:
|
|
365
|
+
schedule = self.waiting_list.pop(_id)
|
|
366
|
+
schedule[1]['waiting_order'] = -1
|
|
367
|
+
if verbose:
|
|
368
|
+
log(f'{colorstr("[WAITING LIST POPPED]")}: {schedule[1]} schedule is popped from the waiting list.')
|
|
369
|
+
|
|
370
|
+
for i, (_, schedule) in enumerate(self.waiting_list):
|
|
371
|
+
schedule['waiting_order'] = i
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def update_fhir(self, fhir_resources: dict):
|
|
375
|
+
"""
|
|
376
|
+
Update resources on the FHIR server.
|
|
377
|
+
|
|
378
|
+
fhir_resources (dict): Dictionary where each key is a FHIR resource type (e.g., 'Appointment', 'Slot'),
|
|
379
|
+
and each value is the corresponding FHIR resource data to be updated.
|
|
380
|
+
"""
|
|
381
|
+
# Update new FHIR resources
|
|
382
|
+
for resource_type, resource in fhir_resources.items():
|
|
383
|
+
if resource and resource_type.lower() in ['patient', 'appointment']:
|
|
384
|
+
self.fhir_manager.create(resource_type, resource, verbose=False)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def delete_fhir(self, fhir_resources: dict):
|
|
388
|
+
"""
|
|
389
|
+
Delete resources on the FHIR server.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
fhir_resources (dict): Dictionary where each key is a FHIR resource type (e.g., 'Appointment', 'Slot'),
|
|
393
|
+
and each value is the corresponding FHIR resource data to be updated.
|
|
394
|
+
"""
|
|
395
|
+
# Delete the existing FHIR resources
|
|
396
|
+
for resource_type, resource in fhir_resources.items():
|
|
397
|
+
if resource and resource_type.lower() in ['patient', 'appointment']:
|
|
398
|
+
self.fhir_manager.delete(resource_type, resource['id'], verbose=False)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def update_current_time(self):
|
|
402
|
+
"""
|
|
403
|
+
Update the current hospital time.
|
|
404
|
+
"""
|
|
405
|
+
min_iso_time = self.current_time
|
|
406
|
+
max_iso_time = (str_to_datetime(self.current_time) + timedelta(hours=self.avg_gap)).isoformat(timespec='seconds')
|
|
407
|
+
self.current_time = generate_random_iso_time_between(min_iso_time, max_iso_time)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def update_patient_status(self):
|
|
411
|
+
"""
|
|
412
|
+
Update the status of each patient based on the current hospital time.
|
|
413
|
+
"""
|
|
414
|
+
for schedule in self.patient_schedules:
|
|
415
|
+
if schedule.get('waiting_order', -1) < 0:
|
|
416
|
+
schedule['waiting_order'] = -1
|
|
417
|
+
|
|
418
|
+
if schedule.get('status') == 'cancelled':
|
|
419
|
+
continue
|
|
420
|
+
|
|
421
|
+
tmp_st_iso_time = get_iso_time(schedule['schedule'][0], date=schedule['date'], utc_offset=self._utc_offset)
|
|
422
|
+
tmp_tr_iso_time = get_iso_time(schedule['schedule'][-1], date=schedule['date'], utc_offset=self._utc_offset)
|
|
423
|
+
|
|
424
|
+
if compare_iso_time(self.current_time, tmp_tr_iso_time):
|
|
425
|
+
status = 'completed'
|
|
426
|
+
elif compare_iso_time(tmp_st_iso_time, self.current_time):
|
|
427
|
+
status = 'scheduled'
|
|
428
|
+
else:
|
|
429
|
+
status = 'in_progress'
|
|
430
|
+
|
|
431
|
+
schedule['status'] = status
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def reset_variable(self):
|
|
435
|
+
"""
|
|
436
|
+
Reset variables.
|
|
437
|
+
"""
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def update_env(self,
|
|
442
|
+
status: bool,
|
|
443
|
+
patient_schedule: Union[dict, str],
|
|
444
|
+
fhir_resources: dict):
|
|
445
|
+
"""
|
|
446
|
+
Update the hospital environment after successfully assigning an appointment.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
status (bool): Whether the appointment was successfully assigned.
|
|
450
|
+
patient_schedule (Union[dict, str]): The patient's new schedule to add. Should contain a 'schedule' key with start and end time.
|
|
451
|
+
fhir_resources (dict): Dictionary where each key is a FHIR resource type (e.g., 'Appointment', 'Slot'),
|
|
452
|
+
and each value is the corresponding FHIR resource data to be updated.
|
|
453
|
+
"""
|
|
454
|
+
if status:
|
|
455
|
+
self.update_fhir(fhir_resources)
|
|
456
|
+
self.update_current_time()
|
|
457
|
+
self.patient_schedules.append(patient_schedule)
|
|
458
|
+
self.update_patient_status()
|
|
459
|
+
self.booking_num[patient_schedule['attending_physician']] += 1
|
|
460
|
+
|
|
461
|
+
self.reset_variable()
|
|
462
|
+
|