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.
Files changed (62) hide show
  1. h_adminsim/__init__.py +5 -0
  2. h_adminsim/admin_staff.py +280 -0
  3. h_adminsim/assets/configs/data4primary.yaml +47 -0
  4. h_adminsim/assets/configs/data4secondary.yaml +47 -0
  5. h_adminsim/assets/configs/data4tertiary.yaml +47 -0
  6. h_adminsim/assets/country/address.json +141859 -0
  7. h_adminsim/assets/country/country_code.json +244 -0
  8. h_adminsim/assets/departments/department.json +85 -0
  9. h_adminsim/assets/departments/symptom.json +4530 -0
  10. h_adminsim/assets/fhir.schema.json +75253 -0
  11. h_adminsim/assets/names/firstname.txt +1219 -0
  12. h_adminsim/assets/names/lastname.txt +88799 -0
  13. h_adminsim/assets/prompts/cancel_patient_system.txt +38 -0
  14. h_adminsim/assets/prompts/intake_staff_task_user.txt +16 -0
  15. h_adminsim/assets/prompts/intake_supervisor_system.txt +8 -0
  16. h_adminsim/assets/prompts/intake_supervisor_user.txt +31 -0
  17. h_adminsim/assets/prompts/reschedule_patient_system.txt +38 -0
  18. h_adminsim/assets/prompts/schedule_patient_rejected_system.txt +42 -0
  19. h_adminsim/assets/prompts/schedule_patient_system.txt +36 -0
  20. h_adminsim/assets/prompts/schedule_staff_reasoning.txt +57 -0
  21. h_adminsim/assets/prompts/schedule_staff_sc_tool_calling.txt +13 -0
  22. h_adminsim/assets/prompts/schedule_staff_system.txt +10 -0
  23. h_adminsim/assets/prompts/schedule_staff_tool_calling.txt +41 -0
  24. h_adminsim/client/__init__.py +3 -0
  25. h_adminsim/client/google_client.py +209 -0
  26. h_adminsim/client/openai_client.py +199 -0
  27. h_adminsim/client/vllm_client.py +160 -0
  28. h_adminsim/environment/__init__.py +1 -0
  29. h_adminsim/environment/hospital.py +462 -0
  30. h_adminsim/environment/op_scheduling_simulation.py +1126 -0
  31. h_adminsim/pipeline/__init__.py +3 -0
  32. h_adminsim/pipeline/data_generator.py +192 -0
  33. h_adminsim/pipeline/evaluator.py +33 -0
  34. h_adminsim/pipeline/simulation.py +231 -0
  35. h_adminsim/registry/__init__.py +5 -0
  36. h_adminsim/registry/errors.py +89 -0
  37. h_adminsim/registry/models.py +126 -0
  38. h_adminsim/registry/phrases.py +10 -0
  39. h_adminsim/registry/pydantic_models.py +21 -0
  40. h_adminsim/registry/variables.py +9 -0
  41. h_adminsim/supervisor.py +182 -0
  42. h_adminsim/task/agent_task.py +900 -0
  43. h_adminsim/task/fhir_manager.py +222 -0
  44. h_adminsim/task/schedule_assign.py +151 -0
  45. h_adminsim/tools/__init__.py +5 -0
  46. h_adminsim/tools/agent_data_builder.py +124 -0
  47. h_adminsim/tools/data_converter.py +536 -0
  48. h_adminsim/tools/data_synthesizer.py +365 -0
  49. h_adminsim/tools/evaluator.py +258 -0
  50. h_adminsim/tools/sanity_checker.py +216 -0
  51. h_adminsim/tools/scheduling_rule.py +420 -0
  52. h_adminsim/utils/__init__.py +136 -0
  53. h_adminsim/utils/common_utils.py +698 -0
  54. h_adminsim/utils/fhir_utils.py +190 -0
  55. h_adminsim/utils/filesys_utils.py +135 -0
  56. h_adminsim/utils/image_preprocess_utils.py +188 -0
  57. h_adminsim/utils/random_utils.py +358 -0
  58. h_adminsim/version.txt +1 -0
  59. h_adminsim-1.0.0.dist-info/LICENSE +30 -0
  60. h_adminsim-1.0.0.dist-info/METADATA +494 -0
  61. h_adminsim-1.0.0.dist-info/RECORD +62 -0
  62. h_adminsim-1.0.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,358 @@
