{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Application to Queueing Systems\n",
    "\n",
    "[The `Python(P)DEVS` documentation speaks of an application to queueing systems](https://msdl.uantwerpen.be/documentation/PythonPDEVS/queueing.html). We have a system that looks like this:\n",
    "<img src=\"https://msdl.uantwerpen.be/documentation/PythonPDEVS/_images/queue.svg\" alt=\"Processors-Overview\" width=\"500\"/>\n",
    "\n",
    "Let's create the corresponding example in `Python(P)DEVS-BBL`! Graphically, we can visualize it as presented below:\n",
    "![Processors-BBL](data/PythonPDEVS-BBL-Processors.svg)\n",
    "\n",
    "Obviously, this image is quite vage, so let's explain what all components do: \n",
    "* The **Random Delay Generator** generates messages with a randomized inter-arrival time. For the given example, these follow an exponential distribution with PDF $f$ and CDF $F$:\n",
    "$$f(x) = \\lambda e^{-\\lambda x}$$\n",
    "$$F(x) = 1 - e^{-\\lambda x}$$\n",
    "  Where $\\dfrac{1}{\\lambda}$ indicates the average time in between customers. Note that there is another random number generator, which is used to create the Job sizes, according to a Gauss distribution.\n",
    "* The **Queue** will enqueue all messages according to a Queueing Discipline (FIFO in our case), until a _requestdequeue_ has been obtained. When that happens, the first message in the queue will be dequeued over the _dequeue_ output.\n",
    "* The **Choose Output** will work as a multiplexer, transferring the message to one of the outputs, depending on the value obtained on the _select_ port.\n",
    "* The **Delayer** will delay a message for a certain time, depending on the Job size that was set in the Random Delay Generator.\n",
    "* The **Pick** is aware of a set of processes, which may be idle or busy and works together with the Choose Output block. When a process is marked \"busy\", it will make sure the Choose Output block will not send a message over the port corresponding to that process. When there is an idle process, a message is outputted on the _free_ port, which is used (in the example) to request a dequeue. Note that \"process\" can be replaced with a set of submodels in the previous explanation.\n",
    "* The **Collector** obtains all messages and allows for analysis of the system.\n",
    "\n",
    "The code for the above example is as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "from pypdevsbbl.generic.generators import RandomDelayGenerator\n",
    "from pypdevsbbl.generic.queues import Queue\n",
    "from pypdevsbbl.generic.routing import ChooseOutput, Pick, Delayer\n",
    "from pypdevsbbl.generic.collectors import TableCollector as Collector\n",
    "# from pypdevsbbl.extra.rngstreams.distributions import normal, exponential\n",
    "# from pypdevsbbl.extra.rngstreams import RngStream, U01, setSeed\n",
    "from pypdevs.DEVS import CoupledDEVS\n",
    "import random\n",
    "\n",
    "class Job:\n",
    "    def __init__(self, size, creation_time):\n",
    "        # Jobs have a size and creation_time parameter\n",
    "        self.size = size\n",
    "        self.creation_time = creation_time\n",
    "        \n",
    "\n",
    "def ptime(item, speed):\n",
    "    return 20.0 + max(1.0, item.size / speed)\n",
    "        \n",
    "\n",
    "class Model(CoupledDEVS):\n",
    "    def __init__(self, mu, size, processes, num):\n",
    "        super().__init__(\"Model\")\n",
    "        procs = len(processes)\n",
    "        \n",
    "        self.gen = self.addSubModel(\n",
    "            RandomDelayGenerator(\"Random Delay Generator\",\n",
    "                                 \n",
    "                                 # The inter-arrival time is exponentially distributed\n",
    "                                 dist=lambda x: random.expovariate(mu),\n",
    "                                 \n",
    "                                 # We set a limit on the amount being generated\n",
    "                                 max=num,\n",
    "                                 \n",
    "                                 # We also create the Jobs that are being generated.\n",
    "                                 func=lambda x, t: Job(int(random.gauss(size, 5)), t)))\n",
    "        \n",
    "        self.queue = self.addSubModel(Queue(\"Queue\"))\n",
    "        self.connectPorts(self.gen.output, self.queue.enqueue)\n",
    "        \n",
    "        self.choose = self.addSubModel(ChooseOutput(\"Choose Output\", procs))\n",
    "        self.connectPorts(self.queue.dequeue, self.choose.input)\n",
    "        self.pick = self.addSubModel(Pick(\"Pick\", procs,\n",
    "                                          # Always pick the available process with the longest idle time\n",
    "                                          select = lambda x: max(x, key=lambda i: i[1])))\n",
    "        self.connectPorts(self.pick.output, self.choose.select)\n",
    "        self.connectPorts(self.pick.free, self.queue.requestdequeue)\n",
    "        self.processes = []\n",
    "        self.collector = self.addSubModel(Collector(\"events\"))\n",
    "        for i, pspeed in enumerate(processes):\n",
    "            self.connectPorts(self.choose.outputs[i], self.pick.claim)\n",
    "            self.processes.append(self.addSubModel(Delayer(\"process %i\" % i,\n",
    "                                                           # The delay is dependent on the Job's size\n",
    "                                                           ft = lambda item: ptime(item, pspeed) + 1)))\n",
    "            self.connectPorts(self.choose.outputs[i], self.processes[i].start)\n",
    "            self.connectPorts(self.processes[i].finished, self.pick.inputs[i])\n",
    "            self.connectPorts(self.processes[i].finished, self.collector.input)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note:** The error message above ( _\"Cannot load RngStreams. Using 'random' module instead.\"_ ) is normal. This means you will use Python's `random` module for generating random numbers instead of PythonPDEVS-BBL's builtin generator.\n",
    "\n",
    "Notice the usage of the lambda functions. They provide a very flexible interface for the user to create their models. Let's simulate this model in a similar fashion to the simulation of the example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "from pypdevs.simulator import Simulator\n",
    "import random\n",
    "\n",
    "# Configuration:\n",
    "# 1) number of customers to simulate\n",
    "num = 500\n",
    "# 2) average time between two customers\n",
    "time = 30.0\n",
    "# 3) average size of customer\n",
    "size = 20.0\n",
    "# 4) efficiency of processors (products/second)\n",
    "speed = 0.5\n",
    "# 5) maximum number of processors used\n",
    "max_processors = 10\n",
    "# End of configuration\n",
    "\n",
    "# Store all results for output to file\n",
    "values = []\n",
    "\n",
    "print(\"RUNNING...\")\n",
    "# Loop over different configurations\n",
    "for i in range(1, max_processors):\n",
    "    random.seed(1)\n",
    "    \n",
    "    # Set up the system\n",
    "    procs = [speed] * i\n",
    "    m = Model(mu=1.0/time, size=size, num=num, processes=procs)\n",
    "\n",
    "    # PythonPDEVS specific setup and configuration\n",
    "    sim = Simulator(m)\n",
    "    sim.setClassicDEVS()\n",
    "    sim.simulate()\n",
    "\n",
    "    # Gather information for output\n",
    "    values.append([el - it.creation_time - ptime(it, speed) for el, it in m.collector.table()])\n",
    "    \n",
    "    \n",
    "# Write data to file\n",
    "with open('output.csv', 'w') as f:\n",
    "    for i in range(num):\n",
    "        f.write(\"%s\" % i)\n",
    "        for j in range(len(values)):\n",
    "            f.write(\", %5f\" % (values[j][i]))\n",
    "        f.write(\"\\n\")\n",
    "        \n",
    "print(\"FINISHED\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The generated csv file can be used as input in some sort of plotting context, as mentioned in the example. Let's display it using matplotlib."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib notebook\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "base = 1\n",
    "plt.figure()\n",
    "for p in range(len(values[base:])):\n",
    "    plt.plot(values[p], label='%i prcs' % (p + 1 + base))\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we compare these results to the ones that can be obtained from the example code, we can see they are quite similar. Differences between the results are a consequence of the random number generators. In fact, the library comes with its own random number generator, while the example uses the one that's bundled with Python."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "frame = pd.read_csv(\"../dependencies/pythonpdevs/examples/queueing/output.csv\", header=None, index_col=0)\n",
    "\n",
    "base = 1\n",
    "plt.figure()\n",
    "for p in range(len(frame.columns[base:])):\n",
    "    plt.plot(frame[p+1], label='%i prcs' % (p + 1 + base))\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "More complex analysis can be done using `gnuplot` with the bundled `plot` file and the csv results:\n",
    "\n",
    "| Library Results | Example/Reference Results |\n",
    "|:---:|:---:|\n",
    "| ![evolution](data/queueing_evolution.svg) | ![evolution](data/queueing_evolution_pypdevs.svg) |\n",
    "| ![boxplot](data/queueing_boxplot.svg) | ![boxplot](data/queueing_boxplot_pypdevs.svg) |"
   ]
  }
 ],
 "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.8.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}