{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "![McLaren Maze Race Banner](media/banner.png)\n", "\n", "# Welcome to Learner Driver - Level 1 of the McLaren Maze Race! \n", "\n", "In this level we will build a basic artificial intelligence (AI) that can drive our simulated F1 car whilst introducing the foundations of the Maze Race Challenge. \n", "\n", "### Introduction to notebooks\n", "\n", "First up, a short introduction to Jupyter notebooks (which this is). If you are already familiar with them, skip to the next heading. Jupyter notebooks like this consist of sections of text (like this one) and sections of code (like the one below). You can just read the whole notebook without running anything (although some of the figures are not shown until the code is run). You will miss out on seeing the AI learn however! To run a code section, first click on it to select it - a blue bar should appear on the left to highlight the currently selected cell. Once selected there are three ways to run it:\n", "- Press ctrl-enter\n", "- Press the play button on the toolbar at the top\n", "- Press the black arrow to the left of the section you want to run (might not appear in Binder depending on your settings)\n", "\n", "**NB: the code sections are all related to each other and need to be run in order.** \n", "\n", "For the best experience, read each text section and run each code section as you move down the page. If you try and run a later piece of code without running the ones above then you might hit an error. If that happens go back and run the cells above. You can reset everything by using the \"Restart Kernel\" option up on the top toolbar. If you get stuck there should be plenty of help articles around online or you can contact us using [this form](https://forms.office.com/Pages/ResponsePage.aspx?id=1D5YJvyfwkadGvDKNaMKjclg_cyBBFJPg8x5VJ87DGJUNlNFTlVHS05LTUpKRk8xR04zOFVORFg3VS4u).\n", "\n", "Please note, some sections of code might take 30 seconds or more to run, so please be patient and don't immediately click the arrow to run again! There is usually and indicator up on the toolbar showing you that the kernel is busy working on the code.\n", "\n", "### Our first maze\n", "\n", "First up, let's take a look at the maze we will be racing through - remember though, that the driver doesn't get\n", "to see the full track when making decisions." ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" }, "scrolled": false }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "bb81b2df9dc64744bfb34d7c5295e508", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "%matplotlib ipympl\n", "from imports import *\n", "\n", "track = TrackStore.load_track(level=Level.Learner, index=1) # provide the level and, optionally, an index. If no index\n", " # is provided a random track will be loaded.\n", "track.plot_track(); # a useful method to display the track" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "In level 1 there is just a single maze, which is much shorter and simpler than the ones we will be encountering later on.\n", "There are no branches or dead ends, the challenge instead focusing on learning to drive the car. To do this, at each point\n", "the AI will need to choose from a set of 6 actions:\n", "- Light Throttle\n", "- Full Throttle\n", "- Light Braking\n", "- Heavy Braking\n", "- Continue\n", "- Turn \n", "\n", "Each of these actions will have a different effect depending on the speed that the car is travelling at. For example,\n", "if the driver tries to turn when the car is travelling too fast then the car will spin. Equally, braking too hard at too\n", "high a speed will lock the wheels and won't slow the car down. On the other hand, if you try and accelerate too hard from\n", "stationary, there won't be enough aerodynamic load on the car and the wheels will spin, leading to a very sluggish launch.\n", "This is shown in the figure below:\n", "\n", "![Full throttle then heavy braking](media/full_throttle_heavy_brake.PNG)\n", "\n", "Now we have taken a look at the track and the car it is time to introduce our hero-to-be, the AI learner driver. This is \n", "implemented in the `LearnerDriver` class in the drivers > learnerdriver.py file. As we look at how this AI driver learns,\n", "you are encouraged to play around with the code to see if you can improve the AI. If you want to take part in the challenge,\n", "then you will need to submit the complete code for your driver class on the McLaren Maze Race website. Your driver will\n", "then compete against all the other drivers to see who wins the Championship!\n", "\n", "Let's create our driver. " ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" } }, "outputs": [], "source": [ "from drivers.learnerdriver import LearnerDriver\n", "\n", "driver_name = 'Dando' # choose a name for your driver\n", "driver = LearnerDriver(driver_name) # create the driver" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now let us test a driver on a race track. To do this we can use the race method from the racecontrol.py file." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" } }, "outputs": [ { "data": { "image/png": "iVBORw0KGgoAAAANSUhEUgAAA3sAAAHZCAYAAAA/hiIvAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAA9hAAAPYQGoP6dpAACxpklEQVR4nOzdd1xT1/sH8M8NU9nIEgVZblzgKHXhxK1Vq1arYmutraPqV9tqta4qrqqtWq2tdW9bbevGXesGtO6KC1wgComgDMn9/cGPlEiAADckgc/79cpLc+/Jc58bLpCHc+45giiKIoiIiIiIiKhUkek7ASIiIiIiIpIeiz0iIiIiIqJSiMUeERERERFRKcRij4iIiIiIqBRisUdERERERFQKsdgjIiIiIiIqhVjsERERERERlUIs9oiIiIiIiEohFntEREREVOatWbMGgiBAEATcu3dP3+kQSYLFHhERkYE6duyY6sOnIAiwsbHBy5cvC3zdq1evYGdnp/baY8eO6T5hI/X48WPV+/To0SPVdlEUUaFCBQiCgBMnTpRILjkLjoIea9asyTPOnj17MG3aNHTu3Bk1a9aEk5MTzMzM4ODggMDAQPzvf//DzZs3S+ScdO3evXtav2f5PUg3vvjiiyL/LDp06BBCQ0Ph5+cHKysr2NnZoVq1aujduzeWL1+O5OTkIueVlJSE8PBwzJo1C927d4e7u7sqx+Dg4CLHNTSm+k6AiIiItJOcnIxdu3ahf//++bb7/fffoVAoSigr4/f3338DALy9veHu7q7afu3aNTx//hzm5uZo3LixvtIrtNevX6NLly4a9yUlJSEyMhKRkZFYsmQJZsyYgS+//LKEM6Sy4uLFi1i4cGGhX5eYmIghQ4bg999/z7VPoVDg1q1b+PXXXxEUFIT69esXKbcGDRqUiR5cFntERERGwNLSEqmpqVi/fn2Bxd769evVXkP5yy72mjVrprb9r7/+AgAEBgbC0tKyxPM6cOCAWvH5psqVK+e5z87ODsHBwWjSpAl8fHxQsWJFlC9fHo8ePcKxY8fwyy+/QC6XY+LEibC3t8fw4cN1cQololKlSrh8+XKe++vUqQMAaNiwIVavXp1nO39/f4SGhkqdXpmlVCoxbNgwvH79Gi4uLoiPj9fqdXK5HO3atUNERAQA4J133kHv3r3h6+sLExMTxMbG4vjx4/j111+LlZ8oiqr/u7q6olGjRti9e3exYhoiFntERERGoFu3bti2bRvCw8Px5MkTuLm5aWwXHx+PgwcPAgC6d++OrVu3lmSaRunUqVMAchd7J0+e1Li9pFSrVg1eXl6Ffp2pqSmePXsGExMTjfu7deuGUaNGITAwEImJifj666/x0Ucf5dne0JmZmcHf37/AdlZWVlq1I2l8//33OH/+PGrUqIF33nkHYWFhWr1u1KhRiIiIgIWFBbZt24Zu3bqp7W/YsCHeeecdLFq0CJmZmUXOb+TIkfD29kbjxo3h4eEBAKVyOC/v2SMiIjIC7du3h5ubGzIzM7F58+Y8223evBmvX7+Gm5sb2rVrV4IZGqdXr14hKioKQN49e/oq9oqjoMLN29sbffr0AQA8ffoUN27cKIm0qIyIiYnBlClTAAArVqyAubm5Vq87efKkamTCN998k6vQy0kQBJiaFr3favz48ejVq5eq0CutWOwREREZARMTE7z33nsA/humqcm6desAAP3799e6pyY9PR0//PADWrVqBWdnZ5ibm8PNzQ2dOnXChg0boFQqc73m/v37kMlkEAQBX331VYHH2Lx5s2ryg71792psEx0djbFjx6JOnTqws7NDuXLl4OPjg9DQUFy4cEGrcymsc+fOISMjAxUqVEDNmjVV22NiYhATEwNBENC0aVOdHFvfbGxsVP/ncN+CZ+MMDg5Wm7wjOjoaw4cPh4+PD8qVKwcvLy98+OGHuH//vtrrrly5giFDhsDHxweWlpbw8PDAJ598ovWwxl27duHdd9+Fp6cnLC0tYW9vj4YNG2L69OlITEws7mnrxIgRI5CcnIzBgwejZcuWWr9u6dKlALKGIY8cOVJX6ZUtIhERERmko0ePigBEAOLq1avFyMhI1fMrV67kan/16lXV/qioKHH16tWq50ePHtV4jLt374o1atRQtdP0aNasmfjs2bNcr23WrJkIQPT29i7wXDp37iwCEJ2dncWMjIxc++fPny+amZnlmYMgCOKUKVMKftPykfP9LM5D03uZ872eOnVqkXPMGefu3btFjlOQly9filWrVhUBiDKZTFQoFDo7lr5lv58tW7bMt11B733Lli1VccLDw0UbGxuN14eLi4t4/fp1URRFcdOmTaK5ubnGdlWqVBEfPnyYZz7Pnz8XW7dune+16OLiIp4+fTrPGFWqVFG1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dEtERERElBcWe6SVnMMupSqmsoddSlVYSZkfEREREZGxY/cFERERERFRKcRij4iIiIiIqBRisUdERERERFQKSV7shYWFoVGjRrCxsYGLiwt69OiBmzdvSn0YIiIiIiIiyofkxd7x48cxYsQInDlzBuHh4cjIyED79u2RkpIi9aGIiIiIiIgoD5LPxrl//36152vWrIGLiwsiIiLQokULqQ9HREREREREGuh86QW5XA4AcHR01Lg/LS0NaWlpqucKhULXKRERURnz5u8WCwsLWFhY6CkbIiKikqHTCVqUSiXGjBmDpk2bwt/fX2ObsLAw2NnZqR4eHh66TImIiMogDw8Ptd81YWFhGtstX74cdevWha2tLWxtbREUFIR9+/ap9qempmLEiBGoUKECrK2t0atXL8TFxanFiImJQefOnVG+fHm4uLhgwoQJeP36tU7Pj4iISBOd9uyNGDECV65cwcmTJ/NsM3HiRIwbN071XKFQICoqSpdpERFRGRMbGwtbW1vV87x69SpXrow5c+agatWqEEURa9euRffu3REVFYXatWtj7Nix2LNnD7Zv3w47OzuMHDkSPXv2xN9//w0AyMzMROfOneHm5oZTp07h8ePHGDRoEMzMzDB79uwSOVciIqJsOiv2Ro4cid27d+PEiROoXLlynu04lIaIiHQtu6euIF27dlV7PmvWLCxfvhxnzpxB5cqVsWrVKmzatAmtW7cGAKxevRo1a9bEmTNn8NZbb+HgwYO4du0aDh06BFdXV9SvXx8zZ87EF198gWnTpsHc3Fwn50dERKSJ5MM4RVHEyJEjsXPnThw5cgTe3t5SH4KIiEjnMjMzsWXLFqSkpCAoKAgRERHIyMhA27ZtVW1q1KgBT09PnD59GgBw+vRp1KlTB66urqo2ISEhUCgUuHr1aomfAxERlW2S9+yNGDECmzZtwu+//w4bGxs8efIEAGBnZ4dy5cpJfTgiIiJJXb58GUFBQUhNTYW1tTV27tyJWrVq4eLFizA3N4e9vb1ae1dXV9XvuidPnqgVetn7s/dpktdEZRkZGcjIyCjWuWS/vrhx9MWY8zfm3AHjzp+5648x569N7sZ4XpIXe8uXLwcABAcHq21fvXo1QkNDpT4cERGRpKpXr46LFy9CLpdjx44dGDx4MI4fP66z44WFhWH69Om5th88eBDly5eX5Bjh4eGS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QRAk7dmLjo6WtGePiLT38OFDtG3bFu+//z7c3d1x48YNrFixAm5ubhoXLifStz///BPXrl3DypUrMXLkyAJnzixrWOwREZUhSqUSoihCqVRKUkzJZDIIggCZTCZJsQdA0vxY7BEVjoODAwIDA/Hzzz/j6dOnsLKyQufOnTFnzhyNi0MT6duoUaMQFxeHTp06qe4VpP+w2CMiIiIiAICdnR22bt2q7zSItHbv3j19p2DQuM4eERERERFRKcRij4iIiIiIqBQqdLF34sQJdO3aFe7u7hAEAbt27cqz7fDhwyEIAhYvXlyMFImIiIiIiKiwCl3spaSkoF69eli2bFm+7Xbu3IkzZ84UuHAlERERERERSa/QE7R07NgRHTt2zLfNw4cPMWrUKBw4cACdO3cucnJERERERERUNJLPxqlUKjFw4EBMmDABtWvXLrB9Wloa0tLSVM8VCoXUKRERURn35u8WCwsLWFhY6CkbIiKikiH5BC1z586FqakpRo8erVX7sLAw2NnZqR4eHh5Sp0RERGWch4eH2u+asLAwfadERESkc5L27EVEROC7775DZGSk1gvZTpw4EePGjVM9VygUiIqKkjItIiIq42JjY2Fra6t6zl49IiIqCyQt9v766y/Ex8fD09NTtS0zMxP/+9//sHjxYo2LHnIoDRER6Zqtra1asUdERFQWSFrsDRw4EG3btlXbFhISgoEDB2LIkCFSHoqIiIiIiIjyUehiLzk5GdHR0arnd+/excWLF+Ho6AhPT09UqFBBrb2ZmRnc3NxQvXr14mdLREREREREWil0sXfhwgW0atVK9Tz7frvBgwdjzZo1kiVGRERERERERVfoYi84OBiiKGrdXtN9ekRERERERKRbki+9QERERERERPrHYo+IiIiIiKgUknQ2TiIiMmwymQyCIEAmk+ZvfUqlEqIoQqlUShZPyvwEQUBmZqYksYiIiIwNiz0iojIkICAAfn5+AFCo+6/zEh8fD7lcDlEUJSnQlEqlpPkplUpEREQUOw4REZExYrFHRFSGREZGqv6VojfOxcUFgiDA2dlZsmIvOjpasvwEQSh2DCIiImPFYo+IqAzJOexSimIq57BQqYZeSpkfiz0iIirLOEELERERERFRKcRij4iIiIiIqBRisUdERERERFQKsdgjIiIiIqO3f/9+1K9fH5aWlhAEAUlJSfpO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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "set_seed(0)\n", "\n", "# The race method will setup the car and driver for a new race and then call driver.make_a_move repeatedly as the car\n", "# navigates the race. Once the driver reaches the end of the race, or the max_number_of_steps is reached, the function \n", "# completes.\n", "race(driver, track=track, plot=True, max_number_of_steps=150); \n", "plt.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Well our learner driver got to the end but had a few crashes along the way! Clearly our AI has a few things to learn yet! Let's race several more times on the track and see if the AI gets any better. We can measure the AI's performance by using the \"race time\" value that is returned by `race`. This is effectively our \"lap time\" and is the value that will ultimately determine the winner of a race - the AI that completes the race in the shortest time wins. The amount of time that a move takes is related to the car speed, so the faster the AI drives the car, the lower it's race time will be. Crashes add 10 seconds to the race time (which is a lot!), though, so speed needs to be balanced with control - a single crash could knock the driver out of the race lead!\n", "\n", "Let's plot how the race time varies as we keep repeating the track." ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" } }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "65189420e3254d6cb8f737eae80f87a0", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "driver = LearnerDriver(driver_name) # reset our driver\n", "num_races = 24 # number of races to complete, one after another\n", "race_times = np.zeros(num_races)\n", "\n", "# Repeat the race num_races times, each time the driver will have learnt more\n", "set_seed(1)\n", "for n in range(num_races):\n", " driver, race_times[n], _ = race(driver, track=track, plot=False, max_number_of_steps=1000)\n", "\n", "# Plot the race time achieved each time\n", "fig = plt.figure(figsize=(9, 5))\n", "plt.plot(range(1, num_races+1), race_times)\n", "plt.ylabel('Race Times', fontsize=16)\n", "plt.xlabel('Number of times we repeated the race', fontsize=16)\n", "fig.gca().xaxis.set_major_locator(MaxNLocator(integer=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that the AI rapidly learns to improve its driving after the first race, and converges to a fixed solution - the race times stop improving. Let's see what our AI looks like now on the race track." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" } }, "outputs": [ { "data": { "image/png": "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DMGhQ4d0dr7IyEjMmjWrwPY9e/agQoUKOjuvJjExMWV6PikZc+yAccfP2PXHmOM3xtifPn2q7xBKTPJkTxRFqaskIiIqM5aWlvD39wcAhISE4NSpU1i6dCn69euHZ8+eIS0tTa11Lzk5GR4eHgAADw8PnDx5Uq0+5WydyjIv0zRRmZeXFzp06KDVOEMp5ObmIiYmBu3bt4eFhUWZnFMqxhw7YNzxM3b9Meb4jTn2hw8f6juEEtP5OntERETGTKFQICcnByEhIbCwsMC+ffvQu3dvAMDly5dx69YthIaGAgBCQ0Mxb948pKSkwM3NDUD+t9cODg6oW7euxvoLm6jMwsKizD8I6eOcUjHm2AHjjp+x648xx2+MsRtbvACTPSIiIpXJkyejU6dO8Pb2xpMnTxAVFYWDBw9i9+7dcHR0xLBhwzBhwgQ4OzvDwcEBY8eORWhoKJo2bQoA6NChA+rWrYtBgwZh0aJFuH//PqZOnYrRo0dz5mkiIipzTPZIKzKZDIIgQCaTZgJXhUIBURShUCgkq0/K+KSqh4iMS0pKCgYPHox79+7B0dER9evXx+7du9G+fXsAwJdffgmZTIbevXsjJycH4eHh+Pbbb1XHm5mZYfv27XjvvfcQGhoKW1tbDBkyBLNnz9bXJRERUTnGZI+0EhwcrBrDIsW4zJSUFMjlcoiiKElipVAoJI1PEIRS10FExmfVqlVF7re2tsayZcuwbNmyQstUq1YNO3fulDo0IiKiEmOyR1qJi4tT/ZSiNc7NzQ2CIMDV1VWyZC8xMVGy+NiyR0RERETGjskeaeXFbpdSJVPKbpdSJVZSxkdEREREZOzYfEFERERERGSCmOwRERERERGZICZ7REREREREJojJHhERERERkQliskdERERERGSCmOwRERERERGZICZ7REREREREJojJHhERERERkQliskdERERERGSCmOwRERERERGZICZ7REREREREJojJHhERERERkQliskdERERERGSCmOwRERERERGZICZ7REREREREJojJHhERERERkQky13cAZBxkMhkEQYBMJs33AwqFAqIoQqFQSFaflPFJVQ8RERERkb4w2SOtBAcHw9/fHwAgimKp60tJSYFcLocoipIkVgqFQtL4BEEodR1ERERERPrEZI+0EhcXp/opRWucm5sbBEGAq6urZMleYmKiZPGx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4iRpjcvJycGDBw/g6uoqWbLXrFkzyeKTyWSFzrRERMale/fuqum+iYiIyhO27JFWXux2KVUypex2KVWXSSnjIyIiIiIydhyYREREREREZIKY7BEREREREZkgJntEREREREQmiMkeERERERGRCWKyR0REREREZIKY7BEREREREZkgJntEREREREQmiMkeERERERGRCWKyR0REREREZIKY7BEREREREZkgJntEREREREQmiMkeERERERGRCWKyR0REREREZIKY7BEREREREZkgJntEREREREQmiMkeERERERGRCTLXdwBkHGQyGQRBgEwmzfcDCoUCoihCoVBIVp+U8UlVDxERERGRvjDZI60EBwfD398fACCKYqnrS0lJgVwuhyiKkiRWCoVC0vgEQSh1HURERERE+sRkj7QSFxen+ilFa5ybmxsEQYCrq6tkyV5iYqJk8bFlj4iIiIiMHZM90sqL3S6lSqaU3S6lSqykjI+IiIiIyNix+YKIiIiIiMgEMdkjIiIiIiIyQUz2iIiIiIiITFCJk73Dhw+jW7du8PT0hCAI2LZtm9p+URQxffp0VK5cGTY2NmjXrh2uXLkiVbxERERERESkhRIne5mZmWjQoAGWLVumcf+iRYvw1VdfYcWKFThx4gRsbW0RHh6O7OzsUgdLRERERERE2inxbJydOnVCp06dNO4TRRFLlizB1KlT0aNHDwDAunXr4O7ujm3btqF///6li5aIiIiIiIi0IumYvevXr+P+/fto166dapujoyOaNGmCY8eOaTwmJycH6enpag8iIiIpvfw+k5OTo++QiIiIdE7SZO/+/fsAAHd3d7Xt7u7uqn0vi4yMhKOjo+rh5eUlZUhERETw8vJSe6+JjIzUd0hEREQ6p/dF1SdPnowJEyaonqenpyM+Pl6PERERkalJSkqCg4OD6rmVlZUeoyEiIiobkiZ7Hh4eAIDk5GRUrlxZtT05ORlBQUEaj7GysuKbLhER6ZSDg4NaskdERFQeSNqN08fHBx4eHti3b59qW3p6Ok6cOIHQ0FApT0VERERERERFKHHLXkZGBhITE1XPr1+/jjNnzsDZ2Rne3t4YP3485s6dixo1asDHxwfTpk2Dp6cnevbsKWXcREREREREVIQSJ3unT59G69atVc+V4+2GDBmCNWvW4OOPP0ZmZiZGjBiBtLQ0NG/eHLt2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\n", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "np.random.seed(0)\n", "race(driver, track=track, plot=True, max_number_of_steps=150);\n", "plt.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That looked much better, it didn't crash at all! We can analyse our driver's performance in closer detail by running the driver down a simple straight and plotting the actions it takes. To do this, use the `straight_line_sim` method from race control." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" }, "scrolled": false }, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "/* global mpl */\n", "window.mpl = {};\n", "\n", "mpl.get_websocket_type = function () {\n", " if (typeof WebSocket !== 'undefined') {\n", " return WebSocket;\n", " } else if (typeof MozWebSocket !== 'undefined') {\n", " return MozWebSocket;\n", " } else {\n", " alert(\n", " 'Your browser does not have WebSocket support. ' +\n", " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", " 'Firefox 4 and 5 are also supported but you ' +\n", " 'have to enable WebSockets in about:config.'\n", " );\n", " }\n", "};\n", "\n", "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", " this.id = figure_id;\n", "\n", " this.ws = websocket;\n", "\n", " this.supports_binary = this.ws.binaryType !== undefined;\n", "\n", " if (!this.supports_binary) {\n", " var warnings = document.getElementById('mpl-warnings');\n", " if (warnings) {\n", " warnings.style.display = 'block';\n", " warnings.textContent =\n", " 'This browser does not support binary websocket messages. ' +\n", " 'Performance may be slow.';\n", " }\n", " }\n", "\n", " this.imageObj = new Image();\n", "\n", " this.context = undefined;\n", " this.message = undefined;\n", " this.canvas = undefined;\n", " this.rubberband_canvas = undefined;\n", " this.rubberband_context = undefined;\n", " this.format_dropdown = undefined;\n", "\n", " this.image_mode = 'full';\n", "\n", " this.root = document.createElement('div');\n", " this.root.setAttribute('style', 'display: inline-block');\n", " this._root_extra_style(this.root);\n", "\n", " parent_element.appendChild(this.root);\n", "\n", " this._init_header(this);\n", " this._init_canvas(this);\n", " this._init_toolbar(this);\n", "\n", " var fig = this;\n", "\n", " this.waiting = false;\n", "\n", " this.ws.onopen = function () {\n", " fig.send_message('supports_binary', { value: fig.supports_binary });\n", " fig.send_message('send_image_mode', {});\n", " if (fig.ratio !== 1) {\n", " fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", " }\n", " fig.send_message('refresh', {});\n", " };\n", "\n", " this.imageObj.onload = function () {\n", " if (fig.image_mode === 'full') {\n", " // Full images could contain transparency (where diff images\n", " // almost always do), so we need to clear the canvas so that\n", " // there is no ghosting.\n", " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", " }\n", " fig.context.drawImage(fig.imageObj, 0, 0);\n", " };\n", "\n", " this.imageObj.onunload = function () {\n", " fig.ws.close();\n", " };\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", "\n", " this.ondownload = ondownload;\n", "};\n", "\n", "mpl.figure.prototype._init_header = function () {\n", " var titlebar = document.createElement('div');\n", " titlebar.classList =\n", " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", " var titletext = document.createElement('div');\n", " titletext.classList = 'ui-dialog-title';\n", " titletext.setAttribute(\n", " 'style',\n", " 'width: 100%; text-align: center; padding: 3px;'\n", " );\n", " titlebar.appendChild(titletext);\n", " this.root.appendChild(titlebar);\n", " this.header = titletext;\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._init_canvas = function () {\n", " var fig = this;\n", "\n", " var canvas_div = (this.canvas_div = document.createElement('div'));\n", " canvas_div.setAttribute(\n", " 'style',\n", " 'border: 1px solid #ddd;' +\n", " 'box-sizing: content-box;' +\n", " 'clear: both;' +\n", " 'min-height: 1px;' +\n", " 'min-width: 1px;' +\n", " 'outline: 0;' +\n", " 'overflow: hidden;' +\n", " 'position: relative;' +\n", " 'resize: both;'\n", " );\n", "\n", " function on_keyboard_event_closure(name) {\n", " return function (event) {\n", " return fig.key_event(event, name);\n", " };\n", " }\n", "\n", " canvas_div.addEventListener(\n", " 'keydown',\n", " on_keyboard_event_closure('key_press')\n", " );\n", " canvas_div.addEventListener(\n", " 'keyup',\n", " on_keyboard_event_closure('key_release')\n", " );\n", "\n", " this._canvas_extra_style(canvas_div);\n", " this.root.appendChild(canvas_div);\n", "\n", " var canvas = (this.canvas = document.createElement('canvas'));\n", " canvas.classList.add('mpl-canvas');\n", " canvas.setAttribute('style', 'box-sizing: content-box;');\n", "\n", " this.context = canvas.getContext('2d');\n", "\n", " var backingStore =\n", " this.context.backingStorePixelRatio ||\n", " this.context.webkitBackingStorePixelRatio ||\n", " this.context.mozBackingStorePixelRatio ||\n", " this.context.msBackingStorePixelRatio ||\n", " this.context.oBackingStorePixelRatio ||\n", " this.context.backingStorePixelRatio ||\n", " 1;\n", "\n", " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", " 'canvas'\n", " ));\n", " rubberband_canvas.setAttribute(\n", " 'style',\n", " 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", " );\n", "\n", " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", " if (this.ResizeObserver === undefined) {\n", " if (window.ResizeObserver !== undefined) {\n", " this.ResizeObserver = window.ResizeObserver;\n", " } else {\n", " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", " this.ResizeObserver = obs.ResizeObserver;\n", " }\n", " }\n", "\n", " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", " var nentries = entries.length;\n", " for (var i = 0; i < nentries; i++) {\n", " var entry = entries[i];\n", " var width, height;\n", " if (entry.contentBoxSize) {\n", " if (entry.contentBoxSize instanceof Array) {\n", " // Chrome 84 implements new version of spec.\n", " width = entry.contentBoxSize[0].inlineSize;\n", " height = entry.contentBoxSize[0].blockSize;\n", " } else {\n", " // Firefox implements old version of spec.\n", " width = entry.contentBoxSize.inlineSize;\n", " height = entry.contentBoxSize.blockSize;\n", " }\n", " } else {\n", " // Chrome <84 implements even older version of spec.\n", " width = entry.contentRect.width;\n", " height = entry.contentRect.height;\n", " }\n", "\n", " // Keep the size of the canvas and rubber band canvas in sync with\n", " // the canvas container.\n", " if (entry.devicePixelContentBoxSize) {\n", " // Chrome 84 implements new version of spec.\n", " canvas.setAttribute(\n", " 'width',\n", " entry.devicePixelContentBoxSize[0].inlineSize\n", " );\n", " canvas.setAttribute(\n", " 'height',\n", " entry.devicePixelContentBoxSize[0].blockSize\n", " );\n", " } else {\n", " canvas.setAttribute('width', width * fig.ratio);\n", " canvas.setAttribute('height', height * fig.ratio);\n", " }\n", " canvas.setAttribute(\n", " 'style',\n", " 'width: ' + width + 'px; height: ' + height + 'px;'\n", " );\n", "\n", " rubberband_canvas.setAttribute('width', width);\n", " rubberband_canvas.setAttribute('height', height);\n", "\n", " // And update the size in Python. We ignore the initial 0/0 size\n", " // that occurs as the element is placed into the DOM, which should\n", " // otherwise not happen due to the minimum size styling.\n", " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", " fig.request_resize(width, height);\n", " }\n", " }\n", " });\n", " this.resizeObserverInstance.observe(canvas_div);\n", "\n", " function on_mouse_event_closure(name) {\n", " return function (event) {\n", " return fig.mouse_event(event, name);\n", " };\n", " }\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mousedown',\n", " on_mouse_event_closure('button_press')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseup',\n", " on_mouse_event_closure('button_release')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'dblclick',\n", " on_mouse_event_closure('dblclick')\n", " );\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband_canvas.addEventListener(\n", " 'mousemove',\n", " on_mouse_event_closure('motion_notify')\n", " );\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mouseenter',\n", " on_mouse_event_closure('figure_enter')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseleave',\n", " on_mouse_event_closure('figure_leave')\n", " );\n", "\n", " canvas_div.addEventListener('wheel', function (event) {\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " on_mouse_event_closure('scroll')(event);\n", " });\n", "\n", " canvas_div.appendChild(canvas);\n", " canvas_div.appendChild(rubberband_canvas);\n", "\n", " this.rubberband_context = rubberband_canvas.getContext('2d');\n", " this.rubberband_context.strokeStyle = '#000000';\n", "\n", " this._resize_canvas = function (width, height, forward) {\n", " if (forward) {\n", " canvas_div.style.width = width + 'px';\n", " canvas_div.style.height = height + 'px';\n", " }\n", " };\n", "\n", " // Disable right mouse context menu.\n", " this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", " event.preventDefault();\n", " return false;\n", " });\n", "\n", " function set_focus() {\n", " canvas.focus();\n", " canvas_div.focus();\n", " }\n", "\n", " window.setTimeout(set_focus, 100);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'mpl-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " continue;\n", " }\n", "\n", " var button = (fig.buttons[name] = document.createElement('button'));\n", " button.classList = 'mpl-widget';\n", " button.setAttribute('role', 'button');\n", " button.setAttribute('aria-disabled', 'false');\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", "\n", " var icon_img = document.createElement('img');\n", " icon_img.src = '_images/' + image + '.png';\n", " icon_img.srcset = '_images/' + image + '_large.png 2x';\n", " icon_img.alt = tooltip;\n", " button.appendChild(icon_img);\n", "\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " var fmt_picker = document.createElement('select');\n", " fmt_picker.classList = 'mpl-widget';\n", " toolbar.appendChild(fmt_picker);\n", " this.format_dropdown = fmt_picker;\n", "\n", " for (var ind in mpl.extensions) {\n", " var fmt = mpl.extensions[ind];\n", " var option = document.createElement('option');\n", " option.selected = fmt === mpl.default_extension;\n", " option.innerHTML = fmt;\n", " fmt_picker.appendChild(option);\n", " }\n", "\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "};\n", "\n", "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", " // which will in turn request a refresh of the image.\n", " this.send_message('resize', { width: x_pixels, height: y_pixels });\n", "};\n", "\n", "mpl.figure.prototype.send_message = function (type, properties) {\n", " properties['type'] = type;\n", " properties['figure_id'] = this.id;\n", " this.ws.send(JSON.stringify(properties));\n", "};\n", "\n", "mpl.figure.prototype.send_draw_message = function () {\n", " if (!this.waiting) {\n", " this.waiting = true;\n", " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " var format_dropdown = fig.format_dropdown;\n", " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", " fig.ondownload(fig, format);\n", "};\n", "\n", "mpl.figure.prototype.handle_resize = function (fig, msg) {\n", " var size = msg['size'];\n", " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", " fig._resize_canvas(size[0], size[1], msg['forward']);\n", " fig.send_message('refresh', {});\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", " var x0 = msg['x0'] / fig.ratio;\n", " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", " var x1 = msg['x1'] / fig.ratio;\n", " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", " x0 = Math.floor(x0) + 0.5;\n", " y0 = Math.floor(y0) + 0.5;\n", " x1 = Math.floor(x1) + 0.5;\n", " y1 = Math.floor(y1) + 0.5;\n", " var min_x = Math.min(x0, x1);\n", " var min_y = Math.min(y0, y1);\n", " var width = Math.abs(x1 - x0);\n", " var height = Math.abs(y1 - y0);\n", "\n", " fig.rubberband_context.clearRect(\n", " 0,\n", " 0,\n", " fig.canvas.width / fig.ratio,\n", " fig.canvas.height / fig.ratio\n", " );\n", "\n", " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", "};\n", "\n", "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", " // Updates the figure title.\n", " fig.header.textContent = msg['label'];\n", "};\n", "\n", "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", " var cursor = msg['cursor'];\n", " switch (cursor) {\n", " case 0:\n", " cursor = 'pointer';\n", " break;\n", " case 1:\n", " cursor = 'default';\n", " break;\n", " case 2:\n", " cursor = 'crosshair';\n", " break;\n", " case 3:\n", " cursor = 'move';\n", " break;\n", " }\n", " fig.rubberband_canvas.style.cursor = cursor;\n", "};\n", "\n", "mpl.figure.prototype.handle_message = function (fig, msg) {\n", " fig.message.textContent = msg['message'];\n", "};\n", "\n", "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", " // Request the server to send over a new figure.\n", " fig.send_draw_message();\n", "};\n", "\n", "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", " fig.image_mode = msg['mode'];\n", "};\n", "\n", "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", " for (var key in msg) {\n", " if (!(key in fig.buttons)) {\n", " continue;\n", " }\n", " fig.buttons[key].disabled = !msg[key];\n", " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", " if (msg['mode'] === 'PAN') {\n", " fig.buttons['Pan'].classList.add('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " } else if (msg['mode'] === 'ZOOM') {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.add('active');\n", " } else {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " }\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Called whenever the canvas gets updated.\n", " this.send_message('ack', {});\n", "};\n", "\n", "// A function to construct a web socket function for onmessage handling.\n", "// Called in the figure constructor.\n", "mpl.figure.prototype._make_on_message_function = function (fig) {\n", " return function socket_on_message(evt) {\n", " if (evt.data instanceof Blob) {\n", " var img = evt.data;\n", " if (img.type !== 'image/png') {\n", " /* FIXME: We get \"Resource interpreted as Image but\n", " * transferred with MIME type text/plain:\" errors on\n", " * Chrome. But how to set the MIME type? It doesn't seem\n", " * to be part of the websocket stream */\n", " img.type = 'image/png';\n", " }\n", "\n", " /* Free the memory for the previous frames */\n", " if (fig.imageObj.src) {\n", " (window.URL || window.webkitURL).revokeObjectURL(\n", " fig.imageObj.src\n", " );\n", " }\n", "\n", " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", " img\n", " );\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " } else if (\n", " typeof evt.data === 'string' &&\n", " evt.data.slice(0, 21) === 'data:image/png;base64'\n", " ) {\n", " fig.imageObj.src = evt.data;\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " }\n", "\n", " var msg = JSON.parse(evt.data);\n", " var msg_type = msg['type'];\n", "\n", " // Call the \"handle_{type}\" callback, which takes\n", " // the figure and JSON message as its only arguments.\n", " try {\n", " var callback = fig['handle_' + msg_type];\n", " } catch (e) {\n", " console.log(\n", " \"No handler for the '\" + msg_type + \"' message type: \",\n", " msg\n", " );\n", " return;\n", " }\n", "\n", " if (callback) {\n", " try {\n", " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", " callback(fig, msg);\n", " } catch (e) {\n", " console.log(\n", " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", " e,\n", " e.stack,\n", " msg\n", " );\n", " }\n", " }\n", " };\n", "};\n", "\n", "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", "mpl.findpos = function (e) {\n", " //this section is from http://www.quirksmode.org/js/events_properties.html\n", " var targ;\n", " if (!e) {\n", " e = window.event;\n", " }\n", " if (e.target) {\n", " targ = e.target;\n", " } else if (e.srcElement) {\n", " targ = e.srcElement;\n", " }\n", " if (targ.nodeType === 3) {\n", " // defeat Safari bug\n", " targ = targ.parentNode;\n", " }\n", "\n", " // pageX,Y are the mouse positions relative to the document\n", " var boundingRect = targ.getBoundingClientRect();\n", " var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", " var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", "\n", " return { x: x, y: y };\n", "};\n", "\n", "/*\n", " * return a copy of an object with only non-object keys\n", " * we need this to avoid circular references\n", " * http://stackoverflow.com/a/24161582/3208463\n", " */\n", "function simpleKeys(original) {\n", " return Object.keys(original).reduce(function (obj, key) {\n", " if (typeof original[key] !== 'object') {\n", " obj[key] = original[key];\n", " }\n", " return obj;\n", " }, {});\n", "}\n", "\n", "mpl.figure.prototype.mouse_event = function (event, name) {\n", " var canvas_pos = mpl.findpos(event);\n", "\n", " if (name === 'button_press') {\n", " this.canvas.focus();\n", " this.canvas_div.focus();\n", " }\n", "\n", " var x = canvas_pos.x * this.ratio;\n", " var y = canvas_pos.y * this.ratio;\n", "\n", " this.send_message(name, {\n", " x: x,\n", " y: y,\n", " button: event.button,\n", " step: event.step,\n", " guiEvent: simpleKeys(event),\n", " });\n", "\n", " /* This prevents the web browser from automatically changing to\n", " * the text insertion cursor when the button is pressed. We want\n", " * to control all of the cursor setting manually through the\n", " * 'cursor' event from matplotlib */\n", " event.preventDefault();\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", " // Handle any extra behaviour associated with a key event\n", "};\n", "\n", "mpl.figure.prototype.key_event = function (event, name) {\n", " // Prevent repeat events\n", " if (name === 'key_press') {\n", " if (event.key === this._key) {\n", " return;\n", " } else {\n", " this._key = event.key;\n", " }\n", " }\n", " if (name === 'key_release') {\n", " this._key = null;\n", " }\n", "\n", " var value = '';\n", " if (event.ctrlKey && event.key !== 'Control') {\n", " value += 'ctrl+';\n", " }\n", " else if (event.altKey && event.key !== 'Alt') {\n", " value += 'alt+';\n", " }\n", " else if (event.shiftKey && event.key !== 'Shift') {\n", " value += 'shift+';\n", " }\n", "\n", " value += 'k' + event.key;\n", "\n", " this._key_event_extra(event, name);\n", "\n", " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", " if (name === 'download') {\n", " this.handle_save(this, null);\n", " } else {\n", " this.send_message('toolbar_button', { name: name });\n", " }\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", "\n", "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", "// prettier-ignore\n", "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", "\n", "mpl.default_extension = \"png\";/* global mpl */\n", "\n", "var comm_websocket_adapter = function (comm) {\n", " // Create a \"websocket\"-like object which calls the given IPython comm\n", " // object with the appropriate methods. Currently this is a non binary\n", " // socket, so there is still some room for performance tuning.\n", " var ws = {};\n", "\n", " ws.binaryType = comm.kernel.ws.binaryType;\n", " ws.readyState = comm.kernel.ws.readyState;\n", " function updateReadyState(_event) {\n", " if (comm.kernel.ws) {\n", " ws.readyState = comm.kernel.ws.readyState;\n", " } else {\n", " ws.readyState = 3; // Closed state.\n", " }\n", " }\n", " comm.kernel.ws.addEventListener('open', updateReadyState);\n", " comm.kernel.ws.addEventListener('close', updateReadyState);\n", " comm.kernel.ws.addEventListener('error', updateReadyState);\n", "\n", " ws.close = function () {\n", " comm.close();\n", " };\n", " ws.send = function (m) {\n", " //console.log('sending', m);\n", " comm.send(m);\n", " };\n", " // Register the callback with on_msg.\n", " comm.on_msg(function (msg) {\n", " //console.log('receiving', msg['content']['data'], msg);\n", " var data = msg['content']['data'];\n", " if (data['blob'] !== undefined) {\n", " data = {\n", " data: new Blob(msg['buffers'], { type: data['blob'] }),\n", " };\n", " }\n", " // Pass the mpl event to the overridden (by mpl) onmessage function.\n", " ws.onmessage(data);\n", " });\n", " return ws;\n", "};\n", "\n", "mpl.mpl_figure_comm = function (comm, msg) {\n", " // This is the function which gets called when the mpl process\n", " // starts-up an IPython Comm through the \"matplotlib\" channel.\n", "\n", " var id = msg.content.data.id;\n", " // Get hold of the div created by the display call when the Comm\n", " // socket was opened in Python.\n", " var element = document.getElementById(id);\n", " var ws_proxy = comm_websocket_adapter(comm);\n", "\n", " function ondownload(figure, _format) {\n", " window.open(figure.canvas.toDataURL());\n", " }\n", "\n", " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", "\n", " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", " // web socket which is closed, not our websocket->open comm proxy.\n", " ws_proxy.onopen();\n", "\n", " fig.parent_element = element;\n", " fig.cell_info = mpl.find_output_cell(\"
\");\n", " if (!fig.cell_info) {\n", " console.error('Failed to find cell for figure', id, fig);\n", " return;\n", " }\n", " fig.cell_info[0].output_area.element.on(\n", " 'cleared',\n", " { fig: fig },\n", " fig._remove_fig_handler\n", " );\n", "};\n", "\n", "mpl.figure.prototype.handle_close = function (fig, msg) {\n", " var width = fig.canvas.width / fig.ratio;\n", " fig.cell_info[0].output_area.element.off(\n", " 'cleared',\n", " fig._remove_fig_handler\n", " );\n", " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", "\n", " // Update the output cell to use the data from the current canvas.\n", " fig.push_to_output();\n", " var dataURL = fig.canvas.toDataURL();\n", " // Re-enable the keyboard manager in IPython - without this line, in FF,\n", " // the notebook keyboard shortcuts fail.\n", " IPython.keyboard_manager.enable();\n", " fig.parent_element.innerHTML =\n", " '';\n", " fig.close_ws(fig, msg);\n", "};\n", "\n", "mpl.figure.prototype.close_ws = function (fig, msg) {\n", " fig.send_message('closing', msg);\n", " // fig.ws.close()\n", "};\n", "\n", "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", " // Turn the data on the canvas into data in the output cell.\n", " var width = this.canvas.width / this.ratio;\n", " var dataURL = this.canvas.toDataURL();\n", " this.cell_info[1]['text/html'] =\n", " '';\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Tell IPython that the notebook contents must change.\n", " IPython.notebook.set_dirty(true);\n", " this.send_message('ack', {});\n", " var fig = this;\n", " // Wait a second, then push the new image to the DOM so\n", " // that it is saved nicely (might be nice to debounce this).\n", " setTimeout(function () {\n", " fig.push_to_output();\n", " }, 1000);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'btn-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " var button;\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " continue;\n", " }\n", "\n", " button = fig.buttons[name] = document.createElement('button');\n", " button.classList = 'btn btn-default';\n", " button.href = '#';\n", " button.title = name;\n", " button.innerHTML = '';\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " // Add the status bar.\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message pull-right';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "\n", " // Add the close button to the window.\n", " var buttongrp = document.createElement('div');\n", " buttongrp.classList = 'btn-group inline pull-right';\n", " button = document.createElement('button');\n", " button.classList = 'btn btn-mini btn-primary';\n", " button.href = '#';\n", " button.title = 'Stop Interaction';\n", " button.innerHTML = '';\n", " button.addEventListener('click', function (_evt) {\n", " fig.handle_close(fig, {});\n", " });\n", " button.addEventListener(\n", " 'mouseover',\n", " on_mouseover_closure('Stop Interaction')\n", " );\n", " buttongrp.appendChild(button);\n", " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", "};\n", "\n", "mpl.figure.prototype._remove_fig_handler = function (event) {\n", " var fig = event.data.fig;\n", " if (event.target !== this) {\n", " // Ignore bubbled events from children.\n", " return;\n", " }\n", " fig.close_ws(fig, {});\n", "};\n", "\n", "mpl.figure.prototype._root_extra_style = function (el) {\n", " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (el) {\n", " // this is important to make the div 'focusable\n", " el.setAttribute('tabindex', 0);\n", " // reach out to IPython and tell the keyboard manager to turn it's self\n", " // off when our div gets focus\n", "\n", " // location in version 3\n", " if (IPython.notebook.keyboard_manager) {\n", " IPython.notebook.keyboard_manager.register_events(el);\n", " } else {\n", " // location in version 2\n", " IPython.keyboard_manager.register_events(el);\n", " }\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (event, _name) {\n", " var manager = IPython.notebook.keyboard_manager;\n", " if (!manager) {\n", " manager = IPython.keyboard_manager;\n", " }\n", "\n", " // Check for shift+enter\n", " if (event.shiftKey && event.which === 13) {\n", " this.canvas_div.blur();\n", " // select the cell after this one\n", " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", " IPython.notebook.select(index + 1);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " fig.ondownload(fig, null);\n", "};\n", "\n", "mpl.find_output_cell = function (html_output) {\n", " // Return the cell and output element which can be found *uniquely* in the notebook.\n", " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", " // IPython event is triggered only after the cells have been serialised, which for\n", " // our purposes (turning an active figure into a static one), is too late.\n", " var cells = IPython.notebook.get_cells();\n", " var ncells = cells.length;\n", " for (var i = 0; i < ncells; i++) {\n", " var cell = cells[i];\n", " if (cell.cell_type === 'code') {\n", " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", " var data = cell.output_area.outputs[j];\n", " if (data.data) {\n", " // IPython >= 3 moved mimebundle to data attribute of output\n", " data = data.data;\n", " }\n", " if (data['text/html'] === html_output) {\n", " return [cell, data, j];\n", " }\n", " }\n", " }\n", " }\n", "};\n", "\n", "// Register the function which deals with the matplotlib target/channel.\n", "// The kernel may be null if the page has been refreshed.\n", "if (IPython.notebook.kernel !== null) {\n", " IPython.notebook.kernel.comm_manager.register_target(\n", " 'matplotlib',\n", " mpl.mpl_figure_comm\n", " );\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "straight_line_sim(driver, level=Level.Learner, straight_length=12, plot=True);" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "The car starts stationary at the beginning of a straight 12 steps long. We can see that the driver starts off accelerating \n", "down the straight, which is good, but then things look a little strange. The AI brakes almost to a halt in the middle of the straight before accelerating again. This is definitely not the fastest way to drive! But if the AI can still improve why did it converge to a fixed set of actions rather than continuing to learn more?\n", "\n", "This is probably a good point to explain how our driver is learning at the moment. We have used what is perhaps the simplest\n", "form of reinforcement learning: Q Learning. We will cover the basics of the implementation here - there are lots of articles and demos available online if you want to dig deeper into how this works.\n", " \n", "We first need a definition of our state, that is the values that capture everything our driver needs to know to make a decision. In the case of our learner track, the only thing the driver needs to know is how much distance there is in front of the car before the next turn, and how fast the car is currently travelling. With this information, the driver can decide whether to accelerate (because there is still lots of distance to travel before the corner) or brake because the corner is close and the car is travelling too fast to make the turn.\n", "\n", "

state = [speed, distance_ahead]

\n", " \n", "One issue to deal with is that car speed is a continuous value, but Q Learning relies on us revisiting previous states. To get around this we quantise speed, by default rounding it up to the nearest 10 (1 - 10 -> 10, 11 - 20 -> 20, etc). This gives up some fine control - the driver can't tell the difference between 151 and 159 speed for example. However, it allows the driver to learn with a reasonable number of states.\n", " \n", "When the driver is asked to decide on the next action to take, it first forms the state vector and then looks for the entry for that state in its Q table. The Q table stores the value that the AI has learnt for taking a particular action in a particular state. The AI will choose to take the action which has the highest Q value for that state. If there are multiple actions all with the same Q value (which is the case at the start when all actions are initialised to have a Q value of 0), the AI will choose one of the actions randomly. \n", "\n", "The Q value is made up of two parts: any immediate reward the AI gets for taking that action in the current state (short term gain), plus the value of being in new state that it ends up in (long term gain). These two parts are traded-off against each other via the `discount_factor`. If the discount factor is high, that is close to 1, then the AI will pay a lot of attention to future rewards; if the discount factor is low, then the AI will focus more on getting short term reward. A lower discount factor can lead to faster and more stable training (especially early on when future rewards are very uncertain) but can prevent the AI from planning ahead and finding the optimal strategy. Finding the right value for this parameter can be important for creating the best AI driver.\n", " \n", "After making a move the driver is given some feedback as to what happened - the new speed that the car reached and whether\n", "the car spun, crashed, or finished the race. We need to take this information and turn it into a reward for our AI, to \n", "encourage it to learn to be the best driver it can be. One option is to give the AI a big reward when it completes the race,\n", "and a penalty for every step it takes along the way. This would encourage it to finish as quickly as possible. This suffers\n", "from the *credit assignment problem* however - only receiving a reward at the end of the race will make it very slow for the \n", "AI to figure out which set of moves along the way were good and which were bad. To help with this we instead use the car\n", "speed as the reward, assuming that the faster the driver can go, the quicker it will reach the end. We also penalise\n", "crashing, as the fastest way to the end definitely doesn't involve having a crash (despite what some racing games suggest). \n", "\n", "We can see inside the AI's 'brain' by plotting the Q table like this,\n", " " ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" }, "scrolled": false }, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "/* global mpl */\n", "window.mpl = {};\n", "\n", "mpl.get_websocket_type = function () {\n", " if (typeof WebSocket !== 'undefined') {\n", " return WebSocket;\n", " } else if (typeof MozWebSocket !== 'undefined') {\n", " return MozWebSocket;\n", " } else {\n", " alert(\n", " 'Your browser does not have WebSocket support. ' +\n", " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", " 'Firefox 4 and 5 are also supported but you ' +\n", " 'have to enable WebSockets in about:config.'\n", " );\n", " }\n", "};\n", "\n", "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", " this.id = figure_id;\n", "\n", " this.ws = websocket;\n", "\n", " this.supports_binary = this.ws.binaryType !== undefined;\n", "\n", " if (!this.supports_binary) {\n", " var warnings = document.getElementById('mpl-warnings');\n", " if (warnings) {\n", " warnings.style.display = 'block';\n", " warnings.textContent =\n", " 'This browser does not support binary websocket messages. ' +\n", " 'Performance may be slow.';\n", " }\n", " }\n", "\n", " this.imageObj = new Image();\n", "\n", " this.context = undefined;\n", " this.message = undefined;\n", " this.canvas = undefined;\n", " this.rubberband_canvas = undefined;\n", " this.rubberband_context = undefined;\n", " this.format_dropdown = undefined;\n", "\n", " this.image_mode = 'full';\n", "\n", " this.root = document.createElement('div');\n", " this.root.setAttribute('style', 'display: inline-block');\n", " this._root_extra_style(this.root);\n", "\n", " parent_element.appendChild(this.root);\n", "\n", " this._init_header(this);\n", " this._init_canvas(this);\n", " this._init_toolbar(this);\n", "\n", " var fig = this;\n", "\n", " this.waiting = false;\n", "\n", " this.ws.onopen = function () {\n", " fig.send_message('supports_binary', { value: fig.supports_binary });\n", " fig.send_message('send_image_mode', {});\n", " if (fig.ratio !== 1) {\n", " fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", " }\n", " fig.send_message('refresh', {});\n", " };\n", "\n", " this.imageObj.onload = function () {\n", " if (fig.image_mode === 'full') {\n", " // Full images could contain transparency (where diff images\n", " // almost always do), so we need to clear the canvas so that\n", " // there is no ghosting.\n", " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", " }\n", " fig.context.drawImage(fig.imageObj, 0, 0);\n", " };\n", "\n", " this.imageObj.onunload = function () {\n", " fig.ws.close();\n", " };\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", "\n", " this.ondownload = ondownload;\n", "};\n", "\n", "mpl.figure.prototype._init_header = function () {\n", " var titlebar = document.createElement('div');\n", " titlebar.classList =\n", " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", " var titletext = document.createElement('div');\n", " titletext.classList = 'ui-dialog-title';\n", " titletext.setAttribute(\n", " 'style',\n", " 'width: 100%; text-align: center; padding: 3px;'\n", " );\n", " titlebar.appendChild(titletext);\n", " this.root.appendChild(titlebar);\n", " this.header = titletext;\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._init_canvas = function () {\n", " var fig = this;\n", "\n", " var canvas_div = (this.canvas_div = document.createElement('div'));\n", " canvas_div.setAttribute(\n", " 'style',\n", " 'border: 1px solid #ddd;' +\n", " 'box-sizing: content-box;' +\n", " 'clear: both;' +\n", " 'min-height: 1px;' +\n", " 'min-width: 1px;' +\n", " 'outline: 0;' +\n", " 'overflow: hidden;' +\n", " 'position: relative;' +\n", " 'resize: both;'\n", " );\n", "\n", " function on_keyboard_event_closure(name) {\n", " return function (event) {\n", " return fig.key_event(event, name);\n", " };\n", " }\n", "\n", " canvas_div.addEventListener(\n", " 'keydown',\n", " on_keyboard_event_closure('key_press')\n", " );\n", " canvas_div.addEventListener(\n", " 'keyup',\n", " on_keyboard_event_closure('key_release')\n", " );\n", "\n", " this._canvas_extra_style(canvas_div);\n", " this.root.appendChild(canvas_div);\n", "\n", " var canvas = (this.canvas = document.createElement('canvas'));\n", " canvas.classList.add('mpl-canvas');\n", " canvas.setAttribute('style', 'box-sizing: content-box;');\n", "\n", " this.context = canvas.getContext('2d');\n", "\n", " var backingStore =\n", " this.context.backingStorePixelRatio ||\n", " this.context.webkitBackingStorePixelRatio ||\n", " this.context.mozBackingStorePixelRatio ||\n", " this.context.msBackingStorePixelRatio ||\n", " this.context.oBackingStorePixelRatio ||\n", " this.context.backingStorePixelRatio ||\n", " 1;\n", "\n", " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", " 'canvas'\n", " ));\n", " rubberband_canvas.setAttribute(\n", " 'style',\n", " 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", " );\n", "\n", " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", " if (this.ResizeObserver === undefined) {\n", " if (window.ResizeObserver !== undefined) {\n", " this.ResizeObserver = window.ResizeObserver;\n", " } else {\n", " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", " this.ResizeObserver = obs.ResizeObserver;\n", " }\n", " }\n", "\n", " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", " var nentries = entries.length;\n", " for (var i = 0; i < nentries; i++) {\n", " var entry = entries[i];\n", " var width, height;\n", " if (entry.contentBoxSize) {\n", " if (entry.contentBoxSize instanceof Array) {\n", " // Chrome 84 implements new version of spec.\n", " width = entry.contentBoxSize[0].inlineSize;\n", " height = entry.contentBoxSize[0].blockSize;\n", " } else {\n", " // Firefox implements old version of spec.\n", " width = entry.contentBoxSize.inlineSize;\n", " height = entry.contentBoxSize.blockSize;\n", " }\n", " } else {\n", " // Chrome <84 implements even older version of spec.\n", " width = entry.contentRect.width;\n", " height = entry.contentRect.height;\n", " }\n", "\n", " // Keep the size of the canvas and rubber band canvas in sync with\n", " // the canvas container.\n", " if (entry.devicePixelContentBoxSize) {\n", " // Chrome 84 implements new version of spec.\n", " canvas.setAttribute(\n", " 'width',\n", " entry.devicePixelContentBoxSize[0].inlineSize\n", " );\n", " canvas.setAttribute(\n", " 'height',\n", " entry.devicePixelContentBoxSize[0].blockSize\n", " );\n", " } else {\n", " canvas.setAttribute('width', width * fig.ratio);\n", " canvas.setAttribute('height', height * fig.ratio);\n", " }\n", " canvas.setAttribute(\n", " 'style',\n", " 'width: ' + width + 'px; height: ' + height + 'px;'\n", " );\n", "\n", " rubberband_canvas.setAttribute('width', width);\n", " rubberband_canvas.setAttribute('height', height);\n", "\n", " // And update the size in Python. We ignore the initial 0/0 size\n", " // that occurs as the element is placed into the DOM, which should\n", " // otherwise not happen due to the minimum size styling.\n", " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", " fig.request_resize(width, height);\n", " }\n", " }\n", " });\n", " this.resizeObserverInstance.observe(canvas_div);\n", "\n", " function on_mouse_event_closure(name) {\n", " return function (event) {\n", " return fig.mouse_event(event, name);\n", " };\n", " }\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mousedown',\n", " on_mouse_event_closure('button_press')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseup',\n", " on_mouse_event_closure('button_release')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'dblclick',\n", " on_mouse_event_closure('dblclick')\n", " );\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband_canvas.addEventListener(\n", " 'mousemove',\n", " on_mouse_event_closure('motion_notify')\n", " );\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mouseenter',\n", " on_mouse_event_closure('figure_enter')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseleave',\n", " on_mouse_event_closure('figure_leave')\n", " );\n", "\n", " canvas_div.addEventListener('wheel', function (event) {\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " on_mouse_event_closure('scroll')(event);\n", " });\n", "\n", " canvas_div.appendChild(canvas);\n", " canvas_div.appendChild(rubberband_canvas);\n", "\n", " this.rubberband_context = rubberband_canvas.getContext('2d');\n", " this.rubberband_context.strokeStyle = '#000000';\n", "\n", " this._resize_canvas = function (width, height, forward) {\n", " if (forward) {\n", " canvas_div.style.width = width + 'px';\n", " canvas_div.style.height = height + 'px';\n", " }\n", " };\n", "\n", " // Disable right mouse context menu.\n", " this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", " event.preventDefault();\n", " return false;\n", " });\n", "\n", " function set_focus() {\n", " canvas.focus();\n", " canvas_div.focus();\n", " }\n", "\n", " window.setTimeout(set_focus, 100);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'mpl-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " continue;\n", " }\n", "\n", " var button = (fig.buttons[name] = document.createElement('button'));\n", " button.classList = 'mpl-widget';\n", " button.setAttribute('role', 'button');\n", " button.setAttribute('aria-disabled', 'false');\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", "\n", " var icon_img = document.createElement('img');\n", " icon_img.src = '_images/' + image + '.png';\n", " icon_img.srcset = '_images/' + image + '_large.png 2x';\n", " icon_img.alt = tooltip;\n", " button.appendChild(icon_img);\n", "\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " var fmt_picker = document.createElement('select');\n", " fmt_picker.classList = 'mpl-widget';\n", " toolbar.appendChild(fmt_picker);\n", " this.format_dropdown = fmt_picker;\n", "\n", " for (var ind in mpl.extensions) {\n", " var fmt = mpl.extensions[ind];\n", " var option = document.createElement('option');\n", " option.selected = fmt === mpl.default_extension;\n", " option.innerHTML = fmt;\n", " fmt_picker.appendChild(option);\n", " }\n", "\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "};\n", "\n", "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", " // which will in turn request a refresh of the image.\n", " this.send_message('resize', { width: x_pixels, height: y_pixels });\n", "};\n", "\n", "mpl.figure.prototype.send_message = function (type, properties) {\n", " properties['type'] = type;\n", " properties['figure_id'] = this.id;\n", " this.ws.send(JSON.stringify(properties));\n", "};\n", "\n", "mpl.figure.prototype.send_draw_message = function () {\n", " if (!this.waiting) {\n", " this.waiting = true;\n", " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " var format_dropdown = fig.format_dropdown;\n", " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", " fig.ondownload(fig, format);\n", "};\n", "\n", "mpl.figure.prototype.handle_resize = function (fig, msg) {\n", " var size = msg['size'];\n", " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", " fig._resize_canvas(size[0], size[1], msg['forward']);\n", " fig.send_message('refresh', {});\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", " var x0 = msg['x0'] / fig.ratio;\n", " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", " var x1 = msg['x1'] / fig.ratio;\n", " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", " x0 = Math.floor(x0) + 0.5;\n", " y0 = Math.floor(y0) + 0.5;\n", " x1 = Math.floor(x1) + 0.5;\n", " y1 = Math.floor(y1) + 0.5;\n", " var min_x = Math.min(x0, x1);\n", " var min_y = Math.min(y0, y1);\n", " var width = Math.abs(x1 - x0);\n", " var height = Math.abs(y1 - y0);\n", "\n", " fig.rubberband_context.clearRect(\n", " 0,\n", " 0,\n", " fig.canvas.width / fig.ratio,\n", " fig.canvas.height / fig.ratio\n", " );\n", "\n", " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", "};\n", "\n", "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", " // Updates the figure title.\n", " fig.header.textContent = msg['label'];\n", "};\n", "\n", "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", " var cursor = msg['cursor'];\n", " switch (cursor) {\n", " case 0:\n", " cursor = 'pointer';\n", " break;\n", " case 1:\n", " cursor = 'default';\n", " break;\n", " case 2:\n", " cursor = 'crosshair';\n", " break;\n", " case 3:\n", " cursor = 'move';\n", " break;\n", " }\n", " fig.rubberband_canvas.style.cursor = cursor;\n", "};\n", "\n", "mpl.figure.prototype.handle_message = function (fig, msg) {\n", " fig.message.textContent = msg['message'];\n", "};\n", "\n", "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", " // Request the server to send over a new figure.\n", " fig.send_draw_message();\n", "};\n", "\n", "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", " fig.image_mode = msg['mode'];\n", "};\n", "\n", "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", " for (var key in msg) {\n", " if (!(key in fig.buttons)) {\n", " continue;\n", " }\n", " fig.buttons[key].disabled = !msg[key];\n", " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", " if (msg['mode'] === 'PAN') {\n", " fig.buttons['Pan'].classList.add('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " } else if (msg['mode'] === 'ZOOM') {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.add('active');\n", " } else {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " }\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Called whenever the canvas gets updated.\n", " this.send_message('ack', {});\n", "};\n", "\n", "// A function to construct a web socket function for onmessage handling.\n", "// Called in the figure constructor.\n", "mpl.figure.prototype._make_on_message_function = function (fig) {\n", " return function socket_on_message(evt) {\n", " if (evt.data instanceof Blob) {\n", " var img = evt.data;\n", " if (img.type !== 'image/png') {\n", " /* FIXME: We get \"Resource interpreted as Image but\n", " * transferred with MIME type text/plain:\" errors on\n", " * Chrome. But how to set the MIME type? It doesn't seem\n", " * to be part of the websocket stream */\n", " img.type = 'image/png';\n", " }\n", "\n", " /* Free the memory for the previous frames */\n", " if (fig.imageObj.src) {\n", " (window.URL || window.webkitURL).revokeObjectURL(\n", " fig.imageObj.src\n", " );\n", " }\n", "\n", " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", " img\n", " );\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " } else if (\n", " typeof evt.data === 'string' &&\n", " evt.data.slice(0, 21) === 'data:image/png;base64'\n", " ) {\n", " fig.imageObj.src = evt.data;\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " }\n", "\n", " var msg = JSON.parse(evt.data);\n", " var msg_type = msg['type'];\n", "\n", " // Call the \"handle_{type}\" callback, which takes\n", " // the figure and JSON message as its only arguments.\n", " try {\n", " var callback = fig['handle_' + msg_type];\n", " } catch (e) {\n", " console.log(\n", " \"No handler for the '\" + msg_type + \"' message type: \",\n", " msg\n", " );\n", " return;\n", " }\n", "\n", " if (callback) {\n", " try {\n", " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", " callback(fig, msg);\n", " } catch (e) {\n", " console.log(\n", " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", " e,\n", " e.stack,\n", " msg\n", " );\n", " }\n", " }\n", " };\n", "};\n", "\n", "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", "mpl.findpos = function (e) {\n", " //this section is from http://www.quirksmode.org/js/events_properties.html\n", " var targ;\n", " if (!e) {\n", " e = window.event;\n", " }\n", " if (e.target) {\n", " targ = e.target;\n", " } else if (e.srcElement) {\n", " targ = e.srcElement;\n", " }\n", " if (targ.nodeType === 3) {\n", " // defeat Safari bug\n", " targ = targ.parentNode;\n", " }\n", "\n", " // pageX,Y are the mouse positions relative to the document\n", " var boundingRect = targ.getBoundingClientRect();\n", " var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", " var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", "\n", " return { x: x, y: y };\n", "};\n", "\n", "/*\n", " * return a copy of an object with only non-object keys\n", " * we need this to avoid circular references\n", " * http://stackoverflow.com/a/24161582/3208463\n", " */\n", "function simpleKeys(original) {\n", " return Object.keys(original).reduce(function (obj, key) {\n", " if (typeof original[key] !== 'object') {\n", " obj[key] = original[key];\n", " }\n", " return obj;\n", " }, {});\n", "}\n", "\n", "mpl.figure.prototype.mouse_event = function (event, name) {\n", " var canvas_pos = mpl.findpos(event);\n", "\n", " if (name === 'button_press') {\n", " this.canvas.focus();\n", " this.canvas_div.focus();\n", " }\n", "\n", " var x = canvas_pos.x * this.ratio;\n", " var y = canvas_pos.y * this.ratio;\n", "\n", " this.send_message(name, {\n", " x: x,\n", " y: y,\n", " button: event.button,\n", " step: event.step,\n", " guiEvent: simpleKeys(event),\n", " });\n", "\n", " /* This prevents the web browser from automatically changing to\n", " * the text insertion cursor when the button is pressed. We want\n", " * to control all of the cursor setting manually through the\n", " * 'cursor' event from matplotlib */\n", " event.preventDefault();\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", " // Handle any extra behaviour associated with a key event\n", "};\n", "\n", "mpl.figure.prototype.key_event = function (event, name) {\n", " // Prevent repeat events\n", " if (name === 'key_press') {\n", " if (event.key === this._key) {\n", " return;\n", " } else {\n", " this._key = event.key;\n", " }\n", " }\n", " if (name === 'key_release') {\n", " this._key = null;\n", " }\n", "\n", " var value = '';\n", " if (event.ctrlKey && event.key !== 'Control') {\n", " value += 'ctrl+';\n", " }\n", " else if (event.altKey && event.key !== 'Alt') {\n", " value += 'alt+';\n", " }\n", " else if (event.shiftKey && event.key !== 'Shift') {\n", " value += 'shift+';\n", " }\n", "\n", " value += 'k' + event.key;\n", "\n", " this._key_event_extra(event, name);\n", "\n", " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", " if (name === 'download') {\n", " this.handle_save(this, null);\n", " } else {\n", " this.send_message('toolbar_button', { name: name });\n", " }\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", "\n", "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", "// prettier-ignore\n", "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", "\n", "mpl.default_extension = \"png\";/* global mpl */\n", "\n", "var comm_websocket_adapter = function (comm) {\n", " // Create a \"websocket\"-like object which calls the given IPython comm\n", " // object with the appropriate methods. Currently this is a non binary\n", " // socket, so there is still some room for performance tuning.\n", " var ws = {};\n", "\n", " ws.binaryType = comm.kernel.ws.binaryType;\n", " ws.readyState = comm.kernel.ws.readyState;\n", " function updateReadyState(_event) {\n", " if (comm.kernel.ws) {\n", " ws.readyState = comm.kernel.ws.readyState;\n", " } else {\n", " ws.readyState = 3; // Closed state.\n", " }\n", " }\n", " comm.kernel.ws.addEventListener('open', updateReadyState);\n", " comm.kernel.ws.addEventListener('close', updateReadyState);\n", " comm.kernel.ws.addEventListener('error', updateReadyState);\n", "\n", " ws.close = function () {\n", " comm.close();\n", " };\n", " ws.send = function (m) {\n", " //console.log('sending', m);\n", " comm.send(m);\n", " };\n", " // Register the callback with on_msg.\n", " comm.on_msg(function (msg) {\n", " //console.log('receiving', msg['content']['data'], msg);\n", " var data = msg['content']['data'];\n", " if (data['blob'] !== undefined) {\n", " data = {\n", " data: new Blob(msg['buffers'], { type: data['blob'] }),\n", " };\n", " }\n", " // Pass the mpl event to the overridden (by mpl) onmessage function.\n", " ws.onmessage(data);\n", " });\n", " return ws;\n", "};\n", "\n", "mpl.mpl_figure_comm = function (comm, msg) {\n", " // This is the function which gets called when the mpl process\n", " // starts-up an IPython Comm through the \"matplotlib\" channel.\n", "\n", " var id = msg.content.data.id;\n", " // Get hold of the div created by the display call when the Comm\n", " // socket was opened in Python.\n", " var element = document.getElementById(id);\n", " var ws_proxy = comm_websocket_adapter(comm);\n", "\n", " function ondownload(figure, _format) {\n", " window.open(figure.canvas.toDataURL());\n", " }\n", "\n", " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", "\n", " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", " // web socket which is closed, not our websocket->open comm proxy.\n", " ws_proxy.onopen();\n", "\n", " fig.parent_element = element;\n", " fig.cell_info = mpl.find_output_cell(\"
\");\n", " if (!fig.cell_info) {\n", " console.error('Failed to find cell for figure', id, fig);\n", " return;\n", " }\n", " fig.cell_info[0].output_area.element.on(\n", " 'cleared',\n", " { fig: fig },\n", " fig._remove_fig_handler\n", " );\n", "};\n", "\n", "mpl.figure.prototype.handle_close = function (fig, msg) {\n", " var width = fig.canvas.width / fig.ratio;\n", " fig.cell_info[0].output_area.element.off(\n", " 'cleared',\n", " fig._remove_fig_handler\n", " );\n", " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", "\n", " // Update the output cell to use the data from the current canvas.\n", " fig.push_to_output();\n", " var dataURL = fig.canvas.toDataURL();\n", " // Re-enable the keyboard manager in IPython - without this line, in FF,\n", " // the notebook keyboard shortcuts fail.\n", " IPython.keyboard_manager.enable();\n", " fig.parent_element.innerHTML =\n", " '';\n", " fig.close_ws(fig, msg);\n", "};\n", "\n", "mpl.figure.prototype.close_ws = function (fig, msg) {\n", " fig.send_message('closing', msg);\n", " // fig.ws.close()\n", "};\n", "\n", "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", " // Turn the data on the canvas into data in the output cell.\n", " var width = this.canvas.width / this.ratio;\n", " var dataURL = this.canvas.toDataURL();\n", " this.cell_info[1]['text/html'] =\n", " '';\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Tell IPython that the notebook contents must change.\n", " IPython.notebook.set_dirty(true);\n", " this.send_message('ack', {});\n", " var fig = this;\n", " // Wait a second, then push the new image to the DOM so\n", " // that it is saved nicely (might be nice to debounce this).\n", " setTimeout(function () {\n", " fig.push_to_output();\n", " }, 1000);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'btn-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " var button;\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " continue;\n", " }\n", "\n", " button = fig.buttons[name] = document.createElement('button');\n", " button.classList = 'btn btn-default';\n", " button.href = '#';\n", " button.title = name;\n", " button.innerHTML = '';\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " // Add the status bar.\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message pull-right';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "\n", " // Add the close button to the window.\n", " var buttongrp = document.createElement('div');\n", " buttongrp.classList = 'btn-group inline pull-right';\n", " button = document.createElement('button');\n", " button.classList = 'btn btn-mini btn-primary';\n", " button.href = '#';\n", " button.title = 'Stop Interaction';\n", " button.innerHTML = '';\n", " button.addEventListener('click', function (_evt) {\n", " fig.handle_close(fig, {});\n", " });\n", " button.addEventListener(\n", " 'mouseover',\n", " on_mouseover_closure('Stop Interaction')\n", " );\n", " buttongrp.appendChild(button);\n", " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", "};\n", "\n", "mpl.figure.prototype._remove_fig_handler = function (event) {\n", " var fig = event.data.fig;\n", " if (event.target !== this) {\n", " // Ignore bubbled events from children.\n", " return;\n", " }\n", " fig.close_ws(fig, {});\n", "};\n", "\n", "mpl.figure.prototype._root_extra_style = function (el) {\n", " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (el) {\n", " // this is important to make the div 'focusable\n", " el.setAttribute('tabindex', 0);\n", " // reach out to IPython and tell the keyboard manager to turn it's self\n", " // off when our div gets focus\n", "\n", " // location in version 3\n", " if (IPython.notebook.keyboard_manager) {\n", " IPython.notebook.keyboard_manager.register_events(el);\n", " } else {\n", " // location in version 2\n", " IPython.keyboard_manager.register_events(el);\n", " }\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (event, _name) {\n", " var manager = IPython.notebook.keyboard_manager;\n", " if (!manager) {\n", " manager = IPython.keyboard_manager;\n", " }\n", "\n", " // Check for shift+enter\n", " if (event.shiftKey && event.which === 13) {\n", " this.canvas_div.blur();\n", " // select the cell after this one\n", " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", " IPython.notebook.select(index + 1);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " fig.ondownload(fig, null);\n", "};\n", "\n", "mpl.find_output_cell = function (html_output) {\n", " // Return the cell and output element which can be found *uniquely* in the notebook.\n", " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", " // IPython event is triggered only after the cells have been serialised, which for\n", " // our purposes (turning an active figure into a static one), is too late.\n", " var cells = IPython.notebook.get_cells();\n", " var ncells = cells.length;\n", " for (var i = 0; i < ncells; i++) {\n", " var cell = cells[i];\n", " if (cell.cell_type === 'code') {\n", " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", " var data = cell.output_area.outputs[j];\n", " if (data.data) {\n", " // IPython >= 3 moved mimebundle to data attribute of output\n", " data = data.data;\n", " }\n", " if (data['text/html'] === html_output) {\n", " return [cell, data, j];\n", " }\n", " }\n", " }\n", " }\n", "};\n", "\n", "// Register the function which deals with the matplotlib target/channel.\n", "// The kernel may be null if the page has been refreshed.\n", "if (IPython.notebook.kernel !== null) {\n", " IPython.notebook.kernel.comm_manager.register_target(\n", " 'matplotlib',\n", " mpl.mpl_figure_comm\n", " );\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "plot_q_table(driver)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This shows the maximum Q value observed in each state, coloured by the action that has this value. Our state is just made\n", "up of two values, which are plotted over the rows and columns. The empty cells are those states that the driver hasn't yet\n", "explored. If the AI had fully explored the space and learnt everything, we would expect to see acceleration actions scoring\n", "highest right up to the last couple of steps before the end of the straight, when the driver would have to brake if the car was \n", "travelling above the safe cornering speed. We can see that the AI still has much to learn, however it is hard to fully intepret\n", "the Q table as the driver doesn't need to explore the full state space, it just needs to find the optimal route through it\n", "that takes it to the end of the race. It would waste a lot of time exploring every single possibility. This brings us to the exploration - exploitation trade-off, and the reason our AI stopped learning before it reached the best it could be.\n", "\n", "Suppose our AI reaches a state for the first time, all the actions in the Q table for that state will be at their default Q values of 0. Because there is no one winning action, the driver will choose randomly from all possible actions. Now suppose that the chosen action leads to an updated Q value that is greater than 0. The next time the AI comes back to the same state and checks the Q table it will find one action with a Q value greater than 0 and all the other actions still at their default 0 value. At the moment our AI will always choose the action with the highest Q value in each state, so it will happily pick the same action it tried previously again. In fact, as long as that action never leads to a crash (which has a negative reward), the AI will never try a different action in that state. This means that once the AI has eliminated crashes it will never explore any other actions and will just keep replaying what it has learnt. It is purely *exploiting* with no *exploration*.\n", "\n", "If we want our AI to get better, we need to get it to occaisonally try new actions. A simple way to do this is to introduce a random switch each time the driver has to choose an action. In one setting the AI proceeds as before and uses the Q table to choose its action. In the other setting the driver will instead choose an action randomly and, hopefully, find an even better \n", "option than it previously knew about. Equally, however, it might accelerate the car into the maze wall - this is the risk of exploration! \n", " \n", "Let's investigate this further by having a look at the key parameter controlling this: the `random_action_probability`. When this is set to 0, its default setting, the AI will never take a random action (pure exploitation); if it is set to 1 then it will always take a random action (pure exploration - never using what it has learnt). Let's compare how the AI performs with different settings of this parameter. To do this properly we need to run the races several times using different random numbers each time. This is because a lot of the choices the driver will make involve making a random decision and so the outcomes can vary a lot depending on how lucky our driver is! We want to create an AI that can perform well reliably - drivers that have one amazing race in a season but then crash in all the others will never win the championship!" ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" }, "scrolled": false }, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "/* global mpl */\n", "window.mpl = {};\n", "\n", "mpl.get_websocket_type = function () {\n", " if (typeof WebSocket !== 'undefined') {\n", " return WebSocket;\n", " } else if (typeof MozWebSocket !== 'undefined') {\n", " return MozWebSocket;\n", " } else {\n", " alert(\n", " 'Your browser does not have WebSocket support. ' +\n", " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", " 'Firefox 4 and 5 are also supported but you ' +\n", " 'have to enable WebSockets in about:config.'\n", " );\n", " }\n", "};\n", "\n", "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", " this.id = figure_id;\n", "\n", " this.ws = websocket;\n", "\n", " this.supports_binary = this.ws.binaryType !== undefined;\n", "\n", " if (!this.supports_binary) {\n", " var warnings = document.getElementById('mpl-warnings');\n", " if (warnings) {\n", " warnings.style.display = 'block';\n", " warnings.textContent =\n", " 'This browser does not support binary websocket messages. ' +\n", " 'Performance may be slow.';\n", " }\n", " }\n", "\n", " this.imageObj = new Image();\n", "\n", " this.context = undefined;\n", " this.message = undefined;\n", " this.canvas = undefined;\n", " this.rubberband_canvas = undefined;\n", " this.rubberband_context = undefined;\n", " this.format_dropdown = undefined;\n", "\n", " this.image_mode = 'full';\n", "\n", " this.root = document.createElement('div');\n", " this.root.setAttribute('style', 'display: inline-block');\n", " this._root_extra_style(this.root);\n", "\n", " parent_element.appendChild(this.root);\n", "\n", " this._init_header(this);\n", " this._init_canvas(this);\n", " this._init_toolbar(this);\n", "\n", " var fig = this;\n", "\n", " this.waiting = false;\n", "\n", " this.ws.onopen = function () {\n", " fig.send_message('supports_binary', { value: fig.supports_binary });\n", " fig.send_message('send_image_mode', {});\n", " if (fig.ratio !== 1) {\n", " fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", " }\n", " fig.send_message('refresh', {});\n", " };\n", "\n", " this.imageObj.onload = function () {\n", " if (fig.image_mode === 'full') {\n", " // Full images could contain transparency (where diff images\n", " // almost always do), so we need to clear the canvas so that\n", " // there is no ghosting.\n", " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", " }\n", " fig.context.drawImage(fig.imageObj, 0, 0);\n", " };\n", "\n", " this.imageObj.onunload = function () {\n", " fig.ws.close();\n", " };\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", "\n", " this.ondownload = ondownload;\n", "};\n", "\n", "mpl.figure.prototype._init_header = function () {\n", " var titlebar = document.createElement('div');\n", " titlebar.classList =\n", " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", " var titletext = document.createElement('div');\n", " titletext.classList = 'ui-dialog-title';\n", " titletext.setAttribute(\n", " 'style',\n", " 'width: 100%; text-align: center; padding: 3px;'\n", " );\n", " titlebar.appendChild(titletext);\n", " this.root.appendChild(titlebar);\n", " this.header = titletext;\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._init_canvas = function () {\n", " var fig = this;\n", "\n", " var canvas_div = (this.canvas_div = document.createElement('div'));\n", " canvas_div.setAttribute(\n", " 'style',\n", " 'border: 1px solid #ddd;' +\n", " 'box-sizing: content-box;' +\n", " 'clear: both;' +\n", " 'min-height: 1px;' +\n", " 'min-width: 1px;' +\n", " 'outline: 0;' +\n", " 'overflow: hidden;' +\n", " 'position: relative;' +\n", " 'resize: both;'\n", " );\n", "\n", " function on_keyboard_event_closure(name) {\n", " return function (event) {\n", " return fig.key_event(event, name);\n", " };\n", " }\n", "\n", " canvas_div.addEventListener(\n", " 'keydown',\n", " on_keyboard_event_closure('key_press')\n", " );\n", " canvas_div.addEventListener(\n", " 'keyup',\n", " on_keyboard_event_closure('key_release')\n", " );\n", "\n", " this._canvas_extra_style(canvas_div);\n", " this.root.appendChild(canvas_div);\n", "\n", " var canvas = (this.canvas = document.createElement('canvas'));\n", " canvas.classList.add('mpl-canvas');\n", " canvas.setAttribute('style', 'box-sizing: content-box;');\n", "\n", " this.context = canvas.getContext('2d');\n", "\n", " var backingStore =\n", " this.context.backingStorePixelRatio ||\n", " this.context.webkitBackingStorePixelRatio ||\n", " this.context.mozBackingStorePixelRatio ||\n", " this.context.msBackingStorePixelRatio ||\n", " this.context.oBackingStorePixelRatio ||\n", " this.context.backingStorePixelRatio ||\n", " 1;\n", "\n", " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", " 'canvas'\n", " ));\n", " rubberband_canvas.setAttribute(\n", " 'style',\n", " 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", " );\n", "\n", " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", " if (this.ResizeObserver === undefined) {\n", " if (window.ResizeObserver !== undefined) {\n", " this.ResizeObserver = window.ResizeObserver;\n", " } else {\n", " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", " this.ResizeObserver = obs.ResizeObserver;\n", " }\n", " }\n", "\n", " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", " var nentries = entries.length;\n", " for (var i = 0; i < nentries; i++) {\n", " var entry = entries[i];\n", " var width, height;\n", " if (entry.contentBoxSize) {\n", " if (entry.contentBoxSize instanceof Array) {\n", " // Chrome 84 implements new version of spec.\n", " width = entry.contentBoxSize[0].inlineSize;\n", " height = entry.contentBoxSize[0].blockSize;\n", " } else {\n", " // Firefox implements old version of spec.\n", " width = entry.contentBoxSize.inlineSize;\n", " height = entry.contentBoxSize.blockSize;\n", " }\n", " } else {\n", " // Chrome <84 implements even older version of spec.\n", " width = entry.contentRect.width;\n", " height = entry.contentRect.height;\n", " }\n", "\n", " // Keep the size of the canvas and rubber band canvas in sync with\n", " // the canvas container.\n", " if (entry.devicePixelContentBoxSize) {\n", " // Chrome 84 implements new version of spec.\n", " canvas.setAttribute(\n", " 'width',\n", " entry.devicePixelContentBoxSize[0].inlineSize\n", " );\n", " canvas.setAttribute(\n", " 'height',\n", " entry.devicePixelContentBoxSize[0].blockSize\n", " );\n", " } else {\n", " canvas.setAttribute('width', width * fig.ratio);\n", " canvas.setAttribute('height', height * fig.ratio);\n", " }\n", " canvas.setAttribute(\n", " 'style',\n", " 'width: ' + width + 'px; height: ' + height + 'px;'\n", " );\n", "\n", " rubberband_canvas.setAttribute('width', width);\n", " rubberband_canvas.setAttribute('height', height);\n", "\n", " // And update the size in Python. We ignore the initial 0/0 size\n", " // that occurs as the element is placed into the DOM, which should\n", " // otherwise not happen due to the minimum size styling.\n", " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", " fig.request_resize(width, height);\n", " }\n", " }\n", " });\n", " this.resizeObserverInstance.observe(canvas_div);\n", "\n", " function on_mouse_event_closure(name) {\n", " return function (event) {\n", " return fig.mouse_event(event, name);\n", " };\n", " }\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mousedown',\n", " on_mouse_event_closure('button_press')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseup',\n", " on_mouse_event_closure('button_release')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'dblclick',\n", " on_mouse_event_closure('dblclick')\n", " );\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband_canvas.addEventListener(\n", " 'mousemove',\n", " on_mouse_event_closure('motion_notify')\n", " );\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mouseenter',\n", " on_mouse_event_closure('figure_enter')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseleave',\n", " on_mouse_event_closure('figure_leave')\n", " );\n", "\n", " canvas_div.addEventListener('wheel', function (event) {\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " on_mouse_event_closure('scroll')(event);\n", " });\n", "\n", " canvas_div.appendChild(canvas);\n", " canvas_div.appendChild(rubberband_canvas);\n", "\n", " this.rubberband_context = rubberband_canvas.getContext('2d');\n", " this.rubberband_context.strokeStyle = '#000000';\n", "\n", " this._resize_canvas = function (width, height, forward) {\n", " if (forward) {\n", " canvas_div.style.width = width + 'px';\n", " canvas_div.style.height = height + 'px';\n", " }\n", " };\n", "\n", " // Disable right mouse context menu.\n", " this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", " event.preventDefault();\n", " return false;\n", " });\n", "\n", " function set_focus() {\n", " canvas.focus();\n", " canvas_div.focus();\n", " }\n", "\n", " window.setTimeout(set_focus, 100);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'mpl-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " continue;\n", " }\n", "\n", " var button = (fig.buttons[name] = document.createElement('button'));\n", " button.classList = 'mpl-widget';\n", " button.setAttribute('role', 'button');\n", " button.setAttribute('aria-disabled', 'false');\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", "\n", " var icon_img = document.createElement('img');\n", " icon_img.src = '_images/' + image + '.png';\n", " icon_img.srcset = '_images/' + image + '_large.png 2x';\n", " icon_img.alt = tooltip;\n", " button.appendChild(icon_img);\n", "\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " var fmt_picker = document.createElement('select');\n", " fmt_picker.classList = 'mpl-widget';\n", " toolbar.appendChild(fmt_picker);\n", " this.format_dropdown = fmt_picker;\n", "\n", " for (var ind in mpl.extensions) {\n", " var fmt = mpl.extensions[ind];\n", " var option = document.createElement('option');\n", " option.selected = fmt === mpl.default_extension;\n", " option.innerHTML = fmt;\n", " fmt_picker.appendChild(option);\n", " }\n", "\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "};\n", "\n", "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", " // which will in turn request a refresh of the image.\n", " this.send_message('resize', { width: x_pixels, height: y_pixels });\n", "};\n", "\n", "mpl.figure.prototype.send_message = function (type, properties) {\n", " properties['type'] = type;\n", " properties['figure_id'] = this.id;\n", " this.ws.send(JSON.stringify(properties));\n", "};\n", "\n", "mpl.figure.prototype.send_draw_message = function () {\n", " if (!this.waiting) {\n", " this.waiting = true;\n", " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " var format_dropdown = fig.format_dropdown;\n", " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", " fig.ondownload(fig, format);\n", "};\n", "\n", "mpl.figure.prototype.handle_resize = function (fig, msg) {\n", " var size = msg['size'];\n", " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", " fig._resize_canvas(size[0], size[1], msg['forward']);\n", " fig.send_message('refresh', {});\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", " var x0 = msg['x0'] / fig.ratio;\n", " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", " var x1 = msg['x1'] / fig.ratio;\n", " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", " x0 = Math.floor(x0) + 0.5;\n", " y0 = Math.floor(y0) + 0.5;\n", " x1 = Math.floor(x1) + 0.5;\n", " y1 = Math.floor(y1) + 0.5;\n", " var min_x = Math.min(x0, x1);\n", " var min_y = Math.min(y0, y1);\n", " var width = Math.abs(x1 - x0);\n", " var height = Math.abs(y1 - y0);\n", "\n", " fig.rubberband_context.clearRect(\n", " 0,\n", " 0,\n", " fig.canvas.width / fig.ratio,\n", " fig.canvas.height / fig.ratio\n", " );\n", "\n", " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", "};\n", "\n", "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", " // Updates the figure title.\n", " fig.header.textContent = msg['label'];\n", "};\n", "\n", "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", " var cursor = msg['cursor'];\n", " switch (cursor) {\n", " case 0:\n", " cursor = 'pointer';\n", " break;\n", " case 1:\n", " cursor = 'default';\n", " break;\n", " case 2:\n", " cursor = 'crosshair';\n", " break;\n", " case 3:\n", " cursor = 'move';\n", " break;\n", " }\n", " fig.rubberband_canvas.style.cursor = cursor;\n", "};\n", "\n", "mpl.figure.prototype.handle_message = function (fig, msg) {\n", " fig.message.textContent = msg['message'];\n", "};\n", "\n", "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", " // Request the server to send over a new figure.\n", " fig.send_draw_message();\n", "};\n", "\n", "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", " fig.image_mode = msg['mode'];\n", "};\n", "\n", "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", " for (var key in msg) {\n", " if (!(key in fig.buttons)) {\n", " continue;\n", " }\n", " fig.buttons[key].disabled = !msg[key];\n", " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", " if (msg['mode'] === 'PAN') {\n", " fig.buttons['Pan'].classList.add('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " } else if (msg['mode'] === 'ZOOM') {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.add('active');\n", " } else {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " }\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Called whenever the canvas gets updated.\n", " this.send_message('ack', {});\n", "};\n", "\n", "// A function to construct a web socket function for onmessage handling.\n", "// Called in the figure constructor.\n", "mpl.figure.prototype._make_on_message_function = function (fig) {\n", " return function socket_on_message(evt) {\n", " if (evt.data instanceof Blob) {\n", " var img = evt.data;\n", " if (img.type !== 'image/png') {\n", " /* FIXME: We get \"Resource interpreted as Image but\n", " * transferred with MIME type text/plain:\" errors on\n", " * Chrome. But how to set the MIME type? It doesn't seem\n", " * to be part of the websocket stream */\n", " img.type = 'image/png';\n", " }\n", "\n", " /* Free the memory for the previous frames */\n", " if (fig.imageObj.src) {\n", " (window.URL || window.webkitURL).revokeObjectURL(\n", " fig.imageObj.src\n", " );\n", " }\n", "\n", " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", " img\n", " );\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " } else if (\n", " typeof evt.data === 'string' &&\n", " evt.data.slice(0, 21) === 'data:image/png;base64'\n", " ) {\n", " fig.imageObj.src = evt.data;\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " }\n", "\n", " var msg = JSON.parse(evt.data);\n", " var msg_type = msg['type'];\n", "\n", " // Call the \"handle_{type}\" callback, which takes\n", " // the figure and JSON message as its only arguments.\n", " try {\n", " var callback = fig['handle_' + msg_type];\n", " } catch (e) {\n", " console.log(\n", " \"No handler for the '\" + msg_type + \"' message type: \",\n", " msg\n", " );\n", " return;\n", " }\n", "\n", " if (callback) {\n", " try {\n", " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", " callback(fig, msg);\n", " } catch (e) {\n", " console.log(\n", " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", " e,\n", " e.stack,\n", " msg\n", " );\n", " }\n", " }\n", " };\n", "};\n", "\n", "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", "mpl.findpos = function (e) {\n", " //this section is from http://www.quirksmode.org/js/events_properties.html\n", " var targ;\n", " if (!e) {\n", " e = window.event;\n", " }\n", " if (e.target) {\n", " targ = e.target;\n", " } else if (e.srcElement) {\n", " targ = e.srcElement;\n", " }\n", " if (targ.nodeType === 3) {\n", " // defeat Safari bug\n", " targ = targ.parentNode;\n", " }\n", "\n", " // pageX,Y are the mouse positions relative to the document\n", " var boundingRect = targ.getBoundingClientRect();\n", " var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", " var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", "\n", " return { x: x, y: y };\n", "};\n", "\n", "/*\n", " * return a copy of an object with only non-object keys\n", " * we need this to avoid circular references\n", " * http://stackoverflow.com/a/24161582/3208463\n", " */\n", "function simpleKeys(original) {\n", " return Object.keys(original).reduce(function (obj, key) {\n", " if (typeof original[key] !== 'object') {\n", " obj[key] = original[key];\n", " }\n", " return obj;\n", " }, {});\n", "}\n", "\n", "mpl.figure.prototype.mouse_event = function (event, name) {\n", " var canvas_pos = mpl.findpos(event);\n", "\n", " if (name === 'button_press') {\n", " this.canvas.focus();\n", " this.canvas_div.focus();\n", " }\n", "\n", " var x = canvas_pos.x * this.ratio;\n", " var y = canvas_pos.y * this.ratio;\n", "\n", " this.send_message(name, {\n", " x: x,\n", " y: y,\n", " button: event.button,\n", " step: event.step,\n", " guiEvent: simpleKeys(event),\n", " });\n", "\n", " /* This prevents the web browser from automatically changing to\n", " * the text insertion cursor when the button is pressed. We want\n", " * to control all of the cursor setting manually through the\n", " * 'cursor' event from matplotlib */\n", " event.preventDefault();\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", " // Handle any extra behaviour associated with a key event\n", "};\n", "\n", "mpl.figure.prototype.key_event = function (event, name) {\n", " // Prevent repeat events\n", " if (name === 'key_press') {\n", " if (event.key === this._key) {\n", " return;\n", " } else {\n", " this._key = event.key;\n", " }\n", " }\n", " if (name === 'key_release') {\n", " this._key = null;\n", " }\n", "\n", " var value = '';\n", " if (event.ctrlKey && event.key !== 'Control') {\n", " value += 'ctrl+';\n", " }\n", " else if (event.altKey && event.key !== 'Alt') {\n", " value += 'alt+';\n", " }\n", " else if (event.shiftKey && event.key !== 'Shift') {\n", " value += 'shift+';\n", " }\n", "\n", " value += 'k' + event.key;\n", "\n", " this._key_event_extra(event, name);\n", "\n", " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", " if (name === 'download') {\n", " this.handle_save(this, null);\n", " } else {\n", " this.send_message('toolbar_button', { name: name });\n", " }\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", "\n", "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", "// prettier-ignore\n", "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", "\n", "mpl.default_extension = \"png\";/* global mpl */\n", "\n", "var comm_websocket_adapter = function (comm) {\n", " // Create a \"websocket\"-like object which calls the given IPython comm\n", " // object with the appropriate methods. Currently this is a non binary\n", " // socket, so there is still some room for performance tuning.\n", " var ws = {};\n", "\n", " ws.binaryType = comm.kernel.ws.binaryType;\n", " ws.readyState = comm.kernel.ws.readyState;\n", " function updateReadyState(_event) {\n", " if (comm.kernel.ws) {\n", " ws.readyState = comm.kernel.ws.readyState;\n", " } else {\n", " ws.readyState = 3; // Closed state.\n", " }\n", " }\n", " comm.kernel.ws.addEventListener('open', updateReadyState);\n", " comm.kernel.ws.addEventListener('close', updateReadyState);\n", " comm.kernel.ws.addEventListener('error', updateReadyState);\n", "\n", " ws.close = function () {\n", " comm.close();\n", " };\n", " ws.send = function (m) {\n", " //console.log('sending', m);\n", " comm.send(m);\n", " };\n", " // Register the callback with on_msg.\n", " comm.on_msg(function (msg) {\n", " //console.log('receiving', msg['content']['data'], msg);\n", " var data = msg['content']['data'];\n", " if (data['blob'] !== undefined) {\n", " data = {\n", " data: new Blob(msg['buffers'], { type: data['blob'] }),\n", " };\n", " }\n", " // Pass the mpl event to the overridden (by mpl) onmessage function.\n", " ws.onmessage(data);\n", " });\n", " return ws;\n", "};\n", "\n", "mpl.mpl_figure_comm = function (comm, msg) {\n", " // This is the function which gets called when the mpl process\n", " // starts-up an IPython Comm through the \"matplotlib\" channel.\n", "\n", " var id = msg.content.data.id;\n", " // Get hold of the div created by the display call when the Comm\n", " // socket was opened in Python.\n", " var element = document.getElementById(id);\n", " var ws_proxy = comm_websocket_adapter(comm);\n", "\n", " function ondownload(figure, _format) {\n", " window.open(figure.canvas.toDataURL());\n", " }\n", "\n", " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", "\n", " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", " // web socket which is closed, not our websocket->open comm proxy.\n", " ws_proxy.onopen();\n", "\n", " fig.parent_element = element;\n", " fig.cell_info = mpl.find_output_cell(\"
\");\n", " if (!fig.cell_info) {\n", " console.error('Failed to find cell for figure', id, fig);\n", " return;\n", " }\n", " fig.cell_info[0].output_area.element.on(\n", " 'cleared',\n", " { fig: fig },\n", " fig._remove_fig_handler\n", " );\n", "};\n", "\n", "mpl.figure.prototype.handle_close = function (fig, msg) {\n", " var width = fig.canvas.width / fig.ratio;\n", " fig.cell_info[0].output_area.element.off(\n", " 'cleared',\n", " fig._remove_fig_handler\n", " );\n", " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", "\n", " // Update the output cell to use the data from the current canvas.\n", " fig.push_to_output();\n", " var dataURL = fig.canvas.toDataURL();\n", " // Re-enable the keyboard manager in IPython - without this line, in FF,\n", " // the notebook keyboard shortcuts fail.\n", " IPython.keyboard_manager.enable();\n", " fig.parent_element.innerHTML =\n", " '';\n", " fig.close_ws(fig, msg);\n", "};\n", "\n", "mpl.figure.prototype.close_ws = function (fig, msg) {\n", " fig.send_message('closing', msg);\n", " // fig.ws.close()\n", "};\n", "\n", "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", " // Turn the data on the canvas into data in the output cell.\n", " var width = this.canvas.width / this.ratio;\n", " var dataURL = this.canvas.toDataURL();\n", " this.cell_info[1]['text/html'] =\n", " '';\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Tell IPython that the notebook contents must change.\n", " IPython.notebook.set_dirty(true);\n", " this.send_message('ack', {});\n", " var fig = this;\n", " // Wait a second, then push the new image to the DOM so\n", " // that it is saved nicely (might be nice to debounce this).\n", " setTimeout(function () {\n", " fig.push_to_output();\n", " }, 1000);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'btn-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " var button;\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " continue;\n", " }\n", "\n", " button = fig.buttons[name] = document.createElement('button');\n", " button.classList = 'btn btn-default';\n", " button.href = '#';\n", " button.title = name;\n", " button.innerHTML = '';\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " // Add the status bar.\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message pull-right';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "\n", " // Add the close button to the window.\n", " var buttongrp = document.createElement('div');\n", " buttongrp.classList = 'btn-group inline pull-right';\n", " button = document.createElement('button');\n", " button.classList = 'btn btn-mini btn-primary';\n", " button.href = '#';\n", " button.title = 'Stop Interaction';\n", " button.innerHTML = '';\n", " button.addEventListener('click', function (_evt) {\n", " fig.handle_close(fig, {});\n", " });\n", " button.addEventListener(\n", " 'mouseover',\n", " on_mouseover_closure('Stop Interaction')\n", " );\n", " buttongrp.appendChild(button);\n", " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", "};\n", "\n", "mpl.figure.prototype._remove_fig_handler = function (event) {\n", " var fig = event.data.fig;\n", " if (event.target !== this) {\n", " // Ignore bubbled events from children.\n", " return;\n", " }\n", " fig.close_ws(fig, {});\n", "};\n", "\n", "mpl.figure.prototype._root_extra_style = function (el) {\n", " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (el) {\n", " // this is important to make the div 'focusable\n", " el.setAttribute('tabindex', 0);\n", " // reach out to IPython and tell the keyboard manager to turn it's self\n", " // off when our div gets focus\n", "\n", " // location in version 3\n", " if (IPython.notebook.keyboard_manager) {\n", " IPython.notebook.keyboard_manager.register_events(el);\n", " } else {\n", " // location in version 2\n", " IPython.keyboard_manager.register_events(el);\n", " }\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (event, _name) {\n", " var manager = IPython.notebook.keyboard_manager;\n", " if (!manager) {\n", " manager = IPython.keyboard_manager;\n", " }\n", "\n", " // Check for shift+enter\n", " if (event.shiftKey && event.which === 13) {\n", " this.canvas_div.blur();\n", " // select the cell after this one\n", " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", " IPython.notebook.select(index + 1);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " fig.ondownload(fig, null);\n", "};\n", "\n", "mpl.find_output_cell = function (html_output) {\n", " // Return the cell and output element which can be found *uniquely* in the notebook.\n", " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", " // IPython event is triggered only after the cells have been serialised, which for\n", " // our purposes (turning an active figure into a static one), is too late.\n", " var cells = IPython.notebook.get_cells();\n", " var ncells = cells.length;\n", " for (var i = 0; i < ncells; i++) {\n", " var cell = cells[i];\n", " if (cell.cell_type === 'code') {\n", " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", " var data = cell.output_area.outputs[j];\n", " if (data.data) {\n", " // IPython >= 3 moved mimebundle to data attribute of output\n", " data = data.data;\n", " }\n", " if (data['text/html'] === html_output) {\n", " return [cell, data, j];\n", " }\n", " }\n", " }\n", " }\n", "};\n", "\n", "// Register the function which deals with the matplotlib target/channel.\n", "// The kernel may be null if the page has been refreshed.\n", "if (IPython.notebook.kernel !== null) {\n", " IPython.notebook.kernel.comm_manager.register_target(\n", " 'matplotlib',\n", " mpl.mpl_figure_comm\n", " );\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "num_races = 100 # number of races over which the AI driver can learn\n", "num_repeats = 20 # number of time to repeat the races, starting from scratch each time, but different random choices\n", "probabilities = [0.0, 0.01, 0.1]\n", "race_times = np.zeros((num_races, num_repeats))\n", "fig = plt.figure(figsize=(9, 6))\n", "fig.add_axes([0.08, 0.08, 0.92, 0.92])\n", "lines = []\n", "\n", "for pi in range(len(probabilities)):\n", " np.random.seed(1) # for repeatability\n", " for i in range(num_repeats):\n", " driver = LearnerDriver(driver_name, random_action_probability=probabilities[pi]) # reset our driver\n", " for n in range(num_races):\n", " driver, race_times[n, i], _ = race(driver, track=track, plot=False, max_number_of_steps=1000)\n", " \n", " # Plot the results\n", " lines += plt.plot(range(1, num_races + 1), np.mean(race_times, axis=1))\n", " plt.fill_between(range(1, num_races + 1), np.min(race_times, axis=1), np.max(race_times, axis=1), \n", " color=lines[-1].get_color(), alpha=0.1)\n", " \n", "plt.ylabel('Race Times', fontsize=16)\n", "plt.xlabel('Race Number', fontsize=16)\n", "fig.gca().xaxis.set_major_locator(MaxNLocator(integer=True))\n", "plt.legend(lines, probabilities, fontsize=16);" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "Hm, introducing the random action probability has made things worse - the average performance (solid lines) are worse and the shaded regions (which cover the best to worst performance) are wider for the two drivers which explored. What went wrong?\n", "\n", "This is caused by the penalty for crashing, which is very steep compared to the benefit of driving faster. This means that it is much better for our AI to drive slowly and safely rather than exploring a new action and risk crashing. But this is no good! We don't want slow, safe drivers, we want fast skillful drivers! Clearly we need a better way of exploring actions. This type of setup is sometimes called a *cliff-edge loss*, as one wrong step can take you over the edge. This is exactly the situation in Formula 1 - the fastest driver is right on the edge of stability, perfectly taking the car to its limit, without ever straying over the edge. \n", "\n", "There are various ways of building an AI to deal with a cliff-edge loss. One simple thing we can try is to start out with a high probability of taking a random action and reduce it over time. This would allow the AI to explore and learn a lot at the start, when it knows very little and is likely to crash anyway, but then focus its learning around what it has learnt as its knowledge increases. There is a parameter in our Driver class that allows us to do that: `random_action_decay`. Let's try it out." ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "pycharm": { "is_executing": false, "name": "#%%\n" }, "scrolled": false }, "outputs": [ { "data": { "application/javascript": [ "/* Put everything inside the global mpl namespace */\n", "/* global mpl */\n", "window.mpl = {};\n", "\n", "mpl.get_websocket_type = function () {\n", " if (typeof WebSocket !== 'undefined') {\n", " return WebSocket;\n", " } else if (typeof MozWebSocket !== 'undefined') {\n", " return MozWebSocket;\n", " } else {\n", " alert(\n", " 'Your browser does not have WebSocket support. ' +\n", " 'Please try Chrome, Safari or Firefox ≥ 6. ' +\n", " 'Firefox 4 and 5 are also supported but you ' +\n", " 'have to enable WebSockets in about:config.'\n", " );\n", " }\n", "};\n", "\n", "mpl.figure = function (figure_id, websocket, ondownload, parent_element) {\n", " this.id = figure_id;\n", "\n", " this.ws = websocket;\n", "\n", " this.supports_binary = this.ws.binaryType !== undefined;\n", "\n", " if (!this.supports_binary) {\n", " var warnings = document.getElementById('mpl-warnings');\n", " if (warnings) {\n", " warnings.style.display = 'block';\n", " warnings.textContent =\n", " 'This browser does not support binary websocket messages. ' +\n", " 'Performance may be slow.';\n", " }\n", " }\n", "\n", " this.imageObj = new Image();\n", "\n", " this.context = undefined;\n", " this.message = undefined;\n", " this.canvas = undefined;\n", " this.rubberband_canvas = undefined;\n", " this.rubberband_context = undefined;\n", " this.format_dropdown = undefined;\n", "\n", " this.image_mode = 'full';\n", "\n", " this.root = document.createElement('div');\n", " this.root.setAttribute('style', 'display: inline-block');\n", " this._root_extra_style(this.root);\n", "\n", " parent_element.appendChild(this.root);\n", "\n", " this._init_header(this);\n", " this._init_canvas(this);\n", " this._init_toolbar(this);\n", "\n", " var fig = this;\n", "\n", " this.waiting = false;\n", "\n", " this.ws.onopen = function () {\n", " fig.send_message('supports_binary', { value: fig.supports_binary });\n", " fig.send_message('send_image_mode', {});\n", " if (fig.ratio !== 1) {\n", " fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio });\n", " }\n", " fig.send_message('refresh', {});\n", " };\n", "\n", " this.imageObj.onload = function () {\n", " if (fig.image_mode === 'full') {\n", " // Full images could contain transparency (where diff images\n", " // almost always do), so we need to clear the canvas so that\n", " // there is no ghosting.\n", " fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);\n", " }\n", " fig.context.drawImage(fig.imageObj, 0, 0);\n", " };\n", "\n", " this.imageObj.onunload = function () {\n", " fig.ws.close();\n", " };\n", "\n", " this.ws.onmessage = this._make_on_message_function(this);\n", "\n", " this.ondownload = ondownload;\n", "};\n", "\n", "mpl.figure.prototype._init_header = function () {\n", " var titlebar = document.createElement('div');\n", " titlebar.classList =\n", " 'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';\n", " var titletext = document.createElement('div');\n", " titletext.classList = 'ui-dialog-title';\n", " titletext.setAttribute(\n", " 'style',\n", " 'width: 100%; text-align: center; padding: 3px;'\n", " );\n", " titlebar.appendChild(titletext);\n", " this.root.appendChild(titlebar);\n", " this.header = titletext;\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._root_extra_style = function (_canvas_div) {};\n", "\n", "mpl.figure.prototype._init_canvas = function () {\n", " var fig = this;\n", "\n", " var canvas_div = (this.canvas_div = document.createElement('div'));\n", " canvas_div.setAttribute(\n", " 'style',\n", " 'border: 1px solid #ddd;' +\n", " 'box-sizing: content-box;' +\n", " 'clear: both;' +\n", " 'min-height: 1px;' +\n", " 'min-width: 1px;' +\n", " 'outline: 0;' +\n", " 'overflow: hidden;' +\n", " 'position: relative;' +\n", " 'resize: both;'\n", " );\n", "\n", " function on_keyboard_event_closure(name) {\n", " return function (event) {\n", " return fig.key_event(event, name);\n", " };\n", " }\n", "\n", " canvas_div.addEventListener(\n", " 'keydown',\n", " on_keyboard_event_closure('key_press')\n", " );\n", " canvas_div.addEventListener(\n", " 'keyup',\n", " on_keyboard_event_closure('key_release')\n", " );\n", "\n", " this._canvas_extra_style(canvas_div);\n", " this.root.appendChild(canvas_div);\n", "\n", " var canvas = (this.canvas = document.createElement('canvas'));\n", " canvas.classList.add('mpl-canvas');\n", " canvas.setAttribute('style', 'box-sizing: content-box;');\n", "\n", " this.context = canvas.getContext('2d');\n", "\n", " var backingStore =\n", " this.context.backingStorePixelRatio ||\n", " this.context.webkitBackingStorePixelRatio ||\n", " this.context.mozBackingStorePixelRatio ||\n", " this.context.msBackingStorePixelRatio ||\n", " this.context.oBackingStorePixelRatio ||\n", " this.context.backingStorePixelRatio ||\n", " 1;\n", "\n", " this.ratio = (window.devicePixelRatio || 1) / backingStore;\n", "\n", " var rubberband_canvas = (this.rubberband_canvas = document.createElement(\n", " 'canvas'\n", " ));\n", " rubberband_canvas.setAttribute(\n", " 'style',\n", " 'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'\n", " );\n", "\n", " // Apply a ponyfill if ResizeObserver is not implemented by browser.\n", " if (this.ResizeObserver === undefined) {\n", " if (window.ResizeObserver !== undefined) {\n", " this.ResizeObserver = window.ResizeObserver;\n", " } else {\n", " var obs = _JSXTOOLS_RESIZE_OBSERVER({});\n", " this.ResizeObserver = obs.ResizeObserver;\n", " }\n", " }\n", "\n", " this.resizeObserverInstance = new this.ResizeObserver(function (entries) {\n", " var nentries = entries.length;\n", " for (var i = 0; i < nentries; i++) {\n", " var entry = entries[i];\n", " var width, height;\n", " if (entry.contentBoxSize) {\n", " if (entry.contentBoxSize instanceof Array) {\n", " // Chrome 84 implements new version of spec.\n", " width = entry.contentBoxSize[0].inlineSize;\n", " height = entry.contentBoxSize[0].blockSize;\n", " } else {\n", " // Firefox implements old version of spec.\n", " width = entry.contentBoxSize.inlineSize;\n", " height = entry.contentBoxSize.blockSize;\n", " }\n", " } else {\n", " // Chrome <84 implements even older version of spec.\n", " width = entry.contentRect.width;\n", " height = entry.contentRect.height;\n", " }\n", "\n", " // Keep the size of the canvas and rubber band canvas in sync with\n", " // the canvas container.\n", " if (entry.devicePixelContentBoxSize) {\n", " // Chrome 84 implements new version of spec.\n", " canvas.setAttribute(\n", " 'width',\n", " entry.devicePixelContentBoxSize[0].inlineSize\n", " );\n", " canvas.setAttribute(\n", " 'height',\n", " entry.devicePixelContentBoxSize[0].blockSize\n", " );\n", " } else {\n", " canvas.setAttribute('width', width * fig.ratio);\n", " canvas.setAttribute('height', height * fig.ratio);\n", " }\n", " canvas.setAttribute(\n", " 'style',\n", " 'width: ' + width + 'px; height: ' + height + 'px;'\n", " );\n", "\n", " rubberband_canvas.setAttribute('width', width);\n", " rubberband_canvas.setAttribute('height', height);\n", "\n", " // And update the size in Python. We ignore the initial 0/0 size\n", " // that occurs as the element is placed into the DOM, which should\n", " // otherwise not happen due to the minimum size styling.\n", " if (fig.ws.readyState == 1 && width != 0 && height != 0) {\n", " fig.request_resize(width, height);\n", " }\n", " }\n", " });\n", " this.resizeObserverInstance.observe(canvas_div);\n", "\n", " function on_mouse_event_closure(name) {\n", " return function (event) {\n", " return fig.mouse_event(event, name);\n", " };\n", " }\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mousedown',\n", " on_mouse_event_closure('button_press')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseup',\n", " on_mouse_event_closure('button_release')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'dblclick',\n", " on_mouse_event_closure('dblclick')\n", " );\n", " // Throttle sequential mouse events to 1 every 20ms.\n", " rubberband_canvas.addEventListener(\n", " 'mousemove',\n", " on_mouse_event_closure('motion_notify')\n", " );\n", "\n", " rubberband_canvas.addEventListener(\n", " 'mouseenter',\n", " on_mouse_event_closure('figure_enter')\n", " );\n", " rubberband_canvas.addEventListener(\n", " 'mouseleave',\n", " on_mouse_event_closure('figure_leave')\n", " );\n", "\n", " canvas_div.addEventListener('wheel', function (event) {\n", " if (event.deltaY < 0) {\n", " event.step = 1;\n", " } else {\n", " event.step = -1;\n", " }\n", " on_mouse_event_closure('scroll')(event);\n", " });\n", "\n", " canvas_div.appendChild(canvas);\n", " canvas_div.appendChild(rubberband_canvas);\n", "\n", " this.rubberband_context = rubberband_canvas.getContext('2d');\n", " this.rubberband_context.strokeStyle = '#000000';\n", "\n", " this._resize_canvas = function (width, height, forward) {\n", " if (forward) {\n", " canvas_div.style.width = width + 'px';\n", " canvas_div.style.height = height + 'px';\n", " }\n", " };\n", "\n", " // Disable right mouse context menu.\n", " this.rubberband_canvas.addEventListener('contextmenu', function (_e) {\n", " event.preventDefault();\n", " return false;\n", " });\n", "\n", " function set_focus() {\n", " canvas.focus();\n", " canvas_div.focus();\n", " }\n", "\n", " window.setTimeout(set_focus, 100);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'mpl-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'mpl-button-group';\n", " continue;\n", " }\n", "\n", " var button = (fig.buttons[name] = document.createElement('button'));\n", " button.classList = 'mpl-widget';\n", " button.setAttribute('role', 'button');\n", " button.setAttribute('aria-disabled', 'false');\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", "\n", " var icon_img = document.createElement('img');\n", " icon_img.src = '_images/' + image + '.png';\n", " icon_img.srcset = '_images/' + image + '_large.png 2x';\n", " icon_img.alt = tooltip;\n", " button.appendChild(icon_img);\n", "\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " var fmt_picker = document.createElement('select');\n", " fmt_picker.classList = 'mpl-widget';\n", " toolbar.appendChild(fmt_picker);\n", " this.format_dropdown = fmt_picker;\n", "\n", " for (var ind in mpl.extensions) {\n", " var fmt = mpl.extensions[ind];\n", " var option = document.createElement('option');\n", " option.selected = fmt === mpl.default_extension;\n", " option.innerHTML = fmt;\n", " fmt_picker.appendChild(option);\n", " }\n", "\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "};\n", "\n", "mpl.figure.prototype.request_resize = function (x_pixels, y_pixels) {\n", " // Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,\n", " // which will in turn request a refresh of the image.\n", " this.send_message('resize', { width: x_pixels, height: y_pixels });\n", "};\n", "\n", "mpl.figure.prototype.send_message = function (type, properties) {\n", " properties['type'] = type;\n", " properties['figure_id'] = this.id;\n", " this.ws.send(JSON.stringify(properties));\n", "};\n", "\n", "mpl.figure.prototype.send_draw_message = function () {\n", " if (!this.waiting) {\n", " this.waiting = true;\n", " this.ws.send(JSON.stringify({ type: 'draw', figure_id: this.id }));\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " var format_dropdown = fig.format_dropdown;\n", " var format = format_dropdown.options[format_dropdown.selectedIndex].value;\n", " fig.ondownload(fig, format);\n", "};\n", "\n", "mpl.figure.prototype.handle_resize = function (fig, msg) {\n", " var size = msg['size'];\n", " if (size[0] !== fig.canvas.width || size[1] !== fig.canvas.height) {\n", " fig._resize_canvas(size[0], size[1], msg['forward']);\n", " fig.send_message('refresh', {});\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_rubberband = function (fig, msg) {\n", " var x0 = msg['x0'] / fig.ratio;\n", " var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;\n", " var x1 = msg['x1'] / fig.ratio;\n", " var y1 = (fig.canvas.height - msg['y1']) / fig.ratio;\n", " x0 = Math.floor(x0) + 0.5;\n", " y0 = Math.floor(y0) + 0.5;\n", " x1 = Math.floor(x1) + 0.5;\n", " y1 = Math.floor(y1) + 0.5;\n", " var min_x = Math.min(x0, x1);\n", " var min_y = Math.min(y0, y1);\n", " var width = Math.abs(x1 - x0);\n", " var height = Math.abs(y1 - y0);\n", "\n", " fig.rubberband_context.clearRect(\n", " 0,\n", " 0,\n", " fig.canvas.width / fig.ratio,\n", " fig.canvas.height / fig.ratio\n", " );\n", "\n", " fig.rubberband_context.strokeRect(min_x, min_y, width, height);\n", "};\n", "\n", "mpl.figure.prototype.handle_figure_label = function (fig, msg) {\n", " // Updates the figure title.\n", " fig.header.textContent = msg['label'];\n", "};\n", "\n", "mpl.figure.prototype.handle_cursor = function (fig, msg) {\n", " var cursor = msg['cursor'];\n", " switch (cursor) {\n", " case 0:\n", " cursor = 'pointer';\n", " break;\n", " case 1:\n", " cursor = 'default';\n", " break;\n", " case 2:\n", " cursor = 'crosshair';\n", " break;\n", " case 3:\n", " cursor = 'move';\n", " break;\n", " }\n", " fig.rubberband_canvas.style.cursor = cursor;\n", "};\n", "\n", "mpl.figure.prototype.handle_message = function (fig, msg) {\n", " fig.message.textContent = msg['message'];\n", "};\n", "\n", "mpl.figure.prototype.handle_draw = function (fig, _msg) {\n", " // Request the server to send over a new figure.\n", " fig.send_draw_message();\n", "};\n", "\n", "mpl.figure.prototype.handle_image_mode = function (fig, msg) {\n", " fig.image_mode = msg['mode'];\n", "};\n", "\n", "mpl.figure.prototype.handle_history_buttons = function (fig, msg) {\n", " for (var key in msg) {\n", " if (!(key in fig.buttons)) {\n", " continue;\n", " }\n", " fig.buttons[key].disabled = !msg[key];\n", " fig.buttons[key].setAttribute('aria-disabled', !msg[key]);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {\n", " if (msg['mode'] === 'PAN') {\n", " fig.buttons['Pan'].classList.add('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " } else if (msg['mode'] === 'ZOOM') {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.add('active');\n", " } else {\n", " fig.buttons['Pan'].classList.remove('active');\n", " fig.buttons['Zoom'].classList.remove('active');\n", " }\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Called whenever the canvas gets updated.\n", " this.send_message('ack', {});\n", "};\n", "\n", "// A function to construct a web socket function for onmessage handling.\n", "// Called in the figure constructor.\n", "mpl.figure.prototype._make_on_message_function = function (fig) {\n", " return function socket_on_message(evt) {\n", " if (evt.data instanceof Blob) {\n", " var img = evt.data;\n", " if (img.type !== 'image/png') {\n", " /* FIXME: We get \"Resource interpreted as Image but\n", " * transferred with MIME type text/plain:\" errors on\n", " * Chrome. But how to set the MIME type? It doesn't seem\n", " * to be part of the websocket stream */\n", " img.type = 'image/png';\n", " }\n", "\n", " /* Free the memory for the previous frames */\n", " if (fig.imageObj.src) {\n", " (window.URL || window.webkitURL).revokeObjectURL(\n", " fig.imageObj.src\n", " );\n", " }\n", "\n", " fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(\n", " img\n", " );\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " } else if (\n", " typeof evt.data === 'string' &&\n", " evt.data.slice(0, 21) === 'data:image/png;base64'\n", " ) {\n", " fig.imageObj.src = evt.data;\n", " fig.updated_canvas_event();\n", " fig.waiting = false;\n", " return;\n", " }\n", "\n", " var msg = JSON.parse(evt.data);\n", " var msg_type = msg['type'];\n", "\n", " // Call the \"handle_{type}\" callback, which takes\n", " // the figure and JSON message as its only arguments.\n", " try {\n", " var callback = fig['handle_' + msg_type];\n", " } catch (e) {\n", " console.log(\n", " \"No handler for the '\" + msg_type + \"' message type: \",\n", " msg\n", " );\n", " return;\n", " }\n", "\n", " if (callback) {\n", " try {\n", " // console.log(\"Handling '\" + msg_type + \"' message: \", msg);\n", " callback(fig, msg);\n", " } catch (e) {\n", " console.log(\n", " \"Exception inside the 'handler_\" + msg_type + \"' callback:\",\n", " e,\n", " e.stack,\n", " msg\n", " );\n", " }\n", " }\n", " };\n", "};\n", "\n", "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", "mpl.findpos = function (e) {\n", " //this section is from http://www.quirksmode.org/js/events_properties.html\n", " var targ;\n", " if (!e) {\n", " e = window.event;\n", " }\n", " if (e.target) {\n", " targ = e.target;\n", " } else if (e.srcElement) {\n", " targ = e.srcElement;\n", " }\n", " if (targ.nodeType === 3) {\n", " // defeat Safari bug\n", " targ = targ.parentNode;\n", " }\n", "\n", " // pageX,Y are the mouse positions relative to the document\n", " var boundingRect = targ.getBoundingClientRect();\n", " var x = e.pageX - (boundingRect.left + document.body.scrollLeft);\n", " var y = e.pageY - (boundingRect.top + document.body.scrollTop);\n", "\n", " return { x: x, y: y };\n", "};\n", "\n", "/*\n", " * return a copy of an object with only non-object keys\n", " * we need this to avoid circular references\n", " * http://stackoverflow.com/a/24161582/3208463\n", " */\n", "function simpleKeys(original) {\n", " return Object.keys(original).reduce(function (obj, key) {\n", " if (typeof original[key] !== 'object') {\n", " obj[key] = original[key];\n", " }\n", " return obj;\n", " }, {});\n", "}\n", "\n", "mpl.figure.prototype.mouse_event = function (event, name) {\n", " var canvas_pos = mpl.findpos(event);\n", "\n", " if (name === 'button_press') {\n", " this.canvas.focus();\n", " this.canvas_div.focus();\n", " }\n", "\n", " var x = canvas_pos.x * this.ratio;\n", " var y = canvas_pos.y * this.ratio;\n", "\n", " this.send_message(name, {\n", " x: x,\n", " y: y,\n", " button: event.button,\n", " step: event.step,\n", " guiEvent: simpleKeys(event),\n", " });\n", "\n", " /* This prevents the web browser from automatically changing to\n", " * the text insertion cursor when the button is pressed. We want\n", " * to control all of the cursor setting manually through the\n", " * 'cursor' event from matplotlib */\n", " event.preventDefault();\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (_event, _name) {\n", " // Handle any extra behaviour associated with a key event\n", "};\n", "\n", "mpl.figure.prototype.key_event = function (event, name) {\n", " // Prevent repeat events\n", " if (name === 'key_press') {\n", " if (event.key === this._key) {\n", " return;\n", " } else {\n", " this._key = event.key;\n", " }\n", " }\n", " if (name === 'key_release') {\n", " this._key = null;\n", " }\n", "\n", " var value = '';\n", " if (event.ctrlKey && event.key !== 'Control') {\n", " value += 'ctrl+';\n", " }\n", " else if (event.altKey && event.key !== 'Alt') {\n", " value += 'alt+';\n", " }\n", " else if (event.shiftKey && event.key !== 'Shift') {\n", " value += 'shift+';\n", " }\n", "\n", " value += 'k' + event.key;\n", "\n", " this._key_event_extra(event, name);\n", "\n", " this.send_message(name, { key: value, guiEvent: simpleKeys(event) });\n", " return false;\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onclick = function (name) {\n", " if (name === 'download') {\n", " this.handle_save(this, null);\n", " } else {\n", " this.send_message('toolbar_button', { name: name });\n", " }\n", "};\n", "\n", "mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) {\n", " this.message.textContent = tooltip;\n", "};\n", "\n", "///////////////// REMAINING CONTENT GENERATED BY embed_js.py /////////////////\n", "// prettier-ignore\n", "var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError(\"Constructor requires 'new' operator\");i.set(this,e)}function h(){throw new TypeError(\"Function is not a constructor\")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line\n", "mpl.toolbar_items = [[\"Home\", \"Reset original view\", \"fa fa-home icon-home\", \"home\"], [\"Back\", \"Back to previous view\", \"fa fa-arrow-left icon-arrow-left\", \"back\"], [\"Forward\", \"Forward to next view\", \"fa fa-arrow-right icon-arrow-right\", \"forward\"], [\"\", \"\", \"\", \"\"], [\"Pan\", \"Left button pans, Right button zooms\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-arrows icon-move\", \"pan\"], [\"Zoom\", \"Zoom to rectangle\\nx/y fixes axis, CTRL fixes aspect\", \"fa fa-square-o icon-check-empty\", \"zoom\"], [\"\", \"\", \"\", \"\"], [\"Download\", \"Download plot\", \"fa fa-floppy-o icon-save\", \"download\"]];\n", "\n", "mpl.extensions = [\"eps\", \"jpeg\", \"pgf\", \"pdf\", \"png\", \"ps\", \"raw\", \"svg\", \"tif\"];\n", "\n", "mpl.default_extension = \"png\";/* global mpl */\n", "\n", "var comm_websocket_adapter = function (comm) {\n", " // Create a \"websocket\"-like object which calls the given IPython comm\n", " // object with the appropriate methods. Currently this is a non binary\n", " // socket, so there is still some room for performance tuning.\n", " var ws = {};\n", "\n", " ws.binaryType = comm.kernel.ws.binaryType;\n", " ws.readyState = comm.kernel.ws.readyState;\n", " function updateReadyState(_event) {\n", " if (comm.kernel.ws) {\n", " ws.readyState = comm.kernel.ws.readyState;\n", " } else {\n", " ws.readyState = 3; // Closed state.\n", " }\n", " }\n", " comm.kernel.ws.addEventListener('open', updateReadyState);\n", " comm.kernel.ws.addEventListener('close', updateReadyState);\n", " comm.kernel.ws.addEventListener('error', updateReadyState);\n", "\n", " ws.close = function () {\n", " comm.close();\n", " };\n", " ws.send = function (m) {\n", " //console.log('sending', m);\n", " comm.send(m);\n", " };\n", " // Register the callback with on_msg.\n", " comm.on_msg(function (msg) {\n", " //console.log('receiving', msg['content']['data'], msg);\n", " var data = msg['content']['data'];\n", " if (data['blob'] !== undefined) {\n", " data = {\n", " data: new Blob(msg['buffers'], { type: data['blob'] }),\n", " };\n", " }\n", " // Pass the mpl event to the overridden (by mpl) onmessage function.\n", " ws.onmessage(data);\n", " });\n", " return ws;\n", "};\n", "\n", "mpl.mpl_figure_comm = function (comm, msg) {\n", " // This is the function which gets called when the mpl process\n", " // starts-up an IPython Comm through the \"matplotlib\" channel.\n", "\n", " var id = msg.content.data.id;\n", " // Get hold of the div created by the display call when the Comm\n", " // socket was opened in Python.\n", " var element = document.getElementById(id);\n", " var ws_proxy = comm_websocket_adapter(comm);\n", "\n", " function ondownload(figure, _format) {\n", " window.open(figure.canvas.toDataURL());\n", " }\n", "\n", " var fig = new mpl.figure(id, ws_proxy, ondownload, element);\n", "\n", " // Call onopen now - mpl needs it, as it is assuming we've passed it a real\n", " // web socket which is closed, not our websocket->open comm proxy.\n", " ws_proxy.onopen();\n", "\n", " fig.parent_element = element;\n", " fig.cell_info = mpl.find_output_cell(\"
\");\n", " if (!fig.cell_info) {\n", " console.error('Failed to find cell for figure', id, fig);\n", " return;\n", " }\n", " fig.cell_info[0].output_area.element.on(\n", " 'cleared',\n", " { fig: fig },\n", " fig._remove_fig_handler\n", " );\n", "};\n", "\n", "mpl.figure.prototype.handle_close = function (fig, msg) {\n", " var width = fig.canvas.width / fig.ratio;\n", " fig.cell_info[0].output_area.element.off(\n", " 'cleared',\n", " fig._remove_fig_handler\n", " );\n", " fig.resizeObserverInstance.unobserve(fig.canvas_div);\n", "\n", " // Update the output cell to use the data from the current canvas.\n", " fig.push_to_output();\n", " var dataURL = fig.canvas.toDataURL();\n", " // Re-enable the keyboard manager in IPython - without this line, in FF,\n", " // the notebook keyboard shortcuts fail.\n", " IPython.keyboard_manager.enable();\n", " fig.parent_element.innerHTML =\n", " '';\n", " fig.close_ws(fig, msg);\n", "};\n", "\n", "mpl.figure.prototype.close_ws = function (fig, msg) {\n", " fig.send_message('closing', msg);\n", " // fig.ws.close()\n", "};\n", "\n", "mpl.figure.prototype.push_to_output = function (_remove_interactive) {\n", " // Turn the data on the canvas into data in the output cell.\n", " var width = this.canvas.width / this.ratio;\n", " var dataURL = this.canvas.toDataURL();\n", " this.cell_info[1]['text/html'] =\n", " '';\n", "};\n", "\n", "mpl.figure.prototype.updated_canvas_event = function () {\n", " // Tell IPython that the notebook contents must change.\n", " IPython.notebook.set_dirty(true);\n", " this.send_message('ack', {});\n", " var fig = this;\n", " // Wait a second, then push the new image to the DOM so\n", " // that it is saved nicely (might be nice to debounce this).\n", " setTimeout(function () {\n", " fig.push_to_output();\n", " }, 1000);\n", "};\n", "\n", "mpl.figure.prototype._init_toolbar = function () {\n", " var fig = this;\n", "\n", " var toolbar = document.createElement('div');\n", " toolbar.classList = 'btn-toolbar';\n", " this.root.appendChild(toolbar);\n", "\n", " function on_click_closure(name) {\n", " return function (_event) {\n", " return fig.toolbar_button_onclick(name);\n", " };\n", " }\n", "\n", " function on_mouseover_closure(tooltip) {\n", " return function (event) {\n", " if (!event.currentTarget.disabled) {\n", " return fig.toolbar_button_onmouseover(tooltip);\n", " }\n", " };\n", " }\n", "\n", " fig.buttons = {};\n", " var buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " var button;\n", " for (var toolbar_ind in mpl.toolbar_items) {\n", " var name = mpl.toolbar_items[toolbar_ind][0];\n", " var tooltip = mpl.toolbar_items[toolbar_ind][1];\n", " var image = mpl.toolbar_items[toolbar_ind][2];\n", " var method_name = mpl.toolbar_items[toolbar_ind][3];\n", "\n", " if (!name) {\n", " /* Instead of a spacer, we start a new button group. */\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", " buttonGroup = document.createElement('div');\n", " buttonGroup.classList = 'btn-group';\n", " continue;\n", " }\n", "\n", " button = fig.buttons[name] = document.createElement('button');\n", " button.classList = 'btn btn-default';\n", " button.href = '#';\n", " button.title = name;\n", " button.innerHTML = '';\n", " button.addEventListener('click', on_click_closure(method_name));\n", " button.addEventListener('mouseover', on_mouseover_closure(tooltip));\n", " buttonGroup.appendChild(button);\n", " }\n", "\n", " if (buttonGroup.hasChildNodes()) {\n", " toolbar.appendChild(buttonGroup);\n", " }\n", "\n", " // Add the status bar.\n", " var status_bar = document.createElement('span');\n", " status_bar.classList = 'mpl-message pull-right';\n", " toolbar.appendChild(status_bar);\n", " this.message = status_bar;\n", "\n", " // Add the close button to the window.\n", " var buttongrp = document.createElement('div');\n", " buttongrp.classList = 'btn-group inline pull-right';\n", " button = document.createElement('button');\n", " button.classList = 'btn btn-mini btn-primary';\n", " button.href = '#';\n", " button.title = 'Stop Interaction';\n", " button.innerHTML = '';\n", " button.addEventListener('click', function (_evt) {\n", " fig.handle_close(fig, {});\n", " });\n", " button.addEventListener(\n", " 'mouseover',\n", " on_mouseover_closure('Stop Interaction')\n", " );\n", " buttongrp.appendChild(button);\n", " var titlebar = this.root.querySelector('.ui-dialog-titlebar');\n", " titlebar.insertBefore(buttongrp, titlebar.firstChild);\n", "};\n", "\n", "mpl.figure.prototype._remove_fig_handler = function (event) {\n", " var fig = event.data.fig;\n", " if (event.target !== this) {\n", " // Ignore bubbled events from children.\n", " return;\n", " }\n", " fig.close_ws(fig, {});\n", "};\n", "\n", "mpl.figure.prototype._root_extra_style = function (el) {\n", " el.style.boxSizing = 'content-box'; // override notebook setting of border-box.\n", "};\n", "\n", "mpl.figure.prototype._canvas_extra_style = function (el) {\n", " // this is important to make the div 'focusable\n", " el.setAttribute('tabindex', 0);\n", " // reach out to IPython and tell the keyboard manager to turn it's self\n", " // off when our div gets focus\n", "\n", " // location in version 3\n", " if (IPython.notebook.keyboard_manager) {\n", " IPython.notebook.keyboard_manager.register_events(el);\n", " } else {\n", " // location in version 2\n", " IPython.keyboard_manager.register_events(el);\n", " }\n", "};\n", "\n", "mpl.figure.prototype._key_event_extra = function (event, _name) {\n", " var manager = IPython.notebook.keyboard_manager;\n", " if (!manager) {\n", " manager = IPython.keyboard_manager;\n", " }\n", "\n", " // Check for shift+enter\n", " if (event.shiftKey && event.which === 13) {\n", " this.canvas_div.blur();\n", " // select the cell after this one\n", " var index = IPython.notebook.find_cell_index(this.cell_info[0]);\n", " IPython.notebook.select(index + 1);\n", " }\n", "};\n", "\n", "mpl.figure.prototype.handle_save = function (fig, _msg) {\n", " fig.ondownload(fig, null);\n", "};\n", "\n", "mpl.find_output_cell = function (html_output) {\n", " // Return the cell and output element which can be found *uniquely* in the notebook.\n", " // Note - this is a bit hacky, but it is done because the \"notebook_saving.Notebook\"\n", " // IPython event is triggered only after the cells have been serialised, which for\n", " // our purposes (turning an active figure into a static one), is too late.\n", " var cells = IPython.notebook.get_cells();\n", " var ncells = cells.length;\n", " for (var i = 0; i < ncells; i++) {\n", " var cell = cells[i];\n", " if (cell.cell_type === 'code') {\n", " for (var j = 0; j < cell.output_area.outputs.length; j++) {\n", " var data = cell.output_area.outputs[j];\n", " if (data.data) {\n", " // IPython >= 3 moved mimebundle to data attribute of output\n", " data = data.data;\n", " }\n", " if (data['text/html'] === html_output) {\n", " return [cell, data, j];\n", " }\n", " }\n", " }\n", " }\n", "};\n", "\n", "// Register the function which deals with the matplotlib target/channel.\n", "// The kernel may be null if the page has been refreshed.\n", "if (IPython.notebook.kernel !== null) {\n", " IPython.notebook.kernel.comm_manager.register_target(\n", " 'matplotlib',\n", " mpl.mpl_figure_comm\n", " );\n", "}\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "Completed repeat 5/10 for parameter setting 1/2\n", "Completed repeat 10/10 for parameter setting 1/2\n", "Completed repeat 5/10 for parameter setting 2/2\n", "Completed repeat 10/10 for parameter setting 2/2\n" ] } ], "source": [ "# This cell can take a while to run depending on the number of races and repeats\n", "num_races = 200 # number of races over which the AI driver can learn\n", "num_repeats = 10 # number of time to repeat the races, starting from scratch each time, but different random choices\n", "param = [[0, 1, 0], [0.5, 0.999, 0]]\n", "race_times = np.zeros((num_races, num_repeats, len(param)))\n", "fig = plt.figure(figsize=(9, 6))\n", "fig.add_axes([0.08, 0.08, 0.92, 0.92])\n", "lines = []\n", "\n", "for pi in range(len(param)):\n", " np.random.seed(0) # for repeatability\n", " for i in range(num_repeats):\n", " driver = LearnerDriver(driver_name, \n", " random_action_probability=param[pi][0], \n", " random_action_decay=param[pi][1],\n", " min_random_action_probability=param[pi][2],\n", " discount_factor=0.9) # set up driver with parameters\n", " for n in range(num_races):\n", " driver, race_times[n, i, pi], _ = race(driver, track=track, plot=False, max_number_of_steps=1000)\n", " if np.mod(i + 1, 5) == 0:\n", " print(f'Completed repeat {i + 1}/{num_repeats} for parameter setting {pi+1}/{len(param)}')\n", " \n", " # Plot the results\n", " lines += plt.plot(range(1, num_races + 1), np.mean(race_times[:, :, pi], axis=1))\n", " plt.fill_between(range(1, num_races + 1), np.min(race_times[:, :, pi], axis=1), \n", " np.max(race_times[:, :, pi], axis=1), color=lines[-1].get_color(), alpha=0.1)\n", "\n", "plt.ylabel('Race Times', fontsize=16)\n", "plt.xlabel('Race Number', fontsize=16)\n", "fig.gca().xaxis.set_major_locator(MaxNLocator(integer=True))\n", "plt.legend(lines, param, fontsize=16);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that initially the driver with 0 random action probability (blue) does a lot better the driver which is exploring (orange). However, once the drivers converge on their best solution the driver which spent more time trying out different actions usually finds a faster way round the track than the driver that just focussed on exploitation. There is a trade-off here then: investing time at the start to explore might lose you championship points in the early races but then gain you points in the later races. Getting this balance right is one of the keys to building a championship winning car!!\n", "\n", "There are also other changes we could make to try and improve our AI driver. Rather than choosing each state with the same chance when choosing randomly, we could weight it to focus on the actions with the highest value - allowing exploration but encouraging the driver to test out actions already seen to have some benefit. \n", "\n", "We could also look at the reward scheme we have set up. Changing this can have a large effect on how our driver learns. We might want to reward accelerating at low speed more than at high speed, where it has a smaller effect, for example.\n", "\n", "If you want to play further with the driver at this level then feel free to continue here. You can try running over more races or diving in and editing the driver code to make bigger changes - using the sub-class below is one easy way to do this. This is just the intro level however, there are many more challenges available in the next level...so move on up when you are ready!\n", "\n", "# We hope to see you in level 2: Young Driver!\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Playground" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "class MyLearnerDriver(LearnerDriver):\n", " def __init__(self, *args, **kwargs):\n", " super().__init__(*args, **kwargs)\n", " \n", " def make_a_move(self, car_state: CarState, track_state: TrackState):\n", " return super().make_a_move(car_state, track_state)\n", " \n", " def _choose_randomly(self, available_actions: List[Action]):\n", " super()._choose_randomly(available_actions)\n", " \n", " def update_with_action_results(self, previous_car_state: CarState, previous_track_state: TrackState,\n", " action: Action, new_car_state: CarState, new_track_state: TrackState,\n", " result: ActionResult):\n", " super().update_with_action_results(previous_car_state, previous_track_state, \n", " action, new_car_state, new_track_state, result)\n", "\n", "my_driver = MyLearnerDriver(driver_name)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(<__main__.MyLearnerDriver at 0x197e0e999a0>, 25.72191857733029, True)" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "race(driver=my_driver, level=Level.Learner)\n", "plt.close()" ] } ], "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.11" }, "pycharm": { "stem_cell": { "cell_type": "raw", "metadata": { "collapsed": false }, "source": [] } } }, "nbformat": 4, "nbformat_minor": 1 }