diff --git a/content/01_sampling.ipynb b/content/01_sampling.ipynb new file mode 100644 index 0000000..bc47725 --- /dev/null +++ b/content/01_sampling.ipynb @@ -0,0 +1,474 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "10df8325-b87f-4633-97ba-fdc1f70c77a9", + "metadata": {}, + "source": [ + "# How to sample from statistical distributions in Python\n", + "\n", + "If you are working in simulation modelling in python it is highly likely that you will need to make use of the `numpy.random` namespace. This provides a variety of statistical distributions that you can use for efficient sampling. Let's take a look at a few example distributions. For example generating 100,000 samples from the uniform, exponential distributions and normal distributions:" + ] + }, + { + "cell_type": "markdown", + "id": "3352e4cf-0342-45e5-a645-132cdf5193e7", + "metadata": {}, + "source": [ + "## 1. Imports\n", + "\n", + "We will import `numpy` for our sampling and `matplotlib` to plot our distributions." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1f15c689-7821-46e7-8fe8-30623b381fd3", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "4f9b6c3a-b126-4e3b-b102-573a1685a782", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## 2. Helper functions\n", + "\n", + "The simple function below can be used to automatically produce a plot illustrating a distribution of samples. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "be8a6201-34f8-4529-9437-8b09017221dd", + "metadata": {}, + "outputs": [], + "source": [ + "def distribution_plot(samples, bins=100, figsize=(5,3)):\n", + " '''\n", + " helper function to visualise the distributions\n", + " \n", + " Params:\n", + " -----\n", + " samples: np.ndarray\n", + " A numpy array of quantitative data to plot as a histogram.\n", + " \n", + " bins: int, optional (default=100)\n", + " The number of bins to include in the histogram\n", + " \n", + " figsize: (int, int)\n", + " Size of the plot in pixels\n", + " \n", + " Returns:\n", + " -------\n", + " fig, ax: a tuple containing matplotlib figure and axis objects.\n", + " '''\n", + " hist = np.histogram(samples, bins=np.arange(bins), \n", + " density=True)\n", + " \n", + " fig = plt.figure(figsize=figsize)\n", + " ax = fig.add_subplot()\n", + " _ = ax.plot(hist[0])\n", + " _ = ax.set_ylabel('p(x)')\n", + " _ = ax.set_xlabel('x')\n", + " \n", + " return fig, ax" + ] + }, + { + "cell_type": "markdown", + "id": "6796cec8-5481-4003-bddc-430811fa95ab", + "metadata": {}, + "source": [ + "## 3. Creating a random number generator object" + ] + }, + { + "cell_type": "markdown", + "id": "f0b09c93-9c0c-421e-8f84-b054955af540", + "metadata": {}, + "source": [ + "To create a `Generator` use the `default_rng()` function of the random module.\n", + "\n", + "For more information on `Generator` you can look at [`numpy` online documentation.](https://numpy.org/doc/stable/reference/random/generator.html)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e10f42c4-6543-4f33-ac84-bd6ed1c5574b", + "metadata": {}, + "outputs": [], + "source": [ + "rng = np.random.default_rng()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ea83794d-6c4f-4234-b745-70d0271ad2df", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "numpy.random._generator.Generator" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "type(rng)" + ] + }, + { + "cell_type": "markdown", + "id": "14809bb3-e67c-452c-a9f4-26f4c55b900f", + "metadata": {}, + "source": [ + "## 4. Steps to create a sample\n", + "\n", + "In general the approach to sampling is:\n", + "\n", + "1. Create a random number generator object\n", + "2. Using the object call the method for the statistical distribution\n", + " * Each method has its own custom parameters\n", + " * Each method will include a `size` parameter that you use to set the number of samples to generate\n", + "3. Store the result in an appropriately named variable.\n", + "\n", + "### 4.1 Uniform distribution\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "28bafa4f-da57-4cd7-a2ce-c3e11b66e907", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# step 1: create a random number generator object - set seed to 42\n", + "rng = np.random.default_rng(42)\n", + "\n", + "# step 2 and 3: call the appropraite method of the generator and store result\n", + "samples = rng.uniform(low=10, high=40, size=1_000_000)\n", + "\n", + "# illustrate with plot.\n", + "fig, ax = distribution_plot(samples, bins=50)" + ] + }, + { + "cell_type": "markdown", + "id": "f4e9c040-9843-4236-ae27-40834aaf7665", + "metadata": {}, + "source": [ + "### 4.2 Exponential distribution" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "69760faf-4773-48d4-9b56-49585782cb61", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(
, )" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rng = np.random.default_rng()\n", + "samples = rng.exponential(scale=12, size=1_000_000)\n", + "distribution_plot(samples, bins=50)" + ] + }, + { + "cell_type": "markdown", + "id": "c298bbcd-b19e-458c-9cb3-0ccb85858fd9", + "metadata": {}, + "source": [ + "## 4.3 Normal distribution" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6b81d05e-b181-4ed1-8b2d-54c0dd04048e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(
, )" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rng = np.random.default_rng()\n", + "samples = rng.normal(loc=25.0, scale=5.0, size=1_000_000)\n", + "distribution_plot(samples, bins=50)" + ] + }, + { + "cell_type": "markdown", + "id": "8484ec86-7ea8-4959-8c3a-ee8665bbbbd6", + "metadata": {}, + "source": [ + "## 4.4 Generating a single sample\n", + "\n", + "If we just need to generate the a single sample we omit the `size` parameter. This returns a scalar value." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "7269e0fd-140e-49a3-ac6f-6eb416ad49e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "26.523585398772155\n", + "\n" + ] + } + ], + "source": [ + "rng = np.random.default_rng(42)\n", + "sample = rng.normal(loc=25.0, scale=5.0)\n", + "print(sample)\n", + "print(type(sample))" + ] + }, + { + "cell_type": "markdown", + "id": "6ed34200-c5ac-4a70-ae83-90adf48aee67", + "metadata": {}, + "source": [ + "Note that you can also set `size` to 1. Just be aware that an array is returned. e.g." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cad9fc7b-e63c-472c-a5d7-863d97b00fa8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[26.5235854]\n", + "\n", + "26.523585398772155\n" + ] + } + ], + "source": [ + "rng = np.random.default_rng(42)\n", + "sample = rng.normal(loc=25.0, scale=5.0, size=1)\n", + "# a numpy array is returned\n", + "print(sample)\n", + "print(type(sample))\n", + "\n", + "# to access the scalar value use the 0 index of the array.\n", + "print(sample[0])" + ] + }, + { + "cell_type": "markdown", + "id": "0dc23b86-1ae1-4da0-b393-97dcf884f442", + "metadata": {}, + "source": [ + "## 6. Encapsulating distributions, parameters, and random seeds.\n", + "\n", + "When building a simulation model it is often useful to *package up* both a random number generator, parameters for a specific distribution, and a seed in a python class. This allows easy creation of generator objects, straightforward sampling, and improves management of streams for each activity in a simulation model.\n", + "\n", + "As an example here a class `Exponential` representing the exponential distribution. It accepts a mean value parameter and you can set the random seed.\n", + "\n", + "We will then instantiate two `Exponential` objects for two different processes in our simulation: acute length of stay, and rehab length of stay." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "90c287a0-8aa1-4eb4-8beb-b7fdead97ca2", + "metadata": {}, + "outputs": [], + "source": [ + "class Exponential:\n", + " '''\n", + " Convenience class for the exponential distribution.\n", + " packages up distribution parameters, seed and random generator.\n", + " '''\n", + " def __init__(self, mean, random_seed=None):\n", + " '''\n", + " Constructor\n", + "\n", + " Params:\n", + " ------\n", + " mean: float\n", + " The mean of the exponential distribution\n", + "\n", + " random_seed: int, optional (default=None)\n", + " A random seed to reproduce samples. If set to none then a unique\n", + " sample is created.\n", + " '''\n", + " self.rand = np.random.default_rng(seed=random_seed)\n", + " self.mean = mean\n", + "\n", + " def sample(self, size=None):\n", + " '''\n", + " Generate a sample from the exponential distribution\n", + "\n", + " Params:\n", + " -------\n", + " size: int, optional (default=None)\n", + " the number of samples to return. If size=None then a single\n", + " sample is returned.\n", + " '''\n", + " return self.rand.exponential(self.mean, size=size)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "f51cc325-f40b-4d7c-9007-02f574c16a00", + "metadata": {}, + "outputs": [], + "source": [ + "acute_los = Exponential(3.0, random_seed=42)\n", + "rehab_los = Exponential(30.0, random_seed=101)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d6e73503-f402-4cb7-8450-347cbad79bb9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "7.2126258118979845" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "acute_los.sample()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "43976ab6-38c2-4f3d-ab64-1f64dea6bbcb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "122.69065518352762" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rehab_los.sample()" + ] + }, + { + "cell_type": "markdown", + "id": "45f7c0f2-0667-48ca-b93e-c80f15f44baf", + "metadata": {}, + "source": [ + "## 7. Next steps" + ] + }, + { + "cell_type": "markdown", + "id": "8b269763-110f-404c-806b-adf91025b7c0", + "metadata": {}, + "source": [ + "We can now move onto creating simple `simpy` models that make use of `numpy` sampling." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/01_urgent_care_model.ipynb b/content/01_urgent_care_model.ipynb deleted file mode 100644 index 3d63597..0000000 --- a/content/01_urgent_care_model.ipynb +++ /dev/null @@ -1,2453 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "4b7fd086", - "metadata": {}, - "source": [ - "# SimPy: Treatment Centre" - ] - }, - { - "cell_type": "markdown", - "id": "f79d76cd", - "metadata": {}, - "source": [ - "## 1. Imports" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b80bb80f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'4.0.1'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import simpy\n", - "simpy.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "7ee7d659", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "import itertools\n", - "import math\n", - "import matplotlib.pyplot as plt\n", - "import fileinput" - ] - }, - { - "cell_type": "markdown", - "id": "a4dce0cb", - "metadata": {}, - "source": [ - "## 2. Constants and defaults for modelling **as-is**" - ] - }, - { - "cell_type": "markdown", - "id": "bf299d92", - "metadata": {}, - "source": [ - "### 2.1 Distribution parameters" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "0aecaabe", - "metadata": {}, - "outputs": [], - "source": [ - "# sign-in/triage parameters\n", - "DEFAULT_TRIAGE_MEAN = 3.0\n", - "\n", - "# registration parameters\n", - "DEFAULT_REG_MEAN = 5.0\n", - "DEFAULT_REG_VAR= 2.0\n", - "\n", - "# examination parameters\n", - "DEFAULT_EXAM_MEAN = 16.0\n", - "DEFAULT_EXAM_VAR = 3.0\n", - "\n", - "# trauma/stabilisation\n", - "DEFAULT_TRAUMA_MEAN = 90.0\n", - "\n", - "# Trauma treatment\n", - "DEFAULT_TRAUMA_TREAT_MEAN = 30.0\n", - "DEFAULT_TRAUMA_TREAT_VAR = 4.0\n", - "\n", - "# Non trauma treatment\n", - "DEFAULT_NON_TRAUMA_TREAT_MEAN = 13.3\n", - "DEFAULT_NON_TRAUMA_TREAT_VAR = 2.0\n", - "\n", - "# prob patient requires treatment given trauma\n", - "DEFAULT_NON_TRAUMA_TREAT_P = 0.60\n", - "\n", - "# proportion of patients triaged as trauma\n", - "DEFAULT_PROB_TRAUMA = 0.12" - ] - }, - { - "cell_type": "markdown", - "id": "9b4ff209", - "metadata": {}, - "source": [ - "### 2.2 Time dependent arrival rates data\n", - "\n", - "The data for arrival rates varies between clinic opening at 6am and closure at 12am." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "f5f662ce", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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wKUo2gEAAAAAcCmKdgAAAAAAXIqiHQAAAAAAl6JoBwAAAADApSjaAQAAAABwKYp2AAAAAABciqIdAAAAAACXomgHAAAAAMClKNoBAAAAAHApinYAAAAAAFyKoh0AAAAAAJeiaAcAAAAAwKUo2gEAAAAAcKk0F+1//PGHrV692nn82WefWfv27W3kyJF25syZqxocAAAAAADXszQX7Q8++KD9+eefZma2detWu+uuuyw8PNzmzJljw4YNS9PfmjBhglWvXt1y5MhhkZGR1r59e9u4caPfPpJszJgxVrBgQcuaNas1bNjQ1q5dm9awAQAAAAAIOmku2v/880+rVKmSmZnNmTPH6tevb++99569+eab9vHHH6fpby1cuND69etnS5YssXnz5llSUpLFxsbaiRMnnH0mT55s06ZNsxkzZtjSpUstKirKmjVrZsePH09r6AAAAAAABJUb0voLkiwlJcXMzL777jtr06aNmZnFxMTYoUOH0vS3vv76a7/Hb7zxhkVGRtrvv/9u9evXN0k2ffp0e/zxx61jx45mZvbWW29ZgQIF7L333rMHH3wwreEDAAAAABA00nylvVq1avb000/b22+/bQsXLrTWrVubmdm2bdusQIEC/yiYY8eOmZlZnjx5nL+5f/9+i42NdfYJCwuzBg0a2OLFiy/4NxITEy0+Pt7vBwAAAACAYJTmon369On2xx9/WP/+/e3xxx+3kiVLmpnZRx99ZHXq1LniQCTZoEGDrG7dula+fHkzM9u/f7+Z2XmdAQUKFHCeO9eECRMsV65czk9MTMwVxwQAAAAAQCCleXh8xYoV/WaP95oyZYplzJjxigPp37+/rVq1yn766afznvN4PH6PJZ23zWvEiBE2aNAg53F8fDyFOwAAAAAgKKW5aL+YLFmyXPHvPvzwwzZ37lxbtGiRFS5c2NkeFRVlZqlX3KOjo53tBw8evOhQ/LCwMAsLC7viWAAAAAAAcIvLKtojIiIuemX7XIcPH77sF5dkDz/8sH3yySe2YMECK168uN/zxYsXt6ioKJs3b55VrlzZzMzOnDljCxcutEmTJl326wAAAAAAEIwuq2ifPn16urx4v3797L333rPPPvvMcuTI4dynnitXLsuaNat5PB4bMGCAjR8/3kqVKmWlSpWy8ePHW3h4uN1zzz3pEhMAAAAAAG5xWUV79+7d0+XFX3rpJTMza9iwod/2N954w3r06GFmZsOGDbNTp05Z37597ciRI1azZk379ttvLUeOHOkSEwAAAAAAbvGP7mk/deqUnT171m9bzpw5L/v3Jf3tPh6Px8aMGWNjxoxJa3gAAAAAAAS1NC/5duLECevfv79FRkZa9uzZLSIiwu8HAAAAAABcHWku2ocNG2bz58+3F1980cLCwuy1116zsWPHWsGCBW3WrFnpESMAAAAAANelNA+P//zzz23WrFnWsGFD69mzp9WrV89KlixpRYsWtXfffde6dOmSHnECAAAAAHDdSfOV9sOHDztLs+XMmdNZ4q1u3bq2aNGiqxsdAAAAAADXsTQX7SVKlLDt27ebmVnZsmXtww8/NLPUK/C5c+e+mrEBAAAAAHBdS3PRft9999nKlSvNzGzEiBHOve0DBw60oUOHXvUAAQAAAAC4XqX5nvaBAwc6/27UqJFt2LDBli1bZjfeeKPdcsstVzU4AAAAAACuZ2ku2rdv327FihVzHhcpUsSKFClyNWMCAAAAAAB2hfe0161b12bOnOlMQgcAAAAAAK6+NBfty5Yts9q1a9vTTz9tBQsWtHbt2tmcOXMsMTExPeIDAAAAAOC6leaivUqVKjZlyhTbuXOnffXVVxYZGWkPPvigRUZGWs+ePdMjRgAAAAAArktpLtq9PB6PNWrUyF599VX77rvvrESJEvbWW29dzdgAAAAAALiuXXHRvmvXLps8ebJVqlTJqlevbtmyZbMZM2ZczdgAAAAAALiupXn2+FdeecXeffdd+/nnn61MmTLWpUsX+/TTT/1mlAcAAAAAAP9cmov2p556yu666y57/vnnrVKlSukQEgAAAAAAMLuCon3nzp3m8XjSIxYAAAAAAODjsor2VatWWfny5S1Dhgy2evXqS+5bsWLFqxIYAACXUmz4F+n+Gtsntk731wBw5a7F94AZ3wUAAuuyivZKlSrZ/v37LTIy0ipVqmQej8ckOc97H3s8HktOTk63YAEAAAAAuJ5cVtG+bds2y58/v/NvAAAAAACQ/i6raC9atKiZmZ09e9bGjBljTzzxhJUoUSJdAwMAAAAA4HqXpnXaM2XKZJ988kl6xQIAAAAAAHykqWg3M+vQoYN9+umn6RAKAAAAAADwleYl30qWLGlPPfWULV682KpWrWrZsmXze/6RRx65asEBAAAAAHA9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k7M77rjD2Z6SkhLAqK7Mvn37nLZ88803koKvHXv27DmvDcFk5syZTvwPPvig/vrrr0CH9I999NFHatKkiUqXLq3ChQvr559/DnRIV4TcuFuo5YfcuFso5cfXm2++6eSqZ8+eSk5ODnRIV8XJkyedc9CGDRs625OSkgIY1ZVLz/M1rrS7gCSrXr269e/f35588kmLi4uzu+66K2iHYufLl8/atm1r0dHRNnToUJs2bZqdPHky0GFdsVDLj5ckS0lJsUqVKtlNN91k77zzjk2aNMlGjx4d6NCuiooVK1rNmjVt9OjRtnTpUuvQoYOZBecV96SkJKtSpYqVLFnSWrRoYZ9++mnQtSN79ux2yy23WGRkpLVv394+++yzQIeUJh07drTu3btb//797Z133rGBAwdaXFxcoMP6R0qUKGEZMmSw//znP1a/fn27/fbbbfHixYEOK83IjbuFWn7IjbuFUn581atXz9q2bWsjR460L774wrp06RJU5wAXkyVLFmvXrp01atTI9u/fb7fccouZBe8V9/Q8X6NodwGPx2NZsmSxfPny2ffff28//PCDeTwe69KlS9AVht4PWPPmza1jx442c+ZMGzt2rE2aNCloC/dQyo8vj8djGTJksP79+9uyZcssPj7e3nzzTRs/fryNHTs20OFdMe97sFKlSlamTBn74YcfbMaMGbZ48WJn+FWwFbz58uWzUqVKWfny5W3MmDHWsWNH++yzz4KmHZIsLCzM6tSpYx07drShQ4dap06dgqpwj4iIMEmWlJRkS5YssU8//dQGDRoUVCe4Z8+e9XtcuXJlK1WqlI0YMcJeffVVq1atmt15551Bd4JLbtwt2PNDbtwtlPNj9n/3RZcoUcKyZctmq1atss8//9y+++4769atW1CcA1yKx+Ox6tWr28qVK+3JJ5+08PBwq1y5spkFZ+GerudrV+V6PdJsz5492rRp03lDx2vUqKHXX39dcXFxKlOmjKpXrx6UQ7G3bdumPHny6JNPPtEnn3wij8ej0aNH68SJE4EO7bKEan7Wrl2rf/3rX1q3bp3f9qlTp+qhhx5SUlKSXnnlFWXMmFFjxowJUJRXZv/+/edtW7VqlVq3bq1FixZp7ty5ypUrlzp16uQ8H0zDy1avXq1ixYrp3Xff1eDBg+XxePTZZ59JCp6h8r/88oty5cqlb7/9ViNHjlTmzJmdNrjNX3/9pUOHDvm9R3bt2qWiRYtq0aJFWrJkibJmzapu3boFxZDSlStXql69epo0aZIWL17sbN+zZ4+aNWum3377TZLUpEkTFSpUyNVDSsmNe3MjhVZ+yI27hVp+fB08eFBJSUl+w8S3bdummjVr6rvvvtP333+vnDlzqmvXrkFzDnApPXr0UN++fbVw4UKVLl1aVapUcZ4LtqHy6XW+RtEeAH/++ac8Ho9uvvlmtWvXTj///LOOHDkiSXrqqad09913S0r9ci1durRq166ttWvXBjDiS1u/fr1mzJihJUuW+G2fMWOGcy/xa6+9Jo/HozFjxri+cA+1/HjFxcU599m0adNGt912m9auXauEhAStXr1aefLkcQ5wr776qrJkyaKhQ4cGOOrL481Zo0aNNG3aNK1evVqSdPz4ccXGxmrYsGGSpLlz5yoiIkJ33XVXIMP9Wxs3btT//vc/HT9+3NkWHx+v3r17a9q0aUpISFCfPn38DgRu64DYuHGjvv32W+3du9dv+yOPPKLRo0fr1KlT6tu3r8LCwlxXuK9du1Yej0fNmjVT//79FRcXp5MnT0qSHnroIY0YMUKS9MMPPyhr1qzq2bOnDh48GMiQLyk5OVm1atVy7hnMmTOnRowYoU8++USS1KlTJ/Xs2dPZv3Xr1sqSJct53+luQG7cmxsptPJDbtybGyn08uNrw4YN8ng8io2N1YgRI7Rr1y5J0unTp3XnnXdq1KhRkqRvv/1WOXPmVI8ePVx3DnAxq1evVrt27bRw4UJt2bLF2f6f//xHrVq1UmJiopYtW6ZSpUoFReF+Lc/XKNoDYOXKlfJ4PCpSpIjuuecexcTEqHv37nrllVe0Y8cORUREaO7cuZJSC60CBQqoUaNGOnPmTIAjP9+hQ4ecQrBGjRpq27atli5dqri4OK1fv1433XSTli9fLim1cM+cObOGDBni6sI9lPLjKyUlRcOGDZPH49GQIUN0zz33qEqVKmrfvr1+/PFH9e7dW127dtWpU6ckSS+88ILy5cvn+t72lJQUzZo1Sx6PR/ny5dPAgQOVM2dOjR8/XsuWLdOGDRt08803a/Xq1UpJSdH//vc/eTwede/ePdChX5DvJCYPPvighg4dqsTEREnShx9+qLx58+rAgQNKSEhQv379lDlzZs2ZMyfAUfvbu3ev04YOHTpo2LBhOnr0qFJSUvTRRx+pWLFiOnr0qCSpX79+yp49u6va8N5778nj8ahSpUqqW7euypUrp0GDBumXX37RggULlC9fPv3555+SpB9//FEej0d9+vRx9UnT3r17VbJkSdWvX18zZ87Uvffeq7p166p9+/aaMmWKcufOrZ9++snZ//bbb3fa6Cbkxr25kUIvP+TGvbmRQis/vt5//315PB7deOON6ty5s/Lnz6+nnnpKv/zyi1auXKnIyEjn4sT333/vnC+4XUJCgqpUqSKPx6NWrVqpUaNGzoTVycnJqlSpkjPKc8mSJSpXrpyKFy8e4Kgv7lqfr1G0X2PeL7/ffvvNKWA/+ugjvfTSSypcuLBuv/125ciRQz169FBCQoIk6fDhw9q8eXMgw76khx9+WB6PR0888YRatWqlli1bql69elq6dKnatGmjtm3b6vTp05Kkl19+WXny5HFtIRiK+TmXt6hdtGiRFi1apMmTJysqKkoFChRQkSJF/HLjLazcLj4+XjNnzlSmTJk0depUff/997rnnntUtmxZNWrUSMWKFdOrr74qSUpMTNRXX33l2tsatmzZojZt2sjj8WjkyJFq2bKlKlSooJEjR2rDhg3q2rWrpk6dKkk6cOCA7rvvPuXJk0fHjx93zRC5I0eOqFmzZs7omsqVK6tx48bq27evDhw4oKZNmzorFZw5c0Y9e/ZUgQIF/HqqA23GjBnyeDx66aWX9OKLL2rkyJEKDw/Xo48+qsyZM2vs2LHOwfmXX37R+vXrAxzx+RISEnTixAnt2bNHUuow2Hz58qlTp0769ddfdejQIfXs2VNNmjSRx+MJiitQErlxu2DPD7lxb26k0M6Pr5dfflkej0evvPKK3n77bT3yyCPKly+f7rnnHhUoUEDTp093jvkLFy507TmNr1OnTumDDz5Q0aJFVadOHX322WcqVaqUWrVqpYEDB2ratGlq3769jh07puTkZC1atEjVq1fX1q1bAx36BW3dulVt27a9ZudrFO3XkHdoh/e/ixYtUqZMmdS7d28dPnxYCQkJevHFF9W6dWu98847ktw35NWX71CVBx98UPnz59d///tfLV68WBMmTFCZMmV04403KioqSrt373b2dWshGGr58XVunA899JCyZ8+uTz/9VFJqoThz5kx9/PHHgQjvivm268SJE5o2bZo8Ho/eeOMNSdLmzZvVpUsXFStWTLNnzw5QlGm3c+dONW/eXMWKFdPevXv1wQcfqGfPnsqdO7fCw8PVoEEDZ2THwYMHL3g/f6DFx8erQYMGqlSpktauXasPP/xQ3bp1U3R0tCIjI1W3bl2nSE9MTAx4Gw4cOKBffvlF//vf/5yD6fjx45UpUyZNmzZNSUlJWrFihYYPH66bbrpJH374oST3ziewbt06tWnTRuXLl1fp0qWd3v2dO3cqMjJS9evX186dOyWlXq36448/AhnuJZEb9+ZGCq38kBv35kYKvfz4OnjwoDPqwXtRaNKkScqQIYNmzpyps2fPav369XrggQdUunRpJ1fBwNsRdPbsWUmpy/JlzZpVo0eP1tGjR/W///1PsbGxypIlizwejzMaIiUlxRn96VZbtmxR69atr8n5GkX7NXDuVeXk5GTnjbto0SJlzpxZd999t+Li4pzng4XvkPCePXsqR44cevfddyVJmzZt0vvvv++qYa8XEqr52bZtm7788ku/bd52SanDkrNkyaKPPvroWof2jyUkJDgnFb4nF6dOndKzzz4rj8ejZ599VlJqvvbt2xeQOC/X9u3bNWPGDI0fP97pSNm/f7/q1KmjG2+80bmfbcGCBerXr5/zGXOT7du367nnntPYsWP1/vvvS0rtSKlWrZrKlSunNWvWSJK+++47PfbYY65qw6pVq1SpUiXFxMQoU6ZMqlatmtOhMHHiRHk8Ho0fP955r7lpRMCFLF++XDly5NBDDz2kxx57TC1btvS7n2737t2KiopSvXr1/K7OuPFEndykcmNupNDKD7lxb26k0MuPr1WrVqly5coqXLiwsmfPrtq1azvnnFOmTFGGDBk0ZcoUSann3d7ngsGGDRvUq1cvNWjQQPfff78zd9IHH3ygsLAwPfroo86+n3/+uX744QdJ7s/buRME1q9fP93P1yja09mff/6pTJkyqVGjRvrwww/Pm7VbSr13KHPmzLr33nv9rki70ebNm/Xtt9/6bfMOfZekBx54QFmyZNG7777r2kkjfIVafrwOHTqk7Nmzy+Px6N5779Urr7zi9Nz66t+/v7JkyeIUisFg/fr1qly5sjp37qytW7fq8OHDfs/7XnGfPHmys92tB4CVK1eqcOHCqlWrlrJly6aCBQvqiSeekJQ67K9BgwYqVKiQMzzMO3GQm6xYsUIxMTGqVauW8ubNq2zZsunxxx+XlHoieOutt6p48eLO58tN3w0rVqxQeHi4hgwZop9++kmvv/66ChcurIYNGzr7TJ06VR6PRxMnTtSxY8cCGO3fW7dunTJnzqyJEyc627Zs2aLy5curTZs2zvtnz549ioqKUtOmTZ17I92G3Lg3N1Jo5YfcuDc3Uujlx9eKFSuULVs2DRo0SIsXL9Zzzz2nEiVKqEWLFs5V5unTp/tdjAgWK1asUEREhDp37qw2bdqoVKlSKleunFatWiVJmjNnjsLDw9WrV68AR3p59u3b58R+rm3btqlhw4bper5G0Z7Oli1bpooVK6pdu3bq0aOHChYsqGnTpp13j83ChQuVLVs2tW/f/rzZlt3i0KFDuuGGG+TxeNSvXz9NmjTpglede/furaxZs2r27NmuH9YSSvnxlZCQoN69e+vll1/WqFGj1LJlS8XExOg///mPli5d6rdvv3795PF49L///S9A0abNyy+/rCpVqqht27aqUaOGOnfurDlz5vgVgidOnNDUqVOVOXNmPf300wGM9tJWrVqlrFmzatSoUTp27Jg2btyo7t27KyYmxnkPbt26VY0bN1ahQoW0fft2Se4a7bFy5UqFh4dr5MiRSkhI0Pr16/Xoo48qZ86cmj9/vqTUW2Lq16+v4sWLu2qlhS1btihz5szO/fVS6v/b4cOHKzo62hlmKcl5P40ZM+a8pSDd4sSJE7rrrruUJUsWZ1Ze7+iaO+64Q507d5b0fyOk9u7dq8yZM+u2225zhi+6Bblxb26k0MoPuXFvbqTQy4+vzZs3Kzw83Fnhxuvhhx9W0aJF/UaCPv/88woLC3P1OY2vNWvWKGvWrH7xfvrpp8qbN6+eeeYZSak5++ijjxQeHq5+/foFKtTLsmfPHuXKlUt58+bV0KFD/SY49NqyZYsaNWqUbudrFO3pbP/+/erataszBPn1119XkyZN1LBhQ/Xq1UsrV650hrnMnz9fBQoUcCbXcKP+/ftr+PDhmjRpkurVq6fSpUtr2rRpzgzxXr1795bH43H90OtQy4+vwYMHKzY2VlLqlc2pU6fqrrvuUoECBTR+/Hi/L5zhw4e7csKZC1m8eLEqVqyo7du3a/ny5Ro7dqxy586t7t27a+rUqX5X1MeNG6c8efK4cijZzp07lT9/frVr185v+6JFi5Q1a1Z99913zrZt27apWbNmCg8P9zvhCrTdu3crOjparVu39tu+aNEi5ciRw29UzrFjx9S4cWNFRES44r2WkpKiF154QZGRkectbfjGG2+oRIkS2rNnj19n0NNPP62IiAgdOnToWod72ebOnet0aHnv59yyZYvCw8P14osvOvt5T3r37dunjRs3BiTWiwnV3Hz++edBnxsp9SR0xowZIZWfTz/9NCRyw2fH3fk518SJExUdHa3Ro0f73W765ptvqmTJktq9e7ffOc3EiRNde07j68iRIypXrpzKly9/3mjIGjVqaMiQIc7jpKQk/fe//5XH49GgQYOudaiXbd26dWratKneeustdezYUbGxsapbt65+//13v1G4O3bsUOPGjdPlfI2iPZ34fiFOnjxZxYsX14EDBySlTsjgXfLg1ltvVZMmTZz7vt18ZTolJUWjR492ejWl1HttevXqpZw5c2rSpEn6/PPPneeGDh3qipPzvxMq+fHyfsEnJyerWrVqeu6555znYmNjFR0drQoVKqhGjRqqVq1a0Awh89WnTx+1bdvWGdK3e/duFShQQB6PR5UrV9b48eO1YsUKSXLtwW3VqlWqUKGCbr/9dn311VfO9sWLFytnzpzn9eJu2bJFbdu2ddVyNb///rsaNWqk2NhYv7XWly5dqhw5cmjRokV++x89elStW7fWpk2brnWoF3To0CFNmzZN5cuXV9++fSWlznGRO3dujR071tnPt6fcre8nX19++aVatWqlunXr6quvvlLx4sX10EMPOc97vyPcdJvCueLi4kIiN+devfzmm2+CPjdSai6CPT8bN27UW2+95Tz++uuvQyY3U6dODercXEgofK+dKz4+XqNGjVLNmjWdQvbw4cPn5crXuUWwW40ZM8Yp0L3DxTdu3KiwsDBnMmevpKQkzZ071/U1Q6tWrZwRAevWrVP37t3VsGFD3Xrrrfrwww918OBBSalX5du0aXPVz9co2q+y3bt3O4Wd94vj5MmTatWqld58801J0n333aeiRYvqjz/+0Mcff6xOnTo5a/m5lffL/eTJkypevLgmTJjgPNe6dWtFRESoYcOGuvnmm1W1alVnogk38/bIhkJ+znXmzBklJSXpscceU58+fSRJ3bp1U4ECBbRjxw799ddf+vTTT9WkSZOg6I328h6UFy1apEaNGjlL7fXq1UvFihXTihUrNHToUNWoUUOFChW64H38brJkyRLV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0kSJiYmu/KzMnj1bZcuW1VNPPaWbb775vCvuR48eDYrcxMfHq0yZMipVqpSk1O/bO+64QzVr1lTGjBnVpEkTvfDCC87+33zzjetz8/rrr6tZs2aSUtszbtw49ezZU927d3cKJu8VdzfnZ8uWLcqVK5ffe+s///mPihYt6tzS59tJ/O2337o6NydOnFDLli39Rmc999xzzvJZviOJ3J4br6+++kp16tTxWzN+/vz5zhK2vmt6B8Nnx+vMmTPq1KmT7r//fmfbzz//rJ49eypz5szO8dJ7xf3WW29Vv379guJK++bNm505he6//36/Sdl8458wYYIqV66sYcOGnTcq1E3WrFmjBg0aOKMDpk2bpuTkZCUnJyslJUUJCQlq0aKFSpQo4cyddK1QtP9Da9euVcaMGZ1e2+PHj6tz587q0KGDEhIS/Aqk+fPnKzw83NVLax04cEBlypRxDmp79+71u9/ba+7cuSpfvryzLJWbhyY99dRT6tq1q9+2/v37y+PxqESJEtq3b5+k1AO02/Pja8OGDbrpppv8JpI7duyY3n//feXJk8fv4NCyZUvVrVs3KA4AcXFxatGihbp166Z69eqpcOHC+v777y/4Hps1a5ayZ89+0XUz3WDv3r2qXLmy320z+/btU0REhP773/9K+r8Oh19//VWNGzdWvXr1NG/evIDEeyFxcXFq3ry57rzzTjVp0kRVqlTRRx99dMHC/eeff9Ydd9yhSpUq6csvvwxUyI6zZ89q0qRJ6ty5s/766y8n1i1btqhQoUJOx503B0eOHNG4ceNUp04d7d+/P2BxX0pCQoI+++wzvytMzzzzjDwej5o3b65bb71V+fLlc4bwSanHHzetPHCuzZs3q1u3bjp48KDGjh2rsmXL6umnn1bfvn319ttvS0rtVHF7bk6dOqVXXnlF4eHhGj58uNq0aaPY2Fh9+OGH+vDDD9WnTx8VK1ZMs2bNcn7H7bkZO3as7r77bklSzZo1FRsbqzvuuENNmzZVxowZnZE1f/31l6vz8/PPP593LiNJ5cqV0x133OE8Pve8za25OXTokMqVK6d3333X2TZs2DCVLFlS1apV00033eR03rk9N16fffaZoqKi/Iqh33//Xc2bN9fUqVOVJ08evw5JN+fH18mTJ1WlShU99dRTftt37NihXr16KU+ePFq4cKGk1I6/AQMGqGnT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zx7Vr19fLVu2VN++fdW7d29FREQE/MT8Qi4nP75F3sqVK3XvvfcqIiLC1V+Ux44dU4cOHXT33Xerb9++qlKlisaMGXPBwv3333/XzJkz1blzZ40YMcKVV9l87d69W9WrV1eZMmVUrFgxjRw58pKF+6lTp9SnTx+FhYU59726SVrb065dO2XPnt1VV3h37tzpN5pD+r/P0Y4dOzR48GC/UQSnTp3Sgw8+qPDw8IDnZPXq1cqZM6ffjPzek4rt27crOjra73aEQ4cOKW/evIqIiHBlB9Dx48dVsGBBeTweZ+Zur61btyoqKkoPPPCA34nG0qVLNWnSJNeN2Lic3IwePVpScCxJeSW5WbJkicaMGePKUWppyY+U+t2wcOFCrV692nXnN1eSGze/5y4nN2PGjJGU2o6PPvpIjz/+uJ577jlXXjC6kvycOnVKixcvduXkZb7Onj2r4cOHq3379vrss8/UpEkTNW/e3K9w971daf369frwww/1v//975LnCm7wdyvIXGgE1fDhw+XxeFxZM1xOB4T3HOi5555T586dNWnSJIWFhQWsYJco2i/Lrl275PF45PF4nOG43hPZrVu3qnDhwurTp4/f73gTf+4skW6Qll7Or776Sr1791b16tXVvXv3gJ+YX0ha85OSkqJt27Zp9OjRrmyPrxMnTmjcuHH66KOPJKUu53Khwj0Yff7552rVqpX+/PNP/etf/1KhQoUuWeju379fd911l7Mqg9tcbnu897U2bdpUHo/HNZ0r27Ztk8fjUcWKFZ3JcLwHsm3btikqKuq8mVITEhLUvn17Z+nHQElJSVGfPn3k8XiUMWNGzZ4923lu9+7dypw5s/r06XPe/azDhw935ZU1KfU7rE+fPmrUqJGio6PVvHlz57nJkyerS5cuF7wy4Lb5U64kN253pblx461KoZafK82NG6UlN27uePAVSvm5kF9//VVvvfWWpNT1vhs3bnxe4e7G74G/czkryPjmLTExUf/9739deQtjWjsg/v3vf8vj8SgiIiLgF1go2i/Dhg0bVKpUKRUqVEjh4eFO0k6dOqWyZcuqa9euQfMlc7m9nL6FYGJiopKSklw7odGV5idYDnLx8fF+8Q8dOlRVqlTR6NGjnfufTp8+HXQHgn379jnLuknS9OnTnULX9x483wn43PoelC6/Pd7P1v79+111X+tnn32m7Nmzq0yZMqpatapzT+GxY8dUpEgR3XfffZe8fy3QPvroI9WtW1cPPPCAPB6P0/F47NgxjR071u/zHiyf/ZkzZ6pw4cL68MMPVbJkSWcGWym4TvzSkptgESq5kUIvP+TGPROBXkgo5edc5/5/X7ly5QWvuHvXZA8Wl7uCjG+nsVvfg5fTAeFrzpw58ng8rrjIR9F+GY4eParWrVurR48e6t+/v99w1p07dwbVAS0tvZwXm5XYbUIpP5fi+2U4ZMgQ54r7vn379Oijj+r22293fa7+zvPPP+8Uut51yp9//nlX9tZejou1x43Dsbdt26abbrpJPXv2VOfOnVW1alVnsqZffvnFde+tc1dTOHLkiG688UYNGTJEEydOlMfjcWZNdlvsadG2bVs99thj+vLLL5U/f36/E1y3XVX3IjfuzY10feSH3LhbsOYnLbz5WbFihVO4f/zxx3rkkUeUO3duHTp0KChz+HcryIwaNSrAEV7a5XZA+Bbw3pGtgUbRfgm+H6bvvvtOMTEx+uSTT9S1a1dlzZrVmSzHLVeaLleo9HKGan4uxTc/Q4cOVfXq1VW+fHlly5bNlRNpXS7fjhXfoeVdunRRjhw5XHeP7t8Jpvb4fj5efPFFVa9eXW+++aaaNGmiatWqOYW72z5HvsvleL3//vu67bbbtHz5cg0ePFgej0fvv/++JLl+COmaNWvUq1cvrVixwrlPOCkpybmfLiUlRd98843y58+v1q1bO7/ntrxI5MbLjbmRQis/5Ma9uZFCLz9/59y5ErztWLlypZo1a6bcuXMrR44cAb0v+mpw+woyl+vvOiBGjhwpyT2j8yjaL2Dz5s369ddf/YbiHj58WF27dtVrr72mgwcPqmPHjgoPD3c+eMH2BRPMvZyhnJ+0rE6QlJSkm266SREREUFxJfrvluHyfd9Nnz5dHo9HuXLlcu1EgcHenl27dp23NN3y5cvVunVr/fTTT/r555916623qnr16tq7d68k93w3rF27Vnnz5tWTTz7pzGotpZ4YVapUyRl6OGjQIHk8Hn3wwQeS3HtlKj4+XjfeeKM8Ho86dOigpk2bavbs2Tp79qzi4+MVExOjl19+WZL09ddfq1ChQqpbt26Ao74wcuPe3EihlR9y497cSKGXnwu5nCUFvfnp3LmzcufO7cqZ1M8VrCvIXIlg6oCgaD/H7t27nUnNHnnkEU2cONF57l//+peKFy+uxMREHThwQJ06dVKuXLkCPgnTpYRaL2eo5ccrrasTJCYm6v7771eWLFlcfwA4fvy48+9zl6vZt2+f/vvf//oNCUxKStLAgQMVERHhmknafIVCe7Zt26ZMmTIpT548mj59ujMzsST17NlTzZo1kyTNnz9fDRo0UO3atS+59vy1dPbsWT366KPOpHl33HGHqlSpooULF+rkyZP697//rZo1ayohIUFHjhxxZrD1TuboRgkJCXrppZeUN29etWrVSm+88YYKFSqk9u3ba+LEiZo0aZK6dOmiU6dO6ezZs/r8889VunRp55YLtyA37s2NFHr5ITfuzY0UWvnxdSVLCj7++OOunUndVzCvIHMxodQBQdF+jjVr1qh27dryeDwaM2aMqlatqrp162ry5Mnas2eP2rZtq9dee01S6hu4RYsWKliwoE6fPu263s5Q7OUMpfx4XcnqBJI0cOBA13dIrFu3ToULF9Ybb7zhbPNdriZ//vx+HS9S6gQtGTNmPG+tZjcIhfakpKTo448/VrFixZQpUyYNGjRIFSpUUMeOHTVv3jytXr1abdu21eLFiyVJ3377rSpVqqTGjRv7LTEYSFu3blW3bt2UNWtWLViwQEOGDFFsbKwqVqyoAQMGqHLlys68FgcPHtTo0aNd02Hiy/s59/4/femll5QhQwa9+uqr2rdvn9555x1Vq1ZNmTNnVpYsWZyT2aSkpAsOo3UDcuPe3EihkR9y497cSKGbH+nKlqyTUs/z3H6BJZhXkLmQUOyAoGg/R0pKilatWqVatWqpWrVqOnTokF588UV17NhRefPmVc6cOdWhQwfnA7lr1y5nlmW3OXnypGbOnBlSvZyhlB+vtM5+74ai6XIkJSXpoYceksfjUaFChZwOIknas2ePsmbNqoceeuiC7fnrr7+uZaiXJZTak5CQoA8++EA333yz2rVrp/379+uBBx5QixYtVLhwYUVEROiJJ55w9p8/f77fDPiBEB8frz179jhD9f/66y/FxsaqSJEi2rFjh/766y+99tpruummm5QhQwZ9++23zu+65X40X3/++acefvhhtWnTRoMHD3bWVPYuLzN58mRn33fffVdff/11oEL9W+TGvbmRQis/5Ma9uZFCLz/nupIl69w6cvVcwb6CjK9Q64Dwomj34fsFuHr1at18882q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c1AWiUlJSXrttdeCrrPrYpYuXSqPx6O1a9cGOpR/bM2aNbrzzjtDoi2haOXKlWrdurXfRYpgdqHbz4JZSkqKqy9GpEVcXJz2798fFJNqSlKTJk00ZswYSXLuvR82bJg6duyoihUr6qWXXpLk/iUFfXlXwbn//vudbTt27NBDDz2kKlWqOO2VUq+4u20FmXNd7HzNt3B36xK26em6Ktolem3dLth6baVLHwAqVaqkF198UceOHVPRokWDZvb7vzsA+LajS5cuqlWrlqvvzdu/f79uvfVWNWnSRPny5dMdd9zhdyDw7UTp2rWra9tzuUujJSUlKS4uTrly5VLGjBld3fH1d4KhgystQuVEXQq+yTOvN8E0YSOQ3lJSUnTixAnVq1dPXbt2dW6L3b17t4oWLarXX39d9957r+rVq+f8TjAsWefVrFkz9ezZU9L/HTf37t2rAQMGqFatWnrzzTedfcePH++6FWS8/u58zbdwd+MStunpuivavei1da9g6bW93ANA/fr1lZSUFFSz30t/fwB46623JEnff/+9YmJinIlc3Oirr75Sly5d9Ntvv+nXX39Vnjx5zjsQeL8Pvv/+exUqVMi17bmcpdHOnj2rQ4cO6euvvw6ZK20AAPxTP/30kzJkyKD69eura9euypYtm3r16iUpddm97NmzB9VxMykpSWfOnNF9992nDh06OCteeM9pduzYoZYtW6pt27ZBsYLM5Zyv+RbublzCNr1ct0U7cLX83QEgW7ZsOnDggOtnv/e63APAbbfdJim1iGzcuLFrJmm7kIMHD/otffbLL784BwLftc2l1NESLVq0cHV7pEsvjTZw4EB16NAhaDqIAAC4Vn777Tfde++96tWrl1544QVn+2effaabb775vPMCNzp3/qoFCxYoY8aMev75551t3vO23377TR6PR8uXL/dbYcaNLvd8zXekl1s7IK42inbgKrjUAeCmm24Kii+UKzkAeO+TduOolYtNyOiNdcmSJX49uGfOnNG//vUv/fbbb649mJ3rYkujZcyYMWTuAwcA4Gq70HF+yJAhatiw4UXnvXGLC60gI6Uu7ZYhQwa9+uqrftvXrVuncuXKaePGjdcyzMt2JedrL774or766itJ7u2AuNpuMAD/WPXq1W3WrFnm8Xj8tv/4448WFRVlN9zg7o/an3/+aZ9//rndc889Fh0d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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# data are held in the Github repo and loaded from there.\n", - "NSPP_PATH = './data/ed_arrivals.csv'\n", - "\n", - "# visualise\n", - "ax = pd.read_csv(NSPP_PATH).plot(y='arrival_rate', x='period', rot=45,\n", - " kind='bar',figsize=(12,5), legend=False)\n", - "ax.set_xlabel('hour of day')\n", - "ax.set_ylabel('mean arrivals');" - ] - }, - { - "cell_type": "markdown", - "id": "a6887d7c", - "metadata": {}, - "source": [ - "### 2.3 Resource counts\n", - "\n", - "> Inter count variables representing the number of resources at each activity in the processes." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "2b07a706", - "metadata": {}, - "outputs": [], - "source": [ - "DEFAULT_N_TRIAGE = 1\n", - "DEFAULT_N_REG = 1\n", - "DEFAULT_N_EXAM = 3\n", - "DEFAULT_N_TRAUMA = 2\n", - "\n", - "# Non-trauma cubicles\n", - "DEFAULT_N_CUBICLES_1 = 1\n", - "\n", - "# trauma pathway cubicles\n", - "DEFAULT_N_CUBICLES_2 = 1" - ] - }, - { - "cell_type": "markdown", - "id": "85fff265", - "metadata": {}, - "source": [ - "### 2.4 Simulation model run settings" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "c683fd56", - "metadata": {}, - "outputs": [], - "source": [ - "# default random number SET\n", - "DEFAULT_RNG_SET = None\n", - "N_STREAMS = 20\n", - "\n", - "# default results collection period\n", - "DEFAULT_RESULTS_COLLECTION_PERIOD = 60 * 19\n", - "\n", - "# number of replications.\n", - "DEFAULT_N_REPS = 5\n", - "\n", - "# Show the a trace of simulated events\n", - "# not recommended when running multiple replications\n", - "TRACE = True" - ] - }, - { - "cell_type": "markdown", - "id": "8b7796c3", - "metadata": {}, - "source": [ - "## 3. Utility functions" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "52ecb3e8", - "metadata": {}, - "outputs": [], - "source": [ - "def trace(msg):\n", - " '''\n", - " Utility function for printing a trace as the\n", - " simulation model executes.\n", - " Set the TRACE constant to False, to turn tracing off.\n", - " \n", - " Params:\n", - " -------\n", - " msg: str\n", - " string to print to screen.\n", - " '''\n", - " if TRACE:\n", - " print(msg)" - ] - }, - { - "cell_type": "markdown", - "id": "4db5f62c", - "metadata": {}, - "source": [ - "## 4. Distribution classes\n", - "\n", - "To help with controlling sampling `numpy` distributions are packaged up into classes that allow easy control of random numbers.\n", - "\n", - "**Distributions included:**\n", - "* Exponential\n", - "* Log Normal\n", - "* Bernoulli\n", - "* Normal\n", - "* Uniform" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "23e4f780", - "metadata": {}, - "outputs": [], - "source": [ - "class Exponential:\n", - " '''\n", - " Convenience class for the exponential distribution.\n", - " packages up distribution parameters, seed and random generator.\n", - " '''\n", - " def __init__(self, mean, random_seed=None):\n", - " '''\n", - " Constructor\n", - " \n", - " Params:\n", - " ------\n", - " mean: float\n", - " The mean of the exponential distribution\n", - " \n", - " random_seed: int, optional (default=None)\n", - " A random seed to reproduce samples. If set to none then a unique\n", - " sample is created.\n", - " '''\n", - " self.rng = np.random.default_rng(seed=random_seed)\n", - " self.mean = mean\n", - " \n", - " def sample(self, size=None):\n", - " '''\n", - " Generate a sample from the exponential distribution\n", - " \n", - " Params:\n", - " -------\n", - " size: int, optional (default=None)\n", - " the number of samples to return. If size=None then a single\n", - " sample is returned.\n", - " '''\n", - " return self.rng.exponential(self.mean, size=size)\n", - "\n", - " \n", - "class Bernoulli:\n", - " '''\n", - " Convenience class for the Bernoulli distribution.\n", - " packages up distribution parameters, seed and random generator.\n", - " '''\n", - " def __init__(self, p, random_seed=None):\n", - " '''\n", - " Constructor\n", - " \n", - " Params:\n", - " ------\n", - " p: float\n", - " probability of drawing a 1\n", - " \n", - " random_seed: int, optional (default=None)\n", - " A random seed to reproduce samples. If set to none then a unique\n", - " sample is created.\n", - " '''\n", - " self.rng = np.random.default_rng(seed=random_seed)\n", - " self.p = p\n", - " \n", - " def sample(self, size=None):\n", - " '''\n", - " Generate a sample from the exponential distribution\n", - " \n", - " Params:\n", - " -------\n", - " size: int, optional (default=None)\n", - " the number of samples to return. If size=None then a single\n", - " sample is returned.\n", - " '''\n", - " return self.rng.binomial(n=1, p=self.p, size=size)\n", - "\n", - "class Lognormal:\n", - " \"\"\"\n", - " Encapsulates a lognormal distirbution\n", - " \"\"\"\n", - " def __init__(self, mean, stdev, random_seed=None):\n", - " \"\"\"\n", - " Params:\n", - " -------\n", - " mean: float\n", - " mean of the lognormal distribution\n", - " \n", - " stdev: float\n", - " standard dev of the lognormal distribution\n", - " \n", - " random_seed: int, optional (default=None)\n", - " Random seed to control sampling\n", - " \"\"\"\n", - " self.rng = np.random.default_rng(seed=random_seed)\n", - " mu, sigma = self.normal_moments_from_lognormal(mean, stdev**2)\n", - " self.mu = mu\n", - " self.sigma = sigma\n", - " \n", - " def normal_moments_from_lognormal(self, m, v):\n", - " '''\n", - " Returns mu and sigma of normal distribution\n", - " underlying a lognormal with mean m and variance v\n", - " source: https://blogs.sas.com/content/iml/2014/06/04/simulate-lognormal\n", - " -data-with-specified-mean-and-variance.html\n", - "\n", - " Params:\n", - " -------\n", - " m: float\n", - " mean of lognormal distribution\n", - " v: float\n", - " variance of lognormal distribution\n", - " \n", - " Returns:\n", - " -------\n", - " (float, float)\n", - " '''\n", - " phi = math.sqrt(v + m**2)\n", - " mu = math.log(m**2/phi)\n", - " sigma = math.sqrt(math.log(phi**2/m**2))\n", - " return mu, sigma\n", - " \n", - " def sample(self):\n", - " \"\"\"\n", - " Sample from the normal distribution\n", - " \"\"\"\n", - " return self.rng.lognormal(self.mu, self.sigma)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "fc9813dc", - "metadata": {}, - "outputs": [], - "source": [ - "class Normal:\n", - " '''\n", - " Convenience class for the normal distribution.\n", - " packages up distribution parameters, seed and random generator.\n", - " '''\n", - " def __init__(self, mean, sigma, random_seed=None):\n", - " '''\n", - " Constructor\n", - " \n", - " Params:\n", - " ------\n", - " mean: float\n", - " The mean of the normal distribution\n", - " \n", - " sigma: float\n", - " The stdev of the normal distribution\n", - " \n", - " random_seed: int, optional (default=None)\n", - " A random seed to reproduce samples. If set to none then a unique\n", - " sample is created.\n", - " '''\n", - " self.rng = np.random.default_rng(seed=random_seed)\n", - " self.mean = mean\n", - " self.sigma = sigma\n", - " \n", - " def sample(self, size=None):\n", - " '''\n", - " Generate a sample from the normal distribution\n", - " \n", - " Params:\n", - " -------\n", - " size: int, optional (default=None)\n", - " the number of samples to return. If size=None then a single\n", - " sample is returned.\n", - " '''\n", - " return self.rng.normal(self.mean, self.sigma, size=size)\n", - "\n", - " \n", - "class Uniform():\n", - " '''\n", - " Convenience class for the Uniform distribution.\n", - " packages up distribution parameters, seed and random generator.\n", - " '''\n", - " def __init__(self, low, high, random_seed=None):\n", - " '''\n", - " Constructor\n", - " \n", - " Params:\n", - " ------\n", - " low: float\n", - " lower range of the uniform\n", - " \n", - " high: float\n", - " upper range of the uniform\n", - " \n", - " random_seed: int, optional (default=None)\n", - " A random seed to reproduce samples. If set to none then a unique\n", - " sample is created.\n", - " '''\n", - " self.rand = np.random.default_rng(seed=random_seed)\n", - " self.low = low\n", - " self.high = high\n", - " \n", - " def sample(self, size=None):\n", - " '''\n", - " Generate a sample from the uniform distribution\n", - " \n", - " Params:\n", - " -------\n", - " size: int, optional (default=None)\n", - " the number of samples to return. If size=None then a single\n", - " sample is returned.\n", - " '''\n", - " return self.rand.uniform(low=self.low, high=self.high, size=size)" - ] - }, - { - "cell_type": "markdown", - "id": "58f4c1a8", - "metadata": {}, - "source": [ - "## 5. Model parameterisation\n", - "\n", - "For convienience a container class is used to hold the large number of model parameters. The `Scenario` class includes defaults these can easily be changed and at runtime to experiments with different designs." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "c62e1a7a", - "metadata": {}, - "outputs": [], - "source": [ - "class Scenario:\n", - " '''\n", - " Container class for scenario parameters/arguments\n", - " \n", - " Passed to a model and its process classes\n", - " '''\n", - " def __init__(self, random_number_set=DEFAULT_RNG_SET,\n", - " n_triage=DEFAULT_N_TRIAGE,\n", - " n_reg=DEFAULT_N_REG,\n", - " n_exam=DEFAULT_N_EXAM,\n", - " n_trauma=DEFAULT_N_TRAUMA,\n", - " n_cubicles_1=DEFAULT_N_CUBICLES_1,\n", - " n_cubicles_2=DEFAULT_N_CUBICLES_2,\n", - " triage_mean=DEFAULT_TRIAGE_MEAN,\n", - " reg_mean=DEFAULT_REG_MEAN,\n", - " reg_var=DEFAULT_REG_VAR,\n", - " exam_mean=DEFAULT_EXAM_MEAN,\n", - " exam_var=DEFAULT_EXAM_VAR,\n", - " trauma_mean=DEFAULT_TRAUMA_MEAN,\n", - " trauma_treat_mean=DEFAULT_TRAUMA_TREAT_MEAN,\n", - " trauma_treat_var=DEFAULT_TRAUMA_TREAT_VAR,\n", - " non_trauma_treat_mean=DEFAULT_NON_TRAUMA_TREAT_MEAN,\n", - " non_trauma_treat_var=DEFAULT_NON_TRAUMA_TREAT_VAR,\n", - " non_trauma_treat_p=DEFAULT_NON_TRAUMA_TREAT_P,\n", - " prob_trauma=DEFAULT_PROB_TRAUMA):\n", - " '''\n", - " Create a scenario to parameterise the simulation model\n", - " \n", - " Parameters:\n", - " -----------\n", - " random_number_set: int, optional (default=DEFAULT_RNG_SET)\n", - " Set to control the initial seeds of each stream of pseudo\n", - " random numbers used in the model.\n", - " \n", - " n_triage: int\n", - " The number of triage cubicles\n", - " \n", - " n_reg: int\n", - " The number of registration clerks\n", - " \n", - " n_exam: int\n", - " The number of examination rooms\n", - " \n", - " n_trauma: int\n", - " The number of trauma bays for stablisation\n", - " \n", - " n_cubicles_1: int\n", - " The number of non-trauma treatment cubicles\n", - " \n", - " n_cubicles_2: int\n", - " The number of trauma treatment cubicles\n", - " \n", - " triage_mean: float\n", - " Mean duration of the triage distribution (Exponential)\n", - " \n", - " reg_mean: float\n", - " Mean duration of the registration distribution (Lognormal)\n", - " \n", - " reg_var: float\n", - " Variance of the registration distribution (Lognormal)\n", - " \n", - " exam_mean: float\n", - " Mean of the examination distribution (Normal)\n", - " \n", - " exam_var: float\n", - " Variance of the examination distribution (Normal)\n", - " \n", - " trauma_mean: float\n", - " Mean of the trauma stabilisation distribution (Exponential)\n", - " \n", - " trauma_treat_mean: float\n", - " Mean of the trauma cubicle treatment distribution (Lognormal)\n", - " \n", - " trauma_treat_var: float\n", - " Variance of the trauma cubicle treatment distribution (Lognormal)\n", - " \n", - " non_trauma_treat_mean: float\n", - " Mean of the non trauma treatment distribution\n", - " \n", - " non_trauma_treat_var: float\n", - " Variance of the non trauma treatment distribution\n", - " \n", - " non_trauma_treat_p: float\n", - " Probability non trauma patient requires treatment\n", - " \n", - " prob_trauma: float\n", - " probability that a new arrival is a trauma patient.\n", - " '''\n", - " # sampling\n", - " self.random_number_set = random_number_set\n", - " \n", - " # store parameters for sampling\n", - " self.triage_mean = triage_mean\n", - " self.reg_mean = reg_mean\n", - " self.reg_var = reg_var\n", - " self.exam_mean= exam_mean\n", - " self.exam_var = exam_var\n", - " self.trauma_mean = trauma_mean\n", - " self.trauma_treat_mean = trauma_treat_mean\n", - " self.trauma_treat_var = trauma_treat_var\n", - " self.non_trauma_treat_mean = non_trauma_treat_mean\n", - " self.non_trauma_treat_var = non_trauma_treat_var\n", - " self.non_trauma_treat_p = non_trauma_treat_p\n", - " self.prob_trauma = prob_trauma\n", - " \n", - " self.init_sampling()\n", - " \n", - " # count of each type of resource\n", - " self.init_resourse_counts(n_triage, n_reg, n_exam, n_trauma,\n", - " n_cubicles_1, n_cubicles_2)\n", - " \n", - " def set_random_no_set(self, random_number_set):\n", - " '''\n", - " Controls the random sampling \n", - " Parameters:\n", - " ----------\n", - " random_number_set: int\n", - " Used to control the set of psuedo random numbers\n", - " used by the distributions in the simulation.\n", - " '''\n", - " self.random_number_set = random_number_set\n", - " self.init_sampling()\n", - "\n", - " def init_resourse_counts(self, n_triage, n_reg, n_exam, n_trauma,\n", - " n_cubicles_1, n_cubicles_2):\n", - " '''\n", - " Init the counts of resources to default values...\n", - " '''\n", - " self.n_triage = n_triage\n", - " self.n_reg = n_reg\n", - " self.n_exam = n_exam\n", - " self.n_trauma = n_trauma\n", - " \n", - " # non-trauma (1), trauma (2) treatment cubicles\n", - " self.n_cubicles_1 = n_cubicles_1\n", - " self.n_cubicles_2 = n_cubicles_2\n", - "\n", - " def init_sampling(self):\n", - " '''\n", - " Create the distributions used by the model and initialise \n", - " the random seeds of each.\n", - " ''' \n", - " # MODIFICATION. Better method for producing n non-overlapping streams\n", - " seed_sequence = np.random.SeedSequence(self.random_number_set)\n", - " \n", - " # Generate n high quality child seeds\n", - " self.seeds = seed_sequence.spawn(N_STREAMS)\n", - " \n", - " # create distributions\n", - " \n", - " # Triage duration\n", - " self.triage_dist = Exponential(self.triage_mean, \n", - " random_seed=self.seeds[0])\n", - " \n", - " # Registration duration (non-trauma only)\n", - " self.reg_dist = Lognormal(self.reg_mean, \n", - " np.sqrt(self.reg_var),\n", - " random_seed=self.seeds[1])\n", - " \n", - " # Evaluation (non-trauma only)\n", - " self.exam_dist = Normal(self.exam_mean,\n", - " np.sqrt(self.exam_var),\n", - " random_seed=self.seeds[2])\n", - " \n", - " # Trauma/stablisation duration (trauma only)\n", - " self.trauma_dist = Exponential(self.trauma_mean, \n", - " random_seed=self.seeds[3])\n", - " \n", - " # Non-trauma treatment\n", - " self.nt_treat_dist = Lognormal(self.non_trauma_treat_mean, \n", - " np.sqrt(self.non_trauma_treat_var),\n", - " random_seed=self.seeds[4])\n", - " \n", - " # treatment of trauma patients\n", - " self.treat_dist = Lognormal(self.trauma_treat_mean, \n", - " np.sqrt(self.trauma_treat_var),\n", - " random_seed=self.seeds[5])\n", - " \n", - " # probability of non-trauma patient requiring treatment\n", - " self.nt_p_treat_dist = Bernoulli(self.non_trauma_treat_p, \n", - " random_seed=self.seeds[6])\n", - " \n", - " \n", - " # probability of non-trauma versus trauma patient\n", - " self.p_trauma_dist = Bernoulli(self.prob_trauma, \n", - " random_seed=self.seeds[7])\n", - " \n", - " # init sampling for non-stationary poisson process\n", - " self.init_nspp()\n", - " \n", - " def init_nspp(self):\n", - " \n", - " # read arrival profile\n", - " self.arrivals = pd.read_csv(NSPP_PATH)\n", - " self.arrivals['mean_iat'] = 60 / self.arrivals['arrival_rate']\n", - " \n", - " # maximum arrival rate (smallest time between arrivals)\n", - " self.lambda_max = self.arrivals['arrival_rate'].max()\n", - " \n", - " # thinning exponential\n", - " self.arrival_dist = Exponential(60.0 / self.lambda_max,\n", - " random_seed=self.seeds[8])\n", - " \n", - " # thinning uniform rng\n", - " self.thinning_rng = Uniform(low=0.0, high=1.0, \n", - " random_seed=self.seeds[9])" - ] - }, - { - "cell_type": "markdown", - "id": "2b88ade3", - "metadata": {}, - "source": [ - "## 6. Patient Pathways Process Logic\n", - "\n", - "`simpy` uses a process based worldview. We can easily create whatever logic - simple or complex for the model. Here the process logic for trauma and non-trauma patients is seperated into two classes `TraumaPathway` and `NonTraumaPathway`. " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "3c2abc80", - "metadata": {}, - "outputs": [], - "source": [ - "class TraumaPathway:\n", - " '''\n", - " Encapsulates the process a patient with severe injuries or illness.\n", - " \n", - " These patients are signed into the ED and triaged as having severe injuries\n", - " or illness.\n", - " \n", - " Patients are stabilised in resus (trauma) and then sent to Treatment. \n", - " Following treatment they are discharged.\n", - " '''\n", - " def __init__(self, identifier, env, args):\n", - " '''\n", - " Constructor method\n", - " \n", - " Params:\n", - " -----\n", - " identifier: int\n", - " a numeric identifier for the patient.\n", - " \n", - " env: simpy.Environment\n", - " the simulation environment\n", - " \n", - " args: Scenario\n", - " Container class for the simulation parameters\n", - " \n", - " '''\n", - " self.identifier = identifier\n", - " self.env = env\n", - " self.args = args\n", - " \n", - " # metrics\n", - " self.arrival = -np.inf\n", - " self.wait_triage = -np.inf\n", - " self.wait_trauma = -np.inf\n", - " self.wait_treat = -np.inf\n", - " self.total_time = -np.inf\n", - " \n", - " self.triage_duration = -np.inf\n", - " self.trauma_duration = -np.inf\n", - " self.treat_duration = -np.inf\n", - " \n", - " def execute(self):\n", - " '''\n", - " simulates the major treatment process for a patient\n", - " \n", - " 1. request and wait for sign-in/triage\n", - " 2. trauma\n", - " 3. treatment\n", - " '''\n", - " # record the time of arrival and entered the triage queue\n", - " self.arrival = self.env.now\n", - "\n", - " # request sign-in/triage \n", - " with self.args.triage.request() as req:\n", - " yield req\n", - " # record the waiting time for triage\n", - " self.wait_triage = self.env.now - self.arrival \n", - " \n", - " trace(f'patient {self.identifier} triaged to trauma '\n", - " f'{self.env.now:.3f}')\n", - " \n", - " # sample triage duration.\n", - " self.triage_duration = self.args.triage_dist.sample()\n", - " yield self.env.timeout(self.triage_duration)\n", - " self.triage_complete()\n", - " \n", - " # record the time that entered the trauma queue\n", - " start_wait = self.env.now\n", - " \n", - " # request trauma room \n", - " with self.args.trauma.request() as req:\n", - " yield req\n", - " \n", - " # record the waiting time for trauma\n", - " self.wait_trauma = self.env.now - start_wait\n", - " \n", - " # sample stablisation duration.\n", - " self.trauma_duration = self.args.trauma_dist.sample()\n", - " yield self.env.timeout(self.trauma_duration)\n", - " \n", - " self.trauma_complete()\n", - " \n", - " # record the time that entered the treatment queue\n", - " start_wait = self.env.now\n", - " \n", - " # request treatment cubicle \n", - " with self.args.cubicle_2.request() as req:\n", - " yield req\n", - " \n", - " # record the waiting time for trauma\n", - " self.wait_treat = self.env.now - start_wait\n", - " trace(f'treatment of patient {self.identifier} at '\n", - " f'{self.env.now:.3f}')\n", - " \n", - " # sample treatment duration.\n", - " self.treat_duration = self.args.treat_dist.sample()\n", - " yield self.env.timeout(self.treat_duration)\n", - " \n", - " self.treatment_complete()\n", - " \n", - " # total time in system\n", - " self.total_time = self.env.now - self.arrival \n", - " \n", - " def triage_complete(self):\n", - " '''\n", - " Triage complete event\n", - " '''\n", - " trace(f'triage {self.identifier} complete {self.env.now:.3f}; '\n", - " f'waiting time was {self.wait_triage:.3f}')\n", - " \n", - " def trauma_complete(self):\n", - " '''\n", - " Patient stay in trauma is complete.\n", - " '''\n", - " trace(f'stabilisation of patient {self.identifier} at '\n", - " f'{self.env.now:.3f}')\n", - " \n", - " def treatment_complete(self):\n", - " '''\n", - " Treatment complete event\n", - " '''\n", - " trace(f'patient {self.identifier} treatment complete {self.env.now:.3f}; '\n", - " f'waiting time was {self.wait_treat:.3f}')" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "b1397f5c", - "metadata": {}, - "outputs": [], - "source": [ - "class NonTraumaPathway:\n", - " '''\n", - " Encapsulates the process a patient with minor injuries and illness.\n", - " \n", - " These patients are signed into the ED and triaged as having minor \n", - " complaints and streamed to registration and then examination. \n", - " \n", - " Post examination 40% are discharged while 60% proceed to treatment. \n", - " Following treatment they are discharged.\n", - " '''\n", - " def __init__(self, identifier, env, args):\n", - " '''\n", - " Constructor method\n", - " \n", - " Params:\n", - " -----\n", - " identifier: int\n", - " a numeric identifier for the patient.\n", - " \n", - " env: simpy.Environment\n", - " the simulation environment\n", - " \n", - " args: Scenario\n", - " Container class for the simulation parameters\n", - " \n", - " '''\n", - " self.identifier = identifier\n", - " self.env = env\n", - " self.args = args\n", - " \n", - " # triage resource\n", - " self.triage = args.triage\n", - " \n", - " # metrics\n", - " self.arrival = -np.inf\n", - " self.wait_triage = -np.inf\n", - " self.wait_reg = -np.inf\n", - " self.wait_exam = -np.inf\n", - " self.wait_treat = -np.inf\n", - " self.total_time = -np.inf\n", - " \n", - " self.triage_duration = -np.inf\n", - " self.reg_duration = -np.inf\n", - " self.exam_duration = -np.inf\n", - " self.treat_duration = -np.inf\n", - " \n", - " \n", - " def execute(self):\n", - " '''\n", - " simulates the non-trauma/minor treatment process for a patient\n", - " \n", - " 1. request and wait for sign-in/triage\n", - " 2. patient registration\n", - " 3. examination\n", - " 4.1 40% discharged\n", - " 4.2 60% treatment then discharge\n", - " '''\n", - " # record the time of arrival and entered the triage queue\n", - " self.arrival = self.env.now\n", - "\n", - " # request sign-in/triage \n", - " with self.triage.request() as req:\n", - " yield req\n", - " \n", - " # record the waiting time for triage\n", - " self.wait_triage = self.env.now - self.arrival\n", - " trace(f'patient {self.identifier} triaged to minors '\n", - " f'{self.env.now:.3f}')\n", - " \n", - " # sample triage duration.\n", - " self.triage_duration = self.args.triage_dist.sample()\n", - " yield self.env.timeout(self.triage_duration)\n", - " \n", - " trace(f'triage {self.identifier} complete {self.env.now:.3f}; '\n", - " f'waiting time was {self.wait_triage:.3f}')\n", - " \n", - " # record the time that entered the registration queue\n", - " start_wait = self.env.now\n", - " \n", - " # request registration clert \n", - " with self.args.registration.request() as req:\n", - " yield req\n", - " \n", - " # record the waiting time for registration\n", - " self.wait_reg = self.env.now - start_wait\n", - " trace(f'registration of patient {self.identifier} at '\n", - " f'{self.env.now:.3f}')\n", - " \n", - " # sample registration duration.\n", - " self.reg_duration = self.args.reg_dist.sample()\n", - " yield self.env.timeout(self.reg_duration)\n", - " \n", - " trace(f'patient {self.identifier} registered at'\n", - " f'{self.env.now:.3f}; '\n", - " f'waiting time was {self.wait_reg:.3f}')\n", - " \n", - " # record the time that entered the evaluation queue\n", - " start_wait = self.env.now\n", - " \n", - " # request examination resource\n", - " with self.args.exam.request() as req:\n", - " yield req\n", - " \n", - " # record the waiting time for registration\n", - " self.wait_exam = self.env.now - start_wait\n", - " trace(f'examination of patient {self.identifier} begins '\n", - " f'{self.env.now:.3f}')\n", - " \n", - " # sample examination duration.\n", - " self.exam_duration = self.args.exam_dist.sample()\n", - " yield self.env.timeout(self.exam_duration)\n", - " \n", - " trace(f'patient {self.identifier} examination complete ' \n", - " f'at {self.env.now:.3f};'\n", - " f'waiting time was {self.wait_exam:.3f}')\n", - " \n", - " # sample if patient requires treatment?\n", - " self.require_treat = self.args.nt_p_treat_dist.sample()\n", - " \n", - " if self.require_treat:\n", - " \n", - " # record the time that entered the treatment queue\n", - " start_wait = self.env.now\n", - " \n", - " # request treatment cubicle\n", - " with self.args.cubicle_1.request() as req:\n", - " yield req\n", - "\n", - " # record the waiting time for treatment\n", - " self.wait_treat = self.env.now - start_wait\n", - " trace(f'treatment of patient {self.identifier} begins '\n", - " f'{self.env.now:.3f}')\n", - "\n", - " # sample treatment duration.\n", - " self.treat_duration = self.args.nt_treat_dist.sample()\n", - " yield self.env.timeout(self.treat_duration)\n", - "\n", - " trace(f'patient {self.identifier} treatment complete '\n", - " f'at {self.env.now:.3f};'\n", - " f'waiting time was {self.wait_treat:.3f}')\n", - " \n", - " # total time in system\n", - " self.total_time = self.env.now - self.arrival " - ] - }, - { - "cell_type": "markdown", - "id": "7bbc8e42", - "metadata": {}, - "source": [ - "## 7. Main model class\n", - "\n", - "The main class that a user interacts with to run the model is `TreatmentCentreModel`. This implements a `.run()` method, contains a simple algorithm for the non-stationary poission process for patients arrivals and inits instances of `TraumaPathway` or `NonTraumaPathway` depending on the arrival type." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "ada6721d", - "metadata": {}, - "outputs": [], - "source": [ - "class TreatmentCentreModel:\n", - " '''\n", - " The treatment centre model\n", - " \n", - " Patients arrive at random to a treatment centre, are triaged\n", - " and then processed in either a trauma or non-trauma pathway.\n", - " '''\n", - " def __init__(self, args):\n", - " self.env = simpy.Environment()\n", - " self.args = args\n", - " self.init_resources()\n", - " \n", - " self.patients = []\n", - " self.trauma_patients = []\n", - " self.non_trauma_patients = []\n", - "\n", - " self.rc_period = None\n", - " self.results = None\n", - " \n", - " def init_resources(self):\n", - " '''\n", - " Init the number of resources\n", - " and store in the arguments container object\n", - " \n", - " Resource list:\n", - " 1. Sign-in/triage bays\n", - " 2. registration clerks\n", - " 3. examination bays\n", - " 4. trauma bays\n", - " 5. non-trauma cubicles (1)\n", - " 6. trauma cubicles (2)\n", - " \n", - " '''\n", - " # sign/in triage\n", - " self.args.triage = simpy.Resource(self.env, \n", - " capacity=self.args.n_triage)\n", - " \n", - " # registration\n", - " self.args.registration = simpy.Resource(self.env, \n", - " capacity=self.args.n_reg)\n", - " \n", - " # examination\n", - " self.args.exam = simpy.Resource(self.env, \n", - " capacity=self.args.n_exam)\n", - " \n", - " # trauma\n", - " self.args.trauma = simpy.Resource(self.env, \n", - " capacity=self.args.n_trauma)\n", - " \n", - " # non-trauma treatment\n", - " self.args.cubicle_1 = simpy.Resource(self.env, \n", - " capacity=self.args.n_cubicles_1)\n", - " \n", - " # trauma treatment\n", - " self.args.cubicle_2 = simpy.Resource(self.env, \n", - " capacity=self.args.n_cubicles_2)\n", - " \n", - " \n", - " \n", - " def run(self, results_collection_period=DEFAULT_RESULTS_COLLECTION_PERIOD):\n", - " '''\n", - " Conduct a single run of the model in its current \n", - " configuration\n", - " \n", - " \n", - " Parameters:\n", - " ----------\n", - " results_collection_period, float, optional\n", - " default = DEFAULT_RESULTS_COLLECTION_PERIOD\n", - " \n", - " warm_up, float, optional (default=0)\n", - " \n", - " length of initial transient period to truncate\n", - " from results.\n", - " \n", - " Returns:\n", - " --------\n", - " None\n", - " '''\n", - " # setup the arrival generator process\n", - " self.env.process(self.arrivals_generator())\n", - " \n", - " # store rc perio\n", - " self.rc_period = results_collection_period\n", - " \n", - " # run\n", - " self.env.run(until=results_collection_period)\n", - " \n", - " \n", - " def arrivals_generator(self): \n", - " ''' \n", - " Simulate the arrival of patients to the model\n", - " \n", - " Patients either follow a TraumaPathway or\n", - " NonTraumaPathway simpy process.\n", - " \n", - " Non stationary arrivals implemented via Thinning acceptance-rejection \n", - " algorithm.\n", - " '''\n", - " for patient_count in itertools.count():\n", - "\n", - " # this give us the index of dataframe to use\n", - " t = int(self.env.now // 60) % self.args.arrivals.shape[0]\n", - " lambda_t = self.args.arrivals['arrival_rate'].iloc[t]\n", - "\n", - " #set to a large number so that at least 1 sample taken!\n", - " u = np.Inf\n", - " \n", - " interarrival_time = 0.0\n", - "\n", - " # reject samples if u >= lambda_t / lambda_max\n", - " while u >= (lambda_t / self.args.lambda_max):\n", - " interarrival_time += self.args.arrival_dist.sample()\n", - " u = self.args.thinning_rng.sample()\n", - "\n", - " # iat\n", - " yield self.env.timeout(interarrival_time)\n", - " \n", - " trace(f'patient {patient_count} arrives at: {self.env.now:.3f}')\n", - " \n", - " # sample if the patient is trauma or non-trauma\n", - " trauma = self.args.p_trauma_dist.sample()\n", - " if trauma:\n", - " # create and store a trauma patient to update KPIs.\n", - " new_patient = TraumaPathway(patient_count, self.env, self.args)\n", - " self.trauma_patients.append(new_patient)\n", - " else:\n", - " # create and store a non-trauma patient to update KPIs.\n", - " new_patient = NonTraumaPathway(patient_count, self.env, \n", - " self.args)\n", - " self.non_trauma_patients.append(new_patient)\n", - " \n", - " # start the pathway process for the patient\n", - " self.env.process(new_patient.execute())" - ] - }, - { - "cell_type": "markdown", - "id": "0fda6af5", - "metadata": {}, - "source": [ - "### 8. Logic to process end of run results.\n", - "\n", - "the class `SimulationSummary` accepts a `TraumaCentreModel`. At the end of a run it can be used calculate mean queuing times and the percentage of the total run that a resource was in use." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "8b7fdd74", - "metadata": {}, - "outputs": [], - "source": [ - "class SimulationSummary:\n", - " '''\n", - " End of run result processing logic of the simulation model\n", - " '''\n", - " def __init__(self, model):\n", - " '''\n", - " Constructor\n", - " \n", - " Params:\n", - " ------\n", - " model: TraumaCentreModel\n", - " The model.\n", - " '''\n", - " self.model = model\n", - " self.args = model.args\n", - " self.results = None\n", - " \n", - " def process_run_results(self):\n", - " '''\n", - " Calculates statistics at end of run.\n", - " '''\n", - " self.results = {}\n", - " # list of all patients \n", - " patients = self.model.non_trauma_patients + self.model.trauma_patients\n", - " \n", - " # mean triage times (both types of patient)\n", - " mean_triage_wait = self.get_mean_metric('wait_triage', patients)\n", - " \n", - " # triage utilisation (both types of patient)\n", - " triage_util = self.get_resource_util('triage_duration', \n", - " self.args.n_triage, \n", - " patients)\n", - " \n", - " # mean waiting time for registration (non_trauma)\n", - " mean_reg_wait = self.get_mean_metric('wait_reg', \n", - " self.model.non_trauma_patients)\n", - " \n", - " # registration utilisation (trauma)\n", - " reg_util = self.get_resource_util('reg_duration', \n", - " self.args.n_reg, \n", - " self.model.non_trauma_patients)\n", - " \n", - " # mean waiting time for examination (non_trauma)\n", - " mean_wait_exam = self.get_mean_metric('wait_exam', \n", - " self.model.non_trauma_patients)\n", - " \n", - " # examination utilisation (non-trauma)\n", - " exam_util = self.get_resource_util('exam_duration', \n", - " self.args.n_exam, \n", - " self.model.non_trauma_patients)\n", - " \n", - " \n", - " # mean waiting time for treatment (non-trauma) \n", - " mean_treat_wait = self.get_mean_metric('wait_treat', \n", - " self.model.non_trauma_patients)\n", - " \n", - " # treatment utilisation (non_trauma)\n", - " treat_util1 = self.get_resource_util('treat_duration', \n", - " self.args.n_cubicles_1, \n", - " self.model.non_trauma_patients)\n", - " \n", - " # mean total time (non_trauma)\n", - " mean_total = self.get_mean_metric('total_time', \n", - " self.model.non_trauma_patients)\n", - " \n", - " # mean waiting time for trauma \n", - " mean_trauma_wait = self.get_mean_metric('wait_trauma', \n", - " self.model.trauma_patients)\n", - " \n", - " # trauma utilisation (trauma)\n", - " trauma_util = self.get_resource_util('trauma_duration', \n", - " self.args.n_trauma, \n", - " self.model.trauma_patients)\n", - " \n", - " # mean waiting time for treatment (rauma) \n", - " mean_treat_wait2 = self.get_mean_metric('wait_treat', \n", - " self.model.trauma_patients)\n", - " \n", - " # treatment utilisation (trauma)\n", - " treat_util2 = self.get_resource_util('treat_duration', \n", - " self.args.n_cubicles_2, \n", - " self.model.trauma_patients)\n", - "\n", - " # mean total time (trauma)\n", - " mean_total2 = self.get_mean_metric('total_time', \n", - " self.model.trauma_patients)\n", - " \n", - " \n", - " self.results = {'00_arrivals':len(patients),\n", - " '01a_triage_wait': mean_triage_wait,\n", - " '01b_triage_util': triage_util,\n", - " '02a_registration_wait':mean_reg_wait,\n", - " '02b_registration_util': reg_util,\n", - " '03a_examination_wait':mean_wait_exam,\n", - " '03b_examination_util': exam_util,\n", - " '04a_treatment_wait(non_trauma)':mean_treat_wait,\n", - " '04b_treatment_util(non_trauma)':treat_util1,\n", - " '05_total_time(non-trauma)':mean_total,\n", - " '06a_trauma_wait':mean_trauma_wait,\n", - " '06b_trauma_util':trauma_util,\n", - " '07a_treatment_wait(trauma)':mean_treat_wait2,\n", - " '07b_treatment_util(trauma)':treat_util2,\n", - " '08_total_time(trauma)':mean_total2,\n", - " '09_throughput': self.get_throughput(patients)}\n", - " \n", - " def get_mean_metric(self, metric, patients):\n", - " '''\n", - " Calculate mean of the performance measure for the\n", - " select cohort of patients,\n", - " \n", - " Only calculates metrics for patients where it has been \n", - " measured.\n", - " \n", - " Params:\n", - " -------\n", - " metric: str\n", - " The name of the metric e.g. 'wait_treat'\n", - " \n", - " patients: list\n", - " A list of patients\n", - " '''\n", - " mean = np.array([getattr(p, metric) for p in patients \n", - " if getattr(p, metric) > -np.inf]).mean()\n", - " return mean\n", - " \n", - " \n", - " def get_resource_util(self, metric, n_resources, patients):\n", - " '''\n", - " Calculate proportion of the results collection period\n", - " where a resource was in use.\n", - " \n", - " Done by tracking the duration by patient.\n", - " \n", - " Only calculates metrics for patients where it has been \n", - " measured.\n", - " \n", - " Params:\n", - " -------\n", - " metric: str\n", - " The name of the metric e.g. 'treatment_duration'\n", - " \n", - " patients: list\n", - " A list of patients\n", - " '''\n", - " total = np.array([getattr(p, metric) for p in patients \n", - " if getattr(p, metric) > -np.inf]).sum() \n", - " \n", - " return total / (self.model.rc_period * n_resources)\n", - " \n", - " def get_throughput(self, patients):\n", - " '''\n", - " Returns the total number of patients that have successfully\n", - " been processed and discharged in the treatment centre\n", - " (they have a total time record)\n", - " \n", - " Params:\n", - " -------\n", - " patients: list\n", - " list of all patient objects simulated.\n", - " \n", - " Returns:\n", - " ------\n", - " float\n", - " '''\n", - " return len([p for p in patients if p.total_time > -np.inf])\n", - " \n", - " def summary_frame(self):\n", - " '''\n", - " Returns run results as a pandas.DataFrame\n", - " \n", - " Returns:\n", - " -------\n", - " pd.DataFrame\n", - " '''\n", - " #append to results df\n", - " if self.results is None:\n", - " self.process_run_results()\n", - "\n", - " df = pd.DataFrame({'1':self.results})\n", - " df = df.T\n", - " df.index.name = 'rep'\n", - " return df\n", - " " - ] - }, - { - "cell_type": "markdown", - "id": "9d263ca8", - "metadata": {}, - "source": [ - "## 9. Model execution\n", - "\n", - "We note that there are **many ways** to setup a `simpy` model and execute it (that is part of its fantastic flexibility). The organisation of code we show below is based on our experience of using the package in practice. The approach also allows for easy parallisation over multiple CPU cores using `joblib`.\n", - "\n", - "We include two functions. `single_run()` and `multiple_replications`. The latter is used to repeatedly call and process the results from `single_run`." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "c35594bd", - "metadata": {}, - "outputs": [], - "source": [ - "def single_run(scenario, rc_period=DEFAULT_RESULTS_COLLECTION_PERIOD, \n", - " random_no_set=DEFAULT_RNG_SET):\n", - " '''\n", - " Perform a single run of the model and return the results\n", - " \n", - " Parameters:\n", - " -----------\n", - " \n", - " scenario: Scenario object\n", - " The scenario/paramaters to run\n", - " \n", - " rc_period: int\n", - " The length of the simulation run that collects results\n", - " \n", - " random_no_set: int or None, optional (default=DEFAULT_RNG_SET)\n", - " Controls the set of random seeds used by the stochastic parts of the \n", - " model. Set to different ints to get different results. Set to None\n", - " for a random set of seeds.\n", - " \n", - " Returns:\n", - " --------\n", - " pandas.DataFrame:\n", - " results from single run.\n", - " ''' \n", - " \n", - " # set random number set - this controls sampling for the run.\n", - " scenario.set_random_no_set(random_no_set)\n", - "\n", - " # create an instance of the model\n", - " model = TreatmentCentreModel(scenario)\n", - "\n", - " # run the model\n", - " model.run(results_collection_period=rc_period)\n", - " \n", - " # run results\n", - " summary = SimulationSummary(model)\n", - " summary_df = summary.summary_frame()\n", - " \n", - " return summary_df" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "84936beb", - "metadata": {}, - "outputs": [], - "source": [ - "def multiple_replications(scenario, rc_period=DEFAULT_RESULTS_COLLECTION_PERIOD, \n", - " n_reps=5):\n", - " '''\n", - " Perform multiple replications of the model.\n", - " \n", - " Params:\n", - " ------\n", - " scenario: Scenario\n", - " Parameters/arguments to configurethe model\n", - " \n", - " rc_period: float, optional (default=DEFAULT_RESULTS_COLLECTION_PERIOD)\n", - " results collection period. \n", - " the number of minutes to run the model to collect results\n", - "\n", - " n_reps: int, optional (default=DEFAULT_N_REPS)\n", - " Number of independent replications to run.\n", - " \n", - " Returns:\n", - " --------\n", - " pandas.DataFrame\n", - " '''\n", - "\n", - " results = [single_run(scenario, rc_period, random_no_set=rep) \n", - " for rep in range(n_reps)]\n", - " \n", - " #format and return results in a dataframe\n", - " df_results = pd.concat(results)\n", - " df_results.index = np.arange(1, len(df_results)+1)\n", - " df_results.index.name = 'rep'\n", - " return df_results" - ] - }, - { - "cell_type": "markdown", - "id": "4f8c3255", - "metadata": {}, - "source": [ - "### 9.1 Single run of the model\n", - "\n", - "The script below performs a single replication of the simulation model. \n", - "\n", - "**Try:**\n", - "\n", - "* Changing the `random_no_set` of the `single_run` call.\n", - "* Assigning the value `True` to `TRACE`" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "ce8e9091", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running simulation ... => simulation complete.\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
rep1
00_arrivals209.000000
01a_triage_wait16.622674
01b_triage_util0.527512
02a_registration_wait111.161345
02b_registration_util0.801061
03a_examination_wait24.927965
03b_examination_util0.851285
04a_treatment_wait(non_trauma)172.435861
04b_treatment_util(non_trauma)0.845652
05_total_time(non-trauma)248.848441
06a_trauma_wait201.144403
06b_trauma_util0.919830
07a_treatment_wait(trauma)22.620880
07b_treatment_util(trauma)0.493576
08_total_time(trauma)310.384648
09_throughput155.000000
\n", - "
" - ], - "text/plain": [ - "rep 1\n", - "00_arrivals 209.000000\n", - "01a_triage_wait 16.622674\n", - "01b_triage_util 0.527512\n", - "02a_registration_wait 111.161345\n", - "02b_registration_util 0.801061\n", - "03a_examination_wait 24.927965\n", - "03b_examination_util 0.851285\n", - "04a_treatment_wait(non_trauma) 172.435861\n", - "04b_treatment_util(non_trauma) 0.845652\n", - "05_total_time(non-trauma) 248.848441\n", - "06a_trauma_wait 201.144403\n", - "06b_trauma_util 0.919830\n", - "07a_treatment_wait(trauma) 22.620880\n", - "07b_treatment_util(trauma) 0.493576\n", - "08_total_time(trauma) 310.384648\n", - "09_throughput 155.000000" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Change this to True to see a trace...\n", - "TRACE = False\n", - "\n", - "# create the default scenario\n", - "args = Scenario()\n", - "\n", - "# use the single_run() func\n", - "# try changing `random_no_set` to see different run results\n", - "print('Running simulation ...', end=' => ')\n", - "results = single_run(args, random_no_set=42)\n", - "print('simulation complete.')\n", - "\n", - "# show results (transpose replication for easier view)\n", - "results.T" - ] - }, - { - "cell_type": "markdown", - "id": "f6485b32", - "metadata": {}, - "source": [ - "### 9.2 Multiple independent replications\n", - "\n", - "Given the set up it is now easy to perform multiple replications of the model.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "22243edf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Running multiple replications => done.\n", - "\n", - "CPU times: user 798 ms, sys: 1.32 ms, total: 799 ms\n", - "Wall time: 799 ms\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
00_arrivals01a_triage_wait01b_triage_util02a_registration_wait02b_registration_util03a_examination_wait03b_examination_util04a_treatment_wait(non_trauma)04b_treatment_util(non_trauma)05_total_time(non-trauma)06a_trauma_wait06b_trauma_util07a_treatment_wait(trauma)07b_treatment_util(trauma)08_total_time(trauma)09_throughput
rep
1230.024.2809430.613250103.2422920.85450431.0896800.861719152.4833940.890904234.759918236.5084441.02888713.7017830.607996346.698079171.0
2227.057.1201140.62134890.0023850.83668514.6884920.847295120.2454740.912127233.882040133.8139010.8341243.7154460.367507301.521195161.0
3229.028.6593830.573698112.2425030.84851421.3740920.85630694.0198850.868888208.361290276.4225660.87424512.2521750.464740440.515502167.0
\n", - "
" - ], - "text/plain": [ - " 00_arrivals 01a_triage_wait 01b_triage_util 02a_registration_wait \\\n", - "rep \n", - "1 230.0 24.280943 0.613250 103.242292 \n", - "2 227.0 57.120114 0.621348 90.002385 \n", - "3 229.0 28.659383 0.573698 112.242503 \n", - "\n", - " 02b_registration_util 03a_examination_wait 03b_examination_util \\\n", - "rep \n", - "1 0.854504 31.089680 0.861719 \n", - "2 0.836685 14.688492 0.847295 \n", - "3 0.848514 21.374092 0.856306 \n", - "\n", - " 04a_treatment_wait(non_trauma) 04b_treatment_util(non_trauma) \\\n", - "rep \n", - "1 152.483394 0.890904 \n", - "2 120.245474 0.912127 \n", - "3 94.019885 0.868888 \n", - "\n", - " 05_total_time(non-trauma) 06a_trauma_wait 06b_trauma_util \\\n", - "rep \n", - "1 234.759918 236.508444 1.028887 \n", - "2 233.882040 133.813901 0.834124 \n", - "3 208.361290 276.422566 0.874245 \n", - "\n", - " 07a_treatment_wait(trauma) 07b_treatment_util(trauma) \\\n", - "rep \n", - "1 13.701783 0.607996 \n", - "2 3.715446 0.367507 \n", - "3 12.252175 0.464740 \n", - "\n", - " 08_total_time(trauma) 09_throughput \n", - "rep \n", - "1 346.698079 171.0 \n", - "2 301.521195 161.0 \n", - "3 440.515502 167.0 " - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "%%time\n", - "args = Scenario()\n", - "\n", - "#run multiple replications.\n", - "#by default it runs 5 replications.\n", - "print('Running multiple replications', end=' => ')\n", - "results = multiple_replications(args, n_reps=50)\n", - "print('done.\\n')\n", - "results.head(3)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "d74aaa7e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "00_arrivals 227.72\n", - "01a_triage_wait 35.24\n", - "01b_triage_util 0.61\n", - "02a_registration_wait 105.57\n", - "02b_registration_util 0.84\n", - "03a_examination_wait 25.55\n", - "03b_examination_util 0.85\n", - "04a_treatment_wait(non_trauma) 136.66\n", - "04b_treatment_util(non_trauma) 0.87\n", - "05_total_time(non-trauma) 234.34\n", - "06a_trauma_wait 151.68\n", - "06b_trauma_util 0.83\n", - "07a_treatment_wait(trauma) 14.31\n", - "07b_treatment_util(trauma) 0.50\n", - "08_total_time(trauma) 292.28\n", - "09_throughput 162.16\n", - "dtype: float64" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# summarise the results (2.dp)\n", - "results.mean().round(2)" - ] - }, - { - "cell_type": "markdown", - "id": "fe2556ab", - "metadata": {}, - "source": [ - "### 9.3 Visualise replications" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "645a11fe", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/IAAAFfCAYAAADtf/chAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjMuNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8QVMy6AAAACXBIWXMAAA9hAAAPYQGoP6dpAABAs0lEQVR4nO3deVjVZf7/8deR5YgGKCrLKRQ0cxcdLRXLJdMys9SZTC21rH5Z5saYymRftVLUJrcYJWw1p7GZcZnMKcUSzMxcwNyXEsVUYszkoCYofH5/OJ7pBCjHzkrPx3Wd6+Lc9/05nxfdN+Sbz2YyDMMQAAAAAADwCVU8HQAAAAAAAFQchTwAAAAAAD6EQh4AAAAAAB9CIQ8AAAAAgA+hkAcAAAAAwIdQyAMAAAAA4EMo5AEAAAAA8CH+ng7gjUpKSnTixAkFBwfLZDJ5Og4AAAAAoJIzDEMFBQWyWCyqUuXqx9wp5Mtw4sQJRUdHezoGAAAAAOA35tixY7rpppuuOoZCvgzBwcGSLv8HDAkJ8XAaAAAAAEBlZ7VaFR0dbatHr4ZCvgxXTqcPCQmhkAcAAAAAuE1FLu/mZncAAAAAAPgQCnkAAAAAAHwIhTwAAAAAAD6EQh4AAAAAAB/Cze6AX4iZuNrTESqdIzN6eToCAAAAUGlwRB4AAAAAAB9CIQ8AAAAAgA+hkAcAAAAAwIdQyAMAAAAA4EMo5AEAAAAA8CEU8gAAAAAA+BAKeQAAAAAAfAiFPAAAAAAAPoRCHgAAAAAAH0IhDwAAAACAD6GQBwAAAADAh1DIAwAAAADgQyjkAQAAAADwIRTyAAAAAAD4EAp5AAAAAAB8iNsK+c8//1yPPPKIOnTooOPHj0uS3nvvPW3cuNFdEQAAAAAA8HluKeSXLVumu+++W0FBQcrKylJhYaEkqaCgQNOnT3dHBAAAAAAAKgW3FPIvv/yyUlJStGjRIgUEBNja4+PjlZmZ6dBnbdiwQb1795bFYpHJZNLKlSvt+g3D0JQpU2SxWBQUFKQuXbpoz549zvg2AAAAAADwOLcU8gcOHFCnTp1KtYeEhOjMmTMOfda5c+cUFxen5OTkMvtnzZql2bNnKzk5WVu3blVkZKS6d++ugoKC64kOAAAAAIBX8XfHTqKiovTNN98oJibGrn3jxo2qX7++Q5/Vs2dP9ezZs8w+wzA0d+5cPf/88+rXr58k6d1331VERITef/99PfXUU9eVHwAAAAAAb+GWI/JPPfWURo8era+++komk0knTpzQX//6V40bN07PPPOM0/aTnZ2t3Nxc9ejRw9ZmNpvVuXNnbdq0qdztCgsLZbVa7V4AAAAAAHgjtxyRHz9+vPLz89W1a1dduHBBnTp1ktls1rhx4/Tss886bT+5ubmSpIiICLv2iIgIHT16tNztkpKSNHXqVKflAAAAAADAVdz2+Llp06bp1KlT2rJlizZv3qz//Oc/eumll1yyL5PJZPfeMIxSbT+XmJio/Px82+vYsWMuyQUAAAAAwK/lliPyV1SrVk1t27Z12edHRkZKunxkPioqytael5dX6ij9z5nNZpnNZpflAgAAAADAWdxSyPft27fMI+Imk0lVq1bVzTffrEGDBqlRo0a/aj+xsbGKjIxUWlqaWrduLUkqKipSRkaGZs6c+as+GwAAAAAAb+CWU+tDQ0P12WefKTMz01bQZ2Vl6bPPPtOlS5f0wQcfKC4uTl988cU1P+vs2bPasWOHduzYIenyDe527NihnJwcmUwmjRkzRtOnT9eKFSu0e/duPfroo6pWrZoGDRrkym8RAAAAAAC3cMsR+cjISA0aNEjJycmqUuXy3w5KSko0evRoBQcHa+nSpRo+fLgmTJigjRs3XvWztm3bpq5du9reJyQkSJKGDh2qd955R+PHj9dPP/2kZ555Rj/++KPatWuntWvXKjg42HXfIAAAAAAAbmIyDMNw9U7q1KmjL774Qrfccotd+8GDBxUfH69Tp05p165duuOOO3TmzBlXx7kmq9Wq0NBQ5efnKyQkxNNx4GYxE1d7OkKlc2RGL09HAAAAALyaI3WoW06tv3Tpkvbv31+qff/+/SouLpYkVa1a9ap3lgcAAAAAAG46tX7w4MF6/PHH9ac//Um33nqrTCaTtmzZounTp2vIkCGSpIyMDDVr1swdcQAAAAAA8FluKeTnzJmjiIgIzZo1S99//70kKSIiQmPHjtWECRMkST169NA999zjjjgAAAAAAPgst1wj/3NWq1WSvPrac66R/23jGnnn4xp5AAAA4OocqUPdckT+5yiMAQAAAAC4fm4r5P/5z3/q73//u3JyclRUVGTXl5mZ6a4YAAAAAAD4NLfctX7+/Pl67LHHFB4erqysLN12222qVauWDh8+rJ49e7oj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+vX14osvqqSkxDaGufMOFPKwc+7cOcXFxSk5ObnM/lmzZmn27NlKTk7W1q1bFRkZqe7du6ugoMDNSfFzGRkZGjFihDZv3qy0tDRdunRJPXr00Llz52xjmDvvc9NNN2nGjBnatm2btm3bpjvvvFMPPPCA7X+GzJn327p1q1JTU9WyZUu7dubOOzVr1kwnT560vXbt2mXrY868148//qiOHTsqICBAH3/8sfbu3atXX31VNWrUsI1h/rzP1q1b7X7e0tLSJEkPPvigJObMW82cOVMpKSlKTk7Wvn37NGvWLL3yyit67bXXbGOYOy9hAOWQZKxYscL2vqSkxIiMjDRmzJhha7tw4YIRGhpqpKSkeCAhypOXl2dIMjIyMgzDYO58Sc2aNY033niDOfMBBQUFRsOGDY20tDSjc+fOxujRow3D4OfNW02ePNmIi4srs485824TJkwwbr/99nL7mT/fMHr0aKNBgwZGSUkJc+bFevXqZQwbNsyurV+/fsYjjzxiGAY/b96EI/KosOzsbOXm5qpHjx62NrPZrM6dO2vTpk0eTIZfys/PlySFhYVJYu58QXFxsZYuXapz586pQ4cOzJkPGDFihHr16qW77rrLrp25816HDh2SxWJRbGysBgwYoMOHD0tizrzdhx9+qLZt2+rBBx9UeHi4WrdurUWLFtn6mT/vV1RUpCVLlmjYsGEymUzMmRe7/fbb9emnn+rgwYOSpK+//lobN27UvffeK4mfN2/i7+kA8B25ubmSpIiICLv2iIgIHT161BORUAbDMJSQkKDbb79dzZs3l8TcebNdu3apQ4cOunDhgm644QatWLFCTZs2tf3PkDnzTkuXLlVmZqa2bt1aqo+fN+/Url07LV68WLfccou+//57vfzyy4qPj9eePXuYMy93+PBhLVy4UAkJCfrTn/6kLVu2aNSoUTKbzRoyZAjz5wNWrlypM2fO6NFHH5XE70lvNmHCBOXn56tx48by8/NTcXGxpk2bpoEDB0pi7rwJhTwcZjKZ7N4bhlGqDZ7z7LPPaufOndq4cWOpPubO+zRq1Eg7duzQmTNntGzZMg0dOlQZGRm2fubM+xw7dkyjR4/W2rVrVbVq1XLHMXfepWfPnravW7RooQ4dOqhBgwZ699131b59e0nMmbcqKSlR27ZtNX36dElS69attWfPHi1cuFBDhgyxjWP+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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, ax = plt.subplots(2, 1, figsize=(12,4))\n", - "ax[0].hist(results['01a_triage_wait']);\n", - "ax[0].set_ylabel('wait for triage')\n", - "ax[1].hist(results['02a_registration_wait']);\n", - "ax[1].set_ylabel('wait for registration');" - ] - }, - { - "cell_type": "markdown", - "id": "aa47f100", - "metadata": {}, - "source": [ - "## 10. Scenario Analysis\n", - "\n", - "The structured approach we took to organising our `simpy` model allows us to easily experiment with alternative scenarios. We could employ a formal experimental design if needed. For simplicity here we will limit ourselves by running user chosen competing scenarios and compare their mean performance to the base case.\n", - "\n", - "> Note that we have our `simpy` model includes an implementation of **Common Random Numbers** across scenarios. " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "2ad1b0b1", - "metadata": {}, - "outputs": [], - "source": [ - "def get_scenarios():\n", - " '''\n", - " Creates a dictionary object containing\n", - " objects of type `Scenario` to run.\n", - " \n", - " Returns:\n", - " --------\n", - " dict\n", - " Contains the scenarios for the model\n", - " '''\n", - " scenarios = {}\n", - " scenarios['base'] = Scenario()\n", - " \n", - " # extra triage capacity\n", - " scenarios['triage+1'] = Scenario(n_triage=DEFAULT_N_TRIAGE+1)\n", - " \n", - " # extra examination capacity\n", - " scenarios['exam+1'] = Scenario(n_exam=DEFAULT_N_EXAM+1)\n", - " \n", - " # extra non-trauma treatment capacity\n", - " scenarios['treat+1'] = Scenario(n_cubicles_1=DEFAULT_N_CUBICLES_1+1)\n", - " \n", - " scenarios['triage+exam'] = Scenario(n_triage=DEFAULT_N_TRIAGE+1,\n", - " n_exam=DEFAULT_N_EXAM+1)\n", - " \n", - " return scenarios" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "b7475a49", - "metadata": {}, - "outputs": [], - "source": [ - "def run_scenario_analysis(scenarios, rc_period, n_reps):\n", - " '''\n", - " Run each of the scenarios for a specified results\n", - " collection period and replications.\n", - " \n", - " Params:\n", - " ------\n", - " scenarios: dict\n", - " dictionary of Scenario objects\n", - " \n", - " rc_period: float\n", - " model run length\n", - " \n", - " n_rep: int\n", - " Number of replications\n", - " \n", - " '''\n", - " print('Scenario Analysis')\n", - " print(f'No. Scenario: {len(scenarios)}')\n", - " print(f'Replications: {n_reps}')\n", - "\n", - " scenario_results = {}\n", - " for sc_name, scenario in scenarios.items():\n", - " \n", - " print(f'Running {sc_name}', end=' => ')\n", - " replications = multiple_replications(scenario, rc_period=rc_period,\n", - " n_reps=n_reps)\n", - " print('done.\\n')\n", - " \n", - " #save the results\n", - " scenario_results[sc_name] = replications\n", - " \n", - " print('Scenario analysis complete.')\n", - " return scenario_results" - ] - }, - { - "cell_type": "markdown", - "id": "76ecd9dc", - "metadata": {}, - "source": [ - "### 10.1 Script to run scenario analysis" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "9148215c", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Scenario Analysis\n", - "No. Scenario: 5\n", - "Replications: 20\n", - "Running base => done.\n", - "\n", - "Running triage+1 => done.\n", - "\n", - "Running exam+1 => done.\n", - "\n", - "Running treat+1 => done.\n", - "\n", - "Running triage+exam => done.\n", - "\n", - "Scenario analysis complete.\n" - ] - } - ], - "source": [ - "#number of replications\n", - "N_REPS = 20\n", - "\n", - "#get the scenarios\n", - "scenarios = get_scenarios()\n", - "\n", - "#run the scenario analysis\n", - "scenario_results = run_scenario_analysis(scenarios, \n", - " DEFAULT_RESULTS_COLLECTION_PERIOD,\n", - " N_REPS)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "1035da4a", - "metadata": {}, - "outputs": [], - "source": [ - "def scenario_summary_frame(scenario_results):\n", - " '''\n", - " Mean results for each performance measure by scenario\n", - " \n", - " Parameters:\n", - " ----------\n", - " scenario_results: dict\n", - " dictionary of replications. \n", - " Key identifies the performance measure\n", - " \n", - " Returns:\n", - " -------\n", - " pd.DataFrame\n", - " '''\n", - " columns = []\n", - " summary = pd.DataFrame()\n", - " for sc_name, replications in scenario_results.items():\n", - " summary = pd.concat([summary, replications.mean()], axis=1)\n", - " columns.append(sc_name)\n", - "\n", - " summary.columns = columns\n", - " return summary" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "3967ce49", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
basetriage+1exam+1treat+1triage+exam
00_arrivals227.25227.25227.25227.25227.25
01a_triage_wait32.561.2632.5632.561.26
01b_triage_util0.610.310.610.610.31
02a_registration_wait104.69131.82104.69104.69131.82
02b_registration_util0.850.850.850.850.85
03a_examination_wait23.3624.440.1423.360.14
03b_examination_util0.860.860.670.860.67
04a_treatment_wait(non_trauma)130.73133.09144.502.15147.81
04b_treatment_util(non_trauma)0.880.880.880.620.88
05_total_time(non-trauma)229.04226.38218.29187.98215.06
06a_trauma_wait166.98189.67166.98166.98189.67
06b_trauma_util0.840.860.840.840.86
07a_treatment_wait(trauma)14.3914.7714.3914.3914.77
07b_treatment_util(trauma)0.520.520.520.520.52
08_total_time(trauma)306.46298.22306.46306.46298.22
09_throughput165.85166.65169.10196.85169.85
\n", - "
" - ], - "text/plain": [ - " base triage+1 exam+1 treat+1 triage+exam\n", - "00_arrivals 227.25 227.25 227.25 227.25 227.25\n", - "01a_triage_wait 32.56 1.26 32.56 32.56 1.26\n", - "01b_triage_util 0.61 0.31 0.61 0.61 0.31\n", - "02a_registration_wait 104.69 131.82 104.69 104.69 131.82\n", - "02b_registration_util 0.85 0.85 0.85 0.85 0.85\n", - "03a_examination_wait 23.36 24.44 0.14 23.36 0.14\n", - "03b_examination_util 0.86 0.86 0.67 0.86 0.67\n", - "04a_treatment_wait(non_trauma) 130.73 133.09 144.50 2.15 147.81\n", - "04b_treatment_util(non_trauma) 0.88 0.88 0.88 0.62 0.88\n", - "05_total_time(non-trauma) 229.04 226.38 218.29 187.98 215.06\n", - "06a_trauma_wait 166.98 189.67 166.98 166.98 189.67\n", - "06b_trauma_util 0.84 0.86 0.84 0.84 0.86\n", - "07a_treatment_wait(trauma) 14.39 14.77 14.39 14.39 14.77\n", - "07b_treatment_util(trauma) 0.52 0.52 0.52 0.52 0.52\n", - "08_total_time(trauma) 306.46 298.22 306.46 306.46 298.22\n", - "09_throughput 165.85 166.65 169.10 196.85 169.85" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# as well as rounding you may want to rename the cols/rows to \n", - "# more readable alternatives.\n", - "summary_frame = scenario_summary_frame(scenario_results)\n", - "summary_frame.round(2)" - ] - }, - { - "cell_type": "markdown", - "id": "29dabdad", - "metadata": {}, - "source": [ - "## 11. Script to produce formatted LaTeX table for paper" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "id": "8f8da295", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Mean waiting time (mins)basetriage+1exam+1treat+1triage+exam
0Triage32.561.2632.5632.561.26
1Registation104.69131.82104.69104.69131.82
2Examination23.3624.440.1423.360.14
3Non-trauma treatment130.73133.09144.502.15147.81
4Trauma stabilisation166.98189.67166.98166.98189.67
5Trauma treatment14.3914.7714.3914.3914.77
\n", - "
" - ], - "text/plain": [ - " Mean waiting time (mins) base triage+1 exam+1 treat+1 triage+exam\n", - "0 Triage 32.56 1.26 32.56 32.56 1.26\n", - "1 Registation 104.69 131.82 104.69 104.69 131.82\n", - "2 Examination 23.36 24.44 0.14 23.36 0.14\n", - "3 Non-trauma treatment 130.73 133.09 144.50 2.15 147.81\n", - "4 Trauma stabilisation 166.98 189.67 166.98 166.98 189.67\n", - "5 Trauma treatment 14.39 14.77 14.39 14.39 14.77" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "HEADER_URL = 'data/tbl_row_headers.csv'\n", - "WAIT = 'wait'\n", - "\n", - "# filter for waiting times only and round to 2dp\n", - "waiting_times = summary_frame[summary_frame.index.str.contains(WAIT)].round(2)\n", - "\n", - "# load formatted table headers\n", - "row_headers = pd.read_csv(HEADER_URL)\n", - "\n", - "# merge and format headers\n", - "waiting_times = waiting_times.reset_index()\n", - "waiting_times = pd.concat([row_headers, waiting_times], axis=1)\n", - "waiting_times = waiting_times.set_index(row_headers.columns[0])\n", - "waiting_times = waiting_times.drop(['index'], axis=1)\n", - "waiting_times = waiting_times.reset_index()\n", - "waiting_times" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "061ea1f8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\\begin{table}\n", - "\\centering\n", - "\\tbl{Simulation results that can be verified by our example reproducible pipeline.}\n", - "\\label{tab:table3}\n", - "\\begin{tabular}{lrrrrr}\n", - "\\toprule\n", - "Mean waiting time (mins) & base & triage+1 & exam+1 & treat+1 & triage+exam \\\\\n", - "\\midrule\n", - " Triage & 32.56 & 1.26 & 32.56 & 32.56 & 1.26 \\\\\n", - " Registation & 104.69 & 131.82 & 104.69 & 104.69 & 131.82 \\\\\n", - " Examination & 23.36 & 24.44 & 0.14 & 23.36 & 0.14 \\\\\n", - " Non-trauma treatment & 130.73 & 133.09 & 144.50 & 2.15 & 147.81 \\\\\n", - " Trauma stabilisation & 166.98 & 189.67 & 166.98 & 166.98 & 189.67 \\\\\n", - " Trauma treatment & 14.39 & 14.77 & 14.39 & 14.39 & 14.77 \\\\\n", - "\\bottomrule\n", - "\\end{tabular}\n", - "\\end{table}\n", - "\n" - ] - } - ], - "source": [ - "# output to file as LaTeX\n", - "OUTPUT_FILE = 'output/table_3.txt'\n", - "LABEL = 'tab:table3'\n", - "CAPTION = 'Simulation results that can be verified by our example reproducible pipeline.'\n", - "\n", - "waiting_times.to_latex(OUTPUT_FILE, index=False, label=LABEL, caption=CAPTION)\n", - "\n", - "# modify LaTeX file -> convert \\caption to \\tbl\n", - "with fileinput.FileInput(OUTPUT_FILE, inplace=1) as latex_file:\n", - " for line in latex_file:\n", - " line = line.replace('caption', 'tbl')\n", - " print(line, end='')\n", - "\n", - "# view LaTeX to verify\n", - "with open(OUTPUT_FILE, \"r+\") as file1:\n", - " print(file1.read())\n" - ] - }, - { - "cell_type": "markdown", - "id": "3707a288", - "metadata": {}, - "source": [ - "## End" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.12" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/content/02_basic_simpy.ipynb b/content/02_basic_simpy.ipynb new file mode 100644 index 0000000..dac5efa --- /dev/null +++ b/content/02_basic_simpy.ipynb @@ -0,0 +1,220 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "50446585-a13f-4c90-9700-58015b670d9c", + "metadata": {}, + "source": [ + "# A first look at simpy\n", + "\n", + "In this tutorial we will make use of **Free and Open Source Software (FOSS)** for discrete-event simulation called `simpy`. \n", + "\n", + "## Why FOSS and simpy?\n", + "An strength of `simpy` is its simplicity and flexibility. As it is part of Python, it is often straightforward to use `simpy` to model complex logic and make use of the SciPy stack! Initially, you will need to write a lot of code (or borrow code from other simulation studies you find online!). But don't worry. As you use `simpy` you will build your own library of reusable code that you can draw on (and build on) for future simulation projects. As `simpy` is FOSS it is useful for research: the model logic is transparent, can be readily shared with others, and can easily link to other data science tools such as those from Machine Learning (e.g. `sklearn` or `pytorch`). \n" + ] + }, + { + "cell_type": "markdown", + "id": "71ec6d31-c791-4945-83fd-c96670e4b492", + "metadata": {}, + "source": [ + "## 1. Imports\n", + "\n", + "The first library we will import is `simpy`. The typical style is to import the whole package as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3e306312-aaa9-425b-b778-ef859f8f882a", + "metadata": {}, + "outputs": [], + "source": [ + "import simpy" + ] + }, + { + "cell_type": "markdown", + "id": "dda6f531-3e2d-4e65-8ad6-0703f09df810", + "metadata": {}, + "source": [ + "We will also need a few other packages in our simulation model. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9f083908-66a9-47ea-af29-c2baaaeef4c4", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import itertools\n", + "import math" + ] + }, + { + "cell_type": "markdown", + "id": "968d7a3d-763d-48a0-9915-421631d1f650", + "metadata": {}, + "source": [ + "## 2. A first example: a hospital pharmacy\n", + "\n", + "In this first example, let's assume (unrealistically) that prescriptions arrive **exactly** 5 minutes apart. To build this model we need the following components:\n", + "\n", + "#### **A simpy environment**\n", + "\n", + "`simpy` has process based worldview. These processes take place in an environment. You can create a environment with the following line of code:\n", + "\n", + "```python\n", + "env = simpy.Environment()\n", + "```\n", + "\n", + "#### **simpy timeouts**\n", + "\n", + "We can introduce **delays** or **activities** into a process. For example these might be the duration of a stay on a ward, or the duration of a operation. In this case we are going to introduce a delay between arrivals (inter-arrival time). In `simpy` you control this with the following method:\n", + "\n", + "```python\n", + "activity_duration = 20\n", + "env.timeout(activity_duration)\n", + "```\n", + "\n", + "#### **generators**\n", + "\n", + "The event process mechanism in `simpy` is implemented using python generators. A basic generator function that yields a new arrival every 5 minutes looks like this:\n", + "\n", + "```python\n", + "def prescription_arrival_generator(env):\n", + " while True:\n", + " yield env.timeout(5.0)\n", + "```\n", + "\n", + "Notice that the generator takes the environment as a parameter. It then internally calls the `env.timeout()` method in an infinite loop.\n", + "\n", + "#### **running a `simpy` model**\n", + "\n", + "Once we have coded the model logic and created an environment instance, there are two remaining instructions we need to code.\n", + "\n", + "1. set the generator up as a simpy process\n", + "\n", + "```python\n", + "env.process(prescription_arrival_generator(env))\n", + "```\n", + "\n", + "2. run the environment for a user specified run length\n", + "\n", + "```python\n", + "env.run(until=25)\n", + "```\n", + "\n", + "The run method handle the infinite loop we set up in `prescription_arrival_generator`. The simulation model has an internal concept of time. It will end execution when its internal clock reaches 25 time units.\n", + "\n", + "**Now that we have covered the basic building blocks, let's code the actual model.** It makes sense to create our model logic first. The code below will generate arrivals every 5 minutes. Note that the function takes an environment object as a parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a6fd524c-7dc4-41c0-876d-3507ce480dfb", + "metadata": {}, + "outputs": [], + "source": [ + "def prescription_arrival_generator(env):\n", + " '''\n", + " Prescriptions arrive with a fixed duration of 5 minutes.\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " '''\n", + " \n", + " # don't worry about the infinite while loop, simpy will\n", + " # exit at the correct time.\n", + " while True:\n", + " \n", + " # sample an inter-arrival time.\n", + " inter_arrival_time = 5.0\n", + " \n", + " # we use the yield keyword instead of return\n", + " yield env.timeout(inter_arrival_time)\n", + " \n", + " # print out the time of the arrival\n", + " print(f'Prescription arrives at: {env.now}')" + ] + }, + { + "cell_type": "markdown", + "id": "aa6042f3-b7a7-4c3a-a5d8-7eb7f3796bf2", + "metadata": {}, + "source": [ + "Now that we have our generator function we can setup the environment, process and call run. We will create a `RUN_LENGTH` parameter that you can change to run the model for different time lengths. What would happen if this was set to 50?" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "f6f74ff5-4c95-400e-8494-42e438b18b90", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prescription arrives at: 5.0\n", + "Prescription arrives at: 10.0\n", + "Prescription arrives at: 15.0\n", + "Prescription arrives at: 20.0\n", + "end of run. simulation clock time = 25\n" + ] + } + ], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 25\n", + "\n", + "# create the simpy environment object\n", + "env = simpy.Environment()\n", + "\n", + "# tell simpy that the `prescription_arrival_generator` is a process\n", + "env.process(prescription_arrival_generator(env))\n", + "\n", + "# run the simulation model\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + }, + { + "cell_type": "markdown", + "id": "c3aa041b-6b8f-4b15-becc-bd966d0eb794", + "metadata": {}, + "source": [ + "## Exercise\n", + "\n", + "Before we learn anything more about `simpy` have a go at the generators exercise. In the exercise you will need to modify the `prescription_arrival_generator` so that it has random arrivals. This exercise test that you have understood the basics of `simpy` and random sampling in `numpy`\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/03a_exercise1.ipynb b/content/03a_exercise1.ipynb new file mode 100644 index 0000000..c06f0ff --- /dev/null +++ b/content/03a_exercise1.ipynb @@ -0,0 +1,150 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f2147e34-ce39-4d8c-a7db-e8acec2b63e0", + "metadata": {}, + "source": [ + "# Generator exercise\n", + "\n", + "To see the solutions please see the [generator exercise solutions notebook](./03b_exercise1_solutions.ipynb)\n" + ] + }, + { + "cell_type": "markdown", + "id": "c034caf3-6766-4c69-af8f-949f45283b37", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85fd4a85-bb77-498c-96ee-c14de89994a2", + "metadata": {}, + "outputs": [], + "source": [ + "import simpy\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "bfab7f97-9cad-419a-8d75-a2ba190edee8", + "metadata": {}, + "source": [ + "## Example code\n", + "\n", + "The code below is taken from the simple pharmacy example. In this code arrivals occur with an IAT of exactly 5 minutes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee2439f2-0d35-41fd-a5e2-4b95954dd5c5", + "metadata": {}, + "outputs": [], + "source": [ + "def prescription_arrival_generator(env):\n", + " '''\n", + " Prescriptions arrive with a fixed duration of\n", + " 5 minutes.\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " '''\n", + " \n", + " # don't worry about the infinite while loop, simpy will\n", + " # exit at the correct time.\n", + " while True:\n", + " \n", + " # sample an inter-arrival time.\n", + " inter_arrival_time = 5.0\n", + " \n", + " # we use the yield keyword instead of return\n", + " yield env.timeout(inter_arrival_time)\n", + " \n", + " # print out the time of the arrival\n", + " print(f'Prescription arrives at: {env.now}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40c495d5-6f55-4c93-99e3-5bfa6cdff36d", + "metadata": {}, + "outputs": [], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 25\n", + "\n", + "# create the simpy environment object\n", + "env = simpy.Environment()\n", + "\n", + "# tell simpy that the `prescription_arrival_generator` is a process\n", + "env.process(prescription_arrival_generator(env))\n", + "\n", + "# run the simulation model\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + }, + { + "cell_type": "markdown", + "id": "6b1bd501-1614-4891-a876-d07bd40c0496", + "metadata": {}, + "source": [ + "### Exercise: modelling a poisson arrival process for prescriptions\n", + "\n", + "**Task:**\n", + "\n", + "* Update `prescription_arrival_generator()` so that inter-arrival times follow an exponential distribution with a mean of 5.0 minutes between arrivals.\n", + "* Use a run length of 25 minutes.\n", + "\n", + "> **Bonus**: try this initially **without** setting a random seed. Then update the method choosing an approach to control random sampling.\n", + "\n", + "**Hints:**\n", + "\n", + "We learnt how to sample using a `numpy` random number generator in the [sampling notebook](./01_sampling.ipynb). The basic form to draw a single sample followed this pattern (note this excludes a random seed).\n", + "\n", + "```python\n", + "rng = np.random.default_rng()\n", + "sample = rng.exponential(scale=12.0)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65823eff-8d0f-4eaf-a531-173c8ab6290c", + "metadata": {}, + "outputs": [], + "source": [ + "# your code here." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/03b_exercise1_solutions.ipynb b/content/03b_exercise1_solutions.ipynb new file mode 100644 index 0000000..a7a94b8 --- /dev/null +++ b/content/03b_exercise1_solutions.ipynb @@ -0,0 +1,381 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f2147e34-ce39-4d8c-a7db-e8acec2b63e0", + "metadata": {}, + "source": [ + "# Exercise 1: Generating random arrivals. \n", + "## SOLUTIONS\n", + "\n", + "> This notebook contains an example solutions to [Exercise 1](./03a_exercise1.ipynb)" + ] + }, + { + "cell_type": "markdown", + "id": "c034caf3-6766-4c69-af8f-949f45283b37", + "metadata": {}, + "source": [ + "## Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "85fd4a85-bb77-498c-96ee-c14de89994a2", + "metadata": {}, + "outputs": [], + "source": [ + "import simpy\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "bfab7f97-9cad-419a-8d75-a2ba190edee8", + "metadata": {}, + "source": [ + "## Example code\n", + "\n", + "The code below is taken from the simple pharmacy example. In this code arrivals occur with an IAT of exactly 5 minutes." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ee2439f2-0d35-41fd-a5e2-4b95954dd5c5", + "metadata": {}, + "outputs": [], + "source": [ + "def prescription_arrival_generator(env):\n", + " '''\n", + " Prescriptions arrive with a fixed duration of\n", + " 5 minutes.\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " '''\n", + " \n", + " # don't worry about the infinite while loop, simpy will\n", + " # exit at the correct time.\n", + " while True:\n", + " \n", + " # sample an inter-arrival time.\n", + " inter_arrival_time = 5.0\n", + " \n", + " # we use the yield keyword instead of return\n", + " yield env.timeout(inter_arrival_time)\n", + " \n", + " # print out the time of the arrival\n", + " print(f'Prescription arrives at: {env.now}')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "40c495d5-6f55-4c93-99e3-5bfa6cdff36d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prescription arrives at: 5.0\n", + "Prescription arrives at: 10.0\n", + "Prescription arrives at: 15.0\n", + "Prescription arrives at: 20.0\n", + "end of run. simulation clock time = 25\n" + ] + } + ], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 25\n", + "\n", + "# create the simpy environment object\n", + "env = simpy.Environment()\n", + "\n", + "# tell simpy that the `prescription_arrival_generator` is a process\n", + "env.process(prescription_arrival_generator(env))\n", + "\n", + "# run the simulation model\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + }, + { + "cell_type": "markdown", + "id": "6b1bd501-1614-4891-a876-d07bd40c0496", + "metadata": {}, + "source": [ + "### Exercise: modelling a poisson arrival process for prescriptions\n", + "\n", + "**Task:**\n", + "\n", + "* Update `prescription_arrival_generator()` so that inter-arrival times follow an exponential distribution with a mean of 5.0 minutes between arrivals.\n", + "* Use a run length of 25 minutes.\n", + "\n", + "> **Bonus**: try this initially **without** setting a random seed. Then update the method choosing an approach to control random sampling." + ] + }, + { + "cell_type": "markdown", + "id": "73cc4a74-9220-437b-a4ff-0c578ad6b701", + "metadata": {}, + "source": [ + "#### Example answer 1" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "14236ac8-d81c-410e-a3b8-ac166118653b", + "metadata": {}, + "outputs": [], + "source": [ + "# example answer\n", + "def prescription_arrival_generator(env, random_seed=None):\n", + " '''\n", + " Prescriptions arrive with a fixed duration of\n", + " 5 minutes.\n", + " \n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " \n", + " random_state: int, optional (default=None)\n", + " if set then used as random seed to control sampling.\n", + " '''\n", + " rs_arrivals = np.random.default_rng(random_seed)\n", + " \n", + " while True:\n", + " inter_arrival_time = rs_arrivals.exponential(5.0)\n", + " yield env.timeout(inter_arrival_time)\n", + " print(f'Prescription arrives at: {env.now}')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1dde3eaf-f020-4850-9207-7b6bb8479098", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prescription arrives at: 13.208116404576343\n", + "Prescription arrives at: 23.351140962530284\n", + "end of run. simulation clock time = 25\n" + ] + } + ], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 25\n", + "\n", + "# create the simpy environment object\n", + "env = simpy.Environment()\n", + "\n", + "# tell simpy that the `prescription_arrival_generator` is a process\n", + "env.process(prescription_arrival_generator(env))\n", + "\n", + "# run the simulation model\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + }, + { + "cell_type": "markdown", + "id": "f52643e4-a098-453e-956b-a8a9178494f7", + "metadata": {}, + "source": [ + "#### Example answer 2\n", + "\n", + "In this solution we first define a class called `Exponential` and pass that as an argument to the generator." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d9535b30-45da-419a-9034-8da7920766a3", + "metadata": {}, + "outputs": [], + "source": [ + "class Exponential:\n", + " '''\n", + " Convenience class for the exponential distribution.\n", + " packages up distribution parameters, seed and random generator.\n", + " '''\n", + " def __init__(self, mean, random_seed=None):\n", + " '''\n", + " Constructor\n", + "\n", + " Params:\n", + " ------\n", + " mean: float\n", + " The mean of the exponential distribution\n", + "\n", + " random_seed: int, optional (default=None)\n", + " A random seed to reproduce samples. If set to none then a unique\n", + " sample is created.\n", + " '''\n", + " self.rand = np.random.default_rng(seed=random_seed)\n", + " self.mean = mean\n", + "\n", + " def sample(self, size=None):\n", + " '''\n", + " Generate a sample from the exponential distribution\n", + "\n", + " Params:\n", + " -------\n", + " size: int, optional (default=None)\n", + " the number of samples to return. If size=None then a single\n", + " sample is returned.\n", + " '''\n", + " return self.rand.exponential(self.mean, size=size)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "b983b6d8-d7c8-4507-9996-5bcbc35bc54b", + "metadata": {}, + "outputs": [], + "source": [ + "# example answer\n", + "def prescription_arrival_generator(env, iat_dist):\n", + " '''\n", + " Prescriptions arrive with a fixed duration of\n", + " 5 minutes.\n", + " \n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " \n", + " iat_dist: object\n", + " A python class that implements a .sample() method\n", + " and generates the IATs\n", + " \n", + " random_state: int, optional (default=None)\n", + " if set then used as random seed to control sampling.\n", + " '''\n", + " \n", + " while True:\n", + " inter_arrival_time = iat_dist.sample()\n", + " yield env.timeout(inter_arrival_time)\n", + " print(f'Prescription arrives at: {env.now}')" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "755cb0b6-c2ba-4166-8ea4-a3c9b2f7b43f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prescription arrives at: 12.021043019829973\n", + "Prescription arrives at: 23.70199129895224\n", + "end of run. simulation clock time = 25\n" + ] + } + ], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 25\n", + "\n", + "# create the simpy environment object\n", + "env = simpy.Environment()\n", + "\n", + "iat = Exponential(mean=5.0, random_seed=42)\n", + "\n", + "# tell simpy that the `prescription_arrival_generator` is a process\n", + "env.process(prescription_arrival_generator(env, iat))\n", + "\n", + "# run the simulation model\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + }, + { + "cell_type": "markdown", + "id": "4a605ad5-c95d-48e8-aacc-a7b33ab9b010", + "metadata": {}, + "source": [ + "### Why would we use solution 2?" + ] + }, + { + "cell_type": "markdown", + "id": "1e7ae3d1-742b-424c-9c3e-1a697fbe52a0", + "metadata": {}, + "source": [ + "Solution 2 is a useful approach as it is now easy to define new experiments. For example, we could experiment with the mean of the exponential or use an entirely different distribution (as long as it implements `.sample()`) without changing our generator function. For example. " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cacab88e-0f8c-461c-9ff0-3074c20dd1c8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Prescription arrives at: 6.0105215099149865\n", + "Prescription arrives at: 11.85099564947612\n", + "Prescription arrives at: 17.812898149161757\n", + "Prescription arrives at: 18.512383873790714\n", + "Prescription arrives at: 18.728477373036657\n", + "Prescription arrives at: 22.360128662302035\n", + "end of run. simulation clock time = 25\n" + ] + } + ], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 25\n", + "\n", + "# create the simpy environment object\n", + "env = simpy.Environment()\n", + "\n", + "# *** MODIFICATION: reduce IAT.\n", + "# Note: with this method we could use a different distribution should as Erlang\n", + "iat = Exponential(mean=2.5, random_seed=42)\n", + "\n", + "# tell simpy that the `prescription_arrival_generator` is a process\n", + "env.process(prescription_arrival_generator(env, iat))\n", + "\n", + "# run the simulation model\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/04_111_model.ipynb b/content/04_111_model.ipynb new file mode 100644 index 0000000..67416e1 --- /dev/null +++ b/content/04_111_model.ipynb @@ -0,0 +1,711 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "415a5a10-1110-4ee5-aa48-b01680a423b9", + "metadata": {}, + "source": [ + "# A full model\n", + "\n", + "We will now build a full `simpy` process model. In this example we will build a 111 call centre queuing model. We will include random arrivals and resources. We will keep this simple, and gradually add in detail and flexibility to our design." + ] + }, + { + "cell_type": "markdown", + "id": "21b7aade-0ead-4203-adc9-8f22b90f4b93", + "metadata": {}, + "source": [ + "## 1. Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "56227fa4-b69e-4760-bb5b-2982a804bca6", + "metadata": {}, + "outputs": [], + "source": [ + "import simpy\n", + "import numpy as np\n", + "import itertools" + ] + }, + { + "cell_type": "markdown", + "id": "52ca88d4-14e2-4650-b7ac-20b81e1e7f40", + "metadata": {}, + "source": [ + "## 2. Problem background\n", + "\n", + "Call operators in an 111 (urgent care) service receive calls at a rate of 100 per hour. Call length can be represented by a triangular distribution. Calls last between 5 minutes and 15 minutes. Most calls last 7 minutes. There are 13 call operators. \n", + "\n", + "> We will build a `simpy` model of the process." + ] + }, + { + "cell_type": "markdown", + "id": "78d33d8d-1640-493d-b959-36c1f2af0ca9", + "metadata": {}, + "source": [ + "## 3. `simpy` resources\n", + "\n", + "To model the call centre we need to introduce a **resource**. If a resource is not available then a process will pause. We create a resource as follows:\n", + "\n", + "```python\n", + "operators = simpy.Resource(env, capacity=20)\n", + "```\n", + "\n", + "When we want to request a resource in our process we create a with block as follows:\n", + "\n", + "```python\n", + "with operators.request() as req:\n", + " yield req\n", + "```\n", + "\n", + "This tells simpy that your process needs an operator resource to progress. The code will pause until a resource is yielded. This gives us our queuing effect. If a resource is not available immediately then the process will wait until one becomes available. We will practice this a lot in the course so do not worry if this does not make sense immediately." + ] + }, + { + "cell_type": "markdown", + "id": "43c7fc53-7a02-44b7-9a41-0e454bcdc328", + "metadata": {}, + "source": [ + "## 4. The service function.\n", + "\n", + "We will first build the service function. We need to do this because our arrival/generator function will be required to call it.\n", + "\n", + "We need to include the following logic:\n", + "\n", + "1. Request and if necessary wait for a call operator\n", + "2. Undergo phone triage (a delay). This is a sample from the Triangular distribution.\n", + "3. Exit the system.\n", + "\n", + "We will build this as a simple python function that we will call `service`. Each patient caller that arrives to the simulation will execute service as a `simpy` process. We will pass a unique patient identifier, a pool of operator resources and the environment to the function." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "25279edf-dc3b-4f72-95af-e9ea7cedac57", + "metadata": {}, + "outputs": [], + "source": [ + "def service(identifier, operators, env, service_rng):\n", + " '''\n", + " Simulates the service process for a call operator\n", + "\n", + " 1. request and wait for a call operator\n", + " 2. phone triage (triangular)\n", + " 3. exit system\n", + " \n", + " Params:\n", + " ------\n", + " \n", + " identifier: int \n", + " A unique identifer for this caller\n", + " \n", + " operators: simpy.Resource\n", + " The pool of call operators that answer calls\n", + " These are shared across resources.\n", + " \n", + " env: simpy.Environment\n", + " The current environent the simulation is running in\n", + " We use this to pause and restart the process after a delay.\n", + "\n", + " service_rng: numpy.random.Generator\n", + " The random number generator used to sample service times\n", + " \n", + " '''\n", + " # record the time that call entered the queue\n", + " start_wait = env.now\n", + "\n", + " # request an operator\n", + " with operators.request() as req:\n", + " yield req\n", + "\n", + " # record the waiting time for call to be answered\n", + " waiting_time = env.now - start_wait\n", + " print(f'operator answered call {identifier} at ' \\\n", + " + f'{env.now:.3f}')\n", + "\n", + " # sample call duration.\n", + " call_duration = service_rng.triangular(left=5.0, mode=7.0,\n", + " right=10.0)\n", + " \n", + " # schedule process to begin again after call_duration\n", + " yield env.timeout(call_duration)\n", + "\n", + " # print out information for patient.\n", + " print(f'call {identifier} ended {env.now:.3f}; ' \\\n", + " + f'waiting time was {waiting_time:.3f}')" + ] + }, + { + "cell_type": "markdown", + "id": "ca62c3c9-de40-40ca-8971-3b5846e4d57c", + "metadata": {}, + "source": [ + "## 5. The generator function.\n", + "\n", + "The generator function is very similar to the pharamacy example. \n", + "\n", + "> **Notice the pattern**. For most models you can just cut, paste and modify code you have used before." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1e28c3bc-4d8f-48a4-ad71-db303e29ea73", + "metadata": {}, + "outputs": [], + "source": [ + "def arrivals_generator(env, operators):\n", + " '''\n", + " IAT is exponentially distributed\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " The simpy environment for the simulation\n", + "\n", + " operators: simpy.Resource\n", + " the pool of call operators.\n", + " '''\n", + " # create the arrival process rng \n", + " arrival_rng = np.random.default_rng()\n", + " \n", + " # create the service rng that we pass to each service process created\n", + " service_rng = np.random.default_rng()\n", + " \n", + " # use itertools as it provides an infinite loop \n", + " # with a counter variable that we can use for unique Ids\n", + " for caller_count in itertools.count(start=1):\n", + "\n", + " # 100 calls per hour (sim time units = minutes). \n", + " inter_arrival_time = arrival_rng.exponential(60/100)\n", + " yield env.timeout(inter_arrival_time)\n", + "\n", + " print(f'call arrives at: {env.now:.3f}')\n", + "\n", + " # create a new simpy process for serving this caller.\n", + " # we pass in the caller id, the operator resources, env, and the rng\n", + " env.process(service(caller_count, operators, env, service_rng))" + ] + }, + { + "cell_type": "markdown", + "id": "12fe16cb-23f3-4223-99af-5c31b546c7af", + "metadata": {}, + "source": [ + "## 6. Run the model" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7f7ca57f-c672-46ca-9b77-85b0b08b04e7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "call arrives at: 0.206\n", + "operator answered call 1 at 0.206\n", + "call arrives at: 1.467\n", + "operator answered call 2 at 1.467\n", + "call arrives at: 1.739\n", + "operator answered call 3 at 1.739\n", + "call arrives at: 2.400\n", + "operator answered call 4 at 2.400\n", + "call arrives at: 2.580\n", + "operator answered call 5 at 2.580\n", + "call arrives at: 2.755\n", + "operator answered call 6 at 2.755\n", + "call arrives at: 3.846\n", + "operator answered call 7 at 3.846\n", + "call arrives at: 5.182\n", + "operator answered call 8 at 5.182\n", + "call arrives at: 5.963\n", + "operator answered call 9 at 5.963\n", + "call arrives at: 6.631\n", + "operator answered call 10 at 6.631\n", + "call arrives at: 7.189\n", + "operator answered call 11 at 7.189\n", + "call 1 ended 7.567; waiting time was 0.000\n", + "call arrives at: 8.082\n", + "operator answered call 12 at 8.082\n", + "call 3 ended 8.435; waiting time was 0.000\n", + "call arrives at: 8.486\n", + "operator answered call 13 at 8.486\n", + "call arrives at: 8.802\n", + "operator answered call 14 at 8.802\n", + "call 2 ended 9.167; waiting time was 0.000\n", + "call 5 ended 9.546; waiting time was 0.000\n", + "call 6 ended 9.776; waiting time was 0.000\n", + "call arrives at: 9.818\n", + "operator answered call 15 at 9.818\n", + "call arrives at: 10.055\n", + "operator answered call 16 at 10.055\n", + "call 7 ended 10.222; waiting time was 0.000\n", + "call 4 ended 10.985; waiting time was 0.000\n", + "call arrives at: 11.905\n", + "operator answered call 17 at 11.905\n", + "call 8 ended 12.315; waiting time was 0.000\n", + "call arrives at: 12.623\n", + "operator answered call 18 at 12.623\n", + "call 9 ended 12.746; waiting time was 0.000\n", + "call arrives at: 12.908\n", + "operator answered call 19 at 12.908\n", + "call arrives at: 13.211\n", + "operator answered call 20 at 13.211\n", + "call arrives at: 13.308\n", + "operator answered call 21 at 13.308\n", + "call 12 ended 13.538; waiting time was 0.000\n", + "call arrives at: 14.291\n", + "operator answered call 22 at 14.291\n", + "call 14 ended 14.788; waiting time was 0.000\n", + "call 13 ended 15.541; waiting time was 0.000\n", + "call arrives at: 15.589\n", + "operator answered call 23 at 15.589\n", + "call 10 ended 16.091; waiting time was 0.000\n", + "call arrives at: 16.366\n", + "operator answered call 24 at 16.366\n", + "call arrives at: 16.701\n", + "operator answered call 25 at 16.701\n", + "call 16 ended 16.986; waiting time was 0.000\n", + "call 11 ended 17.134; waiting time was 0.000\n", + "call arrives at: 17.273\n", + "operator answered call 26 at 17.273\n", + "call 17 ended 18.467; waiting time was 0.000\n", + "call 18 ended 18.500; waiting time was 0.000\n", + "call 21 ended 18.746; waiting time was 0.000\n", + "call 15 ended 19.069; waiting time was 0.000\n", + "call arrives at: 19.349\n", + "operator answered call 27 at 19.349\n", + "call arrives at: 19.784\n", + "operator answered call 28 at 19.784\n", + "call 19 ended 20.379; waiting time was 0.000\n", + "call arrives at: 21.329\n", + "operator answered call 29 at 21.329\n", + "call 20 ended 21.633; waiting time was 0.000\n", + "call 23 ended 21.959; waiting time was 0.000\n", + "call arrives at: 22.076\n", + "operator answered call 30 at 22.076\n", + "call 22 ended 22.090; waiting time was 0.000\n", + "call arrives at: 22.432\n", + "operator answered call 31 at 22.432\n", + "call arrives at: 23.274\n", + "operator answered call 32 at 23.274\n", + "call arrives at: 23.564\n", + "operator answered call 33 at 23.564\n", + "call 26 ended 23.601; waiting time was 0.000\n", + "call 25 ended 23.671; waiting time was 0.000\n", + "call arrives at: 23.944\n", + "operator answered call 34 at 23.944\n", + "call arrives at: 24.421\n", + "operator answered call 35 at 24.421\n", + "call 24 ended 24.902; waiting time was 0.000\n", + "call arrives at: 25.242\n", + "operator answered call 36 at 25.242\n", + "call arrives at: 25.456\n", + "operator answered call 37 at 25.456\n", + "call arrives at: 25.582\n", + "operator answered call 38 at 25.582\n", + "call arrives at: 25.857\n", + "operator answered call 39 at 25.857\n", + "call 27 ended 26.555; waiting time was 0.000\n", + "call arrives at: 27.008\n", + "operator answered call 40 at 27.008\n", + "call arrives at: 27.363\n", + "call 29 ended 27.854; waiting time was 0.000\n", + "operator answered call 41 at 27.854\n", + "call 28 ended 27.897; waiting time was 0.000\n", + "call arrives at: 28.053\n", + "operator answered call 42 at 28.053\n", + "call arrives at: 29.103\n", + "call 30 ended 29.443; waiting time was 0.000\n", + "operator answered call 43 at 29.443\n", + "call 31 ended 29.738; waiting time was 0.000\n", + "call arrives at: 30.774\n", + "operator answered call 44 at 30.774\n", + "call arrives at: 30.777\n", + "call arrives at: 30.797\n", + "call 35 ended 31.114; waiting time was 0.000\n", + "operator answered call 45 at 31.114\n", + "call 32 ended 31.156; waiting time was 0.000\n", + "operator answered call 46 at 31.156\n", + "call 37 ended 31.180; waiting time was 0.000\n", + "call 34 ended 31.289; waiting time was 0.000\n", + "call 36 ended 31.349; waiting time was 0.000\n", + "call 38 ended 31.883; waiting time was 0.000\n", + "call 39 ended 31.980; waiting time was 0.000\n", + "call arrives at: 32.670\n", + "operator answered call 47 at 32.670\n", + "call 40 ended 32.921; waiting time was 0.000\n", + "call 33 ended 33.502; waiting time was 0.000\n", + "call arrives at: 33.786\n", + "operator answered call 48 at 33.786\n", + "call 42 ended 33.910; waiting time was 0.000\n", + "call arrives at: 34.620\n", + "operator answered call 49 at 34.620\n", + "call arrives at: 34.760\n", + "operator answered call 50 at 34.760\n", + "call 41 ended 34.815; waiting time was 0.492\n", + "call arrives at: 35.455\n", + "operator answered call 51 at 35.455\n", + "call arrives at: 35.510\n", + "operator answered call 52 at 35.510\n", + "call arrives at: 35.891\n", + "operator answered call 53 at 35.891\n", + "call arrives at: 36.131\n", + "operator answered call 54 at 36.131\n", + "call 43 ended 36.272; waiting time was 0.340\n", + "call arrives at: 36.617\n", + "operator answered call 55 at 36.617\n", + "call 44 ended 37.110; waiting time was 0.000\n", + "call arrives at: 37.363\n", + "operator answered call 56 at 37.363\n", + "call arrives at: 38.463\n", + "operator answered call 57 at 38.463\n", + "call 46 ended 38.602; waiting time was 0.360\n", + "call 47 ended 38.978; waiting time was 0.000\n", + "call arrives at: 39.262\n", + "operator answered call 58 at 39.262\n", + "call arrives at: 39.990\n", + "operator answered call 59 at 39.990\n", + "call arrives at: 40.086\n", + "call 45 ended 40.231; waiting time was 0.337\n", + "operator answered call 60 at 40.231\n", + "call arrives at: 40.542\n", + "call arrives at: 40.559\n", + "call 50 ended 40.921; waiting time was 0.000\n", + "operator answered call 61 at 40.921\n", + "call 54 ended 42.381; waiting time was 0.000\n", + "operator answered call 62 at 42.381\n", + "call 49 ended 42.458; waiting time was 0.000\n", + "call 51 ended 42.758; waiting time was 0.000\n", + "call 53 ended 42.975; waiting time was 0.000\n", + "call 48 ended 42.994; waiting time was 0.000\n", + "call arrives at: 43.088\n", + "operator answered call 63 at 43.088\n", + "call arrives at: 43.354\n", + "operator answered call 64 at 43.354\n", + "call 55 ended 44.480; waiting time was 0.000\n", + "call 56 ended 44.481; waiting time was 0.000\n", + "call 52 ended 44.621; waiting time was 0.000\n", + "call arrives at: 44.670\n", + "operator answered call 65 at 44.670\n", + "call arrives at: 44.771\n", + "operator answered call 66 at 44.771\n", + "call arrives at: 45.164\n", + "operator answered call 67 at 45.164\n", + "call arrives at: 45.283\n", + "operator answered call 68 at 45.283\n", + "call arrives at: 46.300\n", + "operator answered call 69 at 46.300\n", + "call 59 ended 46.961; waiting time was 0.000\n", + "call 58 ended 47.431; waiting time was 0.000\n", + "call 57 ended 47.536; waiting time was 0.000\n", + "call arrives at: 47.556\n", + "operator answered call 70 at 47.556\n", + "call arrives at: 48.068\n", + "operator answered call 71 at 48.068\n", + "call arrives at: 48.396\n", + "operator answered call 72 at 48.396\n", + "call arrives at: 48.748\n", + "call 60 ended 48.844; waiting time was 0.145\n", + "operator answered call 73 at 48.844\n", + "call 61 ended 49.092; waiting time was 0.379\n", + "call arrives at: 49.183\n", + "operator answered call 74 at 49.183\n", + "call 62 ended 49.855; waiting time was 1.822\n", + "call arrives at: 50.010\n", + "operator answered call 75 at 50.010\n", + "call arrives at: 50.228\n", + "call arrives at: 50.352\n", + "call arrives at: 50.430\n", + "call arrives at: 50.525\n", + "call 64 ended 50.651; waiting time was 0.000\n", + "operator answered call 76 at 50.651\n", + "call 63 ended 50.712; waiting time was 0.000\n", + "operator answered call 77 at 50.712\n", + "call arrives at: 50.814\n", + "call arrives at: 50.828\n", + "call arrives at: 50.903\n", + "call arrives at: 51.040\n", + "call arrives at: 51.206\n", + "call arrives at: 51.217\n", + "call arrives at: 51.877\n", + "call arrives at: 52.080\n", + "call arrives at: 52.159\n", + "call 67 ended 52.455; waiting time was 0.000\n", + "operator answered call 78 at 52.455\n", + "call 68 ended 52.513; waiting time was 0.000\n", + "operator answered call 79 at 52.513\n", + "call 66 ended 52.545; waiting time was 0.000\n", + "operator answered call 80 at 52.545\n", + "call arrives at: 52.582\n", + "call 65 ended 52.768; waiting time was 0.000\n", + "operator answered call 81 at 52.768\n", + "call arrives at: 53.145\n", + "call arrives at: 53.239\n", + "call arrives at: 53.384\n", + "call 70 ended 53.447; waiting time was 0.000\n", + "operator answered call 82 at 53.447\n", + "call arrives at: 54.080\n", + "call 73 ended 54.197; waiting time was 0.096\n", + "operator answered call 83 at 54.197\n", + "call 71 ended 54.508; waiting time was 0.000\n", + "operator answered call 84 at 54.508\n", + "call arrives at: 54.526\n", + "call arrives at: 54.712\n", + "call arrives at: 54.941\n", + "call 74 ended 55.178; waiting time was 0.000\n", + "operator answered call 85 at 55.178\n", + "call arrives at: 55.724\n", + "call 69 ended 55.832; waiting time was 0.000\n", + "operator answered call 86 at 55.832\n", + "call 72 ended 56.192; waiting time was 0.000\n", + "operator answered call 87 at 56.192\n", + "call arrives at: 56.642\n", + "call 75 ended 56.648; waiting time was 0.000\n", + "operator answered call 88 at 56.648\n", + "call arrives at: 57.010\n", + "call arrives at: 57.590\n", + "call arrives at: 57.666\n", + "call 77 ended 57.924; waiting time was 0.361\n", + "operator answered call 89 at 57.924\n", + "call arrives at: 58.218\n", + "call 80 ended 58.401; waiting time was 1.730\n", + "operator answered call 90 at 58.401\n", + "call arrives at: 59.792\n", + "call arrives at: 60.018\n", + "call 76 ended 60.131; waiting time was 0.423\n", + "operator answered call 91 at 60.131\n", + "call arrives at: 60.248\n", + "call 83 ended 60.579; waiting time was 3.157\n", + "operator answered call 92 at 60.579\n", + "call 78 ended 60.912; waiting time was 2.025\n", + "operator answered call 93 at 60.912\n", + "call 79 ended 60.973; waiting time was 1.988\n", + "operator answered call 94 at 60.973\n", + "call 84 ended 60.998; waiting time was 3.302\n", + "operator answered call 95 at 60.998\n", + "call 81 ended 61.303; waiting time was 1.940\n", + "operator answered call 96 at 61.303\n", + "call 82 ended 61.466; waiting time was 2.545\n", + "operator answered call 97 at 61.466\n", + "call 87 ended 61.870; waiting time was 4.112\n", + "operator answered call 98 at 61.870\n", + "call arrives at: 62.158\n", + "call 88 ended 63.709; waiting time was 4.489\n", + "operator answered call 99 at 63.709\n", + "call 85 ended 63.918; waiting time was 3.961\n", + "operator answered call 100 at 63.918\n", + "call 86 ended 63.925; waiting time was 3.955\n", + "operator answered call 101 at 63.925\n", + "call arrives at: 64.526\n", + "call arrives at: 65.682\n", + "call 89 ended 66.457; waiting time was 5.342\n", + "operator answered call 102 at 66.457\n", + "call 90 ended 66.677; waiting time was 5.256\n", + "operator answered call 103 at 66.677\n", + "call 94 ended 67.083; waiting time was 6.447\n", + "operator answered call 104 at 67.083\n", + "call arrives at: 67.892\n", + "call 95 ended 68.481; waiting time was 6.286\n", + "operator answered call 105 at 68.481\n", + "call 96 ended 68.498; waiting time was 6.363\n", + "operator answered call 106 at 68.498\n", + "call 91 ended 68.617; waiting time was 6.892\n", + "operator answered call 107 at 68.617\n", + "call 98 ended 69.027; waiting time was 5.228\n", + "operator answered call 108 at 69.027\n", + "call 92 ended 69.359; waiting time was 7.195\n", + "operator answered call 109 at 69.359\n", + "call 99 ended 70.255; waiting time was 6.699\n", + "call arrives at: 70.265\n", + "operator answered call 110 at 70.265\n", + "call 100 ended 70.581; waiting time was 6.328\n", + "call 93 ended 70.735; waiting time was 6.832\n", + "call 97 ended 71.365; waiting time was 5.742\n", + "call arrives at: 71.744\n", + "operator answered call 111 at 71.744\n", + "call arrives at: 72.067\n", + "operator answered call 112 at 72.067\n", + "call 101 ended 73.262; waiting time was 6.260\n", + "call arrives at: 73.673\n", + "operator answered call 113 at 73.673\n", + "call arrives at: 73.998\n", + "operator answered call 114 at 73.998\n", + "call 104 ended 74.344; waiting time was 7.065\n", + "call arrives at: 74.753\n", + "operator answered call 115 at 74.753\n", + "call 103 ended 74.754; waiting time was 6.885\n", + "call arrives at: 75.058\n", + "operator answered call 116 at 75.058\n", + "call 107 ended 75.217; waiting time was 4.092\n", + "call 102 ended 75.437; waiting time was 8.238\n", + "call 106 ended 75.439; waiting time was 6.340\n", + "call arrives at: 75.504\n", + "operator answered call 117 at 75.504\n", + "call arrives at: 76.040\n", + "operator answered call 118 at 76.040\n", + "call arrives at: 76.408\n", + "operator answered call 119 at 76.408\n", + "call 105 ended 76.675; waiting time was 8.233\n", + "call arrives at: 77.107\n", + "operator answered call 120 at 77.107\n", + "call 108 ended 77.269; waiting time was 3.345\n", + "call arrives at: 77.287\n", + "operator answered call 121 at 77.287\n", + "call arrives at: 77.299\n", + "call 110 ended 77.631; waiting time was 0.000\n", + "operator answered call 122 at 77.631\n", + "call arrives at: 77.768\n", + "call 109 ended 78.550; waiting time was 1.467\n", + "operator answered call 123 at 78.550\n", + "call arrives at: 79.101\n", + "call 113 ended 80.134; waiting time was 0.000\n", + "operator answered call 124 at 80.134\n", + "call arrives at: 80.496\n", + "call 115 ended 80.517; waiting time was 0.000\n", + "operator answered call 125 at 80.517\n", + "call arrives at: 80.719\n", + "call 116 ended 80.925; waiting time was 0.000\n", + "operator answered call 126 at 80.925\n", + "call 114 ended 80.935; waiting time was 0.000\n", + "call arrives at: 81.382\n", + "operator answered call 127 at 81.382\n", + "call 112 ended 81.527; waiting time was 0.000\n", + "call 111 ended 81.710; waiting time was 0.000\n", + "call arrives at: 82.265\n", + "operator answered call 128 at 82.265\n", + "call 121 ended 83.086; waiting time was 0.000\n", + "call 119 ended 83.340; waiting time was 0.000\n", + "call 118 ended 83.505; waiting time was 0.000\n", + "call 123 ended 84.170; waiting time was 0.783\n", + "call 122 ended 84.247; waiting time was 0.331\n", + "call arrives at: 84.578\n", + "operator answered call 129 at 84.578\n", + "call 117 ended 84.579; waiting time was 0.000\n", + "call 120 ended 84.642; waiting time was 0.000\n", + "call arrives at: 84.657\n", + "operator answered call 130 at 84.657\n", + "call arrives at: 85.147\n", + "operator answered call 131 at 85.147\n", + "call arrives at: 85.520\n", + "operator answered call 132 at 85.520\n", + "call arrives at: 85.584\n", + "operator answered call 133 at 85.584\n", + "call arrives at: 85.779\n", + "operator answered call 134 at 85.779\n", + "call arrives at: 85.787\n", + "operator answered call 135 at 85.787\n", + "call arrives at: 86.005\n", + "operator answered call 136 at 86.005\n", + "call arrives at: 86.113\n", + "call arrives at: 86.472\n", + "call arrives at: 86.553\n", + "call arrives at: 86.729\n", + "call 125 ended 87.304; waiting time was 0.021\n", + "operator answered call 137 at 87.304\n", + "call 124 ended 87.728; waiting time was 1.033\n", + "operator answered call 138 at 87.728\n", + "call arrives at: 87.955\n", + "call arrives at: 88.300\n", + "call arrives at: 88.396\n", + "call 126 ended 89.162; waiting time was 0.206\n", + "operator answered call 139 at 89.162\n", + "call 128 ended 89.438; waiting time was 0.000\n", + "operator answered call 140 at 89.438\n", + "call arrives at: 89.973\n", + "call 127 ended 90.321; waiting time was 0.000\n", + "operator answered call 141 at 90.321\n", + "call 130 ended 90.461; waiting time was 0.000\n", + "operator answered call 142 at 90.461\n", + "call arrives at: 90.541\n", + "call arrives at: 90.730\n", + "call 133 ended 91.088; waiting time was 0.000\n", + "operator answered call 143 at 91.088\n", + "call arrives at: 91.737\n", + "call 136 ended 91.854; waiting time was 0.000\n", + "operator answered call 144 at 91.854\n", + "call 129 ended 92.077; waiting time was 0.000\n", + "operator answered call 145 at 92.077\n", + "call 132 ended 92.202; waiting time was 0.000\n", + "operator answered call 146 at 92.202\n", + "call arrives at: 92.226\n", + "call 131 ended 92.296; waiting time was 0.000\n", + "operator answered call 147 at 92.296\n", + "call arrives at: 92.496\n", + "call 137 ended 93.184; waiting time was 1.191\n", + "operator answered call 148 at 93.184\n", + "call arrives at: 93.270\n", + "call 135 ended 93.489; waiting time was 0.000\n", + "operator answered call 149 at 93.489\n", + "call arrives at: 93.579\n", + "call arrives at: 93.839\n", + "call 134 ended 94.408; waiting time was 0.000\n", + "operator answered call 150 at 94.408\n", + "call arrives at: 95.261\n", + "call 139 ended 95.628; waiting time was 2.609\n", + "operator answered call 151 at 95.628\n", + "call 140 ended 96.103; waiting time was 2.709\n", + "operator answered call 152 at 96.103\n", + "call 141 ended 96.259; waiting time was 2.365\n", + "operator answered call 153 at 96.259\n", + "call arrives at: 96.335\n", + "call arrives at: 96.499\n", + "call 138 ended 96.872; waiting time was 1.256\n", + "operator answered call 154 at 96.872\n", + "call 142 ended 98.089; waiting time was 2.161\n", + "operator answered call 155 at 98.089\n", + "call 144 ended 98.753; waiting time was 1.881\n", + "call 145 ended 98.958; waiting time was 1.536\n", + "call 143 ended 99.276; waiting time was 2.693\n", + "call arrives at: 99.625\n", + "operator answered call 156 at 99.625\n", + "call 149 ended 99.992; waiting time was 0.992\n", + "end of run. simulation clock time = 100\n" + ] + } + ], + "source": [ + "# model parameters\n", + "RUN_LENGTH = 100\n", + "N_OPERATORS = 13\n", + "\n", + "# create simpy environment and operator resources\n", + "env = simpy.Environment()\n", + "operators = simpy.Resource(env, capacity=N_OPERATORS)\n", + "\n", + "env.process(arrivals_generator(env, operators))\n", + "env.run(until=RUN_LENGTH)\n", + "print(f'end of run. simulation clock time = {env.now}')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/05_basic_results.ipynb b/content/05_basic_results.ipynb new file mode 100644 index 0000000..36f6486 --- /dev/null +++ b/content/05_basic_results.ipynb @@ -0,0 +1,320 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c3d8c177-68a5-4bda-9879-57fb9a12eefe", + "metadata": {}, + "source": [ + "# Collecting results from a single run\n", + "\n", + "A tool like `simpy` allows you to collect your data flexibly using an approach that makes sense to you! Some options are:\n", + "\n", + "1. **Code an auditor / observer process**. This process will periodically observe the state of the system. We can use this to collect information on **current state at time t**. For example, how many patients are queuing and how many have a call in progress between by time of day. \n", + "\n", + "2. **Store process metrics during a run and perform calculations at the end of a run**. For example, if you want to calculate mean patient waiting time then store each patient waiting time in a list and calculate the mean at the end of the run.\n", + "\n", + "3. **Conduct and audit or calculate running statistics as the simulation executes an event**. For example, as a patient completes a call we can calculate a running mean of waiting times and a running total of the operators are taking calls. The latter measure can then be used to calculate server utilisation. You could also use this approach to audit queue length where the queue length is recorded each time request for a resource is made (and/or when a resource is released).\n" + ] + }, + { + "cell_type": "markdown", + "id": "88f8f4d3-6929-4b12-869c-0c4518c8a4f5", + "metadata": {}, + "source": [ + "## 1. Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "98ae291f-9430-43ee-8214-68d2deaadc5e", + "metadata": {}, + "outputs": [], + "source": [ + "import simpy\n", + "import numpy as np\n", + "import itertools" + ] + }, + { + "cell_type": "markdown", + "id": "ef577b1b-fb31-4636-a314-505821e1a600", + "metadata": {}, + "source": [ + "## 2. Calculating mean waiting time\n", + "\n", + "The second strategy to results collection is to store either a reference to a quantitative value (e.g. waiting time) during the run. Once the run is complete you will need to include a procedure for computing the metric of interest. **An advantage** of this strategy is that it is very simple, captures all data, and has minimal computational overhead during a model run! **A potential disadvantage** is that for complex simulation you may end up storing a large amount of data in memory. In these circumstances, it may be worth exploring event driven strategies to reduce memory requirements.\n", + "\n", + "In our example, we will store each patient's waiting time in a python list. At the end of the run we will loop through these references and calculate **mean waiting time** and **operator utilisation**. \n", + "\n", + "To do this I'm going to declare a list with notebook level scope. The `service` function will then append a `waiting_time` for a caller to the list each time the caller enters service. " + ] + }, + { + "cell_type": "markdown", + "id": "77a6624a-3349-4fb4-8e4d-b65fa8984337", + "metadata": {}, + "source": [ + "### 2.1 Notebook level variables for results collection.\n", + "\n", + "We will create a python dictionary called `results` to store result collection variables. This means that it is simple to add new variables in at a later date.\n", + "\n", + "The dictionary has notebook level scope. This means that any functions or class in the notebook can access and/or append to the list access via the key `waiting_times`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "47b4dfd8-5860-4f79-8be5-dc39f387be3b", + "metadata": {}, + "outputs": [], + "source": [ + "results = {}\n", + "results['waiting_times'] = []" + ] + }, + { + "cell_type": "markdown", + "id": "19b3400d-bf47-4998-a981-eab294e74bc0", + "metadata": {}, + "source": [ + "### 2.2 A helper function\n", + "\n", + "We will create a helper function called trace that wraps `print`. We can set a variable called `TRACE` that switches printing patient level results on and off." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4eed6a29-63bb-4e1f-a0c5-ae3733498255", + "metadata": {}, + "outputs": [], + "source": [ + "def trace(msg):\n", + " '''\n", + " Turing printing of events on and off.\n", + " \n", + " Params:\n", + " -------\n", + " msg: str\n", + " string to print to screen.\n", + " '''\n", + " if TRACE:\n", + " print(msg)" + ] + }, + { + "cell_type": "markdown", + "id": "ebeb9512-f28f-41bb-a8d2-50164bfc31f7", + "metadata": {}, + "source": [ + "## 2.3 Service and arrival functions\n", + "\n", + "The only modification we need to make is to the `service` function. We will add in a line of code to record the `waiting_time` of the caller as they enter service." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "a6bd172d-a609-497e-9656-6533539a7490", + "metadata": {}, + "outputs": [], + "source": [ + "def service(identifier, operators, env, service_rng):\n", + " '''\n", + " Simulates the service process for a call operator\n", + "\n", + " 1. request and wait for a call operator\n", + " 2. phone triage (triangular)\n", + " 3. exit system\n", + " \n", + " Params:\n", + " ------\n", + " \n", + " identifier: int \n", + " A unique identifer for this caller\n", + " \n", + " operators: simpy.Resource\n", + " The pool of call operators that answer calls\n", + " These are shared across resources.\n", + " \n", + " env: simpy.Environment\n", + " The current environent the simulation is running in\n", + " We use this to pause and restart the process after a delay.\n", + "\n", + " service_rng: numpy.random.Generator\n", + " The random number generator used to sample service times\n", + " \n", + " '''\n", + " # record the time that call entered the queue\n", + " start_wait = env.now\n", + "\n", + " # request an operator\n", + " with operators.request() as req:\n", + " yield req\n", + "\n", + " # record the waiting time for call to be answered\n", + " waiting_time = env.now - start_wait\n", + " results['waiting_times'].append(waiting_time)\n", + " \n", + " trace(f'operator answered call {identifier} at ' \\\n", + " + f'{env.now:.3f}')\n", + "\n", + " # sample call duration.\n", + " call_duration = service_rng.triangular(left=5.0, mode=7.0,\n", + " right=10.0)\n", + " \n", + " # schedule process to begin again after call_duration\n", + " yield env.timeout(call_duration)\n", + "\n", + " \n", + "\n", + " # print out information for patient.\n", + " trace(f'call {identifier} ended {env.now:.3f}; ' \\\n", + " + f'waiting time was {waiting_time:.3f}')" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1b640813-7b18-4aa3-8cce-c5098f3a2349", + "metadata": {}, + "outputs": [], + "source": [ + "def arrivals_generator(env, operators):\n", + " '''\n", + " IAT is exponentially distributed\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " The simpy environment for the simulation\n", + "\n", + " operators: simpy.Resource\n", + " the pool of call operators.\n", + " '''\n", + " # create the arrival process rng \n", + " arrival_rng = np.random.default_rng()\n", + " \n", + " # create the service rng that we pass to each service process created\n", + " service_rng = np.random.default_rng()\n", + " \n", + " # use itertools as it provides an infinite loop \n", + " # with a counter variable that we can use for unique Ids\n", + " for caller_count in itertools.count(start=1):\n", + "\n", + " # 100 calls per hour (sim time units = minutes). \n", + " inter_arrival_time = arrival_rng.exponential(60/100)\n", + " yield env.timeout(inter_arrival_time)\n", + "\n", + " trace(f'call arrives at: {env.now:.3f}')\n", + "\n", + " # create a new simpy process for serving this caller.\n", + " # we pass in the caller id, the operator resources, env, and the rng\n", + " env.process(service(caller_count, operators, env, service_rng))" + ] + }, + { + "cell_type": "markdown", + "id": "222e5d74-cb5c-48e5-8f48-a268c7d92c6a", + "metadata": {}, + "source": [ + "## A single run function\n", + "\n", + "We could keep the code to run the model as a script. However, it is useful to create a new function called `single_run` that we use to perform a single replication of the model and return results. If we later want to run multiple replications it is just a case of running `single_run` in a loop." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4c13d343-6359-4fd4-8b13-0689ec904d2f", + "metadata": {}, + "outputs": [], + "source": [ + "def single_run(run_length, n_operators):\n", + " '''\n", + " Perform a single replication of the simulation model and \n", + " return the mean waiting time as a result.\n", + "\n", + " Parameters:\n", + " ----------\n", + " run_length: float\n", + " The duration of the simulation run in minutes.\n", + "\n", + " n_operators: int\n", + " The number of call operators to create as a resource\n", + "\n", + " Returns:\n", + " -------\n", + " mean_waiting_time: int\n", + " '''\n", + " # create simpy environment and operator resources\n", + " env = simpy.Environment()\n", + " operators = simpy.Resource(env, capacity=n_operators)\n", + " \n", + " env.process(arrivals_generator(env, operators))\n", + " env.run(until=run_length)\n", + " print(f'end of run. simulation clock time = {env.now}')\n", + " \n", + " # MODIFICATION calculate results on notebook level variables.\n", + " mean_waiting_time = np.mean(results['waiting_times'])\n", + "\n", + " return mean_waiting_time" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fd4318c1-3541-4d38-ab22-3b7f8a881bb6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "end of run. simulation clock time = 1000\n", + "Simulation Complete\n", + "Waiting time for call operators: 1.94 minutes\n" + ] + } + ], + "source": [ + "# reset data structure holding results\n", + "results = {}\n", + "results['waiting_times'] = []\n", + "\n", + "# model parameters\n", + "RUN_LENGTH = 1000\n", + "N_OPERATORS = 13\n", + "\n", + "# Turn off caller level results.\n", + "TRACE = False\n", + "\n", + "mean_waiting_time = single_run(RUN_LENGTH, N_OPERATORS)\n", + "print(\"Simulation Complete\")\n", + "print(f\"Waiting time for call operators: {mean_waiting_time:.2f} minutes\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/data/ed_arrivals.csv b/content/data/ed_arrivals.csv deleted file mode 100644 index a0c4e5d..0000000 --- a/content/data/ed_arrivals.csv +++ /dev/null @@ -1,19 +0,0 @@ -period,arrival_rate -6AM-7AM,2.36666666666667 -7AM-8AM,2.8 -8AM-9AM,8.83333333333333 -9AM-10AM,10.4333333333333 -10AM-11AM,14.8 -11AM-12PM,26.2666666666667 -12PM-1PM,31.4 -1PM-2PM,18.0666666666667 -2PM-3PM,16.4666666666667 -3PM-4PM,12.0333333333333 -4PM-5PM,11.6 -5PM-6PM,28.8666666666667 -6PM-7PM,18.0333333333333 -7PM-8PM,11.5 -8PM-9PM,5.3 -9PM-10PM,4.06666666666667 -10PM-11PM,2.2 -11PM-12AM,2.1 diff --git a/content/data/tbl_row_headers.csv b/content/data/tbl_row_headers.csv deleted file mode 100644 index 972bdb3..0000000 --- a/content/data/tbl_row_headers.csv +++ /dev/null @@ -1,7 +0,0 @@ -Mean waiting time (mins) -Triage -Registation -Examination -Non-trauma treatment -Trauma stabilisation -Trauma treatment \ No newline at end of file diff --git a/content/output/table_3.txt b/content/output/table_3.txt deleted file mode 100644 index 06ef3b0..0000000 --- a/content/output/table_3.txt +++ /dev/null @@ -1,17 +0,0 @@ -\begin{table} -\centering -\tbl{Simulation results that can be verified by our example reproducible pipeline.} -\label{tab:table3} -\begin{tabular}{lrrrrr} -\toprule -Mean waiting time (mins) & base & triage+1 & exam+1 & treat+1 & triage+exam \\ -\midrule - Triage & 32.56 & 1.26 & 32.56 & 32.56 & 1.26 \\ - Registation & 104.69 & 131.82 & 104.69 & 104.69 & 131.82 \\ - Examination & 23.36 & 24.44 & 0.14 & 23.36 & 0.14 \\ - Non-trauma treatment & 130.73 & 133.09 & 144.50 & 2.15 & 147.81 \\ - Trauma stabilisation & 166.98 & 189.67 & 166.98 & 166.98 & 189.67 \\ - Trauma treatment & 14.39 & 14.77 & 14.39 & 14.39 & 14.77 \\ -\bottomrule -\end{tabular} -\end{table}