diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c312ff4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ + +# Created by https://www.gitignore.io/api/python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +.pytest_cache/ +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule.* + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + + +# End of https://www.gitignore.io/api/python \ No newline at end of file diff --git a/Firewall Demo Animation.ipynb b/Firewall Demo Animation.ipynb new file mode 100644 index 0000000..84223e0 --- /dev/null +++ b/Firewall Demo Animation.ipynb @@ -0,0 +1,310 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook is used to construct an animated GIF illustrating \"challenge\" solved as part of this PyCon talk.\n", + "The challenge is from [Advent of Code 2017 Day 13](http://adventofcode.com/2017/day/13) (Part 2).\n", + "See the main notebook for a detailed description of the challenge." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import textwrap\n", + "import time\n", + "from io import BytesIO, StringIO" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# pillow docs: https://pillow.readthedocs.io/en/latest/\n", + "import PIL\n", + "from PIL import ImageDraw, ImageFont, ImageOps" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import clear_output, display, Image" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def test_input() -> dict:\n", + " \"\"\"The demonstration 'firewall'. Each row is a map of column position to height.\"\"\"\n", + " rows = StringIO(textwrap.dedent(\n", + " \"\"\"\\\n", + " 0: 3\n", + " 1: 2\n", + " 4: 4\n", + " 6: 4\n", + " \"\"\"))\n", + " return {layer: range_ for layer, range_ in (map(int, row.strip().split(': ')) for row in rows)}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "SCANNERS = test_input()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# Image size\n", + "SPACER = 4\n", + "SQUARE_SIZE = 100\n", + "\n", + "cols = 4 + max(SCANNERS.keys()) + 1\n", + "IMG_WIDTH = cols * SQUARE_SIZE + (cols - 1) * SPACER\n", + "\n", + "rows = 2 + max(SCANNERS.values())\n", + "IMG_HEIGHT = rows * SQUARE_SIZE + (rows - 1) * SPACER" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(1140, 620)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IMG_WIDTH, IMG_HEIGHT" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# Colors\n", + "SCANNER_PRESENT = (229, 80, 57)\n", + "SCANNER_ABSENT = (10, 61, 98)\n", + "\n", + "PACKET_FILL = (120, 224, 143)\n", + "\n", + "PATH_FILL = (106, 137, 204)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def py_scanner_layer(scanner_range: int, time_step: int) -> int:\n", + " \"\"\"Calculates the position of a scanner within its range at a given time step.\"\"\"\n", + " cycle_midpoint = scanner_range - 1\n", + " full_cycle = cycle_midpoint * 2\n", + " cycle_position = time_step % full_cycle \n", + " return cycle_position if cycle_position <= cycle_midpoint else full_cycle - cycle_position" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "def draw_scanner(draw: ImageDraw.Draw, col: int, range_: int, t: int):\n", + " \"\"\"Draw one 'scanner' within the firewall.\"\"\"\n", + " col_left = (SQUARE_SIZE + SPACER) * col\n", + " for row in range(1, range_ + 1):\n", + " row_top = (SQUARE_SIZE + SPACER) * row\n", + " scanner_layer = py_scanner_layer(range_, t)\n", + " if scanner_layer + 1 == row:\n", + " color = SCANNER_PRESENT\n", + " else:\n", + " color = SCANNER_ABSENT\n", + " draw.rectangle([(col_left, row_top), (col_left + SQUARE_SIZE, row_top + SQUARE_SIZE)], fill=color)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def draw_scanners(draw: ImageDraw.Draw, scanners: dict, t: int):\n", + " \"\"\"Draw all of the 'scanners' in the firewall.\"\"\"\n", + " for col, range_ in scanners.items():\n", + " draw_scanner(draw, col + 2, range_, t)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def draw_packet(draw: ImageDraw.Draw, t: int):\n", + " \"\"\"Draw the 'packet' at a given time step.\"\"\"\n", + " packet_offset = SQUARE_SIZE // 5\n", + " col_left = SQUARE_SIZE + SPACER\n", + " row_top = SQUARE_SIZE + SPACER\n", + " packet_pos = 1\n", + " if t >= 10:\n", + " packet_pos = t - 9 + 1\n", + " col_left = (SQUARE_SIZE + SPACER) * packet_pos\n", + " draw.ellipse(\n", + " [(col_left + packet_offset, row_top + packet_offset), \n", + " (col_left + SQUARE_SIZE - packet_offset, row_top + SQUARE_SIZE - packet_offset)],\n", + " fill=PACKET_FILL)\n", + " return packet_pos" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "def draw_path(draw: ImageDraw.Draw, packet_pos: int):\n", + " \"\"\"Draws a circle for each square in the path of our 'packet'.\"\"\"\n", + " path_offset = SQUARE_SIZE // 3\n", + " row_top = SQUARE_SIZE + SPACER + path_offset\n", + " row_bottom = SQUARE_SIZE + SPACER + SQUARE_SIZE - path_offset\n", + " for i in range(1, max(SCANNERS.keys()) + 4):\n", + " if i == packet_pos:\n", + " # don't draw a path circle at the current position of the packet\n", + " continue\n", + " col_left = (SQUARE_SIZE + SPACER) * i + path_offset\n", + " col_right = (SQUARE_SIZE + SPACER) * i + SQUARE_SIZE - path_offset\n", + " draw.ellipse([(col_left, row_top), (col_right, row_bottom)], fill=PATH_FILL)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABHQAAAJsCAIAAAATO2MtAAAVIUlEQVR4nO3cO4hcVQDH4d24hbohFoqEKCyIL7DUxhSR2KpFSjsljcQIgiIGBSsRtBF8FEJEQYRgkSaFhVXAlQjRxsIiCAFJFVMISSM4FheGxWw2M2f+93Hu/b5qNju7c7icMye/ObOzPpvN1gAAAFjNvr4HAAAAMAbiCgAAIEBcAQAABIgrAACAAHEFAAAQIK4AAAACxBUAAECAuAIAAAgQVwAAAAHiCgAAIEBcAQAABIgrAACAAHEFAAAQIK4AAAACxBUAAECAuAIAAAgQVwAAAAHiCgAAIEBcAQAABAw6rt5+++31HU6dOtX3iAAAAHa3XFzNZrMLFy688sor99xzTxM8Tz311K73vHTp0vrCfvvtt11/yV133bXHlwAAAMOxseD9zp49+8UXX/z888/Xrl2LD2J9fX3Xf7/zzjv3+BIAAGA4Fo2rH3/88fvvv29jBM8+++zjjz++67ecXAEAALVo62+uHn744dkt/P333w899FBzt62trTNnztxxxx27/pL77rtv55f33ntvS6MFAABY0aInV88999w8db777rtffvml+CFPnDjxxx9/rK2t7du379tvv/1fQe306KOP7vzyscceK35QAACAVi0aV0ePHj169Ghz+/fffy+Oq3Pnzn3zzTfN7TfeeOPw4cN73Pl/bxcUVwAAwGB1+lHsN27cOHnyZHP74MGD77333t73379//wMPPNDcfvDBB/fv39/u+AAAAEp1Gleffvrp5cuXm9vvvvvu5ubmbX9kfnh1qw+9AAAAGILu4urGjRsfffRRc/vAgQMvv/zyIj8lrgAAgCp0F1dnzpy5evVqc/vFF1+8++67F/kpcQUAAFRh0Q+0WN2XX345v/3CCy8s+FMnT56c/5kWAADAYHV0cvXXX39tb283tzc2Np555pluHhcAAKAbHcXVDz/88O+//za3H3nkEZ/7BwAAjExHcXXhwoX57SeeeKKbBwUAAOhMR3H166+/zm9vbW1186AAAACd6SiuLl26NL998ODBbh4UAACgMwvF1Ztvvrm+w9dffz3/1sWLF3d+688//7z5x//5558rV67Mv7z//vtXHzcAAMCgdHFydfXq1fmnWaytrR04cKCDBwUAAOhSF3F17dq1nV+KKwAAYHzWZ7NZ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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "backup = PIL.Image.new('RGB', (IMG_WIDTH, IMG_HEIGHT), 'white')\n", + "# Source Code Pro: https://github.com/adobe-fonts/source-code-pro\n", + "# Or select a truetype font available on your system\n", + "font = ImageFont.truetype('SourceCodePro-Bold.ttf', size=30)\n", + "\n", + "for t in range(0, 18 * 2):\n", + " frame = backup.copy()\n", + " draw = ImageDraw.Draw(frame)\n", + " \n", + " # Each time step is divided in two: in the first half the packet moves\n", + " # and in the second half the scanners move.\n", + " # This is implemented by moving the packet on even time steps\n", + " # and scanners on the odd time steps.\n", + " if t % 2 == 0:\n", + " # packet moves\n", + " draw_scanners(draw, SCANNERS, t // 2)\n", + " append = ''\n", + " else:\n", + " # scanners move (indicated with a ' marker on time step numbers)\n", + " draw_scanners(draw, SCANNERS, t // 2 + 1)\n", + " append = '\\''\n", + " \n", + " packet_pos = draw_packet(draw, t // 2)\n", + " draw_path(draw, packet_pos)\n", + " \n", + " # flip to put the path on the bottom\n", + " frame = ImageOps.flip(frame)\n", + " draw = ImageDraw.Draw(frame)\n", + " draw.text((10, 10), str(t // 2) + append, fill='black', font=font)\n", + " \n", + " # Display the image in the notebook to preview the animation\n", + " clear_output(wait=True)\n", + " display(frame)\n", + " time.sleep(1)\n", + " \n", + " frame.save(f'./images/frame{t // 2:02}{\"_\" if append else \"\"}.gif')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use [ImageMagick](https://www.imagemagick.org/script/index.php) to convert all of the frames to an animated gif with a spacing of 1 second.\n", + "(Run as a shell command.)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "!convert -delay 50 images/frame*.gif images/scanners.gif" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..96e1772 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Python Performance Investigation by Example + +Slides [here](./presentation/Python Performance Investigation - PyCon 2018.pdf). + +Use `snakeviz slow_mode.cprof` to see the SnakeViz profile demo'd in the talk. +(Requires Python 3.) diff --git a/Scanner Position Plot.ipynb b/Scanner Position Plot.ipynb new file mode 100644 index 0000000..055e08a --- /dev/null +++ b/Scanner Position Plot.ipynb @@ -0,0 +1,114 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This notebook creates the plot showing scanner position over time." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def py_scanner_layer(scanner_range: int, time_step: int) -> int:\n", + " \"\"\"Calculates the position of a scanner within its range at a given time step.\"\"\"\n", + " cycle_midpoint = scanner_range - 1\n", + " full_cycle = cycle_midpoint * 2\n", + " cycle_position = time_step % full_cycle \n", + " return cycle_position if cycle_position <= cycle_midpoint else full_cycle - cycle_position" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "SCANNER_DEPTH = 5\n", + "TIME_STEPS = np.arange((SCANNER_DEPTH - 1) * 2 + 1)\n", + "SCANNER_POS = np.int_([py_scanner_layer(SCANNER_DEPTH, t) for t in TIME_STEPS])" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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DRgwgNcYjxjIAWVlZWnUqFHatWuXJk6cKDPzHQkAkEEqKys1aNAgDR482HeUQKP0QWamSZMmaezYserXr5/vOACADGNmFL4UwOHdADt5FzuFDwCQDJ988onmzZunSCTiO0qgUPoCKhQK6bnnntO6det8RwEABMi+ffs0a9YsrVu3Tu+//77vOIHC4d2AWrZsmbZv367t27dr4MCBKisr8x0JABAAAwcO1Be/+EXt2bPn0wEeSA5KX0BdffXV2r17t4YOHUrhAwAk1Ze//GVJUk4ONSSZ+NcOqKKiIj300EO+YwAAAujksnf83L6sLM46SyT+dQOks7NToVDo08tZWVn8gQEAvGpubtasWbO0ePFi31EyHq/4AeGc0+9+9zs9++yzOnz4sO84AABIklauXKktW7boww8/VH19ve84GY3DuwGxZ88erV27VpK0ceNGTZw40XMiAACk6667Tjt37tT48eNVUVHhO05Gs3RdDqWystJVV1f7jpFWampqtHHjRt11112sugEASBnOOV6XzpKZrXLOVcZyW/b0Bcjo0aM1evRo3zEAADjByYXv0KFDLBiQAJzTl8Gcc9qzZ4/vGAAAxMQ5pw8//FC/+MUvPj0lCfFD6ctgH330kWbOnKmlS5d+Zsk1AABSjXNOmzdvViQS0YcffshrV5xxeDdDRSIRbdiwQVLXbnIAAFJdVlaW7rnnHr399tu6+eabOc8vzhjIkcFCoZDee+89TZw4Ufn5+b7jAACAOOvJQA4O72awnJwcTZkyhcIHAEhbbW1tqqur8x0jI1D6MszatWvV3NzsOwYAAOfswIEDmjlzpn79619r7969vuOkPUpfBtmyZYvmz5+vX/3qV9q/f7/vOAAAnJPCwkKFw2E559TQ0OA7TtpjIEcGaWtrU25urgoLC1VSUuI7DgAA56SgoED33XefIpGIzj//fN9x0h6lL4OMGTNGZWVlys7OVm5uru84AACcs0GDBvmOkDE4vJthysrK1L9/f98xAABIiDVr1mjnzp2+Y6QlSl+aq6mp0QcffMAElgCAjLds2TItWLBAL7/8slpaWnzHSTuUvjR2+PBhvfLKK1q0aJGWLVvmOw4AAAk1ZswY5eTkqLy83HeUtMQ5fWmsd+/euvDCC1VXV6fLL7/cdxwAABJq4MCB+s53vqOBAweyWsdZoPSlsby8PN1zzz06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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "with matplotlib.style.context('seaborn-talk'):\n", + " fig, ax = plt.subplots(figsize=None)\n", + " ax.plot(TIME_STEPS, SCANNER_POS, linestyle='dotted', color='gray')\n", + " ax.scatter(TIME_STEPS, SCANNER_POS, s=500, marker='s', zorder=10)\n", + " \n", + " ax.set_xlabel('Time')\n", + " ax.set_ylabel('Scanner Position')\n", + " ax.set_xticks(range(9))\n", + " ax.set_yticks(range(5))\n", + " \n", + " fig.savefig('./images/scanner_position.pdf')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/day13/input.txt b/day13/input.txt new file mode 100644 index 0000000..e911a64 --- /dev/null +++ b/day13/input.txt @@ -0,0 +1,43 @@ +0: 3 +1: 2 +2: 4 +4: 6 +6: 4 +8: 6 +10: 5 +12: 6 +14: 9 +16: 6 +18: 8 +20: 8 +22: 8 +24: 8 +26: 8 +28: 8 +30: 12 +32: 14 +34: 10 +36: 12 +38: 12 +40: 10 +42: 12 +44: 12 +46: 12 +48: 12 +50: 12 +52: 14 +54: 14 +56: 12 +62: 12 +64: 14 +66: 14 +68: 14 +70: 17 +72: 14 +74: 14 +76: 14 +82: 14 +86: 18 +88: 14 +96: 14 +98: 44 diff --git a/fast_mode.cprof b/fast_mode.cprof new file mode 100644 index 0000000..b092557 Binary files /dev/null and b/fast_mode.cprof differ diff --git a/fast_mode.py b/fast_mode.py new file mode 100644 index 0000000..a55647f --- /dev/null +++ b/fast_mode.py @@ -0,0 +1,46 @@ +import itertools + + +def puzzle_input() -> dict: + with open('./day13/input.txt') as f: + return { + layer: range_ + for layer, range_ in ( + map(int, row.strip().split(': ')) for row in f)} + + +def scanner_layer(scanner_height: int, time_step: int) -> int: + """ + Calculates the position of a scanner within its range at a + given time step. + + """ + cycle_midpoint = scanner_height - 1 + full_cycle = cycle_midpoint * 2 + cycle_position = time_step % full_cycle + return ( + cycle_position + if cycle_position <= cycle_midpoint + else full_cycle - cycle_position) + + +def check_capture(firewall: dict, num_layers: int, t_start: int) -> bool: + """Returns True if the packet is caught while crossing, otherwise False.""" + for pos in range(num_layers): + if pos in firewall: + scanner_height = firewall[pos] + scanner_pos = scanner_layer(scanner_height, t_start + pos) + if scanner_pos == 0: + return True + return False + + +def find_start(firewall: dict) -> int: + num_layers = max(firewall.keys()) + 1 + for t_start in itertools.count(0): + if not check_capture(firewall, num_layers, t_start): + break + return t_start + + +print(find_start(puzzle_input())) diff --git a/images/frame00.gif b/images/frame00.gif new file mode 100644 index 0000000..975747e Binary files /dev/null and b/images/frame00.gif differ diff --git a/images/frame00_.gif b/images/frame00_.gif new file mode 100644 index 0000000..aea6997 Binary files /dev/null and b/images/frame00_.gif differ diff --git a/images/frame01.gif b/images/frame01.gif new file mode 100644 index 0000000..e9986c9 Binary files /dev/null and b/images/frame01.gif differ diff --git a/images/frame01_.gif b/images/frame01_.gif new file mode 100644 index 0000000..35e8334 Binary files /dev/null and b/images/frame01_.gif differ diff --git a/images/frame02.gif b/images/frame02.gif new file mode 100644 index 0000000..1984f33 Binary files /dev/null and b/images/frame02.gif differ diff --git a/images/frame02_.gif b/images/frame02_.gif new file mode 100644 index 0000000..e366450 Binary files /dev/null and b/images/frame02_.gif differ diff --git a/images/frame03.gif b/images/frame03.gif new file mode 100644 index 0000000..7b37d53 Binary files /dev/null and b/images/frame03.gif differ diff --git a/images/frame03_.gif b/images/frame03_.gif new file mode 100644 index 0000000..7016759 Binary files /dev/null and b/images/frame03_.gif differ diff --git a/images/frame04.gif b/images/frame04.gif new file mode 100644 index 0000000..a6cee70 Binary files /dev/null and b/images/frame04.gif differ diff --git a/images/frame04_.gif b/images/frame04_.gif new file mode 100644 index 0000000..4bc2258 Binary files /dev/null and b/images/frame04_.gif differ diff --git a/images/frame05.gif b/images/frame05.gif new file mode 100644 index 0000000..204ff2f Binary files /dev/null and b/images/frame05.gif differ diff --git a/images/frame05_.gif b/images/frame05_.gif new file mode 100644 index 0000000..6b16d25 Binary files /dev/null and b/images/frame05_.gif differ diff --git a/images/frame06.gif b/images/frame06.gif new file mode 100644 index 0000000..74eb253 Binary files /dev/null and b/images/frame06.gif differ diff --git a/images/frame06_.gif b/images/frame06_.gif new file mode 100644 index 0000000..6531e08 Binary files /dev/null and b/images/frame06_.gif differ diff --git a/images/frame07.gif b/images/frame07.gif new file mode 100644 index 0000000..278f164 Binary files /dev/null and b/images/frame07.gif differ diff --git a/images/frame07_.gif b/images/frame07_.gif new file mode 100644 index 0000000..fa03f79 Binary files /dev/null and b/images/frame07_.gif differ diff --git a/images/frame08.gif b/images/frame08.gif new file mode 100644 index 0000000..3a0a2f1 Binary files /dev/null and b/images/frame08.gif differ diff --git a/images/frame08_.gif b/images/frame08_.gif new file mode 100644 index 0000000..04aa0c9 Binary files /dev/null and b/images/frame08_.gif differ diff --git a/images/frame09.gif b/images/frame09.gif new file mode 100644 index 0000000..5d52c53 Binary files /dev/null and b/images/frame09.gif differ diff --git a/images/frame09_.gif b/images/frame09_.gif new file mode 100644 index 0000000..2e0e781 Binary files /dev/null and b/images/frame09_.gif differ diff --git a/images/frame10.gif b/images/frame10.gif new file mode 100644 index 0000000..401b640 Binary files /dev/null and b/images/frame10.gif differ diff --git a/images/frame10_.gif b/images/frame10_.gif new file mode 100644 index 0000000..43a110a Binary files /dev/null and b/images/frame10_.gif differ diff --git a/images/frame11.gif b/images/frame11.gif new file mode 100644 index 0000000..c05bd04 Binary files /dev/null and b/images/frame11.gif differ diff --git a/images/frame11_.gif b/images/frame11_.gif new file mode 100644 index 0000000..e4322f3 Binary files /dev/null and b/images/frame11_.gif differ diff --git a/images/frame12.gif b/images/frame12.gif new file mode 100644 index 0000000..ec4205b Binary files /dev/null and b/images/frame12.gif differ diff --git a/images/frame12_.gif b/images/frame12_.gif new file mode 100644 index 0000000..3bd982f Binary files /dev/null and b/images/frame12_.gif differ diff --git a/images/frame13.gif b/images/frame13.gif new file mode 100644 index 0000000..7237bfc Binary files /dev/null and b/images/frame13.gif differ diff --git a/images/frame13_.gif b/images/frame13_.gif new file mode 100644 index 0000000..e6a8975 Binary files /dev/null and b/images/frame13_.gif differ diff --git a/images/frame14.gif b/images/frame14.gif new file mode 100644 index 0000000..c94845c Binary files /dev/null and b/images/frame14.gif differ diff --git a/images/frame14_.gif b/images/frame14_.gif new file mode 100644 index 0000000..4a25884 Binary files /dev/null and b/images/frame14_.gif differ diff --git a/images/frame15.gif b/images/frame15.gif new file mode 100644 index 0000000..cd81e8a Binary files /dev/null and b/images/frame15.gif differ diff --git a/images/frame15_.gif b/images/frame15_.gif new file mode 100644 index 0000000..a66578a Binary files /dev/null and b/images/frame15_.gif differ diff --git a/images/frame16.gif b/images/frame16.gif new file mode 100644 index 0000000..8bb4891 Binary files /dev/null and b/images/frame16.gif differ diff --git a/images/frame16_.gif b/images/frame16_.gif new file mode 100644 index 0000000..457c239 Binary files /dev/null and b/images/frame16_.gif differ diff --git a/images/frame17.gif b/images/frame17.gif new file mode 100644 index 0000000..9441d08 Binary files /dev/null and b/images/frame17.gif differ diff --git a/images/frame17_.gif b/images/frame17_.gif new file mode 100644 index 0000000..ae9565f Binary files /dev/null and b/images/frame17_.gif differ diff --git a/images/scanner_position.pdf b/images/scanner_position.pdf new file mode 100644 index 0000000..fb544b8 Binary files /dev/null and b/images/scanner_position.pdf differ diff --git a/images/scanners.gif b/images/scanners.gif new file mode 100644 index 0000000..5e206e3 Binary files /dev/null and b/images/scanners.gif differ diff --git a/images/snakeviz_screenshot.png b/images/snakeviz_screenshot.png new file mode 100644 index 0000000..8934e96 Binary files /dev/null and b/images/snakeviz_screenshot.png differ diff --git a/presentation/Python Performance Investigation - PyCon 2018.key b/presentation/Python Performance Investigation - PyCon 2018.key new file mode 100644 index 0000000..7558a95 Binary files /dev/null and b/presentation/Python Performance Investigation - PyCon 2018.key differ diff --git a/presentation/Python Performance Investigation - PyCon 2018.pdf b/presentation/Python Performance Investigation - PyCon 2018.pdf new file mode 100644 index 0000000..4453435 Binary files /dev/null and b/presentation/Python Performance Investigation - PyCon 2018.pdf differ diff --git a/slow_mode.cprof b/slow_mode.cprof new file mode 100644 index 0000000..95c054f Binary files /dev/null and b/slow_mode.cprof differ diff --git a/slow_mode.py b/slow_mode.py new file mode 100644 index 0000000..6865376 --- /dev/null +++ b/slow_mode.py @@ -0,0 +1,94 @@ +import itertools +from typing import Dict, Iterator + + +class Scanner: + """Holds the state of a single scanner.""" + def __init__(self, layer: int, range_: int): + self.layer = layer + self.range_ = range_ + self.pos = 0 + self.dir_ = 'down' + + def advance(self): + """Advance this scanner one time step.""" + if self.dir_ == 'down': + self.pos += 1 + if self.pos == self.range_ - 1: + self.dir_ = 'up' + else: + self.pos -= 1 + if self.pos == 0: + self.dir_ = 'down' + + def copy(self): + """Make a copy of this scanner in the same state.""" + inst = self.__class__(self.layer, self.range_) + inst.pos = self.pos + inst.dir_ = self.dir_ + return inst + + +def init_firewall(rows: Iterator[str]) -> Dict[int, Scanner]: + """Create a dictionary of scanners from the puzzle input.""" + firewall = {} + for row in rows: + layer, range_ = (int(x) for x in row.split(': ')) + firewall[layer] = Scanner(layer, range_) + return firewall + + +def puzzle_input() -> Dict[int, Scanner]: + """Helper for loading puzzle input.""" + with open('./day13/input.txt') as f: + return init_firewall(f) + + +def check_capture( + firewall: Dict[int, Scanner], num_layers: int) -> bool: + """Returns True if the packet is caught while crossing, otherwise False.""" + for packet_pos in range(num_layers): + # check if the scanner is captured in this position + if packet_pos in firewall and firewall[packet_pos].pos == 0: + return True + + # advance scanners to their next positions + for scanner in firewall.values(): + scanner.advance() + + return False + + +def copy_firewall( + firewall: Dict[int, Scanner]) -> Dict[int, Scanner]: + """Make a copy of a firewall dictionary""" + return { + layer: scanner.copy() for layer, scanner in firewall.items() + } + + +def find_start(firewall: Dict[int, Scanner]) -> int: + """Attempt crossing until we make it uncaught.""" + loop_firewall = copy_firewall(firewall) + num_layers = max(firewall.keys()) + 1 + + for t_start in itertools.count(0): + # save the state of the firewall before we start to cross + # so we can use it as the basis of a potential next attempt + pre_check_firewall = copy_firewall(loop_firewall) + + # check if the packet is caught while attempting a crossing starting + # at this time step + if check_capture(loop_firewall, num_layers): + # reset to pre-check state and advance once + # so we can attempt a crossing at the next timestep + loop_firewall = copy_firewall(pre_check_firewall) + for scanner in loop_firewall.values(): + scanner.advance() + else: + break + + return t_start + + +print(find_start(puzzle_input()))