{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Basic profiling\n",
    "\n",
    "This demo assumes you are familiar with the basics of running an Intrepydd, which is covered in the [\"Hello, world!\" demo](./001-hello-world.ipynb), as well as the principle of [type specialization](./002-typing-basics.ipynb)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sum_elements(xs):\n",
    "    '''Sums all elements in a 3-D array `xs`.'''\n",
    "    assert len(xs.shape) == 3\n",
    "    s = 0.0\n",
    "    for i in range(xs.shape[0]):\n",
    "        for j in range(xs.shape[1]):\n",
    "            for k in range(xs.shape[2]):\n",
    "                s += xs[i, j, k]\n",
    "    return s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here is a function to test this function. We'll reuse this function later to test an Intrepydd version."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def test_code(fun=sum_elements):\n",
    "    from numpy import arange\n",
    "    xs = arange(10000000).reshape(1000,100,100).astype('double')\n",
    "    return fun(xs)\n",
    "    \n",
    "test_code()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Basic profiling ###\n",
    "\n",
    "To help find bottlenecks, you can use any of Python's standard tools for timing or profiling. Here, we show an example of using the [`line_profiler`](https://jakevdp.github.io/PythonDataScienceHandbook/01.07-timing-and-profiling.html), which gives you a line-by-line breakdown of where time is spent. Its use in Jupyter requires a magic command to load the module on first use:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%load_ext line_profiler"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Once loaded, you can then use the `%lprun` magic to invoke the profiler on any code statement. For example, let's apply it to the tester function (`test_code()`) again. The additional `-f <fun>` argument tells the profiler to only consider the statements, line-by-line, in the body of the specified function `<fun>`. (You can supply the `-f` argument multiple times to record these data for different function bodies.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%lprun -f test_code test_code()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Reimplement using Intrepydd ###\n",
    "\n",
    "Let's apply the technique of [type specialization](./jupyter-demo2.ipynb) to speed up this code. It requires minimal changes in this case: adding types to the signature and return value, and replacing Numpy field variable references with their corresponding functions (i.e., `xs.shape[0]` with `shape(xs, 0)`):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%writefile demo3.pydd\n",
    "# demo3.pydd\n",
    "\n",
    "def sum_elements(xs: Array(double, 3)) -> double:\n",
    "    '''Sums all elements in a 3-D array `xs`. (Intrepydd version)'''\n",
    "    s = 0.0\n",
    "    for i in range(shape(xs, 0)):\n",
    "        for j in range(shape(xs, 1)):\n",
    "            for k in range(shape(xs, 2)):\n",
    "                s += xs[i, j, k]\n",
    "    return s\n",
    "\n",
    "# eof"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then, compile with Intrepydd:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pyddc demo3.pydd"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And finally, let's load this new module and re-run the tester:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import demo3\n",
    "test_code(demo3.sum_elements)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If everything went well, you should see the same numerical output as with the original version. Now let's see if we're any faster:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%lprun -f test_code test_code(demo3.sum_elements)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Other timers.** Of course, since you are operating in Jupyter, you can use any timing or profiling tool at your disposal. For instance, here is how we can use the built-in `%timeit` magic function to programmatically measure the time and report the speedup of the two versions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "baseline_time = %timeit -o test_code()\n",
    "intrepydd_time = %timeit -o test_code(demo3.sum_elements)\n",
    "print(\"Speedup: ~ {:.1f}x\".format(baseline_time.best / intrepydd_time.average))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Summary and next steps ###\n",
    "\n",
    "A key first step in enabling higher performance is type specialization. The first way you do that in Intrepydd is by modifying the signatures of your function definitions to include annotations."
   ]
  }
 ],
 "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.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}