1
+ import uuid
2
+ import random
3
+ from importlib import resources
4
+ from datetime import datetime, timedelta
5
+ from typing import Tuple, Any, Union, Optional
6
+
7
+ from h_adminsim import registry
8
+ from h_adminsim.utils import log
9
+ from h_adminsim.utils.filesys_utils import txt_load, json_load
10
+ from h_adminsim.utils.common_utils import str_to_datetime, datetime_to_str
11
+
12
+
13
+
14
+ def random_uuid(is_develop: bool = False) -> str:
15
+ """
16
+ Generate ranodm UUID
17
+
18
+ Args:
19
+ is_develop (bool, optional): If True, generates a UUID using fully random bytes
20
+ to support reproducibility during development or debugging.
21
+ Defaults to False.
22
+
23
+ Returns:
24
+ str: The generated UUID
25
+ """
26
+ if is_develop:
27
+ # For development purposes, generate controlled random UUID
28
+ rand_bytes = random.getrandbits(128).to_bytes(16, 'big')
29
+ return str(uuid.UUID(bytes=rand_bytes))
30
+ return str(uuid.uuid1())
31
+
32
+
33
+
34
+ def generate_random_number_string(length: int) -> str:
35
+ """
36
+ Generate a random numeric string of the given length.
37
+
38
+ Args:
39
+ length (int): The desired length of the random numeric string.
40
+
41
+ Returns:
42
+ str: A string consisting of randomly generated digits (0-9)
43
+ with the specified length.
44
+ """
45
+ return ''.join(str(random.randint(0, 9)) for _ in range(length))
46
+
47
+
48
+
49
+ def generate_random_names(n: int,
50
+ first_name_file: Optional[str] = None,
51
+ last_name_file: Optional[str] = None) -> list[str]:
52
+ """
53
+ Generate a list of random names by combining first and last names from specified files.
54
+
55
+ Args:
56
+ n (int): Number of random names to generate.
57
+ first_name_file (Optional[str], optional): Path to the file containing first names. Defaults to None.
58
+ last_name_file (Optional[str], optional): Path to the file containing last names. Defaults to None.
59
+
60
+ Returns:
61
+ list[str]: List of randomly generated names in the format "First Last".
62
+ """
63
+ if first_name_file == None:
64
+ first_name_file = str(resources.files("h_adminsim.assets.names").joinpath("firstname.txt"))
65
+ if last_name_file == None:
66
+ last_name_file = str(resources.files("h_adminsim.assets.names").joinpath("lastname.txt"))
67
+
68
+ if registry.FIRST_NAMES is None:
69
+ registry.FIRST_NAMES = [word.capitalize() for word in txt_load(first_name_file).split('\n') if word.strip()]
70
+ if registry.LAST_NAMES is None:
71
+ registry.LAST_NAMES = [word.capitalize() for word in txt_load(last_name_file).split('\n') if word.strip()]
72
+
73
+ # Ensure unique names
74
+ duplicate_name_num, names = dict(), set()
75
+ while len(names) < n:
76
+ first_name = random.choice(registry.FIRST_NAMES)
77
+ last_name = random.choice(registry.LAST_NAMES)
78
+ full_name = f'{first_name} {last_name}'
79
+
80
+ if full_name in names:
81
+ duplicate_name_num[full_name] = duplicate_name_num.setdefault(full_name, 1) + 1
82
+ full_name = f'{full_name}{duplicate_name_num[full_name]}'
83
+
84
+ names.add(f"{first_name} {last_name}")
85
+ return sorted(list(names))
86
+
87
+
88
+
89
+ def generate_random_address(address_file_path: Optional[str] = None,
90
+ format: str = '{street}, {district}, {city}') -> str:
91
+ """
92
+ Generate random address.
93
+
94
+ Args:
95
+ address_file (Optional[str], optional): Path to the file containing addresses. Defaults to None.
96
+ format (str, optional): Format of the address.
97
+
98
+ Returns:
99
+ str: Random generated address based on the asset.
100
+ """
101
+ if address_file_path == None:
102
+ address_file_path = str(resources.files("h_adminsim.assets.country").joinpath("address.json"))
103
+
104
+ if registry.ADDRESSES is None:
105
+ registry.ADDRESSES = json_load(address_file_path)
106
+
107
+ kwargs = dict()
108
+ if 'street' in format:
109
+ kwargs['street'] = f'{generate_random_number_string(random.randint(1, 2))}, {random.choice(registry.ADDRESSES["street"])}'
110
+ if 'neighborhood' in format:
111
+ kwargs['neighborhood'] = random.choice(registry.ADDRESSES['neighborhood'])
112
+ if 'district' in format:
113
+ kwargs['district'] = random.choice(registry.ADDRESSES['district'])
114
+ if 'county' in format:
115
+ kwargs['county'] = random.choice(registry.ADDRESSES['county'])
116
+ if 'city' in format:
117
+ kwargs['city'] = random.choice(registry.ADDRESSES['city'])
118
+ if 'state' in format:
119
+ kwargs['state'] = random.choice(registry.ADDRESSES['state'])
120
+
121
+ return format.format(**kwargs)
122
+
123
+
124
+
125
+ def generate_random_prob(has_schedule_prob: float, coverage_min: float, coverage_max: float) -> float:
126
+ """
127
+ Determine the final schedule ratio for a doctor.
128
+
129
+ Args:
130
+ has_schedule_prob (float): Probability that a doctor has any schedule.
131
+ coverage_min (float): Minimum proportion of total available hours the schedule can occupy.
132
+ coverage_max (float): Maximum proportion of total available hours the schedule can occupy.
133
+
134
+ Returns:
135
+ float: The final schedule ratio. 0.0 if the doctor has no schedule. A float in [coverage_min, coverage_max] if the doctor has a schedule.
136
+ """
137
+ has_schedule = random.random() < has_schedule_prob
138
+ if not has_schedule:
139
+ return 0.0
140
+
141
+ return random.uniform(coverage_min, coverage_max)
142
+
143
+
144
+
145
+ def generate_random_symptom(department: str,
146
+ symptom_file_path: Optional[str] = None,
147
+ ensure_unique_department: bool = True,
148
+ min_n: int = 2,
149
+ max_n: int = 4,
150
+ verbose: bool = True) -> Union[dict, str]:
151
+ """
152
+ Generate a string of random symptom from pre-defined data file.
153
+
154
+ Args:
155
+ department (str, optional): A name of hospital department.
156
+ symptom_file_path (Optional[str], optional): A path of pre-defined symptom data. Defaults to None.
157
+ ensure_unique_department (bool): Ensure that the disease can only be treated in a single medical specialty.
158
+ min_n (int, optional): Minimum number of symptoms to select. Defaults to 2.
159
+ max_n (int, optional): Maximum number of symptoms to select. Defaults to 4.
160
+ verbose (bool, optional): If True, print a warning message when no matching department is found. Defaults to True.
161
+
162
+ Returns:
163
+ Union[dict, str]:
164
+ - dict: A randomly selected disease and its associated symptoms for the given department.
165
+ - str: The string `"{PLACEHOLDER}"` if the department is not found in the data.
166
+ """
167
+ if symptom_file_path == None:
168
+ symptom_file_path = str(resources.files("h_adminsim.assets.departments").joinpath("symptom.json"))
169
+
170
+ if registry.SYMPTOM_MAP is None:
171
+ registry.SYMPTOM_MAP = json_load(symptom_file_path)
172
+
173
+ if department in registry.SYMPTOM_MAP:
174
+ # Ensure that the disease can only be treated in a single medical specialty
175
+ if ensure_unique_department:
176
+ unique_diseases = [dis for dis in registry.SYMPTOM_MAP[department] if len(dis[list(dis.keys())[0]]['department']) == 1]
177
+ if not len(unique_diseases):
178
+ log(f"In the specified {department}, there is no disease that can be treated within that specialty.\
179
+ As a result, if the department prediction later turns out to be a different specialty,\
180
+ it may not align with the patient’s preferred primary physician’s department, which can cause errors in the scheduling simulation.", 'warning')
181
+ disease_list = unique_diseases if len(unique_diseases) else registry.SYMPTOM_MAP[department]
182
+ disease_info = random.choice(disease_list)
183
+ else:
184
+ disease_info = random.choice(registry.SYMPTOM_MAP[department])
185
+ disease = list(disease_info.keys())[0]
186
+ disease_info = {'disease': disease, **disease_info[disease]}
187
+ symptom_n = min(random.randint(min_n, max_n), len(disease_info['symptom']))
188
+ disease_info['symptom'] = random.sample(disease_info['symptom'], symptom_n)
189
+ return disease_info
190
+
191
+ if verbose:
192
+ log(f'No matched department {department}. `{{PLACEHOLDER}}` string will return.', 'warning')
193
+ return '{PLACEHOLDER}'
194
+
195
+
196
+
197
+ def generate_random_telecom(min_length: int = 8,
198
+ max_length: int = 13,
199
+ country_code: str = 'KR',
200
+ country_to_dial_map_file: Optional[str] = None) -> str:
201
+ """
202
+ Generate a random telecom number including the country dialing code.
203
+
204
+ Args:
205
+ min_length (int, optional): The minimum length of the subscriber number (excluding country code). Defaults to 8.
206
+ max_length (int, optional): The maximum length of the subscriber number (excluding country code). Defaults to 13.
207
+ country_code (str, optional): The ISO country code to determine the dialing prefix. Default is 'KR' (South Korea).
208
+ country_to_dial_map_file (Optional[str], optional): Path to the JSON file mapping country codes to their dialing prefixes. Defaults to None.
209
+
210
+ Raises:
211
+ KeyError: If the provided country_code is not found in the dialing code registry.
212
+
213
+ Returns:
214
+ str: A random telecom number string starting with the country dialing code, followed by a random sequence of digits.
215
+ """
216
+ if country_to_dial_map_file == None:
217
+ country_to_dial_map_file = str(resources.files("h_adminsim.assets.country").joinpath("country_code.json"))
218
+
219
+ if registry.TELECOM_COUNTRY_CODE is None:
220
+ registry.TELECOM_COUNTRY_CODE = json_load(country_to_dial_map_file)
221
+
222
+ try:
223
+ dial_code = registry.TELECOM_COUNTRY_CODE[country_code.upper()]
224
+ except KeyError:
225
+ dial_code = ''
226
+
227
+ length = random.randint(min_length, max_length)
228
+
229
+ return dial_code + ''.join(random.choices('0123456789', k=length))
230
+
231
+
232
+
233
+ def generate_random_date(start_date: Union[str, datetime] = '1960-01-01',
234
+ end_date: Union[str, datetime] = '2000-12-31') -> str:
235
+ """
236
+ Generate a random date string in 'YYYY-MM-DD' format between the given start and end dates.
237
+
238
+ Args:
239
+ start_date (Union[str, datetime], optional): The start date in 'YYYY-MM-DD' format. Default is '2000-01-01'.
240
+ end_date (Union[str, datetime], optional): The end date in 'YYYY-MM-DD' format. Default is '2025-12-31'.
241
+
242
+ Returns:
243
+ str: A randomly generated date string in 'YYYY-MM-DD' format.
244
+ """
245
+ start = str_to_datetime(start_date)
246
+ end = str_to_datetime(end_date)
247
+ delta = (end - start).days
248
+ random_days = random.randint(0, delta)
249
+ random_date = start + timedelta(days=random_days)
250
+ return datetime_to_str(random_date, '%Y-%m-%d')
251
+
252
+
253
+
254
+ def generate_random_id_number(start_date: Union[str, datetime] = '1960-01-01',
255
+ end_date: Union[str, datetime] = '2000-12-31',
256
+ birth_date: Optional[str] = None) -> str:
257
+ """
258
+ Generate a random ID number consisting of a birth date and a random numeric sequence.
259
+
260
+ Args:
261
+ start_date (Union[str, datetime], optional): The earliest possible date of birth
262
+ to consider when generating a random date. Defaults to '1960-01-01'.
263
+ end_date (Union[str, datetime], optional): The latest possible date of birth
264
+ to consider when generating a random date. Defaults to '2000-12-31'.
265
+ birth_date (Optional[str], optional): A specific birth date in 'YYYY-MM-DD' format.
266
+ If provided, this date is used instead of generating a random one. Defaults to None.
267
+
268
+ Returns:
269
+ str: A randomly generated ID number in the format 'YYMMDD-XXXXXXX',
270
+ where 'YYMMDD' is the birth date and 'XXXXXXX' is a 7-digit random number.
271
+ """
272
+ if not birth_date:
273
+ birth_date = generate_random_date(start_date, end_date)
274
+ birth_date = birth_date.replace('-', '')[2:]
275
+ return f"{birth_date}-{generate_random_number_string(7)}"
276
+
277
+
278
+
279
+ def generate_random_code(category: str) -> str:
280
+ """
281
+ Generate a random code value based on the specified category.
282
+
283
+ Supported categories:
284
+ - 'use': returns either 'mobile' or 'work'
285
+ - 'gender': returns either 'male' or 'female'
286
+
287
+ Args:
288
+ category (str): The category for which to generate a code. Must be one of ['use', 'gender'].
289
+
290
+ Raises:
291
+ AssertionError: If the category is not one of the supported values.
292
+
293
+ Returns:
294
+ str: A randomly selected code corresponding to the given category.
295
+ """
296
+ categories = ['use', 'gender']
297
+ assert category in categories, log(f"The category must be one of the values in the {categories}, but got {category}", "error")
298
+
299
+ if category == 'use':
300
+ return random.choice(['mobile', 'work'])
301
+ elif category == 'gender':
302
+ return random.choice(['male', 'female'])
303
+
304
+
305
+
306
+ def generate_random_code_with_prob(codes: list[Any],
307
+ probs: list[float]) -> Tuple[int, str]:
308
+ """
309
+ Select a random code from the given list of codes based on the provided probabilities.
310
+
311
+ Args:
312
+ codes (list[Any]): A list of codes from which to choose.
313
+ probs (list[float]): A list of probabilities corresponding to each code.
314
+ The probabilities must sum to 1.
315
+
316
+ Returns:
317
+ Tuple[int, str]: The randomly chosen code. The exact type depends on the elements of `codes`.
318
+ (Adjust this description if the return type is known specifically.)
319
+ """
320
+ assert round(sum(probs), 4) == 1, log(f"The sum of the probabilities would be a 1, but got {probs}", "error")
321
+ assert len(codes) == len(probs), log(f"The lengths of codes and probabilities must be the same, but got codes length: {len(codes)} and probs length: {len(probs)}", "error")
322
+
323
+ chosen = random.choices(population=codes, weights=probs, k=1)[0]
324
+
325
+ return chosen
326
+
327
+
328
+
329
+ def generate_random_specialty(department: str,
330
+ specialty_path: Optional[str] = None,
331
+ verbose: bool = True) -> Tuple[str, str]:
332
+ """
333
+ Generate a random specialty and its corresponding code for a given department.
334
+
335
+ Args:
336
+ department (str): The name of the department to look up specialties for.
337
+ specialty_path (Optional[str], optional): Path to the JSON file containing department-specialty mappings. Defaults to None.
338
+ verbose (bool, optional): Whether to log a warning message if the department is not found.
339
+ Defaults to True.
340
+
341
+ Returns:
342
+ Tuple[str, str]: A tuple containing a randomly selected specialty (str) and its code (int).
343
+ If the department is not found, returns ('{PLACEHOLDER}', '{PLACEHOLDER}').
344
+ """
345
+ if specialty_path == None:
346
+ specialty_path = str(resources.files("h_adminsim.assets.departments").joinpath("department.json"))
347
+
348
+ if registry.SPECIALTIES is None:
349
+ department_data = json_load(specialty_path)['specialty']
350
+ registry.SPECIALTIES = {k2: {'code': v2['code'], 'field': v2['field']} for v1 in department_data.values() for k2, v2 in v1['subspecialty'].items()}
351
+
352
+ if department in registry.SPECIALTIES:
353
+ index = random.choice(range(len(registry.SPECIALTIES[department]['field'])))
354
+ return registry.SPECIALTIES[department]['field'][index], f"{registry.SPECIALTIES[department]['code']}-{index}"
355
+
356
+ if verbose:
357
+ log(f'No matched department {department}. `{{PLACEHOLDER}}` string will return.', 'warning')
358
+ return '{PLACEHOLDER}', '{PLACEHOLDER}'
h_adminsim/version.txt ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,30 @@
1
+ Copyright (c) 2025, H-AdminSim developers.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are
6
+ met:
7
+
8
+ * Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above
12
+ copyright notice, this list of conditions and the following
13
+ disclaimer in the documentation and/or other materials provided
14
+ with the distribution.
15
+
16
+ * Neither the name of the H-AdminSim Developers nor the names of any
17
+ contributors may be used to endorse or promote products derived
18
+ from this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.