├── .gitignore ├── LICENSE ├── README.md ├── create_agg_outputs.ipynb ├── create_configs.py ├── doc ├── devnotes.rst └── obflow_6_dev.rst ├── explore_output_variability.ipynb ├── notes.rst ├── obflow_1.py ├── obflow_2_resource.py ├── obflow_3_resources.py ├── obflow_4_oo_1.py ├── obflow_5_oo_2.py ├── obflow_6.py ├── obflow_6_output.py ├── simhacking.ipynb ├── simpy-first-oo-patflow-model.ipynb ├── simpy-getting-started.ipynb └── warmup.ipynb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/*.xml 2 | .idea/*.iml 3 | .ipynb_checkpoints/ 4 | __pycache__/ 5 | simpy_examples/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Mark Isken 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Getting started with using SimPy to build patient flow discrete event simulation models. 2 | 3 | **UPDATE** I fixed the following links so they now point to my new [Bits Of Analytics blog](https://bitsofanalytics.org/). 4 | 5 | - [Getting started with SimPy for patient flow modeling](https://bitsofanalytics.org/posts/simpy-getting-started-patflow-model/simpy-getting-started.html) 6 | - [An object oriented SimPy patient flow simulation model](https://bitsofanalytics.org/posts/simpy-oo-patflow-model/simpy-oo-patflow-model.html) 7 | 8 | Some more recent work includes: 9 | 10 | - [Using SimPy to simulate a vaccine clinic - Part 1](https://bitsofanalytics.org/posts/simpy-vaccine-clinic-part1/simpy_getting_started_vaccine_clinic.html) 11 | - [Using SimPy to simulate a vaccine clinic - Part 2](https://bitsofanalytics.org/posts/simpy-vaccine-clinic-part2/simpy_vaccine_clinic_improvements.html) 12 | -------------------------------------------------------------------------------- /create_agg_outputs.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "fad77f18-1b83-4f6c-bda9-e0cad90270c0", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 62, 14 | "id": "740d85e0-4441-4969-bfc8-d2849d1684be", 15 | "metadata": { 16 | "tags": [] 17 | }, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy as np\n", 21 | "import pandas as pd\n", 22 | "from scipy.stats import erlang, expon\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "import math\n", 25 | "import operator\n", 26 | "\n", 27 | "import obnetwork2" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 3, 33 | "id": "05fd5600-5a44-46b4-90f4-0c1326de4fd1", 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "%matplotlib inline" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 15, 43 | "id": "7f129a07-ae19-4177-9305-fd9bc069748d", 44 | "metadata": {}, 45 | "outputs": [ 46 | { 47 | "name": "stdout", 48 | "output_type": "stream", 49 | "text": [ 50 | "\n", 51 | "Int64Index: 3750 entries, 0 to 1442\n", 52 | "Data columns (total 78 columns):\n", 53 | " # Column Non-Null Count Dtype \n", 54 | "--- ------ -------------- ----- \n", 55 | " 0 scenario 3750 non-null int64 \n", 56 | " 1 rep 3750 non-null int64 \n", 57 | " 2 timestamp 3750 non-null object \n", 58 | " 3 num_days 3750 non-null float64\n", 59 | " 4 num_visits_obs 3750 non-null float64\n", 60 | " 5 num_visits_ldr 3750 non-null float64\n", 61 | " 6 num_visits_pp 3750 non-null float64\n", 62 | " 7 num_visits_csect 3750 non-null float64\n", 63 | " 8 planned_los_mean_obs 3750 non-null float64\n", 64 | " 9 actual_los_mean_obs 3750 non-null float64\n", 65 | " 10 planned_los_sd_obs 3750 non-null float64\n", 66 | " 11 actual_los_sd_obs 3750 non-null float64\n", 67 | " 12 planned_los_cv2_obs 3750 non-null float64\n", 68 | " 13 actual_los_cv2_obs 3750 non-null float64\n", 69 | " 14 planned_los_skew_obs 3750 non-null float64\n", 70 | " 15 actual_los_skew_obs 3750 non-null float64\n", 71 | " 16 planned_los_kurt_obs 3750 non-null float64\n", 72 | " 17 actual_los_kurt_obs 3750 non-null float64\n", 73 | " 18 planned_los_mean_ldr 3750 non-null float64\n", 74 | " 19 actual_los_mean_ldr 3750 non-null float64\n", 75 | " 20 planned_los_sd_ldr 3750 non-null float64\n", 76 | " 21 actual_los_sd_ldr 3750 non-null float64\n", 77 | " 22 planned_los_cv2_ldr 3750 non-null float64\n", 78 | " 23 actual_los_cv2_ldr 3750 non-null float64\n", 79 | " 24 planned_los_skew_ldr 3750 non-null float64\n", 80 | " 25 actual_los_skew_ldr 3750 non-null float64\n", 81 | " 26 planned_los_kurt_ldr 3750 non-null float64\n", 82 | " 27 actual_los_kurt_ldr 3750 non-null float64\n", 83 | " 28 planned_los_mean_pp 3750 non-null float64\n", 84 | " 29 actual_los_mean_pp 3750 non-null float64\n", 85 | " 30 planned_los_sd_pp 3750 non-null float64\n", 86 | " 31 actual_los_sd_pp 3750 non-null float64\n", 87 | " 32 planned_los_cv2_pp 3750 non-null float64\n", 88 | " 33 actual_los_cv2_pp 3750 non-null float64\n", 89 | " 34 planned_los_skew_pp 3750 non-null float64\n", 90 | " 35 actual_los_skew_pp 3750 non-null float64\n", 91 | " 36 planned_los_kurt_pp 3750 non-null float64\n", 92 | " 37 actual_los_kurt_pp 3750 non-null float64\n", 93 | " 38 planned_los_mean_csect 3750 non-null float64\n", 94 | " 39 actual_los_mean_csect 3750 non-null float64\n", 95 | " 40 planned_los_sd_csect 3750 non-null float64\n", 96 | " 41 actual_los_sd_csect 3750 non-null float64\n", 97 | " 42 planned_los_cv2_csect 3750 non-null float64\n", 98 | " 43 actual_los_cv2_csect 3750 non-null float64\n", 99 | " 44 planned_los_skew_csect 3750 non-null float64\n", 100 | " 45 actual_los_skew_csect 3750 non-null float64\n", 101 | " 46 planned_los_kurt_csect 3750 non-null float64\n", 102 | " 47 actual_los_kurt_csect 3750 non-null float64\n", 103 | " 48 iatime_mean_obs 3750 non-null float64\n", 104 | " 49 iatime_sd_obs 3750 non-null float64\n", 105 | " 50 iatime_skew_obs 3750 non-null float64\n", 106 | " 51 iatime_kurt_obs 3750 non-null float64\n", 107 | " 52 iatime_mean_ldr 3750 non-null float64\n", 108 | " 53 iatime_sd_ldr 3750 non-null float64\n", 109 | " 54 iatime_skew_ldr 3750 non-null float64\n", 110 | " 55 iatime_kurt_ldr 3750 non-null float64\n", 111 | " 56 iatime_mean_csection 3750 non-null float64\n", 112 | " 57 iatime_sd_csection 3750 non-null float64\n", 113 | " 58 iatime_skew_csection 3750 non-null float64\n", 114 | " 59 iatime_kurt_csection 3750 non-null float64\n", 115 | " 60 iatime_mean_pp 3750 non-null float64\n", 116 | " 61 iatime_sd_pp 3750 non-null float64\n", 117 | " 62 iatime_skew_pp 3750 non-null float64\n", 118 | " 63 iatime_kurt_pp 3750 non-null float64\n", 119 | " 64 occ_mean_obs 3750 non-null float64\n", 120 | " 65 occ_mean_ldr 3750 non-null float64\n", 121 | " 66 occ_mean_csect 3750 non-null float64\n", 122 | " 67 occ_mean_pp 3750 non-null float64\n", 123 | " 68 occ_p95_obs 3750 non-null float64\n", 124 | " 69 occ_p95_ldr 3750 non-null float64\n", 125 | " 70 occ_p95_csect 3750 non-null float64\n", 126 | " 71 occ_p95_pp 3750 non-null float64\n", 127 | " 72 pct_waitq_ldr 3750 non-null float64\n", 128 | " 73 waitq_ldr_mean 3750 non-null float64\n", 129 | " 74 waitq_ldr_p95 3750 non-null float64\n", 130 | " 75 pct_blocked_by_pp 3750 non-null float64\n", 131 | " 76 blocked_by_pp_mean 3750 non-null float64\n", 132 | " 77 blocked_by_pp_p95 3750 non-null float64\n", 133 | "dtypes: float64(75), int64(2), object(1)\n", 134 | "memory usage: 2.3+ MB\n" 135 | ] 136 | } 137 | ], 138 | "source": [ 139 | "output_stats_summary_df = pd.read_csv('./output/output_stats_summary.csv').sort_values(by=['scenario', 'rep'])\n", 140 | "output_stats_summary_df.info()" 141 | ] 142 | }, 143 | { 144 | "cell_type": "code", 145 | "execution_count": 18, 146 | "id": "15c65687-6351-43b1-9f02-8fdbbf617c64", 147 | "metadata": {}, 148 | "outputs": [], 149 | "source": [ 150 | "output_stats_summary_agg_df = output_stats_summary_df.groupby(['scenario']).agg(\n", 151 | " num_visits_obs_mean=pd.NamedAgg(column='num_visits_obs', aggfunc='mean'),\n", 152 | " num_visits_ldr_mean=pd.NamedAgg(column='num_visits_ldr', aggfunc='mean'),\n", 153 | " num_visits_csect_mean=pd.NamedAgg(column='num_visits_csect', aggfunc='mean'),\n", 154 | " num_visits_pp_mean=pd.NamedAgg(column='num_visits_pp', aggfunc='mean'),\n", 155 | "\n", 156 | " planned_los_mean_mean_obs=pd.NamedAgg(column='planned_los_mean_obs', aggfunc='mean'),\n", 157 | " planned_los_mean_mean_ldr=pd.NamedAgg(column='planned_los_mean_ldr', aggfunc='mean'),\n", 158 | " planned_los_mean_mean_csect=pd.NamedAgg(column='planned_los_mean_csect', aggfunc='mean'),\n", 159 | " planned_los_mean_mean_pp=pd.NamedAgg(column='planned_los_mean_pp', aggfunc='mean'),\n", 160 | " \n", 161 | " actual_los_mean_mean_obs=pd.NamedAgg(column='actual_los_mean_obs', aggfunc='mean'),\n", 162 | " actual_los_mean_mean_ldr=pd.NamedAgg(column='actual_los_mean_ldr', aggfunc='mean'),\n", 163 | " actual_los_mean_mean_csect=pd.NamedAgg(column='actual_los_mean_csect', aggfunc='mean'),\n", 164 | " actual_los_mean_mean_pp=pd.NamedAgg(column='actual_los_mean_pp', aggfunc='mean'),\n", 165 | " \n", 166 | " planned_los_mean_cv2_obs=pd.NamedAgg(column='planned_los_cv2_obs', aggfunc='mean'),\n", 167 | " planned_los_mean_cv2_ldr=pd.NamedAgg(column='planned_los_cv2_ldr', aggfunc='mean'),\n", 168 | " planned_los_mean_cv2_csect=pd.NamedAgg(column='planned_los_cv2_csect', aggfunc='mean'),\n", 169 | " planned_los_mean_cv2_pp=pd.NamedAgg(column='planned_los_cv2_pp', aggfunc='mean'),\n", 170 | " \n", 171 | " actual_los_mean_cv2_obs=pd.NamedAgg(column='actual_los_cv2_obs', aggfunc='mean'),\n", 172 | " actual_los_mean_cv2_ldr=pd.NamedAgg(column='actual_los_cv2_ldr', aggfunc='mean'),\n", 173 | " actual_los_mean_cv2_csect=pd.NamedAgg(column='actual_los_cv2_csect', aggfunc='mean'),\n", 174 | " actual_los_mean_cv2_pp=pd.NamedAgg(column='actual_los_cv2_pp', aggfunc='mean'),\n", 175 | " \n", 176 | " occ_mean_mean_obs=pd.NamedAgg(column='occ_mean_obs', aggfunc='mean'),\n", 177 | " occ_mean_mean_ldr=pd.NamedAgg(column='occ_mean_ldr', aggfunc='mean'),\n", 178 | " occ_mean_mean_csect=pd.NamedAgg(column='occ_mean_csect', aggfunc='mean'),\n", 179 | " occ_mean_mean_pp=pd.NamedAgg(column='occ_mean_pp', aggfunc='mean'),\n", 180 | "\n", 181 | " occ_mean_p95_obs=pd.NamedAgg(column='occ_p95_obs', aggfunc='mean'),\n", 182 | " occ_mean_p95_ldr=pd.NamedAgg(column='occ_p95_ldr', aggfunc='mean'),\n", 183 | " occ_mean_p95_csect=pd.NamedAgg(column='occ_p95_csect', aggfunc='mean'),\n", 184 | " occ_mean_p95_pp=pd.NamedAgg(column='occ_p95_pp', aggfunc='mean'),\n", 185 | " \n", 186 | " mean_pct_waitq_ldr=pd.NamedAgg(column='pct_waitq_ldr', aggfunc='mean'),\n", 187 | " mean_waitq_ldr_mean=pd.NamedAgg(column='waitq_ldr_mean', aggfunc='mean'),\n", 188 | " mean_waitq_ldr_p95=pd.NamedAgg(column='waitq_ldr_p95', aggfunc='mean'), \n", 189 | "\n", 190 | " mean_pct_blocked_by_pp=pd.NamedAgg(column='pct_waitq_ldr', aggfunc='mean'),\n", 191 | " mean_blocked_by_pp_mean=pd.NamedAgg(column='blocked_by_pp_mean', aggfunc='mean'),\n", 192 | " mean_blocked_by_pp_p95=pd.NamedAgg(column='blocked_by_pp_p95', aggfunc='mean'),\n", 193 | ")" 194 | ] 195 | }, 196 | { 197 | "cell_type": "code", 198 | "execution_count": 23, 199 | "id": "629f89ee-184e-49a6-bfa4-e47bc799b87f", 200 | "metadata": {}, 201 | "outputs": [], 202 | "source": [ 203 | "output_stats_summary_agg_df.to_csv('./output/output_stats_summary_agg.csv', index=True)" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "id": "fc0d50f7-8720-4add-8a1a-fe8d0b1c76a6", 209 | "metadata": {}, 210 | "source": [ 211 | "Now compute queueing approximations" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 27, 217 | "id": "b1dc29f6-10d2-4a16-8335-032bf218c53b", 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "scenario_summary_stats_df = pd.read_csv('./output/output_stats_summary_agg.csv')\n", 222 | "input_params_df = pd.read_csv('./inputs/exp10_tandem05_metainputs.csv')\n", 223 | "#train_df.set_index('scenario', drop=False, inplace=True)" 224 | ] 225 | }, 226 | { 227 | "cell_type": "code", 228 | "execution_count": 26, 229 | "id": "0c7aae25-69ab-405e-9fbe-c1623bd90bef", 230 | "metadata": { 231 | "collapsed": true, 232 | "jupyter": { 233 | "outputs_hidden": true 234 | }, 235 | "tags": [] 236 | }, 237 | "outputs": [ 238 | { 239 | "name": "stdout", 240 | "output_type": "stream", 241 | "text": [ 242 | "\n", 243 | "RangeIndex: 150 entries, 0 to 149\n", 244 | "Data columns (total 35 columns):\n", 245 | " # Column Non-Null Count Dtype \n", 246 | "--- ------ -------------- ----- \n", 247 | " 0 scenario 150 non-null int64 \n", 248 | " 1 num_visits_obs_mean 150 non-null float64\n", 249 | " 2 num_visits_ldr_mean 150 non-null float64\n", 250 | " 3 num_visits_csect_mean 150 non-null float64\n", 251 | " 4 num_visits_pp_mean 150 non-null float64\n", 252 | " 5 planned_los_mean_mean_obs 150 non-null float64\n", 253 | " 6 planned_los_mean_mean_ldr 150 non-null float64\n", 254 | " 7 planned_los_mean_mean_csect 150 non-null float64\n", 255 | " 8 planned_los_mean_mean_pp 150 non-null float64\n", 256 | " 9 actual_los_mean_mean_obs 150 non-null float64\n", 257 | " 10 actual_los_mean_mean_ldr 150 non-null float64\n", 258 | " 11 actual_los_mean_mean_csect 150 non-null float64\n", 259 | " 12 actual_los_mean_mean_pp 150 non-null float64\n", 260 | " 13 planned_los_mean_cv2_obs 150 non-null float64\n", 261 | " 14 planned_los_mean_cv2_ldr 150 non-null float64\n", 262 | " 15 planned_los_mean_cv2_csect 150 non-null float64\n", 263 | " 16 planned_los_mean_cv2_pp 150 non-null float64\n", 264 | " 17 actual_los_mean_cv2_obs 150 non-null float64\n", 265 | " 18 actual_los_mean_cv2_ldr 150 non-null float64\n", 266 | " 19 actual_los_mean_cv2_csect 150 non-null float64\n", 267 | " 20 actual_los_mean_cv2_pp 150 non-null float64\n", 268 | " 21 occ_mean_mean_obs 150 non-null float64\n", 269 | " 22 occ_mean_mean_ldr 150 non-null float64\n", 270 | " 23 occ_mean_mean_csect 150 non-null float64\n", 271 | " 24 occ_mean_mean_pp 150 non-null float64\n", 272 | " 25 occ_mean_p95_obs 150 non-null float64\n", 273 | " 26 occ_mean_p95_ldr 150 non-null float64\n", 274 | " 27 occ_mean_p95_csect 150 non-null float64\n", 275 | " 28 occ_mean_p95_pp 150 non-null float64\n", 276 | " 29 mean_pct_waitq_ldr 150 non-null float64\n", 277 | " 30 mean_waitq_ldr_mean 150 non-null float64\n", 278 | " 31 mean_waitq_ldr_p95 150 non-null float64\n", 279 | " 32 mean_pct_blocked_by_pp 150 non-null float64\n", 280 | " 33 mean_blocked_by_pp_mean 150 non-null float64\n", 281 | " 34 mean_blocked_by_pp_p95 150 non-null float64\n", 282 | "dtypes: float64(34), int64(1)\n", 283 | "memory usage: 41.1 KB\n" 284 | ] 285 | } 286 | ], 287 | "source": [ 288 | "scenario_summary_stats_df.info()" 289 | ] 290 | }, 291 | { 292 | "cell_type": "code", 293 | "execution_count": 28, 294 | "id": "2db39827-ba0e-4a5c-9704-796f0480454a", 295 | "metadata": { 296 | "tags": [] 297 | }, 298 | "outputs": [ 299 | { 300 | "name": "stdout", 301 | "output_type": "stream", 302 | "text": [ 303 | "\n", 304 | "RangeIndex: 150 entries, 0 to 149\n", 305 | "Data columns (total 40 columns):\n", 306 | " # Column Non-Null Count Dtype \n", 307 | "--- ------ -------------- ----- \n", 308 | " 0 scenario 150 non-null int64 \n", 309 | " 1 arrival_rate 150 non-null float64\n", 310 | " 2 mean_los_obs 150 non-null float64\n", 311 | " 3 num_erlang_stages_obs 150 non-null int64 \n", 312 | " 4 mean_los_ldr 150 non-null int64 \n", 313 | " 5 num_erlang_stages_ldr 150 non-null int64 \n", 314 | " 6 mean_los_csect 150 non-null int64 \n", 315 | " 7 num_erlang_stages_csect 150 non-null int64 \n", 316 | " 8 mean_los_pp_noc 150 non-null int64 \n", 317 | " 9 mean_los_pp_c 150 non-null int64 \n", 318 | " 10 num_erlang_stages_pp 150 non-null int64 \n", 319 | " 11 c_sect_prob 150 non-null float64\n", 320 | " 12 tot_c_rate 150 non-null float64\n", 321 | " 13 acc_tar_obs 150 non-null float64\n", 322 | " 14 acc_tar_ldr 150 non-null float64\n", 323 | " 15 acc_tar_pp 150 non-null float64\n", 324 | " 16 daily_arr_rate 150 non-null float64\n", 325 | " 17 alos_obs 150 non-null float64\n", 326 | " 18 alos_ldr_1 150 non-null float64\n", 327 | " 19 alos_ldr_2 150 non-null float64\n", 328 | " 20 alos_pp_noc 150 non-null int64 \n", 329 | " 21 alos_pp_c 150 non-null int64 \n", 330 | " 22 load_obs 150 non-null float64\n", 331 | " 23 load_ldr 150 non-null float64\n", 332 | " 24 load_pp 150 non-null float64\n", 333 | " 25 cap_obs 150 non-null int64 \n", 334 | " 26 cap_ldr 150 non-null int64 \n", 335 | " 27 cap_pp 150 non-null int64 \n", 336 | " 28 lam_obs 150 non-null float64\n", 337 | " 29 lam_ldr 150 non-null float64\n", 338 | " 30 lam_pp 150 non-null float64\n", 339 | " 31 alos_obs.1 150 non-null float64\n", 340 | " 32 alos_ldr 150 non-null float64\n", 341 | " 33 alos_pp 150 non-null float64\n", 342 | " 34 rho_obs 150 non-null float64\n", 343 | " 35 rho_ldr 150 non-null float64\n", 344 | " 36 rho_pp 150 non-null float64\n", 345 | " 37 cv2_obs 150 non-null int64 \n", 346 | " 38 cv2_ldr 150 non-null float64\n", 347 | " 39 cv2_pp 150 non-null float64\n", 348 | "dtypes: float64(25), int64(15)\n", 349 | "memory usage: 47.0 KB\n" 350 | ] 351 | } 352 | ], 353 | "source": [ 354 | "input_params_df.info()" 355 | ] 356 | }, 357 | { 358 | "cell_type": "code", 359 | "execution_count": 35, 360 | "id": "62054ee9-4882-4a05-80ab-b5ec188dbb74", 361 | "metadata": {}, 362 | "outputs": [ 363 | { 364 | "data": { 365 | "text/html": [ 366 | "
\n", 367 | "\n", 380 | "\n", 381 | " \n", 382 | " \n", 383 | " \n", 384 | " \n", 385 | " \n", 386 | " \n", 387 | " \n", 388 | " \n", 389 | " \n", 390 | " \n", 391 | " \n", 392 | " \n", 393 | " \n", 394 | " \n", 395 | " \n", 396 | " \n", 397 | " \n", 398 | " \n", 399 | " \n", 400 | " \n", 401 | " \n", 402 | " \n", 403 | " \n", 404 | " \n", 405 | " \n", 406 | " \n", 407 | " \n", 408 | " \n", 409 | " \n", 410 | " \n", 411 | " \n", 412 | " \n", 413 | " \n", 414 | " \n", 415 | " \n", 416 | " \n", 417 | " \n", 418 | " \n", 419 | " \n", 420 | " \n", 421 | " \n", 422 | " \n", 423 | " \n", 424 | " \n", 425 | " \n", 426 | " \n", 427 | " \n", 428 | " \n", 429 | " \n", 430 | " \n", 431 | " \n", 432 | " \n", 433 | " \n", 434 | " \n", 435 | " \n", 436 | " \n", 437 | " \n", 438 | " \n", 439 | " \n", 440 | " \n", 441 | " \n", 442 | " \n", 443 | " \n", 444 | " \n", 445 | " \n", 446 | " \n", 447 | " \n", 448 | " \n", 449 | " \n", 450 | " \n", 451 | " \n", 452 | " \n", 453 | " \n", 454 | " \n", 455 | " \n", 456 | " \n", 457 | " \n", 458 | " \n", 459 | " \n", 460 | " \n", 461 | " \n", 462 | " \n", 463 | "
num_erlang_stages_ldrmean_los_csectnum_erlang_stages_csectmean_los_pp_nocmean_los_pp_cnum_erlang_stages_ppc_sect_probtot_c_rateacc_tar_obsacc_tar_ldr
0214487280.20.20.999950.95
1214487280.20.20.999950.95
2214487280.20.20.999950.95
3214487280.20.20.999950.85
4214487280.20.20.999950.85
\n", 464 | "
" 465 | ], 466 | "text/plain": [ 467 | " num_erlang_stages_ldr mean_los_csect num_erlang_stages_csect \\\n", 468 | "0 2 1 4 \n", 469 | "1 2 1 4 \n", 470 | "2 2 1 4 \n", 471 | "3 2 1 4 \n", 472 | "4 2 1 4 \n", 473 | "\n", 474 | " mean_los_pp_noc mean_los_pp_c num_erlang_stages_pp c_sect_prob \\\n", 475 | "0 48 72 8 0.2 \n", 476 | "1 48 72 8 0.2 \n", 477 | "2 48 72 8 0.2 \n", 478 | "3 48 72 8 0.2 \n", 479 | "4 48 72 8 0.2 \n", 480 | "\n", 481 | " tot_c_rate acc_tar_obs acc_tar_ldr \n", 482 | "0 0.2 0.99995 0.95 \n", 483 | "1 0.2 0.99995 0.95 \n", 484 | "2 0.2 0.99995 0.95 \n", 485 | "3 0.2 0.99995 0.85 \n", 486 | "4 0.2 0.99995 0.85 " 487 | ] 488 | }, 489 | "execution_count": 35, 490 | "metadata": {}, 491 | "output_type": "execute_result" 492 | } 493 | ], 494 | "source": [ 495 | "input_params_df.iloc[:, 5:15].head()" 496 | ] 497 | }, 498 | { 499 | "cell_type": "code", 500 | "execution_count": 29, 501 | "id": "247c8d37-9f47-4614-82b7-d5ced839684b", 502 | "metadata": {}, 503 | "outputs": [], 504 | "source": [ 505 | "obsim_mm_means_df = input_params_df.merge(scenario_summary_stats_df, on='scenario')" 506 | ] 507 | }, 508 | { 509 | "cell_type": "code", 510 | "execution_count": 30, 511 | "id": "3a00c26a-1891-4715-b8ad-8d4221d612db", 512 | "metadata": {}, 513 | "outputs": [ 514 | { 515 | "name": "stdout", 516 | "output_type": "stream", 517 | "text": [ 518 | "\n", 519 | "Int64Index: 150 entries, 0 to 149\n", 520 | "Data columns (total 74 columns):\n", 521 | " # Column Non-Null Count Dtype \n", 522 | "--- ------ -------------- ----- \n", 523 | " 0 scenario 150 non-null int64 \n", 524 | " 1 arrival_rate 150 non-null float64\n", 525 | " 2 mean_los_obs 150 non-null float64\n", 526 | " 3 num_erlang_stages_obs 150 non-null int64 \n", 527 | " 4 mean_los_ldr 150 non-null int64 \n", 528 | " 5 num_erlang_stages_ldr 150 non-null int64 \n", 529 | " 6 mean_los_csect 150 non-null int64 \n", 530 | " 7 num_erlang_stages_csect 150 non-null int64 \n", 531 | " 8 mean_los_pp_noc 150 non-null int64 \n", 532 | " 9 mean_los_pp_c 150 non-null int64 \n", 533 | " 10 num_erlang_stages_pp 150 non-null int64 \n", 534 | " 11 c_sect_prob 150 non-null float64\n", 535 | " 12 tot_c_rate 150 non-null float64\n", 536 | " 13 acc_tar_obs 150 non-null float64\n", 537 | " 14 acc_tar_ldr 150 non-null float64\n", 538 | " 15 acc_tar_pp 150 non-null float64\n", 539 | " 16 daily_arr_rate 150 non-null float64\n", 540 | " 17 alos_obs 150 non-null float64\n", 541 | " 18 alos_ldr_1 150 non-null float64\n", 542 | " 19 alos_ldr_2 150 non-null float64\n", 543 | " 20 alos_pp_noc 150 non-null int64 \n", 544 | " 21 alos_pp_c 150 non-null int64 \n", 545 | " 22 load_obs 150 non-null float64\n", 546 | " 23 load_ldr 150 non-null float64\n", 547 | " 24 load_pp 150 non-null float64\n", 548 | " 25 cap_obs 150 non-null int64 \n", 549 | " 26 cap_ldr 150 non-null int64 \n", 550 | " 27 cap_pp 150 non-null int64 \n", 551 | " 28 lam_obs 150 non-null float64\n", 552 | " 29 lam_ldr 150 non-null float64\n", 553 | " 30 lam_pp 150 non-null float64\n", 554 | " 31 alos_obs.1 150 non-null float64\n", 555 | " 32 alos_ldr 150 non-null float64\n", 556 | " 33 alos_pp 150 non-null float64\n", 557 | " 34 rho_obs 150 non-null float64\n", 558 | " 35 rho_ldr 150 non-null float64\n", 559 | " 36 rho_pp 150 non-null float64\n", 560 | " 37 cv2_obs 150 non-null int64 \n", 561 | " 38 cv2_ldr 150 non-null float64\n", 562 | " 39 cv2_pp 150 non-null float64\n", 563 | " 40 num_visits_obs_mean 150 non-null float64\n", 564 | " 41 num_visits_ldr_mean 150 non-null float64\n", 565 | " 42 num_visits_csect_mean 150 non-null float64\n", 566 | " 43 num_visits_pp_mean 150 non-null float64\n", 567 | " 44 planned_los_mean_mean_obs 150 non-null float64\n", 568 | " 45 planned_los_mean_mean_ldr 150 non-null float64\n", 569 | " 46 planned_los_mean_mean_csect 150 non-null float64\n", 570 | " 47 planned_los_mean_mean_pp 150 non-null float64\n", 571 | " 48 actual_los_mean_mean_obs 150 non-null float64\n", 572 | " 49 actual_los_mean_mean_ldr 150 non-null float64\n", 573 | " 50 actual_los_mean_mean_csect 150 non-null float64\n", 574 | " 51 actual_los_mean_mean_pp 150 non-null float64\n", 575 | " 52 planned_los_mean_cv2_obs 150 non-null float64\n", 576 | " 53 planned_los_mean_cv2_ldr 150 non-null float64\n", 577 | " 54 planned_los_mean_cv2_csect 150 non-null float64\n", 578 | " 55 planned_los_mean_cv2_pp 150 non-null float64\n", 579 | " 56 actual_los_mean_cv2_obs 150 non-null float64\n", 580 | " 57 actual_los_mean_cv2_ldr 150 non-null float64\n", 581 | " 58 actual_los_mean_cv2_csect 150 non-null float64\n", 582 | " 59 actual_los_mean_cv2_pp 150 non-null float64\n", 583 | " 60 occ_mean_mean_obs 150 non-null float64\n", 584 | " 61 occ_mean_mean_ldr 150 non-null float64\n", 585 | " 62 occ_mean_mean_csect 150 non-null float64\n", 586 | " 63 occ_mean_mean_pp 150 non-null float64\n", 587 | " 64 occ_mean_p95_obs 150 non-null float64\n", 588 | " 65 occ_mean_p95_ldr 150 non-null float64\n", 589 | " 66 occ_mean_p95_csect 150 non-null float64\n", 590 | " 67 occ_mean_p95_pp 150 non-null float64\n", 591 | " 68 mean_pct_waitq_ldr 150 non-null float64\n", 592 | " 69 mean_waitq_ldr_mean 150 non-null float64\n", 593 | " 70 mean_waitq_ldr_p95 150 non-null float64\n", 594 | " 71 mean_pct_blocked_by_pp 150 non-null float64\n", 595 | " 72 mean_blocked_by_pp_mean 150 non-null float64\n", 596 | " 73 mean_blocked_by_pp_p95 150 non-null float64\n", 597 | "dtypes: float64(59), int64(15)\n", 598 | "memory usage: 87.9 KB\n" 599 | ] 600 | } 601 | ], 602 | "source": [ 603 | "obsim_mm_means_df.info()" 604 | ] 605 | }, 606 | { 607 | "cell_type": "code", 608 | "execution_count": 71, 609 | "id": "e0022e31-9ed0-4a4c-b5a4-a9d13738cbc9", 610 | "metadata": {}, 611 | "outputs": [], 612 | "source": [ 613 | "def hyper_erlang_moment(rates, stages, probs, moment):\n", 614 | " terms = [probs[i - 1] * math.factorial(stages[i - 1] + moment - 1) * ( 1 / math.factorial(stages[i - 1] - 1)) * (stages[i - 1] * rates[i - 1]) ** (-moment) \n", 615 | " for i in range(1, len(rates) + 1)]\n", 616 | " \n", 617 | " return sum(terms)" 618 | ] 619 | }, 620 | { 621 | "cell_type": "code", 622 | "execution_count": 77, 623 | "id": "7c2acea8-c9cf-47db-9d3c-add9b56c7f3a", 624 | "metadata": {}, 625 | "outputs": [ 626 | { 627 | "name": "stdout", 628 | "output_type": "stream", 629 | "text": [ 630 | " scenario arr_rate prob_blockedby_ldr_approx prob_blockedby_ldr_sim \\\n", 631 | "0 1.0 0.114155 0.055086 0.051794 \n", 632 | "1 2.0 0.114155 0.063771 0.064413 \n", 633 | "2 3.0 0.114155 0.080531 0.086950 \n", 634 | "3 4.0 0.114155 0.165820 0.163261 \n", 635 | "4 5.0 0.114155 0.182374 0.181308 \n", 636 | ".. ... ... ... ... \n", 637 | "145 146.0 0.799087 0.000009 0.000006 \n", 638 | "146 147.0 0.799087 0.000009 0.000003 \n", 639 | "147 148.0 1.027397 0.000006 0.000000 \n", 640 | "148 149.0 1.027397 0.000006 0.000004 \n", 641 | "149 150.0 1.027397 0.000006 0.000004 \n", 642 | "\n", 643 | " condmeantime_blockedbyldr_approx condmeantime_blockedbyldr_sim \\\n", 644 | "0 3.778654 3.334622 \n", 645 | "1 4.035329 4.465882 \n", 646 | "2 4.507946 6.290030 \n", 647 | "3 5.224348 4.321107 \n", 648 | "4 5.596022 5.122632 \n", 649 | ".. ... ... \n", 650 | "145 0.729366 0.050032 \n", 651 | "146 0.729366 0.006303 \n", 652 | "147 0.639653 0.000000 \n", 653 | "148 0.639653 0.018089 \n", 654 | "149 0.639653 0.076608 \n", 655 | "\n", 656 | " ldr_effmean_svctime_approx ldr_effmean_svctime_sim \\\n", 657 | "0 11.912974 11.956362 \n", 658 | "1 12.487194 12.470950 \n", 659 | "2 13.475060 13.292243 \n", 660 | "3 11.254823 11.502984 \n", 661 | "4 11.723960 11.910698 \n", 662 | ".. ... ... \n", 663 | "145 11.999993 12.001325 \n", 664 | "146 11.999993 11.990008 \n", 665 | "147 11.999996 11.988540 \n", 666 | "148 11.999996 12.006090 \n", 667 | "149 11.999996 11.990163 \n", 668 | "\n", 669 | " prob_blockedby_pp_approx prob_blockedby_pp_sim \\\n", 670 | "0 2.319907e-02 0.051794 \n", 671 | "1 1.037940e-01 0.064413 \n", 672 | "2 2.001325e-01 0.086950 \n", 673 | "3 2.319907e-02 0.163261 \n", 674 | "4 1.037940e-01 0.181308 \n", 675 | ".. ... ... \n", 676 | "145 1.235314e-08 0.000006 \n", 677 | "146 1.933882e-09 0.000003 \n", 678 | "147 1.769363e-08 0.000000 \n", 679 | "148 2.147217e-09 0.000004 \n", 680 | "149 4.791773e-10 0.000004 \n", 681 | "\n", 682 | " condmeantime_blockedbypp_approx condmeantime_blockedbypp_sim \n", 683 | "0 6.526361 6.409555 \n", 684 | "1 8.966441 9.101602 \n", 685 | "2 11.480445 11.307689 \n", 686 | "3 6.526361 6.174898 \n", 687 | "4 8.966441 8.511664 \n", 688 | ".. ... ... \n", 689 | "145 1.345115 0.000000 \n", 690 | "146 1.317322 0.000000 \n", 691 | "147 1.181717 0.000000 \n", 692 | "148 1.151514 0.000000 \n", 693 | "149 1.136850 0.000000 \n", 694 | "\n", 695 | "[150 rows x 12 columns]\n" 696 | ] 697 | } 698 | ], 699 | "source": [ 700 | "results = []\n", 701 | "scenarios = range(1,151)\n", 702 | "\n", 703 | "for row in obsim_mm_means_df.iterrows():\n", 704 | "\n", 705 | " scenario = row[1]['scenario']\n", 706 | " arr_rate = row[1]['arrival_rate']\n", 707 | " c_sect_prob = row[1]['c_sect_prob']\n", 708 | " ldr_mean_svctime = row[1]['mean_los_ldr']\n", 709 | " ldr_cv2_svctime = 1 / row[1]['num_erlang_stages_ldr']\n", 710 | " ldr_cap = row[1]['cap_ldr']\n", 711 | " pp_mean_svctime = c_sect_prob * row[1]['mean_los_pp_c'] + (1 - c_sect_prob) * row[1]['mean_los_pp_noc']\n", 712 | " \n", 713 | " rates = [1 / row[1]['mean_los_pp_c'], 1 / row[1]['mean_los_pp_noc']]\n", 714 | " probs = [c_sect_prob, 1 - c_sect_prob]\n", 715 | " stages = [int(row[1]['num_erlang_stages_pp']), int(row[1]['num_erlang_stages_pp'])]\n", 716 | " moments = [hyper_erlang_moment(rates, stages, probs, moment) for moment in [1, 2]]\n", 717 | " variance = moments[1] - moments[0] ** 2\n", 718 | " cv2 = variance / moments[0] ** 2\n", 719 | " \n", 720 | " pp_cv2_svctime = cv2\n", 721 | " \n", 722 | " pp_cap = row[1]['cap_pp']\n", 723 | " sim_mean_waitq_ldr_mean = row[1]['mean_waitq_ldr_mean']\n", 724 | " sim_mean_pct_waitq_ldr = row[1]['mean_pct_waitq_ldr']\n", 725 | " sim_actual_los_mean_mean_ldr = row[1]['actual_los_mean_mean_ldr']\n", 726 | " sim_mean_pct_blocked_by_pp = row[1]['mean_pct_blocked_by_pp']\n", 727 | " sim_mean_blocked_by_pp_mean = row[1]['mean_blocked_by_pp_mean']\n", 728 | "\n", 729 | " ldr_pct_blockedby_pp = obnetwork2.ldr_prob_blockedby_pp_hat(arr_rate, pp_mean_svctime, pp_cap, pp_cv2_svctime)\n", 730 | " ldr_meantime_blockedby_pp = obnetwork2.ldr_condmeantime_blockedby_pp_hat(arr_rate, pp_mean_svctime, pp_cap, pp_cv2_svctime)\n", 731 | " (obs_meantime_blockedbyldr, ldr_effmean_svctime, obs_prob_blockedby_ldr, obs_condmeantime_blockedbyldr) = \\\n", 732 | " obnetwork2.obs_blockedby_ldr_hats(arr_rate, c_sect_prob, ldr_mean_svctime, ldr_cv2_svctime, ldr_cap,\n", 733 | " pp_mean_svctime, pp_cv2_svctime, pp_cap)\n", 734 | "\n", 735 | " scen_results = {'scenario': scenario,\n", 736 | " 'arr_rate': arr_rate,\n", 737 | " 'prob_blockedby_ldr_approx': obs_prob_blockedby_ldr,\n", 738 | " 'prob_blockedby_ldr_sim': sim_mean_pct_waitq_ldr,\n", 739 | "\n", 740 | " 'condmeantime_blockedbyldr_approx': obs_condmeantime_blockedbyldr,\n", 741 | " 'condmeantime_blockedbyldr_sim': sim_mean_waitq_ldr_mean,\n", 742 | " 'ldr_effmean_svctime_approx': ldr_effmean_svctime,\n", 743 | " 'ldr_effmean_svctime_sim': sim_actual_los_mean_mean_ldr,\n", 744 | " 'prob_blockedby_pp_approx': ldr_pct_blockedby_pp,\n", 745 | " 'prob_blockedby_pp_sim': sim_mean_pct_blocked_by_pp,\n", 746 | " 'condmeantime_blockedbypp_approx': ldr_meantime_blockedby_pp,\n", 747 | " 'condmeantime_blockedbypp_sim': sim_mean_blocked_by_pp_mean}\n", 748 | "\n", 749 | " results.append(scen_results)\n", 750 | "\n", 751 | " # print(\"scenario {}\\n\".format(scenario))\n", 752 | " # print(results)\n", 753 | "\n", 754 | "\n", 755 | "results_df = pd.DataFrame(results)\n", 756 | "print(results_df)\n", 757 | "\n", 758 | "#results_df.to_csv(\"obnetwork_approx_vs_sim.csv\")\n", 759 | "results_df.to_csv(\"./output/obnetwork_approx_vs_sim_testing.csv\")" 760 | ] 761 | }, 762 | { 763 | "cell_type": "code", 764 | "execution_count": 67, 765 | "id": "b7997abe-0761-4196-8197-b2be5fb676fa", 766 | "metadata": {}, 767 | "outputs": [], 768 | "source": [ 769 | "means = [48, 72]\n", 770 | "rates = [1 / mean for mean in means]\n", 771 | "probs = [0.8, 0.2]\n", 772 | "stages = [8, 8]\n", 773 | "moment = 1" 774 | ] 775 | }, 776 | { 777 | "cell_type": "code", 778 | "execution_count": 68, 779 | "id": "24a98076-7ce5-4b88-8122-d765e1d96d4a", 780 | "metadata": {}, 781 | "outputs": [ 782 | { 783 | "name": "stdout", 784 | "output_type": "stream", 785 | "text": [ 786 | "[38.400000000000006, 14.4]\n" 787 | ] 788 | }, 789 | { 790 | "data": { 791 | "text/plain": [ 792 | "52.800000000000004" 793 | ] 794 | }, 795 | "execution_count": 68, 796 | "metadata": {}, 797 | "output_type": "execute_result" 798 | } 799 | ], 800 | "source": [ 801 | "hyper_erlang_moment(rates, stages, probs, moment)" 802 | ] 803 | }, 804 | { 805 | "cell_type": "code", 806 | "execution_count": 73, 807 | "id": "bc3ecc95-f793-4079-8571-fdedc5b669c6", 808 | "metadata": {}, 809 | "outputs": [], 810 | "source": [ 811 | "moments = [hyper_erlang_moment(rates, stages, probs, moment) for moment in [1, 2]]" 812 | ] 813 | }, 814 | { 815 | "cell_type": "code", 816 | "execution_count": 74, 817 | "id": "74e79856-a1ce-42fc-9508-cdb20fcc5d39", 818 | "metadata": {}, 819 | "outputs": [ 820 | { 821 | "data": { 822 | "text/plain": [ 823 | "452.1600000000003" 824 | ] 825 | }, 826 | "execution_count": 74, 827 | "metadata": {}, 828 | "output_type": "execute_result" 829 | } 830 | ], 831 | "source": [ 832 | "variance = moments[1] - moments[0] ** 2\n", 833 | "variance" 834 | ] 835 | }, 836 | { 837 | "cell_type": "code", 838 | "execution_count": 75, 839 | "id": "193cb54b-1c90-4b9f-a854-10eaaeeb5960", 840 | "metadata": {}, 841 | "outputs": [ 842 | { 843 | "data": { 844 | "text/plain": [ 845 | "0.16219008264462817" 846 | ] 847 | }, 848 | "execution_count": 75, 849 | "metadata": {}, 850 | "output_type": "execute_result" 851 | } 852 | ], 853 | "source": [ 854 | "cv2 = variance / moments[0] ** 2\n", 855 | "cv2" 856 | ] 857 | }, 858 | { 859 | "cell_type": "code", 860 | "execution_count": 63, 861 | "id": "333ecc17-51e7-42fd-a345-f96a5a6cf79a", 862 | "metadata": {}, 863 | "outputs": [], 864 | "source": [ 865 | "def sumproduct(*lists): return sum(map(operator.mul, *lists))" 866 | ] 867 | }, 868 | { 869 | "cell_type": "code", 870 | "execution_count": 69, 871 | "id": "a519b4af-2de2-442f-969a-3bd656141fee", 872 | "metadata": {}, 873 | "outputs": [ 874 | { 875 | "data": { 876 | "text/plain": [ 877 | "52.800000000000004" 878 | ] 879 | }, 880 | "execution_count": 69, 881 | "metadata": {}, 882 | "output_type": "execute_result" 883 | } 884 | ], 885 | "source": [ 886 | "sumproduct(means, probs)" 887 | ] 888 | }, 889 | { 890 | "cell_type": "code", 891 | "execution_count": 47, 892 | "id": "8b0b3f37-d63a-4a01-8a78-4aaa425716f3", 893 | "metadata": {}, 894 | "outputs": [ 895 | { 896 | "data": { 897 | "text/plain": [ 898 | "24" 899 | ] 900 | }, 901 | "execution_count": 47, 902 | "metadata": {}, 903 | "output_type": "execute_result" 904 | } 905 | ], 906 | "source": [ 907 | "math.factorial(4)" 908 | ] 909 | }, 910 | { 911 | "cell_type": "code", 912 | "execution_count": 48, 913 | "id": "83016d45-062d-4d59-987c-3c540efafd5f", 914 | "metadata": {}, 915 | "outputs": [ 916 | { 917 | "data": { 918 | "text/plain": [ 919 | "-2" 920 | ] 921 | }, 922 | "execution_count": 48, 923 | "metadata": {}, 924 | "output_type": "execute_result" 925 | } 926 | ], 927 | "source": [ 928 | "-2" 929 | ] 930 | }, 931 | { 932 | "cell_type": "code", 933 | "execution_count": 49, 934 | "id": "9b2913ed-a471-4284-9fa5-386cd4ac5dda", 935 | "metadata": {}, 936 | "outputs": [ 937 | { 938 | "data": { 939 | "text/plain": [ 940 | "-3" 941 | ] 942 | }, 943 | "execution_count": 49, 944 | "metadata": {}, 945 | "output_type": "execute_result" 946 | } 947 | ], 948 | "source": [ 949 | "x = 3\n", 950 | "-x" 951 | ] 952 | } 953 | ], 954 | "metadata": { 955 | "kernelspec": { 956 | "display_name": "Python 3", 957 | "language": "python", 958 | "name": "python3" 959 | }, 960 | "language_info": { 961 | "codemirror_mode": { 962 | "name": "ipython", 963 | "version": 3 964 | }, 965 | "file_extension": ".py", 966 | "mimetype": "text/x-python", 967 | "name": "python", 968 | "nbconvert_exporter": "python", 969 | "pygments_lexer": "ipython3", 970 | "version": "3.8.8" 971 | } 972 | }, 973 | "nbformat": 4, 974 | "nbformat_minor": 5 975 | } 976 | -------------------------------------------------------------------------------- /create_configs.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pandas as pd 4 | import numpy as np 5 | import yaml 6 | 7 | 8 | def config_from_csv(exp_suffix_, scenarios_csv_path_, settings_path_, config_path_, output_path_, bat_path_): 9 | 10 | # Read scenarios file in DataFrame 11 | scenarios_df = pd.read_csv(scenarios_csv_path_) 12 | # Read settings file 13 | with open(settings_path_, 'rt') as settings_file: 14 | settings = yaml.safe_load(settings_file) 15 | print(settings) 16 | 17 | global_vars = {} 18 | with open(bat_path_, 'w') as bat_file: 19 | # Iterate over rows in scenarios file 20 | for row in scenarios_df.iterrows(): 21 | scenario = int(row[1]['scenario'].tolist()) 22 | 23 | global_vars['arrival_rate'] = row[1]['arrival_rate'].tolist() 24 | 25 | global_vars['mean_los_obs'] = row[1]['mean_los_obs'].tolist() 26 | global_vars['num_erlang_stages_obs'] = int(row[1]['num_erlang_stages_obs']) 27 | 28 | global_vars['mean_los_ldr'] = float(row[1]['mean_los_ldr']) 29 | global_vars['num_erlang_stages_ldr'] = int(row[1]['num_erlang_stages_ldr']) 30 | 31 | global_vars['mean_los_pp_noc'] = float(row[1]['mean_los_pp_noc']) 32 | global_vars['mean_los_pp_c'] = float(row[1]['mean_los_pp_c']) 33 | global_vars['num_erlang_stages_pp'] = int(row[1]['num_erlang_stages_pp']) 34 | 35 | global_vars['mean_los_csect'] = float(row[1]['mean_los_csect']) 36 | global_vars['num_erlang_stages_csect'] = int(row[1]['num_erlang_stages_csect']) 37 | 38 | global_vars['c_sect_prob'] = float(row[1]['c_sect_prob']) 39 | 40 | config = {} 41 | config['locations'] = settings['locations'] 42 | cap_obs = int(row[1]['cap_obs'].tolist()) 43 | cap_ldr = int(row[1]['cap_ldr'].tolist()) 44 | cap_pp = int(row[1]['cap_pp'].tolist()) 45 | config['locations'][1]['capacity'] = cap_obs 46 | config['locations'][2]['capacity'] = cap_ldr 47 | config['locations'][4]['capacity'] = cap_pp 48 | 49 | 50 | # Write scenario config file 51 | 52 | config['scenario'] = scenario 53 | config['run_settings'] = settings['run_settings'] 54 | config['paths'] = settings['paths'] 55 | config['random_number_streams'] = settings['random_number_streams'] 56 | 57 | config['routes'] = settings['routes'] 58 | config['global_vars'] = global_vars 59 | 60 | config_file_path = Path(config_path_) / f'scenario_{scenario}.yaml' 61 | 62 | with open(config_file_path, 'w', encoding='utf-8') as config_file: 63 | yaml.dump(config, config_file) 64 | 65 | run_line = f"python obflow_6.py {config_file_path} --loglevel=WARNING\n" 66 | bat_file.write(run_line) 67 | 68 | # Create output file processing line 69 | output_proc_line = f'python obflow_6_output.py {output_path_} {exp_suffix_} ' 70 | output_proc_line += f"--run_time {settings['run_settings']['run_time']} " 71 | output_proc_line += f"--warmup_time {settings['run_settings']['warmup_time']} --include_inputs " 72 | output_proc_line += f"--scenario_inputs_path {scenarios_csv_path_} --process_logs " 73 | output_proc_line += f"--stop_log_path {settings['paths']['stop_logs']} " 74 | output_proc_line += f"--occ_stats_path {settings['paths']['occ_stats']}" 75 | bat_file.write(output_proc_line) 76 | 77 | 78 | if __name__ == '__main__': 79 | exp_suffix = 'exp12' 80 | scenarios_csv_path = Path(f'input/{exp_suffix}_obflow06_metainputs_pc.csv') 81 | settings_path = Path(f'input/{exp_suffix}_obflow06_settings.yaml') 82 | config_path = Path(f'input/config/{exp_suffix}') 83 | bat_path = Path('./run') / f'{exp_suffix}_obflow06_run.sh' 84 | output_path = Path('./output') / f'{exp_suffix}/' 85 | 86 | config_from_csv(exp_suffix, scenarios_csv_path, settings_path, config_path, output_path, bat_path) -------------------------------------------------------------------------------- /doc/devnotes.rst: -------------------------------------------------------------------------------- 1 | Useful links 2 | ============ 3 | 4 | Docs 5 | https://simpy.readthedocs.io/en/latest/index.html 6 | 7 | Network models 8 | https://www.grotto-networking.com/DiscreteEventPython.html#Intro 9 | 10 | One approach to custom Resource 11 | http://simpy.readthedocs.io/en/latest/examples/latency.html 12 | 13 | 14 | DesMod = New DES package that builds on SimPy 15 | http://desmod.readthedocs.io/en/latest/ 16 | 17 | Not sure how active. I think I should start with just SimPy to 18 | decide for myself on the metalevel needs in terms of model building, 19 | logging, config files, CLI, etc. 20 | 21 | Tidygraph - maybe for representing flow networks visually? 22 | http://www.data-imaginist.com/2017/Introducing-tidygraph/ 23 | 24 | Vehicle traffic simulation with SUMO 25 | http://www.sumo.dlr.de/userdoc/Sumo_at_a_Glance.html 26 | http://sumo.dlr.de/wiki/Tutorials 27 | 28 | 29 | Router design 30 | ============= 31 | 32 | 33 | Logging 34 | ======= 35 | 36 | How best to do trace messages? Is this same use case as "logging"? 37 | 38 | In ns-3: 39 | 40 | No, tracing is for simulation output and logging for debugging, warnings and errors. 41 | 42 | https://www.nsnam.org/docs/release/3.29/manual/html/tracing.html 43 | https://www.nsnam.org/docs/release/3.29/manual/html/data-collection.html 44 | 45 | Developing a good tracing system is very important for subsequent 46 | analysis of output and potential animation. 47 | 48 | https://docs.python.org/3/library/logging.html 49 | 50 | https://bitbucket.org/snippets/benhowes/MKLXy/simpy30-fridge 51 | 52 | Software Project Mgt 53 | ==================== 54 | 55 | Semantic versioning seems like a good idea - https://semver.org/ 56 | -------------------------------------------------------------------------------- /doc/obflow_6_dev.rst: -------------------------------------------------------------------------------- 1 | Dev notes for obflow_6 2 | ---------------------- 3 | 4 | Let's try to add PatientType 2 - unscheduled C-section. 5 | 6 | - hard code in branching prob in constants 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /notes.rst: -------------------------------------------------------------------------------- 1 | Fast network simulation setup 2 | ----------------------------- 3 | 4 | https://fnss.github.io/ 5 | 6 | https://lorenzosaino.github.io/publications/fnss-simutools13-slides.pdf 7 | 8 | grotto networking 9 | ----------------- 10 | 11 | https://www.grotto-networking.com/DiscreteEventPython.html 12 | 13 | ns-3 14 | ---- 15 | 16 | Mature network simulation package aimed at researchers. Was thinking maybe 17 | could be used to simulation ptube or patient flow but seems so specific 18 | to computer networks that the additional complexity not worth it. 19 | 20 | Still might be useful for learning how to architect a similar, simpler, 21 | simulation package for healthcare systems. 22 | 23 | 24 | nxsim 25 | ----- 26 | 27 | https://github.com/kentwait/nxsim 28 | 29 | NetworkX + SimPy 30 | 31 | Forked from ComplexNetworkSim (Python 2) 32 | https://pythonhosted.org/ComplexNetworkSim/introduction.html 33 | 34 | This app has definite similarities to patient flow simulation. Docs 35 | address things like workflow and simulating multiple scenarios. -------------------------------------------------------------------------------- /obflow_1.py: -------------------------------------------------------------------------------- 1 | import simpy 2 | import numpy as np 3 | from numpy.random import RandomState 4 | 5 | """ 6 | Simple OB patient flow model - NOT OO 7 | 8 | Details: 9 | 10 | - Generate arrivals via Poisson process 11 | - Simple delays modeling stay in OBS, LDR, and PP units. No capacitated resource contention modeled. 12 | - Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files. 13 | 14 | """ 15 | 16 | # Arrival rate and length of stay inputs. 17 | ARR_RATE = 0.4 18 | MEAN_LOS_OBS = 3 19 | MEAN_LOS_LDR = 12 20 | MEAN_LOS_PP = 48 21 | 22 | RNG_SEED = 6353 23 | 24 | def source(env, arr_rate, prng=RandomState(0)): 25 | """Source generates patients according to a simple Poisson process 26 | 27 | Parameters 28 | ---------- 29 | env : simpy.Environment 30 | the simulation environment 31 | arr_rate : float 32 | exponential arrival rate 33 | prng : RandomState object 34 | Seeded RandomState object for generating pseudo-random numbers. 35 | See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html 36 | 37 | """ 38 | 39 | patients_created = 0 40 | 41 | # Infinite loop for generatirng patients according to a poisson process. 42 | while True: 43 | 44 | # Generate next interarrival time 45 | iat = prng.exponential(1.0 / arr_rate) 46 | 47 | # Generate length of stay in each unit for this patient 48 | los_obs = prng.exponential(MEAN_LOS_OBS) 49 | los_ldr = prng.exponential(MEAN_LOS_LDR) 50 | los_pp = prng.exponential(MEAN_LOS_PP) 51 | 52 | # Update counter of patients 53 | patients_created += 1 54 | 55 | # Create a new patient flow process. 56 | obp = obpatient_flow(env, 'Patient{}'.format(patients_created), 57 | los_obs=los_obs, los_ldr=los_ldr, los_pp=los_pp) 58 | 59 | # Register the process with the simulation environment 60 | env.process(obp) 61 | 62 | # This process will now yield to a 'timeout' event. This process will resume after iat time units. 63 | yield env.timeout(iat) 64 | 65 | # Define an obpatient_flow "process" 66 | def obpatient_flow(env, name, los_obs, los_ldr, los_pp): 67 | """Process function modeling how a patient flows through system. 68 | 69 | Parameters 70 | ---------- 71 | env : simpy.Environment 72 | the simulation environment 73 | name : str 74 | process instance id 75 | los_obs : float 76 | length of stay in OBS unit 77 | los_ldr : float 78 | length of stay in LDR unit 79 | los_pp : float 80 | length of stay in PP unit 81 | """ 82 | 83 | # Note how we are simply modeling each stay as a delay. There 84 | # is NO contention for any resources. 85 | print("{} entering OBS at {}".format(name, env.now)) 86 | yield env.timeout(los_obs) 87 | 88 | print("{} entering LDR at {}".format(name, env.now)) 89 | yield env.timeout(los_ldr) 90 | 91 | print("{} entering PP at {}".format(name, env.now)) 92 | yield env.timeout(los_pp) 93 | 94 | 95 | # Initialize a simulation environment 96 | env = simpy.Environment() 97 | 98 | # Initialize a random number generator. 99 | # See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html 100 | prng = RandomState(RNG_SEED) 101 | 102 | # Create a process generator and start it and add it to the env 103 | # Calling obpatient(env) creates the generator. 104 | # env.process() starts and adds it to env 105 | runtime = 250 106 | env.process(source(env, ARR_RATE, prng)) 107 | 108 | # Run the simulation 109 | env.run(until=runtime) -------------------------------------------------------------------------------- /obflow_2_resource.py: -------------------------------------------------------------------------------- 1 | import simpy 2 | import numpy as np 3 | from numpy.random import RandomState 4 | 5 | """ 6 | Simple OB patient flow model 2 - NOT OO 7 | 8 | Details: 9 | 10 | - Generate arrivals via Poisson process 11 | - Uses one Resource objects to model OBS, the other units are modeled as simple delays. 12 | - Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files. 13 | 14 | """ 15 | # Arrival rate and length of stay inputs. 16 | ARR_RATE = 0.4 17 | MEAN_LOS_OBS = 3 18 | MEAN_LOS_LDR = 12 19 | MEAN_LOS_PP = 48 20 | 21 | CAPACITY_OBS = 2 22 | 23 | RNG_SEED = 6353 24 | 25 | def patient_generator(env, arr_rate, prng=RandomState(0)): 26 | """Generates patients according to a simple Poisson process 27 | 28 | Parameters 29 | ---------- 30 | env : simpy.Environment 31 | the simulation environment 32 | arr_rate : float 33 | exponential arrival rate 34 | prng : RandomState object 35 | Seeded RandomState object for generating pseudo-random numbers. 36 | See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html 37 | 38 | """ 39 | 40 | patients_created = 0 41 | 42 | # Infinite loop for generatirng patients according to a poisson process. 43 | while True: 44 | 45 | # Generate next interarrival time 46 | iat = prng.exponential(1.0 / arr_rate) 47 | 48 | # Generate length of stay in each unit for this patient 49 | los_obs = prng.exponential(MEAN_LOS_OBS) 50 | los_ldr = prng.exponential(MEAN_LOS_LDR) 51 | los_pp = prng.exponential(MEAN_LOS_PP) 52 | 53 | # Update counter of patients 54 | patients_created += 1 55 | 56 | # Create a new patient flow process. 57 | obp = obpatient_flow(env, 'Patient{}'.format(patients_created), 58 | los_obs=los_obs, los_ldr=los_ldr, los_pp=los_pp) 59 | 60 | # Register the process with the simulation environment 61 | env.process(obp) 62 | 63 | # This process will now yield to a 'timeout' event. This process will resume after iat time units. 64 | yield env.timeout(iat) 65 | 66 | 67 | def obpatient_flow(env, name, los_obs, los_ldr, los_pp): 68 | """Process function modeling how a patient flows through system. 69 | 70 | Parameters 71 | ---------- 72 | env : simpy.Environment 73 | the simulation environment 74 | name : str 75 | process instance id 76 | los_obs : float 77 | length of stay in OBS unit 78 | los_ldr : float 79 | length of stay in LDR unit 80 | los_pp : float 81 | length of stay in PP unit 82 | """ 83 | 84 | print("{} trying to get OBS at {}".format(name, env.now)) 85 | 86 | # Timestamp when patient tried to get OBS bed 87 | bed_request_ts = env.now 88 | # Request an obs bed 89 | bed_request = obs_unit.request() 90 | # Yield this process until a bed is available 91 | yield bed_request 92 | 93 | # We got an OBS bed 94 | print("{} entering OBS at {}".format(name, env.now)) 95 | # Let's see if we had to wait to get the bed. 96 | if env.now > bed_request_ts: 97 | print("{} waited {} time units for OBS bed".format(name, env.now - bed_request_ts)) 98 | 99 | # Yield this process again. Now wait until our length of stay elapses. 100 | # This is the actual stay in the bed 101 | yield env.timeout(los_obs) 102 | 103 | # All done with OBS, release the bed. Note that we pass the bed_request object 104 | # to the release() function so that the correct unit of the resource is released. 105 | obs_unit.release(bed_request) 106 | print("{} leaving OBS at {}".format(name, env.now)) 107 | 108 | # Continue on through LDR and PP; modeled as simple delays for now. 109 | 110 | print("{} entering LDR at {}".format(name, env.now)) 111 | yield env.timeout(los_ldr) 112 | 113 | print("{} entering PP at {}".format(name, env.now)) 114 | yield env.timeout(los_pp) 115 | 116 | 117 | # Initialize a simulation environment 118 | env = simpy.Environment() 119 | 120 | # Initialize a random number generator. 121 | # See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html 122 | prng = RandomState(RNG_SEED) 123 | 124 | # Declare a Resource to model OBS unit. Default capacity is 1, we pass in desired capacity. 125 | obs_unit = simpy.Resource(env, CAPACITY_OBS) 126 | 127 | # Run the simulation for a while 128 | runtime = 250 129 | env.process(patient_generator(env, ARR_RATE, prng)) 130 | env.run(until=runtime) 131 | -------------------------------------------------------------------------------- /obflow_3_resources.py: -------------------------------------------------------------------------------- 1 | import simpy 2 | import numpy as np 3 | from numpy.random import RandomState 4 | 5 | """ 6 | Simple OB patient flow model 3 - NOT OO 7 | 8 | Details: 9 | 10 | - Generate arrivals via Poisson process 11 | - Uses one Resource objects to model OBS, LDR, and PP. 12 | - Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files. 13 | - Additional functionality added to arrival generator (initial delay and arrival stop time). 14 | 15 | """ 16 | ARR_RATE = 0.4 17 | MEAN_LOS_OBS = 3 18 | MEAN_LOS_LDR = 12 19 | MEAN_LOS_PP = 48 20 | 21 | CAPACITY_OBS = 2 22 | CAPACITY_LDR = 6 23 | CAPACITY_PP = 24 24 | 25 | RNG_SEED = 6353 26 | 27 | def patient_generator(env, arr_stream, arr_rate, initial_delay=0, 28 | stoptime=simpy.core.Infinity, prng=RandomState(0)): 29 | """Generates patients according to a simple Poisson process 30 | 31 | Parameters 32 | ---------- 33 | env : simpy.Environment 34 | the simulation environment 35 | arr_rate : float 36 | exponential arrival rate 37 | initial_delay: float (default 0) 38 | time before arrival generation should begin 39 | stoptime: float (default Infinity) 40 | time after which no arrivals are generated 41 | prng : RandomState object 42 | Seeded RandomState object for generating pseudo-random numbers. 43 | See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html 44 | 45 | """ 46 | 47 | patients_created = 0 48 | 49 | # Yield for the initial delay 50 | yield env.timeout(initial_delay) 51 | 52 | # Generate arrivals as long as simulation time is before stoptime 53 | while env.now < stoptime: 54 | 55 | iat = prng.exponential(1.0 / arr_rate) 56 | 57 | # Sample los distributions 58 | los_obs = prng.exponential(MEAN_LOS_OBS) 59 | los_ldr = prng.exponential(MEAN_LOS_LDR) 60 | los_pp = prng.exponential(MEAN_LOS_PP) 61 | 62 | 63 | # Create new patient process instance 64 | patients_created += 1 65 | obp = obpatient_flow(env, 'Patient{}'.format(patients_created), 66 | los_obs=los_obs, los_ldr=los_ldr, los_pp=los_pp) 67 | 68 | env.process(obp) 69 | 70 | # Compute next interarrival time 71 | 72 | yield env.timeout(iat) 73 | 74 | 75 | def obpatient_flow(env, name, los_obs, los_ldr, los_pp): 76 | """Process function modeling how a patient flows through system. 77 | 78 | Parameters 79 | ---------- 80 | env : simpy.Environment 81 | the simulation environment 82 | name : str 83 | process instance id 84 | los_obs : float 85 | length of stay in OBS unit 86 | los_ldr : float 87 | length of stay in LDR unit 88 | los_pp : float 89 | length of stay in PP unit 90 | """ 91 | 92 | # Note the repetitive code and the use of separate request objects for each 93 | # stay in the different units. 94 | 95 | # OBS 96 | print("{} trying to get OBS at {}".format(name, env.now)) 97 | bed_request_ts = env.now 98 | bed_request1 = obs_unit.request() # Request an OBS bed 99 | yield bed_request1 100 | print("{} entering OBS at {}".format(name, env.now)) 101 | if env.now > bed_request_ts: 102 | print("{} waited {} time units for OBS bed".format(name, env.now- bed_request_ts)) 103 | yield env.timeout(los_obs) # Stay in obs bed 104 | 105 | print("{} trying to get LDR at {}".format(name, env.now)) 106 | bed_request_ts = env.now 107 | bed_request2 = ldr_unit.request() # Request an LDR bed 108 | yield bed_request2 109 | 110 | # Got LDR bed, release OBS bed 111 | obs_unit.release(bed_request1) # Release the OBS bed 112 | print("{} leaving OBS at {}".format(name, env.now)) 113 | 114 | # LDR stay 115 | print("{} entering LDR at {}".format(name, env.now)) 116 | if env.now > bed_request_ts: 117 | print("{} waited {} time units for LDR bed".format(name, env.now - bed_request_ts)) 118 | yield env.timeout(los_ldr) # Stay in LDR bed 119 | 120 | print("{} trying to get PP at {}".format(name, env.now)) 121 | bed_request_ts = env.now 122 | bed_request3 = pp_unit.request() # Request a PP bed 123 | yield bed_request3 124 | 125 | # Got PP bed, release LDR bed 126 | ldr_unit.release(bed_request2) # Release the obs bed 127 | print("{} leaving LDR at {}".format(name, env.now)) 128 | 129 | # PP stay 130 | print("{} entering PP at {}".format(name, env.now)) 131 | if env.now > bed_request_ts: 132 | print("{} waited {} time units for PP bed".format(name, env.now - bed_request_ts)) 133 | yield env.timeout(los_pp) # Stay in PP bed 134 | pp_unit.release(bed_request3) # Release the PP bed 135 | 136 | print("{} leaving PP and system at {}".format(name, env.now)) 137 | 138 | # Initialize a simulation environment 139 | env = simpy.Environment() 140 | 141 | prng = RandomState(RNG_SEED) 142 | 143 | rho_obs = ARR_RATE * MEAN_LOS_OBS / CAPACITY_OBS 144 | rho_ldr = ARR_RATE * MEAN_LOS_LDR / CAPACITY_LDR 145 | rho_pp = ARR_RATE * MEAN_LOS_PP / CAPACITY_PP 146 | 147 | print(rho_obs, rho_ldr, rho_pp) 148 | 149 | # Declare Resources to model all units 150 | obs_unit = simpy.Resource(env, CAPACITY_OBS) 151 | ldr_unit = simpy.Resource(env, CAPACITY_LDR) 152 | pp_unit = simpy.Resource(env, CAPACITY_PP) 153 | 154 | # Run the simulation for a while. Let's shut arrivals off after 100 time units. 155 | runtime = 250 156 | stop_arrivals = 100 157 | env.process(patient_generator(env, "Type1", ARR_RATE, 0, stop_arrivals, prng)) 158 | env.run(until=runtime) -------------------------------------------------------------------------------- /obflow_4_oo_1.py: -------------------------------------------------------------------------------- 1 | import simpy 2 | import numpy as np 3 | from numpy.random import RandomState 4 | 5 | 6 | 7 | """ 8 | Simple OB patient flow model 4 - Very simple OO 9 | 10 | Details: 11 | 12 | - Generate arrivals via Poisson process 13 | - Define an OBUnit class that contains a simpy.Resource object as a member. 14 | Not subclassing Resource, just trying to use it as a member. 15 | - Routing is hard coded (no Router class yet) 16 | - Just trying to get objects/processes to communicate 17 | 18 | """ 19 | 20 | ARR_RATE = 0.4 21 | MEAN_LOS_OBS = 3 22 | MEAN_LOS_LDR = 12 23 | MEAN_LOS_PP = 48 24 | 25 | CAPACITY_OBS = 2 26 | CAPACITY_LDR = 6 27 | CAPACITY_PP = 24 28 | 29 | RNG_SEED = 6353 30 | 31 | 32 | class OBunit(object): 33 | """ Models an OB unit with fixed capacity. 34 | 35 | Parameters 36 | ---------- 37 | env : simpy.Environment 38 | the simulation environment 39 | name : str 40 | unit name 41 | capacity : integer (or None) 42 | Number of beds. Use None for infinite capacity. 43 | 44 | """ 45 | 46 | def __init__(self, env, name, capacity=None, debug=False): 47 | if capacity is None: 48 | self.capacity = capacity=simpy.core.Infinity 49 | else: 50 | self.capacity = capacity 51 | 52 | self.unit = simpy.Resource(env, capacity) 53 | self.env = env 54 | self.name = name 55 | 56 | self.debug = debug 57 | self.num_entries = 0 58 | self.num_exits = 0 59 | self.tot_occ_time = 0.0 60 | 61 | def basic_stats_msg(self): 62 | 63 | msg = "{:6}:\t Entries={}, Exits={}, ALOS={:4.2f}".format(self.name, 64 | self.num_entries, 65 | self.num_exits, 66 | self.tot_occ_time / self.num_exits) 67 | return msg 68 | 69 | 70 | class OBpatient(object): 71 | def __init__(self, arrtime, arrstream, patient_id=0, prng=0): 72 | self.arrtime = arrtime 73 | self.arrstream = arrstream 74 | self.patient_id = patient_id 75 | self.name = 'Patient_{}'.format(patient_id) 76 | 77 | # Hard coding for now 78 | 79 | self.planned_los_obs = prng.exponential(MEAN_LOS_OBS) 80 | self.planned_los_ldr = prng.exponential(MEAN_LOS_LDR) 81 | self.planned_los_pp = prng.exponential(MEAN_LOS_PP) 82 | 83 | def __repr__(self): 84 | return "patientid: {}, arr_stream: {}, time: {}". \ 85 | format(self.patient_id, self.arrstream, self.arrtime) 86 | 87 | 88 | class OBPatientGenerator(object): 89 | """ Generates patients. 90 | 91 | Set the "out" member variable to the resource at which patient generated. 92 | 93 | Parameters 94 | ---------- 95 | env : simpy.Environment 96 | the simulation environment 97 | adist : function 98 | a no parameter function that returns the successive inter-arrival times of the packets 99 | initial_delay : number 100 | Starts generation after an initial delay. Default = 0 101 | stoptime : number 102 | Stops generation at the stoptime. Default is infinite 103 | 104 | """ 105 | 106 | def __init__(self, env, arr_stream, arr_rate, initial_delay=0, stoptime=250, debug=False): 107 | self.id = id 108 | self.env = env 109 | self.arr_rate = arr_rate 110 | self.arr_stream = arr_stream 111 | self.initial_delay = initial_delay 112 | self.stoptime = stoptime 113 | self.debug = debug 114 | self.num_patients_created = 0 115 | 116 | self.prng = RandomState(RNG_SEED) 117 | 118 | self.action = env.process(self.run()) # starts the run() method as a SimPy process 119 | 120 | 121 | def run(self): 122 | """The patient generator. 123 | """ 124 | # Delay for initial_delay 125 | yield self.env.timeout(self.initial_delay) 126 | # Main generator loop that terminates when stoptime reached 127 | while self.env.now < self.stoptime: 128 | # Delay until time for next arrival 129 | # Compute next interarrival time 130 | iat = self.prng.exponential(1.0 / self.arr_rate) 131 | yield self.env.timeout(iat) 132 | self.num_patients_created += 1 133 | # Create new patient 134 | obp = OBpatient(self.env.now, self.arr_stream, patient_id=self.num_patients_created, prng=self.prng) 135 | # Create a new flow instance for this patient. The OBpatient object carries all necessary info. 136 | obflow = obpatient_flow(env, obp, self.debug) 137 | # Register the new flow instance as a SimPy process. 138 | self.env.process(obflow) 139 | 140 | 141 | def obpatient_flow(env, obp, debug=False): 142 | """ Models the patient flow process. 143 | 144 | The sequence of units is hard coded for now. 145 | 146 | Parameters 147 | ---------- 148 | env : simpy.Environment 149 | the simulation environment 150 | obp : OBpatient object 151 | the patient to send through the flow process 152 | 153 | """ 154 | 155 | name = obp.name 156 | 157 | # OBS 158 | if debug: 159 | print("{} trying to get OBS at {}".format(name, env.now)) 160 | bed_request_ts = env.now 161 | bed_request1 = obs_unit.unit.request() # Request an obs bed 162 | yield bed_request1 163 | if debug: 164 | print("{} entering OBS at {}".format(name, env.now)) 165 | obs_unit.num_entries += 1 166 | enter_ts = env.now 167 | if debug: 168 | if env.now > bed_request_ts: 169 | print("{} waited {} time units for OBS bed".format(name, env.now- bed_request_ts)) 170 | 171 | yield env.timeout(obp.planned_los_obs) # Stay in obs bed 172 | 173 | if debug: 174 | print("{} trying to get LDR at {}".format(name, env.now)) 175 | bed_request_ts = env.now 176 | bed_request2 = ldr_unit.unit.request() # Request an obs bed 177 | yield bed_request2 178 | 179 | # Got LDR bed, release OBS bed 180 | obs_unit.unit.release(bed_request1) # Release the obs bed 181 | obs_unit.num_exits += 1 182 | exit_ts = env.now 183 | obs_unit.tot_occ_time += exit_ts - enter_ts 184 | if debug: 185 | print("{} leaving OBS at {}".format(name, env.now)) 186 | 187 | # LDR stay 188 | if debug: 189 | print("{} entering LDR at {}".format(name, env.now)) 190 | ldr_unit.num_entries += 1 191 | enter_ts = env.now 192 | if debug: 193 | if env.now > bed_request_ts: 194 | print("{} waited {} time units for LDR bed".format(name, env.now- bed_request_ts)) 195 | yield env.timeout(obp.planned_los_ldr) # Stay in LDR bed 196 | 197 | if debug: 198 | print("{} trying to get PP at {}".format(name, env.now)) 199 | bed_request_ts = env.now 200 | bed_request3 = pp_unit.unit.request() # Request a PP bed 201 | yield bed_request3 202 | 203 | # Got PP bed, release LDR bed 204 | ldr_unit.unit.release(bed_request2) # Release the ldr bed 205 | ldr_unit.num_exits += 1 206 | exit_ts = env.now 207 | ldr_unit.tot_occ_time += exit_ts - enter_ts 208 | if debug: 209 | print("{} leaving LDR at {}".format(name, env.now)) 210 | 211 | # PP stay 212 | if debug: 213 | print("{} entering PP at {}".format(name, env.now)) 214 | pp_unit.num_entries += 1 215 | enter_ts = env.now 216 | if debug: 217 | if env.now > bed_request_ts: 218 | print("{} waited {} time units for PP bed".format(name, env.now- bed_request_ts)) 219 | yield env.timeout(obp.planned_los_pp) # Stay in LDR bed 220 | pp_unit.unit.release(bed_request3) # Release the PP bed 221 | pp_unit.num_exits += 1 222 | exit_ts = env.now 223 | pp_unit.tot_occ_time += exit_ts - enter_ts 224 | 225 | if debug: 226 | print("{} leaving PP and system at {}".format(name, env.now)) 227 | 228 | 229 | 230 | # Initialize a simulation environment 231 | env = simpy.Environment() 232 | 233 | rho_obs = ARR_RATE * MEAN_LOS_OBS / CAPACITY_OBS 234 | rho_ldr = ARR_RATE * MEAN_LOS_LDR / CAPACITY_LDR 235 | rho_pp = ARR_RATE * MEAN_LOS_PP / CAPACITY_PP 236 | 237 | print("rho_obs: {:6.3f}, rho_ldr: {:6.3f}, rho_pp: {:6.3f}".format(rho_obs, rho_ldr, rho_pp)) 238 | 239 | 240 | # Declare a Resource to model OBS unit 241 | obs_unit = OBunit(env, "OBS", CAPACITY_OBS, debug=True) 242 | ldr_unit = OBunit(env, "LDR", CAPACITY_LDR, debug=True) 243 | pp_unit = OBunit(env, "PP", CAPACITY_PP, debug=True) 244 | 245 | # Run the simulation for a while 246 | runtime = 100000 247 | debug = False 248 | 249 | obpat_gen = OBPatientGenerator(env, "Type1", ARR_RATE, 0, runtime, debug=debug) 250 | env.run() 251 | 252 | print("\nNum patients generated: {}\n".format(obpat_gen.num_patients_created)) 253 | print(obs_unit.basic_stats_msg()) 254 | print(ldr_unit.basic_stats_msg()) 255 | print(pp_unit.basic_stats_msg()) 256 | 257 | 258 | -------------------------------------------------------------------------------- /obflow_5_oo_2.py: -------------------------------------------------------------------------------- 1 | import simpy 2 | from numpy.random import RandomState 3 | 4 | """ 5 | Simple OB patient flow model 5 - Very simple OO 6 | 7 | Details: 8 | 9 | - Generate arrivals via Poisson process 10 | - Define an OBUnit class that contains a simpy.Resource object as a member. 11 | Not subclassing Resource, just trying to use it as a member. 12 | - Routing is done via setting ``out`` member of an OBUnit instance to 13 | another OBUnit instance to which the OB patient flow instance should be 14 | routed. The routing logic, for now, is in OBUnit object. Later, 15 | we need some sort of router object and data driven routing. 16 | - Trying to get patient flow working without a process function that 17 | explicitly articulates the sequence of units and stays. 18 | 19 | Key Lessons Learned: 20 | 21 | - Any function that is a generator and might potentially yield for an event 22 | must get registered as a process. 23 | 24 | """ 25 | # Arrival rate and length of stay inputs. 26 | ARR_RATE = 0.4 27 | MEAN_LOS_OBS = 3 28 | MEAN_LOS_LDR = 12 29 | MEAN_LOS_PP = 48 30 | 31 | # Unit capacities 32 | CAPACITY_OBS = 2 33 | CAPACITY_LDR = 6 34 | CAPACITY_PP = 24 35 | 36 | # Random number seed 37 | RNG_SEED = 6353 38 | 39 | 40 | class OBunit(object): 41 | """ Models an OB unit with fixed capacity. 42 | 43 | Parameters 44 | ---------- 45 | env : simpy.Environment 46 | the simulation environment 47 | name : str 48 | unit name 49 | capacity : integer (or None) 50 | Number of beds. Use None for infinite capacity. 51 | 52 | """ 53 | 54 | def __init__(self, env, name, capacity=None, debug=False): 55 | if capacity is None: 56 | self.capacity = simpy.core.Infinity 57 | else: 58 | self.capacity = capacity 59 | 60 | self.env = env 61 | self.name = name 62 | self.debug = debug 63 | 64 | # Use a simpy Resource as one of the class members 65 | self.unit = simpy.Resource(env, capacity) 66 | 67 | # Statistical accumulators 68 | self.num_entries = 0 69 | self.num_exits = 0 70 | self.tot_occ_time = 0.0 71 | 72 | # The out member will get set to destination unit 73 | self.out = None 74 | 75 | def put(self, obp): 76 | """ A process method called when a bed is requested in the unit. 77 | 78 | The logic of this method is reminiscent of the routing logic 79 | in the process oriented obflow models 1-3. However, this method 80 | is used for all of the units - no repeated logic. 81 | 82 | Parameters 83 | ---------- 84 | env : simpy.Environment 85 | the simulation environment 86 | 87 | obp : OBPatient object 88 | the patient requestion the bed 89 | 90 | """ 91 | 92 | if self.debug: 93 | print("{} trying to get {} at {:.4f}".format(obp.name, 94 | self.name, self.env.now)) 95 | 96 | # Increments patient's attribute number of units visited 97 | obp.current_stay_num += 1 98 | # Timestamp of request time 99 | bed_request_ts = self.env.now 100 | # Request a bed 101 | bed_request = self.unit.request() 102 | # Store bed request and timestamp in patient's request lists 103 | obp.bed_requests[obp.current_stay_num] = bed_request 104 | obp.request_entry_ts[obp.current_stay_num] = self.env.now 105 | # Yield until we get a bed 106 | yield bed_request 107 | 108 | # Seized a bed. 109 | obp.entry_ts[obp.current_stay_num] = self.env.now 110 | 111 | # Check if we have a bed from a previous stay and release it. 112 | # Update stats for previous unit. 113 | 114 | if obp.bed_requests[obp.current_stay_num - 1] is not None: 115 | previous_request = obp.bed_requests[obp.current_stay_num - 1] 116 | previous_unit_name = \ 117 | obp.planned_route_stop[obp.current_stay_num - 1] 118 | previous_unit = obunits[previous_unit_name] 119 | previous_unit.unit.release(previous_request) 120 | previous_unit.num_exits += 1 121 | previous_unit.tot_occ_time += \ 122 | self.env.now - obp.entry_ts[obp.current_stay_num - 1] 123 | obp.exit_ts[obp.current_stay_num - 1] = self.env.now 124 | 125 | if self.debug: 126 | print("{} entering {} at {:.4f}".format(obp.name, self.name, 127 | self.env.now)) 128 | self.num_entries += 1 129 | if self.debug: 130 | if self.env.now > bed_request_ts: 131 | waittime = self.env.now - bed_request_ts 132 | print("{} waited {:.4f} time units for {} bed".format(obp.name, 133 | waittime, 134 | self.name)) 135 | 136 | # Determine los and then yield for the stay 137 | los = obp.planned_los[obp.current_stay_num] 138 | yield self.env.timeout(los) 139 | 140 | # Go to next destination (which could be an exitflow) 141 | next_unit_name = obp.planned_route_stop[obp.current_stay_num + 1] 142 | self.out = obunits[next_unit_name] 143 | if obp.current_stay_num == obp.route_length: 144 | # For ExitFlow object, no process needed 145 | self.out.put(obp) 146 | else: 147 | # Process for putting patient into next bed 148 | self.env.process(self.out.put(obp)) 149 | 150 | # def get(self, unit): 151 | # """ Release """ 152 | # return unit.get() 153 | 154 | def basic_stats_msg(self): 155 | """ Compute entries, exits, avg los and create summary message. 156 | 157 | 158 | Returns 159 | ------- 160 | str 161 | Message with basic stats 162 | """ 163 | 164 | if self.num_exits > 0: 165 | alos = self.tot_occ_time / self.num_exits 166 | else: 167 | alos = 0 168 | 169 | msg = "{:6}:\t Entries={}, Exits={}, Occ={}, ALOS={:4.2f}".\ 170 | format(self.name, self.num_entries, self.num_exits, 171 | self.unit.count, alos) 172 | return msg 173 | 174 | 175 | class ExitFlow(object): 176 | """ Patients routed here when ready to exit. 177 | 178 | Patient objects put into a Store. Can be accessed later for stats 179 | and logs. A little worried about how big the Store will get. 180 | 181 | Parameters 182 | ---------- 183 | env : simpy.Environment 184 | the simulation environment 185 | debug : boolean 186 | if true then patient details printed on arrival 187 | """ 188 | 189 | def __init__(self, env, name, store_obp=True, debug=False): 190 | self.store = simpy.Store(env) 191 | self.env = env 192 | self.name = name 193 | self.store_obp = store_obp 194 | self.debug = debug 195 | self.num_exits = 0 196 | self.last_exit = 0.0 197 | 198 | def put(self, obp): 199 | 200 | if obp.bed_requests[obp.current_stay_num] is not None: 201 | previous_request = obp.bed_requests[obp.current_stay_num] 202 | previous_unit_name = obp.planned_route_stop[obp.current_stay_num] 203 | previous_unit = obunits[previous_unit_name] 204 | previous_unit.unit.release(previous_request) 205 | previous_unit.num_exits += 1 206 | previous_unit.tot_occ_time += self.env.now - obp.entry_ts[ 207 | obp.current_stay_num] 208 | obp.exit_ts[obp.current_stay_num - 1] = self.env.now 209 | 210 | self.last_exit = self.env.now 211 | self.num_exits += 1 212 | 213 | if self.debug: 214 | print(obp) 215 | 216 | # Store patient 217 | if self.store_obp: 218 | self.store.put(obp) 219 | 220 | def basic_stats_msg(self): 221 | """ Create summary message with basic stats on exits. 222 | 223 | 224 | Returns 225 | ------- 226 | str 227 | Message with basic stats 228 | """ 229 | 230 | msg = "{:6}:\t Exits={}, Last Exit={:10.2f}".format(self.name, 231 | self.num_exits, 232 | self.last_exit) 233 | 234 | return msg 235 | 236 | 237 | class OBPatient(object): 238 | """ Models an OB patient 239 | 240 | Parameters 241 | ---------- 242 | arr_time : float 243 | Patient arrival time 244 | patient_id : int 245 | Unique patient id 246 | patient_type : int 247 | Patient type id (default 1). Currently just one patient type. 248 | In our prior research work we used a scheme with 11 patient types. 249 | arr_stream : int 250 | Arrival stream id (default 1). Currently there is just one arrival 251 | stream corresponding to the one patient generator class. In future, 252 | likely to be be multiple generators for generating random and 253 | scheduled arrivals. 254 | 255 | """ 256 | 257 | def __init__(self, arr_time, patient_id, patient_type=1, arr_stream=1, 258 | prng=RandomState(0)): 259 | self.arr_time = arr_time 260 | self.patient_id = patient_id 261 | self.patient_type = patient_type 262 | self.arr_stream = arr_stream 263 | 264 | self.name = 'Patient_{}'.format(patient_id) 265 | 266 | # Hard coding route, los and bed requests for now 267 | # Not sure how best to do routing related data structures. 268 | # Hack for now using combination of lists here, the out member 269 | # and the obunits dictionary. 270 | self.current_stay_num = 0 271 | self.route_length = 3 272 | 273 | self.planned_route_stop = [] 274 | self.planned_route_stop.append(None) 275 | self.planned_route_stop.append("OBS") 276 | self.planned_route_stop.append("LDR") 277 | self.planned_route_stop.append("PP") 278 | self.planned_route_stop.append("EXIT") 279 | 280 | self.planned_los = [] 281 | self.planned_los.append(None) 282 | self.planned_los.append(prng.exponential(MEAN_LOS_OBS)) 283 | self.planned_los.append(prng.exponential(MEAN_LOS_LDR)) 284 | self.planned_los.append(prng.exponential(MEAN_LOS_PP)) 285 | 286 | # Since we have fixed route for now, just initialize full list to 287 | # hold bed requests 288 | self.bed_requests = [None for _ in range(self.route_length + 1)] 289 | self.request_entry_ts = [None for _ in range(self.route_length + 1)] 290 | self.entry_ts = [None for _ in range(self.route_length + 1)] 291 | self.exit_ts = [None for _ in range(self.route_length + 1)] 292 | 293 | def __repr__(self): 294 | return "patientid: {}, arr_stream: {}, time: {}". \ 295 | format(self.patient_id, self.arr_stream, self.arr_time) 296 | 297 | 298 | class OBPatientGenerator(object): 299 | """ Generates patients. 300 | 301 | Set the "out" member variable to resource at which patient generated. 302 | 303 | Parameters 304 | ---------- 305 | env : simpy.Environment 306 | the simulation environment 307 | arr_rate : float 308 | Poisson arrival rate (expected number of arrivals per unit time) 309 | patient_type : int 310 | Patient type id (default 1). Currently just one patient type. 311 | In our prior research work we used a scheme with 11 patient types. 312 | arr_stream : int 313 | Arrival stream id (default 1). Currently there is just one arrival 314 | stream corresponding to the one patient generator class. In future, 315 | likely to be be multiple generators for generating random and 316 | scheduled arrivals 317 | initial_delay : float 318 | Starts generation after an initial delay. (default 0.0) 319 | stoptime : float 320 | Stops generation at the stoptime. (default Infinity) 321 | max_arrivals : int 322 | Stops generation after max_arrivals. (default Infinity) 323 | debug: bool 324 | If True, status message printed after 325 | each patient created. (default False) 326 | 327 | """ 328 | 329 | def __init__(self, env, arr_rate, patient_type=1, arr_stream=1, 330 | initial_delay=0, 331 | stoptime=simpy.core.Infinity, 332 | max_arrivals=simpy.core.Infinity, debug=False): 333 | 334 | self.env = env 335 | self.arr_rate = arr_rate 336 | self.patient_type = patient_type 337 | self.arr_stream = arr_stream 338 | self.initial_delay = initial_delay 339 | self.stoptime = stoptime 340 | self.max_arrivals = max_arrivals 341 | self.debug = debug 342 | self.out = None 343 | self.num_patients_created = 0 344 | 345 | self.prng = RandomState(RNG_SEED) 346 | 347 | self.action = env.process( 348 | self.run()) # starts the run() method as a SimPy process 349 | 350 | def run(self): 351 | """The patient generator. 352 | """ 353 | # Delay for initial_delay 354 | yield self.env.timeout(self.initial_delay) 355 | # Main generator loop that terminates when stoptime reached 356 | while self.env.now < self.stoptime and \ 357 | self.num_patients_created < self.max_arrivals: 358 | # Delay until time for next arrival 359 | # Compute next interarrival time 360 | iat = self.prng.exponential(1.0 / self.arr_rate) 361 | yield self.env.timeout(iat) 362 | self.num_patients_created += 1 363 | # Create new patient 364 | obp = OBPatient(self.env.now, self.num_patients_created, 365 | self.patient_type, self.arr_stream, 366 | prng=self.prng) 367 | 368 | if self.debug: 369 | print("Patient {} created at {:.2f}.".format( 370 | self.num_patients_created, self.env.now)) 371 | 372 | # Set out member to OBunit object representing next destination 373 | self.out = obunits[obp.planned_route_stop[1]] 374 | # Initiate process of requesting first bed in route 375 | self.env.process(self.out.put(obp)) 376 | 377 | 378 | # Initialize a simulation environment 379 | simenv = simpy.Environment() 380 | 381 | # Compute and display traffic intensities 382 | rho_obs = ARR_RATE * MEAN_LOS_OBS / CAPACITY_OBS 383 | rho_ldr = ARR_RATE * MEAN_LOS_LDR / CAPACITY_LDR 384 | rho_pp = ARR_RATE * MEAN_LOS_PP / CAPACITY_PP 385 | 386 | print("rho_obs: {:6.3f}\nrho_ldr: {:6.3f}\nrho_pp: {:6.3f}".format(rho_obs, 387 | rho_ldr, 388 | rho_pp)) 389 | 390 | # Create nursing units 391 | obs_unit = OBunit(simenv, 'OBS', CAPACITY_OBS, debug=False) 392 | ldr_unit = OBunit(simenv, 'LDR', CAPACITY_LDR, debug=True) 393 | pp_unit = OBunit(simenv, 'PP', CAPACITY_PP, debug=False) 394 | 395 | # Define system exit 396 | exitflow = ExitFlow(simenv, 'EXIT', store_obp=False) 397 | 398 | # Create dictionary of units keyed by name. This object can be passed along 399 | # to other objects so that the units are accessible as patients "flow". 400 | obunits = {'OBS': obs_unit, 'LDR': ldr_unit, 'PP': pp_unit, 'EXIT': exitflow} 401 | 402 | # Create a patient generator 403 | obpat_gen = OBPatientGenerator(simenv, ARR_RATE, debug=False) 404 | 405 | # Routing logic 406 | # Currently routing logic is hacked into the OBPatientGenerator 407 | # and OBPatient objects 408 | 409 | # Run the simulation for a while 410 | runtime = 1000 411 | simenv.run(until=runtime) 412 | 413 | # Patient generator stats 414 | print("\nNum patients generated: {}\n".format(obpat_gen.num_patients_created)) 415 | 416 | # Unit stats 417 | print(obs_unit.basic_stats_msg()) 418 | print(ldr_unit.basic_stats_msg()) 419 | print(pp_unit.basic_stats_msg()) 420 | 421 | # System exit stats 422 | print("\nNum patients exiting system: {}\n".format(exitflow.num_exits)) 423 | print("Last exit at: {:.2f}\n".format(exitflow.last_exit)) 424 | -------------------------------------------------------------------------------- /obflow_6.py: -------------------------------------------------------------------------------- 1 | from sys import stdout 2 | import logging 3 | from enum import IntEnum 4 | from copy import deepcopy 5 | from pathlib import Path 6 | import argparse 7 | 8 | import simpy 9 | from numpy.random import default_rng 10 | import networkx as nx 11 | import pandas as pd 12 | import yaml 13 | 14 | from obflow_6_output import compute_occ_stats 15 | 16 | """ 17 | Simple OB patient flow model 6 - Very simple OO 18 | 19 | Details: 20 | 21 | - Generate arrivals via Poisson process 22 | - Define an OBUnit class that contains a simpy.Resource object as a member. 23 | Not subclassing Resource, just trying to use it as a member. 24 | - Routing is done via setting ``out`` member of an OBUnit instance to 25 | another OBUnit instance to which the OB patient flow instance should be 26 | routed. The routing logic, for now, is in OBUnit object. Later, 27 | we need some sort of router object and data driven routing. 28 | - Trying to get patient flow working without a process function that 29 | explicitly articulates the sequence of units and stays. 30 | 31 | Planned enhancements from obflow_5: 32 | 33 | Goal is to be able to run new scenarios for the obsim experiments. Model 34 | needs to match Simio model functionality. 35 | 36 | - LOS distributions that match the obsim experiments 37 | - LOS adjustment in LDR based on wait time in OBS 38 | - logging 39 | - read key scenario inputs from a file 40 | 41 | Key Lessons Learned: 42 | 43 | - Any function that is a generator and might potentially yield for an event 44 | must get registered as a process. 45 | 46 | """ 47 | 48 | 49 | class OBsystem(object): 50 | def __init__(self, env, locations, global_vars): 51 | self.env = env 52 | 53 | # Create individual patient care units 54 | # enter = EnterFlow(self.env, 'ENTRY') 55 | # exit = ExitFlow(self.env, 'EXIT') 56 | # self.obunits = [enter] 57 | 58 | self.obunits = [] 59 | 60 | # Unit index in obunits list should correspond to Unit enum value 61 | for location in locations: 62 | self.obunits.append(OBunit(env, unit_id=location, name=locations[location]['name'], 63 | capacity=locations[location]['capacity'])) 64 | # self.obunits.append(exit) 65 | 66 | self.global_vars = global_vars 67 | 68 | # Create list to hold timestamps dictionaries (one per patient stop) 69 | self.patient_timestamps_list = [] 70 | 71 | # Create list to hold timestamps dictionaries (one per patient) 72 | self.stops_timestamps_list = [] 73 | 74 | 75 | class PatientType(IntEnum): 76 | REG_DELIVERY_UNSCHED = 1 77 | CSECT_DELIVERY_UNSCHED = 2 78 | 79 | 80 | class Unit(IntEnum): 81 | ENTRY = 0 82 | OBS = 1 83 | LDR = 2 84 | CSECT = 3 85 | PP = 4 86 | EXIT = 5 87 | 88 | 89 | class OBunit(object): 90 | """ Models an OB unit with fixed capacity. 91 | 92 | Parameters 93 | ---------- 94 | env : simpy.Environment 95 | the simulation environment 96 | name : str 97 | unit name 98 | capacity : integer (or None) 99 | Number of beds. Use None for infinite capacity. 100 | 101 | """ 102 | 103 | def __init__(self, env, unit_id, name, capacity=simpy.core.Infinity): 104 | 105 | self.env = env 106 | self.id = unit_id 107 | self.name = name 108 | self.capacity = capacity 109 | 110 | # Use a simpy Resource as one of the class members 111 | self.unit = simpy.Resource(env, capacity) 112 | 113 | # Statistical accumulators 114 | self.num_entries = 0 115 | self.num_exits = 0 116 | self.tot_occ_time = 0.0 117 | self.last_entry = None 118 | self.last_exit = None 119 | 120 | # Create list to hold occupancy tuples (time, occ) 121 | self.occupancy_list = [(0.0, 0.0)] 122 | 123 | def put(self, obpatient, obsystem): 124 | """ A process method called when a bed is requested in the unit. 125 | 126 | The logic of this method is reminiscent of the routing logic 127 | in the process oriented obflow models 1-3. However, this method 128 | is used for all of the units - no repeated logic. 129 | 130 | Parameters 131 | ---------- 132 | obpatient : OBPatient object 133 | the patient requesting the bed 134 | obsystem : OBSystem object 135 | 136 | """ 137 | 138 | obpatient.current_stop_num += 1 139 | logger.debug( 140 | f"{obpatient.name} trying to get {self.name} at {self.env.now:.4f} for stop_num {obpatient.current_stop_num}") 141 | 142 | # Timestamp of request time 143 | bed_request_ts = self.env.now 144 | # Request a bed 145 | bed_request = self.unit.request() 146 | # Store bed request and timestamp in patient's request lists 147 | obpatient.bed_requests[obpatient.current_stop_num] = bed_request 148 | obpatient.unit_stops[obpatient.current_stop_num] = self.id 149 | obpatient.request_entry_ts[obpatient.current_stop_num] = self.env.now 150 | 151 | # If we are coming from upstream unit, we are trying to exit that unit now 152 | if obpatient.bed_requests[obpatient.current_stop_num - 1] is not None: 153 | obpatient.request_exit_ts[obpatient.current_stop_num - 1] = self.env.now 154 | 155 | # Yield until we get a bed 156 | yield bed_request 157 | 158 | # Seized a bed. 159 | # Increments patient's attribute number of units visited (includes ENTRY and EXIT) 160 | 161 | obpatient.entry_ts[obpatient.current_stop_num] = self.env.now 162 | obpatient.wait_to_enter[obpatient.current_stop_num] = self.env.now - obpatient.request_entry_ts[ 163 | obpatient.current_stop_num] 164 | obpatient.current_unit_id = self.id 165 | 166 | self.num_entries += 1 167 | self.last_entry = self.env.now 168 | 169 | # Increment occupancy 170 | self.inc_occ() 171 | 172 | # Check if we have a bed from a previous stay and release it if we do. 173 | # Update stats for previous unit. 174 | 175 | if obpatient.bed_requests[obpatient.current_stop_num - 1] is not None: 176 | obpatient.exit_ts[obpatient.current_stop_num - 1] = self.env.now 177 | obpatient.wait_to_exit[obpatient.current_stop_num - 1] = \ 178 | self.env.now - obpatient.request_exit_ts[obpatient.current_stop_num - 1] 179 | obpatient.previous_unit_id = obpatient.unit_stops[obpatient.current_stop_num - 1] 180 | previous_unit = obsystem.obunits[obpatient.previous_unit_id] 181 | previous_request = obpatient.bed_requests[obpatient.current_stop_num - 1] 182 | previous_unit.unit.release(previous_request) 183 | previous_unit.tot_occ_time += \ 184 | self.env.now - obpatient.entry_ts[obpatient.current_stop_num - 1] 185 | previous_unit.num_exits += 1 186 | previous_unit.last_exit = self.env.now 187 | # Decrement occupancy 188 | previous_unit.dec_occ() 189 | 190 | logger.debug(f"{self.env.now:.4f}:{obpatient.name} entering {self.name} at {self.env.now:.4f}") 191 | logger.debug( 192 | f"{self.env.now:.4f}:{obpatient.name} waited {self.env.now - bed_request_ts:.4f} time units for {self.name} bed") 193 | 194 | # Retrieve los and then yield for the stay 195 | los = obpatient.route_graph.nodes(data=True)[obpatient.current_unit_id]['planned_los'] 196 | obpatient.planned_los[obpatient.current_stop_num] = los 197 | 198 | # Do any blocking related los adjustments 199 | if self.name == 'LDR': 200 | adj_los = max(0, los - obpatient.wait_to_exit[obpatient.current_stop_num - 1]) 201 | else: 202 | adj_los = los 203 | 204 | obpatient.adjusted_los[obpatient.current_stop_num] = adj_los 205 | 206 | # Wait for LOS to elapse 207 | yield self.env.timeout(adj_los) 208 | 209 | # Go to next destination (which could be an exitflow) 210 | if obpatient.current_unit_id == Unit.EXIT: 211 | obpatient.previous_unit_id = obpatient.unit_stops[obpatient.current_stop_num] 212 | previous_unit = obsystem.obunits[obpatient.previous_unit_id] 213 | previous_request = obpatient.bed_requests[obpatient.current_stop_num] 214 | previous_unit.unit.release(previous_request) 215 | previous_unit.tot_occ_time += \ 216 | self.env.now - obpatient.entry_ts[obpatient.current_stop_num] 217 | previous_unit.num_exits += 1 218 | previous_unit.last_exit = self.env.now 219 | # Decrement occupancy 220 | previous_unit.dec_occ() 221 | 222 | obpatient.request_exit_ts[obpatient.current_stop_num] = self.env.now 223 | obpatient.exit_ts[obpatient.current_stop_num] = self.env.now 224 | self.exit_system(obpatient, obsystem) 225 | else: 226 | obpatient.next_unit_id = obpatient.router.get_next_unit_id(obpatient) 227 | self.env.process(obsystem.obunits[obpatient.next_unit_id].put(obpatient, obsystem)) 228 | 229 | # EXIT is now an OBunit so following is deprecated 230 | # if obpatient.next_unit_id == Unit.EXIT: 231 | # # For ExitFlow object, no process needed 232 | # obsystem.obunits[obpatient.next_unit_id].put(obpatient, obsystem) 233 | # else: 234 | # # Process for putting patient into next bed 235 | # self.env.process(obsystem.obunits[obpatient.next_unit_id].put(obpatient, obsystem)) 236 | 237 | def inc_occ(self, increment=1): 238 | 239 | # Update occupancy - increment by 1 240 | prev_occ = self.occupancy_list[-1][1] 241 | new_occ = (self.env.now, prev_occ + increment) 242 | self.occupancy_list.append(new_occ) 243 | 244 | def dec_occ(self, decrement=1): 245 | 246 | # Update occupancy - increment by 1 247 | prev_occ = self.occupancy_list[-1][1] 248 | new_occ = (self.env.now, prev_occ - decrement) 249 | self.occupancy_list.append(new_occ) 250 | 251 | def exit_system(self, obpatient, obsystem): 252 | 253 | logger.debug(f"{self.env.now:.4f}:Patient {obpatient.name} exited system at {self.env.now:.2f}.") 254 | 255 | # Create dictionaries of timestamps for patient_stop log 256 | for stop in range(len(obpatient.unit_stops)): 257 | if obpatient.unit_stops[stop] is not None: 258 | timestamps = {'patient_id': obpatient.patient_id, 259 | 'patient_type': obpatient.patient_type.value, 260 | 'unit': Unit(obpatient.unit_stops[stop]).name, 261 | 'request_entry_ts': obpatient.request_entry_ts[stop], 262 | 'entry_ts': obpatient.entry_ts[stop], 263 | 'request_exit_ts': obpatient.request_exit_ts[stop], 264 | 'exit_ts': obpatient.exit_ts[stop], 265 | 'planned_los': obpatient.planned_los[stop], 266 | 'adjusted_los': obpatient.adjusted_los[stop], 267 | 'entry_tryentry': obpatient.entry_ts[stop] - obpatient.request_entry_ts[stop], 268 | 'tryexit_entry': obpatient.request_exit_ts[stop] - obpatient.entry_ts[stop], 269 | 'exit_tryexit': obpatient.exit_ts[stop] - obpatient.request_exit_ts[stop], 270 | 'exit_enter': obpatient.exit_ts[stop] - obpatient.entry_ts[stop], 271 | 'exit_tryenter': obpatient.exit_ts[stop] - obpatient.request_entry_ts[stop], 272 | 'wait_to_enter': obpatient.wait_to_enter[stop], 273 | 'wait_to_exit': obpatient.wait_to_exit[stop], 274 | 'bwaited_to_enter': obpatient.entry_ts[stop] > obpatient.request_entry_ts[stop], 275 | 'bwaited_to_exit': obpatient.exit_ts[stop] > obpatient.request_exit_ts[stop]} 276 | 277 | obsystem.stops_timestamps_list.append(timestamps) 278 | 279 | def basic_stats_msg(self): 280 | """ Compute entries, exits, avg los and create summary message. 281 | 282 | 283 | Returns 284 | ------- 285 | str 286 | Message with basic stats 287 | """ 288 | 289 | if self.num_exits > 0: 290 | alos = self.tot_occ_time / self.num_exits 291 | else: 292 | alos = 0 293 | 294 | msg = "{:6}:\t Entries={}, Exits={}, Occ={}, ALOS={:4.2f}". \ 295 | format(self.name, self.num_entries, self.num_exits, 296 | self.unit.count, alos) 297 | return msg 298 | 299 | 300 | class OBPatient(object): 301 | """ 302 | 303 | """ 304 | 305 | def __init__(self, obsystem, router, arr_time, patient_id, arr_stream_rg): 306 | """ 307 | 308 | Parameters 309 | ---------- 310 | obsystem 311 | router 312 | arr_time 313 | patient_id 314 | arr_stream_rg 315 | """ 316 | self.system_arrival_ts = arr_time 317 | self.patient_id = patient_id 318 | self.router = router 319 | 320 | # Determine patient type 321 | if arr_stream_rg.random() > obsystem.global_vars['c_sect_prob']: 322 | self.patient_type = PatientType.REG_DELIVERY_UNSCHED 323 | else: 324 | self.patient_type = PatientType.CSECT_DELIVERY_UNSCHED 325 | 326 | self.name = f'Patient_i{patient_id}_t{self.patient_type}' 327 | 328 | self.current_stop_num = -1 329 | self.previous_unit_id = None 330 | self.current_unit_id = None 331 | self.next_unit_id = None 332 | 333 | self.route_graph = router.create_route(self.patient_type) 334 | self.route_length = len(self.route_graph.edges) + 1 # Includes ENTRY and EXIT 335 | 336 | # Since we have fixed route, just initialize full list to hold bed requests 337 | # The index numbers are stop numbers and so slot 0 is for ENTRY location 338 | self.bed_requests = [None for _ in range(self.route_length)] 339 | self.unit_stops = [None for _ in range(self.route_length)] 340 | self.planned_los = [None for _ in range(self.route_length)] 341 | self.adjusted_los = [None for _ in range(self.route_length)] 342 | self.request_entry_ts = [None for _ in range(self.route_length)] 343 | self.entry_ts = [None for _ in range(self.route_length)] 344 | 345 | self.wait_to_enter = [None for _ in range(self.route_length)] 346 | 347 | self.request_exit_ts = [None for _ in range(self.route_length)] 348 | self.exit_ts = [None for _ in range(self.route_length)] 349 | 350 | self.wait_to_exit = [None for _ in range(self.route_length)] 351 | 352 | self.system_exit_ts = None 353 | 354 | def __repr__(self): 355 | return "patientid: {}, patient_type: {}, time: {}". \ 356 | format(self.patient_id, self.patient_type, self.system_arrival_ts) 357 | 358 | 359 | class OBStaticRouter(object): 360 | def __init__(self, env, obsystem, locations, routes, rg): 361 | """ 362 | 363 | Parameters 364 | ---------- 365 | env 366 | obsystem 367 | routes 368 | rg 369 | """ 370 | 371 | self.env = env 372 | self.obsystem = obsystem 373 | self.rg = rg 374 | 375 | # List of networkx DiGraph objects. Padded with None at 0 index to align with patient type ints 376 | self.route_graphs = {} 377 | 378 | # Create route templates from routes list (of unit numbers) 379 | for route_num, route in routes.items(): 380 | route_graph = nx.DiGraph() 381 | 382 | # Add each unit number as a node 383 | for loc_num, location in locations.items(): 384 | route_graph.add_node(location['id'], id=location['id'], 385 | planned_los=0.0, actual_los=0.0, blocked_duration=0.0, 386 | name=location['name']) 387 | 388 | # Add edges - simple serial route in this case 389 | for edge in route['edges']: 390 | route_graph.add_edge(edge['from'], edge['to']) 391 | 392 | # Each patient will eventually end up with their own copy of the route since it contains LOS values 393 | self.route_graphs[route_num] = route_graph.copy() 394 | logger.debug(f"{self.env.now:.4f}:route graph {route_num} - {route_graph.edges}") 395 | 396 | def create_route(self, patient_type): 397 | """ 398 | 399 | Parameters 400 | ---------- 401 | patient_type 402 | 403 | Returns 404 | ------- 405 | 406 | """ 407 | 408 | # Copy the route template to create new graph object 409 | route_graph = deepcopy(self.route_graphs[patient_type]) 410 | 411 | # Pull out the LOS parameters for convenience 412 | k_obs = self.obsystem.global_vars['num_erlang_stages_obs'] 413 | mean_los_obs = self.obsystem.global_vars['mean_los_obs'] 414 | k_ldr = self.obsystem.global_vars['num_erlang_stages_ldr'] 415 | mean_los_ldr = self.obsystem.global_vars['mean_los_ldr'] 416 | k_pp = self.obsystem.global_vars['num_erlang_stages_pp'] 417 | mean_los_pp_noc = self.obsystem.global_vars['mean_los_pp_noc'] 418 | mean_los_pp_c = self.obsystem.global_vars['mean_los_pp_c'] 419 | 420 | # Generate the random planned LOS values by patient type 421 | if patient_type == PatientType.REG_DELIVERY_UNSCHED: 422 | route_graph.nodes[Unit.OBS]['planned_los'] = self.rg.gamma(k_obs, mean_los_obs / k_obs) 423 | route_graph.nodes[Unit.LDR]['planned_los'] = self.rg.gamma(k_ldr, mean_los_ldr / k_ldr) 424 | route_graph.nodes[Unit.PP]['planned_los'] = self.rg.gamma(k_pp, mean_los_pp_noc / k_pp) 425 | 426 | elif patient_type == PatientType.CSECT_DELIVERY_UNSCHED: 427 | k_csect = self.obsystem.global_vars['num_erlang_stages_csect'] 428 | mean_los_csect = self.obsystem.global_vars['mean_los_csect'] 429 | 430 | route_graph.nodes[Unit.OBS]['planned_los'] = self.rg.gamma(k_obs, mean_los_obs / k_obs) 431 | route_graph.nodes[Unit.LDR]['planned_los'] = self.rg.gamma(k_ldr, mean_los_ldr / k_ldr) 432 | route_graph.nodes[Unit.CSECT]['planned_los'] = self.rg.gamma(k_csect, mean_los_csect / k_csect) 433 | route_graph.nodes[Unit.PP]['planned_los'] = self.rg.gamma(k_pp, mean_los_pp_c / k_pp) 434 | 435 | return route_graph 436 | 437 | def get_next_unit_id(self, obpatient): 438 | 439 | G = obpatient.route_graph 440 | successors = [G.nodes(data='id')[n] for n in G.successors(obpatient.current_unit_id)] 441 | next_unit_id = successors[0] 442 | 443 | if next_unit_id is None: 444 | logger.error(f"{self.env.now:.4f}:{obpatient.name} has no next unit at {obpatient.current_unit_id}.") 445 | exit(1) 446 | 447 | logger.debug( 448 | f"{self.env.now:.4f}:{obpatient.name} current_unit_id {obpatient.current_unit_id}, next_unit_id {next_unit_id}") 449 | return next_unit_id 450 | 451 | 452 | class OBPatientGenerator(object): 453 | """ Generates patients. 454 | 455 | Parameters 456 | ---------- 457 | env : simpy.Environment 458 | the simulation environment 459 | obsystem : OBSystem 460 | the OB system containing the obunits list 461 | router : OBStaticRouter like 462 | used to route new arrival to first location 463 | arr_rate : float 464 | Poisson arrival rate (expected number of arrivals per unit time) 465 | arr_stream_rg : numpy.random.Generator 466 | used for interarrival time generation 467 | initial_delay : float 468 | Starts generation after an initial delay. (default 0.0) 469 | stoptime : float 470 | Stops generation at the stoptime. (default Infinity) 471 | max_arrivals : int 472 | Stops generation after max_arrivals. (default Infinity) 473 | 474 | """ 475 | 476 | def __init__(self, env, obsystem, router, arr_rate, arr_stream_rg, 477 | initial_delay=0, stoptime=simpy.core.Infinity, max_arrivals=simpy.core.Infinity): 478 | self.env = env 479 | self.obsystem = obsystem 480 | self.router = router 481 | self.arr_rate = arr_rate 482 | self.arr_stream_rg = arr_stream_rg 483 | self.initial_delay = initial_delay 484 | self.stoptime = stoptime 485 | self.max_arrivals = max_arrivals 486 | 487 | self.out = None 488 | self.num_patients_created = 0 489 | 490 | # Register the run() method as a SimPy process 491 | env.process(self.run()) 492 | 493 | def run(self): 494 | """The patient generator. 495 | """ 496 | 497 | # Delay for initial_delay 498 | yield self.env.timeout(self.initial_delay) 499 | # Main generator loop that terminates when stoptime reached 500 | while self.env.now < self.stoptime and \ 501 | self.num_patients_created < self.max_arrivals: 502 | # Compute next interarrival time 503 | iat = self.arr_stream_rg.exponential(1.0 / self.arr_rate) 504 | # Delay until time for next arrival 505 | yield self.env.timeout(iat) 506 | self.num_patients_created += 1 507 | # Create new patient 508 | obpatient = OBPatient(self.obsystem, self.router, self.env.now, 509 | self.num_patients_created, self.arr_stream_rg) 510 | 511 | logger.debug(f"{self.env.now:.4f}:Patient {obpatient.name} created at {self.env.now:.4f}.") 512 | 513 | # Initiate process of patient entering system 514 | self.env.process(self.obsystem.obunits[Unit.ENTRY].put(obpatient, self.obsystem)) 515 | 516 | 517 | def process_command_line(): 518 | """ 519 | Parse command line arguments 520 | 521 | `argv` is a list of arguments, or `None` for ``sys.argv[1:]``. 522 | Return a Namespace representing the argument list. 523 | """ 524 | 525 | # Create the parser 526 | parser = argparse.ArgumentParser(prog='obflow_6', 527 | description='Run inpatient OB simulation') 528 | 529 | # Add arguments 530 | parser.add_argument( 531 | "config", type=str, 532 | help="Configuration file containing input parameter arguments and values" 533 | ) 534 | 535 | parser.add_argument("--loglevel", default='WARNING', 536 | help="Use valid values for logging package") 537 | 538 | # do the parsing 539 | args = parser.parse_args() 540 | 541 | # Read inputs from config file 542 | with open(args.config, 'rt') as yaml_file: 543 | yaml_config = yaml.safe_load(yaml_file) 544 | 545 | return yaml_config, args.loglevel 546 | 547 | 548 | def write_stop_log(csv_path, obsystem, egress=False): 549 | timestamp_df = pd.DataFrame(obsystem.stops_timestamps_list) 550 | if egress: 551 | timestamp_df.to_csv(csv_path, index=False) 552 | else: 553 | timestamp_df[(timestamp_df['unit'] != 'ENTRY') & 554 | (timestamp_df['unit'] != 'EXIT')].to_csv(csv_path, index=False) 555 | 556 | if egress: 557 | timestamp_df.to_csv(csv_path, index=False) 558 | else: 559 | timestamp_df[(timestamp_df['unit'] != 'ENTRY') & 560 | (timestamp_df['unit'] != 'EXIT')].to_csv(csv_path, index=False) 561 | 562 | 563 | def output_header(msg, linelen, scenario, rep_num): 564 | header = f"\n{msg} (scenario={scenario} rep={rep_num})\n{'-' * linelen}\n" 565 | return header 566 | 567 | 568 | def simulate(sim_inputs, rep_num): 569 | """ 570 | 571 | Parameters 572 | ---------- 573 | sim_inputs : dict whose keys are the simulation input args 574 | rep_num : int, simulation replication number 575 | 576 | Returns 577 | ------- 578 | Nothing returned but numerous output files written to ``args_dict[output_path]`` 579 | 580 | """ 581 | 582 | scenario = sim_inputs['scenario'] 583 | 584 | run_settings = sim_inputs['run_settings'] 585 | run_time = run_settings['run_time'] 586 | warmup_time = run_settings['warmup_time'] 587 | global_vars = sim_inputs['global_vars'] 588 | paths = sim_inputs['paths'] 589 | random_number_streams = sim_inputs['random_number_streams'] 590 | locations = sim_inputs['locations'] 591 | routes = sim_inputs['routes'] 592 | 593 | stop_log_path = Path(paths['stop_logs']) / f"unit_stop_log_scenario_{scenario}_rep_{rep_num}.csv" 594 | occ_log_path = Path(paths['occ_logs']) / f"unit_occ_log_scenario_{scenario}_rep_{rep_num}.csv" 595 | occ_stats_path = Path(paths['occ_stats']) / f"unit_occ_stats_scenario_{scenario}_rep_{rep_num}.csv" 596 | 597 | # Initialize a simulation environment 598 | env = simpy.Environment() 599 | 600 | # Create an OB System 601 | obsystem = OBsystem(env, locations, global_vars) 602 | 603 | # Create random number generators 604 | rg = {} 605 | for stream, seed in random_number_streams.items(): 606 | rg[stream] = default_rng(seed + rep_num - 1) 607 | 608 | # Create router 609 | router = OBStaticRouter(env, obsystem, locations, routes, rg['los']) 610 | 611 | # Create patient generator 612 | obpat_gen = OBPatientGenerator(env, obsystem, router, global_vars['arrival_rate'], 613 | rg['arrivals'], max_arrivals=1000000) 614 | 615 | # Run the simulation replication 616 | env.run(until=run_time) 617 | 618 | # Compute and display traffic intensities 619 | header = output_header("Input traffic parameters", 50, scenario, rep_num) 620 | print(header) 621 | 622 | rho_obs = global_vars['arrival_rate'] * global_vars['mean_los_obs'] / locations[Unit.OBS]['capacity'] 623 | rho_ldr = global_vars['arrival_rate'] * global_vars['mean_los_ldr'] / locations[Unit.LDR]['capacity'] 624 | mean_los_pp = global_vars['mean_los_pp_c'] * global_vars['c_sect_prob'] + \ 625 | global_vars['mean_los_pp_noc'] * (1 - global_vars['c_sect_prob']) 626 | 627 | rho_pp = global_vars['arrival_rate'] * mean_los_pp / locations[Unit.PP]['capacity'] 628 | 629 | print(f"rho_obs: {rho_obs:6.3f}\nrho_ldr: {rho_ldr:6.3f}\nrho_pp: {rho_pp:6.3f}") 630 | 631 | # Patient generator stats 632 | header = output_header("Patient generator and entry/exit stats", 50, scenario, rep_num) 633 | print(header) 634 | print("Num patients generated: {}\n".format(obpat_gen.num_patients_created)) 635 | 636 | # Unit stats 637 | for unit in obsystem.obunits[1:-1]: 638 | print(unit.basic_stats_msg()) 639 | 640 | # System exit stats 641 | print("\nNum patients exiting system: {}".format(obsystem.obunits[Unit.EXIT].num_exits)) 642 | print("Last exit at: {:.2f}\n".format(obsystem.obunits[Unit.EXIT].last_exit)) 643 | 644 | # Create output files 645 | write_stop_log(stop_log_path, obsystem) 646 | 647 | occ_stats_df = compute_occ_stats(obsystem, run_time, 648 | log_path=occ_log_path, 649 | warmup=warmup_time, quantiles=[0.05, 0.25, 0.5, 0.75, 0.95, 0.99]) 650 | 651 | occ_stats_df.to_csv(occ_stats_path, index=False) 652 | header = output_header("Occupancy stats", 50, scenario, rep_num) 653 | print(header) 654 | print(occ_stats_df) 655 | 656 | header = output_header("Output logs", 50, scenario, rep_num) 657 | print(header) 658 | print(f"Stop log written to {stop_log_path}") 659 | print(f"Occupancy log written to {occ_log_path}") 660 | print(f"Occupancy stats written to {occ_stats_path}") 661 | 662 | 663 | if __name__ == '__main__': 664 | 665 | # Main program 666 | 667 | config, loglevel = process_command_line() 668 | 669 | logging.basicConfig( 670 | level=loglevel, 671 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 672 | stream=stdout, 673 | ) 674 | 675 | logger = logging.getLogger(__name__) 676 | 677 | num_replications = config['run_settings']['num_replications'] 678 | # run_time = config['run_settings']['run_time'] 679 | # warmup_time = config['run_settings']['warmup_time'] 680 | # paths = config['paths'] 681 | # 682 | # stop_log_path = Path(paths['stop_logs']) 683 | # occ_log_path = Path(paths['occ_logs']) 684 | # occ_stats_path = Path(paths['occ_stats']) 685 | # output_path = Path(paths['output']) 686 | 687 | # Main simulation replication loop 688 | for i in range(1, num_replications + 1): 689 | simulate(config, i) 690 | 691 | # process_obsim_logs(stop_log_path, occ_stats_path, output_path, warmup=warmup_time, run_time=run_time) 692 | 693 | # Consolidate the patient logs and compute summary stats 694 | # patient_log_stats = process_sim_output(output_dir, scenario) 695 | # print(f"\nScenario: {scenario}") 696 | # pd.set_option("display.precision", 3) 697 | # pd.set_option('display.max_columns', None) 698 | # pd.set_option('display.width', 120) 699 | # print(patient_log_stats['patient_log_rep_stats']) 700 | # print(patient_log_stats['patient_log_ci']) 701 | -------------------------------------------------------------------------------- /obflow_6_output.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | from datetime import datetime 4 | from pathlib import Path 5 | import math 6 | 7 | import numpy as np 8 | import pandas as pd 9 | from statsmodels.stats.weightstats import DescrStatsW 10 | from scipy.stats import t 11 | 12 | from obnetwork import obnetwork 13 | 14 | 15 | def num_gt_0(column): 16 | return (column != 0).sum() 17 | 18 | 19 | def get_stats(group, stub=''): 20 | if group.sum() == 0: 21 | return {stub + 'count': group.count(), stub + 'mean': 0.0, 22 | stub + 'min': 0.0, stub + 'num_gt_0': 0, 23 | stub + 'max': 0.0, 'stdev': 0.0, 'sem': 0.0, 24 | stub + 'var': 0.0, 'cv': 0.0, 25 | stub + 'skew': 0.0, 'kurt': 0.0, 26 | stub + 'p01': 0.0, stub + 'p025': 0.0, 27 | stub + 'p05': 0.0, stub + 'p25': 0.0, 28 | stub + 'p50': 0.0, stub + 'p75': 0.0, 29 | stub + 'p90': 0.0, stub + 'p95': 0.0, 30 | stub + 'p975': 0.0, stub + 'p99': 0.0} 31 | else: 32 | return {stub + 'count': group.count(), stub + 'mean': group.mean(), 33 | stub + 'min': group.min(), stub + 'num_gt_0': num_gt_0(group), 34 | stub + 'max': group.max(), 'stdev': group.std(), 'sem': group.sem(), 35 | stub + 'var': group.var(), 'cv': group.std() / group.mean(), 36 | stub + 'skew': group.skew(), 'kurt': group.kurt(), 37 | stub + 'p01': group.quantile(0.01), stub + 'p025': group.quantile(0.025), 38 | stub + 'p05': group.quantile(0.05), stub + 'p25': group.quantile(0.25), 39 | stub + 'p50': group.quantile(0.5), stub + 'p75': group.quantile(0.75), 40 | stub + 'p90': group.quantile(0.9), stub + 'p95': group.quantile(0.95), 41 | stub + 'p975': group.quantile(0.975), stub + 'p99': group.quantile(0.99)} 42 | 43 | 44 | def process_obsim_logs(stop_log_path, occ_stats_path, output_path, 45 | run_time, units=('OBS', 'LDR', 'CSECT', 'PP'), 46 | warmup=0, output_file_stem='scenario_rep_stats_summary'): 47 | """ 48 | 49 | Parameters 50 | ---------- 51 | stop_log_path : Path, directory containing stop logs from obflow simulation model 52 | occ_stats_path : Path, directory containing occupancy stats from obflow simulation model 53 | output_path : Path, destination for the output summary files 54 | run_time : float, specified run time for simulation model 55 | units : tuple of str, unit names for which to compute stats 56 | warmup : float, time before which data is discarded for computing summary stats 57 | output_file_stem : str, csv output filename without the extension 58 | 59 | Returns 60 | ------- 61 | No return value; writes out summary by scenario and replication to scv 62 | 63 | """ 64 | 65 | start_analysis = warmup 66 | end_analysis = run_time 67 | num_days = (run_time - warmup) / 24.0 68 | 69 | rx = re.compile(r'_scenario_([0-9]{1,4})_rep_([0-9]{1,4})') 70 | 71 | results = [] 72 | active_units = [] 73 | 74 | for log_fn in stop_log_path.glob('unit_stop_log*.csv'): 75 | # Get scenario and rep numbers from filename 76 | m = re.search(rx, str(log_fn)) 77 | scenario_name = m.group(0) 78 | scenario_num = int(m.group(1)) 79 | rep_num = int(m.group(2)) 80 | print(scenario_name, scenario_num, rep_num) 81 | 82 | # Read the log file and filter by included categories 83 | stops_df = pd.read_csv(log_fn) 84 | 85 | stops_df = stops_df[(stops_df['entry_ts'] <= end_analysis) & (stops_df['exit_ts'] >= start_analysis) & \ 86 | (stops_df['entry_ts'] < stops_df['exit_ts'])] 87 | 88 | # LOS means and sds - planned and actual 89 | stops_df_grp_unit = stops_df.groupby(['unit']) 90 | plos_mean = stops_df_grp_unit['planned_los'].mean() 91 | plos_sd = stops_df_grp_unit['planned_los'].std() 92 | plos_skew = stops_df_grp_unit['planned_los'].skew() 93 | plos_kurt = stops_df_grp_unit['planned_los'].apply(pd.DataFrame.kurt) 94 | 95 | actlos_mean = stops_df_grp_unit['exit_enter'].mean() 96 | actlos_sd = stops_df_grp_unit['exit_enter'].std() 97 | actlos_skew = stops_df_grp_unit['exit_enter'].skew() 98 | actlos_kurt = stops_df_grp_unit['exit_enter'].apply(pd.DataFrame.kurt) 99 | 100 | grp_all = stops_df.groupby(['unit']) 101 | grp_blocked = stops_df[(stops_df['entry_tryentry'] > 0)].groupby(['unit']) 102 | 103 | blocked_uncond_stats = grp_all['entry_tryentry'].apply(get_stats, 'delay_') 104 | blocked_cond_stats = grp_blocked['entry_tryentry'].apply(get_stats, 'delay_') 105 | 106 | # Create new summary record as dict 107 | newrec = {'scenario': scenario_num} 108 | 109 | newrec['rep'] = rep_num 110 | newrec['num_days'] = num_days 111 | 112 | # Number of visits to each unit 113 | for unit in units: 114 | if (unit, 'delay_count') in blocked_uncond_stats.index: 115 | newrec[f'num_visits_{unit.lower()}'] = blocked_uncond_stats[(unit, 'delay_count')] 116 | else: 117 | newrec[f'num_visits_{unit.lower()}'] = 0 118 | 119 | #newrec[f'num_visits_{unit.lower()}'] = stops_df_grp_unit['exit_ts'].count()[unit] 120 | 121 | # LOS stats for each unit 122 | for unit in units: 123 | if newrec[f'num_visits_{unit.lower()}'] > 0: 124 | active_units.append(unit) 125 | 126 | newrec[f'planned_los_mean_{unit.lower()}'] = plos_mean[unit] 127 | newrec[f'actual_los_mean_{unit.lower()}'] = actlos_mean[unit] 128 | newrec[f'planned_los_sd_{unit.lower()}'] = plos_sd[unit] 129 | newrec[f'actual_los_sd_{unit.lower()}'] = actlos_sd[unit] 130 | 131 | newrec[f'planned_los_cv2_{unit.lower()}'] = (plos_sd[unit] / plos_mean[unit]) ** 2 132 | newrec[f'actual_los_cv2_{unit.lower()}'] = (actlos_sd[unit] / actlos_mean[unit]) ** 2 133 | 134 | newrec[f'planned_los_skew_{unit.lower()}'] = plos_skew[unit] 135 | newrec[f'actual_los_skew_{unit.lower()}'] = actlos_skew[unit] 136 | newrec[f'planned_los_kurt_{unit.lower()}'] = plos_kurt[unit] 137 | newrec[f'actual_los_kurt_{unit.lower()}'] = actlos_kurt[unit] 138 | 139 | # Interarrival time stats for each unit 140 | for unit in units: 141 | if newrec[f'num_visits_{unit.lower()}'] > 0: 142 | arrtimes_unit = stops_df.loc[stops_df.unit == unit, 'request_entry_ts'] 143 | # Make sure arrival times are sorted to compute interarrival times 144 | arrtimes_unit.sort_values(inplace=True) 145 | iatimes_unit = arrtimes_unit.diff(1)[1:] 146 | 147 | newrec[f'iatime_mean_{unit.lower()}'] = iatimes_unit.mean() 148 | newrec[f'iatime_sd_{unit.lower()}'] = iatimes_unit.std() 149 | newrec[f'iatime_skew_{unit.lower()}'] = iatimes_unit.skew() 150 | newrec[f'iatime_kurt_{unit.lower()}'] = iatimes_unit.kurtosis() 151 | 152 | # Get occ from occ stats summaries 153 | occ_stats_fn = Path(occ_stats_path) / f"unit_occ_stats_scenario_{scenario_num}_rep_{rep_num}.csv" 154 | occ_stats_df = pd.read_csv(occ_stats_fn, index_col=0) 155 | for unit in units: 156 | if newrec[f'num_visits_{unit.lower()}'] > 0: 157 | newrec[f'occ_mean_{unit.lower()}'] = occ_stats_df.loc[unit]['mean_occ'] 158 | newrec[f'occ_stdev_{unit.lower()}'] = occ_stats_df.loc[unit]['sd_occ'] 159 | newrec[f'occ_p05_{unit.lower()}'] = occ_stats_df.loc[unit]['p05_occ'] 160 | newrec[f'occ_p25_{unit.lower()}'] = occ_stats_df.loc[unit]['p25_occ'] 161 | newrec[f'occ_p50_{unit.lower()}'] = occ_stats_df.loc[unit]['p50_occ'] 162 | newrec[f'occ_p75_{unit.lower()}'] = occ_stats_df.loc[unit]['p75_occ'] 163 | newrec[f'occ_p95_{unit.lower()}'] = occ_stats_df.loc[unit]['p95_occ'] 164 | newrec[f'occ_p99_{unit.lower()}'] = occ_stats_df.loc[unit]['p99_occ'] 165 | newrec[f'occ_min_{unit.lower()}'] = occ_stats_df.loc[unit]['min_occ'] 166 | newrec[f'occ_max_{unit.lower()}'] = occ_stats_df.loc[unit]['max_occ'] 167 | 168 | newrec['prob_blockedby_ldr'] = \ 169 | blocked_uncond_stats[('LDR', 'delay_num_gt_0')] / blocked_uncond_stats[('LDR', 'delay_count')] 170 | 171 | if ('LDR', 'delay_mean') in blocked_cond_stats.index: 172 | newrec['blockedby_ldr_mean'] = blocked_cond_stats[('LDR', 'delay_mean')] 173 | newrec['blockedby_ldr_p95'] = blocked_cond_stats[('LDR', 'delay_p95')] 174 | else: 175 | newrec['blockedby_ldr_mean'] = 0.0 176 | newrec['blockedby_ldr_p95'] = 0.0 177 | 178 | newrec['pct_blocked_by_pp'] = \ 179 | blocked_uncond_stats[('PP', 'delay_num_gt_0')] / blocked_uncond_stats[('PP', 'delay_count')] 180 | 181 | if ('PP', 'delay_mean') in blocked_cond_stats.index: 182 | newrec['blockedby_pp_mean'] = blocked_cond_stats[('PP', 'delay_mean')] 183 | newrec['blockedby_pp_p95'] = blocked_cond_stats[('PP', 'delay_p95')] 184 | else: 185 | newrec['blockedby_pp_mean'] = 0.0 186 | newrec['blockedby_pp_p95'] = 0.0 187 | 188 | newrec['timestamp'] = str(datetime.now()) 189 | 190 | print(newrec) 191 | 192 | results.append(newrec) 193 | 194 | json_stats_path = output_path / 'json' 195 | json_stats_path.mkdir(exist_ok=True) 196 | 197 | output_json_file = json_stats_path / f'output_stats_scenario_{scenario_num}_rep_{rep_num}.json' 198 | with open(output_json_file, 'w') as json_output: 199 | json_output.write(str(newrec) + '\n') 200 | 201 | results_df = pd.DataFrame(results) 202 | # cols = ["scenario", "rep", "timestamp", "num_days", 203 | # "num_visits_obs", "num_visits_ldr", "num_visits_pp", "num_visits_csect", 204 | # "planned_los_mean_obs", "actual_los_mean_obs", "planned_los_sd_obs", "actual_los_sd_obs", 205 | # "planned_los_cv2_obs", "actual_los_cv2_obs", 206 | # "planned_los_skew_obs", "actual_los_skew_obs", "planned_los_kurt_obs", "actual_los_kurt_obs", 207 | # "planned_los_mean_ldr", "actual_los_mean_ldr", "planned_los_sd_ldr", "actual_los_sd_ldr", 208 | # "planned_los_cv2_ldr", "actual_los_cv2_ldr", 209 | # "planned_los_skew_ldr", "actual_los_skew_ldr", "planned_los_kurt_ldr", "actual_los_kurt_ldr", 210 | # "planned_los_mean_pp", "actual_los_mean_pp", "planned_los_sd_pp", "actual_los_sd_pp", 211 | # "planned_los_cv2_pp", "actual_los_cv2_pp", 212 | # "planned_los_skew_pp", "actual_los_skew_pp", "planned_los_kurt_pp", "actual_los_kurt_pp", 213 | # "planned_los_mean_csect", "actual_los_mean_csect", "planned_los_sd_csect", "actual_los_sd_csect", 214 | # "planned_los_cv2_csect", "actual_los_cv2_csect", 215 | # "planned_los_skew_csect", "actual_los_skew_csect", "planned_los_kurt_csect", "actual_los_kurt_csect", 216 | # "iatime_mean_obs", "iatime_sd_obs", "iatime_skew_obs", "iatime_kurt_obs", 217 | # "iatime_mean_ldr", "iatime_sd_ldr", "iatime_skew_ldr", "iatime_kurt_ldr", 218 | # "iatime_mean_csection", "iatime_sd_csection", "iatime_skew_csection", "iatime_kurt_csection", 219 | # "iatime_mean_pp", "iatime_sd_pp", "iatime_skew_pp", "iatime_kurt_pp", 220 | # "occ_mean_obs", "occ_mean_ldr", "occ_mean_csect", "occ_mean_pp", 221 | # "occ_p95_obs", "occ_p95_ldr", "occ_p95_csect", "occ_p95_pp", 222 | # "prob_blockedby_ldr", "blockedby_ldr_mean", "blockedby_ldr_p95", 223 | # "pct_blocked_by_pp", "blockedby_pp_mean", "blockedby_pp_p95"] 224 | 225 | output_csv_file = output_path / f'{output_file_stem}.csv' 226 | #results_df = results_df[cols] 227 | results_df = results_df.sort_values(by=['scenario', 'rep']) 228 | results_df.to_csv(output_csv_file, index=False) 229 | 230 | return active_units 231 | 232 | 233 | def compute_occ_stats(obsystem, end_time, egress=False, log_path=None, warmup=0, 234 | quantiles=(0.05, 0.25, 0.5, 0.75, 0.95, 0.99)): 235 | occ_stats_dfs = [] 236 | occ_dfs = [] 237 | for unit in obsystem.obunits: 238 | if len(unit.occupancy_list) > 1: 239 | occ = unit.occupancy_list 240 | 241 | df = pd.DataFrame(occ, columns=['timestamp', 'occ']) 242 | df['occ_weight'] = -1 * df['timestamp'].diff(periods=-1) 243 | 244 | last_weight = end_time - df.iloc[-1, 0] 245 | df.fillna(last_weight, inplace=True) 246 | df['unit'] = unit.name 247 | occ_dfs.append(df) 248 | 249 | # Filter out recs before warmup 250 | df = df[df['timestamp'] > warmup] 251 | 252 | weighted_stats = DescrStatsW(df['occ'], weights=df['occ_weight'], ddof=0) 253 | 254 | occ_quantiles = weighted_stats.quantile(quantiles) 255 | occ_unit_stats_df = pd.DataFrame([{'unit': unit.name, 'capacity': unit.capacity, 256 | 'mean_occ': weighted_stats.mean, 'sd_occ': weighted_stats.std, 257 | 'min_occ': df['occ'].min(), 'max_occ': df['occ'].max()}]) 258 | 259 | quantiles_df = pd.DataFrame(occ_quantiles).transpose() 260 | quantiles_df.rename(columns=lambda x: f"p{100 * x:02.0f}_occ", inplace=True) 261 | 262 | occ_unit_stats_df = pd.concat([occ_unit_stats_df, quantiles_df], axis=1) 263 | occ_stats_dfs.append(occ_unit_stats_df) 264 | 265 | occ_stats_df = pd.concat(occ_stats_dfs) 266 | 267 | if log_path is not None: 268 | occ_df = pd.concat(occ_dfs) 269 | if egress: 270 | occ_df.to_csv(log_path, index=False) 271 | else: 272 | occ_df[(occ_df['unit'] != 'ENTRY') & 273 | (occ_df['unit'] != 'EXIT')].to_csv(log_path, index=False) 274 | 275 | return occ_stats_df 276 | 277 | 278 | def output_header(msg, line_len, scenario, rep_num): 279 | header = f"\n{msg} (scenario={scenario} rep={rep_num})\n{'-' * line_len}\n" 280 | return header 281 | 282 | 283 | # 284 | 285 | def aggregate_over_reps(scen_rep_summary_path, active_units=('OBS', 'LDR', 'CSECT', 'PP')): 286 | """Compute summary stats by scenario (aggregating over replications)""" 287 | 288 | output_stats_summary_df = pd.read_csv(scen_rep_summary_path).sort_values(by=['scenario', 'rep']) 289 | 290 | unit_dfs = [] 291 | 292 | # Need better, more generalizable way to do this, but good enough for now. 293 | if 'OBS' in active_units: 294 | output_stats_summary_agg_obs_df = output_stats_summary_df.groupby(['scenario']).agg( 295 | num_visits_obs_mean=pd.NamedAgg(column='num_visits_obs', aggfunc='mean'), 296 | planned_los_mean_mean_obs=pd.NamedAgg(column='planned_los_mean_obs', aggfunc='mean'), 297 | actual_los_mean_mean_obs=pd.NamedAgg(column='actual_los_mean_obs', aggfunc='mean'), 298 | planned_los_mean_cv2_obs=pd.NamedAgg(column='planned_los_cv2_obs', aggfunc='mean'), 299 | actual_los_mean_cv2_obs=pd.NamedAgg(column='actual_los_cv2_obs', aggfunc='mean'), 300 | occ_mean_mean_obs=pd.NamedAgg(column='occ_mean_obs', aggfunc='mean'), 301 | occ_mean_p95_obs=pd.NamedAgg(column='occ_p95_obs', aggfunc='mean'), 302 | prob_blockedby_ldr=pd.NamedAgg(column='prob_blockedby_ldr', aggfunc='mean'), 303 | condmeantime_blockedby_ldr=pd.NamedAgg(column='blockedby_ldr_mean', aggfunc='mean'), 304 | condp95time_blockedby_ldr=pd.NamedAgg(column='blockedby_ldr_p95', aggfunc='mean') 305 | ) 306 | unit_dfs.append(output_stats_summary_agg_obs_df) 307 | 308 | if 'LDR' in active_units: 309 | output_stats_summary_agg_ldr_df = output_stats_summary_df.groupby(['scenario']).agg( 310 | num_visits_ldr_mean=pd.NamedAgg(column='num_visits_ldr', aggfunc='mean'), 311 | planned_los_mean_mean_ldr=pd.NamedAgg(column='planned_los_mean_ldr', aggfunc='mean'), 312 | actual_los_mean_mean_ldr=pd.NamedAgg(column='actual_los_mean_ldr', aggfunc='mean'), 313 | planned_los_mean_cv2_ldr=pd.NamedAgg(column='planned_los_cv2_ldr', aggfunc='mean'), 314 | actual_los_mean_cv2_ldr=pd.NamedAgg(column='actual_los_cv2_ldr', aggfunc='mean'), 315 | occ_mean_mean_ldr=pd.NamedAgg(column='occ_mean_ldr', aggfunc='mean'), 316 | occ_mean_p95_ldr=pd.NamedAgg(column='occ_p95_ldr', aggfunc='mean'), 317 | prob_blockedby_pp=pd.NamedAgg(column='pct_blocked_by_pp', aggfunc='mean'), 318 | condmeantime_blockedby_pp=pd.NamedAgg(column='blockedby_pp_mean', aggfunc='mean'), 319 | condp95time_blockedby_pp=pd.NamedAgg(column='blockedby_pp_p95', aggfunc='mean') 320 | ) 321 | unit_dfs.append(output_stats_summary_agg_ldr_df) 322 | 323 | if 'CSECT' in active_units: 324 | output_stats_summary_agg_csect_df = output_stats_summary_df.groupby(['scenario']).agg( 325 | num_visits_csect_mean=pd.NamedAgg(column='num_visits_csect', aggfunc='mean'), 326 | planned_los_mean_mean_csect=pd.NamedAgg(column='planned_los_mean_csect', aggfunc='mean'), 327 | actual_los_mean_mean_csect=pd.NamedAgg(column='actual_los_mean_csect', aggfunc='mean'), 328 | planned_los_mean_cv2_csect=pd.NamedAgg(column='planned_los_cv2_csect', aggfunc='mean'), 329 | actual_los_mean_cv2_csect=pd.NamedAgg(column='actual_los_cv2_csect', aggfunc='mean'), 330 | occ_mean_mean_csect=pd.NamedAgg(column='occ_mean_csect', aggfunc='mean'), 331 | occ_mean_p95_csect=pd.NamedAgg(column='occ_p95_csect', aggfunc='mean') 332 | ) 333 | unit_dfs.append(output_stats_summary_agg_csect_df) 334 | 335 | if 'PP' in active_units: 336 | output_stats_summary_agg_pp_df = output_stats_summary_df.groupby(['scenario']).agg( 337 | num_visits_pp_mean=pd.NamedAgg(column='num_visits_pp', aggfunc='mean'), 338 | planned_los_mean_mean_pp=pd.NamedAgg(column='planned_los_mean_pp', aggfunc='mean'), 339 | actual_los_mean_mean_pp=pd.NamedAgg(column='actual_los_mean_pp', aggfunc='mean'), 340 | planned_los_mean_cv2_pp=pd.NamedAgg(column='planned_los_cv2_pp', aggfunc='mean'), 341 | actual_los_mean_cv2_pp=pd.NamedAgg(column='actual_los_cv2_pp', aggfunc='mean'), 342 | occ_mean_mean_pp=pd.NamedAgg(column='occ_mean_pp', aggfunc='mean'), 343 | occ_mean_p95_pp=pd.NamedAgg(column='occ_p95_pp', aggfunc='mean') 344 | ) 345 | unit_dfs.append(output_stats_summary_agg_pp_df) 346 | 347 | output_stats_summary_agg_df = pd.concat(unit_dfs, axis=1) 348 | 349 | return output_stats_summary_agg_df 350 | 351 | 352 | 353 | def conf_intervals(scenario_rep_summary_df): 354 | """Compute CIs by scenario (aggregating over replications)""" 355 | 356 | occ_mean_obs_ci = varsum(scenario_rep_summary_df, 'obs', 'occ_mean_obs', 0.05) 357 | occ_p05_obs_ci = varsum(scenario_rep_summary_df, 'obs', 'occ_p95_obs', 0.05) 358 | 359 | occ_mean_ldr_ci = varsum(scenario_rep_summary_df, 'ldr', 'occ_mean_ldr', 0.05) 360 | occ_p05_ldr_ci = varsum(scenario_rep_summary_df, 'ldr', 'occ_p95_ldr', 0.05) 361 | 362 | occ_mean_pp_ci = varsum(scenario_rep_summary_df, 'pp', 'occ_mean_pp', 0.05) 363 | occ_p05_pp_ci = varsum(scenario_rep_summary_df, 'pp', 'occ_p95_pp', 0.05) 364 | 365 | prob_blockedby_ldr_ci = varsum(scenario_rep_summary_df, 'ldr', 'prob_blockedby_ldr', 0.05) 366 | blockedby_ldr_mean_ci = varsum(scenario_rep_summary_df, 'ldr', 'blockedby_ldr_mean', 0.05) 367 | blockedby_ldr_p95_ci = varsum(scenario_rep_summary_df, 'ldr', 'blockedby_ldr_p95', 0.05) 368 | 369 | pct_blocked_by_pp_ci = varsum(scenario_rep_summary_df, 'pp', 'pct_blocked_by_pp', 0.05) 370 | blockedby_pp_mean_ci = varsum(scenario_rep_summary_df, 'pp', 'blockedby_pp_mean', 0.05) 371 | blockedby_pp_p95_ci = varsum(scenario_rep_summary_df, 'pp', 'blockedby_pp_p95', 0.05) 372 | 373 | ci_dfs = [occ_mean_obs_ci, occ_p05_obs_ci, 374 | occ_mean_ldr_ci, occ_p05_ldr_ci, 375 | occ_mean_pp_ci, occ_p05_pp_ci, 376 | prob_blockedby_ldr_ci, blockedby_ldr_mean_ci, blockedby_ldr_p95_ci, 377 | pct_blocked_by_pp_ci, blockedby_pp_mean_ci, blockedby_pp_p95_ci] 378 | 379 | ci_df = pd.concat(ci_dfs) 380 | return ci_df 381 | 382 | 383 | # def hyper_erlang_moment(rates, stages, probs, moment): 384 | # terms = [probs[i - 1] * math.factorial(stages[i - 1] + moment - 1) * (1 / math.factorial(stages[i - 1] - 1)) * ( 385 | # stages[i - 1] * rates[i - 1]) ** (-moment) 386 | # for i in range(1, len(rates) + 1)] 387 | # 388 | # return sum(terms) 389 | 390 | 391 | def create_sim_summaries(output_path, suffix, 392 | include_inputs=True, 393 | scenario_inputs_path=None, 394 | active_units=('OBS', 'LDR', 'CSECT', 'PP') 395 | ): 396 | scenario_rep_simout_stem_ = f'scenario_rep_simout_{suffix}' 397 | scenario_simout_stem = f'scenario_simout_{suffix}' 398 | scenario_siminout_stem = f'scenario_siminout_{suffix}' 399 | scenario_ci_stem = f'scenario_ci_{suffix}' 400 | 401 | # Compute summary stats by scenario (aggregating over the replications) 402 | scenario_rep_simout_path = output_path / f"{scenario_rep_simout_stem_}.csv" 403 | scenario_rep_simout_df = pd.read_csv(scenario_rep_simout_path) 404 | scenario_simout_path = output_path / f"{scenario_simout_stem}.csv" 405 | scenario_siminout_path = output_path / f"{scenario_siminout_stem}.csv" 406 | scenario_simout_df = aggregate_over_reps(scenario_rep_simout_path, active_units) 407 | scenario_simout_df.to_csv(scenario_simout_path, index=True) 408 | 409 | scenario_ci_df = conf_intervals(scenario_rep_simout_df) 410 | scenario_ci_path = output_path / f"{scenario_ci_stem}.csv" 411 | scenario_ci_df.to_csv(scenario_ci_path, index=True) 412 | 413 | # Merge the scenario summary with the scenario inputs 414 | if include_inputs: 415 | 416 | scenario_rep_siminout_stem = f'scenario_rep_siminout_{suffix}' 417 | scenario_simin_df = pd.read_csv(scenario_inputs_path) 418 | scenario_siminout_df = scenario_simin_df.merge(scenario_simout_df, on=['scenario']) 419 | scenario_siminout_df.to_csv(scenario_siminout_path, index=False) 420 | 421 | scenario_rep_siminout_df = scenario_rep_simout_df.merge(scenario_simin_df, on=['scenario']) 422 | scenario_rep_siminout_path = output_path / f"{scenario_rep_siminout_stem}.csv" 423 | scenario_rep_siminout_df.to_csv(scenario_rep_siminout_path, index=False) 424 | 425 | # Using the scenario summary we just created 426 | # if include_qng_approx: 427 | # scenario_siminout_qng_stem = f'scenario_siminout_qng_{suffix}' 428 | # qng_approx_df = qng_approx(scenario_siminout_df) 429 | # scenario_siminout_qng_df = scenario_siminout_df.merge(qng_approx_df, on=['scenario']) 430 | # scenario_siminout_qng_path = output_path / f"{scenario_siminout_qng_stem}.csv" 431 | # scenario_siminout_qng_df.to_csv(scenario_siminout_qng_path, index=False) 432 | 433 | 434 | def process_command_line(): 435 | """ 436 | Parse command line arguments 437 | 438 | `argv` is a list of arguments, or `None` for ``sys.argv[1:]``. 439 | Return a Namespace representing the argument list. 440 | """ 441 | 442 | # Create the parser 443 | parser = argparse.ArgumentParser(prog='obflow_6_output', 444 | description='Run inpatient OB simulation output processor') 445 | 446 | # Add arguments 447 | parser.add_argument( 448 | "output_path", type=str, 449 | help="Destination Path for output summary files" 450 | ) 451 | 452 | parser.add_argument( 453 | "suffix", type=str, 454 | help="String to append to various summary filenames" 455 | ) 456 | 457 | parser.add_argument('--process_logs', dest='process_logs', action='store_true') 458 | parser.add_argument( 459 | "--stop_log_path", type=str, default=None, 460 | help="Path containing stop logs" 461 | ) 462 | parser.add_argument( 463 | "--occ_stats_path", type=str, default=None, 464 | help="Path containing occ stats csvs" 465 | ) 466 | 467 | parser.add_argument( 468 | "--run_time", type=float, default=None, 469 | help="Simulation run time" 470 | ) 471 | 472 | parser.add_argument( 473 | "--warmup_time", type=float, default=None, 474 | help="Simulation warmup time" 475 | ) 476 | 477 | parser.add_argument('--include_inputs', dest='include_inputs', action='store_true') 478 | parser.add_argument( 479 | "--scenario_inputs_path", type=str, default=None, 480 | help="Filename for scenario inputs" 481 | ) 482 | #parser.add_argument('--include_qng_approx', dest='include_qng_approx', action='store_true') 483 | 484 | # do the parsing 485 | args = parser.parse_args() 486 | 487 | return args 488 | 489 | 490 | def varsum(df, unit, pm, alpha): 491 | """Summarize variance in performance measure across replications within a scenario""" 492 | 493 | # alpha is for construction 1-alpha CI 494 | # Precision is CI half width / mean and is measure of relative error 495 | 496 | pm_varsum_df = df.groupby(['scenario']).agg( 497 | pm_mean=pd.NamedAgg(column=pm, aggfunc='mean'), 498 | pm_std=pd.NamedAgg(column=pm, aggfunc='std'), 499 | pm_n=pd.NamedAgg(column=pm, aggfunc='count'), 500 | pm_min=pd.NamedAgg(column=pm, aggfunc='min'), 501 | pm_max=pd.NamedAgg(column=pm, aggfunc='max')) 502 | 503 | pm_varsum_df['pm_cv'] = \ 504 | pm_varsum_df['pm_std'] / pm_varsum_df['pm_mean'] 505 | 506 | pm_varsum_df['ci_halfwidth'] = \ 507 | t.ppf(1 - alpha / 2, pm_varsum_df['pm_n'] - 1) * pm_varsum_df['pm_std'] / np.sqrt(pm_varsum_df['pm_n']) 508 | 509 | pm_varsum_df['ci_precision'] = pm_varsum_df['ci_halfwidth'] / pm_varsum_df['pm_mean'] 510 | 511 | pm_varsum_df['ci_lower'] = pm_varsum_df['pm_mean'] - pm_varsum_df['ci_halfwidth'] 512 | pm_varsum_df['ci_upper'] = pm_varsum_df['pm_mean'] + pm_varsum_df['ci_halfwidth'] 513 | 514 | pm_varsum_df['unit'] = unit 515 | pm_varsum_df['pm'] = pm 516 | 517 | return pm_varsum_df 518 | 519 | 520 | if __name__ == '__main__': 521 | 522 | inputs = process_command_line() 523 | active_units = ('OBS', 'LDR', 'CSECT', 'PP') 524 | 525 | if inputs.process_logs: 526 | # From the patient stop logs, compute summary stats by scenario by replication 527 | scenario_rep_summary_stem = f'scenario_rep_simout_{inputs.suffix}' 528 | 529 | active_units = process_obsim_logs(Path(inputs.stop_log_path), Path(inputs.occ_stats_path), 530 | Path(inputs.output_path), inputs.run_time, warmup=inputs.warmup_time, 531 | output_file_stem=scenario_rep_summary_stem) 532 | 533 | create_sim_summaries(Path(inputs.output_path), inputs.suffix, 534 | include_inputs=inputs.include_inputs, 535 | scenario_inputs_path=inputs.scenario_inputs_path, 536 | active_units=active_units) 537 | -------------------------------------------------------------------------------- /simpy-first-oo-patflow-model.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "In [Part 1 of this series](http://hselab.org/simpy-getting-started-patient-flow-modeling.html), we built a few simple patient flow simulation models using SimPy. They all were *process oriented*. In this post, I'll share my first attempt at an object oriented (OO) SimPy model for the same system. As mentioned in my previous post, I've had good luck building large complex discete event simulation models (including patient flow) with the object oriented Java library Simkit. In the process of building these initial OO models, I learned a bunch from the [posts done by the folks at Grotto Networking](https://www.grotto-networking.com/DiscreteEventPython.html#Intro)." 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "## OB patient flow system to model\n", 15 | "As a reminder, here's a picture of the simple system I'll be modeling.\n", 16 | "\n", 17 | "![](images/obflow.png)\n", 18 | "\n", 19 | "For this first set of simple models, features/assumptions of the system include:\n", 20 | "\n", 21 | "* Patients in labor arrive according to a poisson process\n", 22 | "* Assume c-section rate is zero and that all patients flow from OBS --> LDR --> PP units.\n", 23 | "* Length of stay at each unit is exponentially distributed with a given unit specific mean\n", 24 | "* Each unit has capacity level (i.e. the number of beds)\n", 25 | "* Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files.\n", 26 | "\n", 27 | "I'm going to start by building the simplest model possible and then start layering on complexity and features." 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "## obflow_5_oo_2: A simple object oriented SimPy patient flow model\n", 35 | "\n", 36 | "Since I'm new to SimPy, this first object oriented model will be relatively simple. There are only four classes:\n", 37 | "\n", 38 | "* ``OBPatientGenerator`` - generates patients\n", 39 | "* ``OBPatient`` - a patient; generated by ``OBPatientGenerator``\n", 40 | "* ``OBUnit`` - for modeling the OBS, LDR, and PP units\n", 41 | "* ``ExitFlow`` - sink to which patients are routed when ready to leave the system\n", 42 | "\n", 43 | "My goal was to just figure out how to model a simple static route, OBS -> LDR -> PP -> Exit, using SimPy in an OO way. Let's look at each class and then at the main driver script. You can get the code for this and the other models at https://github.com/misken/obsimpy. " 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "### Imports and constants" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 2, 56 | "metadata": { 57 | "collapsed": true, 58 | "jupyter": { 59 | "outputs_hidden": true 60 | } 61 | }, 62 | "outputs": [], 63 | "source": [ 64 | "import simpy\n", 65 | "from numpy.random import RandomState\n", 66 | "\n", 67 | "\"\"\"\n", 68 | "Simple OB patient flow model 5 - Very simple OO\n", 69 | "\n", 70 | "Details:\n", 71 | "\n", 72 | "- Generate arrivals via Poisson process\n", 73 | "- Define an OBUnit class that contains a simpy.Resource object as a member.\n", 74 | " Not subclassing Resource, just trying to use it as a member.\n", 75 | "- Routing is done via setting ``out`` member of an OBUnit instance to\n", 76 | " another OBUnit instance to which the OB patient flow instance should be\n", 77 | " routed. The routing logic, for now, is in OBUnit object. Later,\n", 78 | " we need some sort of router object and data driven routing.\n", 79 | "- Trying to get patient flow working without a process function that\n", 80 | "explicitly articulates the sequence of units and stays.\n", 81 | "\n", 82 | "Key Lessons Learned:\n", 83 | "\n", 84 | "- Any function that is a generator and might potentially yield for an event\n", 85 | " must get registered as a process.\n", 86 | "\n", 87 | "\"\"\"\n", 88 | "# Arrival rate and length of stay inputs.\n", 89 | "ARR_RATE = 0.4\n", 90 | "MEAN_LOS_OBS = 3\n", 91 | "MEAN_LOS_LDR = 12\n", 92 | "MEAN_LOS_PP = 48\n", 93 | "\n", 94 | "# Unit capacities\n", 95 | "CAPACITY_OBS = 2\n", 96 | "CAPACITY_LDR = 6\n", 97 | "CAPACITY_PP = 24\n", 98 | "\n", 99 | "# Random number seed\n", 100 | "RNG_SEED = 6353\n" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "### The ``OBPatientGenerator`` class\n", 108 | "This class generates OB patients according to a simple poisson process (exponential interarrival times). Its ``run()`` method gets registered as a SimPy process. The main loop in ``run()`` yields until the time for the next arrival. Upon arrival, a new ``OBPatient`` instance is generated and a request is initiated for a bed in the\n", 109 | "first unit of the patient's route - OBS in this case. We'll see shortly how the routing information is included in\n", 110 | "the ``OBPatient`` class (a \"hack\" that we'll remedy later with proper routing related classes).\n", 111 | "\n", 112 | "A few niceties were added to the class based on things I know will be useful to have later. These include:\n", 113 | "\n", 114 | "* arrival stream and patient type attributes; future versions of this model will support multiple patient types (e.g. spontaneous labor and induced labor) and arrival streams (e.g. random and scheduled),\n", 115 | "* ability to shut off arrivals based on simulation time or number of patients generated.\n", 116 | "\n" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": 3, 122 | "metadata": { 123 | "collapsed": false, 124 | "jupyter": { 125 | "outputs_hidden": false 126 | } 127 | }, 128 | "outputs": [], 129 | "source": [ 130 | "class OBPatientGenerator(object):\n", 131 | " \"\"\" Generates patients.\n", 132 | "\n", 133 | " Set the \"out\" member variable to resource at which patient generated.\n", 134 | "\n", 135 | " Parameters\n", 136 | " ----------\n", 137 | " env : simpy.Environment\n", 138 | " the simulation environment\n", 139 | " arr_rate : float\n", 140 | " Poisson arrival rate (expected number of arrivals per unit time)\n", 141 | " arr_stream : int\n", 142 | " Arrival stream id (default 1). Currently there is just one arrival\n", 143 | " stream corresponding to the one patient generator class. In future,\n", 144 | " likely to be be multiple generators for generating random and\n", 145 | " scheduled arrivals\n", 146 | " initial_delay : float\n", 147 | " Starts generation after an initial delay. (default 0.0)\n", 148 | " stoptime : float\n", 149 | " Stops generation at the stoptime. (default Infinity)\n", 150 | " max_arrivals : int\n", 151 | " Stops generation after max_arrivals. (default Infinity)\n", 152 | " debug: bool\n", 153 | " If True, status message printed after\n", 154 | " each patient created. (default False)\n", 155 | "\n", 156 | " \"\"\"\n", 157 | "\n", 158 | " def __init__(self, env, arr_rate, patient_type=1, arr_stream=1,\n", 159 | " initial_delay=0,\n", 160 | " stoptime=simpy.core.Infinity,\n", 161 | " max_arrivals=simpy.core.Infinity, debug=False):\n", 162 | "\n", 163 | " self.env = env\n", 164 | " self.arr_rate = arr_rate\n", 165 | " self.patient_type = patient_type\n", 166 | " self.arr_stream = arr_stream\n", 167 | " self.initial_delay = initial_delay\n", 168 | " self.stoptime = stoptime\n", 169 | " self.max_arrivals = max_arrivals\n", 170 | " self.debug = debug\n", 171 | " self.out = None\n", 172 | " self.num_patients_created = 0\n", 173 | "\n", 174 | " self.prng = RandomState(RNG_SEED)\n", 175 | "\n", 176 | " self.action = env.process(self.run()) # starts the run() method as a SimPy process\n", 177 | "\n", 178 | " def run(self):\n", 179 | " \"\"\"The patient generator.\n", 180 | " \"\"\"\n", 181 | " # Delay for initial_delay\n", 182 | " yield self.env.timeout(self.initial_delay)\n", 183 | " # Main generator loop that terminates when stoptime reached\n", 184 | " while self.env.now < self.stoptime and \\\n", 185 | " self.num_patients_created < self.max_arrivals:\n", 186 | " # Delay until time for next arrival\n", 187 | " # Compute next interarrival time\n", 188 | " iat = self.prng.exponential(1.0 / self.arr_rate)\n", 189 | " yield self.env.timeout(iat)\n", 190 | " self.num_patients_created += 1\n", 191 | " # Create new patient\n", 192 | " obp = OBPatient(self.env.now, self.num_patients_created,\n", 193 | " self.patient_type, self.arr_stream,\n", 194 | " prng=self.prng)\n", 195 | "\n", 196 | " if self.debug:\n", 197 | " print(\"Patient {} created at {:.2f}.\".format(\n", 198 | " self.num_patients_created, self.env.now))\n", 199 | "\n", 200 | " # Set out member to OBunit object representing next destination\n", 201 | " self.out = obunits[obp.planned_route_stop[1]]\n", 202 | " # Initiate process of requesting first bed in route\n", 203 | " self.env.process(self.out.put(obp))\n" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "### The ``OBPatient`` class\n", 211 | "These are the patients. As mentioned above, routing info (for now) is hard coded into this class. Each patient has same static route of OBS -> LDR -> PP. Lists are used to store routing information. Python uses 0 based indexing but we've chosen to use the [1] spot for OBS, [2] for LDR, and [3] for PP quantities. The [0] slot isn't used. Just wanted to avoid a bunch of ``current_stop_num - 1`` type of code fragments. Makes the code more readable, especially since we often have to refer to the previous stop and wish to avoid things like ``current_stop_num - 2`` to mean the previous stop. Obviously this is simple to change if you'd rather use 0 for stop number 1. \n", 212 | "\n", 213 | "Several lists are used to store bed request instances as well as useful timestamps (any variable name ending in ``_ts`` such as entry and exit times to each unit." 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 4, 219 | "metadata": { 220 | "collapsed": true, 221 | "jupyter": { 222 | "outputs_hidden": true 223 | } 224 | }, 225 | "outputs": [], 226 | "source": [ 227 | "class OBPatient(object):\n", 228 | " \"\"\" Models an OB patient\n", 229 | "\n", 230 | " Parameters\n", 231 | " ----------\n", 232 | " arr_time : float\n", 233 | " Patient arrival time\n", 234 | " patient_id : int\n", 235 | " Unique patient id\n", 236 | " patient_type : int\n", 237 | " Patient type id (default 1). Currently just one patient type.\n", 238 | " In our prior research work we used a scheme with 11 patient types.\n", 239 | " arr_stream : int\n", 240 | " Arrival stream id (default 1). Currently there is just one arrival\n", 241 | " stream corresponding to the one patient generator class. In future,\n", 242 | " likely to be be multiple generators for generating random and\n", 243 | " scheduled arrivals.\n", 244 | "\n", 245 | " \"\"\"\n", 246 | "\n", 247 | " def __init__(self, arr_time, patient_id, patient_type=1, arr_stream=1,\n", 248 | " prng=RandomState(0)):\n", 249 | " self.arr_time = arr_time\n", 250 | " self.patient_id = patient_id\n", 251 | " self.patient_type = patient_type\n", 252 | " self.arr_stream = arr_stream\n", 253 | "\n", 254 | " self.name = 'Patient_{}'.format(patient_id)\n", 255 | "\n", 256 | " # Hard coding route, los and bed requests for now\n", 257 | " # Not sure how best to do routing related data structures.\n", 258 | " # Hack for now using combination of lists here, the out member\n", 259 | " # and the obunits dictionary.\n", 260 | " self.current_stay_num = 0\n", 261 | " self.route_length = 3\n", 262 | "\n", 263 | " self.planned_route_stop = []\n", 264 | " self.planned_route_stop.append(None)\n", 265 | " self.planned_route_stop.append(\"OBS\")\n", 266 | " self.planned_route_stop.append(\"LDR\")\n", 267 | " self.planned_route_stop.append(\"PP\")\n", 268 | " self.planned_route_stop.append(\"EXIT\")\n", 269 | "\n", 270 | " self.planned_los = []\n", 271 | " self.planned_los.append(None)\n", 272 | " self.planned_los.append(prng.exponential(MEAN_LOS_OBS))\n", 273 | " self.planned_los.append(prng.exponential(MEAN_LOS_LDR))\n", 274 | " self.planned_los.append(prng.exponential(MEAN_LOS_PP))\n", 275 | "\n", 276 | " # Since we have fixed route for now, just initialize full list to\n", 277 | " # hold bed requests\n", 278 | " self.bed_requests = [None for _ in range(self.route_length + 1)]\n", 279 | " self.request_entry_ts = [None for _ in range(self.route_length + 1)]\n", 280 | " self.entry_ts = [None for _ in range(self.route_length + 1)]\n", 281 | " self.exit_ts = [None for _ in range(self.route_length + 1)]\n", 282 | "\n", 283 | " def __repr__(self):\n", 284 | " return \"patientid: {}, arr_stream: {}, time: {}\". \\\n", 285 | " format(self.patient_id, self.arr_stream, self.arr_time)" 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "metadata": {}, 291 | "source": [ 292 | "### The ``OBUnit`` class\n", 293 | "This class is used to model the OBS, LDR and PP units. It does most of the heavy lifting related to patients flowing into and out of the various units. It can be used to model both finite and infinite capacity resources. A few basic statistal accumulators are included. The ``out`` member gets set to the next unit to be visited by the patient." 294 | ] 295 | }, 296 | { 297 | "cell_type": "code", 298 | "execution_count": 6, 299 | "metadata": { 300 | "collapsed": true, 301 | "jupyter": { 302 | "outputs_hidden": true 303 | } 304 | }, 305 | "outputs": [], 306 | "source": [ 307 | "class OBunit(object):\n", 308 | " \"\"\" Models an OB unit with fixed capacity.\n", 309 | "\n", 310 | " Parameters\n", 311 | " ----------\n", 312 | " env : simpy.Environment\n", 313 | " the simulation environment\n", 314 | " name : str\n", 315 | " unit name\n", 316 | " capacity : integer (or None)\n", 317 | " Number of beds. Use None for infinite capacity.\n", 318 | "\n", 319 | " \"\"\"\n", 320 | "\n", 321 | " def __init__(self, env, name, capacity=None, debug=False):\n", 322 | " if capacity is None:\n", 323 | " self.capacity = simpy.core.Infinity\n", 324 | " else:\n", 325 | " self.capacity = capacity\n", 326 | "\n", 327 | " self.env = env\n", 328 | " self.name = name\n", 329 | " self.debug = debug\n", 330 | "\n", 331 | " # Use a simpy Resource as one of the class members\n", 332 | " self.unit = simpy.Resource(env, capacity)\n", 333 | "\n", 334 | " # Statistical accumulators\n", 335 | " self.num_entries = 0\n", 336 | " self.num_exits = 0\n", 337 | " self.tot_occ_time = 0.0\n", 338 | "\n", 339 | " # The out member will get set to destination unit\n", 340 | " self.out = None\n", 341 | "\n", 342 | " def put(self, obp):\n", 343 | " \"\"\" A process method called when a bed is requested in the unit.\n", 344 | "\n", 345 | " The logic of this method is reminiscent of the routing logic\n", 346 | " in the process oriented obflow models 1-3. However, this method\n", 347 | " is used for all of the units - no repeated logic.\n", 348 | "\n", 349 | " Parameters\n", 350 | " ----------\n", 351 | " env : simpy.Environment\n", 352 | " the simulation environment\n", 353 | "\n", 354 | " obp : OBPatient object\n", 355 | " the patient requestion the bed\n", 356 | "\n", 357 | " \"\"\"\n", 358 | "\n", 359 | " if self.debug:\n", 360 | " print(\"{} trying to get {} at {:.4f}\".format(obp.name,\n", 361 | " self.name, self.env.now))\n", 362 | "\n", 363 | " # Increments patient's attribute number of units visited\n", 364 | " obp.current_stay_num += 1\n", 365 | " # Timestamp of request time\n", 366 | " bed_request_ts = self.env.now\n", 367 | " # Request a bed\n", 368 | " bed_request = self.unit.request()\n", 369 | " # Store bed request and timestamp in patient's request lists\n", 370 | " obp.bed_requests[obp.current_stay_num] = bed_request\n", 371 | " obp.request_entry_ts[obp.current_stay_num] = self.env.now\n", 372 | " # Yield until we get a bed\n", 373 | " yield bed_request\n", 374 | "\n", 375 | " # Seized a bed.\n", 376 | " obp.entry_ts[obp.current_stay_num] = self.env.now\n", 377 | "\n", 378 | " # Check if we have a bed from a previous stay and release it.\n", 379 | " # Update stats for previous unit.\n", 380 | "\n", 381 | " if obp.bed_requests[obp.current_stay_num - 1] is not None:\n", 382 | " previous_request = obp.bed_requests[obp.current_stay_num - 1]\n", 383 | " previous_unit_name = \\\n", 384 | " obp.planned_route_stop[obp.current_stay_num - 1]\n", 385 | " previous_unit = obunits[previous_unit_name]\n", 386 | " previous_unit.unit.release(previous_request)\n", 387 | " previous_unit.num_exits += 1\n", 388 | " previous_unit.tot_occ_time += \\\n", 389 | " self.env.now - obp.entry_ts[obp.current_stay_num - 1]\n", 390 | " obp.exit_ts[obp.current_stay_num - 1] = self.env.now\n", 391 | "\n", 392 | " if self.debug:\n", 393 | " print(\"{} entering {} at {:.4f}\".format(obp.name, self.name,\n", 394 | " self.env.now))\n", 395 | " self.num_entries += 1\n", 396 | " if self.debug:\n", 397 | " if self.env.now > bed_request_ts:\n", 398 | " waittime = self.env.now - bed_request_ts\n", 399 | " print(\"{} waited {:.4f} time units for {} bed\".format(obp.name,\n", 400 | " waittime,\n", 401 | " self.name))\n", 402 | "\n", 403 | " # Determine los and then yield for the stay\n", 404 | " los = obp.planned_los[obp.current_stay_num]\n", 405 | " yield self.env.timeout(los)\n", 406 | "\n", 407 | " # Go to next destination (which could be an exitflow)\n", 408 | " next_unit_name = obp.planned_route_stop[obp.current_stay_num + 1]\n", 409 | " self.out = obunits[next_unit_name]\n", 410 | " if obp.current_stay_num == obp.route_length:\n", 411 | " # For ExitFlow object, no process needed\n", 412 | " self.out.put(obp)\n", 413 | " else:\n", 414 | " # Process for putting patient into next bed\n", 415 | " self.env.process(self.out.put(obp))\n", 416 | "\n", 417 | " def basic_stats_msg(self):\n", 418 | " \"\"\" Compute entries, exits, avg los and create summary message.\n", 419 | "\n", 420 | "\n", 421 | " Returns\n", 422 | " -------\n", 423 | " str\n", 424 | " Message with basic stats\n", 425 | " \"\"\"\n", 426 | "\n", 427 | " if self.num_exits > 0:\n", 428 | " alos = self.tot_occ_time / self.num_exits\n", 429 | " else:\n", 430 | " alos = 0\n", 431 | "\n", 432 | " msg = \"{:6}:\\t Entries={}, Exits={}, Occ={}, ALOS={:4.2f}\".\\\n", 433 | " format(self.name, self.num_entries, self.num_exits,\n", 434 | " self.unit.count, alos)\n", 435 | " return msg\n" 436 | ] 437 | }, 438 | { 439 | "cell_type": "markdown", 440 | "metadata": {}, 441 | "source": [ 442 | "### The ``ExitFlow`` class\n", 443 | "This is a \"sink\" and is used as a place to trigger any post-processing needs after the patient has completed their entire route through the OB system. For now we are just doing some basic statistical and conservation of flow summaries. More importantly, this class handles releasing the last bed in the patient's route." 444 | ] 445 | }, 446 | { 447 | "cell_type": "code", 448 | "execution_count": 7, 449 | "metadata": { 450 | "collapsed": true, 451 | "jupyter": { 452 | "outputs_hidden": true 453 | } 454 | }, 455 | "outputs": [], 456 | "source": [ 457 | "class ExitFlow(object):\n", 458 | " \"\"\" Patients routed here when ready to exit.\n", 459 | "\n", 460 | " Patient objects put into a Store. Can be accessed later for stats\n", 461 | " and logs. A little worried about how big the Store will get.\n", 462 | "\n", 463 | " Parameters\n", 464 | " ----------\n", 465 | " env : simpy.Environment\n", 466 | " the simulation environment\n", 467 | " debug : boolean\n", 468 | " if true then patient details printed on arrival\n", 469 | " \"\"\"\n", 470 | "\n", 471 | " def __init__(self, env, name, store_obp=True, debug=False):\n", 472 | " self.store = simpy.Store(env)\n", 473 | " self.env = env\n", 474 | " self.name = name\n", 475 | " self.store_obp = store_obp\n", 476 | " self.debug = debug\n", 477 | " self.num_exits = 0\n", 478 | " self.last_exit = 0.0\n", 479 | "\n", 480 | " def put(self, obp):\n", 481 | "\n", 482 | " if obp.bed_requests[obp.current_stay_num] is not None:\n", 483 | " previous_request = obp.bed_requests[obp.current_stay_num]\n", 484 | " previous_unit_name = obp.planned_route_stop[obp.current_stay_num]\n", 485 | " previous_unit = obunits[previous_unit_name]\n", 486 | " previous_unit.unit.release(previous_request)\n", 487 | " previous_unit.num_exits += 1\n", 488 | " previous_unit.tot_occ_time += self.env.now - obp.entry_ts[\n", 489 | " obp.current_stay_num]\n", 490 | " obp.exit_ts[obp.current_stay_num - 1] = self.env.now\n", 491 | "\n", 492 | " self.last_exit = self.env.now\n", 493 | " self.num_exits += 1\n", 494 | "\n", 495 | " if self.debug:\n", 496 | " print(obp)\n", 497 | "\n", 498 | " # Store patient\n", 499 | " if self.store_obp:\n", 500 | " self.store.put(obp)\n", 501 | "\n", 502 | " def basic_stats_msg(self):\n", 503 | " \"\"\" Create summary message with basic stats on exits.\n", 504 | "\n", 505 | "\n", 506 | " Returns\n", 507 | " -------\n", 508 | " str\n", 509 | " Message with basic stats\n", 510 | " \"\"\"\n", 511 | "\n", 512 | " msg = \"{:6}:\\t Exits={}, Last Exit={:10.2f}\".format(self.name,\n", 513 | " self.num_exits,\n", 514 | " self.last_exit)\n", 515 | "\n", 516 | " return msg\n" 517 | ] 518 | }, 519 | { 520 | "cell_type": "markdown", 521 | "metadata": {}, 522 | "source": [ 523 | "## Main driver script\n", 524 | "This looks much like the code in the previous models detailed in Part 1 of this series. One important change in this model is that the simulation environment variable is never used as a \"global\" variable. Any class needing access to the simulation environment explicitly requires it in its ``__init__`` method." 525 | ] 526 | }, 527 | { 528 | "cell_type": "code", 529 | "execution_count": 9, 530 | "metadata": { 531 | "collapsed": false, 532 | "jupyter": { 533 | "outputs_hidden": false 534 | } 535 | }, 536 | "outputs": [ 537 | { 538 | "name": "stdout", 539 | "output_type": "stream", 540 | "text": [ 541 | "rho_obs: 0.600\n", 542 | "rho_ldr: 0.800\n", 543 | "rho_pp: 0.800\n", 544 | "\n", 545 | "Num patients generated: 453\n", 546 | "\n", 547 | "OBS :\t Entries=431, Exits=429, Occ=2, ALOS=4.27\n", 548 | "LDR :\t Entries=429, Exits=425, Occ=4, ALOS=11.68\n", 549 | "PP :\t Entries=425, Exits=402, Occ=23, ALOS=48.40\n", 550 | "\n", 551 | "Num patients exiting system: 402\n", 552 | "\n", 553 | "Last exit at: 998.69\n", 554 | "\n" 555 | ] 556 | } 557 | ], 558 | "source": [ 559 | "# Initialize a simulation environment\n", 560 | "simenv = simpy.Environment()\n", 561 | "\n", 562 | "# Compute and display traffic intensities\n", 563 | "rho_obs = ARR_RATE * MEAN_LOS_OBS / CAPACITY_OBS\n", 564 | "rho_ldr = ARR_RATE * MEAN_LOS_LDR / CAPACITY_LDR\n", 565 | "rho_pp = ARR_RATE * MEAN_LOS_PP / CAPACITY_PP\n", 566 | "\n", 567 | "print(\"rho_obs: {:6.3f}\\nrho_ldr: {:6.3f}\\nrho_pp: {:6.3f}\".format(rho_obs,\n", 568 | " rho_ldr,\n", 569 | " rho_pp))\n", 570 | "\n", 571 | "# Create nursing units\n", 572 | "obs_unit = OBunit(simenv, 'OBS', CAPACITY_OBS, debug=False)\n", 573 | "ldr_unit = OBunit(simenv, 'LDR', CAPACITY_LDR, debug=False)\n", 574 | "pp_unit = OBunit(simenv, 'PP', CAPACITY_PP, debug=False)\n", 575 | "\n", 576 | "# Define system exit\n", 577 | "exitflow = ExitFlow(simenv, 'EXIT', store_obp=False)\n", 578 | "\n", 579 | "# Create dictionary of units keyed by name. This object can be passed along\n", 580 | "# to other objects so that the units are accessible as patients \"flow\".\n", 581 | "obunits = {'OBS': obs_unit, 'LDR': ldr_unit, 'PP': pp_unit, 'EXIT': exitflow}\n", 582 | "\n", 583 | "# Create a patient generator\n", 584 | "obpat_gen = OBPatientGenerator(simenv, ARR_RATE, debug=False)\n", 585 | "\n", 586 | "# Routing logic\n", 587 | "# Currently routing logic is hacked into the OBPatientGenerator\n", 588 | "# and OBPatient objects\n", 589 | "\n", 590 | "# Run the simulation for a while\n", 591 | "runtime = 1000\n", 592 | "simenv.run(until=runtime)\n", 593 | "\n", 594 | "# Patient generator stats\n", 595 | "print(\"\\nNum patients generated: {}\\n\".format(obpat_gen.num_patients_created))\n", 596 | "\n", 597 | "# Unit stats\n", 598 | "print(obs_unit.basic_stats_msg())\n", 599 | "print(ldr_unit.basic_stats_msg())\n", 600 | "print(pp_unit.basic_stats_msg())\n", 601 | "\n", 602 | "# System exit stats\n", 603 | "print(\"\\nNum patients exiting system: {}\\n\".format(exitflow.num_exits))\n", 604 | "print(\"Last exit at: {:.2f}\\n\".format(exitflow.last_exit))\n" 605 | ] 606 | }, 607 | { 608 | "cell_type": "markdown", 609 | "metadata": {}, 610 | "source": [ 611 | "## Next Steps\n", 612 | "Now that I have a feel for how SimPy works, I've got two parallel directions to pursue.\n", 613 | "\n", 614 | "### Task 1: Research quality SimPy model\n", 615 | "\n", 616 | "I'll start creating a more full featured version that replicates the functionality of the Simio based model I used in a recent research project. Some of the goals for this new version will be:\n", 617 | "\n", 618 | "* volume, length of stay, routing information and other key inputs should be read in from structured data input files (probably YAML or JSON format),\n", 619 | "* create routing classes to handle both random and scheduled arrivals,\n", 620 | "* capability to generate time dependent Poisson arrivals,\n", 621 | "* detailed patient flow logging to files,\n", 622 | "* more statistical output analysis,\n", 623 | "* comprehensive documentation,\n", 624 | "* a test suite including validation via queueing models (for tractable cases).\n", 625 | "\n", 626 | "\n", 627 | "### Task 2: Rebuild this model using the R package, simmer\n", 628 | "\n", 629 | "I'm a regular user of R for both research and teaching. So, I was pleasantly surprised to learn about a DES framework for R (!). The [simmer](http://r-simmer.org/) package looks relatively new and seems to be positioning itself similarly to SimPy. Here's the description from their main web page:\n", 630 | "\n", 631 | "> simmer is a process-oriented and trajectory-based Discrete-Event Simulation (DES) package for R. \n", 632 | "> Designed to be a generic framework like [SimPy](https://simpy.readthedocs.org/) or [SimJulia](http://simjuliajl.readthedocs.org/), it leverages the power of [Rcpp](http://www.rcpp.org/) to \n", 633 | "> boost the performance and turning DES in R feasible. As a noteworthy characteristic, \n", 634 | "> simmer exploits the concept of trajectory: a common path in the\n", 635 | "> simulation model for entities of the same type. It is pretty flexible and simple to use, and leverages the\n", 636 | "> chaining/piping workflow introduced by the [magrittr](https://github.com/smbache/magrittr) package.\n", 637 | "\n", 638 | "Interesting. While I love chaining/piping via [magrittr](https://github.com/smbache/magrittr), especially when using the mighty [dplyr](https://github.com/tidyverse/dplyr) package, I'm wondering if this will tie simmer to a process oriented simulation world view. I can see how the notion of a *trajectory* might lend itself to chaining and piping. Let's find out. I'll post the simmer version as soon as I get one working.\n" 639 | ] 640 | }, 641 | { 642 | "cell_type": "code", 643 | "execution_count": null, 644 | "metadata": { 645 | "collapsed": true, 646 | "jupyter": { 647 | "outputs_hidden": true 648 | } 649 | }, 650 | "outputs": [], 651 | "source": [] 652 | } 653 | ], 654 | "metadata": { 655 | "anaconda-cloud": {}, 656 | "kernelspec": { 657 | "display_name": "Python 3", 658 | "language": "python", 659 | "name": "python3" 660 | }, 661 | "language_info": { 662 | "codemirror_mode": { 663 | "name": "ipython", 664 | "version": 3 665 | }, 666 | "file_extension": ".py", 667 | "mimetype": "text/x-python", 668 | "name": "python", 669 | "nbconvert_exporter": "python", 670 | "pygments_lexer": "ipython3", 671 | "version": "3.8.5" 672 | } 673 | }, 674 | "nbformat": 4, 675 | "nbformat_minor": 4 676 | } 677 | -------------------------------------------------------------------------------- /simpy-getting-started.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "Over the last 30 or so years, I've built numerous healthcare discrete event simulation (DES) models. I built models as a student, as an analyst for several healthcare systems and consulting firms, and as a teacher and researcher at Oakland University. I've built models of inpatient units, pharmacies, pneumatic tube systems, outpatient clinics, surgical and recovery suites, emergency rooms and a whole bunch more. I've built models in several different commercial discrete event simulation languages/packages such as SIMAN, Arena, MedModel, and Simio. While each of these are very fine products, one thing that's always bothered me was how difficult to it was to share, review, and improve upon the models built by the healthcare simulation modeling community. These packages are really expensive often have pretty restrictive licensing. So, academics and practitioners publish articles describing how a simulation model of some healthcare related system (e.g. inpatient beds, pharmacy, lab, surgical suites...) was built and used in some project. Great. Inspiring. Not super helpful for the person looking to build a similar model, or better yet, build upon that same model. Models were rarely if ever made available and even if they were, might be written in a package that you don't have, don't know, or can't afford.\n", 8 | "\n", 9 | "As the years went on, I found myself more and more attracted to the world of open source software. I fell in love with Linux and R and Python. However, open source discrete event simulation options were not plentiful. I did have success building a pretty big pneumatic tube simulation model in Java using the Simkit library developed by [Arnie Buss and others at the Naval Postgraduate School](https://www.movesinstitute.org/research/simulation-modeling-for-analysis/). While I really like Simkit, I'm not a huge fan of programming in Java. Most of my research work and teaching is focused on R and Python and ideally would love to have a Python based DES framework. So, I was pretty happy when I stumbled onto the SimPy project and followed it for a bit as it moved through its early phases of growth and change. With SimPy 3.x now available, I decided to take the plunge and try to build some DES models for an ongoing research project involving simulating a simplified inpatient obstetrical patient flow network. I originally built the model in [Simio](http://hselab.org/comparing-predictive-models-for-obstetrical-unit-occupancy-using-caret-part-1.html). I describe the project in a bit of detail in a [previous post](http://hselab.org/comparing-predictive-models-for-obstetrical-unit-occupancy-using-caret-part-1.html) on using the R package `caret` for building metamodels of simulation output.\n", 10 | "\n", 11 | "I decided to start by building SimPy models of overly simplified versions of this patient flow network. As my SimPy knowledge grew, I'd add more features to the model and overhaul its architecture. My goal is be able to build models of a similar complexity to the Java based pneumatic tube model. I'll also explore the ecosystem of Python tools that might be used alongside SimPy for things like model animation, logging and monitoring of simulation runs, simulation experiment management and management of simulation input and output data.\n", 12 | "\n", 13 | "If you aren't familiar with [SimPy](https://simpy.readthedocs.io/en/latest/), visit the main site and check out the documentation. It's got an overview, installation instructions, a basic tutorial, topic guide, example models and the API. Another site with some very nice network models and tutorials is [available from Grotto Networking](https://www.grotto-networking.com/DiscreteEventPython.html#Intro). Models in SimPy can range from purely process based with no object oriented (OO) features to almost fully OO based models.\n", 14 | "\n", 15 | "**Note:** This series of blog posts assumes some knowledge of DES modeling, Python, and SimPy. I'm not trying to teach you how to use SimPy in general, but instead to share my journey of building progressively move complex DES patient flow models in SimPy. In the end, I hope to end up with a nice set of models and accompanying explanatory text that others can use and build on. I highly recommend the [SimPy in 10 Minutes](https://simpy.readthedocs.io/en/latest/simpy_intro/basic_concepts.html) introduction in the official docs." 16 | ] 17 | }, 18 | { 19 | "cell_type": "markdown", 20 | "metadata": {}, 21 | "source": [ 22 | "## OB patient flow system to model\n", 23 | "Here's a picture of the simple system I'll be modeling.\n", 24 | "\n", 25 | "![](images/obflow.png)\n", 26 | "\n", 27 | "For this first set of simple models, features/assumptions of the system include:\n", 28 | "\n", 29 | "* Patients in labor arrive according to a poisson process\n", 30 | "* Assume c-section rate is zero and that all patients flow from OBS --> LDR --> PP units.\n", 31 | "* Length of stay at each unit is exponentially distributed with a given unit specific mean\n", 32 | "* Each unit has capacity level (i.e. the number of beds)\n", 33 | "* Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files.\n", 34 | "\n", 35 | "I'm going to start by building the simplest model possible and then start layering on complexity and features." 36 | ] 37 | }, 38 | { 39 | "cell_type": "markdown", 40 | "metadata": {}, 41 | "source": [ 42 | "## obflow_1: A Hello World level SimPy patient flow model\n", 43 | "\n", 44 | "For my first ever SimPy model I decided to model the system as a sequence of delays. In other words, even though SimPy has features for modeling capacitated resources, I'm going to ignore them for now. This model is entirely *process oriented*. SimPy models processes with generator functions that can yield for events such as time durations or some system state condition. No classes are created in this first model. You'll also note that all the functions take\n", 45 | "a *simulation environment* instance as their first argurment. This is what you do in SimPy. It handles the clock and next event list logic as well as providing the means for running or single stepping through your model.\n", 46 | "\n", 47 | "The model prints out a message as each patient enters each delay stage. No summary stats are computed." 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 2, 53 | "metadata": { 54 | "collapsed": false 55 | }, 56 | "outputs": [ 57 | { 58 | "name": "stdout", 59 | "output_type": "stream", 60 | "text": [ 61 | "Patient1 entering OBS at 0.0000\n", 62 | "Patient1 entering LDR at 0.3475\n", 63 | "Patient2 entering OBS at 3.7668\n", 64 | "Patient3 entering OBS at 5.6439\n", 65 | "Patient1 entering PP at 6.1449\n", 66 | "Patient4 entering OBS at 7.2625\n", 67 | "Patient4 entering LDR at 7.3175\n", 68 | "Patient4 entering PP at 7.8836\n", 69 | "Patient5 entering OBS at 8.9829\n", 70 | "Patient3 entering LDR at 10.8140\n", 71 | "Patient2 entering LDR at 12.9060\n", 72 | "Patient6 entering OBS at 16.8153\n", 73 | "Patient5 entering LDR at 16.9444\n", 74 | "Patient7 entering OBS at 17.8737\n", 75 | "Patient7 entering LDR at 18.0987\n", 76 | "Patient7 entering PP at 18.1332\n", 77 | "Patient8 entering OBS at 18.7157\n", 78 | "Patient6 entering LDR at 19.4866\n", 79 | "Patient9 entering OBS at 20.4913\n", 80 | "Patient8 entering LDR at 22.8456\n", 81 | "Patient8 entering PP at 23.1882\n", 82 | "Patient5 entering PP at 24.5646\n" 83 | ] 84 | } 85 | ], 86 | "source": [ 87 | "import simpy\n", 88 | "import numpy as np\n", 89 | "from numpy.random import RandomState\n", 90 | "\n", 91 | "\"\"\"\n", 92 | "Simple OB patient flow model - NOT OO\n", 93 | "\n", 94 | "Details:\n", 95 | "\n", 96 | "- Generate arrivals via Poisson process\n", 97 | "- Simple delays modeling stay in OBS, LDR, and PP units. No capacitated resource contention modeled.\n", 98 | "- Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files.\n", 99 | "\n", 100 | "\"\"\"\n", 101 | "\n", 102 | "# Arrival rate and length of stay inputs.\n", 103 | "ARR_RATE = 0.4\n", 104 | "MEAN_LOS_OBS = 3\n", 105 | "MEAN_LOS_LDR = 12\n", 106 | "MEAN_LOS_PP = 48\n", 107 | "\n", 108 | "RNG_SEED = 6353\n", 109 | "\n", 110 | "def source(env, arr_rate, prng=RandomState(0)):\n", 111 | " \"\"\"Source generates patients according to a simple Poisson process\n", 112 | "\n", 113 | " Parameters\n", 114 | " ----------\n", 115 | " env : simpy.Environment\n", 116 | " the simulation environment\n", 117 | " arr_rate : float\n", 118 | " exponential arrival rate\n", 119 | " prng : RandomState object\n", 120 | " Seeded RandomState object for generating pseudo-random numbers.\n", 121 | " See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html\n", 122 | "\n", 123 | " \"\"\"\n", 124 | "\n", 125 | " patients_created = 0\n", 126 | "\n", 127 | " # Infinite loop for generatirng patients according to a poisson process.\n", 128 | " while True:\n", 129 | "\n", 130 | " # Generate next interarrival time\n", 131 | " iat = prng.exponential(1.0 / arr_rate)\n", 132 | "\n", 133 | " # Generate length of stay in each unit for this patient\n", 134 | " los_obs = prng.exponential(MEAN_LOS_OBS)\n", 135 | " los_ldr = prng.exponential(MEAN_LOS_LDR)\n", 136 | " los_pp = prng.exponential(MEAN_LOS_PP)\n", 137 | "\n", 138 | " # Update counter of patients\n", 139 | " patients_created += 1\n", 140 | "\n", 141 | " # Create a new patient flow process.\n", 142 | " obp = obpatient_flow(env, 'Patient{}'.format(patients_created),\n", 143 | " los_obs=los_obs, los_ldr=los_ldr, los_pp=los_pp)\n", 144 | "\n", 145 | " # Register the process with the simulation environment\n", 146 | " env.process(obp)\n", 147 | "\n", 148 | " # This process will now yield to a 'timeout' event. This process will resume after iat time units.\n", 149 | " yield env.timeout(iat)\n", 150 | "\n", 151 | "# Define an obpatient_flow \"process\"\n", 152 | "def obpatient_flow(env, name, los_obs, los_ldr, los_pp):\n", 153 | " \"\"\"Process function modeling how a patient flows through system.\n", 154 | "\n", 155 | " Parameters\n", 156 | " ----------\n", 157 | " env : simpy.Environment\n", 158 | " the simulation environment\n", 159 | " name : str\n", 160 | " process instance id\n", 161 | " los_obs : float\n", 162 | " length of stay in OBS unit\n", 163 | " los_ldr : float\n", 164 | " length of stay in LDR unit\n", 165 | " los_pp : float\n", 166 | " length of stay in PP unit\n", 167 | " \"\"\"\n", 168 | "\n", 169 | " # Note how we are simply modeling each stay as a delay. There\n", 170 | " # is NO contention for any resources.\n", 171 | " print(\"{} entering OBS at {:.4f}\".format(name, env.now))\n", 172 | " yield env.timeout(los_obs)\n", 173 | "\n", 174 | " print(\"{} entering LDR at {:.4f}\".format(name, env.now))\n", 175 | " yield env.timeout(los_ldr)\n", 176 | "\n", 177 | " print(\"{} entering PP at {:.4f}\".format(name, env.now))\n", 178 | " yield env.timeout(los_pp)\n", 179 | "\n", 180 | "\n", 181 | "# Initialize a simulation environment\n", 182 | "env = simpy.Environment()\n", 183 | "\n", 184 | "# Initialize a random number generator.\n", 185 | "# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html\n", 186 | "prng = RandomState(RNG_SEED)\n", 187 | "\n", 188 | "# Create a process generator and start it and add it to the env\n", 189 | "# Calling obpatient(env) creates the generator.\n", 190 | "# env.process() starts and adds it to env\n", 191 | "runtime = 25\n", 192 | "env.process(source(env, ARR_RATE, prng))\n", 193 | "\n", 194 | "# Run the simulation\n", 195 | "env.run(until=runtime)" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "This first simple model illustrates the basic structure of a SimPy model with a single process function. We see how the simulation environment is needed for orchestrating the process as it yields and resumes. Now let's move on to a slightly more complex model in which the OBS, LDR, and PP units are modeled as ``Resource`` objects." 203 | ] 204 | }, 205 | { 206 | "cell_type": "markdown", 207 | "metadata": {}, 208 | "source": [ 209 | "## obflow_2: OBS unit modeled as a Resource\n", 210 | "\n", 211 | "To start, let's model just the OBS unit as a ``Resource`` - LDR and PP units will still be modeled with delays. By doing this we can make sure we get the ``request`` and ``release`` logic working for one unit before duplicating it for the other two units. Like ``obflow_1``, this model is process oriented." 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 5, 217 | "metadata": { 218 | "collapsed": false 219 | }, 220 | "outputs": [ 221 | { 222 | "name": "stdout", 223 | "output_type": "stream", 224 | "text": [ 225 | "Patient1 trying to get OBS at 0.0000\n", 226 | "Patient1 entering OBS at 0.0000\n", 227 | "Patient1 leaving OBS at 0.3475\n", 228 | "Patient1 entering LDR at 0.3475\n", 229 | "Patient2 trying to get OBS at 3.7668\n", 230 | "Patient2 entering OBS at 3.7668\n", 231 | "Patient3 trying to get OBS at 5.6439\n", 232 | "Patient3 entering OBS at 5.6439\n", 233 | "Patient1 entering PP at 6.1449\n", 234 | "Patient4 trying to get OBS at 7.2625\n", 235 | "Patient5 trying to get OBS at 8.9829\n", 236 | "Patient3 leaving OBS at 10.8140\n", 237 | "Patient3 entering LDR at 10.8140\n", 238 | "Patient4 entering OBS at 10.8140\n", 239 | "Patient4 waited 3.5515 time units for OBS bed\n", 240 | "Patient4 leaving OBS at 10.8690\n", 241 | "Patient4 entering LDR at 10.8690\n", 242 | "Patient5 entering OBS at 10.8690\n", 243 | "Patient5 waited 1.8861 time units for OBS bed\n", 244 | "Patient4 entering PP at 11.4351\n", 245 | "Patient2 leaving OBS at 12.9060\n", 246 | "Patient2 entering LDR at 12.9060\n", 247 | "Patient6 trying to get OBS at 16.8153\n", 248 | "Patient6 entering OBS at 16.8153\n", 249 | "Patient7 trying to get OBS at 17.8737\n", 250 | "Patient8 trying to get OBS at 18.7157\n", 251 | "Patient5 leaving OBS at 18.8306\n", 252 | "Patient5 entering LDR at 18.8306\n", 253 | "Patient7 entering OBS at 18.8306\n", 254 | "Patient7 waited 0.9569 time units for OBS bed\n", 255 | "Patient7 leaving OBS at 19.0556\n", 256 | "Patient7 entering LDR at 19.0556\n", 257 | "Patient8 entering OBS at 19.0556\n", 258 | "Patient8 waited 0.3400 time units for OBS bed\n", 259 | "Patient7 entering PP at 19.0901\n", 260 | "Patient6 leaving OBS at 19.4866\n", 261 | "Patient6 entering LDR at 19.4866\n", 262 | "Patient9 trying to get OBS at 20.4913\n", 263 | "Patient9 entering OBS at 20.4913\n", 264 | "Patient8 leaving OBS at 23.1856\n", 265 | "Patient8 entering LDR at 23.1856\n", 266 | "Patient8 entering PP at 23.5282\n", 267 | "Patient2 entering PP at 25.7847\n", 268 | "Patient5 entering PP at 26.4507\n", 269 | "Patient10 trying to get OBS at 26.8687\n", 270 | "Patient10 entering OBS at 26.8687\n", 271 | "Patient10 leaving OBS at 27.6307\n", 272 | "Patient10 entering LDR at 27.6307\n", 273 | "Patient11 trying to get OBS at 27.6876\n", 274 | "Patient11 entering OBS at 27.6876\n", 275 | "Patient6 entering PP at 28.3114\n", 276 | "Patient9 leaving OBS at 30.4208\n", 277 | "Patient9 entering LDR at 30.4208\n", 278 | "Patient11 leaving OBS at 30.4652\n", 279 | "Patient11 entering LDR at 30.4652\n", 280 | "Patient11 entering PP at 30.5128\n", 281 | "Patient12 trying to get OBS at 31.4872\n", 282 | "Patient12 entering OBS at 31.4872\n", 283 | "Patient13 trying to get OBS at 31.9981\n", 284 | "Patient13 entering OBS at 31.9981\n", 285 | "Patient12 leaving OBS at 32.2156\n", 286 | "Patient12 entering LDR at 32.2156\n", 287 | "Patient13 leaving OBS at 32.5002\n", 288 | "Patient13 entering LDR at 32.5002\n", 289 | "Patient14 trying to get OBS at 33.5398\n", 290 | "Patient14 entering OBS at 33.5398\n", 291 | "Patient12 entering PP at 35.1766\n", 292 | "Patient15 trying to get OBS at 35.6094\n", 293 | "Patient15 entering OBS at 35.6094\n", 294 | "Patient13 entering PP at 35.8941\n", 295 | "Patient16 trying to get OBS at 36.5130\n", 296 | "Patient17 trying to get OBS at 36.5369\n", 297 | "Patient15 leaving OBS at 36.6000\n", 298 | "Patient15 entering LDR at 36.6000\n", 299 | "Patient16 entering OBS at 36.6000\n", 300 | "Patient16 waited 0.0870 time units for OBS bed\n", 301 | "Patient16 leaving OBS at 37.4484\n", 302 | "Patient16 entering LDR at 37.4484\n", 303 | "Patient17 entering OBS at 37.4484\n", 304 | "Patient17 waited 0.9115 time units for OBS bed\n", 305 | "Patient18 trying to get OBS at 39.6783\n", 306 | "Patient16 entering PP at 39.7335\n", 307 | "Patient17 leaving OBS at 39.9760\n", 308 | "Patient17 entering LDR at 39.9760\n", 309 | "Patient18 entering OBS at 39.9760\n", 310 | "Patient18 waited 0.2977 time units for OBS bed\n", 311 | "Patient9 entering PP at 40.7127\n", 312 | "Patient14 leaving OBS at 41.0861\n", 313 | "Patient14 entering LDR at 41.0861\n", 314 | "Patient19 trying to get OBS at 42.1910\n", 315 | "Patient19 entering OBS at 42.1910\n", 316 | "Patient10 entering PP at 42.5130\n", 317 | "Patient18 leaving OBS at 42.7172\n", 318 | "Patient18 entering LDR at 42.7172\n", 319 | "Patient17 entering PP at 44.1436\n", 320 | "Patient18 entering PP at 44.4498\n", 321 | "Patient15 entering PP at 44.6092\n", 322 | "Patient20 trying to get OBS at 44.6790\n", 323 | "Patient20 entering OBS at 44.6790\n", 324 | "Patient3 entering PP at 44.9184\n", 325 | "Patient20 leaving OBS at 45.2709\n", 326 | "Patient20 entering LDR at 45.2709\n", 327 | "Patient21 trying to get OBS at 46.6338\n", 328 | "Patient21 entering OBS at 46.6338\n", 329 | "Patient21 leaving OBS at 47.8777\n", 330 | "Patient21 entering LDR at 47.8777\n", 331 | "Patient19 leaving OBS at 48.7520\n", 332 | "Patient19 entering LDR at 48.7520\n", 333 | "Patient14 entering PP at 49.3856\n", 334 | "Patient19 entering PP at 49.7737\n", 335 | "Patient22 trying to get OBS at 49.9682\n", 336 | "Patient22 entering OBS at 49.9682\n", 337 | "Patient22 leaving OBS at 50.6934\n", 338 | "Patient22 entering LDR at 50.6934\n", 339 | "Patient21 entering PP at 50.6969\n", 340 | "Patient23 trying to get OBS at 51.9160\n", 341 | "Patient23 entering OBS at 51.9160\n", 342 | "Patient24 trying to get OBS at 52.8406\n", 343 | "Patient24 entering OBS at 52.8406\n", 344 | "Patient24 leaving OBS at 53.3548\n", 345 | "Patient24 entering LDR at 53.3548\n", 346 | "Patient23 leaving OBS at 54.9689\n", 347 | "Patient23 entering LDR at 54.9689\n", 348 | "Patient25 trying to get OBS at 54.9893\n", 349 | "Patient25 entering OBS at 54.9893\n", 350 | "Patient24 entering PP at 55.3316\n", 351 | "Patient20 entering PP at 55.7203\n", 352 | "Patient23 entering PP at 57.8120\n", 353 | "Patient22 entering PP at 58.5375\n", 354 | "Patient26 trying to get OBS at 61.0166\n", 355 | "Patient26 entering OBS at 61.0166\n", 356 | "Patient26 leaving OBS at 61.1124\n", 357 | "Patient26 entering LDR at 61.1124\n", 358 | "Patient27 trying to get OBS at 62.3948\n", 359 | "Patient27 entering OBS at 62.3948\n", 360 | "Patient25 leaving OBS at 63.4644\n", 361 | "Patient25 entering LDR at 63.4644\n", 362 | "Patient27 leaving OBS at 63.5210\n", 363 | "Patient27 entering LDR at 63.5210\n", 364 | "Patient26 entering PP at 65.5079\n", 365 | "Patient28 trying to get OBS at 68.1463\n", 366 | "Patient28 entering OBS at 68.1463\n", 367 | "Patient29 trying to get OBS at 68.4204\n", 368 | "Patient29 entering OBS at 68.4204\n", 369 | "Patient25 entering PP at 68.5573\n", 370 | "Patient28 leaving OBS at 68.5577\n", 371 | "Patient28 entering LDR at 68.5577\n", 372 | "Patient30 trying to get OBS at 72.7648\n", 373 | "Patient30 entering OBS at 72.7648\n", 374 | "Patient30 leaving OBS at 72.7858\n", 375 | "Patient30 entering LDR at 72.7858\n", 376 | "Patient31 trying to get OBS at 72.8227\n", 377 | "Patient31 entering OBS at 72.8227\n", 378 | "Patient27 entering PP at 73.2677\n", 379 | "Patient29 leaving OBS at 73.3031\n", 380 | "Patient29 entering LDR at 73.3031\n", 381 | "Patient31 leaving OBS at 73.5402\n", 382 | "Patient31 entering LDR at 73.5402\n", 383 | "Patient32 trying to get OBS at 73.7931\n", 384 | "Patient32 entering OBS at 73.7931\n", 385 | "Patient30 entering PP at 74.6737\n" 386 | ] 387 | } 388 | ], 389 | "source": [ 390 | "import simpy\n", 391 | "import numpy as np\n", 392 | "from numpy.random import RandomState\n", 393 | "\n", 394 | "\"\"\"\n", 395 | "Simple OB patient flow model 2 - NOT OO\n", 396 | "\n", 397 | "Details:\n", 398 | "\n", 399 | "- Generate arrivals via Poisson process\n", 400 | "- Uses one Resource objects to model OBS, the other units are modeled as simple delays.\n", 401 | "- Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files.\n", 402 | "\n", 403 | "\"\"\"\n", 404 | "# Arrival rate and length of stay inputs.\n", 405 | "ARR_RATE = 0.4\n", 406 | "MEAN_LOS_OBS = 3\n", 407 | "MEAN_LOS_LDR = 12\n", 408 | "MEAN_LOS_PP = 48\n", 409 | "\n", 410 | "CAPACITY_OBS = 2\n", 411 | "\n", 412 | "RNG_SEED = 6353\n", 413 | "\n", 414 | "def patient_generator(env, arr_rate, prng=RandomState(0)):\n", 415 | " \"\"\"Generates patients according to a simple Poisson process\n", 416 | "\n", 417 | " Parameters\n", 418 | " ----------\n", 419 | " env : simpy.Environment\n", 420 | " the simulation environment\n", 421 | " arr_rate : float\n", 422 | " exponential arrival rate\n", 423 | " prng : RandomState object\n", 424 | " Seeded RandomState object for generating pseudo-random numbers.\n", 425 | " See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html\n", 426 | "\n", 427 | " \"\"\"\n", 428 | "\n", 429 | " patients_created = 0\n", 430 | "\n", 431 | " # Infinite loop for generatirng patients according to a poisson process.\n", 432 | " while True:\n", 433 | "\n", 434 | " # Generate next interarrival time\n", 435 | " iat = prng.exponential(1.0 / arr_rate)\n", 436 | "\n", 437 | " # Generate length of stay in each unit for this patient\n", 438 | " los_obs = prng.exponential(MEAN_LOS_OBS)\n", 439 | " los_ldr = prng.exponential(MEAN_LOS_LDR)\n", 440 | " los_pp = prng.exponential(MEAN_LOS_PP)\n", 441 | "\n", 442 | " # Update counter of patients\n", 443 | " patients_created += 1\n", 444 | "\n", 445 | " # Create a new patient flow process.\n", 446 | " obp = obpatient_flow(env, 'Patient{}'.format(patients_created),\n", 447 | " los_obs=los_obs, los_ldr=los_ldr, los_pp=los_pp)\n", 448 | "\n", 449 | " # Register the process with the simulation environment\n", 450 | " env.process(obp)\n", 451 | "\n", 452 | " # This process will now yield to a 'timeout' event. This process will resume after iat time units.\n", 453 | " yield env.timeout(iat)\n", 454 | "\n", 455 | "\n", 456 | "def obpatient_flow(env, name, los_obs, los_ldr, los_pp):\n", 457 | " \"\"\"Process function modeling how a patient flows through system.\n", 458 | "\n", 459 | " Parameters\n", 460 | " ----------\n", 461 | " env : simpy.Environment\n", 462 | " the simulation environment\n", 463 | " name : str\n", 464 | " process instance id\n", 465 | " los_obs : float\n", 466 | " length of stay in OBS unit\n", 467 | " los_ldr : float\n", 468 | " length of stay in LDR unit\n", 469 | " los_pp : float\n", 470 | " length of stay in PP unit\n", 471 | " \"\"\"\n", 472 | "\n", 473 | " print(\"{} trying to get OBS at {:.4f}\".format(name, env.now))\n", 474 | "\n", 475 | " # Timestamp when patient tried to get OBS bed\n", 476 | " bed_request_ts = env.now\n", 477 | " # Request an obs bed\n", 478 | " bed_request = obs_unit.request()\n", 479 | " # Yield this process until a bed is available\n", 480 | " yield bed_request\n", 481 | "\n", 482 | " # We got an OBS bed\n", 483 | " print(\"{} entering OBS at {:.4f}\".format(name, env.now))\n", 484 | " # Let's see if we had to wait to get the bed.\n", 485 | " if env.now > bed_request_ts:\n", 486 | " print(\"{} waited {:.4f} time units for OBS bed\".format(name, env.now - bed_request_ts))\n", 487 | "\n", 488 | " # Yield this process again. Now wait until our length of stay elapses.\n", 489 | " # This is the actual stay in the bed\n", 490 | " yield env.timeout(los_obs)\n", 491 | "\n", 492 | " # All done with OBS, release the bed. Note that we pass the bed_request object\n", 493 | " # to the release() function so that the correct unit of the resource is released.\n", 494 | " obs_unit.release(bed_request)\n", 495 | " print(\"{} leaving OBS at {:.4f}\".format(name, env.now))\n", 496 | "\n", 497 | " # Continue on through LDR and PP; modeled as simple delays for now.\n", 498 | "\n", 499 | " print(\"{} entering LDR at {:.4f}\".format(name, env.now))\n", 500 | " yield env.timeout(los_ldr)\n", 501 | "\n", 502 | " print(\"{} entering PP at {:.4f}\".format(name, env.now))\n", 503 | " yield env.timeout(los_pp)\n", 504 | "\n", 505 | "\n", 506 | "# Initialize a simulation environment\n", 507 | "env = simpy.Environment()\n", 508 | "\n", 509 | "# Initialize a random number generator.\n", 510 | "# See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html\n", 511 | "prng = RandomState(RNG_SEED)\n", 512 | "\n", 513 | "# Declare a Resource to model OBS unit. Default capacity is 1, we pass in desired capacity.\n", 514 | "obs_unit = simpy.Resource(env, CAPACITY_OBS)\n", 515 | "\n", 516 | "# Run the simulation for a while\n", 517 | "runtime = 75\n", 518 | "env.process(patient_generator(env, ARR_RATE, prng))\n", 519 | "env.run(until=runtime)\n" 520 | ] 521 | }, 522 | { 523 | "cell_type": "markdown", 524 | "metadata": {}, 525 | "source": [ 526 | "Seems to be working. Note that with an arrival rate of 0.4 and a runtime of 75, we expect about..." 527 | ] 528 | }, 529 | { 530 | "cell_type": "code", 531 | "execution_count": 6, 532 | "metadata": { 533 | "collapsed": false 534 | }, 535 | "outputs": [ 536 | { 537 | "name": "stdout", 538 | "output_type": "stream", 539 | "text": [ 540 | "Expected number of arrivals = 30.00\n" 541 | ] 542 | } 543 | ], 544 | "source": [ 545 | "print(\"Expected number of arrivals = {:.2f}\".format(ARR_RATE * runtime))" 546 | ] 547 | }, 548 | { 549 | "cell_type": "markdown", 550 | "metadata": {}, 551 | "source": [ 552 | "We got 32 with this random number seed. No cause for alarm. Of course, we should run this thing longer and compute some stats so that we can make sure things are working fine. We'll do that shortly." 553 | ] 554 | }, 555 | { 556 | "cell_type": "markdown", 557 | "metadata": {}, 558 | "source": [ 559 | "## obflow_3: All units modeled as a Resources\n", 560 | "\n", 561 | "Let's build on `obflow_2` by modeling all three units as resources. Since these are hospital beds, there are no queues per se. If a patient finishes their stay in OBS and there are no beds available in the LDR, they wait in OBS. Likewise, when trying to get into PP after delivering the baby in the LDR, they may get blocked in the LDR waiting for an available PP bed. In more complex models we may actually do length of stay adjustments for those patients who are blocked. For now, let's just keep it simple and not do any such adjustments.\n", 562 | "\n", 563 | "In SimPy terms, we have to be careful that we don't release a bed we are in until we have successfully reserved a bed in the next unit we are to visit. As you'll see, this leads to quite a bit of repetitive code in our process oriented approach. This will motivate us to start exploring object oriented models. \n", 564 | "\n", 565 | "Finally, we'll improve our patient generator by adding the ability to model an initial delay before patient generation should start as well as to specify a stop time for the patient generation process (which can be different than the simulation model should stop)." 566 | ] 567 | }, 568 | { 569 | "cell_type": "code", 570 | "execution_count": 9, 571 | "metadata": { 572 | "collapsed": false 573 | }, 574 | "outputs": [ 575 | { 576 | "name": "stdout", 577 | "output_type": "stream", 578 | "text": [ 579 | "Patient1 trying to get OBS at 0.0000\n", 580 | "Patient1 entering OBS at 0.0000\n", 581 | "Patient1 trying to get LDR at 0.3475\n", 582 | "Patient1 leaving OBS at 0.3474891089551544\n", 583 | "Patient1 entering LDR at 0.3475\n", 584 | "Patient2 trying to get OBS at 3.7668\n", 585 | "Patient2 entering OBS at 3.7668\n", 586 | "Patient3 trying to get OBS at 5.6439\n", 587 | "Patient3 entering OBS at 5.6439\n", 588 | "Patient1 trying to get PP at 6.1449\n", 589 | "Patient1 leaving LDR at 6.1449\n", 590 | "Patient1 entering PP at 6.1449\n", 591 | "Patient4 trying to get OBS at 7.2625\n", 592 | "Patient5 trying to get OBS at 8.9829\n", 593 | "Patient3 trying to get LDR at 10.8140\n", 594 | "Patient3 leaving OBS at 10.813985162144862\n", 595 | "Patient3 entering LDR at 10.8140\n", 596 | "Patient4 entering OBS at 10.8140\n", 597 | "Patient4 waited 3.5515 time units for OBS bed\n", 598 | "Patient4 trying to get LDR at 10.8690\n", 599 | "Patient4 leaving OBS at 10.869006908518232\n", 600 | "Patient4 entering LDR at 10.8690\n", 601 | "Patient5 entering OBS at 10.8690\n", 602 | "Patient5 waited 1.8861 time units for OBS bed\n", 603 | "Patient4 trying to get PP at 11.4351\n", 604 | "Patient4 leaving LDR at 11.4351\n", 605 | "Patient4 entering PP at 11.4351\n", 606 | "Patient2 trying to get LDR at 12.9060\n", 607 | "Patient2 leaving OBS at 12.90600490911474\n", 608 | "Patient2 entering LDR at 12.9060\n", 609 | "Patient6 trying to get OBS at 16.8153\n", 610 | "Patient6 entering OBS at 16.8153\n", 611 | "Patient7 trying to get OBS at 17.8737\n", 612 | "Patient8 trying to get OBS at 18.7157\n", 613 | "Patient5 trying to get LDR at 18.8306\n", 614 | "Patient5 leaving OBS at 18.830564537083035\n", 615 | "Patient5 entering LDR at 18.8306\n", 616 | "Patient7 entering OBS at 18.8306\n", 617 | "Patient7 waited 0.9569 time units for OBS bed\n", 618 | "Patient7 trying to get LDR at 19.0556\n", 619 | "Patient7 leaving OBS at 19.05561983104953\n", 620 | "Patient7 entering LDR at 19.0556\n", 621 | "Patient8 entering OBS at 19.0556\n", 622 | "Patient8 waited 0.3400 time units for OBS bed\n", 623 | "Patient7 trying to get PP at 19.0901\n", 624 | "Patient7 leaving LDR at 19.0901\n", 625 | "Patient7 entering PP at 19.0901\n", 626 | "Patient6 trying to get LDR at 19.4866\n", 627 | "Patient6 leaving OBS at 19.48663625825889\n", 628 | "Patient6 entering LDR at 19.4866\n", 629 | "Patient9 trying to get OBS at 20.4913\n", 630 | "Patient9 entering OBS at 20.4913\n", 631 | "Patient8 trying to get LDR at 23.1856\n", 632 | "Patient8 leaving OBS at 23.185576349161767\n", 633 | "Patient8 entering LDR at 23.1856\n", 634 | "Patient8 trying to get PP at 23.5282\n", 635 | "Patient8 leaving LDR at 23.5282\n", 636 | "Patient8 entering PP at 23.5282\n", 637 | "Patient2 trying to get PP at 25.7847\n", 638 | "Patient2 leaving LDR at 25.7847\n", 639 | "Patient2 entering PP at 25.7847\n", 640 | "Patient5 trying to get PP at 26.4507\n", 641 | "Patient5 leaving LDR at 26.4507\n", 642 | "Patient5 entering PP at 26.4507\n", 643 | "Patient10 trying to get OBS at 26.8687\n", 644 | "Patient10 entering OBS at 26.8687\n", 645 | "Patient10 trying to get LDR at 27.6307\n", 646 | "Patient10 leaving OBS at 27.63070937211375\n", 647 | "Patient10 entering LDR at 27.6307\n", 648 | "Patient11 trying to get OBS at 27.6876\n", 649 | "Patient11 entering OBS at 27.6876\n", 650 | "Patient6 trying to get PP at 28.3114\n", 651 | "Patient6 leaving LDR at 28.3114\n", 652 | "Patient6 entering PP at 28.3114\n", 653 | "Patient9 trying to get LDR at 30.4208\n", 654 | "Patient9 leaving OBS at 30.420840322655998\n", 655 | "Patient9 entering LDR at 30.4208\n", 656 | "Patient11 trying to get LDR at 30.4652\n", 657 | "Patient11 leaving OBS at 30.465221449496795\n", 658 | "Patient11 entering LDR at 30.4652\n", 659 | "Patient11 trying to get PP at 30.5128\n", 660 | "Patient11 leaving LDR at 30.5128\n", 661 | "Patient11 entering PP at 30.5128\n", 662 | "Patient12 trying to get OBS at 31.4872\n", 663 | "Patient12 entering OBS at 31.4872\n", 664 | "Patient13 trying to get OBS at 31.9981\n", 665 | "Patient13 entering OBS at 31.9981\n", 666 | "Patient12 trying to get LDR at 32.2156\n", 667 | "Patient12 leaving OBS at 32.215634051591636\n", 668 | "Patient12 entering LDR at 32.2156\n", 669 | "Patient13 trying to get LDR at 32.5002\n", 670 | "Patient13 leaving OBS at 32.500238016146994\n", 671 | "Patient13 entering LDR at 32.5002\n", 672 | "Patient14 trying to get OBS at 33.5398\n", 673 | "Patient14 entering OBS at 33.5398\n", 674 | "Patient7 leaving PP and system at 34.5316\n", 675 | "Patient12 trying to get PP at 35.1766\n", 676 | "Patient12 leaving LDR at 35.1766\n", 677 | "Patient12 entering PP at 35.1766\n", 678 | "Patient15 trying to get OBS at 35.6094\n", 679 | "Patient15 entering OBS at 35.6094\n", 680 | "Patient13 trying to get PP at 35.8941\n", 681 | "Patient13 leaving LDR at 35.8941\n", 682 | "Patient13 entering PP at 35.8941\n", 683 | "Patient16 trying to get OBS at 36.5130\n", 684 | "Patient17 trying to get OBS at 36.5369\n", 685 | "Patient15 trying to get LDR at 36.6000\n", 686 | "Patient15 leaving OBS at 36.59999969293714\n", 687 | "Patient15 entering LDR at 36.6000\n", 688 | "Patient16 entering OBS at 36.6000\n", 689 | "Patient16 waited 0.0870 time units for OBS bed\n", 690 | "Patient16 trying to get LDR at 37.4484\n", 691 | "Patient16 leaving OBS at 37.44839668463982\n", 692 | "Patient16 entering LDR at 37.4484\n", 693 | "Patient17 entering OBS at 37.4484\n", 694 | "Patient17 waited 0.9115 time units for OBS bed\n", 695 | "Patient11 leaving PP and system at 38.1145\n", 696 | "Patient18 trying to get OBS at 39.6783\n", 697 | "Patient16 trying to get PP at 39.7335\n", 698 | "Patient16 leaving LDR at 39.7335\n", 699 | "Patient16 entering PP at 39.7335\n", 700 | "Patient17 trying to get LDR at 39.9760\n", 701 | "Patient17 leaving OBS at 39.97599817647182\n", 702 | "Patient17 entering LDR at 39.9760\n", 703 | "Patient18 entering OBS at 39.9760\n", 704 | "Patient18 waited 0.2977 time units for OBS bed\n", 705 | "Patient9 trying to get PP at 40.7127\n", 706 | "Patient9 leaving LDR at 40.7127\n", 707 | "Patient9 entering PP at 40.7127\n", 708 | "Patient14 trying to get LDR at 41.0861\n", 709 | "Patient14 leaving OBS at 41.086116528209494\n", 710 | "Patient14 entering LDR at 41.0861\n", 711 | "Patient19 trying to get OBS at 42.1910\n", 712 | "Patient19 entering OBS at 42.1910\n", 713 | "Patient10 trying to get PP at 42.5130\n", 714 | "Patient10 leaving LDR at 42.5130\n", 715 | "Patient10 entering PP at 42.5130\n", 716 | "Patient18 trying to get LDR at 42.7172\n", 717 | "Patient18 leaving OBS at 42.71719377930215\n", 718 | "Patient18 entering LDR at 42.7172\n", 719 | "Patient1 leaving PP and system at 42.8634\n", 720 | "Patient17 trying to get PP at 44.1436\n", 721 | "Patient17 leaving LDR at 44.1436\n", 722 | "Patient17 entering PP at 44.1436\n", 723 | "Patient18 trying to get PP at 44.4498\n", 724 | "Patient18 leaving LDR at 44.4498\n", 725 | "Patient18 entering PP at 44.4498\n", 726 | "Patient15 trying to get PP at 44.6092\n", 727 | "Patient15 leaving LDR at 44.6092\n", 728 | "Patient15 entering PP at 44.6092\n", 729 | "Patient20 trying to get OBS at 44.6790\n", 730 | "Patient20 entering OBS at 44.6790\n", 731 | "Patient3 trying to get PP at 44.9184\n", 732 | "Patient3 leaving LDR at 44.9184\n", 733 | "Patient3 entering PP at 44.9184\n", 734 | "Patient20 trying to get LDR at 45.2709\n", 735 | "Patient20 leaving OBS at 45.270857202928546\n", 736 | "Patient20 entering LDR at 45.2709\n", 737 | "Patient21 trying to get OBS at 46.6338\n", 738 | "Patient21 entering OBS at 46.6338\n", 739 | "Patient21 trying to get LDR at 47.8777\n", 740 | "Patient21 leaving OBS at 47.8776934769442\n", 741 | "Patient21 entering LDR at 47.8777\n", 742 | "Patient19 trying to get LDR at 48.7520\n", 743 | "Patient19 leaving OBS at 48.75197164898342\n", 744 | "Patient19 entering LDR at 48.7520\n", 745 | "Patient14 trying to get PP at 49.3856\n", 746 | "Patient14 leaving LDR at 49.3856\n", 747 | "Patient14 entering PP at 49.3856\n", 748 | "Patient19 trying to get PP at 49.7737\n", 749 | "Patient19 leaving LDR at 49.7737\n", 750 | "Patient19 entering PP at 49.7737\n", 751 | "Patient22 trying to get OBS at 49.9682\n", 752 | "Patient22 entering OBS at 49.9682\n", 753 | "Patient22 trying to get LDR at 50.6934\n", 754 | "Patient22 leaving OBS at 50.69343889384862\n", 755 | "Patient22 entering LDR at 50.6934\n", 756 | "Patient21 trying to get PP at 50.6969\n", 757 | "Patient21 leaving LDR at 50.6969\n", 758 | "Patient21 entering PP at 50.6969\n", 759 | "Patient20 trying to get PP at 55.7203\n", 760 | "Patient20 leaving LDR at 55.7203\n", 761 | "Patient20 entering PP at 55.7203\n", 762 | "Patient16 leaving PP and system at 56.0439\n", 763 | "Patient18 leaving PP and system at 57.0161\n", 764 | "Patient12 leaving PP and system at 58.3965\n", 765 | "Patient22 trying to get PP at 58.5375\n", 766 | "Patient22 leaving LDR at 58.5375\n", 767 | "Patient22 entering PP at 58.5375\n", 768 | "Patient10 leaving PP and system at 59.8131\n", 769 | "Patient9 leaving PP and system at 60.4868\n", 770 | "Patient5 leaving PP and system at 63.4959\n", 771 | "Patient17 leaving PP and system at 69.8364\n", 772 | "Patient14 leaving PP and system at 69.9692\n", 773 | "Patient15 leaving PP and system at 70.5547\n", 774 | "Patient22 leaving PP and system at 72.2739\n", 775 | "Patient20 leaving PP and system at 73.4076\n" 776 | ] 777 | } 778 | ], 779 | "source": [ 780 | "import simpy\n", 781 | "import numpy as np\n", 782 | "from numpy.random import RandomState\n", 783 | "\n", 784 | "\"\"\"\n", 785 | "Simple OB patient flow model 3 - NOT OO\n", 786 | "\n", 787 | "Details:\n", 788 | "\n", 789 | "- Generate arrivals via Poisson process\n", 790 | "- Uses one Resource objects to model OBS, LDR, and PP.\n", 791 | "- Arrival rates and mean lengths of stay hard coded as constants. Later versions will read these from input files.\n", 792 | "- Additional functionality added to arrival generator (initial delay and arrival stop time).\n", 793 | "\n", 794 | "\"\"\"\n", 795 | "ARR_RATE = 0.4\n", 796 | "MEAN_LOS_OBS = 3\n", 797 | "MEAN_LOS_LDR = 12\n", 798 | "MEAN_LOS_PP = 48\n", 799 | "\n", 800 | "CAPACITY_OBS = 2\n", 801 | "CAPACITY_LDR = 6\n", 802 | "CAPACITY_PP = 24\n", 803 | "\n", 804 | "RNG_SEED = 6353\n", 805 | "\n", 806 | "def patient_generator(env, arr_stream, arr_rate, initial_delay=0,\n", 807 | " stoptime=simpy.core.Infinity, prng=RandomState(0)):\n", 808 | " \"\"\"Generates patients according to a simple Poisson process\n", 809 | "\n", 810 | " Parameters\n", 811 | " ----------\n", 812 | " env : simpy.Environment\n", 813 | " the simulation environment\n", 814 | " arr_rate : float\n", 815 | " exponential arrival rate\n", 816 | " initial_delay: float (default 0)\n", 817 | " time before arrival generation should begin\n", 818 | " stoptime: float (default Infinity)\n", 819 | " time after which no arrivals are generated\n", 820 | " prng : RandomState object\n", 821 | " Seeded RandomState object for generating pseudo-random numbers.\n", 822 | " See https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.RandomState.html\n", 823 | "\n", 824 | " \"\"\"\n", 825 | "\n", 826 | " patients_created = 0\n", 827 | "\n", 828 | " # Yield for the initial delay\n", 829 | " yield env.timeout(initial_delay)\n", 830 | "\n", 831 | " # Generate arrivals as long as simulation time is before stoptime\n", 832 | " while env.now < stoptime:\n", 833 | "\n", 834 | " iat = prng.exponential(1.0 / arr_rate)\n", 835 | "\n", 836 | " # Sample los distributions\n", 837 | " los_obs = prng.exponential(MEAN_LOS_OBS)\n", 838 | " los_ldr = prng.exponential(MEAN_LOS_LDR)\n", 839 | " los_pp = prng.exponential(MEAN_LOS_PP)\n", 840 | "\n", 841 | "\n", 842 | " # Create new patient process instance\n", 843 | " patients_created += 1\n", 844 | " obp = obpatient_flow(env, 'Patient{}'.format(patients_created),\n", 845 | " los_obs=los_obs, los_ldr=los_ldr, los_pp=los_pp)\n", 846 | "\n", 847 | " env.process(obp)\n", 848 | "\n", 849 | " # Compute next interarrival time\n", 850 | "\n", 851 | " yield env.timeout(iat)\n", 852 | "\n", 853 | "\n", 854 | "def obpatient_flow(env, name, los_obs, los_ldr, los_pp):\n", 855 | " \"\"\"Process function modeling how a patient flows through system.\n", 856 | "\n", 857 | " Parameters\n", 858 | " ----------\n", 859 | " env : simpy.Environment\n", 860 | " the simulation environment\n", 861 | " name : str\n", 862 | " process instance id\n", 863 | " los_obs : float\n", 864 | " length of stay in OBS unit\n", 865 | " los_ldr : float\n", 866 | " length of stay in LDR unit\n", 867 | " los_pp : float\n", 868 | " length of stay in PP unit\n", 869 | " \"\"\"\n", 870 | "\n", 871 | " # Note the repetitive code and the use of separate request objects for each\n", 872 | " # stay in the different units.\n", 873 | "\n", 874 | " # OBS\n", 875 | " print(\"{} trying to get OBS at {:.4f}\".format(name, env.now))\n", 876 | " bed_request_ts = env.now\n", 877 | " bed_request1 = obs_unit.request() # Request an OBS bed\n", 878 | " yield bed_request1\n", 879 | " print(\"{} entering OBS at {:.4f}\".format(name, env.now))\n", 880 | " if env.now > bed_request_ts:\n", 881 | " print(\"{} waited {:.4f} time units for OBS bed\".format(name, env.now- bed_request_ts))\n", 882 | " yield env.timeout(los_obs) # Stay in obs bed\n", 883 | "\n", 884 | " print(\"{} trying to get LDR at {:.4f}\".format(name, env.now))\n", 885 | " bed_request_ts = env.now\n", 886 | " bed_request2 = ldr_unit.request() # Request an LDR bed\n", 887 | " yield bed_request2\n", 888 | "\n", 889 | " # Got LDR bed, release OBS bed\n", 890 | " obs_unit.release(bed_request1) # Release the OBS bed\n", 891 | " print(\"{} leaving OBS at {}\".format(name, env.now))\n", 892 | "\n", 893 | " # LDR stay\n", 894 | " print(\"{} entering LDR at {:.4f}\".format(name, env.now))\n", 895 | " if env.now > bed_request_ts:\n", 896 | " print(\"{} waited {:.4f} time units for LDR bed\".format(name, env.now - bed_request_ts))\n", 897 | " yield env.timeout(los_ldr) # Stay in LDR bed\n", 898 | "\n", 899 | " print(\"{} trying to get PP at {:.4f}\".format(name, env.now))\n", 900 | " bed_request_ts = env.now\n", 901 | " bed_request3 = pp_unit.request() # Request a PP bed\n", 902 | " yield bed_request3\n", 903 | "\n", 904 | " # Got PP bed, release LDR bed\n", 905 | " ldr_unit.release(bed_request2) # Release the obs bed\n", 906 | " print(\"{} leaving LDR at {:.4f}\".format(name, env.now))\n", 907 | "\n", 908 | " # PP stay\n", 909 | " print(\"{} entering PP at {:.4f}\".format(name, env.now))\n", 910 | " if env.now > bed_request_ts:\n", 911 | " print(\"{} waited {:.4f} time units for PP bed\".format(name, env.now - bed_request_ts))\n", 912 | " yield env.timeout(los_pp) # Stay in PP bed\n", 913 | " pp_unit.release(bed_request3) # Release the PP bed\n", 914 | "\n", 915 | " print(\"{} leaving PP and system at {:.4f}\".format(name, env.now))\n", 916 | "\n", 917 | "# Initialize a simulation environment\n", 918 | "env = simpy.Environment()\n", 919 | "\n", 920 | "prng = RandomState(RNG_SEED)\n", 921 | "\n", 922 | "rho_obs = ARR_RATE * MEAN_LOS_OBS / CAPACITY_OBS\n", 923 | "rho_ldr = ARR_RATE * MEAN_LOS_LDR / CAPACITY_LDR\n", 924 | "rho_pp = ARR_RATE * MEAN_LOS_PP / CAPACITY_PP\n", 925 | "\n", 926 | "# Declare Resources to model all units\n", 927 | "obs_unit = simpy.Resource(env, CAPACITY_OBS)\n", 928 | "ldr_unit = simpy.Resource(env, CAPACITY_LDR)\n", 929 | "pp_unit = simpy.Resource(env, CAPACITY_PP)\n", 930 | "\n", 931 | "# Run the simulation for a while. Let's shut arrivals off after 50 time units.\n", 932 | "runtime = 75\n", 933 | "stop_arrivals = 50\n", 934 | "env.process(patient_generator(env, \"Type1\", ARR_RATE, 0, stop_arrivals, prng))\n", 935 | "env.run(until=runtime)" 936 | ] 937 | }, 938 | { 939 | "cell_type": "markdown", 940 | "metadata": {}, 941 | "source": [ 942 | "Notice we get far fewer patients because we turned off the arrival spigot at time 50.\n", 943 | "\n", 944 | "As a veteran simulation modeler, I'm not really liking this process oriented approach. It's not going to scale well as we add more units (e.g. if we were modeling all the inpatient beds) and more routing complexity. Having built a pretty big object oriented DES model (in Java with Simkit) for analyzing pneumatic tube systems, I know that that's the way to go. In the next post, I'll share my first object oriented SimPy model. Not only does an object oriented approach let us avoid so much code repitition, it also makes collecting stats and logging easier." 945 | ] 946 | } 947 | ], 948 | "metadata": { 949 | "anaconda-cloud": {}, 950 | "kernelspec": { 951 | "display_name": "Python [conda env:hselab]", 952 | "language": "python", 953 | "name": "conda-env-hselab-py" 954 | }, 955 | "language_info": { 956 | "codemirror_mode": { 957 | "name": "ipython", 958 | "version": 3 959 | }, 960 | "file_extension": ".py", 961 | "mimetype": "text/x-python", 962 | "name": "python", 963 | "nbconvert_exporter": "python", 964 | "pygments_lexer": "ipython3", 965 | "version": "3.5.2" 966 | } 967 | }, 968 | "nbformat": 4, 969 | "nbformat_minor": 2 970 | } 971 | --------------------------------------------------------------------------------