{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"Here I present the work for FLAIR: Focused Learning AI Returner. This is AI/ML agent that takes in tracking data from the NFL of other players on the field and learns optimal return strategies! To accomplish this, I breakdown the process into three parts: Data Wrangling, Environment Development, and Agent Training. I also include mentions of its uses for a New Metric, Broadcast Potential, and Coaching.","metadata":{}},{"cell_type":"markdown","source":"# 1. Wrangle\n\nThe key here is to get the data we need, and in the usable format. This means that we have to extract just the \"routine\" kickoffs. These are the ones where the ball is kicked, caught, and returned, with no other events occluding this simple order (muffs, onsides, etc.) Naturally we will only be looking at kickoffs. Due to the tools, I used R to acheive this. \n\nFirst, I created a new ID field by concatenating the Game ID and Play ID (note the new ID is still unique). Then I ordered all of the data by Game and Frame, so the plays were tidy. I sent this CSV over to python for further editting.\n\nOnce I read in the data, I structured this database of plays by:\n* Play\n* * Frame\n* * * (1) Returner\n* * * (2-11) Returner's Teammates\n* * * (12-22) Opponent Players\n\nI only included frames from the point the kick was received and on, as this is the only data my agent needs to learn from.","metadata":{}},{"cell_type":"markdown","source":"# 2. Simulate!\n\nSo, now that I put my data in an easily accessible structure, it is time to set up the environment. This simplistic simulation allowed me to have the agent interact with the data. I let the agent control the direction it ran. (Later modifications allow us to control things like speed and acceleration, but to start I will just focus on direction, and use the returner's real speed). If it was too close to an opponent, without a teammate to block, then it was tackled and did not score as well, naturally. So, it was incentivised to avoid others and run forward. We also need to confine the AI to the field of play!\n\nSimilarly to the way the data was collected, I used frames, or steps, similar to OpenAI's gym, to create the environment, complete with __init__, reset(), step(), and render() functions. These allowed me to access the simulation and have the agent train. ","metadata":{}},{"cell_type":"markdown","source":"# 3. Train!\n\nOnce the environment was set up, I used a Deep Q Network to train the agent. There are extensive technical descriptions online; in short, I allowed the agent to explore, store up memories, and then learn from those memories to dread a Deep Neural Net. This would be allagous to having someone return a kickoff a million times, and as they are running, remember back to previous similar situations, and take an action that was useful before. (I let the agent see the whole field, which is not always accurate, but is a great future modification.)\n\nThe training results are here: We can see a bit of learning, so our agent is getitng better at returning, but this may take a lot of data. (I have more runs going now that I will post as soon as they are done!)\n\n![p2.png](attachment:a8ad56af-ade6-4655-92d0-a32291368bd1.png)","metadata":{},"attachments":{"a8ad56af-ade6-4655-92d0-a32291368bd1.png":{"image/png":"iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAA9hAAAPYQGoP6dpAABcIklEQVR4nO3deVxUVeMG8OfODDMM27DJliCo5J4bLqhpJoVa9lqmWVhopmXuSy6VmuWSVma2aJlpGmb6S63MNFPKNDUTNXchQVwAF/ZtmOX8/kAmh8WwuMMyz/fzmc/LnHvmnjMX8j7vueeeKwkhBIiIiIjIbiiquwNEREREZFsMgERERER2hgGQiIiIyM4wABIRERHZGQZAIiIiIjvDAEhERERkZxgAiYiIiOwMAyARERGRnWEAJCIiIrIzDIBEREREdoYBkIiIiMjOMAASERER2RkGQCIiIiI7wwBIREREZGcYAImIiIjsDAMgERERkZ1hACQiIiKyMwyARERERHaGAZCIiIjIzjAAEhEREdkZBkAiIiIiO8MASERERGRnGACJiIiI7AwDIBEREZGdYQAkIiIisjMMgERERER2hgGQiIiIyM4wABIRERHZGQZAIiIiIjvDAEhERERkZxgAiYiIiOwMAyARERGRnWEAJCIiIrIzDIBEREREdoYBkIiohlq9ejUkSUJSUpJsbSQlJUGSJKxevVq2Noio5mEAJKoBSk705b2mT59uqRccHIyHH3640vvdtm0bJElCQEAAzGZzuXXK22fpPri5uaFHjx74/vvv7/i7nT59GpIkwdHREZmZmXf8+bps6NChFf7et2/fXt3dKyMpKQnDhg1Do0aN4OjoCD8/P3Tv3h2zZ8+Wtd0rV67gtddew9GjR2Vth8ieqKq7A0T0t9dffx0hISFWZS1btvzX+4uJiUFwcDCSkpKwe/duREREVPqzDzzwAJ555hkIIXDhwgUsW7YM/fr1ww8//IDIyMhK7+eLL76An58fMjIy8H//93947rnn/s1XqbM0Gg0+/fTTMuWtW7fGAw88gMGDB0Oj0VRDz6wlJCSgQ4cO0Gq1ePbZZxEcHIyUlBTExcVh4cKFmDNnjmxtX7lyBXPmzEFwcDDatGkjWztE9oQBkKgG6dOnD8LCwqpkX3l5efjmm2+wYMECrFq1CjExMXcUAO+++24MGTLE8n7AgAFo3rw53nvvvUoHQCEE1q1bh6eeegqJiYmIiYmplgCYn58PJycnm7dbGSqVyuo4l6ZUKm3Ym4q9++67yM3NxdGjR9GgQQOrbVevXq2mXhHRv8VLwER11ObNm1FQUICBAwdi8ODB2LRpEwoLC//1/po1awZvb2/89ddflf7Mvn37kJSUhMGDB2Pw4MHYs2cPLl26ZNn+8MMPo2HDhuV+Njw8vEwY/uKLL9C+fXtotVp4enpi8ODBuHjxolWd++67Dy1btsThw4fRvXt3ODk54eWXXwYAfPPNN3jooYcQEBAAjUaDRo0a4Y033oDJZCrT/ocffoiGDRtCq9WiY8eO+PXXX3Hffffhvvvus6qn1+sxe/ZsNG7cGBqNBoGBgZg6dSr0en2lj1NFypsDWHLJfu/evejYsSMcHR3RsGFDrFmzxuqz6enpmDJlClq1agUXFxe4ubmhT58+OHbs2L/qy19//YX69euXCX8A4OPjY/W+pI8//vgj2rRpA0dHRzRv3hybNm0q89nz589j4MCB8PT0hJOTEzp37mw11eDnn39Ghw4dAADDhg2zXCLnnEWi/4YBkKgGycrKwvXr161e/1ZMTAx69uwJPz8/DB48GDk5Ofjuu+/+U98yMjLg4eFxR31o1KgROnTogH79+sHJyQlffvmlZfsTTzyBxMREHDp0yOpzFy5cwIEDBzB48GBL2bx58/DMM88gNDQUixcvxoQJE7Br1y507969zNzCGzduoE+fPmjTpg2WLFmCnj17AigOVC4uLpg0aRLee+89tG/fHrNmzbKaZwkAy5Ytw5gxY1C/fn0sWrQI9957L/r3728VXgHAbDbjkUcewdtvv41+/frh/fffR//+/fHuu+/iiSeeqPRxKv07z8rKum39hIQEPP7443jggQfwzjvvwMPDA0OHDsXJkyctdc6fP48tW7bg4YcfxuLFi/HSSy/h+PHj6NGjB65cuVLpvpVo0KABLl68iN27d1eqfnx8PJ544gn06dMHCxYsgEqlwsCBA7Fz505LnbS0NHTp0gU7duzAiy++iHnz5qGwsBCPPPIINm/eDKD4/3i8/vrrAICRI0di7dq1WLt2Lbp3737H34GIbiGIqNqtWrVKACj3dasGDRqIhx566B/3l5aWJlQqlVixYoWlrEuXLuJ///tfmbrl7ROAGD58uLh27Zq4evWq+OOPP0Tv3r0FAPHWW29V6jsVFRUJLy8v8corr1jKnnrqKdG6dWvL+6ysLKHRaMTkyZOtPrto0SIhSZK4cOGCEEKIpKQkoVQqxbx586zqHT9+XKhUKqvyHj16CABi+fLlZfqUn59fpuz5558XTk5OorCwUAghhF6vF15eXqJDhw7CYDBY6q1evVoAED169LCUrV27VigUCvHrr79a7XP58uUCgNi3b19Fh0cIIUR0dHS5v/OSNkr+LhITEy2fadCggQAg9uzZYym7evVqmeNYWFgoTCaTVXuJiYlCo9GI119/3aoMgFi1atVt+3rixAmh1WoFANGmTRsxfvx4sWXLFpGXl1embkkfv/76a0tZVlaW8Pf3F23btrWUTZgwQQCwOn45OTkiJCREBAcHW/p/6NChSvWRiCqPI4BENciHH36InTt3Wr3+jfXr10OhUGDAgAGWsieffBI//PADMjIyKrWPlStXol69evDx8UFYWBh27dqFqVOnYtKkSZX6/A8//IAbN27gySeftOrDsWPHLCNVJZclN2zYACGEpd5XX32Fzp07IygoCACwadMmmM1mDBo0yGqkzM/PD6GhoYiNjbVqW6PRYNiwYWX6pNVqLT/n5OTg+vXruPfee5Gfn48zZ84AAP744w/cuHEDI0aMgEr19zTpqKioMqOfGzduRLNmzdC0aVOrft1///0AUKZf5XF0dCzzO3/nnXdu+5nmzZvj3nvvtbyvV68emjRpgvPnz1sdA4Wi+J94k8mEGzduwMXFBU2aNEFcXNw/9qu0Fi1a4OjRoxgyZAiSkpLw3nvvoX///vD19cWKFSvK1A8ICMCjjz5qee/m5oZnnnkGR44cQWpqKoDiu9Q7duyIbt26Weq5uLhg5MiRSEpKwqlTp+64n0RUObwJhKgG6dixY5XcBPLFF1+gY8eOuHHjBm7cuAEAaNu2LYqKirBx40aMHDnyH/fxv//9D2PGjEFRUREOHTqE+fPnIz8/3xIqKtOHkJAQaDQaJCQkAAAaNWoEJycnxMTEYP78+QCKLwNv2bIF+/fvR5cuXfDXX3/h8OHDWLJkiWVf8fHxEEIgNDS03LYcHBys3t91111Qq9Vl6p08eRKvvvoqdu/ejezsbKttJZddL1y4AABo3Lix1XaVSoXg4GCrsvj4eJw+fRr16tUrt1+VuTlCqVTe0c05ACzB+FYeHh5W4d5sNuO9997DRx99hMTERKt5jl5eXnfUXom7774ba9euhclkwqlTp7B161YsWrQII0eOREhIiNX3aNy4MSRJKvN5oHg5GT8/P1y4cAGdOnUq006zZs0AFP8u/std8ERUMQZAojomPj7eMqeuvMAUExNTqQBYv359ywm9b9++8Pb2xpgxY9CzZ0889thjt/1sdnY2vvvuOxQWFpbbh3Xr1mHevHmQJMkyN3DDhg3o0qULNmzYAIVCgYEDB1rqm81mSJKEH374ody7Yl1cXKze3zrSVyIzMxM9evSAm5sbXn/9dctadnFxcZg2bVqF6yTejtlsRqtWrbB48eJytwcGBt7xPiujojuDbx1FnT9/PmbOnIlnn30Wb7zxBjw9PaFQKDBhwoR/9V1Lt9+qVSu0atUK4eHh6Nmz5x3fZU5E1YsBkKiOiYmJgYODA9auXVsmKOzduxdLly5FcnJyuaNIt/P888/j3XffxauvvopHH320zOjOrUruOF62bBm8vb2ttp09exavvvoq9u3bh27dusHZ2RkPP/wwNm7ciMWLF+Orr77Cvffei4CAAMtnGjVqBCEEQkJCLKNId+rnn3/GjRs3sGnTJqsbCBITE63qldzlmpCQYLl5BACMRiOSkpJwzz33WPXr2LFj6NWr122PR3X4v//7P/Ts2RMrV660Ks/MzCzzO/kvSkasU1JSrMoTEhIghLA6LufOnQMAy0hqgwYNcPbs2TL7LLkcX/K7qGnHlqgu4BxAojomJiYG9957L5544gk8/vjjVq+XXnoJAKzuxK0slUqFyZMn4/Tp0/jmm29uW/eLL75Aw4YN8cILL5Tpw5QpU+Di4oKYmBhL/SeeeAJXrlzBp59+imPHjpW5g/axxx6DUqnEnDlzrEa5gOJRr5LL3LdTEoZv/XxRURE++ugjq3phYWHw8vLCihUrYDQaLeUxMTFl5k8OGjQIly9fLncOXEFBAfLy8v6xX3JRKpVljtXGjRtx+fLlf7W/X3/9FQaDoUz5tm3bAABNmjSxKr9y5YrlTl6geFR4zZo1aNOmDfz8/AAUjyz//vvv2L9/v6VeXl4ePvnkEwQHB6N58+YAAGdnZwDgk2SIqhBHAIlqmYSEBMydO7dMedu2beHt7Y2EhASMGTOm3M/eddddaNeuHWJiYjBt2rQ7bnvo0KGYNWsWFi5ciP79+5db58qVK4iNjcW4cePK3a7RaBAZGYmNGzdi6dKlcHBwQN++feHq6oopU6ZAqVRa3bwCFI+0zZ07FzNmzEBSUhL69+8PV1dXJCYmYvPmzRg5ciSmTJly27536dIFHh4eiI6Oxrhx4yBJEtauXVsmJKnVarz22msYO3Ys7r//fgwaNAhJSUlYvXo1GjVqZDUa9fTTT2PDhg144YUXEBsbi65du8JkMuHMmTPYsGEDduzYUWULe9+phx9+GK+//jqGDRuGLl264Pjx44iJialw3cV/snDhQhw+fBiPPfaYZRQ0Li4Oa9asgaenJyZMmGBV/+6778bw4cNx6NAh+Pr64rPPPkNaWhpWrVplqTN9+nR8+eWX6NOnD8aNGwdPT098/vnnSExMxNdff22Zb9qoUSO4u7tj+fLlcHV1hbOzMzp16lTmqTlEdAeq6/ZjIvpbyXIfhw4dum29kuU1ynsNHz5cjB07VgAQf/31V4X7eO211wQAcezYMcs+y1sGZvTo0bf9fGxsbLnb33nnHQFA7Nq1q8I+lCyp8s0331jKoqKiBAARERFR4ee+/vpr0a1bN+Hs7CycnZ1F06ZNxejRo8XZs2ctdXr06CFatGhR7uf37dsnOnfuLLRarQgICBBTp04VO3bsKPf7LF26VDRo0EBoNBrRsWNHsW/fPtG+fXvRu3dvq3pFRUVi4cKFokWLFkKj0QgPDw/Rvn17MWfOHJGVlVXhdxGieBkYZ2fnCrdXtAxMeUsB9ejRw2qJmsLCQjF58mTh7+8vtFqt6Nq1q9i/f3+ZepVdBmbfvn1i9OjRomXLlkKn0wkHBwcRFBQkhg4dWubvraSPO3bsEPfcc4/QaDSiadOmYuPGjWX2+9dff4nHH39cuLu7C0dHR9GxY0exdevWMvW++eYb0bx5c6FSqbgkDFEVkIQo9X9/iYioDLPZjHr16uGxxx4r95Iv/S04OBgtW7bE1q1bq7srRFQBzgEkIiqlsLCwzKXhNWvWID09vcyj4IiIaiPOASQiKuXAgQOYOHEiBg4cCC8vL8TFxWHlypVo2bKl1fI0RES1FQMgEVEpwcHBCAwMxNKlS5Geng5PT08888wzePPNN8tdYJqIqLaplZeA9+zZg379+iEgIACSJGHLli2WbQaDAdOmTUOrVq3g7OyMgIAAPPPMM2Uefp6eno6oqCi4ubnB3d0dw4cPR25uro2/CRHVRMHBwfj222+RmpqKoqIipKam4rPPPoOPj091d61WSEpK4vw/ohquVgbAvLw8tG7dGh9++GGZbfn5+YiLi8PMmTMRFxeHTZs24ezZs3jkkUes6kVFReHkyZPYuXMntm7dij179lTq6QhEREREtV2tvwtYkiRs3ry5wjXJAODQoUPo2LEjLly4gKCgIJw+fRrNmzfHoUOHLGt0bd++HX379sWlS5esnkBAREREVNfUyhHAO5WVlQVJkuDu7g4A2L9/P9zd3a0WaI2IiIBCocDBgwerqZdEREREtlHnbwIpLCzEtGnT8OSTT8LNzQ0AkJqaWmYuj0qlgqenJ1JTUyvcl16vh16vt7w3m81IT0+Hl5cXn1VJRERUSwghkJOTg4CAAMsTZ+xNnQ6ABoMBgwYNghACy5Yt+8/7W7BgAebMmVMFPSMiIqLqdvHiRdSvX7+6u1Et6mwALAl/Fy5cwO7duy2jfwDg5+eHq1evWtU3Go1IT0+3PKS8PDNmzMCkSZMs77OyshAUFISLFy9a7Z+IiIhqruzsbAQGBsLV1bW6u1Jt6mQALAl/8fHxiI2NhZeXl9X28PBwZGZm4vDhw2jfvj0AYPfu3TCbzejUqVOF+9VoNNBoNGXK3dzcGACJiIhqGXuevlUrA2Bubi4SEhIs7xMTE3H06FF4enrC398fjz/+OOLi4rB161aYTCbLvD5PT0+o1Wo0a9YMvXv3xogRI7B8+XIYDAaMGTMGgwcP5h3AREREVOfVymVgfv75Z/Ts2bNMeXR0NF577TWEhISU+7nY2FjLczzT09MxZswYfPfdd1AoFBgwYACWLl0KFxeXSvcjOzsbOp0OWVlZHAEkIiKqJXj+rqUBsKbgHxAREVHtw/O3nawDSERERER/YwAkIiIisjMMgERERER2hgGQiIiIyM4wABIRERHZGQZAIiIiIjvDAEhERERkZxgAiYiIiOwMAyARERGRnWEAJCIiIrIzDIBEREREdoYBkIiIiMjOMAASERER2RkGQCIiIiI7wwBIREREZGcYAImIiIjsDAMgERERkZ1hACQiIiKyMwyARERERHaGAZCIiIjIzjAAEhEREdkZBkAiIiIiO8MASERERGRnGACJiIiI7AwDIBEREZGdYQAkIiIisjMMgERERER2hgGQiIiIyM4wABIRERHZGQZAIiIiIjvDAEhERERkZxgAiYiIiOwMAyARERGRnWEAJCIiIrIzDIBEREREdoYBkIiIiMjOMAASERER2RkGQCIiIiI7wwBIREREZGcYAImIiIjsDAMgERERkZ1hACQiIiKyM7UyAO7Zswf9+vVDQEAAJEnCli1brLYLITBr1iz4+/tDq9UiIiIC8fHxVnXS09MRFRUFNzc3uLu7Y/jw4cjNzbXhtyAiIiKqHrUyAObl5aF169b48MMPy92+aNEiLF26FMuXL8fBgwfh7OyMyMhIFBYWWupERUXh5MmT2LlzJ7Zu3Yo9e/Zg5MiRtvoKRERERNVGEkKI6u7EfyFJEjZv3oz+/fsDKB79CwgIwOTJkzFlyhQAQFZWFnx9fbF69WoMHjwYp0+fRvPmzXHo0CGEhYUBALZv346+ffvi0qVLCAgIqFTb2dnZ0Ol0yMrKgpubmyzfj4iIiKoWz9+1dATwdhITE5GamoqIiAhLmU6nQ6dOnbB//34AwP79++Hu7m4JfwAQEREBhUKBgwcPVrhvvV6P7OxsqxcRERFRbVPnAmBqaioAwNfX16rc19fXsi01NRU+Pj5W21UqFTw9PS11yrNgwQLodDrLKzAwsIp7T0RERCS/OhcA5TRjxgxkZWVZXhcvXqzuLhERERHdsToXAP38/AAAaWlpVuVpaWmWbX5+frh69arVdqPRiPT0dEud8mg0Gri5uVm9iIiIiGqbOhcAQ0JC4Ofnh127dlnKsrOzcfDgQYSHhwMAwsPDkZmZicOHD1vq7N69G2azGZ06dbJ5n4mIiIhsSVXdHfg3cnNzkZCQYHmfmJiIo0ePwtPTE0FBQZgwYQLmzp2L0NBQhISEYObMmQgICLDcKdysWTP07t0bI0aMwPLly2EwGDBmzBgMHjy40ncAExEREdVWtTIA/vHHH+jZs6fl/aRJkwAA0dHRWL16NaZOnYq8vDyMHDkSmZmZ6NatG7Zv3w5HR0fLZ2JiYjBmzBj06tULCoUCAwYMwNKlS23+XYiIiIhsrdavA1iduI4QERFR7cPzdx2cA0hEREREt8cASERERGRnGACJiIiI7AwDIBEREZGdYQAkIiIisjMMgERERER2hgGQiIiIyM4wABIRERHZGQZAIiIiIjvDAEhERERkZxgAiYiIiOwMAyARERGRnWEAJCIiIrIzDIBEREREdoYBkIiIiMjOqGzRSHJyMi5cuID8/HzUq1cPLVq0gEajsUXTRERERFSKbAEwKSkJy5Ytw/r163Hp0iUIISzb1Go17r33XowcORIDBgyAQsGBSCIiIiJbkSV5jRs3Dq1bt0ZiYiLmzp2LU6dOISsrC0VFRUhNTcW2bdvQrVs3zJo1C/fccw8OHTokRzeIiIiIqByyjAA6Ozvj/Pnz8PLyKrPNx8cH999/P+6//37Mnj0b27dvx8WLF9GhQwc5ukJEREREpUji1muzdEeys7Oh0+mQlZUFNze36u4OERERVQLP3za6C9hoNOKnn37Cxx9/jJycHADAlStXkJuba4vmiYiIiOgWst8FfOHCBfTu3RvJycnQ6/V44IEH4OrqioULF0Kv12P58uVyd4GIiIiIbiH7COD48eMRFhaGjIwMaLVaS/mjjz6KXbt2yd08EREREZUi+wjgr7/+it9++w1qtdqqPDg4GJcvX5a7eSIiIiIqRfYRQLPZDJPJVKb80qVLcHV1lbt5IiIiIipF9gD44IMPYsmSJZb3kiQhNzcXs2fPRt++feVunoiIiIhKkX0ZmEuXLiEyMhJCCMTHxyMsLAzx8fHw9vbGnj174OPjI2fzsuJt5ERERLUPz982WgfQaDRi/fr1+PPPP5Gbm4t27dohKirK6qaQ2oh/QERERLUPz982uAkEAFQqFYYMGWKLpoiIiIjoH9gkAMbHxyM2NhZXr16F2Wy22jZr1ixbdIGIiIiIbpI9AK5YsQKjRo2Ct7c3/Pz8IEmSZZskSQyARERERDYmewCcO3cu5s2bh2nTpsndFBERERFVguzLwGRkZGDgwIFyN0NERERElSR7ABw4cCB+/PFHuZshIiIiokqS/RJw48aNMXPmTBw4cACtWrWCg4OD1fZx48bJ3QUiIiIiuoXs6wCGhIRU3Lgk4fz583I2LyuuI0RERFT78PxtgxHAxMREuZsgIiIiojsg+xxAIiIiIqpZZBkBnDRpEt544w04Oztj0qRJt627ePFiObpARERERBWQJQAeOXIEBoPB8nNFbl0UmoiIiIhsQ/abQOoyTiIlIiKqfXj+5hxAIiIiIrsjyyXgxx57rNJ1N23aJEcXYDKZ8Nprr+GLL75AamoqAgICMHToULz66quWS89CCMyePRsrVqxAZmYmunbtimXLliE0NFSWPhERERHVBLIEQJ1OJ8du78jChQuxbNkyfP7552jRogX++OMPDBs2DDqdzrL49KJFi7B06VJ8/vnnCAkJwcyZMxEZGYlTp07B0dGxmr8BERERkTzq7BzAhx9+GL6+vli5cqWlbMCAAdBqtfjiiy8ghEBAQAAmT56MKVOmAACysrLg6+uL1atXY/Dgwf/YBucQEBER1T48f9fhOYBdunTBrl27cO7cOQDAsWPHsHfvXvTp0wdA8QLVqampiIiIsHxGp9OhU6dO2L9/f7n71Ov1yM7OtnoRERER1TayXAJu27ZtpZd4iYuLk6MLmD59OrKzs9G0aVMolUqYTCbMmzcPUVFRAIDU1FQAgK+vr9XnfH19LdtKW7BgAebMmSNLf4mIiIhsRZYA2L9/f8vPhYWF+Oijj9C8eXOEh4cDAA4cOICTJ0/ixRdflKN5AMCGDRsQExODdevWoUWLFjh69CgmTJiAgIAAREdH/6t9zpgxw2ph6+zsbAQGBlZVl4mIiIhsQpYAOHv2bMvPzz33HMaNG4c33nijTJ2LFy/K0TwA4KWXXsL06dMtc/latWqFCxcuYMGCBYiOjoafnx8AIC0tDf7+/pbPpaWloU2bNuXuU6PRQKPRyNZnIiIiIluQfQ7gxo0b8cwzz5QpHzJkCL7++mvZ2s3Pz4dCYf31lEolzGYzACAkJAR+fn7YtWuXZXt2djYOHjxoGakkIiIiqotkGQG8lVarxb59+8qsrbdv3z5Zl1rp168f5s2bh6CgILRo0QJHjhzB4sWL8eyzzwIofgzdhAkTMHfuXISGhlqWgQkICLC6hE1ERERU18geACdMmIBRo0YhLi4OHTt2BAAcPHgQn332GWbOnClbu++//z5mzpyJF198EVevXkVAQACef/55zJo1y1Jn6tSpyMvLw8iRI5GZmYlu3bph+/btXAOQiIiI6jSbrAO4YcMGvPfeezh9+jQAoFmzZhg/fjwGDRokd9Oy4jpCREREtQ/P3zKPABqNRsyfPx/PPvtsrQ97RERERHWFrDeBqFQqLFq0CEajUc5miIiIiOgOyH4XcK9evfDLL7/I3QwRERERVZLsN4H06dMH06dPx/Hjx9G+fXs4OztbbX/kkUfk7gIRERER3UL2m0BKr8Vn1bgkwWQyydm8rDiJlIiIqPbh+dsGI4AlCy8TERERUc0g+xxAIiIiIqpZZB8BBIC8vDz88ssvSE5ORlFRkdW2cePG2aILRERERHST7AHwyJEj6Nu3L/Lz85GXlwdPT09cv34dTk5O8PHxYQAkIiIisjHZLwFPnDgR/fr1Q0ZGBrRaLQ4cOIALFy6gffv2ePvtt+VunoiIiIhKkT0AHj16FJMnT4ZCoYBSqYRer0dgYCAWLVqEl19+We7miYiIiKgU2QOgg4ODZSkYHx8fJCcnAwB0Oh0uXrwod/NEREREVIrscwDbtm2LQ4cOITQ0FD169MCsWbNw/fp1rF27Fi1btpS7eSIiIiIqRbYRwEGDBiE9PR3z58+Hv78/AGDevHnw8PDAqFGjcO3aNXzyySdyNU9EREREFZBtBPDSpUto2bIlVqxYgYceeghA8SXg7du3y9UkEREREVWCbCOA+/btw8SJEzFw4EA899xzyM3NlaspIiIiIroDsj8L+MyZMxg2bBhSU1MxduxYqFTWg461eR1APkuQiIio9uH52wYBEAA+/fRTvPDCC/D397cKgJIk4fz583I3Lxv+AREREdU+PH/LfBdwWloannvuOezduxcrV65EdHS0nM0RERERUSXINgdw/fr1aNGiBQoKCnDs2DGGPyIiIqIaQrYAOHz4cMyePRs//fQTgoKC5GqGiIiIiO6QbJeAjx49itDQULl2T0RERET/kiwjgAcOHKh0+MvPz8fJkyfl6AYRERERlUOWAPj0008jMjISGzduRF5eXrl1Tp06hZdffhmNGjXC4cOH5egGEREREZVDlkvAp06dwrJly/Dqq6/iqaeewt13342AgAA4OjoiIyMDZ86cQW5uLh599FH8+OOPaNWqlRzdICIiIqJyyL4O4B9//IG9e/fiwoULKCgogLe3N9q2bYuePXvC09NTzqZlx3WEiIiIah+ev2VeBxAAwsLCEBYWJnczRERERFRJsi0DQ0REREQ1EwMgERERkZ1hACQiIiKyMwyARERERHZG9gC4Zs0a6PX6MuVFRUVYs2aN3M0TERERUSmyLwOjVCqRkpICHx8fq/IbN27Ax8cHJpNJzuZlxdvIiYiIah+ev20wAiiEgCRJZcovXboEnU4nd/NEREREVIps6wC2bdsWkiRBkiT06tULKtXfTZlMJiQmJqJ3795yNU9EREREFZAtAPbv3x8AcPToUURGRsLFxcWyTa1WIzg4GAMGDJCreSIiIiKqgGwBcPbs2QCA4OBgPPHEE3B0dJSrKSIiIiK6A7I/Ci46OlruJoiIiIjoDsgSAD08PMq98aM86enpcnSBiIiIiCogSwBcsmSJHLslIiIioiogSwDkZV8iIiKimkv2OYDJycm33R4UFCR3F4iIiIjoFrIvBB0cHIyQkJAKX3K6fPkyhgwZAi8vL2i1WrRq1Qp//PGHZbsQArNmzYK/vz+0Wi0iIiIQHx8va5+IiIiIqpvsI4BHjhyxem8wGHDkyBEsXrwY8+bNk63djIwMdO3aFT179sQPP/yAevXqIT4+Hh4eHpY6ixYtwtKlS/H5558jJCQEM2fORGRkJE6dOsVla4iIiKjOkv1ZwBX5/vvv8dZbb+Hnn3+WZf/Tp0/Hvn378Ouvv5a7XQiBgIAATJ48GVOmTAEAZGVlwdfXF6tXr8bgwYP/sQ0+S5CIiKj24fnbBpeAK9KkSRMcOnRItv1/++23CAsLw8CBA+Hj44O2bdtixYoVlu2JiYlITU1FRESEpUyn06FTp07Yv39/ufvU6/XIzs62ehERERHVNrIHwNKBKSsrC2fOnMGrr76K0NBQ2do9f/48li1bhtDQUOzYsQOjRo3CuHHj8PnnnwMAUlNTAQC+vr5Wn/P19bVsK23BggXQ6XSWV2BgoGz9JyIiIpKL7HMA3d3dyywKLYRAYGAg1q9fL1u7ZrMZYWFhmD9/PgCgbdu2OHHiBJYvX/6vl6mZMWMGJk2aZHmfnZ3NEEhERES1juwBMDY21uq9QqFAvXr10LhxY6hU8jXv7++P5s2bW5U1a9YMX3/9NQDAz88PAJCWlgZ/f39LnbS0NLRp06bcfWo0Gmg0Gnk6TERERGQjsgfAHj16yN1Eubp27YqzZ89alZ07dw4NGjQAAISEhMDPzw+7du2yBL7s7GwcPHgQo0aNsnV3iYiIiGxG9jmAn3/+Ob7//nvL+6lTp8Ld3R1dunTBhQsXZGt34sSJOHDgAObPn4+EhASsW7cOn3zyCUaPHg0AkCQJEyZMwNy5c/Htt9/i+PHjeOaZZxAQEID+/fvL1i8iIiKi6iZ7AJw/fz60Wi0AYP/+/fjggw+waNEieHt7Y+LEibK126FDB2zevBlffvklWrZsiTfeeANLlixBVFSUpc7UqVMxduxYjBw5Eh06dEBubi62b9/ONQCJiIioTpN9HUAnJyecOXMGQUFBmDZtGlJSUrBmzRqcPHkS9913H65duyZn87LiOkJERES1D8/fNhgBdHFxwY0bNwAAP/74Ix544AEAgKOjIwoKCuRunoiIiIhKkf0mkAceeADPPfcc2rZti3PnzqFv374AgJMnTyI4OFju5omIiIioFNlHAD/88EOEh4fj2rVr+Prrr+Hl5QUAOHz4MJ588km5myciIiKiUqrtWcB1AecQEBER1T48f9voWcC//vorhgwZgi5duuDy5csAgLVr12Lv3r22aJ6IiIiIbiF7APz6668RGRkJrVaLuLg46PV6AEBWVpblMW1EREREZDuyB8C5c+di+fLlWLFiBRwcHCzlXbt2RVxcnNzNExEREVEpsgfAs2fPonv37mXKdTodMjMz5W6eiIiIiEqRPQD6+fkhISGhTPnevXvRsGFDuZsnIiIiolJkD4AjRozA+PHjcfDgQUiShCtXriAmJgZTpkzBqFGj5G6eiIiIiEqRfSHo6dOnw2w2o1evXsjPz0f37t2h0WgwZcoUjB07Vu7miYiIiKgUm60DWFRUhISEBOTm5qJ58+ZwcXFBQUEBtFqtLZqXBdcRIiIiqn14/rbROoAAoFar0bx5c3Ts2BEODg5YvHgxQkJCbNU8EREREd0kWwDU6/WYMWMGwsLC0KVLF2zZsgUAsGrVKoSEhODdd9/FxIkT5WqeiIiIiCog2xzAWbNm4eOPP0ZERAR+++03DBw4EMOGDcOBAwewePFiDBw4EEqlUq7miYiIiKgCsgXAjRs3Ys2aNXjkkUdw4sQJ3HPPPTAajTh27BgkSZKrWSIiIiL6B7JdAr506RLat28PAGjZsiU0Gg0mTpzI8EdERERUzWQLgCaTCWq12vJepVLBxcVFruaIiIiIqJJkuwQshMDQoUOh0WgAAIWFhXjhhRfg7OxsVW/Tpk1ydYGIiIiIyiFbAIyOjrZ6P2TIELmaIiIiIqI7IFsAXLVqlVy7JiIiIqL/wGYLQRMRERFRzcAASERERGRnGACJiIiI7AwDIBEREZGdYQAkIiIisjOy3QV8q/j4eMTGxuLq1aswm81W22bNmmWLLhARERHRTbIHwBUrVmDUqFHw9vaGn5+f1aPgJEliACQiIiKyMdkD4Ny5czFv3jxMmzZN7qaIiIiIqBJknwOYkZGBgQMHyt0MEREREVWS7AFw4MCB+PHHH+VuhoiIiIgqSfZLwI0bN8bMmTNx4MABtGrVCg4ODlbbx40bJ3cXiIiIiOgWkhBCyNlASEhIxY1LEs6fPy9n87LKzs6GTqdDVlYW3Nzcqrs7REREVAk8f8s8AiiEwM8//wwfHx9otVo5myIiIiKiSpJ1DqAQAqGhobh06ZKczRARERHRHZA1ACoUCoSGhuLGjRtyNkNEREREd0D2u4DffPNNvPTSSzhx4oTcTRERERFRJch+E4iHhwfy8/NhNBqhVqvLzAVMT0+Xs3lZcRIpERFR7cPztw2WgVmyZIncTRARERHRHZA9AEZHR8vdBBERERHdAdkDYHJy8m23BwUFyd0FIiIiIrqF7DeBBAcHIyQkpMKXLbz55puQJAkTJkywlBUWFmL06NHw8vKCi4sLBgwYgLS0NJv0h4iIiKg6yT4CeOTIEav3BoMBR44cweLFizFv3jy5m8ehQ4fw8ccf45577rEqnzhxIr7//nts3LgROp0OY8aMwWOPPYZ9+/bJ3iciIiKi6iR7AGzdunWZsrCwMAQEBOCtt97CY489Jlvbubm5iIqKwooVKzB37lxLeVZWFlauXIl169bh/vvvBwCsWrUKzZo1w4EDB9C5c2fZ+kRERERU3WS/BFyRJk2a4NChQ7K2MXr0aDz00EOIiIiwKj98+DAMBoNVedOmTREUFIT9+/fL2iciIiKi6ib7CGB2drbVeyEEUlJS8NprryE0NFS2dtevX4+4uLhyQ2ZqairUajXc3d2tyn19fZGamlrhPvV6PfR6veV96e9GREREVBvIHgDd3d0hSZJVmRACgYGBWL9+vSxtXrx4EePHj8fOnTvh6OhYZftdsGAB5syZU2X7IyIiIqoOsgfA2NhYq/cKhQL16tVD48aNoVLJ0/zhw4dx9epVtGvXzlJmMpmwZ88efPDBB9ixYweKioqQmZlpNQqYlpYGPz+/Cvc7Y8YMTJo0yfI+OzsbgYGBsnwHIiIiIrnIHgAlSUKXLl3KhD2j0Yg9e/age/fuVd5mr169cPz4cauyYcOGoWnTppg2bRoCAwPh4OCAXbt2YcCAAQCAs2fPIjk5GeHh4RXuV6PRQKPRVHl/iYiIiGxJ9gDYs2dPpKSkwMfHx6o8KysLPXv2hMlkqvI2XV1d0bJlS6syZ2dneHl5WcqHDx+OSZMmwdPTE25ubhg7dizCw8N5BzARERHVebIHQCFEmTmAAHDjxg04OzvL3XyF3n33XSgUCgwYMAB6vR6RkZH46KOPqq0/RERERLYiCSGEHDsuWd/vm2++Qe/eva0unZpMJvz5559o0qQJtm/fLkfzNpGdnQ2dToesrCy4ublVd3eIiIioEnj+lnEEUKfTASgeAXR1dYVWq7VsU6vV6Ny5M0aMGCFX80RERERUAdkC4KpVqwAUPwt4ypQp1Xq5l4iIiIj+JvuTQGbPng2NRoOffvoJH3/8MXJycgAAV65cQW5urtzNExEREVEpst8EcuHCBfTu3RvJycnQ6/V44IEH4OrqioULF0Kv12P58uVyd4GIiIiIbiH7COD48eMRFhaGjIwMq3mAjz76KHbt2iV380RERERUiuwjgL/++it+++03qNVqq/Lg4GBcvnxZ7uaJiIiIqBTZRwDNZnO5iz1funQJrq6ucjdPRERERKXIHgAffPBBLFmyxPJekiTk5uZi9uzZ6Nu3r9zNExEREVEpsi0EXeLSpUuIjIyEEALx8fEICwtDfHw8vL29sWfPnjKPiKtNuJAkERFR7cPztw0CIAAYjUZ89dVXOHbsGHJzc9GuXTtERUVZ3RRSG/EPiIiIqPbh+dtGAbA8KSkpmDdvHj744IPqaL5K8A+IiIio9uH5W+a7gE+ePInY2Fio1WoMGjQI7u7uuH79OubNm4fly5ejYcOGcjZPREREROWQ7SaQb7/9Fm3btsW4cePwwgsvICwsDLGxsWjWrBlOnz6NzZs34+TJk3I1T0REREQVkC0Azp07F6NHj0Z2djYWL16M8+fPY9y4cdi2bRu2b9+O3r17y9U0EREREd2GbHMAdTodDh8+jMaNG8NkMkGj0WD79u2IiIiQo7lqwTkEREREtQ/P3zKOAObk5FgOqlKphFar5Zw/IiIiohpA1ptAduzYAZ1OB6D4iSC7du3CiRMnrOo88sgjcnaBiIiIiEqR7RKwQvHPg4uSJJX7mLjagkPIREREtQ/P3zKOAJrNZrl2TURERET/gezPAiYiIiKimoUBkIiIiMjOMAASERER2RkGQCIiIiI7wwBIREREZGdsEgAzMzPx6aefYsaMGUhPTwcAxMXF4fLly7ZonoiIiIhuIetC0ADw559/IiIiAjqdDklJSRgxYgQ8PT2xadMmJCcnY82aNXJ3gYiIiIhuIfsI4KRJkzB06FDEx8fD0dHRUt63b1/s2bNH7uaJiIiIqBTZA+ChQ4fw/PPPlym/6667kJqaKnfzRERERFSK7AFQo9EgOzu7TPm5c+dQr149uZsnIiIiolJkD4CPPPIIXn/9dRgMBgDFz/9NTk7GtGnTMGDAALmbJyIiIqJSZA+A77zzDnJzc+Hj44OCggL06NEDjRs3hqurK+bNmyd380RERERUiux3Aet0OuzcuRN79+7Fn3/+idzcXLRr1w4RERFyN01ERERE5ZCEEKK6O1FbZWdnQ6fTISsrC25ubtXdHSIiIqoEnr9tMAK4dOnScsslSYKjoyMaN26M7t27Q6lUyt0VIiIiIoINAuC7776La9euIT8/Hx4eHgCAjIwMODk5wcXFBVevXkXDhg0RGxuLwMBAubtDREREZPdkvwlk/vz56NChA+Lj43Hjxg3cuHED586dQ6dOnfDee+8hOTkZfn5+mDhxotxdISIiIiLYYA5go0aN8PXXX6NNmzZW5UeOHMGAAQNw/vx5/PbbbxgwYABSUlLk7EqV4xwCIiKi2ofnbxuMAKakpMBoNJYpNxqNlieBBAQEICcnR+6uEBERERFsEAB79uyJ559/HkeOHLGUHTlyBKNGjcL9998PADh+/DhCQkLk7goRERERwQYBcOXKlfD09ET79u2h0Wig0WgQFhYGT09PrFy5EgDg4uKCd955R+6uEBERERFsuA7gmTNncO7cOQBAkyZN0KRJE1s0KyvOISAiIqp9eP62wTIwJZo2bYqmTZvaqjkiIiIiqoBNAuClS5fw7bffIjk5GUVFRVbbFi9eLEubCxYswKZNm3DmzBlotVp06dIFCxcutBp5LCwsxOTJk7F+/Xro9XpERkbio48+gq+vryx9IiIiIqoJZA+Au3btwiOPPIKGDRvizJkzaNmyJZKSkiCEQLt27WRr95dffsHo0aPRoUMHGI1GvPzyy3jwwQdx6tQpODs7AwAmTpyI77//Hhs3boROp8OYMWPw2GOPYd++fbL1i4iIiKi6yT4HsGPHjujTpw/mzJkDV1dXHDt2DD4+PoiKikLv3r0xatQoOZu3uHbtGnx8fPDLL7+ge/fuyMrKQr169bBu3To8/vjjAIrnKTZr1gz79+9H586d/3GfnENARERU+/D8bYO7gE+fPo1nnnkGAKBSqVBQUAAXFxe8/vrrWLhwodzNW2RlZQEAPD09AQCHDx+GwWBARESEpU7Tpk0RFBSE/fv326xfRERERLYmewB0dna2zPvz9/fHX3/9Zdl2/fp1uZsHAJjNZkyYMAFdu3ZFy5YtAQCpqalQq9Vwd3e3quvr62tZoLo0vV6P7OxsqxcRERFRbSP7HMDOnTtj7969aNasGfr27YvJkyfj+PHj2LRpU6Uus1aF0aNH48SJE9i7d+9/2s+CBQswZ86cKuoVERERUfWQfQRw8eLF6NSpEwBgzpw56NWrF7766isEBwdbFoKW05gxY7B161bExsaifv36lnI/Pz8UFRUhMzPTqn5aWhr8/PzK3deMGTOQlZVleV28eFHOrhMRERHJQtYRQJPJhEuXLuGee+4BUHw5ePny5XI2aSGEwNixY7F582b8/PPPZR411759ezg4OGDXrl0YMGAAAODs2bNITk5GeHh4ufsseZIJERERUW0mawBUKpV48MEHcfr06TJz7eQ2evRorFu3Dt988w1cXV0t8/p0Oh20Wi10Oh2GDx+OSZMmwdPTE25ubhg7dizCw8NtdmmaiIiIqDrIPgewZcuWOH/+fJkROLktW7YMAHDfffdZla9atQpDhw4FALz77rtQKBQYMGCA1ULQRERERHWZ7OsAbt++HTNmzMAbb7yB9u3bWxZhLlGb19/hOkJERES1D8/fNgiACsXf95lIkmT5WQgBSZJgMpnkbF5W/AMiIiKqfXj+tsEl4NjYWLmbICIiIqI7IHsA7NGjh9xNEBEREdEdkH0dQAD49ddfMWTIEHTp0gWXL18GAKxdu/Y/L8xMRERERHdO9gD49ddfIzIyElqtFnFxcdDr9QCKn807f/58uZsnIiIiolJkD4Bz587F8uXLsWLFCjg4OFjKu3btiri4OLmbJyIiIqJSZA+AZ8+eRffu3cuU63S6Mo9hIyIiIiL5yR4A/fz8kJCQUKZ87969aNiwodzNExEREVEpsgfAESNGYPz48Th48CAkScKVK1cQExODKVOmYNSoUXI3T0RERESlyL4MzPTp02E2m9GrVy/k5+eje/fu0Gg0mDJlCsaOHSt380RERERUiuxPAilRVFSEhIQE5Obmonnz5nBxcbFFs7LiSuJERES1D8/fNrgE/MUXXyA/Px9qtRrNmzdHx44d60T4IyIiIqqtZA+AEydOhI+PD5566ils27atVj/7l4iIiKgukD0ApqSkYP369ZAkCYMGDYK/vz9Gjx6N3377Te6miYiIiKgcNpsDCAD5+fnYvHkz1q1bh59++gn169fHX3/9ZavmqxznEBAREdU+PH/b4C7gWzk5OSEyMhIZGRm4cOECTp8+bcvmiYiIiAg2uAQMFI/8xcTEoG/fvrjrrruwZMkSPProozh58qQtmiciIiKiW8g+Ajh48GBs3boVTk5OGDRoEGbOnInw8HC5myUiIiKiCsgeAJVKJTZs2IDIyEgolUqrbSdOnEDLli3l7gIRERER3UL2ABgTE2P1PicnB19++SU+/fRTHD58mMvCEBEREdmYTeYAAsCePXsQHR0Nf39/vP3227j//vtx4MABWzVPRERERDfJOgKYmpqK1atXY+XKlcjOzsagQYOg1+uxZcsWNG/eXM6miYiIiKgCso0A9uvXD02aNMGff/6JJUuW4MqVK3j//fflao6IiIiIKkm2EcAffvgB48aNw6hRoxAaGipXM0RERER0h2QbAdy7dy9ycnLQvn17dOrUCR988AGuX78uV3NEREREVEmyBcDOnTtjxYoVSElJwfPPP4/169cjICAAZrMZO3fuRE5OjlxNExEREdFt2PRZwGfPnsXKlSuxdu1aZGZm4oEHHsC3335rq+arHJ8lSEREVPvw/G3jAFjCZDLhu+++w2effcYASEREVAWEECgwmKBSKKBWFV/gM5sFjGYBsxAQAjCJ4p/NZgGzAIqMZhhMZjg6KCFJQGZ+EfL0JuQUGiEgoJQkmIRAbqERRSYz8otMyC00wmgW0KgU0DgooFEpi39WKaBxUMLx5v9qVAooFRJyCo3ILjRAbzAju9CA7AIDlAoJACABMN1MISqFBKVCgkohQaC4b4UGE8KCPdC+gWeVHiuev6spANYV/AMiInslhIDeaIZGpYDJLGAwCaRkFaDIZIYECU5qJdy0DoAALqTnIT2vCIUGM/RGE4qMxYHDQSnBLIBCgwmODkooFZJlv5n5BpiFgEqpgFKSUGgwwXzzdKU3mqGQJJjMZuiNZhhMAtmFBphMAgICOYVGaFQKODoo4eighMZBAUeVElp1cTgpMpmRpzfBSV283WAyw2gW0GkdoHVQwmQWyDeYkJlXhJITpFkIaB2UUCkVyNMbkZ5XhIz8ouLP3mxfo1JCe/N7mIXAjdwi3MjTw9XRAS6av++5FBCljmXpYwururl6I/L1xd/f0UGJ/CITCg0mqFUKmM0CeUUmGE1m5BtMls86qZWQAOQV1f6HLYy7vzEmPdikSvfJ87cNngRCRFRdhBC4nluEazl63MjT41JGARwdFHBWqywnfmeNCv46RwS4a+GgLH9atN5YPOqhVSuhUSmhkIpDSE6hEXl6IzLyi3A9twhGkxmmm0EhPa8I6XlFyC8yWUZjUrMKkas3wiwE8vRG6I1mmMwCapUCKoUEbxcNdFoHCAAFRSY4KBVwUErIyC+C0Vw8GmMWxWHEQSlBpVQgX188MqNWKqBQSDeDlgkeTmp4OKvh6aSGSikh+UY+JEmCWqVArr6434WG4hDk6+YIhSQhJasQeqPJEmqUCulm4DDiWo4e2Te/r1IhwWQuDmpqpQIGs7lMiKHqlV/J4KdUSFArFSg0FodHN0cVXDQquDiqoJAkCAFIEuCiUUGtUsBJrYSLRgWlojhI6w0m6I3FwV5vNENvuOXnm3/fLhoVXB1V0KgUcHV0gIeTA0yi+L9PAUApSZAkwGgWMJkEjGYzAMBBWRziWwe6y3eg7BgDIBHZXJHRjAs38qA3mqFVK3Ejt3gkBSgeufB20cBZo4KjQ3FYMd68RuTprIajgxJ5eiOOXszE1ZxCnEnJwamUbJjM4mZgKr4UdTEjH/FpuSgwVO5EqJCAAHctAj2c4OmixvUcPS7cyEdekRE5hUbZjkVtV3Tz9wYU/+6c1EoIAeTojSgyFm/zdtHAx1UDrVoJtbL48mShwQSTWUCSAEcH5S3viy8D1nPRFIcCU/ElTI2D4mYgKR4FA4p/Z8Ujbgq4OhYHFJNZwN3JwXL5sMBgQqGh+OdCgxmFRhPUyuIgU1BksrxXSBKyCw0oMJigVCigdVDAw0kNSbp5qVICCotMMJqLA76ni9oyYqhUSHB1VN0MQCaYb4ZhT2c1vJzVxSN4RSbc3FW5pFIbb33nolHBSa2EQiEhv6g4tGsdlNAbzVApJDhrio+Bs0YJZ7UKRpNAZkERhACcbwY3pUKCQgIUknTzdfPnm5dizeabYUxxm05SncIASHQbQogy/zDXVQVFJlzL0eOv67k4fikLOq0DWgS4wU/nCJ22+BLWvzkWhQYTfjqdhj3nruGva3kwmsw4dwfBrDSd1gF5+uI5SJUhSYCnkxoujio09HaGwXTzklqRERpVcZi8nFkAvdGMSxkFuJRRcEf9cdGo4KxRwtNZA+ebJ+lCgwkuGhW8XTRwdVRZQoz/zWMpSYBGpYBWrYKDUoLBJFBkNONajh45hQZIN4ONwSxgMJrhpnWAg1IBAQEJEpQKwGASMJqKg5HWQYkiY/FlTE9nB2jVKmTmF49AZty89Brk5QSFJKHIaIazRnlzREaJXL0RadmFkFAcWEraclBKKDIJ6A0mOKlV8HRWw8PZAc5qFUzm4tFBF42qeM6Zsnik0PWWv5GSS7kALIGNbEfn5HBH9RUMfnaHAZBqjPwiI3afuQoXjQpLforH0YuZcFBKaO7vhmOXstDc3w0PtvDFgHb14XBzFOHUlWzc5aFFkKcTFBJwKiUbbo4OyCow4P3d8fBxdYSHsxqHL6TjRm4Rgr2c0djHBe2DPRDs5YwQb2cAQE6hAQlXc5FVYEDS9TwcTEzHb3/dQK7eiEb1nFFoMKO5vxuUCglFJjM8ndTw0zmiYT1nNKrnAoUkQefkALNZwE9XfDmt+FKaKB6R0DpU6h9YIQRy9EY4qpQQENColBBCILvQCAigwGBCXpEReoMZro4qJFzNxY28IhQUGZGZb0BmgQF6o+nm3KOim5foik/UWrUSKVmFxRPBzQJu2uJLPJcyCpCSVYBCg/m2fVMqJOi0DpYRApVCwl3uWgCAWqWAj6sGadl6ZBUYYDAVz9GSJODCjfwKw567kwP0BjN83TSWy695eqOl7yUclMVtGkwCWQUGAEB9j+LRugZeTmgX5AG1SgGDqfiyU5HRjAB3LRr7uCDQUwuN6vYBRAiBazl6JKfn42JGPjLyDPBwdkCIt8vNIKeGq6MDCg3Fl0cNJmG5lGzvJ06PCsolSWLwI6rBeBPIf8BJpGXpjSbEp+WiRYBbpUaL3tpxBh/G/vWf21UppEqPCFUHJ7USfm6OECieY1PPVQNvl+JXRn4RUrIKkZ5XhOT0fKTnFVk+p1YpLJfRbEF5M9Q19nGBEAJnU3NwPa/oP/fBX+eIvq380SbQHRqVAkFeTrjbx/W24clkFsgvMlpClhACGfkGpOfp4eigRH0Pp//UJyKyXzx/cwSQ/qM8vREvbz6OXs18kVNowCubT1htf6pTEJ7tGoxvj6XgUkY+Ood4wcVRhVNXsvFBbMJt961SSGjg5YQujbyRll2IJn6u2JdwHXHJmWXqVhT+Hr7HH0eSM3E5s/iy3sjuDXH+Wi5+On31tm37uTli8aDWqOeqQeL1PMRfzUVmfhG0alXxZdKruQhwd8TZtFxcyykEAGTmG4ovsZnKhqX8IhPOX8+7bZvlKR28lAoJjqri+Up6kxnOaiUaeDnD100DDyc1dE4O0CgV8HLRwMtFXXyDgVKBHL0R+XojfNwcoVYqLMs6GEzFI2UB7o6o5+IIR7Wi3NGyQoMJWQUGZBUYUHRzwndBkRnXc/VQqxTQG01IzdLD09kB9VyLR/NMN5eeqO/hhEb1XO54blHxvKq/L2NJkgRPZzU8ndV3fByJiMgaRwD/A1v8PwizWWDQx/vxx4UM7J3W8x9HPYQQ6DDvJ1zPLcKW0V3RxNcVWnXVXoY5dSUbi3eexYs9G+Oxj36rkn0+1y0E3q4aPHyPP7xdNLe9dJRVYEBWvgGujsUjQ85qJa7l6hF3IRNhwR7QaR1wMT0fwd7OFd7VCQDXc/X4PTEdaqUCOicHtK7vbrlb898o+U/pRl4RlJIEpVKC5ub+4tNycS2nOCzl35xrdz1Xj2s5ehQYTAjxdsZd7loEuGvRxM8VRpPZsjxGocFkuUP1v/SPiIiKcQSQAfA/scUf0HfHrmDsl0cAAE18XbFjYvfb1n8x5jC2HU8td9uQzkH44kCy5X3/NgFoVM8FL/ZsXO7oTMLVHEQs3mN57+7kACFgmYNVkee6heDTvYlQSEB5A3NKhYTwhl64lJGPp8ODMbxbyG33R0REVJUYAHkJuMa7dT7Y2bSKn598/lou7n/nl9vu69bwBwBbjl4BALyz8xw+eKotHr4nANdz9Xhj6ym4OqrK1M/MLz/4DWxfH28NbI38IiMMpuLFVF99uDmA4nlc13L08HHVQKGQYDYXr1TvrOGfHhERUXXhWbiGiz1rPVdt3cFkPNUpyKrs+KUs9Ptgr1WZh5MDMioIbOUZs+4Ixqw7Uqm6XRt74ZOnw3DySjbMQqBzQy8AgJO67J+TUiHBT+doea9QSAx/RERE1Yxn4hru57PXrN5/GJuAh+7xh5vj3+ttjfkyzqrO7sk90LCei1VZ0vU8vLH1FKb3aQoBIMjTCefScpB4PQ/j1x+tsP273LXYN/1+AMXzEZNu5CHE2xmSJKFjSNU+m5GIiIhsg3MA/wNbzCHoMO8nXMvRV7j94Xv8sfXPFMv7xAV9/9VivVv/vGIZAXxnYGs8dI8/Eq/noamfq90shExERPaBcwA5AljjdWvsjc1HLmNol2Cs/i2pzPZbw9+OCd3/dVh7+J4APHxPgFVZM3/7/I+CiIiorrP7NSU+/PBDBAcHw9HREZ06dcLvv/9e3V2yUrKmXLDXPy9628TPVe7uEBERUR1g1wHwq6++wqRJkzB79mzExcWhdevWiIyMxNWrt18k2JbOXytePFitUuKrkZ0rrJcwr4+tukRERES1nF3PAezUqRM6dOiADz74AABgNpsRGBiIsWPHYvr06f/4eTnnEFxMz4feaEbE4uKlXYZ0DsLc/q2s6ry/Kx7bTqRi/YjOd/zgbyIiInvFOYB2PAewqKgIhw8fxowZMyxlCoUCERER2L9/f7mf0ev10Ov/viEjOztblr4JIXDvolirsjMpZdcAHNsrFGN7hcrSByIiIqq77PYS8PXr12EymeDr62tV7uvri9TU8p+ksWDBAuh0OssrMDBQlr6V9/SMcQx6REREVEXsNgD+GzNmzEBWVpbldfHiRVnaMZWTAO/y0MrSFhEREdkfu70E7O3tDaVSibS0NKvytLQ0+Pn5lfsZjUYDjUYje9/M5UzLVHAtPiIiIqoidjsCqFar0b59e+zatctSZjabsWvXLoSHh1djz8ofAdSo7PZXRURERFXMbkcAAWDSpEmIjo5GWFgYOnbsiCVLliAvLw/Dhg2r1n6VHgF0d3JAgDsvARMREVHVsOsA+MQTT+DatWuYNWsWUlNT0aZNG2zfvr3MjSG2ZjZbv3+igzw3mxAREZF9susACABjxozBmDFjqrsbVkylRgA5/4+IiIiqEieW1UCl5wCeuJxVTT0hIiKiuogBsAYqPQfw1/jr1dQTIiIiqosYAGug0iOA347pWk09ISIiorrI7ucA1kQlI4AalQJn5/ap5t4QERFRXcMRwBqo5C5gpYI3fxAREVHVYwCsgUruAlby7l8iIiKSAQNgDVQyB1DBEUAiIiKSAQNgDVQyB5CXgImIiEgODIA1UEkA5ALQREREJAcGwBrIcgmY+Y+IiIhkwABYA/EuYCIiIpITA2ANZOIlYCIiIpIRA2ANVHIJmCOAREREJAcGwBqIdwETERGRnBgAa6BCgwkAcDE9v5p7QkRERHURA2ANtHJvIgDAePNSMBEREVFVYgCsgf5IyqjuLhAREVEdxgBYAw1od1d1d4GIiIjqMAbAGqhFgA4A0K2xdzX3hIiIiOoiBsAaqOQuYEcH/nqIiIio6jFh1EAlt35IXAiaiIiIZMAAWAOZBZ8FTERERPJhAKyBSlZ/kcAESERERFWPAbAmujkCyCvAREREJAcGwBqoZA6gggmQiIiIZMAAWAOZ/74GTERERFTlGABrIMtdwNXaCyIiIqqrGABrIFEyAMhLwERERCQDBsAaiCOAREREJCcGwBpIcB1AIiIikhEDYA3ES8BEREQkJwbAGkjcvAjM+EdERERyYACsgQQnARIREZGMGABrIC4ETURERHJiAKyBzIKXgImIiEg+DIA10N83gVRvP4iIiKhuYgCswSSOARIREZEMGABrIMs6gPztEBERkQwYMWogc8ldIBwBJCIiIhkwANZAnANIREREcmIArIG4EDQRERHJiQGwBuIIIBEREcmpTgbApKQkDB8+HCEhIdBqtWjUqBFmz56NoqIiq3p//vkn7r33Xjg6OiIwMBCLFi2qph5bG3N/Y5x+vTdmPty8urtCREREdZCqujsghzNnzsBsNuPjjz9G48aNceLECYwYMQJ5eXl4++23AQDZ2dl48MEHERERgeXLl+P48eN49tln4e7ujpEjR1Zr/x2UCjgoq7ULREREVIdJQliePFunvfXWW1i2bBnOnz8PAFi2bBleeeUVpKamQq1WAwCmT5+OLVu24MyZM5XaZ3Z2NnQ6HbKysuDm5iZb34mIiKjq8PxdR0cAy5OVlQVPT0/L+/3796N79+6W8AcAkZGRWLhwITIyMuDh4VFmH3q9Hnq93mqfQPEfEhEREdUOJedtOxkDK5ddBMCEhAS8//77lsu/AJCamoqQkBCrer6+vpZt5QXABQsWYM6cOWXKAwMDq7jHREREJLecnBzodLrq7ka1qFUBcPr06Vi4cOFt65w+fRpNmza1vL98+TJ69+6NgQMHYsSIEf+p/RkzZmDSpEmW92azGenp6fDy8oJUxbfsZmdnIzAwEBcvXrTb4Wlb4HG2DR5n2+Bxtg0eZ9uQ8zgLIZCTk4OAgIAq3W9tUqsC4OTJkzF06NDb1mnYsKHl5ytXrqBnz57o0qULPvnkE6t6fn5+SEtLsyoree/n51fuvjUaDTQajVWZu7t7JXv/77i5ufEfGBvgcbYNHmfb4HG2DR5n25DrONvryF+JWhUA69Wrh3r16lWq7uXLl9GzZ0+0b98eq1atgqLUg3XDw8PxyiuvwGAwwMHBAQCwc+dONGnSpNzLv0RERER1RZ1cB/Dy5cu47777EBQUhLfffhvXrl1DamoqUlNTLXWeeuopqNVqDB8+HCdPnsRXX32F9957z+oSLxEREVFdVKtGACtr586dSEhIQEJCAurXr2+1reSOH51Ohx9//BGjR49G+/bt4e3tjVmzZlX7GoAlNBoNZs+eXeaSM1UtHmfb4HG2DR5n2+Bxtg0eZ3nZzTqARERERFSsTl4CJiIiIqKKMQASERER2RkGQCIiIiI7wwBIREREZGcYAGugDz/8EMHBwXB0dESnTp3w+++/V3eXaqwFCxagQ4cOcHV1hY+PD/r374+zZ89a1SksLMTo0aPh5eUFFxcXDBgwoMwi4MnJyXjooYfg5OQEHx8fvPTSSzAajVZ1fv75Z7Rr1w4ajQaNGzfG6tWr5f56Ndabb74JSZIwYcIESxmPc9W5fPkyhgwZAi8vL2i1WrRq1Qp//PGHZbsQArNmzYK/vz+0Wi0iIiIQHx9vtY/09HRERUXBzc0N7u7uGD58OHJzc63q/Pnnn7j33nvh6OiIwMBALFq0yCbfryYwmUyYOXMmQkJCoNVq0ahRI7zxxhtWz4blcb5ze/bsQb9+/RAQEABJkrBlyxar7bY8phs3bkTTpk3h6OiIVq1aYdu2bVX+fWs1QTXK+vXrhVqtFp999pk4efKkGDFihHB3dxdpaWnV3bUaKTIyUqxatUqcOHFCHD16VPTt21cEBQWJ3NxcS50XXnhBBAYGil27dok//vhDdO7cWXTp0sWy3Wg0ipYtW4qIiAhx5MgRsW3bNuHt7S1mzJhhqXP+/Hnh5OQkJk2aJE6dOiXef/99oVQqxfbt2236fWuC33//XQQHB4t77rlHjB8/3lLO41w10tPTRYMGDcTQoUPFwYMHxfnz58WOHTtEQkKCpc6bb74pdDqd2LJlizh27Jh45JFHREhIiCgoKLDU6d27t2jdurU4cOCA+PXXX0Xjxo3Fk08+admelZUlfH19RVRUlDhx4oT48ssvhVarFR9//LFNv291mTdvnvDy8hJbt24ViYmJYuPGjcLFxUW89957ljo8zndu27Zt4pVXXhGbNm0SAMTmzZutttvqmO7bt08olUqxaNEicerUKfHqq68KBwcHcfz4cdmPQW3BAFjDdOzYUYwePdry3mQyiYCAALFgwYJq7FXtcfXqVQFA/PLLL0IIITIzM4WDg4PYuHGjpc7p06cFALF//34hRPE/WAqFQqSmplrqLFu2TLi5uQm9Xi+EEGLq1KmiRYsWVm098cQTIjIyUu6vVKPk5OSI0NBQsXPnTtGjRw9LAORxrjrTpk0T3bp1q3C72WwWfn5+4q233rKUZWZmCo1GI7788kshhBCnTp0SAMShQ4csdX744QchSZK4fPmyEEKIjz76SHh4eFiOfUnbTZo0qeqvVCM99NBD4tlnn7Uqe+yxx0RUVJQQgse5KpQOgLY8poMGDRIPPfSQVX86deoknn/++Sr9jrUZLwHXIEVFRTh8+DAiIiIsZQqFAhEREdi/f3819qz2yMrKAgB4enoCAA4fPgyDwWB1TJs2bYqgoCDLMd2/fz9atWoFX19fS53IyEhkZ2fj5MmTljq37qOkjr39XkaPHo2HHnqozLHgca463377LcLCwjBw4ED4+Pigbdu2WLFihWV7YmIiUlNTrY6TTqdDp06drI61u7s7wsLCLHUiIiKgUChw8OBBS53u3btDrVZb6kRGRuLs2bPIyMiQ+2tWuy5dumDXrl04d+4cAODYsWPYu3cv+vTpA4DHWQ62PKb8t+SfMQDWINevX4fJZLI6QQKAr6+v1WPsqHxmsxkTJkxA165d0bJlSwBAamoq1Go13N3drereekxTU1PLPeYl225XJzs7GwUFBXJ8nRpn/fr1iIuLw4IFC8ps43GuOufPn8eyZcsQGhqKHTt2YNSoURg3bhw+//xzAH8fq9v9O5GamgofHx+r7SqVCp6ennf0+6jLpk+fjsGDB6Np06ZwcHBA27ZtMWHCBERFRQHgcZaDLY9pRXXs7ZjfTp18FBzZp9GjR+PEiRPYu3dvdXelzrl48SLGjx+PnTt3wtHRsbq7U6eZzWaEhYVh/vz5AIC2bdvixIkTWL58OaKjo6u5d3XHhg0bEBMTg3Xr1qFFixY4evQoJkyYgICAAB5nsgscAaxBvL29oVQqy9w5mZaWBj8/v2rqVe0wZswYbN26FbGxsVbPf/bz80NRUREyMzOt6t96TP38/Mo95iXbblfHzc0NWq22qr9OjXP48GFcvXoV7dq1g0qlgkqlwi+//IKlS5dCpVLB19eXx7mK+Pv7o3nz5lZlzZo1Q3JyMoC/j9Xt/p3w8/PD1atXrbYbjUakp6ff0e+jLnvppZcso4CtWrXC008/jYkTJ1pGuHmcq54tj2lFdeztmN8OA2ANolar0b59e+zatctSZjabsWvXLoSHh1djz2ouIQTGjBmDzZs3Y/fu3QgJCbHa3r59ezg4OFgd07NnzyI5OdlyTMPDw3H8+HGrf3R27twJNzc3y4k4PDzcah8ldezl99KrVy8cP34cR48etbzCwsIQFRVl+ZnHuWp07dq1zFJG586dQ4MGDQAAISEh8PPzszpO2dnZOHjwoNWxzszMxOHDhy11du/eDbPZjE6dOlnq7NmzBwaDwVJn586daNKkCTw8PGT7fjVFfn4+FArrU6BSqYTZbAbA4ywHWx5T/ltSCdV9FwpZW79+vdBoNGL16tXi1KlTYuTIkcLd3d3qzkn626hRo4ROpxM///yzSElJsbzy8/MtdV544QURFBQkdu/eLf744w8RHh4uwsPDLdtLlid58MEHxdGjR8X27dtFvXr1yl2e5KWXXhKnT58WH374od0tT1LarXcBC8HjXFV+//13oVKpxLx580R8fLyIiYkRTk5O4osvvrDUefPNN4W7u7v45ptvxJ9//in+97//lbuURtu2bcXBgwfF3r17RWhoqNVSGpmZmcLX11c8/fTT4sSJE2L9+vXCycmpzi5PUlp0dLS46667LMvAbNq0SXh7e4upU6da6vA437mcnBxx5MgRceTIEQFALF68WBw5ckRcuHBBCGG7Y7pv3z6hUqnE22+/LU6fPi1mz57NZWBKYQCsgd5//30RFBQk1Gq16Nixozhw4EB1d6nGAlDua9WqVZY6BQUF4sUXXxQeHh7CyclJPProoyIlJcVqP0lJSaJPnz5Cq9UKb29vMXnyZGEwGKzqxMbGijZt2gi1Wi0aNmxo1YY9Kh0AeZyrznfffSdatmwpNBqNaNq0qfjkk0+stpvNZjFz5kzh6+srNBqN6NWrlzh79qxVnRs3bognn3xSuLi4CDc3NzFs2DCRk5NjVefYsWOiW7duQqPRiLvuuku8+eabsn+3miI7O1uMHz9eBAUFCUdHR9GwYUPxyiuvWC0twuN852JjY8v9Nzk6OloIYdtjumHDBnH33XcLtVotWrRoIb7//nvZvndtJAlxy7LnRERERFTncQ4gERERkZ1hACQiIiKyMwyARERERHaGAZCIiIjIzjAAEhEREdkZBkAiIiIiO8MASERERGRnGACJyO4kJSVBkiQcPXpUtjaGDh2K/v37y7Z/IqL/ggGQiGqdoUOHQpKkMq/evXtX6vOBgYFISUlBy5YtZe4pEVHNpKruDhAR/Ru9e/fGqlWrrMo0Gk2lPqtUKuHn5ydHt4iIagWOABJRraTRaODn52f18vDwAABIkoRly5ahT58+0Gq1aNiwIf7v//7P8tnSl4AzMjIQFRWFevXqQavVIjQ01CpcHj9+HPfffz+0Wi28vLwwcuRI5ObmWrabTCZMmjQJ7u7u8PLywtSpU1H6KZtmsxkLFixASEgItFotWrdubdUnIiJbYgAkojpp5syZGDBgAI4dO4aoqCgMHjwYp0+frrDuqVOn8MMPP+D06dNYtmwZvL29AQB5eXmIjIyEh4cHDh06hI0bN+Knn37CmDFjLJ9/5513sHr1anz22WfYu3cv0tPTsXnzZqs2FixYgDVr1mD58uU4efIkJk6ciCFDhuCXX36R7yAQEVVEEBHVMtHR0UKpVApnZ2er17x584QQQgAQL7zwgtVnOnXqJEaNGiWEECIxMVEAEEeOHBFCCNGvXz8xbNiwctv65JNPhIeHh8jNzbWUff/990KhUIjU1FQhhBD+/v5i0aJFlu0Gg0HUr19f/O9//xNCCFFYWCicnJzEb7/9ZrXv4cOHiyeffPLfHwgion+JcwCJqFbq2bMnli1bZlXm6elp+Tk8PNxqW3h4eIV3/Y4aNQoDBgxAXFwcHnzwQfTv3x9dunQBAJw+fRqtW7eGs7OzpX7Xrl1hNptx9uxZODo6IiUlBZ06dbJsV6lUCAsLs1wGTkhIQH5+Ph544AGrdouKitC2bds7//JERP8RAyAR1UrOzs5o3LhxleyrT58+uHDhArZt24adO3eiV69eGD16NN5+++0q2X/JfMHvv/8ed911l9W2yt64QkRUlTgHkIjqpAMHDpR536xZswrr16tXD9HR0fjiiy+wZMkSfPLJJwCAZs2a4dixY8jLy7PU3bdvHxQKBZo0aQKdTgd/f38cPHjQst1oNOLw4cOW982bN4dGo0FycjIaN25s9QoMDKyqr0xEVGkcASSiWkmv1yM1NdWqTKVSWW7e2LhxI8LCwtCtWzfExMTg999/x8qVK8vd16xZs9C+fXu0aNECer0eW7dutYTFqKgozJ49G9HR0Xjttddw7do1jB07Fk8//TR8fX0BAOPHj8ebb76J0NBQNG3aFIsXL0ZmZqZl/66urpgyZQomTpwIs9mMbt26ISsrC/v27YObmxuio6NlOEJERBVjACSiWmn79u3w9/e3KmvSpAnOnDkDAJgzZw7Wr1+PF198Ef7+/vjyyy/RvHnzcvelVqsxY8YMJCUlQavV4t5778X69esBAE5OTtixYwfGjx+PDh06wMnJCQMGDMDixYstn588eTJSUlIQHR0NhUKBZ599Fo8++iiysrIsdd544w3Uq1cPCxYswPnz5+Hu7o527drh5ZdfrupDQ0T0jyQhSi1WRURUy0mShM2bN/NRbEREFeAcQCIiIiI7wwBIREREZGc4B5CI6hzObCEiuj2OABIRERHZGQZAIiIiIjvDAEhERERkZxgAiYiIiOwMAyARERGRnWEAJCIiIrIzDIBEREREdoYBkIiIiMjOMAASERER2Zn/By7yMNAMBCZ4AAAAAElFTkSuQmCC"}}},{"cell_type":"markdown","source":"# 4. Impacts\nVisualization: A development I am currently working on for FLAIR, is the visualization. Currently, it is not animated, but this is coming soon, and we will be able to watch the AI return. I wanted to include this, as it would be great to see the agent move around (I got stuck in the time crunch), but since we can see evidence of improvment of results, we know that there is some learning occurring.\n\nBroadcast: It would great to be able to put a viz in the broadcast (similar to RomoVision) and have the audience follow along and compare the AI to real returners.\n\nA New Statistic: If we say that FLAIR returns optimally, then we could compare a real returners return roucte, and decipher a Optimality Percentage of the player.\n\nCoaching: While not fully accurate to in-game scenarios, FLAIR, with more enhancements, could be a useful coaching tool, as players and coaches could work out patterns and trends that FLAIR finds.\n\n","metadata":{}},{"cell_type":"markdown","source":"# 5. Discussion\nIt is always good to include some conceptual discussion!\n\nI chose this model not only because of my familiarity with it, but also becuase the idea of practicing and storing memories translates nicely to the problem at hand. Having working with DQN's I was able to leverage some previous knowledge and tinker with the paramters a but to get them to work. Of course, this DQN can be trained more, and refined, which I am currently working on, what I show here is like a proof of concept.\n\nI should note that for something like this, we have a small data set. (I only used 1877 plays, selecting a random one for the agent to train on.) In the scale of Reinforcement learning this can be vary small.\n\nThere is a natural extension to have this become a multi-agent model: the opponents just stop moving after the scripted data ends! Having an AI play for them, as well as the teammates is a geat extension.\n\nI think what woudl be most effective is some reward shaping. Currently, I am just using the yardline of return to inform the agent, but I htink adding in things like broken tackles, avoiding and such would be usefull to.","metadata":{}},{"cell_type":"code","source":"#The Following is the code base for verification, it is not all ironed out yet.\n#Use with caution, and note the R and Python Code.\n\n# #Big Data Bowl\n# setwd(\"C:/Users/dcraw/OneDrive/Desktop/Data Analytics Bowl/nfl-big-data-bowl-2022\")\n# library(data.table)\n# plays = fread(\"plays.csv\")\n# plays = cbind(id = paste0(plays$gameId, plays$playId), plays)\n# \n# kickoff_plays = unique(plays$id[intersect(which(plays$specialTeamsPlayType == 'Kickoff'), which(plays$specialTeamsResult == \"Return\"))])\n# \n# tracking_file_names = c(\"tracking2018.csv\", \"tracking2019.csv\", \"tracking2020.csv\")\n# tracking_data = data.table()\n# \n# for(f in tracking_file_names){\n#   tracking_data = rbind(tracking_data, fread(f))\n# }\n# \n# tracking_data = cbind(id = paste0(tracking_data$gameId, tracking_data$playId),tracking_data)\n# \n# \n# kickoff_plays_tracking_data = tracking_data[which(tracking_data$id %in% kickoff_plays),]\n# \n# \n# #Only kick received plays\n# #get rid of punt\n# get_rid_of = function(df, s){\n#   plays = unique(df$id[which(df$event == s)])\n#   return(df[!which(df$id %in% plays),])\n# }\n# \n# #carve\n# df = get_rid_of(kickoff_plays_tracking_data, \"punt\")\n# df = get_rid_of(df, \"penalty_flag\")\n# df = get_rid_of(df, \"fumble\")\n# df = get_rid_of(df, \"handoff\")\n# df = get_rid_of(df, \"onside_kick\")\n# df = get_rid_of(df, \"drop_kick\")\n# df = get_rid_of(df, \"onside_kick\")\n# df = get_rid_of(df, \"out_of_bounds\")\n# df = get_rid_of(df, \"fair_catch\")\n# df = get_rid_of(df, \"kick_recovered\")\n# df = get_rid_of(df, \"lateral\")\n# df = get_rid_of(df, \"fumble_defense_recovered\")\n# df = get_rid_of(df, \"fumble_offense_recovered\")\n# df = get_rid_of(df, \"punt_downed\")\n# df = get_rid_of(df, \"punt_received\")\n# df = get_rid_of(df, \"autoevent_kickoff\")\n# df = get_rid_of(df, \"free_kick\")\n# \n# df_ordered = df[with(df, order(df$id, df$frame, df$displayName)),]\n# \n# fwrite(df_ordered, file = \"simple_kickoff_plays_ordered.csv\")\n\n#STEP 1: Read In and Format Data\n#Read in and manipulate the data\nimport torch\nimport csv\nimport random\nimport pandas as pd\nimport numpy as np\nfrom matplotlib import pyplot as plt\nimport matplotlib\nfrom itertools import groupby\nimport time\nimport torch.optim as optim\nfrom collections import deque, namedtuple\n\ndef index_of_closest(f, x, y):\n    cl_i = 0\n    football_coords = np.array([float(x),float(y)])\n    b = np.array([float(f[cl_i][2]), float(f[cl_i][3])])\n    cl_d = np.linalg.norm(football_coords - b)\n    for i in range(len(f)):\n        player_coords = np.array([ float(f[i][2]) , float(f[i][3])])\n        dist = np.linalg.norm(football_coords - player_coords)\n        if dist < cl_d:\n            cl_d = dist\n            cl_i = i\n    return cl_i              \n        \n\n\n\nfile = open('C:/Users/dcraw/OneDrive/Desktop/Data Analytics Bowl/nfl-big-data-bowl-2022/simple_kickoff_plays_ordered.csv')\n\nreader = csv.reader(file)\nd = []\nheader = next(reader)\nfor r in reader:\n    d.append(r)\n\na = [list(v) for l,v in groupby(sorted(d, key = lambda x:x[0]), lambda x: x[0])]\n\nfor p in a:\n    for f in p:\n            f[15] = int(f[15])\n\n\nplays_db = []\nfor e in a:\n    plays_db.append([list(v) for l,v in groupby(sorted(e, key = lambda x: x[15]), lambda x: x[15])])\n\n\n#Flip plays to go one direction, to the right, meaning play direction is 'left'\nfor play in plays_db:\n    if play[0][0][-1] == 'right':\n        for frame in play:\n            for d in frame:\n                #print(d[11])\n                if d[11] != 'football':\n                    d[2] = float(d[2])-120.0\n                    d[7] = 360.0 - float(d[7])\n                    d[8] = 360.0 - float(d[8])\n                    d[-1] = 'left'\n    \n#Id Returners\nreturners = {}\nfor play in plays_db:\n    #index (frame) of 'kick_received'\n    f = [x for x in play if x[0][9] == 'kick_received'][0]\n    xf, yf = f[-1][2], f[-1][3]\n    i = index_of_closest(f[0:22], xf, yf)\n    #print(i)\n    returners.update({play[0][i][0]: f[i][10]})\n\n\n#Frame when ball caught\nframe_when_received = {}\nfor play in plays_db:\n    for frame in play:\n        if frame[0][9] == \"kick_received\":\n            frame_when_received.update({frame[0][0]: int(frame[0][15])})\n            break\n\n\n#Order: [0]: returner, [1:10]: teammates, [11:21]: opponents, [22]: ball\nordered_plays_db = []\nfor play in plays_db:\n    ordered_play = []\n    returner_id = returners[play[0][0][0]]\n    for frame in play:\n        ordered_frame = []\n        if int(frame[0][15]) >= frame_when_received[frame[0][0]]:\n            is_of_returner = [i for i,x in enumerate(frame) if x[10] == returner_id]\n            i_r = is_of_returner[0]\n            ordered_frame.append(frame[i_r])\n\n            returner_team = frame[i_r][14]\n            returner_teammates = [x for i,x in enumerate(frame) if x[14] == returner_team]\n            returner_teammates.remove(frame[i_r])\n            for tm in returner_teammates:\n                ordered_frame.append(tm)\n\n            opposition_players = [x for i,x in enumerate(frame) if x[14] != returner_team]\n            for opp in opposition_players:\n                ordered_frame.append(opp)\n                    \n            ordered_play.append(ordered_frame)\n\n    ordered_plays_db.append(ordered_play)\n\n\n#STEP 2: Initiate environment\n#Set Up Environment\nimport numpy as np\nimport gym\n\ndef move(d,s,x,y):\n    new_x = float(x) + float((np.sin(np.radians(d))*s))\n    new_y = float(y) + float((np.cos(np.radians(d))*s))\n    return new_x, new_y\n\ndef distance(a,b,c,d,m):\n    return np.linalg.norm(np.array([a,b]) - np.array([c,c])) < m\n\ndef close(x1, y1, x2, y2, d):\n    a = np.array([float(x1),float(y1)])\n    b = np.array([float(x2),float(y2)])\n    return np.linalg.norm(a-b) <= d\n\nclass KickoffReturn_ENV(gym.Env):\n    \n    def __init__(self):\n        #self.action_space = gym.spaces.Box(\n        #    low = 0.0,\n        #    high = 359.99,\n        #    shape = (1,1))\n\n        #360 degrees\n        self.action_space = gym.spaces.Discrete(360)\n        \n        #22 players: 1 self, 10 teammates, 11 opponents\n        #each has (x, y, s, a, dis, o)\n        self.observation_space = gym.spaces.Box(\n            low = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),\n            high = np.array([120.0, 53.3, 100.0, 100.0, 10.0, 360.0]))\n        \n\n    def reset(self):\n        self.state = np.array([[0.0]*6]*22)\n        self.reward = 0\n        self.done = False\n        self.info = {}\n\n        self.steps = 0\n\n        return flatten_state(self.state)\n\n    def step(self, action, players):\n\n        players = players[:-1]\n        \n        if self.steps == 0:\n            state_copy = []\n            for p in players:\n                #print(p)\n                state_copy.append([float(d) for d in p[2:8]])\n            \n            self.state = state_copy\n            \n        spd = 0.8\n        \n        if self.done:\n            print('Episode Over')\n        else:\n            assert self.action_space.contains(action)\n            self.steps += 1\n\n\n            if len(self.state) != 22:\n                self.state = stack_state(self.state)\n            #move agent\n            agent_x = self.state[0][0]\n            agent_y = self.state[0][1]\n\n            #print(agent_x)\n            self.state[0][0] = float(move(float(action), spd, agent_x, agent_y)[0])\n            self.state[0][1] = float(move(float(action), spd, agent_x, agent_y)[1])\n\n            #move others\n            for i in range(21):\n                self.state[i+1] = players[i+1][2:8]\n            \n            tackle_d = 1.0\n            block_d = 0.5\n            #check if tackle\n            agent_state = self.state[0]\n            for opp_state in self.state[11:21]:\n                if close(agent_state[0],agent_state[1], opp_state[0], opp_state[1], tackle_d):\n                #check if block\n                    self.done = True\n                    #self.reward = self.state[0][0]\n                    for tm_state in self.state[1:10]:\n                        if close(tm_state[0],tm_state[1], opp_state[0], opp_state[1], block_d):\n                            self.done = False\n\n            self.reward = self.state[0][0]\n\n            state_copy = []\n            for p in self.state:\n                state_copy.append([float(d) for d in p])\n\n            #Check In bounds\n            if self.state[0][0] < 0.0 or self.state[0][0] > 100.0:\n                self.done = True\n\n            if self.state[0][1] < 0.0 or self.state[0][1] > 53.3:\n                self.done = True\n                \n            self.state = flatten_state(state_copy)\n\n            \n\n            \n            return self.state, self.reward, self.done, self.info\n\n    def render(self):\n        w = 100\n        h = 53.3\n        fig = plt.figure()\n        \n        ax = plt.axes(xlim = (-10, w + 30),\n                      ylim = (-15, h + 5))\n        \n        plt.axis('off')\n\n        #agent marker\n        \n\n        rect = matplotlib.patches.Rectangle(\n            (-10, -5),\n            w + 40,\n            h + 10,\n            linewidth = 1,\n            facecolor = '#037f51',\n            capstyle = 'round')\n        ax.add_patch(rect)\n\n        rect = plt.Rectangle(\n            (0,0),\n            w + 20,\n            h,\n            ec = 'w',\n            fc = 'none',\n            lw = 2)\n\n        ax.add_patch(rect)\n\n        ax.add_patch(plt.Circle((self.state[0][0], self.state[0][1]), 0.2, color='yellow'))\n\n        for tm in self.state[1:11]:\n            ax.add_patch(plt.Circle((float(tm[0]), float(tm[1])), 0.1, color='b'))\n\n        for op in self.state[11:23]:\n            ax.add_patch(plt.Circle((float(op[0]), float(op[1])), 0.1, color='r'))\n\n        for i in range(21):\n            plt.plot([10+5*i,10+5*i],\n                     [0,h],\n                     c = 'w',\n                     lw = 2)\n        \n        \n        fig.show()\n        #fig, ax\n        \n\n\n\ndef run_one_episode(env, play_data):\n    env.reset()\n    sum_r = 0\n\n    for i in range(len(play_data)):\n        action = env.action_space.sample()\n\n        action = agent.get_action(env.state)\n\n        players = play_data[i][0:22]\n        \n        s, r, d, i = env.step(action, players)\n\n        #sum_r += r\n        #env.render()\n        if d:\n            break\n\n    return r\n\ndef flatten_state(s):\n    return [float(x) for p in s for x in p]\n\ndef stack_state(s):\n    a = []\n    for i in range(22):\n        b = []\n        for j in range(6):\n            b.append(float(s[i*0+j]))\n        a.append(b)\n            \n    return a\n\n\n\nclass qnet(torch.nn.Module):\n    def __init__(self, state_size, action_size, hidden_n):\n        super(qnet, self).__init__()\n        self.layer1 = torch.nn.Linear(state_size,hidden_n)\n        self.layer2 = torch.nn.Linear(hidden_n,hidden_n)\n        self.layer3 = torch.nn.Linear(hidden_n, action_size)\n\n    def forward(self, x):\n        x = self.layer1(x)\n        x = torch.nn.functional.relu(x)\n        x = self.layer2(x)\n        x = torch.nn.functional.relu(x)\n        x = self.layer3(x)\n        return x\n\n\ndevice = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n\nclass Agent():\n    def __init__(self, state_size, action_size, learning_rate, batch_size, bank_size, hidden_n):\n        self.state_size = state_size\n        self.action_size = action_size\n\n        self.net = qnet(state_size, action_size, hidden_n).to(device)\n        self.target_net = qnet(state_size, action_size, hidden_n).to(device)\n        self.optimizer = torch.optim.Adam(self.net.parameters(), lr = learning_rate)\n\n        self.memories = ReplayBuffer(action_size, bank_size, batch_size)\n        self.t = 0\n\n    def step(self, state, action, r, next_state, terminated, tau, gamma, update_every, batch_size):\n        self.memories.add(state, action, r, next_state, terminated)\n        self.t += 1\n\n        if self.t % update_every == 0:\n            if len(self.memories) > batch_size:\n                mems_to_learn_from = self.memories.sample()\n                self.learn(mems_to_learn_from, gamma, tau)\n\n    def act(self, state, epsilon = 0):\n        state = torch.from_numpy(np.array(state)).float().unsqueeze(0).to(device)\n        self.net.eval()\n        with torch.no_grad():\n            action_values = self.net(state)\n        self.net.train()\n\n        if np.random.random() > epsilon:\n            return np.argmax(action_values.cpu().data.numpy())\n        else:\n            return np.random.choice(np.arange(self.action_size))\n\n    def learn(self, memories, gamma, tau):\n        states, actions, rs, next_states, terminateds = memories\n\n        #next_qtargets = self.target_net(next_states).detech().max(1)[0].unsqueeze(1)\n        next_qtargets = self.target_net(next_states).max(1)[0].unsqueeze(1)\n        target_val = rs + gamma * next_qtargets*(1-terminateds)\n        exp_val = self.net(states).gather(1,actions.type(torch.int64))\n\n        loss = torch.nn.functional.mse_loss(exp_val, target_val)\n        self.optimizer.zero_grad()\n        loss.backward()\n        self.optimizer.step()\n\n        self.update(self.net, self.target_net, tau)\n\n    def update(self, n, tn, tau):\n        for target_param, param in zip(tn.parameters(), n.parameters()):\n              target_param.data.copy_(tau * param.data + (1.0 - tau) * target_param.data)         \n\nclass ReplayBuffer:\n    def __init__(self, action_size, bank_size, batch_size):\n        self.batch_size = batch_size\n        self.action_size = action_size\n        self.memories = deque(maxlen=bank_size)\n        self.memory = namedtuple(\"Memory\", field_names=[\"state\",\"action\",\"reward\",\"next_state\",\"terminated\"])\n\n\n    def add(self, state, action, r, next_state, terminated):\n        self.memories.append(self.memory(state, action, r, next_state, terminated))\n\n\n    def sample(self):\n        sampled_mems = random.sample(self.memories, self.batch_size)\n\n        #print(torch.from_numpy(np.vstack([m.state for m in sampled_mems if m is not None])).float().to(device))\n\n        states = torch.from_numpy(np.vstack([m.state for m in sampled_mems if m is not None])).float().to(device)\n        actions = torch.from_numpy(np.vstack([m.action for m in sampled_mems if m is not None])).float().to(device)\n        rs = torch.from_numpy(np.vstack([m.reward for m in sampled_mems if m is not None])).float().to(device)\n        next_states = torch.from_numpy(np.vstack([m.next_state for m in sampled_mems if m is not None])).float().to(device)\n        terminateds = torch.from_numpy(np.vstack([m.terminated for m in sampled_mems if m is not None])).float().to(device)\n        \n        return (states, actions, rs, next_states, terminateds)\n\n    def __len__(self):\n        return len(self.memories)\n\n\n\n\ndef train_dqn(plays, num_eps, max_steps,\n              epsilon, epsilon_min, epsilon_decay,\n              tau, gamma, update_every, batch_size):\n\n    terminal_rewards = []\n\n    for ep_n in range(1, num_eps+1):\n\n        #ENV\n        env = KickoffReturn_ENV()\n        \n        play = random.choice(ordered_plays_db)\n        state = env.reset()\n        current_r = 0\n\n        for step_n in range(max_steps):\n            action_num = agent.act(state, epsilon)\n\n            if step_n < len(play):\n                players = play[step_n]\n            else:\n                players = play[-1]\n                \n            next_state, r, terminated, info = env.step(action_num, players)\n\n        \n            agent.step(state, action_num, r, next_state, terminated, tau, gamma, update_every, batch_size)\n            state = next_state\n            current_r = r\n            if terminated:\n                break\n        terminal_rewards.append(current_r)\n        epsilon = max(epsilon*epsilon_decay, epsilon_min)\n\n        if ep_n % 1000 == 0:\n            print(\"EPISODE:\", ep_n, \"   RETURNED TO:\", current_r)\n\n    return terminal_rewards\n\n#PARAMS\nNUMBER_OF_EPISODES = 100000\nMAX_NUMBER_STEPS = 5000 \nEPSILON = 1.0 \nEPSILON_MIN = 0.01 \nEPSILON_DECAY = 0.995 \nTAU = 0.01 \nGAMMA = 0.99 \nLEARNING_RATE = 0.0005 \nUPDATE_EVERY = 4 \nBATCH_SIZE = 64 \nBANK_SIZE = 100000\nNUM_HIDDEN_NODES = 360\n\n\n\nagent = Agent(132,360, LEARNING_RATE,\n              BATCH_SIZE, BANK_SIZE,NUM_HIDDEN_NODES)\n\n#def __init__(self, state_size, action_size,\n#             learning_rate, batch_size, bank_size, hidden_n):\n    \nscores = train_dqn(ordered_plays_db, NUMBER_OF_EPISODES, MAX_NUMBER_STEPS,\n                   EPSILON, EPSILON_MIN, EPSILON_DECAY, TAU,\n                   GAMMA, UPDATE_EVERY, BATCH_SIZE)\n\n\n\nplt.plot(range(len(scores)), scores)\nplt.title(\"FLAIR Returning Final Spot\")\nplt.xlabel(\"Episode\")\nplt.ylabel(\"Return Result (Yardline)\")\nplt.ylim(-20,120)\nplt.show()\n\navs = []\nfor i in range(len(scores)):\n    avs.append(np.array(scores[0:i]).mean())\n\nplt.plot(range(len(avs)), avs)\nplt.title(\"FLAIR Average Final Spot\")\nplt.xlabel(\"Episode\")\nplt.ylabel(\"Average Return Result (Yardline)\")\nplt.ylim(-20,120)\nplt.show()\n\navs100 = []\nfor j in range(len(scores)):\n    if j > 100:\n        avs100.append(np.array(scores[-100:-1]).mean())\n    \nplt.plot(range(len(avs100)), avs100)\nplt.title(\"FLAIR Average Final Spot of Previous 100 Returns\")\nplt.xlabel(\"Episode\")\nplt.ylabel(\"Average Return Result (Yardline)\")\nplt.ylim(-20,120)\nplt.show()\n","metadata":{},"execution_count":null,"outputs":[]}]}