From 5e4a796172ecb11ca77f29f5a490aa14992b16cb Mon Sep 17 00:00:00 2001 From: TomMonks Date: Sat, 24 May 2025 17:47:19 +0100 Subject: [PATCH 1/6] feat(init_bias): warmup + init conds drafts --- content/02_basic_simpy.ipynb | 2 +- content/03a_exercise1.ipynb | 2 +- content/13_warm_up.ipynb | 502 +++++++++++++++++++++++ content/14_initial_conditions.ipynb | 615 ++++++++++++++++++++++++++++ content/distributions.py | 201 ++++++++- 5 files changed, 1317 insertions(+), 5 deletions(-) create mode 100644 content/13_warm_up.ipynb create mode 100644 content/14_initial_conditions.ipynb diff --git a/content/02_basic_simpy.ipynb b/content/02_basic_simpy.ipynb index b2887a8..488db42 100644 --- a/content/02_basic_simpy.ipynb +++ b/content/02_basic_simpy.ipynb @@ -230,7 +230,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/content/03a_exercise1.ipynb b/content/03a_exercise1.ipynb index 754412c..615d044 100644 --- a/content/03a_exercise1.ipynb +++ b/content/03a_exercise1.ipynb @@ -142,7 +142,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/content/13_warm_up.ipynb b/content/13_warm_up.ipynb new file mode 100644 index 0000000..1fe9da4 --- /dev/null +++ b/content/13_warm_up.ipynb @@ -0,0 +1,502 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0be7dabf-cb34-4faf-abb1-e2c8e735beda", + "metadata": {}, + "source": [ + "# Implementing a warm-up period\n", + "\n", + "We will implement warm-up as an event that resets all of our results collection variables. \n", + "\n", + "This is a simpler approach than including lots of if statements in `simpy` processes." + ] + }, + { + "cell_type": "markdown", + "id": "0d9383eb-420c-49f8-b178-f2fe9e6b2a90", + "metadata": {}, + "source": [ + "## 1. Imports " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c1cee9f9-8696-4b13-94ff-bee2a2a2e5f8", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import itertools\n", + "import simpy" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "ea3d507f-9e6d-4ff0-8b90-f9c63c8a8bdf", + "metadata": {}, + "outputs": [], + "source": [ + "# to reduce code these classes can be found in distribution.py\n", + "from distributions import (\n", + " Exponential, \n", + " Lognormal\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c422046d-488a-4743-8ad4-97e9f3dab420", + "metadata": {}, + "source": [ + "## 2. Constants" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1ecf0429-f03f-4ad2-abb4-46692a74e559", + "metadata": {}, + "outputs": [], + "source": [ + "# default mean inter-arrival times(exp)\n", + "# time is in days\n", + "IAT_STROKES = 1.0\n", + "\n", + "# resources\n", + "N_ACUTE_BEDS = 9\n", + "\n", + "# Acute LoS (Lognormal)\n", + "ACUTE_LOS_MEAN = 7.0\n", + "ACUTE_LOC_STD = 1.0\n", + "\n", + "# sampling settings\n", + "N_STREAMS = 2\n", + "DEFAULT_RND_SET = 0\n", + "\n", + "# Boolean switch to simulation results as the model runs\n", + "TRACE = False\n", + "\n", + "# run variables (units = days)\n", + "WU_PERIOD = 0.0\n", + "RC_PERIOD = 100" + ] + }, + { + "cell_type": "markdown", + "id": "5f2a4ad9-6d5e-480d-850f-84d4882a738b", + "metadata": {}, + "source": [ + "## 2. Helper classes and functions" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "52c9271f-1d05-454d-a199-8768bdf5b6e8", + "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": "5a8c050c-4bb6-408f-a805-3a4aaab56916", + "metadata": {}, + "source": [ + "## 3. Experiment class" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "576ae9b4-b21b-4ed0-9b13-e5898d423173", + "metadata": {}, + "outputs": [], + "source": [ + "class Experiment:\n", + " \"\"\"\n", + " Encapsulates the concept of an experiment 🧪 for the stroke pathway\n", + " bed blocking simulator. Manages parameters, PRNG streams and results.\n", + " \"\"\"\n", + " def __init__(\n", + " self,\n", + " random_number_set=DEFAULT_RND_SET,\n", + " n_streams=N_STREAMS,\n", + " iat_strokes=IAT_STROKES,\n", + " acute_los_mean=ACUTE_LOS_MEAN,\n", + " acute_los_std=ACUTE_LOC_STD,\n", + " n_acute_beds=N_ACUTE_BEDS, \n", + " ):\n", + " \"\"\"\n", + " The init method sets up our defaults.\n", + " \"\"\"\n", + " # sampling\n", + " self.random_number_set = random_number_set\n", + " self.n_streams = n_streams\n", + "\n", + " # store parameters for the run of the model\n", + " self.iat_strokes = iat_strokes\n", + " self.acute_los_mean = acute_los_mean\n", + " self.acute_los_std = acute_los_std\n", + "\n", + " # place holder for resources\n", + " self.acute_ward = None\n", + " self.n_acute_beds = n_acute_beds\n", + " \n", + " # initialise results to zero\n", + " self.init_results_variables()\n", + "\n", + " # initialise sampling objects\n", + " self.init_sampling()\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 pseudo random numbers used by\n", + " the distributions in the simulation.\n", + " \"\"\"\n", + " self.random_number_set = random_number_set\n", + " self.init_sampling()\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", + " # produce n non-overlapping streams\n", + " seed_sequence = np.random.SeedSequence(self.random_number_set)\n", + " self.seeds = seed_sequence.spawn(self.n_streams)\n", + "\n", + " # create distributions\n", + "\n", + " # inter-arrival time distributions\n", + " self.arrival_strokes = Exponential(\n", + " self.iat_strokes, random_seed=self.seeds[0]\n", + " )\n", + "\n", + " self.acute_los = Lognormal(\n", + " self.acute_los_mean, self.acute_los_std, random_seed=self.seeds[1]\n", + " )\n", + "\n", + " def init_results_variables(self):\n", + " \"\"\"\n", + " Initialise all of the experiment variables used in results\n", + " collection. This method is called at the start of each run\n", + " of the model\n", + " \"\"\"\n", + " # variable used to store results of experiment\n", + " self.results = {}\n", + " self.results[\"n_arrivals\"] = 0\n", + " self.results[\"waiting_acute\"] = []" + ] + }, + { + "cell_type": "markdown", + "id": "7ff9beae-89cc-419c-b584-c05b81086865", + "metadata": {}, + "source": [ + "## 🥵 Warm-up period" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "dff74a08-37fd-4b18-8bcd-97994f38369a", + "metadata": {}, + "outputs": [], + "source": [ + "def warmup_complete(warm_up_period, env, args):\n", + " \"\"\"\n", + " End of warm-up period event. Used to reset results collection variables.\n", + "\n", + " Parameters:\n", + " ----------\n", + " warm_up_period: float\n", + " Duration of warm-up period in simultion time units\n", + "\n", + " env: simpy.Environment\n", + " The simpy environment\n", + "\n", + " args: Experiment\n", + " The simulation experiment that contains the results being collected.\n", + " \"\"\"\n", + " yield env.timeout(warm_up_period)\n", + " trace(f\"{env.now:.2f}: 🥵 Warm up complete.\")\n", + " \n", + " args.init_results_variables()" + ] + }, + { + "cell_type": "markdown", + "id": "94f0f9c5-22cb-493a-9f1f-4e2a8325beaa", + "metadata": {}, + "source": [ + "## 4. Pathway process logic\n", + "\n", + "The key things to recognise are \n", + "\n", + "* We include a optional parameter called `collection_results` that defaults to `True`. We may set this `False` in our functions that setup initial conditions" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "911528e1-e4eb-4307-bb26-632faf7769d1", + "metadata": {}, + "outputs": [], + "source": [ + "def acute_stroke_pathway(patient_id, env, args):\n", + " \"\"\"Process a patient through the acute ward\n", + " Simpy generator function.\n", + " \n", + " Parameters:\n", + " -----------\n", + " patient_id: int\n", + " A unique id representing the patient in the process\n", + "\n", + " env: simpy.Environment\n", + " The simulation environment\n", + "\n", + " args: Experiment\n", + " Container class for the simulation parameters/results.\n", + " \"\"\"\n", + " arrival_time = env.now\n", + "\n", + " with args.acute_ward.request() as acute_bed_request:\n", + " yield acute_bed_request\n", + " \n", + " acute_admit_time = env.now\n", + " wait_for_acute = acute_admit_time - arrival_time\n", + " \n", + " args.results['waiting_acute'].append(wait_for_acute)\n", + " \n", + " trace(f\"{env.now:.2f}: Patient {patient_id} admitted to acute ward.\" \\\n", + " + f\"(waited {wait_for_acute:.2f} days)\")\n", + " \n", + " # Simulate acute care treatment\n", + " acute_care_duration = args.acute_los.sample()\n", + " yield env.timeout(acute_care_duration)\n", + " \n", + " trace(f\"{env.now:.2f}: Patient {patient_id} discharged.\")" + ] + }, + { + "cell_type": "markdown", + "id": "de8990c2-a330-4c02-ac77-26c30d3e0a41", + "metadata": {}, + "source": [ + "## 4. Arrivals generator\n", + "\n", + "This is a standard arrivals generator. We create stroke arrivals according to their distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b3e686ce-5371-4471-a052-b9d43309bc85", + "metadata": {}, + "outputs": [], + "source": [ + "def stroke_arrivals_generator(env, args):\n", + " \"\"\"\n", + " Arrival process for strokes.\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " The simpy environment for the simulation\n", + "\n", + " args: Experiment\n", + " The settings and input parameters for the simulation.\n", + " \"\"\"\n", + " # use itertools as it provides an infinite loop\n", + " # with a counter variable that we can use for unique Ids\n", + " for patient_id in itertools.count(start=1):\n", + "\n", + " # the sample distribution is defined by the experiment.\n", + " inter_arrival_time = args.arrival_strokes.sample()\n", + " yield env.timeout(inter_arrival_time)\n", + "\n", + " args.results[\"n_arrivals\"] = patient_id\n", + " \n", + " trace(f\"{env.now:.2f}: Stroke arrival.\")\n", + "\n", + " # patient enters pathway\n", + " env.process(acute_stroke_pathway(patient_id, env, args))" + ] + }, + { + "cell_type": "markdown", + "id": "6058571e-9fdb-4961-be27-8a3b8c2fe26e", + "metadata": {}, + "source": [ + "## 5. Single run function" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0d0ea6cf-7d95-4d2c-9690-fcdbdae35d84", + "metadata": {}, + "outputs": [], + "source": [ + "def single_run(\n", + " experiment, \n", + " rep=0,\n", + " wu_period=WU_PERIOD,\n", + " rc_period=RC_PERIOD\n", + "):\n", + " \"\"\"\n", + " Perform a single run of the model and return the results\n", + "\n", + " Parameters:\n", + " -----------\n", + "\n", + " experiment: Experiment\n", + " The experiment/paramaters to use with model\n", + "\n", + " rep: int\n", + " The replication number.\n", + "\n", + " wu_period: float, optional (default=WU_PERIOD)\n", + " Warm-up period\n", + "\n", + " rc_period: float, optional (default=RC_PERIOD)\n", + " The run length of the model\n", + " \"\"\"\n", + "\n", + " # reset all results variables to zero and empty\n", + " experiment.init_results_variables()\n", + "\n", + " # set random number set to the replication no.\n", + " # this controls sampling for the run.\n", + " experiment.set_random_no_set(rep)\n", + "\n", + " # environment is (re)created inside single run\n", + " env = simpy.Environment()\n", + "\n", + " # simpy resources\n", + " experiment.acute_ward = simpy.Resource(env, experiment.n_acute_beds)\n", + "\n", + " # schedule a warm_up period\n", + " env.process(warmup_complete(wu_period, env, experiment))\n", + " \n", + " # we pass all arrival generators to simpy \n", + " env.process(stroke_arrivals_generator(env, experiment))\n", + "\n", + " # run model\n", + " env.run(until=wu_period+rc_period)\n", + "\n", + " # quick stats\n", + " results = {}\n", + " results['mean_acute_wait'] = np.array(\n", + " experiment.results[\"waiting_acute\"]\n", + " ).mean()\n", + "\n", + " # return single run results\n", + " return results" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3.29: Stroke arrival.\n", + "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", + "4.06: Stroke arrival.\n", + "4.06: Patient 2 admitted to acute ward.(waited 0.00 days)\n", + "5.00: 🥵 Warm up complete.\n", + "5.31: Stroke arrival.\n", + "5.31: Patient 3 admitted to acute ward.(waited 0.00 days)\n", + "5.53: Stroke arrival.\n", + "5.53: Patient 4 admitted to acute ward.(waited 0.00 days)\n", + "5.76: Stroke arrival.\n", + "5.76: Patient 5 admitted to acute ward.(waited 0.00 days)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'mean_acute_wait': 0.0}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TRACE = True\n", + "experiment = Experiment()\n", + "results = single_run(experiment, rep=0, wu_period=5.0, rc_period=1.0)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "ddedb4f1-207d-4295-9ae4-c49b2c7cdcaf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'n_arrivals': 5, 'waiting_acute': [0.0, 0.0, 0.0]}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# check how many patient waiting times recorded.\n", + "experiment.results" + ] + } + ], + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/14_initial_conditions.ipynb b/content/14_initial_conditions.ipynb new file mode 100644 index 0000000..a825403 --- /dev/null +++ b/content/14_initial_conditions.ipynb @@ -0,0 +1,615 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0be7dabf-cb34-4faf-abb1-e2c8e735beda", + "metadata": {}, + "source": [ + "# Setting initial conditions\n", + "\n", + "\n", + "**IMPORTANT: There is still an initialisation bias problem:**\n", + "\n", + "* We load patients into the model before we begin the run. \n", + "* If there are $n$ acute stroke beds then the first $n$ patients loaded into the queue will begin service immediately and have a zero queuing time.\n", + "* This applies if we have one queue in the model or if we have multiple queues and activities: the time in system metrics will be biased for initial condition patients.\n", + "* This is the same problem faced when starting the model from time zero with no patients.\n", + "\n", + "**Some Options:**\n", + "* Include a setting in a process to switch results collection on and off.\n", + "* Code a seperate process for initial conditions that does not include results collection.\n", + "* Mixed initial conditions and (a shorter) warm-up period.\n", + " * You will need to do some analysis to ensure this is working acceptably.\n", + " * The warm-up pushes patients into service and also resets results collection. (deletes an initial transient)." + ] + }, + { + "cell_type": "markdown", + "id": "0d9383eb-420c-49f8-b178-f2fe9e6b2a90", + "metadata": {}, + "source": [ + "## 1. Imports " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c1cee9f9-8696-4b13-94ff-bee2a2a2e5f8", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import itertools\n", + "import simpy" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ea3d507f-9e6d-4ff0-8b90-f9c63c8a8bdf", + "metadata": {}, + "outputs": [], + "source": [ + "# to reduce code these classes can be found in distribution.py\n", + "from distributions import (\n", + " Exponential, \n", + " Lognormal, \n", + " DiscreteEmpirical,\n", + " FixedDistribution\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c422046d-488a-4743-8ad4-97e9f3dab420", + "metadata": {}, + "source": [ + "## 2. Constants" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "1ecf0429-f03f-4ad2-abb4-46692a74e559", + "metadata": {}, + "outputs": [], + "source": [ + "# default mean inter-arrival times(exp)\n", + "# time is in days\n", + "IAT_STROKES = 1.0\n", + "\n", + "# resources\n", + "N_ACUTE_BEDS = 9\n", + "\n", + "# Acute LoS (Lognormal)\n", + "ACUTE_LOS_MEAN = 7.0\n", + "ACUTE_LOC_STD = 1.0\n", + "\n", + "# initial conditions for acute queue + service \n", + "# if there are 9 beds then 10 = 1 queuing\n", + "# < 9 = 0 queuing etc.\n", + "# these can be fixed or random\n", + "# Note we are adding N_ACUTE_BEDS patients to queue lengths\n", + "\n", + "INIT_COND_PARAMS = {\n", + " \"mode\": \"fixed\",\n", + " \"fixed\": 8,\n", + " \"rnd\": {\n", + " \"values\":[6, 7, 8, 9, 10, 11],\n", + " \"freq\":[0.25, 0.25, 0.2, 0.1, 0.1, 0.1]\n", + " }\n", + "}\n", + " \n", + "# sampling settings\n", + "N_STREAMS = 3\n", + "DEFAULT_RND_SET = 0\n", + "\n", + "# Boolean switch to simulation results as the model runs\n", + "TRACE = False\n", + "\n", + "# run variables (units = days)\n", + "WU_PERIOD = 0.0\n", + "RC_PERIOD = 100" + ] + }, + { + "cell_type": "markdown", + "id": "5f2a4ad9-6d5e-480d-850f-84d4882a738b", + "metadata": {}, + "source": [ + "## 2. Helper classes and functions" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "52c9271f-1d05-454d-a199-8768bdf5b6e8", + "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": "5a8c050c-4bb6-408f-a805-3a4aaab56916", + "metadata": {}, + "source": [ + "## 3. Experiment class" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "576ae9b4-b21b-4ed0-9b13-e5898d423173", + "metadata": {}, + "outputs": [], + "source": [ + "class Experiment:\n", + " \"\"\"\n", + " Encapsulates the concept of an experiment 🧪 for the stroke pathway\n", + " bed blocking simulator. Manages parameters, PRNG streams and results.\n", + " \"\"\"\n", + " def __init__(\n", + " self,\n", + " random_number_set=DEFAULT_RND_SET,\n", + " n_streams=N_STREAMS,\n", + " iat_strokes=IAT_STROKES,\n", + " acute_los_mean=ACUTE_LOS_MEAN,\n", + " acute_los_std=ACUTE_LOC_STD,\n", + " n_acute_beds=N_ACUTE_BEDS,\n", + " init_cond_params=INIT_COND_PARAMS, \n", + " ):\n", + " \"\"\"\n", + " The init method sets up our defaults.\n", + " \"\"\"\n", + " # sampling\n", + " self.random_number_set = random_number_set\n", + " self.n_streams = n_streams\n", + "\n", + " # store parameters for the run of the model\n", + " self.iat_strokes = iat_strokes\n", + " self.acute_los_mean = acute_los_mean\n", + " self.acute_los_std = acute_los_std\n", + "\n", + " # stored initial conditions\n", + " self.init_cond_params = init_cond_params\n", + "\n", + " # place holder for resources\n", + " self.acute_ward = None\n", + " self.n_acute_beds = n_acute_beds\n", + " \n", + " # initialise results to zero\n", + " self.init_results_variables()\n", + "\n", + " # initialise sampling objects\n", + " self.init_sampling()\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 pseudo random numbers used by\n", + " the distributions in the simulation.\n", + " \"\"\"\n", + " self.random_number_set = random_number_set\n", + " self.init_sampling()\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", + " # produce n non-overlapping streams\n", + " seed_sequence = np.random.SeedSequence(self.random_number_set)\n", + " self.seeds = seed_sequence.spawn(self.n_streams)\n", + "\n", + " # create distributions\n", + "\n", + " # inter-arrival time distributions\n", + " self.arrival_strokes = Exponential(\n", + " self.iat_strokes, random_seed=self.seeds[0]\n", + " )\n", + "\n", + " self.acute_los = Lognormal(\n", + " self.acute_los_mean, self.acute_los_std, random_seed=self.seeds[1]\n", + " )\n", + "\n", + " if self.init_cond_params[\"mode\"] == \"fixed\":\n", + " self.init_conds = FixedDistribution(\n", + " self.init_cond_params[\"fixed\"]\n", + " )\n", + " else:\n", + " self.init_conds = DiscreteEmpirical(\n", + " values = self.init_cond_params[\"rnd\"][\"values\"],\n", + " freq = self.init_cond_params[\"rnd\"][\"freq\"],\n", + " random_seed=self.seeds[2]\n", + " )\n", + " \n", + "\n", + " def init_results_variables(self):\n", + " \"\"\"\n", + " Initialise all of the experiment variables used in results\n", + " collection. This method is called at the start of each run\n", + " of the model\n", + " \"\"\"\n", + " # variable used to store results of experiment\n", + " self.results = {}\n", + " self.results[\"n_arrivals\"] = 0\n", + " self.results[\"waiting_acute\"] = []" + ] + }, + { + "cell_type": "markdown", + "id": "7ff9beae-89cc-419c-b584-c05b81086865", + "metadata": {}, + "source": [ + "## 🥵 Warm-up period" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "dff74a08-37fd-4b18-8bcd-97994f38369a", + "metadata": {}, + "outputs": [], + "source": [ + "def warmup_complete(warm_up_period, env, args):\n", + " \"\"\"\n", + " End of warm-up period event. Used to reset results collection variables.\n", + "\n", + " Parameters:\n", + " ----------\n", + " warm_up_period: float\n", + " Duration of warm-up period in simultion time units\n", + "\n", + " env: simpy.Environment\n", + " The simpy environment\n", + "\n", + " args: Experiment\n", + " The simulation experiment that contains the results being collected.\n", + " \"\"\"\n", + " yield env.timeout(warm_up_period)\n", + " trace(f\"{env.now:.2f}: 🥵 Warm up complete.\")\n", + " \n", + " args.init_results_variables()" + ] + }, + { + "cell_type": "markdown", + "id": "94f0f9c5-22cb-493a-9f1f-4e2a8325beaa", + "metadata": {}, + "source": [ + "## 4. Pathway process logic\n", + "\n", + "The key things to recognise are \n", + "\n", + "* We include a optional parameter called `collection_results` that defaults to `True`. We may set this `False` in our functions that setup initial conditions" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "911528e1-e4eb-4307-bb26-632faf7769d1", + "metadata": {}, + "outputs": [], + "source": [ + "def acute_stroke_pathway(patient_id, env, args, collect_results=True):\n", + " \"\"\"Process a patient through the acute ward\n", + " Simpy generator function.\n", + " \n", + " Parameters:\n", + " -----------\n", + " patient_id: int\n", + " A unique id representing the patient in the process\n", + "\n", + " env: simpy.Environment\n", + " The simulation environment\n", + "\n", + " args: Experiment\n", + " Container class for the simulation parameters/results.\n", + " \"\"\"\n", + " arrival_time = env.now\n", + "\n", + " with args.acute_ward.request() as acute_bed_request:\n", + " yield acute_bed_request\n", + " \n", + " acute_admit_time = env.now\n", + " wait_for_acute = acute_admit_time - arrival_time\n", + "\n", + " if collect_results:\n", + " args.results['waiting_acute'].append(wait_for_acute)\n", + " \n", + " trace(f\"{env.now:.2f}: Patient {patient_id} admitted to acute ward.\" \\\n", + " + f\"(waited {wait_for_acute:.2f} days)\")\n", + " \n", + " # Simulate acute care treatment\n", + " acute_care_duration = args.acute_los.sample()\n", + " yield env.timeout(acute_care_duration)\n", + " \n", + " trace(f\"{env.now:.2f}: Patient {patient_id} discharged.\")" + ] + }, + { + "cell_type": "markdown", + "id": "de8990c2-a330-4c02-ac77-26c30d3e0a41", + "metadata": {}, + "source": [ + "## 4. Arrivals generator\n", + "\n", + "This is a standard arrivals generator. We create stroke arrivals according to their distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b3e686ce-5371-4471-a052-b9d43309bc85", + "metadata": {}, + "outputs": [], + "source": [ + "def stroke_arrivals_generator(env, args):\n", + " \"\"\"\n", + " Arrival process for strokes.\n", + "\n", + " Parameters:\n", + " ------\n", + " env: simpy.Environment\n", + " The simpy environment for the simulation\n", + "\n", + " args: Experiment\n", + " The settings and input parameters for the simulation.\n", + " \"\"\"\n", + " # use itertools as it provides an infinite loop\n", + " # with a counter variable that we can use for unique Ids\n", + " for patient_id in itertools.count(start=1):\n", + "\n", + " # the sample distribution is defined by the experiment.\n", + " inter_arrival_time = args.arrival_strokes.sample()\n", + " yield env.timeout(inter_arrival_time)\n", + "\n", + " args.results[\"n_arrivals\"] = patient_id\n", + " \n", + " trace(f\"{env.now:.2f}: Stroke arrival.\")\n", + "\n", + " # patient enters pathway\n", + " env.process(acute_stroke_pathway(patient_id, env, args))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "ca37c9cd-158c-416b-8490-b4c5c2e63412", + "metadata": {}, + "outputs": [], + "source": [ + "def setup_initial_conditions(\n", + " env: simpy.Environment, \n", + " args: Experiment,\n", + " collect_results: bool = False,\n", + "):\n", + " \"\"\"Set up initial conditions with patients already in the acute\n", + " stroke queue\n", + "\n", + " Parmaters:\n", + " ---------\n", + " env: simpy.Environment\n", + " The simpy environment for the simulation\n", + "\n", + " args: Experiment\n", + " The settings and input parameters for the simulation.\n", + "\n", + " collect_results: bool, optional (default = False)#\n", + " Should results be collected for initial conditions patients?\n", + " \n", + " \"\"\"\n", + " # sample the no. patients to load into queue\n", + " patients_to_load = args.init_conds.sample()\n", + "\n", + " for initial_id in range(1, patients_to_load+1):\n", + " # Create patients with negative IDs to distinguish them as init cond.\n", + " # we may or may not want collect results for initial conditions\n", + " env.process(acute_stroke_pathway(-initial_id, env, args, collect_results))\n", + " trace(f\"{env.now:.2f}: Patient {-initial_id} loaded into queue\")" + ] + }, + { + "cell_type": "markdown", + "id": "6058571e-9fdb-4961-be27-8a3b8c2fe26e", + "metadata": {}, + "source": [ + "## 5. Single run function" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "0d0ea6cf-7d95-4d2c-9690-fcdbdae35d84", + "metadata": {}, + "outputs": [], + "source": [ + "def single_run(\n", + " experiment, \n", + " rep=0,\n", + " wu_period=WU_PERIOD,\n", + " rc_period=RC_PERIOD\n", + "):\n", + " \"\"\"\n", + " Perform a single run of the model and return the results\n", + "\n", + " Parameters:\n", + " -----------\n", + "\n", + " experiment: Experiment\n", + " The experiment/paramaters to use with model\n", + "\n", + " rep: int\n", + " The replication number.\n", + "\n", + " wu_period: float, optional (default=WU_PERIOD)\n", + " Warm-up period\n", + "\n", + " rc_period: float, optional (default=RC_PERIOD)\n", + " The run length of the model\n", + " \"\"\"\n", + "\n", + " # reset all results variables to zero and empty\n", + " experiment.init_results_variables()\n", + "\n", + " # set random number set to the replication no.\n", + " # this controls sampling for the run.\n", + " experiment.set_random_no_set(rep)\n", + "\n", + " # environment is (re)created inside single run\n", + " env = simpy.Environment()\n", + "\n", + " # simpy resources\n", + " experiment.acute_ward = simpy.Resource(env, experiment.n_acute_beds)\n", + "\n", + " # load the acute stroke queue\n", + " setup_initial_conditions(env, experiment)\n", + "\n", + " # schedule a warm_up period\n", + " env.process(warmup_complete(wu_period, env, experiment))\n", + " \n", + " # we pass all arrival generators to simpy \n", + " env.process(stroke_arrivals_generator(env, experiment))\n", + "\n", + " # run model\n", + " env.run(until=wu_period+rc_period)\n", + "\n", + " # quick stats\n", + " results = {}\n", + " results['mean_acute_wait'] = np.array(\n", + " experiment.results[\"waiting_acute\"]\n", + " ).mean()\n", + "\n", + " # return single run results\n", + " return results" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.00: Patient -1 loaded into queue\n", + "0.00: Patient -2 loaded into queue\n", + "0.00: Patient -3 loaded into queue\n", + "0.00: Patient -4 loaded into queue\n", + "0.00: Patient -5 loaded into queue\n", + "0.00: Patient -6 loaded into queue\n", + "0.00: Patient -7 loaded into queue\n", + "0.00: Patient -8 loaded into queue\n", + "0.00: Patient -1 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -2 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -3 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -4 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -5 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -6 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -7 admitted to acute ward.(waited 0.00 days)\n", + "0.00: Patient -8 admitted to acute ward.(waited 0.00 days)\n", + "3.29: Stroke arrival.\n", + "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", + "4.06: Stroke arrival.\n", + "4.22: Patient -3 discharged.\n", + "4.22: Patient 2 admitted to acute ward.(waited 0.16 days)\n", + "5.00: 🥵 Warm up complete.\n", + "5.28: Patient -2 discharged.\n", + "5.31: Stroke arrival.\n", + "5.31: Patient 3 admitted to acute ward.(waited 0.00 days)\n", + "5.53: Stroke arrival.\n", + "5.76: Stroke arrival.\n", + "5.98: Patient -8 discharged.\n", + "5.98: Patient 4 admitted to acute ward.(waited 0.44 days)\n", + "6.60: Patient -6 discharged.\n", + "6.60: Patient 5 admitted to acute ward.(waited 0.84 days)\n", + "7.25: Patient -7 discharged.\n", + "7.56: Stroke arrival.\n", + "7.56: Patient 6 admitted to acute ward.(waited 0.00 days)\n", + "7.62: Stroke arrival.\n", + "7.77: Patient -1 discharged.\n", + "7.77: Patient 7 admitted to acute ward.(waited 0.15 days)\n", + "7.91: Patient -4 discharged.\n", + "8.33: Patient -5 discharged.\n", + "8.52: Stroke arrival.\n", + "8.52: Patient 8 admitted to acute ward.(waited 0.00 days)\n", + "8.82: Stroke arrival.\n", + "8.82: Patient 9 admitted to acute ward.(waited 0.00 days)\n", + "9.80: Patient 1 discharged.\n", + "11.12: Stroke arrival.\n", + "11.12: Patient 10 admitted to acute ward.(waited 0.00 days)\n", + "11.93: Patient 2 discharged.\n", + "12.98: Stroke arrival.\n", + "12.98: Patient 11 admitted to acute ward.(waited 0.00 days)\n", + "12.99: Stroke arrival.\n", + "13.26: Patient 3 discharged.\n", + "13.26: Patient 12 admitted to acute ward.(waited 0.26 days)\n", + "13.80: Patient 5 discharged.\n", + "14.19: Patient 7 discharged.\n", + "14.20: Patient 4 discharged.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'mean_acute_wait': 0.16965284328644098}" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TRACE = True\n", + "init_cond_params = INIT_COND_PARAMS.copy()\n", + "init_cond_params[\"mode\"] = \"fixed\"\n", + "\n", + "# uncomment to vary the fixed amount 10 = 1 in queue.\n", + "# init_cond_params[\"fixed\"] = 10\n", + "\n", + "experiment = Experiment(init_cond_params=init_cond_params)\n", + "results = single_run(experiment, rep=0, wu_period=5.0, rc_period=10.0)\n", + "results" + ] + } + ], + "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.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/content/distributions.py b/content/distributions.py index 33d8ab5..fc2e943 100644 --- a/content/distributions.py +++ b/content/distributions.py @@ -3,9 +3,9 @@ """ import numpy as np -from typing import Optional, Union, Tuple +from typing import Optional, Union, Tuple, Any from numpy.random import SeedSequence -from numpy.typing import NDArray +from numpy.typing import NDArray, ArrayLike import math class Lognormal: @@ -184,4 +184,199 @@ def sample(self, size=None): ------- float or np.ndarray (if size >=1) """ - return self.rand.binomial(n=1, p=self.p, size=size) \ No newline at end of file + return self.rand.binomial(n=1, p=self.p, size=size) + +class DiscreteEmpirical: + """ + DiscreteEmpirical distribution implementation. + + A probability distribution that samples values with specified frequencies. + Useful for modeling categorical data or discrete outcomes with known + probabilities. + + Example uses: + ------------- + 1. Routing percentages + 2. Classes of entity + 3. Batch sizes of arrivals + 4. Initial conditions - no. entities in a queue. + """ + + def __init__( + self, + values: ArrayLike, + freq: ArrayLike, + random_seed: Optional[Union[int, SeedSequence]] = None, + ): + """ + Initialize a discrete distribution. + + Parameters + ---------- + values : ArrayLike + List of possible outcome values. Must be of equal length to freq. + + freq : ArrayLike + List of observed frequencies or probabilities. Must be of equal + length to values. These will be normalized to sum to 1. + + random_seed : Optional[Union[int, SeedSequence]], default=None + A random seed or SeedSequence to reproduce samples. If None, a + unique sample sequence is generated. + + Raises + ------ + TypeError + If values or freq are not positive arrays + ValueError + If values and freq have different lengths. + """ + + # convert to array first + self.values = np.asarray(values) + self.freq = np.asarray(freq) + + if len(self.values) != len(self.freq): + raise ValueError( + "values and freq arguments must be of equal length" + ) + + self.rng = np.random.default_rng(random_seed) + self.probabilities = self.freq / self.freq.sum() + + def __repr__(self): + values_repr = ( + str(self.values.tolist()) + if len(self.values) < 4 + else f"[{', '.join(str(x) for x in self.values[:3])}, ...]" + ) + freq_repr = ( + str(self.freq.tolist()) + if len(self.freq) < 4 + else f"[{', '.join(str(x) for x in self.freq[:3])}, ...]" + ) + return f"Discrete(values={values_repr}, freq={freq_repr})" + + def sample( + self, size: Optional[Union[int, Tuple[int, ...]]] = None + ) -> Union[Any, NDArray]: + """ + Generate random samples from the discrete distribution. + + Parameters + ---------- + size : Optional[Union[int, Tuple[int, ...]]], default=None + The number/shape of samples to generate: + - If None: returns a single sample + - If int: returns a 1-D array with that many samples + - If tuple of ints: returns an array with that shape + + Returns + ------- + Union[Any, NDArray] + Random samples from the discrete distribution: + - A single value (of whatever type was in the values array) when + size is None + - A numpy array of values with shape determined by size parameter + """ + sample = self.rng.choice(self.values, p=self.probabilities, size=size) + + if size is None: + return sample.item() + return sample + +class FixedDistribution: + """ + Fixed distribution implementation. + + A degenerate distribution that always returns the same fixed value. + Useful for constants or deterministic parameters in models. + + Provides a method to + sample a constant value regardless of the number of samples requested. + """ + + def __init__(self, value: float): + """ + Initialize a fixed distribution. + + Parameters + ---------- + value : float + The constant value that will be returned by sampling. + """ + self.value = value + + def __repr__(self): + return f"FixedDistribution(value={self.value})" + + def sample( + self, size: Optional[Union[int, Tuple[int, ...]]] = None + ) -> Union[float, NDArray[np.float64]]: + """ + Generate "samples" from the fixed distribution (always the same value). + + Parameters + ---------- + size : Optional[Union[int, Tuple[int, ...]]], default=None + The number/shape of samples to generate: + - If None: returns the fixed value as a float + - If int: returns a 1-D array filled with the fixed value + - If tuple of ints: returns an array with that shape filled with + the fixed value + + Returns + ------- + Union[float, NDArray[np.float64]] + The fixed value: + - A single float when size is None + - A numpy array filled with the fixed value with shape + determined by size parameter + """ + if size is not None: + return np.full(size, self.value) + return self.value + +class Bernoulli: + """ + Convenience class for the Bernoulli distribution. + packages up distribution parameters, seed and random generator. + + The Bernoulli distribution is a special case of the binomial distribution + where a single trial is conducted + + Use the Bernoulli distribution to sample success or failure. + """ + + def __init__(self, p, random_seed=None): + """ + Constructor + + Params: + ------ + p: float + probability of drawing a 1 + + random_seed: int | SeedSequence, optional (default=None) + A random seed to reproduce samples. If set to none then a unique + sample is created. + """ + self.rand = np.random.default_rng(seed=random_seed) + self.p = p + + def sample(self, size=None): + """ + Generate a sample from the exponential distribution + + Params: + ------- + size: int, optional (default=None) + the number of samples to return. If size=None then a single + sample is returned. + + Returns: + ------- + float or np.ndarray (if size >=1) + """ + return self.rand.binomial(n=1, p=self.p, size=size) + From 02f3a8f30261aeae508f3f640d278619a2c9a2d4 Mon Sep 17 00:00:00 2001 From: TomMonks Date: Sat, 24 May 2025 17:51:19 +0100 Subject: [PATCH 2/6] feat(warm-up): quick checks --- content/13_warm_up.ipynb | 93 +++++++++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/content/13_warm_up.ipynb b/content/13_warm_up.ipynb index 1fe9da4..f5c36cf 100644 --- a/content/13_warm_up.ipynb +++ b/content/13_warm_up.ipynb @@ -7,7 +7,7 @@ "source": [ "# Implementing a warm-up period\n", "\n", - "We will implement warm-up as an event that resets all of our results collection variables. \n", + "We will implement warm-up as a single event that resets all of our results collection variables. \n", "\n", "This is a simpler approach than including lots of if statements in `simpy` processes." ] @@ -309,7 +309,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 33, "id": "b3e686ce-5371-4471-a052-b9d43309bc85", "metadata": {}, "outputs": [], @@ -334,7 +334,7 @@ " inter_arrival_time = args.arrival_strokes.sample()\n", " yield env.timeout(inter_arrival_time)\n", "\n", - " args.results[\"n_arrivals\"] = patient_id\n", + " args.results[\"n_arrivals\"] += 1\n", " \n", " trace(f\"{env.now:.2f}: Stroke arrival.\")\n", "\n", @@ -352,7 +352,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 34, "id": "0d0ea6cf-7d95-4d2c-9690-fcdbdae35d84", "metadata": {}, "outputs": [], @@ -414,11 +414,84 @@ " return results" ] }, + { + "cell_type": "markdown", + "id": "c13f5e57-723c-409b-a1ce-cdb831b4e166", + "metadata": {}, + "source": [ + "## Quick check 1: No warm-up" + ] + }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 35, "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.00: 🥵 Warm up complete.\n", + "3.29: Stroke arrival.\n", + "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", + "4.06: Stroke arrival.\n", + "4.06: Patient 2 admitted to acute ward.(waited 0.00 days)\n" + ] + }, + { + "data": { + "text/plain": [ + "{'mean_acute_wait': 0.0}" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "TRACE = True\n", + "experiment = Experiment()\n", + "results = single_run(experiment, rep=0, wu_period=0.0, rc_period=5.0)\n", + "results" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "ddedb4f1-207d-4295-9ae4-c49b2c7cdcaf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'n_arrivals': 2, 'waiting_acute': [0.0, 0.0]}" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# check how many patient waiting times recorded.\n", + "experiment.results" + ] + }, + { + "cell_type": "markdown", + "id": "660ea2e1-d9c2-4355-876c-43dfd9dab0fe", + "metadata": {}, + "source": [ + "## Quick check 1: Include a warm-up" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "72b5284a-1fcb-4126-b663-c0ef0002e4bf", + "metadata": {}, "outputs": [ { "name": "stdout", @@ -443,7 +516,7 @@ "{'mean_acute_wait': 0.0}" ] }, - "execution_count": 23, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -457,17 +530,17 @@ }, { "cell_type": "code", - "execution_count": 24, - "id": "ddedb4f1-207d-4295-9ae4-c49b2c7cdcaf", + "execution_count": 38, + "id": "7f5e282b-0f41-41df-bdca-f128e7d418c1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'n_arrivals': 5, 'waiting_acute': [0.0, 0.0, 0.0]}" + "{'n_arrivals': 3, 'waiting_acute': [0.0, 0.0, 0.0]}" ] }, - "execution_count": 24, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } From 0b40b7f019f6c390cee00829fcf82fa9e7d1decb Mon Sep 17 00:00:00 2001 From: TomMonks Date: Sat, 24 May 2025 17:52:15 +0100 Subject: [PATCH 3/6] feat(initconds): fix patient count --- content/14_initial_conditions.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/14_initial_conditions.ipynb b/content/14_initial_conditions.ipynb index a825403..1ad9e65 100644 --- a/content/14_initial_conditions.ipynb +++ b/content/14_initial_conditions.ipynb @@ -354,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 35, "id": "b3e686ce-5371-4471-a052-b9d43309bc85", "metadata": {}, "outputs": [], @@ -379,7 +379,7 @@ " inter_arrival_time = args.arrival_strokes.sample()\n", " yield env.timeout(inter_arrival_time)\n", "\n", - " args.results[\"n_arrivals\"] = patient_id\n", + " args.results[\"n_arrivals\"] += 1\n", " \n", " trace(f\"{env.now:.2f}: Stroke arrival.\")\n", "\n", From 78a56cbc9162817d0cf6bf51d3aa1999643b21d7 Mon Sep 17 00:00:00 2001 From: TomMonks Date: Sun, 25 May 2025 11:32:04 +0100 Subject: [PATCH 4/6] feat(warmup): nb finalised for warmup coding --- content/13_warm_up.ipynb | 65 ++++++++++++++++++--------- content/img/acute_stroke_pathway.png | Bin 0 -> 68111 bytes 2 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 content/img/acute_stroke_pathway.png diff --git a/content/13_warm_up.ipynb b/content/13_warm_up.ipynb index f5c36cf..4f864ef 100644 --- a/content/13_warm_up.ipynb +++ b/content/13_warm_up.ipynb @@ -5,11 +5,26 @@ "id": "0be7dabf-cb34-4faf-abb1-e2c8e735beda", "metadata": {}, "source": [ - "# Implementing a warm-up period\n", + "# Coding a warm-up period in SimPy\n", "\n", - "We will implement warm-up as a single event that resets all of our results collection variables. \n", + "## Why do you need a warm-up period?\n", "\n", - "This is a simpler approach than including lots of if statements in `simpy` processes." + "Typically when you are modelling a non-terminating system, you will need to deal with **initialisation bias**. That is the real system always has work-in-progress (e.g. patients in queues and in service), but the model starts from empty. One way to do this is to split the model's run length into warm-up and data collection periods. We discard all results in the warm-up period.\n", + "\n", + "> In this tutorial we will focus on coding a warm-up period rather than analysis to determine its length\n", + "\n", + "## But how do you code it?\n", + "\n", + "💪 We will implement warm-up as a **single event** that resets all of our results collection variables. \n", + "\n", + "> This is a simpler approach than including lots of if statements in `simpy` processes.\n", + "\n", + "## Illustrative example model\n", + "\n", + "We will use a very simple model for this example. This is a acute stroke pathway with a single arrival processes, a single type of resource, and a single treatment process. This is a non-terminating system. There are always patients in the system - it does not start up from empty\n", + "\n", + "\n", + "![model image](img/acute_stroke_pathway.png \"stroke pathway\")" ] }, { @@ -34,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 2, "id": "ea3d507f-9e6d-4ff0-8b90-f9c63c8a8bdf", "metadata": {}, "outputs": [], @@ -212,7 +227,11 @@ "id": "7ff9beae-89cc-419c-b584-c05b81086865", "metadata": {}, "source": [ - "## 🥵 Warm-up period" + "## 🥵 Warm-up period\n", + "\n", + "The acute stroke pathway model starts from empty. As it is a non-terminating system our estimate of waiting time is biased due to the empty period at the start of the simulation. We can remove this initialisation bias using a warm-up period. \n", + "\n", + "We will implement a warm-up through an **event** that happens once in a single run of the model. The model will be run for the **warm-up period + results collection period**. At the end of the warm-up period an event will happen where all variables in the current experiment are reset (e.g. empty lists and set quantitative values to 0)." ] }, { @@ -248,7 +267,7 @@ "id": "94f0f9c5-22cb-493a-9f1f-4e2a8325beaa", "metadata": {}, "source": [ - "## 4. Pathway process logic\n", + "## 4. Stroke pathway process logic\n", "\n", "The key things to recognise are \n", "\n", @@ -309,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 8, "id": "b3e686ce-5371-4471-a052-b9d43309bc85", "metadata": {}, "outputs": [], @@ -352,7 +371,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 9, "id": "0d0ea6cf-7d95-4d2c-9690-fcdbdae35d84", "metadata": {}, "outputs": [], @@ -424,7 +443,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 22, "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", "metadata": {}, "outputs": [ @@ -436,7 +455,13 @@ "3.29: Stroke arrival.\n", "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", "4.06: Stroke arrival.\n", - "4.06: Patient 2 admitted to acute ward.(waited 0.00 days)\n" + "4.06: Patient 2 admitted to acute ward.(waited 0.00 days)\n", + "5.31: Stroke arrival.\n", + "5.31: Patient 3 admitted to acute ward.(waited 0.00 days)\n", + "5.53: Stroke arrival.\n", + "5.53: Patient 4 admitted to acute ward.(waited 0.00 days)\n", + "5.76: Stroke arrival.\n", + "5.76: Patient 5 admitted to acute ward.(waited 0.00 days)\n" ] }, { @@ -445,7 +470,7 @@ "{'mean_acute_wait': 0.0}" ] }, - "execution_count": 35, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -453,23 +478,23 @@ "source": [ "TRACE = True\n", "experiment = Experiment()\n", - "results = single_run(experiment, rep=0, wu_period=0.0, rc_period=5.0)\n", + "results = single_run(experiment, rep=0, wu_period=0.0, rc_period=6.0)\n", "results" ] }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 23, "id": "ddedb4f1-207d-4295-9ae4-c49b2c7cdcaf", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'n_arrivals': 2, 'waiting_acute': [0.0, 0.0]}" + "{'n_arrivals': 5, 'waiting_acute': [0.0, 0.0, 0.0, 0.0, 0.0]}" ] }, - "execution_count": 36, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } @@ -484,12 +509,12 @@ "id": "660ea2e1-d9c2-4355-876c-43dfd9dab0fe", "metadata": {}, "source": [ - "## Quick check 1: Include a warm-up" + "## Quick check 2: Include a warm-up" ] }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 24, "id": "72b5284a-1fcb-4126-b663-c0ef0002e4bf", "metadata": {}, "outputs": [ @@ -516,7 +541,7 @@ "{'mean_acute_wait': 0.0}" ] }, - "execution_count": 37, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } @@ -530,7 +555,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 25, "id": "7f5e282b-0f41-41df-bdca-f128e7d418c1", "metadata": {}, "outputs": [ @@ -540,7 +565,7 @@ "{'n_arrivals': 3, 'waiting_acute': [0.0, 0.0, 0.0]}" ] }, - "execution_count": 38, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } diff --git a/content/img/acute_stroke_pathway.png b/content/img/acute_stroke_pathway.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3ef95931fb5173af28e3cfad765a6c7332c050 GIT binary patch literal 68111 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 Law+?walroo(uY6y literal 0 HcmV?d00001 From 5820fef2a47c0a9da33b20ea722488d4973f581b Mon Sep 17 00:00:00 2001 From: TomMonks Date: Sun, 25 May 2025 15:25:14 +0100 Subject: [PATCH 5/6] feat(init): updates arrival msg --- content/13_warm_up.ipynb | 18 ++-- content/14_initial_conditions.ipynb | 131 +++++++++++++++++----------- 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/content/13_warm_up.ipynb b/content/13_warm_up.ipynb index 4f864ef..4446048 100644 --- a/content/13_warm_up.ipynb +++ b/content/13_warm_up.ipynb @@ -11,7 +11,7 @@ "\n", "Typically when you are modelling a non-terminating system, you will need to deal with **initialisation bias**. That is the real system always has work-in-progress (e.g. patients in queues and in service), but the model starts from empty. One way to do this is to split the model's run length into warm-up and data collection periods. We discard all results in the warm-up period.\n", "\n", - "> In this tutorial we will focus on coding a warm-up period rather than analysis to determine its length\n", + "> In this tutorial we will focus on coding a warm-up period in Python and SimPy rather than analysis to determine its length\n", "\n", "## But how do you code it?\n", "\n", @@ -443,7 +443,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 10, "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", "metadata": {}, "outputs": [ @@ -470,7 +470,7 @@ "{'mean_acute_wait': 0.0}" ] }, - "execution_count": 22, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -484,7 +484,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 11, "id": "ddedb4f1-207d-4295-9ae4-c49b2c7cdcaf", "metadata": {}, "outputs": [ @@ -494,7 +494,7 @@ "{'n_arrivals': 5, 'waiting_acute': [0.0, 0.0, 0.0, 0.0, 0.0]}" ] }, - "execution_count": 23, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -514,7 +514,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 12, "id": "72b5284a-1fcb-4126-b663-c0ef0002e4bf", "metadata": {}, "outputs": [ @@ -541,7 +541,7 @@ "{'mean_acute_wait': 0.0}" ] }, - "execution_count": 24, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -555,7 +555,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 13, "id": "7f5e282b-0f41-41df-bdca-f128e7d418c1", "metadata": {}, "outputs": [ @@ -565,7 +565,7 @@ "{'n_arrivals': 3, 'waiting_acute': [0.0, 0.0, 0.0]}" ] }, - "execution_count": 25, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } diff --git a/content/14_initial_conditions.ipynb b/content/14_initial_conditions.ipynb index 1ad9e65..4ae93ad 100644 --- a/content/14_initial_conditions.ipynb +++ b/content/14_initial_conditions.ipynb @@ -69,7 +69,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 3, "id": "1ecf0429-f03f-4ad2-abb4-46692a74e559", "metadata": {}, "outputs": [], @@ -95,8 +95,8 @@ " \"mode\": \"fixed\",\n", " \"fixed\": 8,\n", " \"rnd\": {\n", - " \"values\":[6, 7, 8, 9, 10, 11],\n", - " \"freq\":[0.25, 0.25, 0.2, 0.1, 0.1, 0.1]\n", + " \"values\":[8, 9, 10, 11, 12, 13],\n", + " \"freq\":[25, 25, 2, 1, 1, 1]\n", " }\n", "}\n", " \n", @@ -354,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 8, "id": "b3e686ce-5371-4471-a052-b9d43309bc85", "metadata": {}, "outputs": [], @@ -381,7 +381,7 @@ "\n", " args.results[\"n_arrivals\"] += 1\n", " \n", - " trace(f\"{env.now:.2f}: Stroke arrival.\")\n", + " trace(f\"{env.now:.2f}: Patient {patient_id}. Stroke arrival.\")\n", "\n", " # patient enters pathway\n", " env.process(acute_stroke_pathway(patient_id, env, args))" @@ -389,7 +389,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "id": "ca37c9cd-158c-416b-8490-b4c5c2e63412", "metadata": {}, "outputs": [], @@ -434,7 +434,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 10, "id": "0d0ea6cf-7d95-4d2c-9690-fcdbdae35d84", "metadata": {}, "outputs": [], @@ -501,7 +501,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 11, "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", "metadata": {}, "outputs": [ @@ -517,6 +517,8 @@ "0.00: Patient -6 loaded into queue\n", "0.00: Patient -7 loaded into queue\n", "0.00: Patient -8 loaded into queue\n", + "0.00: Patient -9 loaded into queue\n", + "0.00: Patient -10 loaded into queue\n", "0.00: Patient -1 admitted to acute ward.(waited 0.00 days)\n", "0.00: Patient -2 admitted to acute ward.(waited 0.00 days)\n", "0.00: Patient -3 admitted to acute ward.(waited 0.00 days)\n", @@ -525,54 +527,52 @@ "0.00: Patient -6 admitted to acute ward.(waited 0.00 days)\n", "0.00: Patient -7 admitted to acute ward.(waited 0.00 days)\n", "0.00: Patient -8 admitted to acute ward.(waited 0.00 days)\n", - "3.29: Stroke arrival.\n", - "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", - "4.06: Stroke arrival.\n", - "4.22: Patient -3 discharged.\n", - "4.22: Patient 2 admitted to acute ward.(waited 0.16 days)\n", - "5.00: 🥵 Warm up complete.\n", - "5.28: Patient -2 discharged.\n", - "5.31: Stroke arrival.\n", - "5.31: Patient 3 admitted to acute ward.(waited 0.00 days)\n", - "5.53: Stroke arrival.\n", - "5.76: Stroke arrival.\n", - "5.98: Patient -8 discharged.\n", - "5.98: Patient 4 admitted to acute ward.(waited 0.44 days)\n", - "6.60: Patient -6 discharged.\n", - "6.60: Patient 5 admitted to acute ward.(waited 0.84 days)\n", - "7.25: Patient -7 discharged.\n", - "7.56: Stroke arrival.\n", - "7.56: Patient 6 admitted to acute ward.(waited 0.00 days)\n", - "7.62: Stroke arrival.\n", - "7.77: Patient -1 discharged.\n", - "7.77: Patient 7 admitted to acute ward.(waited 0.15 days)\n", - "7.91: Patient -4 discharged.\n", - "8.33: Patient -5 discharged.\n", - "8.52: Stroke arrival.\n", - "8.52: Patient 8 admitted to acute ward.(waited 0.00 days)\n", - "8.82: Stroke arrival.\n", - "8.82: Patient 9 admitted to acute ward.(waited 0.00 days)\n", - "9.80: Patient 1 discharged.\n", - "11.12: Stroke arrival.\n", - "11.12: Patient 10 admitted to acute ward.(waited 0.00 days)\n", - "11.93: Patient 2 discharged.\n", - "12.98: Stroke arrival.\n", - "12.98: Patient 11 admitted to acute ward.(waited 0.00 days)\n", - "12.99: Stroke arrival.\n", - "13.26: Patient 3 discharged.\n", - "13.26: Patient 12 admitted to acute ward.(waited 0.26 days)\n", - "13.80: Patient 5 discharged.\n", - "14.19: Patient 7 discharged.\n", - "14.20: Patient 4 discharged.\n" + "0.00: Patient -9 admitted to acute ward.(waited 0.00 days)\n", + "0.00: 🥵 Warm up complete.\n", + "2.74: Patient 1. Stroke arrival.\n", + "2.78: Patient 2. Stroke arrival.\n", + "3.36: Patient 3. Stroke arrival.\n", + "4.42: Patient 4. Stroke arrival.\n", + "4.68: Patient 5. Stroke arrival.\n", + "5.80: Patient -3 discharged.\n", + "5.80: Patient -10 admitted to acute ward.(waited 5.80 days)\n", + "5.80: Patient -8 discharged.\n", + "5.80: Patient 1 admitted to acute ward.(waited 3.06 days)\n", + "5.97: Patient 6. Stroke arrival.\n", + "6.08: Patient -6 discharged.\n", + "6.08: Patient 2 admitted to acute ward.(waited 3.30 days)\n", + "6.22: Patient -7 discharged.\n", + "6.22: Patient 3 admitted to acute ward.(waited 2.86 days)\n", + "6.33: Patient 7. Stroke arrival.\n", + "7.28: Patient 8. Stroke arrival.\n", + "7.41: Patient -4 discharged.\n", + "7.41: Patient 4 admitted to acute ward.(waited 2.99 days)\n", + "7.43: Patient 9. Stroke arrival.\n", + "7.55: Patient 10. Stroke arrival.\n", + "7.94: Patient -5 discharged.\n", + "7.94: Patient 5 admitted to acute ward.(waited 3.26 days)\n", + "8.11: Patient -2 discharged.\n", + "8.11: Patient 6 admitted to acute ward.(waited 2.14 days)\n", + "8.26: Patient 11. Stroke arrival.\n", + "8.42: Patient 12. Stroke arrival.\n", + "8.68: Patient 13. Stroke arrival.\n", + "8.72: Patient 14. Stroke arrival.\n", + "8.82: Patient -9 discharged.\n", + "8.82: Patient 7 admitted to acute ward.(waited 2.50 days)\n", + "8.95: Patient 15. Stroke arrival.\n", + "9.18: Patient 16. Stroke arrival.\n", + "9.87: Patient -1 discharged.\n", + "9.87: Patient 8 admitted to acute ward.(waited 2.58 days)\n", + "9.92: Patient 17. Stroke arrival.\n" ] }, { "data": { "text/plain": [ - "{'mean_acute_wait': 0.16965284328644098}" + "{'mean_acute_wait': 2.8362718457918676}" ] }, - "execution_count": 34, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -583,12 +583,41 @@ "init_cond_params[\"mode\"] = \"fixed\"\n", "\n", "# uncomment to vary the fixed amount 10 = 1 in queue.\n", - "# init_cond_params[\"fixed\"] = 10\n", + "init_cond_params[\"fixed\"] = 10\n", "\n", "experiment = Experiment(init_cond_params=init_cond_params)\n", - "results = single_run(experiment, rep=0, wu_period=5.0, rc_period=10.0)\n", + "results = single_run(experiment, rep=1, wu_period=0.0, rc_period=10.0)\n", "results" ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0aaef408-09ca-49e0-8d39-f4a088a4ef1b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'n_arrivals': 17,\n", + " 'waiting_acute': [3.0586175577655674,\n", + " 3.303449502025928,\n", + " 2.857579305401165,\n", + " 2.9908615153438225,\n", + " 3.2622390398417513,\n", + " 2.136685286636955,\n", + " 2.496306611009193,\n", + " 2.58443594831056]}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "experiment.results" + ] } ], "metadata": { From 60a0d6b28bddfdf229ce9fa4c0b01dbcb0ab161f Mon Sep 17 00:00:00 2001 From: TomMonks Date: Mon, 26 May 2025 15:08:09 +0100 Subject: [PATCH 6/6] fix(warmup): typo --- content/13_warm_up.ipynb | 124 ++++++++------------------------------- 1 file changed, 23 insertions(+), 101 deletions(-) diff --git a/content/13_warm_up.ipynb b/content/13_warm_up.ipynb index 4446048..c3ea9cc 100644 --- a/content/13_warm_up.ipynb +++ b/content/13_warm_up.ipynb @@ -9,7 +9,7 @@ "\n", "## Why do you need a warm-up period?\n", "\n", - "Typically when you are modelling a non-terminating system, you will need to deal with **initialisation bias**. That is the real system always has work-in-progress (e.g. patients in queues and in service), but the model starts from empty. One way to do this is to split the model's run length into warm-up and data collection periods. We discard all results in the warm-up period.\n", + "Typically when you are modelling a non-terminating system, you will need to deal with **initialisation bias**. That is the real system always has work-in-progress (e.g. patients in queues and in service), but the model starts from empty. One way to deal with this is to split the model's run length into warm-up and data collection periods. We discard all results in the warm-up period.\n", "\n", "> In this tutorial we will focus on coding a warm-up period in Python and SimPy rather than analysis to determine its length\n", "\n", @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "c1cee9f9-8696-4b13-94ff-bee2a2a2e5f8", "metadata": {}, "outputs": [], @@ -49,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "ea3d507f-9e6d-4ff0-8b90-f9c63c8a8bdf", "metadata": {}, "outputs": [], @@ -71,7 +71,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "1ecf0429-f03f-4ad2-abb4-46692a74e559", "metadata": {}, "outputs": [], @@ -96,7 +96,7 @@ "\n", "# run variables (units = days)\n", "WU_PERIOD = 0.0\n", - "RC_PERIOD = 100" + "RC_PERIOD = 100.0" ] }, { @@ -109,7 +109,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "52c9271f-1d05-454d-a199-8768bdf5b6e8", "metadata": {}, "outputs": [], @@ -137,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "576ae9b4-b21b-4ed0-9b13-e5898d423173", "metadata": {}, "outputs": [], @@ -236,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "dff74a08-37fd-4b18-8bcd-97994f38369a", "metadata": {}, "outputs": [], @@ -248,7 +248,7 @@ " Parameters:\n", " ----------\n", " warm_up_period: float\n", - " Duration of warm-up period in simultion time units\n", + " Duration of warm-up period in simulation time units\n", "\n", " env: simpy.Environment\n", " The simpy environment\n", @@ -276,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "911528e1-e4eb-4307-bb26-632faf7769d1", "metadata": {}, "outputs": [], @@ -328,7 +328,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "b3e686ce-5371-4471-a052-b9d43309bc85", "metadata": {}, "outputs": [], @@ -371,7 +371,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "0d0ea6cf-7d95-4d2c-9690-fcdbdae35d84", "metadata": {}, "outputs": [], @@ -438,67 +438,28 @@ "id": "c13f5e57-723c-409b-a1ce-cdb831b4e166", "metadata": {}, "source": [ - "## Quick check 1: No warm-up" + "## No warm-up" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "caf52390-5455-4fa1-bb22-60b5b91ad8d0", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.00: 🥵 Warm up complete.\n", - "3.29: Stroke arrival.\n", - "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", - "4.06: Stroke arrival.\n", - "4.06: Patient 2 admitted to acute ward.(waited 0.00 days)\n", - "5.31: Stroke arrival.\n", - "5.31: Patient 3 admitted to acute ward.(waited 0.00 days)\n", - "5.53: Stroke arrival.\n", - "5.53: Patient 4 admitted to acute ward.(waited 0.00 days)\n", - "5.76: Stroke arrival.\n", - "5.76: Patient 5 admitted to acute ward.(waited 0.00 days)\n" - ] - }, - { - "data": { - "text/plain": [ - "{'mean_acute_wait': 0.0}" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "TRACE = True\n", "experiment = Experiment()\n", - "results = single_run(experiment, rep=0, wu_period=0.0, rc_period=6.0)\n", + "results = single_run(experiment, rep=2, wu_period=10.0, rc_period=6.0)\n", "results" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "ddedb4f1-207d-4295-9ae4-c49b2c7cdcaf", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'n_arrivals': 5, 'waiting_acute': [0.0, 0.0, 0.0, 0.0, 0.0]}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# check how many patient waiting times recorded.\n", "experiment.results" @@ -509,43 +470,15 @@ "id": "660ea2e1-d9c2-4355-876c-43dfd9dab0fe", "metadata": {}, "source": [ - "## Quick check 2: Include a warm-up" + "## Include a warm-up" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "72b5284a-1fcb-4126-b663-c0ef0002e4bf", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "3.29: Stroke arrival.\n", - "3.29: Patient 1 admitted to acute ward.(waited 0.00 days)\n", - "4.06: Stroke arrival.\n", - "4.06: Patient 2 admitted to acute ward.(waited 0.00 days)\n", - "5.00: 🥵 Warm up complete.\n", - "5.31: Stroke arrival.\n", - "5.31: Patient 3 admitted to acute ward.(waited 0.00 days)\n", - "5.53: Stroke arrival.\n", - "5.53: Patient 4 admitted to acute ward.(waited 0.00 days)\n", - "5.76: Stroke arrival.\n", - "5.76: Patient 5 admitted to acute ward.(waited 0.00 days)\n" - ] - }, - { - "data": { - "text/plain": [ - "{'mean_acute_wait': 0.0}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "TRACE = True\n", "experiment = Experiment()\n", @@ -555,21 +488,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "7f5e282b-0f41-41df-bdca-f128e7d418c1", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'n_arrivals': 3, 'waiting_acute': [0.0, 0.0, 0.0]}" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# check how many patient waiting times recorded.\n", "experiment.results"