{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Statistics Tracer\n",
    "\n",
    "## Problem Statement:\n",
    "\n",
    "You are the owner of a small, local supermarket. Because of budget cuts, you cannot afford to open too many cash registries at once. Yet, you also know that people don't like to wait too long before checking out their groceries.\n",
    "\n",
    "To make sure you won't lose any customers, you want to determine the average waiting time in your store when you have a single cash registry open. You know from experience that, on a good day, the amount of people that enters your store every hour follows a normal distribution with mean 120 and standard derivation 80. On average, the shopping takes 25 minutes and waiting at the cash registry until the previous customer has checked out his groceries takes about 5 minutes.\n",
    "\n",
    "We will analyze the store for 7 days, where the store is open from 8 a.m. until 8 p.m. and no new people may enter in the last hour.\n",
    "\n",
    "Let's assume the inter-arrival time in the store is uniformly distributed over the hour.\n",
    "\n",
    "## Our Model:\n",
    "\n",
    "Let's say that an hour is modeled by 60 time units (e.g. one minute = one time unit). We will use a `RandomNumberGenerator` for creating the amount of customers and a `BulkGenerator` to make sure we have a single message for each customer. This is followed by a `SimpleQueue` that represents the store, a `Queue` that indicates the cash registry and finally a `Finish` block. Additionally, we will use the `Transformer` and `ChooseOutput` blocks to evenly distribute the arrivals over the hour."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "pixiedust": {
     "displayParams": {}
    }
   },
   "outputs": [],
   "source": [
    "from pypdevs.DEVS import CoupledDEVS\n",
    "\n",
    "from pypdevsbbl.generic.generators import RandomNumberGenerator as RNG, BulkGenerator\n",
    "from pypdevsbbl.extra.rngstreams.distributions import normal\n",
    "from pypdevsbbl.generic.queues import Queue, SimpleQueue\n",
    "from pypdevsbbl.generic.routing import Finish, ChooseOutput\n",
    "from pypdevsbbl.generic.transformers import Transformer\n",
    "\n",
    "class Model(CoupledDEVS):\n",
    "    def __init__(self):\n",
    "        CoupledDEVS.__init__(self, \"Model\")\n",
    "\n",
    "        self.cgen = self.addSubModel(RNG(\"CGen\", dt=60, seed=15,\n",
    "                                         dist=normal, args=(120, 80)))                # -- [1]\n",
    "        \n",
    "        self.close = self.addSubModel(Transformer(\"Close\",\n",
    "                                                  lambda i, e, t: (t % 720) < 660))   # -- [2]\n",
    "        self.selct = self.addSubModel(ChooseOutput(\"Select\"))                         # -- [2]\n",
    "        \n",
    "        self.dist = self.addSubModel(Transformer(\"Dist\", lambda i, e, t: 60 / i))     # -- [3]\n",
    "        self.enter = self.addSubModel(BulkGenerator(\"Enter\"))                         # -- [4]\n",
    "        self.store = self.addSubModel(SimpleQueue(\"Store\", dr=25))                    # -- [5]\n",
    "        self.cash = self.addSubModel(Queue(\"Cash Registry\", dd=5))                    # -- [6]\n",
    "        self.leave = self.addSubModel(Finish(\"Leave\"))\n",
    "        \n",
    "        self.connectPorts(self.cgen.output, self.close.input)\n",
    "        \n",
    "        self.connectPorts(self.close.transformed, self.selct.select)\n",
    "        self.connectPorts(self.close.original, self.selct.input)\n",
    "        self.connectPorts(self.selct.outputs[1], self.dist.input)\n",
    "        \n",
    "        self.connectPorts(self.dist.transformed, self.enter.dt)\n",
    "        self.connectPorts(self.dist.original, self.enter.input)\n",
    "        self.connectPorts(self.enter.output, self.store.enqueue)\n",
    "        self.connectPorts(self.store.renege, self.cash.enqueue)\n",
    "        self.connectPorts(self.cash.dequeue, self.leave.input)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's discuss some of the specialities for this model:\n",
    "1. The `RNG` generates a value $n$, representative of how many customers we will see in the store. It follows the $N(120, 80)$-distribution. For reproducability, we'll use a seed of 15.\n",
    "2. We ignore the hours that the store is closed and no new customers arrive the last hour of every day.\n",
    "3. We assume the inter-arrival time of the customers is uniformly distributed over the hour, meaning that we can compute the delay between customers $\\Delta t$ as $\\dfrac{60}{n}$. E.g. if 60 customers arrive, we assume there is one customer every minute. If we have 120 customers that arrive, we assume there is one customer every half a minute.\n",
    "\n",
    "   Note that we do an integer division because (1) we don't want anyone to arrive at the start of the new hour and (2) _PythonPDEVS_ deadlocks if we don't.\n",
    "4. The `BulkGenerator` will generate $n$ customers evenly distributed over the hour as indicated by $\\Delta t$.\n",
    "5. A single customer would remain in the store for 25 minutes (on average). They do not have to wait for the previous customer to complete shopping before going to the cash registry. This is called _reneging_.\n",
    "6. At the cash registry, the second customer waits after the first one and so on. Each customer waits approximately 5 minutes. This is indicative of a _dequeueing_ operation.\n",
    "\n",
    "Let's create a simulator for this model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pypdevs.simulator import Simulator\n",
    "\n",
    "model = Model()\n",
    "sim = Simulator(model)\n",
    "sim.setClassicDEVS()\n",
    "sim.setTerminationTime(5040)   # -- [7]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "7. We will simulate this model for 7 days from 8 a.m. until 8 p.m. (= 12 hours per day). Because our model uses a single time unit for each minute, we get $7\\cdot 12\\cdot 60 = 5040$ time units. Note that this is an inclusive value.\n",
    "\n",
    "## The Tracer:\n",
    "\n",
    "To get the average waiting time in our store, we need something that can keep track of the delay in the queue: the `StatisticsTracer`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from pypdevsbbl.tracers import setTracer, StatisticsTracer\n",
    "\n",
    "setTracer(sim, StatisticsTracer, None, [model.cash.enqueue], [model.cash.dequeue])  # -- [8]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "8. The `StatisticsTracer` requires a list of input ports and a list of output ports to listen on. We only need the information of the cash registry. To prevent the need of giving the ports of submodels of coupled DEVS, the tracer will automatically assign this for you. If it didn't, instead of `[model.cash.enqueue]`, we would need `[model.cash._queue.enqueue]`, since the `Queue` is such a coupled DEVS.\n",
    "\n",
    "Let's simulate our model!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "pixiedust": {
     "displayParams": {}
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "STATISTICS:\n",
      "===========\n",
      "\tSUMMARY:..\n",
      "\t\tAverage Time in System:                   2.52820\n",
      "\t\tAverage Num of Messages per Time Unit:    5.68937\n",
      "\t\tTotal Throughput:                         9421\n",
      "\t\tTotal Amount of Arrivals:                 9421\n",
      "\t\tTotal Amount of Departures:               9421\n",
      "\t\tAmount of Newly Generated Messages:       0\n",
      "\t\tAmount of Bequeathed Messages:            0\n"
     ]
    }
   ],
   "source": [
    "sim.simulate()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, we get 7 statistics:\n",
    "- **Average Time in System:** The average time that each customer was at the cash registry.\n",
    "- **Average Num of Messages per Time Unit:** The average amount of customers that were waiting for one minute at the registry.\n",
    "- **Total Throughput:** The total amount of people that entered and left the store.\n",
    "- **Total Amount of Arrivals:** The amount of people that entered the store.\n",
    "- **Total Amount of Departures:** The amount of people that left the store.\n",
    "- **Amount of Newly Generated Messages:** The amount of people that suddenly started to exist in the store.\n",
    "- **Amount of Bequeathed Messages:** The amount of people that were left behind in the store.\n",
    "\n",
    "From these statistics, we can see that by average 5.68937 people were waiting (at the same time) at the cash registry for one minute. Additionally, the mean waiting time before payment is 2.52820 minutes, which is about 50% faster than anticipated.\n",
    "\n",
    "When thinking about it, this totally makes sense. Because of the high shopping time, the low checkout time and the fact that all customers will be shopping for a predefined amount of time, the waiting line at the cash registry will always have very few people waiting."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "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.6.9"
  },
  "pycharm": {
   "stem_cell": {
    "cell_type": "raw",
    "metadata": {
     "collapsed": false
    },
    "source": []
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
