diff --git a/.gitignore b/.gitignore index 4dbbd1d..641c15d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ target/ # ipython notebook .ipynb_checkpoints + +# python virtual env +venv/ \ No newline at end of file diff --git a/README.md b/README.md index 8cffd80..c942773 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,23 @@ flow: You can run the tests locally using pytest. Take a look at the `test`-folder +You can follow below steps to setup your virtual environment and run the tests. + +```bash +# Go to repo +cd python-pathfinding + +# Setup virtual env and activate it - Mac/Linux for windows use source venv/Scripts/activate +python3 -m venv venv +source venv/bin/activate + +# Install test requirements +pip install -r test/requirements.txt + +# Run all the tests +pytest +``` + ## Contributing Please use the [issue tracker](https://github.com/brean/python-pathfinding/issues) to submit bug reports and feature requests. Please use merge requests as described [here](/CONTRIBUTING.md) to add/adapt functionality. diff --git a/pathfinding.ipynb b/notebooks/pathfinding.ipynb similarity index 100% rename from pathfinding.ipynb rename to notebooks/pathfinding.ipynb diff --git a/notebooks/performance.ipynb b/notebooks/performance.ipynb new file mode 100644 index 0000000..ed7a541 --- /dev/null +++ b/notebooks/performance.ipynb @@ -0,0 +1,112 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Simple notebook showing one performance test result in plot." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy\n", + "from pathfinding.core.diagonal_movement import DiagonalMovement\n", + "from pathfinding.core.grid import Grid\n", + "from pathfinding.finder.a_star import AStarFinder\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "150801\n", + "[, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , ]\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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cAEalAyvqamJ6dOnx5YtWyrHent7Y8uWLVEulwdjSQAAwAVu0N72tmTJkli4cGHMmDEjbrzxxviHf/iHOHLkSOXubwAAAGfToMXPZz/72XjzzTdj2bJl0d7eHtddd108++yz77oJAgAAwNkwqDc8WLx4cSxevHgwlwAAACTh1hsAAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABSED8AAEAK4gcAAEhB/AAAACmIHwAAIAXxAwAApCB+AACAFMQPAACQgvgBAABS6Hf8bNu2LT7zmc/EpEmTYtiwYfHd7363z3hRFLFs2bL4wAc+EBdddFHMmTMnfv7zn/eZ89Zbb8WCBQuiVCrFuHHj4s4774y33377jE4EAADgvfQ7fo4cORLXXnttrF69+pTjK1eujFWrVsWaNWtix44dMXr06GhqaoqjR49W5ixYsCD27NkTmzdvjo0bN8a2bdvirrvuOv2zAAAAeB/DiqIoTvvJw4bFhg0b4tZbb42I3171mTRpUnz5y1+Ov/7rv46IiM7Ozqivr4+1a9fG5z73ufjpT38ajY2NsXPnzpgxY0ZERDz77LPxqU99Kv77v/87Jk2a9L6v29XVFXV1dfHrn30oSmO9cw8A4HzUNOm6iIh47sCuQV0HF7auw71x8Ud+EZ2dnVEqld5z7lkth9deey3a29tjzpw5lWN1dXUxc+bMaG1tjYiI1tbWGDduXCV8IiLmzJkTVVVVsWPHjlN+3+7u7ujq6urzAAAA6I+zGj/t7e0REVFfX9/neH19fWWsvb09Jk6c2Ge8uro6xo8fX5nzTsuXL4+6urrKY8qUKWdz2QAAQAJD4j1jS5cujc7Ozspj//79g70kAABgiDmr8dPQ0BARER0dHX2Od3R0VMYaGhri4MGDfcaPHz8eb731VmXOO9XW1kapVOrzAAAA6I+zGj/Tpk2LhoaG2LJlS+VYV1dX7NixI8rlckRElMvlOHToULS1tVXmbN26NXp7e2PmzJlnczkAAAAV1f19wttvvx3/+Z//Wfn6tddei127dsX48eNj6tSpce+998bf//3fx+WXXx7Tpk2Lr371qzFp0qTKHeGuvPLK+OQnPxlf/OIXY82aNXHs2LFYvHhxfO5zn/s/3ekNAADgdPQ7fl588cX4kz/5k8rXS5YsiYiIhQsXxtq1a+Nv/uZv4siRI3HXXXfFoUOH4hOf+EQ8++yzMXLkyMpzvvWtb8XixYtj9uzZUVVVFfPnz49Vq1adhdMBAAA4tTP6nJ/B4nN+AADOfz7nh4EwaJ/zAwAAcL4SPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJBCv+Jn+fLl8bGPfSzGjh0bEydOjFtvvTX27t3bZ87Ro0ejubk5JkyYEGPGjIn58+dHR0dHnzn79u2LefPmxahRo2LixIlx//33x/Hjx8/8bAAAAH6PfsVPS0tLNDc3x/PPPx+bN2+OY8eOxdy5c+PIkSOVOffdd18888wzsX79+mhpaYkDBw7EbbfdVhk/ceJEzJs3L3p6emL79u3x5JNPxtq1a2PZsmVn76wAAADeYVhRFMXpPvnNN9+MiRMnRktLS/zhH/5hdHZ2xqWXXhpPPfVU3H777RER8eqrr8aVV14Zra2tMWvWrNi0aVN8+tOfjgMHDkR9fX1ERKxZsya+8pWvxJtvvhk1NTXv+7pdXV1RV1cXv/7Zh6I01jv3AADOR02TrouIiOcO7BrUdXBh6zrcGxd/5BfR2dkZpVLpPedWn8kLdXZ2RkTE+PHjIyKira0tjh07FnPmzKnMueKKK2Lq1KmV+GltbY2rr766Ej4REU1NTbFo0aLYs2dPXH/99e96ne7u7uju7v7/J9jVdSbLBjgvnfwlAc41v4gCWZ32ZZPe3t6499574+Mf/3hcddVVERHR3t4eNTU1MW7cuD5z6+vro729vTLnd8Pn5PjJsVNZvnx51NXVVR5Tpkw53WUDAABJnXb8NDc3x09+8pNYt27d2VzPKS1dujQ6Ozsrj/3795/z1wQAAC4sp/W2t8WLF8fGjRtj27ZtMXny5MrxhoaG6OnpiUOHDvW5+tPR0RENDQ2VOS+88EKf73fybnAn57xTbW1t1NbWns5SAQAAIqKfV36KoojFixfHhg0bYuvWrTFt2rQ+49OnT48RI0bEli1bKsf27t0b+/bti3K5HBER5XI5du/eHQcPHqzM2bx5c5RKpWhsbDyTcwEAAPi9+nXlp7m5OZ566ql4+umnY+zYsZW/0amrq4uLLroo6urq4s4774wlS5bE+PHjo1Qqxd133x3lcjlmzZoVERFz586NxsbGuOOOO2LlypXR3t4eDz74YDQ3N7u6AwAAnDP9ip/HH388IiL++I//uM/xJ554Iv7iL/4iIiIeffTRqKqqivnz50d3d3c0NTXFY489Vpk7fPjw2LhxYyxatCjK5XKMHj06Fi5cGA8//PCZnQkAAMB76Ff8/F8+EmjkyJGxevXqWL169e+dc9lll8X3vve9/rw0AADAGfEJoQAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFLoV/w8/vjjcc0110SpVIpSqRTlcjk2bdpUGT969Gg0NzfHhAkTYsyYMTF//vzo6Ojo8z327dsX8+bNi1GjRsXEiRPj/vvvj+PHj5+dswEAAPg9+hU/kydPjhUrVkRbW1u8+OKLcdNNN8Utt9wSe/bsiYiI++67L5555plYv359tLS0xIEDB+K2226rPP/EiRMxb9686Onpie3bt8eTTz4Za9eujWXLlp3dswIAAHiHYUVRFGfyDcaPHx9f//rX4/bbb49LL700nnrqqbj99tsjIuLVV1+NK6+8MlpbW2PWrFmxadOm+PSnPx0HDhyI+vr6iIhYs2ZNfOUrX4k333wzampq/k+v2dXVFXV1dfHrn30oSmO9cw+4MDRNum6wl0ASzx3YNdhLIImTP9fsOc6lrsO9cfFHfhGdnZ1RKpXec2716b7IiRMnYv369XHkyJEol8vR1tYWx44dizlz5lTmXHHFFTF16tRK/LS2tsbVV19dCZ+IiKampli0aFHs2bMnrr/++lO+Vnd3d3R3d///E+zqOt1lA5y3/HIAAOdWvy+b7N69O8aMGRO1tbXxpS99KTZs2BCNjY3R3t4eNTU1MW7cuD7z6+vro729PSIi2tvb+4TPyfGTY7/P8uXLo66urvKYMmVKf5cNAAAk1+/4+ehHPxq7du2KHTt2xKJFi2LhwoXxyiuvnIu1VSxdujQ6Ozsrj/3795/T1wMAAC48/X7bW01NTXz4wx+OiIjp06fHzp074xvf+EZ89rOfjZ6enjh06FCfqz8dHR3R0NAQERENDQ3xwgsv9Pl+J+8Gd3LOqdTW1kZtbW1/lwoAAFBxxncL6O3tje7u7pg+fXqMGDEitmzZUhnbu3dv7Nu3L8rlckRElMvl2L17dxw8eLAyZ/PmzVEqlaKxsfFMlwIAAPB79evKz9KlS+Pmm2+OqVOnxuHDh+Opp56KH/7wh/Hcc89FXV1d3HnnnbFkyZIYP358lEqluPvuu6NcLsesWbMiImLu3LnR2NgYd9xxR6xcuTLa29vjwQcfjObmZld2AACAc6pf8XPw4MH48z//83jjjTeirq4urrnmmnjuuefiT//0TyMi4tFHH42qqqqYP39+dHd3R1NTUzz22GOV5w8fPjw2btwYixYtinK5HKNHj46FCxfGww8/fHbPCgAA4B3O+HN+BoPP+QEAOP/5nB8GQn8+50c5AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASOGM4mfFihUxbNiwuPfeeyvHjh49Gs3NzTFhwoQYM2ZMzJ8/Pzo6Ovo8b9++fTFv3rwYNWpUTJw4Me6///44fvz4mSwFAADgPZ12/OzcuTO++c1vxjXXXNPn+H333RfPPPNMrF+/PlpaWuLAgQNx2223VcZPnDgR8+bNi56enti+fXs8+eSTsXbt2li2bNnpnwUAAMD7OK34efvtt2PBggXxT//0T3HxxRdXjnd2dsa//Mu/xCOPPBI33XRTTJ8+PZ544onYvn17PP/88xER8f3vfz9eeeWV+Ld/+7e47rrr4uabb46/+7u/i9WrV0dPT8/ZOSsAAIB3OK34aW5ujnnz5sWcOXP6HG9ra4tjx471OX7FFVfE1KlTo7W1NSIiWltb4+qrr476+vrKnKampujq6oo9e/ac8vW6u7ujq6urzwMAAKA/qvv7hHXr1sVLL70UO3fufNdYe3t71NTUxLhx4/ocr6+vj/b29sqc3w2fk+Mnx05l+fLl8bWvfa2/SwUAAKjo15Wf/fv3xz333BPf+ta3YuTIkedqTe+ydOnS6OzsrDz2798/YK8NAMDpee7ArnjuwK7BXgZU9Ct+2tra4uDBg3HDDTdEdXV1VFdXR0tLS6xatSqqq6ujvr4+enp64tChQ32e19HREQ0NDRER0dDQ8K67v538+uScd6qtrY1SqdTnAQAA0B/9ip/Zs2fH7t27Y9euXZXHjBkzYsGCBZX/HjFiRGzZsqXynL1798a+ffuiXC5HRES5XI7du3fHwYMHK3M2b94cpVIpGhsbz9JpAQAA9NWvv/kZO3ZsXHXVVX2OjR49OiZMmFA5fuedd8aSJUti/PjxUSqV4u67745yuRyzZs2KiIi5c+dGY2Nj3HHHHbFy5cpob2+PBx98MJqbm6O2tvYsnRYAAEBf/b7hwft59NFHo6qqKubPnx/d3d3R1NQUjz32WGV8+PDhsXHjxli0aFGUy+UYPXp0LFy4MB5++OGzvRQAAICKYUVRFIO9iP7q6uqKurq6+PXPPhSlsaf9Oa0AAMAQ13W4Ny7+yC+is7Pzfe8NoBwAAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJCC+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAUxA8AAJBC9WAv4HQURREREV1v9w7ySgAAgMF0sglONsJ7GZLxc/jw4YiIuOyGXw7uQgAAgPPC4cOHo66u7j3nDCv+L4l0nunt7Y29e/dGY2Nj7N+/P0ql0mAviQtYV1dXTJkyxV7jnLPXGCj2GgPFXmMgFEURhw8fjkmTJkVV1Xv/Vc+QvPJTVVUVf/AHfxAREaVSyT8mBoS9xkCx1xgo9hoDxV7jXHu/Kz4nueEBAACQgvgBAABSGLLxU1tbGw899FDU1tYO9lK4wNlrDBR7jYFirzFQ7DXON0PyhgcAAAD9NWSv/AAAAPSH+AEAAFIQPwAAQAriBwAASEH8AAAAKYgfAAAgBfEDAACkIH4AAIAU/h+3nIFZDcOpdwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def _add_block(g: numpy.ndarray, x: int, y: int, padding: int):\n", + " for i in range(x - padding, x + padding):\n", + " for j in range(y - padding, y + padding):\n", + " g[j][i] = 0\n", + "\n", + "\"\"\"Test performance.\"\"\"\n", + "# Get a 500 x 500 grid\n", + "grid = numpy.ones((500, 500), numpy.int32)\n", + "\n", + "# Add a block at the center\n", + "_add_block(grid, 250, 250, 50)\n", + "\n", + "finder_grid = Grid(matrix=grid)\n", + "start = finder_grid.node(0, 0)\n", + "end = finder_grid.node(400, 400)\n", + "\n", + "finder = AStarFinder(diagonal_movement=DiagonalMovement.never)\n", + "path, runs = finder.find_path(start, end, finder_grid)\n", + "\n", + "# print runs, path\n", + "print(runs)\n", + "print(path)\n", + "\n", + "# update grid\n", + "for p in path:\n", + " grid[p.y][p.x] = 0\n", + "\n", + "\n", + "plt.rcParams['figure.figsize'] = [10, 10] # increase size of image so we can see the path\n", + "plt.matshow(grid)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "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.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/pathfinding/core/heap.py b/pathfinding/core/heap.py new file mode 100644 index 0000000..f6fb152 --- /dev/null +++ b/pathfinding/core/heap.py @@ -0,0 +1,87 @@ +"""Simple heap with ordering and removal.""" +import heapq +from .graph import Graph +from .grid import Grid +from .world import World + +class SimpleHeap: + """Simple wrapper around open_list that keeps track of order and removed nodes automatically.""" + + def __init__(self, node, grid): + self.grid = grid + self.open_list = [self._get_node_tuple(node, 0)] + self.removed_node_tuples = set() + self.heap_order = {} + self.number_pushed = 0 + + def _get_node_tuple(self, node, heap_order): + if isinstance(self.grid, Graph): + return (node.f, heap_order, node.node_id) + elif isinstance(self.grid, Grid): + return (node.f, heap_order, node.x, node.y) + elif isinstance(self.grid, World): + return (node.f, heap_order, node.x, node.y, node.grid_id) + else: + assert False, "unsupported heap node node=%s" % node + + def _get_node_id(self, node): + if isinstance(self.grid, Graph): + return node.node_id + elif isinstance(self.grid, Grid): + return (node.x, node.y) + elif isinstance(self.grid, World): + return (node.x, node.y, node.grid_id) + + + def pop_node(self): + """ + Pops node off the heap. i.e. returns the one with the lowest f. + + Notes: + 1. Checks if that values is in removed_node_tuples first, if not tries again. + 2. We use this approach to avoid invalidating the heap structure. + """ + node_tuple = heapq.heappop(self.open_list) + while node_tuple in self.removed_node_tuples: + node_tuple = heapq.heappop(self.open_list) + + if isinstance(self.grid, Graph): + node = self.grid.node(node_tuple[2]) + elif isinstance(self.grid, Grid): + node = self.grid.node(node_tuple[2], node_tuple[3]) + elif isinstance(self.grid, World): + node = self.grid.grids[node_tuple[4]].node(node_tuple[2], node_tuple[3]) + + return node + + def push_node(self, node): + """ + Push node into heap. + + :param node: The node to push. + """ + self.number_pushed = self.number_pushed + 1 + node_tuple = self._get_node_tuple(node, self.number_pushed) + node_id = self._get_node_id(node) + + self.heap_order[node_id] = self.number_pushed + + heapq.heappush(self.open_list, node_tuple) + + def remove_node(self, node, f): + """ + Remove the node from the heap. + + This just stores it in a set and we just ignore the node if it does get popped from the heap. + + :param node: The node to remove. + :param f: The old f value of the node. + """ + node_id = self._get_node_id(node) + heap_order = self.heap_order[node_id] + node_tuple = self._get_node_tuple(node, heap_order) + self.removed_node_tuples.add(node_tuple) + + def __len__(self): + """Returns the length of the open_list.""" + return len(self.open_list) \ No newline at end of file diff --git a/pathfinding/finder/a_star.py b/pathfinding/finder/a_star.py index 0d56980..3709bbe 100644 --- a/pathfinding/finder/a_star.py +++ b/pathfinding/finder/a_star.py @@ -1,4 +1,3 @@ -import heapq # used for the so colled "open list" that stores known nodes from .finder import BY_END, Finder, MAX_RUNS, TIME_LIMIT from ..core.diagonal_movement import DiagonalMovement from ..core.heuristic import manhattan, octile @@ -50,8 +49,7 @@ def check_neighbors(self, start, end, graph, open_list, :param open_list: stores nodes that will be processed next """ # pop node with minimum 'f' value - node = heapq.nsmallest(1, open_list)[0] - open_list.remove(node) + node = open_list.pop_node() node.closed = True # if reached the end position, construct the path and return it diff --git a/pathfinding/finder/bi_a_star.py b/pathfinding/finder/bi_a_star.py index b3a6b5b..41892b2 100644 --- a/pathfinding/finder/bi_a_star.py +++ b/pathfinding/finder/bi_a_star.py @@ -2,6 +2,7 @@ from .a_star import AStarFinder from .finder import BY_END, BY_START, MAX_RUNS, TIME_LIMIT from ..core.diagonal_movement import DiagonalMovement +from ..core.heap import SimpleHeap class BiAStarFinder(AStarFinder): @@ -45,12 +46,12 @@ def find_path(self, start, end, grid): self.start_time = time.time() # execution time limitation self.runs = 0 # count number of iterations - start_open_list = [start] + start_open_list = SimpleHeap(start, grid) start.g = 0 start.f = 0 start.opened = BY_START - end_open_list = [end] + end_open_list = SimpleHeap(end, grid) end.g = 0 end.f = 0 end.opened = BY_END diff --git a/pathfinding/finder/breadth_first.py b/pathfinding/finder/breadth_first.py index a8e5a09..6174ec8 100644 --- a/pathfinding/finder/breadth_first.py +++ b/pathfinding/finder/breadth_first.py @@ -19,7 +19,7 @@ def __init__(self, heuristic=None, weight=1, self.diagonalMovement = DiagonalMovement.never def check_neighbors(self, start, end, grid, open_list): - node = open_list.pop(0) + node = open_list.pop_node() node.closed = True if node == end: @@ -30,6 +30,6 @@ def check_neighbors(self, start, end, grid, open_list): if neighbor.closed or neighbor.opened: continue - open_list.append(neighbor) + open_list.push_node(neighbor) neighbor.opened = True neighbor.parent = node diff --git a/pathfinding/finder/finder.py b/pathfinding/finder/finder.py index 7b5aedf..b2080b2 100644 --- a/pathfinding/finder/finder.py +++ b/pathfinding/finder/finder.py @@ -1,6 +1,7 @@ import heapq # used for the so colled "open list" that stores known nodes import time # for time limitation from ..core.diagonal_movement import DiagonalMovement +from ..core.heap import SimpleHeap # max. amount of tries we iterate until we abort the search @@ -107,20 +108,21 @@ def process_node( ng = parent.g + graph.calc_cost(parent, node, self.weighted) if not node.opened or ng < node.g: + old_f = node.f node.g = ng node.h = node.h or self.apply_heuristic(node, end) # f is the estimated total cost from start to goal node.f = node.g + node.h node.parent = parent if not node.opened: - heapq.heappush(open_list, node) + open_list.push_node(node) node.opened = open_value else: # the node can be reached with smaller cost. # Since its f value has been updated, we have to # update its position in the open list - open_list.remove(node) - heapq.heappush(open_list, node) + open_list.remove_node(node, old_f) + open_list.push_node(node) def check_neighbors(self, start, end, graph, open_list, open_value=True, backtrace_by=None): @@ -150,7 +152,7 @@ def find_path(self, start, end, grid): self.runs = 0 # count number of iterations start.opened = True - open_list = [start] + open_list = SimpleHeap(start, grid) while len(open_list) > 0: self.runs += 1 diff --git a/pathfinding/finder/msp.py b/pathfinding/finder/msp.py index d0805d5..bb3800d 100644 --- a/pathfinding/finder/msp.py +++ b/pathfinding/finder/msp.py @@ -3,6 +3,7 @@ from collections import deque, namedtuple from ..core import heuristic from ..finder.finder import Finder +from ..core.heap import SimpleHeap class MinimumSpanningTree(Finder): @@ -31,14 +32,13 @@ def itertree(self, grid, start): start.opened = True - open_list = [start] + open_list = SimpleHeap(start, grid) while len(open_list) > 0: self.runs += 1 self.keep_running() - node = heapq.nsmallest(1, open_list)[0] - open_list.remove(node) + node = open_list.pop_node() node.closed = True yield node diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..ea224bc --- /dev/null +++ b/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +pythonpath = . +log_cli = 1 +log_cli_level = INFO +log_cli_format = %(asctime)s.%(msecs)03d [%(levelname)8s] (%(filename)s:%(lineno)s) %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S \ No newline at end of file diff --git a/test/path_test_scenarios.json b/test/path_test_scenarios.json index d163514..9362197 100644 --- a/test/path_test_scenarios.json +++ b/test/path_test_scenarios.json @@ -1,5 +1,6 @@ [ { + "name": "s1", "startX": 0, "startY": 0, "endX": 1, @@ -10,6 +11,7 @@ "expectedDiagonalLength": 2 }, { + "name": "s2", "startX": 1, "startY": 1, "endX": 4, @@ -24,6 +26,7 @@ "expectedDiagonalLength": 5 }, { + "name": "s3", "startX": 0, "startY": 3, "endX": 3, @@ -38,6 +41,7 @@ "expectedDiagonalLength": 6 }, { + "name": "s4", "startX": 4, "startY": 4, "endX": 19, @@ -66,6 +70,7 @@ "expectedDiagonalLength": 16 }, { + "name": "s5", "startX": 0, "startY": 0, "endX": 4, diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..84a70ce --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,6 @@ +pytest==7.4.4 +snakeviz==2.2.0 +pytest-profiling==1.7.0 +numpy==1.26.3 +pandas==2.1.4 +matplotlib==3.8.2 \ No newline at end of file diff --git a/test/test_heap.py b/test/test_heap.py new file mode 100644 index 0000000..31cc3f0 --- /dev/null +++ b/test/test_heap.py @@ -0,0 +1,25 @@ +from pathfinding.core.heap import SimpleHeap +from pathfinding.core.grid import Grid + +def test_heap(): + grid = Grid(width=10, height=10) + start = grid.node(0, 0) + open_list = SimpleHeap(start, grid) + + # Test pop + assert open_list.pop_node() == start + assert len(open_list) == 0 + + # Test push + open_list.push_node(grid.node(1, 1)) + open_list.push_node(grid.node(1, 2)) + open_list.push_node(grid.node(1, 3)) + + # Test removal and pop + assert len(open_list) == 3 + open_list.remove_node(grid.node(1, 2), 0) + assert len(open_list) == 3 + + assert open_list.pop_node() == grid.node(1, 1) + assert open_list.pop_node() == grid.node(1, 3) + assert len(open_list) == 0 diff --git a/test/test_performance.py b/test/test_performance.py new file mode 100644 index 0000000..1e042ce --- /dev/null +++ b/test/test_performance.py @@ -0,0 +1,29 @@ +import numpy +from pathfinding.core.diagonal_movement import DiagonalMovement +from pathfinding.core.grid import Grid +from pathfinding.finder.a_star import AStarFinder + + +def _add_block(g: numpy.ndarray, x: int, y: int, padding: int): + for i in range(x - padding, x + padding): + for j in range(y - padding, y + padding): + g[j][i] = 0 + +def test_a_star(): + """Test performance.""" + # Get a 500 x 500 grid + grid = numpy.ones((500, 500), numpy.int32) + + # Add a block at the center + _add_block(grid, 250, 250, 50) + + finder_grid = Grid(matrix=grid) + start = finder_grid.node(0, 0) + end = finder_grid.node(400, 400) + + finder = AStarFinder(diagonal_movement=DiagonalMovement.never) + path, runs = finder.find_path(start, end, finder_grid) + + assert path[0] == start + assert path[-1] == end + assert len(path) == 801 \ No newline at end of file