From 041cf56c1272ab14e23918700d29914091a42323 Mon Sep 17 00:00:00 2001 From: quirinecker Date: Sun, 19 Oct 2025 22:18:14 +0200 Subject: [PATCH] intitial commit --- README.md | 9 + introducing_pig.ipynb | 576 ++++++++++++++++++ pig_lite/.gitignore | 1 + pig_lite/.idea/.gitignore | 8 + .../inspectionProfiles/Project_Default.xml | 24 + .../inspectionProfiles/profiles_settings.xml | 6 + pig_lite/.idea/misc.xml | 7 + pig_lite/.idea/modules.xml | 8 + pig_lite/.idea/pig_lite.iml | 12 + pig_lite/.idea/vcs.xml | 6 + pig_lite/README.md | 3 + pig_lite/bayesian_net/__init__.py | 0 pig_lite/bayesian_net/bayesian_net.py | 154 +++++ pig_lite/datastructures/__init__.py | 0 pig_lite/datastructures/priority_queue.py | 61 ++ pig_lite/datastructures/queue.py | 27 + pig_lite/datastructures/stack.py | 21 + pig_lite/decision_tree/dt_base.py | 61 ++ pig_lite/decision_tree/dt_node.py | 70 +++ pig_lite/decision_tree/training_set.py | 84 +++ pig_lite/environment/__init__.py | 0 pig_lite/environment/base.py | 60 ++ pig_lite/environment/gridworld.py | 360 +++++++++++ pig_lite/game/__init__.py | 0 pig_lite/game/base.py | 87 +++ pig_lite/game/tictactoe.py | 371 +++++++++++ pig_lite/instance_generation/__init__.py | 0 pig_lite/instance_generation/enc.py | 5 + .../instance_generation/problem_factory.py | 96 +++ .../simple_2d-checkpoint.py | 529 ++++++++++++++++ pig_lite/problem/base.py | 92 +++ pig_lite/problem/simple_2d.py | 529 ++++++++++++++++ shell.nix | 15 + 33 files changed, 3282 insertions(+) create mode 100644 README.md create mode 100644 introducing_pig.ipynb create mode 100644 pig_lite/.gitignore create mode 100644 pig_lite/.idea/.gitignore create mode 100644 pig_lite/.idea/inspectionProfiles/Project_Default.xml create mode 100644 pig_lite/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 pig_lite/.idea/misc.xml create mode 100644 pig_lite/.idea/modules.xml create mode 100644 pig_lite/.idea/pig_lite.iml create mode 100644 pig_lite/.idea/vcs.xml create mode 100644 pig_lite/README.md create mode 100644 pig_lite/bayesian_net/__init__.py create mode 100644 pig_lite/bayesian_net/bayesian_net.py create mode 100644 pig_lite/datastructures/__init__.py create mode 100644 pig_lite/datastructures/priority_queue.py create mode 100644 pig_lite/datastructures/queue.py create mode 100644 pig_lite/datastructures/stack.py create mode 100644 pig_lite/decision_tree/dt_base.py create mode 100644 pig_lite/decision_tree/dt_node.py create mode 100644 pig_lite/decision_tree/training_set.py create mode 100644 pig_lite/environment/__init__.py create mode 100644 pig_lite/environment/base.py create mode 100644 pig_lite/environment/gridworld.py create mode 100644 pig_lite/game/__init__.py create mode 100644 pig_lite/game/base.py create mode 100644 pig_lite/game/tictactoe.py create mode 100644 pig_lite/instance_generation/__init__.py create mode 100644 pig_lite/instance_generation/enc.py create mode 100644 pig_lite/instance_generation/problem_factory.py create mode 100644 pig_lite/problem/.ipynb_checkpoints/simple_2d-checkpoint.py create mode 100644 pig_lite/problem/base.py create mode 100644 pig_lite/problem/simple_2d.py create mode 100644 shell.nix diff --git a/README.md b/README.md new file mode 100644 index 0000000..39232e3 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# JKU AI UE Exercise + +## How to use + +1. `nix-shell` +2. jupyter lab . + + + diff --git a/introducing_pig.ipynb b/introducing_pig.ipynb new file mode 100644 index 0000000..ecddc51 --- /dev/null +++ b/introducing_pig.ipynb @@ -0,0 +1,576 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Artificial Intelligence UE\n", + "## pig_lite\n", + "\n", + "In this class we are going to work with a small framework called `pig_lite`. `pig` stands for Problem Instance Generation and `pig_lite` is a simplified version of the framework that we have used in previous years. The main purpose of this framework is to generate problems that can be used to test your algorithms.\n", + "\n", + "The purpose of this notebook is to show you how to work with `pig_lite`, and more specifically, with instances of the class `Problem`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import random\n", + "\n", + "from pig_lite.problem.base import Problem\n", + "from pig_lite.instance_generation.problem_factory import ProblemFactory" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Creating problems\n", + "\n", + "For generating problems we first need an object of the class `ProblemFactory`." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "factory = ProblemFactory()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Create a random problem\n", + "\n", + "The function `generate_problem` takes three parameters:\n", + "- problem_type: for now we are going to use `maze` (but you can also try `terrain` and `rooms`)\n", + "- problem_size: the size of the problems, for a maze this means the width and height\n", + "- random generator" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAIYCAYAAACbqzQ2AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAARsBJREFUeJzt3XucF3W9P/D3lwV2QXaXIBcWogXMG4K3vCR6VA5qIpJdzWuo1a+UzlHLjpYWqEdJKo+dRExPKeYFTcMsTypqYqUo3iqlNBXBFMVEWUUB2e/8/lh3j+ty2WXnyw47z+fjMY/aYfYz79kvOLx5feYzhSRJkgAAAABKoltnFwAAAABdmcYbAAAASkjjDQAAACWk8QYAAIAS0ngDAABACWm8AQAAoIQ03gAAAFBCGm8AAAAoIY03AAAAlJDGm1wrFApt2u6555645557olAoxI033ljyuh599NHYb7/9orq6OgqFQlx00UXN57/nnnvaPV57vve4446LoUOHtvscAJuzrN4PNqUXX3wxpkyZEo899ljqY59wwglx8MEHt9i3tntdZ1qwYEFMmTIlnnvuuU6t4/2ee+65KBQKceWVV3Z2KZl1ySWXrPXns7af3ZVXXhmFQmGjPuef/vSnMXjw4FixYsXGF0tude/sAqAz3X///S2+Pvfcc+N3v/td3H333S32jxgxIh555JFNVtcJJ5wQK1asiFmzZsUHPvCBGDp0aPTu3Tvuv//+GDFixCarAyAvsno/2JRefPHFOPvss2Po0KGx8847pzbuo48+GjNnzowHHnigxf613es604IFC+Lss8+O/fffv9Nrea/a2tq4//77Y6uttursUjLrkksuiQ9+8INx3HHHtdif9s9u4sSJccEFF8S0adPi7LPPTmVM8kPjTa597GMfa/H1lltuGd26dWu1f1N7/PHH48tf/nKMGzeuxf7Orgugq8rq/aAr+N73vhd77LFH7Lbbbi32r+tet7HeeeedKBQK0b171/rrbXl5ud+HGyntn1337t3jK1/5Spx77rlx+umnR+/evVMbm67PVHNop3feeSfOPPPMGDRoUFRVVcUBBxwQTz75ZKvj7rzzzhg7dmxUVVVF7969Y++994677rprvWM3TX9as2ZNzJgxo3lqY8S6p4s/9NBD8YlPfCL69esXFRUVscsuu8QNN9zQpmu58sorY9ttt43y8vLYfvvt46qrrmrbDwGAkt4Pmrz++uvxjW98I4YPHx7l5eVRU1MThxxySPztb39rPmbZsmVx0kknxeDBg6Nnz54xfPjwOPPMM2PVqlUtxvrFL34Re+65Z1RXV0fv3r1j+PDhccIJJ0RE4z1m9913j4iI448/vvn+M2XKlIiIePbZZ+OII46IQYMGRXl5eQwYMCDGjh27wWnpL7/8csyePTuOPfbY5n3ru9dFNDbkhx12WHzgAx+IioqK2HnnnWPmzJktxm26J/785z+Pb3zjGzF48OAoLy+Pp59+ep21zJgxI3baaafo06dPVFZWxnbbbRff/va3m2v63Oc+FxERY8aMaa7pvVOU2/I5TpkyJQqFQjz66KPx6U9/OqqqqqK6ujqOOeaYeOWVV1ocO3To0Dj00ENj9uzZseOOO0ZFRUUMHz48/vu//7vFcWubLt10nieeeCKOPPLIqK6ujgEDBsQJJ5wQy5cvb/H9r7/+enzxi1+Mfv36RZ8+fWL8+PHx7LPPtvh812fx4sVxzDHHRE1NTfPfF374wx9GsVhsVeMPfvCDuPDCC2PYsGHRp0+f2GuvvWLevHkbPMcrr7wSJ510UowYMSL69OkTNTU18a//+q/x+9//foPfO3To0HjiiSdi7ty5zZ9b04yF9kzTb+uf06OPPjrq6+tj1qxZGxwT3kvjDe307W9/OxYtWhT/8z//E5dddln8/e9/jwkTJkRDQ0PzMVdffXUcdNBBUVVVFTNnzowbbrgh+vXrFx//+MfX+5et8ePHN093/OxnPxv3339/q+mP7/W73/0u9t5773j99dfj0ksvjV/96lex8847x+c///kN3mSuvPLKOP7442P77bePm266Kc4666w499xzW02rBGDtSnk/iIh44403Yp999omf/OQncfzxx8evf/3ruPTSS2ObbbaJJUuWRETEypUrY8yYMXHVVVfF17/+9bj11lvjmGOOiWnTpsWnP/3p5rHuv//++PznPx/Dhw+PWbNmxa233hrf/e53Y82aNRERseuuu8YVV1wRERFnnXVW8/3nS1/6UkREHHLIIfHwww/HtGnTYs6cOTFjxozYZZdd4vXXX1/vNdxxxx3xzjvvxJgxY5r3re9e9+STT8bo0aPjiSeeiP/+7/+OX/7ylzFixIg47rjjYtq0aa3G/9a3vhWLFy+OSy+9NH79619HTU3NWuuYNWtWnHTSSbHffvvF7Nmz4+abb45TTz21+Vnd8ePHx/nnnx8REdOnT2+uafz48RHR/s/xU5/6VHzkIx+JG2+8MaZMmRI333xzfPzjH4933nmnxXGPPfZYnHLKKXHqqafG7NmzY/To0XHyySfHD37wg/X+XJt85jOfiW222SZuuummOOOMM+Laa6+NU089tfnXi8ViTJgwIa699to4/fTTY/bs2bHnnnu2et5+XV555ZUYPXp03HHHHXHuuefGLbfcEgcccECcdtpp8bWvfa3V8dOnT485c+bERRddFNdcc02sWLEiDjnkkFb/GPB+y5Yti4iIyZMnx6233hpXXHFFDB8+PPbff/8Nrk8ze/bsGD58eOyyyy7Nn9vs2bPbdH1N2vP5Dhw4MLbbbru49dZb23UOiARoNnHixGSLLbZY66/97ne/SyIiOeSQQ1rsv+GGG5KISO6///4kSZJkxYoVSb9+/ZIJEya0OK6hoSHZaaedkj322GODdUREMmnSpLWe/3e/+13zvu222y7ZZZddknfeeafFsYceemhSW1ubNDQ0rPV7GxoakkGDBiW77rprUiwWm7/vueeeS3r06JHU1dVtsEaAriwL94NzzjkniYhkzpw56zzm0ksvTSIiueGGG1rsv+CCC5KISO64444kSZLkBz/4QRIRyeuvv77OsebPn59ERHLFFVe02P/Pf/4ziYjkoosuWm+9a3PiiScmvXr1anGvabK2e90RRxyRlJeXJ4sXL26xf9y4cUnv3r2b62/6DPbdd9821fG1r30t6du373qP+cUvftHqPpsk7fscJ0+enEREcuqpp7Y49pprrkkiIrn66qub99XV1SWFQiF57LHHWhx74IEHJlVVVcmKFSuSJEmShQsXtvpcms4zbdq0Ft970kknJRUVFc0/71tvvTWJiGTGjBktjps6dWoSEcnkyZPX+zM544wzkohIHnjggRb7TzzxxKRQKCRPPvlkixpHjRqVrFmzpvm4Bx98MImI5Lrrrlvved5vzZo1yTvvvJOMHTs2+dSnPrXB43fYYYdkv/32a7V/bT+7K664IomIZOHChUmSbNyf06OPPjoZMGBAu64JJN7QTp/4xCdafL3jjjtGRMSiRYsiIuK+++6LZcuWxcSJE2PNmjXNW7FYjIMPPjjmz5+fymqYTz/9dPztb3+Lo48+OiKixbkOOeSQWLJkyVqnPEY0JgovvvhiHHXUUS2m99XV1cXo0aM7XBtAHpT6fvDb3/42ttlmmzjggAPWeczdd98dW2yxRXz2s59tsb9pkammtK5pGvnhhx8eN9xwQ7zwwgttvs5+/frFVlttFd///vfjwgsvjEcffbTFNOP1efHFF2PLLbdsca9Zn7vvvjvGjh0bQ4YMabH/uOOOi7feeqvVLLDPfOYzbRp3jz32iNdffz2OPPLI+NWvfhX//Oc/2/R9ERv3OTbdm5scfvjh0b179/jd737XYv8OO+wQO+20U4t9Rx11VNTX17dpEb+1/R5cuXJlLF26NCIi5s6d23z+9zryyCM3OHZE4+cxYsSI2GOPPVrsP+644yJJklaz5MaPHx9lZWUt6on4vz8T63PppZfGrrvuGhUVFdG9e/fo0aNH3HXXXfHXv/61TbVurI35fGtqamLp0qXNM0agLTTe0E79+/dv8XV5eXlERLz99tsR0fg8W0Tj9LkePXq02C644IJIkqR5SlVHNJ3ntNNOa3Wek046KSJinX+xePXVVyOicbrU+61tHwCtlfp+8Morr8SHPvSh9dbw6quvxsCBA1s1tjU1NdG9e/fm/97vu+++cfPNN8eaNWviC1/4QnzoQx+KkSNHxnXXXbfB6ywUCnHXXXfFxz/+8Zg2bVrsuuuuseWWW8a///u/xxtvvLHe73377bejoqJig+d47/XU1ta22j9o0KDmX3+vtR27Nscee2z87Gc/i0WLFsVnPvOZqKmpiT333DPmzJmzwe/dmM/x/ffS7t27R//+/VvVv7778PuPXZsN/R589dVXo3v37tGvX78Wxw0YMGCDYzd9f3s+jw3Vsy4XXnhhnHjiibHnnnvGTTfdFPPmzYv58+fHwQcfvMHv7aiN+XwrKioiSZJYuXJlSWuja+layz5CBnzwgx+MiIgf//jH61xJs603vLac51vf+laL5/jea9ttt13r/qYb40svvdTq19a2D4D26+j9YMstt4x//OMf6z1H//7944EHHogkSVo0301pXFMNERGHHXZYHHbYYbFq1aqYN29eTJ06NY466qgYOnRo7LXXXus9T11dXfz0pz+NiIinnnoqbrjhhpgyZUqsXr06Lr300nV+3wc/+MF2vX6tf//+zc+vv9eLL77YPN57tTVJj2hcNO7444+PFStWxL333huTJ0+OQw89NJ566qmoq6tb5/dtzOf40ksvxeDBg5u/XrNmTbz66qutGtP13Yfff+zG6N+/f6xZsyaWLVvWovlu672+vZ/Hxrr66qtj//33jxkzZrTYv6F/2EnDxny+y5Yti/Ly8ujTp0/J66PrkHhDyvbee+/o27dvLFiwIHbbbbe1bj179uzwebbddtvYeuut409/+tM6z1NZWbnO762trY3rrrsukiRp3r9o0aK47777OlwbAB2/H4wbNy6eeuqp9S56OXbs2HjzzTfj5ptvbrG/6S0VY8eObfU95eXlsd9++8UFF1wQEY3v2W7aH7HhdHKbbbaJs846K0aNGrXBpnq77baLV199dYOLa733eu6+++7mxu6919O7d+9UXg21xRZbxLhx4+LMM8+M1atXxxNPPBER677+jfkcr7nmmhZf33DDDbFmzZrYf//9W+x/4okn4k9/+lOLfddee21UVlbGrrvu2uFr3W+//SIi4vrrr2+xv60rco8dOzYWLFjQ6nO+6qqrolAotFg0ryMKhULzz7/Jn//85/UuMPte5eXlG52Mb8zn++yzz8aIESM26nzkl8QbUtanT5/48Y9/HBMnToxly5bFZz/72aipqYlXXnkl/vSnP8Urr7zS6l90N9ZPfvKTGDduXHz84x+P4447LgYPHhzLli2Lv/71r/HII4/EL37xi7V+X7du3eLcc8+NL33pS/GpT30qvvzlL8frr78eU6ZMMdUcICUdvR+ccsopcf3118dhhx0WZ5xxRuyxxx7x9ttvx9y5c+PQQw+NMWPGxBe+8IWYPn16TJw4MZ577rkYNWpU/OEPf4jzzz8/DjnkkObnw7/73e/GP/7xjxg7dmx86EMfitdffz1+9KMfRY8ePZqbs6222ip69eoV11xzTWy//fbRp0+fGDRoUPzzn/+Mr33ta/G5z30utt566+jZs2fcfffd8ec//znOOOOM9f4M9t9//0iSJB544IE46KCDNvgzmzx5cvzmN7+JMWPGxHe/+93o169fXHPNNXHrrbfGtGnTorq6uh2fwP/58pe/HL169Yq99947amtr46WXXoqpU6dGdXV18/PvI0eOjIiIyy67LCorK6OioiKGDRsW/fv3b/fn+Mtf/jK6d+8eBx54YDzxxBPxne98J3baaadWz1oPGjQoPvGJT8SUKVOitrY2rr766pgzZ05ccMEFqbwj+uCDD4699947vvGNb0R9fX189KMfjfvvv7/5H2a6dVt/BnfqqafGVVddFePHj49zzjkn6urq4tZbb41LLrkkTjzxxNhmm206XGNExKGHHhrnnntuTJ48Ofbbb7948skn45xzzolhw4a16TnqUaNGxaxZs+L666+P4cOHR0VFRYwaNapN527vn9NisRgPPvhgfPGLX9zo6yWnOm1ZN8igtqxi+4tf/KLF/rWtmJkkSTJ37txk/PjxSb9+/ZIePXokgwcPTsaPH9/q+9cm2riqeZIkyZ/+9Kfk8MMPT2pqapIePXokAwcOTP71X/81ufTSSzf4vf/zP/+TbL311knPnj2TbbbZJvnZz36WTJw40armQO5l5X7w2muvJSeffHLy4Q9/OOnRo0dSU1OTjB8/Pvnb3/7WfMyrr76afPWrX01qa2uT7t27J3V1dcm3vvWtZOXKlc3H/OY3v0nGjRuXDB48OOnZs2dSU1OTHHLIIcnvf//7Fue77rrrku222y7p0aNH86rXL7/8cnLccccl2223XbLFFlskffr0SXbcccfkv/7rv1qsYL02DQ0NydChQ5OTTjqp1a+t7V6XJEnyl7/8JZkwYUJSXV2d9OzZM9lpp51a/UzX9Rmsy8yZM5MxY8YkAwYMSHr27JkMGjQoOfzww5M///nPLY676KKLkmHDhiVlZWWtPsu2fI5Nq40//PDDyYQJE5I+ffoklZWVyZFHHpm8/PLLLc5VV1eXjB8/PrnxxhuTHXbYIenZs2cydOjQ5MILL2xx3PpWNX/llVdaHPv+FbuTJEmWLVuWHH/88Unfvn2T3r17JwceeGAyb968JCKSH/3oRxv82S1atCg56qijkv79+yc9evRItt122+T73/9+85tT3lvj97///VbfH21YPX3VqlXJaaedlgwePDipqKhIdt111+Tmm29u899JnnvuueSggw5KKisrk4ho/p62rGrepK1/Tu+6667mzxjao5Ak75lnCgAAKfrhD38Y5513XrzwwgvRq1evzi6npKZMmRJnn312vPLKKxt8/nno0KExcuTI+M1vfrOJqvs/1157bRx99NHxxz/+0dtM2unYY4+NZ599Nv74xz92dilsZkw1BwCgZCZNmhQXX3xxTJ8+PU477bTOLid3rrvuunjhhRdi1KhR0a1bt5g3b158//vfj3333VfT3U7PPPNMXH/99etddwHWReMNAEDJVFRUxM9//vPmRdzYtCorK2PWrFnxn//5n7FixYqora2N4447Lv7zP/+zs0vb7CxevDguvvji2GeffTq7FDZDppoDAABACXmdGABsAvfee29MmDAhBg0aFIVCodXrnwCATW9D9+ckSWLKlCkxaNCg6NWrV+y///7NryFsD403AGwCK1asiJ122ikuvvjizi4FAHjXhu7P06ZNiwsvvDAuvvjimD9/fgwcODAOPPDAeOONN9p1HlPNAWATKxQKMXv27PjkJz/Z2aUAAO96//05SZIYNGhQnHLKKXH66adHRMSqVatiwIABccEFF8RXvvKVNo/d7sXVisVivPjii1FZWRmFQqG93w4Am1SSJM2v9unWLb2JXkmStLoPlpeXR3l5eWrnaA/3ZwA2J6W6PzeNncY9euHChfHSSy/FQQcd1GKc/fbbL+67777SNN7Tp0+P6dOnx+rVq+OZZ55pV8EA0NX06dMn3nzzzRb7Jk+eHFOmTNmkdbg/A7C5GlhTFi8tbUh93LTu0S+99FJERAwYMKDF/gEDBsSiRYvaNVabG+9JkybFpEmTYvny5dG3b994/vnno6qqql0nA/Kpurq6s0soieXLl3d2CbTBCy+8ECNGjEh93DfffLPVvbAz0u7335/32P9b0b17xSavo5TeGNI1335a+fyazi6BNur13GudXUJJNDy9sLNLIMdWxlsxb+mcWPTw0KiqTC/xrn+jGHUffS7Ve/T70/O1Jeob0u47WdMJqqqqNN5Arvlv4Oahvr6++f+nNQW7aXmULN0Lm66te/eK6N6jazXeZT27ZuPdvYfGe3PRvaxzHiEptUKhR2eXQI51Txp///WpLESfyvQekSpGev3qwIEDI6Ix+a6trW3ev3Tp0lYp+IZY1RyAXCgUCqluAEDHNSTF1Le0DBs2LAYOHBhz5sxp3rd69eqYO3dujB49ul1jdc1/QgaAjHnzzTfj6aefbv564cKF8dhjj0W/fv3iwx/+cCdWBgD5taH78ymnnBLnn39+bL311rH11lvH+eefH717946jjjqqXefReAOQC2kn1e19G+dDDz0UY8aMaf7661//ekRETJw4Ma688srU6gKAzUkxkihGem+4bu9YG7o//8d//Ee8/fbbcdJJJ8Vrr70We+65Z9xxxx1RWVnZ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Gm8AyKCczDU31RwAAABKSOINQC5IvAEgg1Keah4ZnWou8QYAAIASkngDkAsSbwDIniRp3NIcL4s03gDkgsYbALInL6uam2oOAAAAJSTxBiAXJN4AkEFJId0F0STeAAAAkD8SbwByQeINANljcTUA6EI03gCQQcm7W5rjZZCp5gAAAFBCEm8AckHiDQDZ43ViAAAAQIdJvAHIBYk3AGRURp/LTpPGG4Bc0HgDQPaYag4AAAB0mMQbgFyQeANABnmdGAAAANBREm8AckNSDQBZU3h3S3O87NF4A5ALppoDQAaZag4AAABd1wsvvBDHHHNM9O/fP3r37h0777xzPPzww6mfR+INQC5IvAEggzox8X7ttddi7733jjFjxsRvf/vbqKmpiWeeeSb69u2bYkGNNN4AAADkzgUXXBBDhgyJK664onnf0KFDS3IuU80ByIWmxDutDQBIQVJIf4uI+vr6FtuqVatanfqWW26J3XbbLT73uc9FTU1N7LLLLnH55ZeX5DI13gDkgsYbALInSdLfIiKGDBkS1dXVzdvUqVNbnfvZZ5+NGTNmxNZbbx233357fPWrX41///d/j6uuuir16zTVHAAAgC7l+eefj6qqquavy8vLWx1TLBZjt912i/PPPz8iInbZZZd44oknYsaMGfGFL3wh1Xo03gDkgsXVACCDSrS4WlVVVYvGe21qa2tjxIgRLfZtv/32cdNNN6VYUCNTzQEAAMidvffeO5588skW+5566qmoq6tL/VwSbwByQeINABn0ngXRUhuvjU499dQYPXp0nH/++XH44YfHgw8+GJdddllcdtll6dXzLok3AAAAubP77rvH7Nmz47rrrouRI0fGueeeGxdddFEcffTRqZ9L4g1ALki8ASB7CknjluZ47XHooYfGoYceml4B66DxBiAXNN4AkEElWlwta0w1BwAAgBKSeAOQCxJvAMigTlxcbVOSeAMAAEAJSbwByAWJNwBkUE6e8dZ4A5ALGm8AyKCcNN6mmgMAAEAJSbwByAWJNwBkkMQbAAAA6CiJNwC5IPEGgAzKyevENN4A5ILGGwCyp5A0bmmOl0WmmgMAAEAJSbwByAWJNwBkkMXVAAAAgI6SeAOQCxJvAKCzaLwByAWNNwBkTyFSXlwtvaFSpfHOgSTJ6IMO5EZXbVL82QJobdXg1Z1dQupWDe7sCkqj/IWe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5vGee+65kl4XAADkhcQb2uC2x5fEiVc/Esn79r+0fGWcePUjMeOYXePgkbWpn3fJkiVx5JFHxrRp0+JTn/pUvPHGG/H73/8+vvCFL8TixYujvr4+rrjiioiI6NevX0REVFZWxpVXXhmDBg2Kv/zlL/HlL385Kisr4z/+4z+ax3366afjhhtuiJtuuinKysqirq4u/v73v8fIkSPjnHPOiYiILbfcMvXrAQCAPNJ4wwY0FJM4+9cLWjXdERFJRBQi4uxfL4gDRwxMfdr5kiVLYs2aNfHpT3866urqIiJi1KhRERHRq1evWLVqVQwcOLDF95x11lnN/3/o0KHxjW98I66//voWjffq1avj5z//eYvmumfPntG7d+9W4wEAAB1jqjlswIMLl8WS5SvX+etJRCxZvjIeXLgs9XPvtNNOMXbs2Bg1alR87nOfi8svvzxee+219X7PjTfeGPvss08MHDgw+vTpE9/5zndi8eLFLY6pq6uTaAMAwCai8YYNWPrGupvujTmuPcrKymLOnDnx29/+NkaMGBE//vGPY9ttt42FCxeu9fh58+bFEUccEePGjYvf/OY38eijj8aZZ54Zq1evbnHcFltskXqtAADA2plqDhtQU1mR6nHtVSgUYu+994699947vvvd70ZdXV3Mnj07evbsGQ0NDS2O/eMf/xh1dXVx5plnNu9btGhRm86ztvEAAICO03jDBuwxrF/UVlfES8tXrvU570JEDKxufLVY2h544IG466674qCDDoqampp44IEH4pVXXontt98+Vq5cGbfffns8+eST0b9//6iuro6PfOQjsXjx4pg1a1bsvvvuceutt8bs2bPbdK6hQ4fGAw88EM8991z06dMn+vXrF926mRQDAAAd5W/VsAFl3QoxecKIiGhsst+r6evJE0aU5H3eVVVVce+998YhhxwS22yzTZx11lnxwx/+MMaNGxdf/vKXY9ttt43ddtstttxyy/jjH/8Yhx12WJx66qnxta99LXbeeee477774jvf+U6bznXaaadFWVlZjBgxIrbccstWz4UDAAAbp5AkydpCPOhSVq5cGQsXLoxhw4ZFRcXGTQm/7fElcfavF7RYaK22uiImTxhRkleJZVEaP0cAAMgbU82hjQ4eWRsHjhgYDy5cFkvfWBk1lY3Ty0uRdAMAAF2HxhvaoaxbIfbaqn9nlwEAAGxGPOMNAAAAJaTxBgAAgBLSeAMAAEAJabwBAACghDTeAAAAUEIabwAAACghjTcAAACUkMYbcmjKlCmx8847d3YZAACQCxpvAAAAKKHunV0AbFaKDRGL7ot48+WIPgMi6kZHdCvr7KoAAIAMk3hDWy24JeKikREzD4246YuN/3vRyMb9JZQkSUybNi2GDx8evXr1ip122iluvPHGiIi45557olAoxF133RW77bZb9O7dO0aPHh1PPvlkizG+973vxYABA6KysjK++MUvxsqVK0taMwAA8H803tAWC26JuOELEfUvttxfv6Rxfwmb77POOiuuuOKKmDFjRjzxxBNx6qmnxjHHHBNz585tPubMM8+MH/7wh/HQQw9F9+7d44QTTmj+tRtuuCEmT54c5513Xjz00ENRW1sbl1xyScnqBQAAWiokSZJ0dhFQaitXroyFCxfGsGHDoqKion3fXGxoTLbf33Q3K0RUDYo45S+pTztfsWJFfPCDH4y777479tprr+b9X/rSl+Ktt96K//f//l+MGTMm7rzzzhg7dmxERPzv//5vjB8/Pt5+++2oqKiI0aNHx0477RQzZsxo/v6PfexjsXLlynjsscfaVU+Hfo4AAJBTEm/YkEX3rafpjohIIupfaDwuZQsWLIiVK1fGgQceGH369GnerrrqqnjmmWeaj9txxx2b/39tbW1ERCxdujQiIv7617+2aNojotXXAABA6VhcDTbkzZfTPa4disViRETceuutMXjw4Ba/Vl5e3tx89+jRo3l/oVBo8b0AAEDnknjDhvQZkO5x7TBixIgoLy+PxYsXx0c+8pEW25AhQ9o0xvbbbx/z5s1rse/9XwMAAKUj8YYNqRvd+Ax3/ZKIWNuSCO8+4103OvVTV1ZWxmmnnRannnpqFIvF2GeffaK+vj7uu+++6NOnT9TV1W1wjJNPPjkmTpwYu+22W+yzzz5xzTXXxBNPPBHDhw9PvV4AAKA1jTdsSLeyiIMvaFy9PArRsvlunNYdB3+vZO/zPvfcc6OmpiamTp0azz77bPTt2zd23XXX+Pa3v92m6eSf//zn45lnnonTTz89Vq5cGZ/5zGfixBNPjNtvv70k9QIAAC1Z1ZxcSGU17gW3RNx2esuF1qoGNzbdIz6RTqEZZ1VzAABoP4k3tNWIT0RsN75x9fI3X258prtudMmSbgAAoGvQeEN7dCuLGPYvnV0FAACwGbGqOQAAAJSQxhsAAABKSONNrlhLsGP8/AAAoP003uRCWVnjAmirV6/u5Eo2b2+99VZERPTo0aOTKwEAgM2HxdXIhe7du0fv3r3jlVdeiR49ekS3bv7NqT2SJIm33norli5dGn379m3+hwwAAGDDvMeb3Fi9enUsXLgwisViZ5ey2erbt28MHDgwCoVCZ5cCAACbDY03uVIsFk0330g9evSQdAMAwEbQeAMAAEAJedAVAAAASkjjDQAAACWk8QYAAIAS0ngDAABACWm8AQAAoIQ03gAAAFBCGm8AAAAoof8PcPgw4FUMv3IAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rng = np.random.RandomState(seed=123)\n", + "\n", + "problem = factory.generate_problem('rooms', 8, rng)\n", + "problem.visualize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Read a previously stored problem from a JSON file\n", + "\n", + "The function `create_problem_from_json` takes the file path of a stored problem instance as a parameter." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAIcCAYAAAAAOnYgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAPYBJREFUeJzt3XucVXW9N/Dv5jYDzgwc0OEmDVChEoi3LNEUDt4Q0a6WqYGWz3MUz5OWz6OlBerpkFQeO4lYnfKSF0Q92IXyxPFeiuKtDMqOiWIKQnIQxVCYvZ4/ppnjCAyzcW02a6/3+/Var9qLtX/rt9ce/PGdz2/9ViFJkiQAAACAsuhS6Q4AAABANVN4AwAAQBkpvAEAAKCMFN4AAABQRgpvAAAAKCOFNwAAAJSRwhsAAADKSOENAAAAZaTwBgAAgDJSeENEFAqFTm333HNP3HPPPVEoFOLWW28te78ef/zxOOyww6J3795RKBTi8ssvbzv/PffcU3J7pbx36tSpMXTo0JLPAVBtdtYxYkd68cUXY8aMGfHEE0+k3vZpp50WRx99dLt9Wxr/Kmnp0qUxY8aMePbZZyvaj7d79tlno1AoxDXXXFPpruy0rrzyyi1eny1du2uuuSYKhcJ2fc8/+MEPYvDgwbF+/frt7yxVrVulOwA7gwcffLDd60suuSTuvvvuuOuuu9rtHzlyZDz22GM7rF+nnXZarF+/PubOnRt/93d/F0OHDo1evXrFgw8+GCNHjtxh/QDIs511jNiRXnzxxbjoooti6NChsc8++6TW7uOPPx7XXnttPPTQQ+32b2n8q6SlS5fGRRddFOPGjat4X95q4MCB8eCDD8a73/3uSndlp3XllVfGrrvuGlOnTm23P+1rN2XKlLj00ktj1qxZcdFFF6XSJtVF4Q0R8cEPfrDd69122y26dOmy2f4d7Xe/+12cfvrpMXHixHb7K90vgDzZWceIavD1r389DjzwwDjggAPa7d/a+Le9Nm7cGIVCIbp1q65/+tbU1Pg53E5pX7tu3brF//7f/zsuueSSOO+886JXr16ptU11MNUcttPGjRvjggsuiEGDBkVDQ0Mcfvjh8dRTT2123H/+53/GhAkToqGhIXr16hUHH3xw3HnnnR223TrVadOmTTFnzpy2aYwRW58u/sgjj8Rxxx0Xffv2jdra2th3331j3rx5nfos11xzTeyxxx5RU1MTe+21V1x33XWduwgAbFE5x4hWa9eujS9+8YsxfPjwqKmpicbGxjjmmGPiD3/4Q9sxa9asiTPPPDMGDx4cPXr0iOHDh8cFF1wQb7zxRru2brnllvjABz4QvXv3jl69esXw4cPjtNNOi4iWcef9739/RESceuqpbWPSjBkzIiLimWeeiU996lMxaNCgqKmpif79+8eECRO2OS39pZdeivnz58cpp5zStq+j8S+ipSA//vjj4+/+7u+itrY29tlnn7j22mvbtds6Tv7oRz+KL37xizF48OCoqamJp59+eqt9mTNnTowZMybq6uqivr4+9txzz/jyl7/c1qdPfOITERExfvz4tj69dYpyZ77HGTNmRKFQiMcffzw++tGPRkNDQ/Tu3TtOPvnkWL16dbtjhw4dGscee2zMnz8/9t5776itrY3hw4fHv/7rv7Y7bkvTpVvPs2TJkjjxxBOjd+/e0b9//zjttNPilVdeaff+tWvXxmc/+9no27dv1NXVxaRJk+KZZ55p9/12ZPny5XHyySdHY2Nj278hvvWtb0WxWNysj9/85jfjsssui2HDhkVdXV0cdNBBsWjRom2eY/Xq1XHmmWfGyJEjo66uLhobG+Pv//7v4/7779/me4cOHRpLliyJe++9t+17a52xUMo0/c7+PT3ppJNi3bp1MXfu3G22Sf4ovGE7ffnLX47nnnsu/u3f/i2+973vxX/913/F5MmTo7m5ue2Y66+/Po488shoaGiIa6+9NubNmxd9+/aNo446qsN/WE2aNKltauPHP/7xePDBBzeb6vhWd999dxx88MGxdu3auOqqq+LHP/5x7LPPPvHJT35ymwPKNddcE6eeemrstddecdttt8WFF14Yl1xyyWZTKAHovHKOERERr776ahxyyCHx3e9+N0499dT46U9/GldddVWMGDEiVqxYERERGzZsiPHjx8d1110XX/jCF2LBggVx8sknx6xZs+KjH/1oW1sPPvhgfPKTn4zhw4fH3LlzY8GCBfHVr341Nm3aFBER++23X1x99dUREXHhhRe2jUmf+9znIiLimGOOiUcffTRmzZoVCxcujDlz5sS+++4ba9eu7fAz/PKXv4yNGzfG+PHj2/Z1NP499dRTMXbs2FiyZEn867/+a/z7v/97jBw5MqZOnRqzZs3arP0vfelLsXz58rjqqqvipz/9aTQ2Nm6xH3Pnzo0zzzwzDjvssJg/f37cfvvtcc4557Tdqztp0qT453/+54iImD17dlufJk2aFBGlf48f+chH4j3veU/ceuutMWPGjLj99tvjqKOOio0bN7Y77oknnoizzz47zjnnnJg/f36MHTs2Pv/5z8c3v/nNDq9rq4997GMxYsSIuO222+L888+PG2+8Mc4555y2Py8WizF58uS48cYb47zzzov58+fHBz7wgc3ut9+a1atXx9ixY+OXv/xlXHLJJfGTn/wkDj/88Dj33HPjrLPO2uz42bNnx8KFC+Pyyy+PG264IdavXx/HHHPMZr8MeLs1a9ZERMT06dNjwYIFcfXVV8fw4cNj3Lhx21yzZv78+TF8+PDYd9992763+fPnd+rztSrl+x0wYEDsueeesWDBgpLOQU4kwGamTJmS7LLLLlv8s7vvvjuJiOSYY45pt3/evHlJRCQPPvhgkiRJsn79+qRv377J5MmT2x3X3NycjBkzJjnwwAO32Y+ISKZNm7bF8999991t+/bcc89k3333TTZu3Nju2GOPPTYZOHBg0tzcvMX3Njc3J4MGDUr222+/pFgstr3v2WefTbp37540NTVts48AebMzjBEXX3xxEhHJwoULt3rMVVddlUREMm/evHb7L7300iQikl/+8pdJkiTJN7/5zSQikrVr1261rcWLFycRkVx99dXt9v/lL39JIiK5/PLLO+zvlpxxxhlJz549240/rbY0/n3qU59KampqkuXLl7fbP3HixKRXr15t/W/9Dg499NBO9eOss85K+vTp0+Ext9xyy2Zjb5KU9j1Onz49iYjknHPOaXfsDTfckEREcv3117fta2pqSgqFQvLEE0+0O/aII45IGhoakvXr1ydJkiTLli3b7HtpPc+sWbPavffMM89Mamtr2673ggULkohI5syZ0+64mTNnJhGRTJ8+vcNrcv755ycRkTz00EPt9p9xxhlJoVBInnrqqXZ9HD16dLJp06a24x5++OEkIpKbbrqpw/O83aZNm5KNGzcmEyZMSD7ykY9s8/j3ve99yWGHHbbZ/i1du6uvvjqJiGTZsmVJkmzf39OTTjop6d+/f0mfiXyQeMN2Ou6449q93nvvvSMi4rnnnouIiAceeCDWrFkTU6ZMiU2bNrVtxWIxjj766Fi8eHEqK18+/fTT8Yc//CFOOumkiIh25zrmmGNixYoVW5zeGNGSHrz44ovx6U9/ut1Uvqamphg7duw77htAXpV7jPjFL34RI0aMiMMPP3yrx9x1112xyy67xMc//vF2+1sXmWpN61qnkZ9wwgkxb968eOGFFzr9Ofv27Rvvfve74xvf+EZcdtll8fjjj7ebZtyRF198MXbbbbd2409H7rrrrpgwYUIMGTKk3f6pU6fG66+/vtnMsI997GOdavfAAw+MtWvXxoknnhg//vGP4y9/+Uun3hexfd9j63jd6oQTTohu3brF3Xff3W7/+973vhgzZky7fZ/+9Kdj3bp1nVrEb0s/gxs2bIhVq1ZFRMS9997bdv63OvHEE7fZdkTL9zFy5Mg48MAD2+2fOnVqJEmy2cy5SZMmRdeuXdv1J+J//k505Kqrror99tsvamtro1u3btG9e/e488474/e//32n+rq9tuf7bWxsjFWrVrXNGIFWCm/YTv369Wv3uqamJiIi/vrXv0ZEy71rES1T5bp3795uu/TSSyNJkrbpU+9E63nOPffczc5z5plnRkRs9R8RL7/8ckS0TI16uy3tA6Bzyj1GrF69OnbfffcO+/Dyyy/HgAEDNitsGxsbo1u3bm1jwKGHHhq33357bNq0KT7zmc/E7rvvHqNGjYqbbrppm5+zUCjEnXfeGUcddVTMmjUr9ttvv9htt93i//yf/xOvvvpqh+/961//GrW1tds8x1s/z8CBAzfbP2jQoLY/f6stHbslp5xySvzwhz+M5557Lj72sY9FY2NjfOADH4iFCxdu873b8z2+fXzt1q1b9OvXb7P+dzQ2v/3YLdnWz+DLL78c3bp1i759+7Y7rn///ttsu/X9pXwf2+rP1lx22WVxxhlnxAc+8IG47bbbYtGiRbF48eI4+uijt/ned2p7vt/a2tpIkiQ2bNhQ1r6RPdW1tCPsRHbdddeIiPjOd76z1VUzOzu4deY8X/rSl9rds/dWe+yxxxb3tw6CK1eu3OzPtrQPgHS80zFit912iz//+c8dnqNfv37x0EMPRZIk7Yrv1jSutQ8REccff3wcf/zx8cYbb8SiRYti5syZ8elPfzqGDh0aBx10UIfnaWpqih/84AcREfHHP/4x5s2bFzNmzIg333wzrrrqqq2+b9dddy3p8Wv9+vVru3/9rV588cW29t6qs0l6RMuicaeeemqsX78+7rvvvpg+fXoce+yx8cc//jGampq2+r7t+R5XrlwZgwcPbnu9adOmePnllzcrTDsam99+7Pbo169fbNq0KdasWdOu+O7s+F/q97G9rr/++hg3blzMmTOn3f5t/WInDdvz/a5ZsyZqamqirq6u7P0jWyTeUCYHH3xw9OnTJ5YuXRoHHHDAFrcePXq84/Psscce8d73vjd+85vfbPU89fX1W33vwIED46abbookSdr2P/fcc/HAAw+8474BsGXvdIyYOHFi/PGPf+xwIcwJEybEa6+9Frfffnu7/a1PrpgwYcJm76mpqYnDDjssLr300ohoec526/6IbaeTI0aMiAsvvDBGjx69zaJ6zz33jJdffnmbi2u99fPcddddbYXdWz9Pr169Unk01C677BITJ06MCy64IN58881YsmRJRGz982/P93jDDTe0ez1v3rzYtGlTjBs3rt3+JUuWxG9+85t2+2688caor6+P/fbb7x1/1sMOOywiIm6++eZ2+zu7IveECRNi6dKlm33P1113XRQKhXaL5r0ThUKh7fq3+u1vf9vhorNvVVNTs93J+PZ8v88880yMHDlyu85HdZN4Q5nU1dXFd77znZgyZUqsWbMmPv7xj0djY2OsXr06fvOb38Tq1as3++3t9vrud78bEydOjKOOOiqmTp0agwcPjjVr1sTvf//7eOyxx+KWW27Z4vu6dOkSl1xySXzuc5+Lj3zkI3H66afH2rVrY8aMGaaaA5TROx0jzj777Lj55pvj+OOPj/PPPz8OPPDA+Otf/xr33ntvHHvssTF+/Pj4zGc+E7Nnz44pU6bEs88+G6NHj45f/epX8c///M9xzDHHtN0f/tWvfjX+/Oc/x4QJE2L33XePtWvXxre//e3o3r17W3H27ne/O3r27Bk33HBD7LXXXlFXVxeDBg2Kv/zlL3HWWWfFJz7xiXjve98bPXr0iLvuuit++9vfxvnnn9/hNRg3blwkSRIPPfRQHHnkkdu8ZtOnT4+f/exnMX78+PjqV78affv2jRtuuCEWLFgQs2bNit69e5fwDfyP008/PXr27BkHH3xwDBw4MFauXBkzZ86M3r17t93/PmrUqIiI+N73vhf19fVRW1sbw4YNi379+pX8Pf77v/97dOvWLY444ohYsmRJfOUrX4kxY8Zsdq/1oEGD4rjjjosZM2bEwIED4/rrr4+FCxfGpZdemsozoo8++ug4+OCD44tf/GKsW7cu9t9//3jwwQfbfjHTpUvH+dw555wT1113XUyaNCkuvvjiaGpqigULFsSVV14ZZ5xxRowYMeId9zEi4thjj41LLrkkpk+fHocddlg89dRTcfHFF8ewYcM6dR/16NGjY+7cuXHzzTfH8OHDo7a2NkaPHt2pc5f697RYLMbDDz8cn/3sZ7f781LFKrasG+zEOrNi7S233NJu/5ZWx0ySJLn33nuTSZMmJX379k26d++eDB48OJk0adJm79+S6OSq5kmSJL/5zW+SE044IWlsbEy6d++eDBgwIPn7v//75Kqrrtrme//t3/4tee9735v06NEjGTFiRPLDH/4wmTJlilXNAbZgZxkj/vu//zv5/Oc/n7zrXe9KunfvnjQ2NiaTJk1K/vCHP7Qd8/LLLyf/8A//kAwcODDp1q1b0tTUlHzpS19KNmzY0HbMz372s2TixInJ4MGDkx49eiSNjY3JMccck9x///3tznfTTTcle+65Z9K9e/e2Va9feumlZOrUqcmee+6Z7LLLLkldXV2y9957J//yL//SbgXrLWlubk6GDh2anHnmmZv92ZbGvyRJkieffDKZPHly0rt376RHjx7JmDFjNrumW/sOtubaa69Nxo8fn/Tv3z/p0aNHMmjQoOSEE05Ifvvb37Y77vLLL0+GDRuWdO3adbPvsjPfY+tq448++mgyefLkpK6uLqmvr09OPPHE5KWXXmp3rqampmTSpEnJrbfemrzvfe9LevTokQwdOjS57LLL2h3X0armq1evbnfs21fsTpIkWbNmTXLqqacmffr0SXr16pUcccQRyaJFi5KISL797W9v89o999xzyac//emkX79+Sffu3ZM99tgj+cY3vtH2NJW39vEb3/jGZu+PTqye/sYbbyTnnntuMnjw4KS2tjbZb7/9kttvv73T/0559tlnkyOPPDKpr69PIqLtPZ1Z1bxVZ/+e3nnnnW3fMbxdIUneMr8UAAB2kG9961vxta99LV544YXo2bNnpbtTVjNmzIiLLrooVq9evc37n4cOHRqjRo2Kn/3sZzuod//jxhtvjJNOOil+/etfe8JJiU455ZR45pln4te//nWlu8JOyFRzAAAqYtq0aXHFFVfE7Nmz49xzz610d3LnpptuihdeeCFGjx4dXbp0iUWLFsU3vvGN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LbobGxujV69eiscSJEkSr7/+eqxatSoiIgYOHFjhHgEAwM5P4U2uNDc3txXd/fr1q3R3Mqlnz54REbFq1apobGw07RwAALbB4mrkSus93b169apwT7Kt9fq5Rx4AALZN4U0umV7+zrh+AADQeQpvAAAAKCOFNwAAAJSRwhsyburUqfHhD384tfbGjRsXZ599dmrtAQBA3lnVHLZDczGJh5etiVWvbojG+to4cFjf6Nol2/c9b9y4Mbp3717pbgAAQNWReEOJ7vjdijjk0rvixO8vis/PfSJO/P6iOOTSu+KO360o63lvvfXWGD16dPTs2TP69esXhx9+ePzf//t/49prr40f//jHUSgUolAoxD333BMREeedd16MGDEievXqFcOHD4+vfOUr7VYhnzFjRuyzzz7xwx/+MIYPHx41NTUxZcqUuPfee+Pb3/52W3vPPvtsWT8XAABUO4k3lOCO362IM65/LJK37V/5yoY44/rHYs7J+8XRowamft4VK1bEiSeeGLNmzYqPfOQj8eqrr8b9998fn/nMZ2L58uWxbt26uPrqqyMiom/fvhERUV9fH9dcc00MGjQonnzyyTj99NOjvr4+/t//+39t7T799NMxb968uO2226Jr167R1NQU//Vf/xWjRo2Kiy++OCIidtttt9Q/DwAA5InCGzqpuZjERT9dulnRHRGRREQhIi766dI4YuSA1Kedr1ixIjZt2hQf/ehHo6mpKSIiRo8eHRERPXv2jDfeeCMGDBjQ7j0XXnhh2/8fOnRofPGLX4ybb765XeH95ptvxo9+9KN2xXWPHj2iV69em7UHAABsH1PNoZMeXrYmVryyYat/nkTEilc2xMPL1qR+7jFjxsSECRNi9OjR8YlPfCK+//3vx3//9393+J5bb701DjnkkBgwYEDU1dXFV77ylVi+fHm7Y5qamiTaAABQZgpv6KRVr2696N6e40rRtWvXWLhwYfziF7+IkSNHxne+853YY489YtmyZVs8ftGiRfGpT30qJk6cGD/72c/i8ccfjwsuuCDefPPNdsftsssuqfcVAABoz1Rz6KTG+tpUjytVoVCIgw8+OA4++OD46le/Gk1NTTF//vzo0aNHNDc3tzv217/+dTQ1NcUFF1zQtu+5557r1Hm21B4AALD9FN7QSQcO6xsDe9fGylc2bPE+70JEDOjd8mixtD300ENx5513xpFHHhmNjY3x0EMPxerVq2OvvfaKDRs2xH/8x3/EU089Ff369YvevXvHe97znli+fHnMnTs33v/+98eCBQti/vz5nTrX0KFD46GHHopnn3026urqom/fvtGli8kxAACwvfxrGjqpa5dCTJ88MiJaiuy3an09ffLIsjzPu6GhIe6777445phjYsSIEXHhhRfGt771rZg4cWKcfvrpsccee8QBBxwQu+22W/z617+O448/Ps4555w466yzYp999okHHnggvvKVr3TqXOeee2507do1Ro4cGbvttttm94UDAAClKSRJsqXwDqrShg0bYtmyZTFs2LCord2+KeF3/G5FXPTTpe0WWhvYuzamTx5ZlkeJ7YzSuI4AAJAXpppDiY4eNTCOGDkgHl62Jla9uiEa61uml5cj6QYAALJP4Q3boWuXQhz07n6V7gYAAJAB7vEGAACAMlJ4AwAAQBkpvAEAAKCMFN4AAABQRgpvAAAAKCOFNwAAAJSRwhsAAADKSOENOTZjxozYZ599Kt0NAACoagpvAAAAKKNule4AZFKxOeK5ByJeeymirn9E09iILl0r3SsAAGAnJPGGUi39ScTloyKuPTbits+2/O/lo1r2l1GSJDFr1qwYPnx49OzZM8aMGRO33nprRETcc889USgU4s4774wDDjggevXqFWPHjo2nnnqqXRtf//rXo3///lFfXx+f/exnY8OGDWXtMwAAoPCG0iz9ScS8z0Sse7H9/nUrWvaXsfi+8MIL4+qrr445c+bEkiVL4pxzzomTTz457r333rZjLrjggvjWt74VjzzySHTr1i1OO+20tj+bN29eTJ8+Pb72ta/FI488EgMHDowrr7yybP0FAABaFJIkSSrdCdhRNmzYEMuWLYthw4ZFbW1taW8uNrck228vutsUIhoGRZz9ZOrTztevXx+77rpr3HXXXXHQQQe17f/c5z4Xr7/+evyv//W/Yvz48fGf//mfMWHChIiI+PnPfx6TJk2Kv/71r1FbWxtjx46NMWPGxJw5c9re/8EPfjA2bNgQTzzxREn9eUfXEQAAckbiDZ313AMdFN0REUnEuhdajkvZ0qVLY8OGDXHEEUdEXV1d23bdddfFn/70p7bj9t5777b/P3DgwIiIWLVqVURE/P73v29XtEfEZq8BAID0WVwNOuu1l9I9rgTFYjEiIhYsWBCDBw9u92c1NTVtxXf37t3b9hcKhXbvBQAAKkPiDZ1V1z/d40owcuTIqKmpieXLl8d73vOedtuQIUM61cZee+0VixYtarfv7a8BAID0Sbyhs5rGttzDvW5FRGxpaYS/3ePdNDb1U9fX18e5554b55xzThSLxTjkkENi3bp18cADD0RdXV00NTVts43Pf/7zMWXKlDjggAPikEMOiRtuuCGWLFkSw4cPT72/AADA/1B4Q2d16Rpx9KUtq5dHIdoX3y3TuuPor5fted6XXHJJNDY2xsyZM+OZZ56JPn36xH777Rdf/vKXOzWd/JOf/GT86U9/ivPOOy82bNgQH/vYx+KMM86I//iP/yhLfwEAgBZWNSdXUlmNe+lPIu44r/1Caw2DW4rukcel09GdnFXNAQCg8yTeUKqRx0XsOall9fLXXmq5p7tpbNmSbgAAINsU3rA9unSNGPahSvcCAADIAKuaAwAAQBkpvAEAAKCMFN7kkjUF3xnXDwAAOk/hTa507949IiJef/31Cvck21qvX+v1BAAAts7iauRK165do0+fPrFq1aqIiOjVq1cUCoUK9yo7kiSJ119/PVatWhV9+vSJrl2t5A4AANviOd7kTpIksXLlyli7dm2lu5JZffr0iQEDBvilBQAAdILCm9xqbm6OjRs3VrobmdO9e3dJNwAAlEDhDQAAAGVkcTUAAAAoI4U3AAAAlJHCGwAAAMpI4Q0AAABlpPAGAACAMlJ4AwAAQBkpvAEAAKCM/j9Sg0LHBUDMTgAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "problem = factory.create_problem_from_json('boards/tiny0.json')\n", + "problem.visualize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Create a problem manually\n", + "\n", + "We can also manually create problem by passing a dictionary that contains the keys `board`, `costs`, `start_state`, and `end_state`. Here you might notice that transposed versions of the board and costs are visualized." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAIcCAYAAAAAOnYgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAQqpJREFUeJzt3XucVXW9P/735jYDwgyBjlykASxUBFHzkuhJOXhDJCvzlhpo+j1fxXO8ZD/NS6AeI6n82knE7BTexVuo5UnDG1aKd82gNBXBFEUlHEVBmVm/P2jmODIMs2FtN2vW8/l4rEfuxVqf9dl7D314z+uzPquQJEkSAAAAQEl0KHcHAAAAoD1TeAMAAEAJKbwBAACghBTeAAAAUEIKbwAAACghhTcAAACUkMIbAAAASkjhDQAAACWk8AYAAIASUnhDRBQKhTZtDzzwQDzwwANRKBTilltuKXm/nnrqqdhzzz2juro6CoVCXHLJJU3Xf+CBB4pur5hzJ0yYEAMHDiz6GgDtzcY6RnyaXnvttZg8eXI8/fTTqbd97LHHxv77799sX0vjXznNnz8/Jk+eHC+//HJZ+/FJL7/8chQKhbjyyivL3ZWN1mWXXdbi59PSZ3fllVdGoVBYr+/5F7/4RfTv3z+WL1++/p2lXetU7g7AxuDhhx9u9vqCCy6I+++/P+67775m+4cOHRpPPvnkp9avY489NpYvXx4zZ86Mz3zmMzFw4MDo1q1bPPzwwzF06NBPrR8AebaxjhGfptdeey3OO++8GDhwYGy//faptfvUU0/FVVddFY888kiz/S2Nf+U0f/78OO+882KvvfYqe18+rm/fvvHwww/HlltuWe6ubLQuu+yy2HTTTWPChAnN9qf92Y0fPz4uuuiimDp1apx33nmptEn7ovCGiPjiF7/Y7PVmm20WHTp0WGP/p+3Pf/5zHH/88TFmzJhm+8vdL4A82VjHiPbgBz/4Qeyyyy6x0047Ndu/tvFvfX300UdRKBSiU6f29U/fiooKP4frKe3PrlOnTvFv//ZvccEFF8QZZ5wR3bp1S61t2gdTzWE9ffTRR3H22WdHv379oqqqKvbee+947rnn1jjunnvuidGjR0dVVVV069Ytdt9997j33ntbbbtxqtOqVati+vTpTdMYI9Y+Xfzxxx+PL3/5y9GrV6+orKyMHXbYIW666aY2vZcrr7wyttpqq6ioqIhtttkmrr766rZ9CAC0qJRjRKNly5bFt7/97Rg8eHBUVFRETU1NHHDAAfHXv/616ZilS5fGiSeeGP37948uXbrE4MGD4+yzz46VK1c2a+vmm2+OXXfdNaqrq6Nbt24xePDgOPbYYyNi9biz8847R0TEMccc0zQmTZ48OSIiXnrppTj88MOjX79+UVFREZtvvnmMHj16ndPS33jjjZg1a1YcffTRTftaG/8iVhfkBx10UHzmM5+JysrK2H777eOqq65q1m7jOHnNNdfEt7/97ejfv39UVFTECy+8sNa+TJ8+PUaMGBHdu3ePHj16xNZbbx1nnXVWU58OOeSQiIgYNWpUU58+PkW5Ld/j5MmTo1AoxFNPPRVf+9rXoqqqKqqrq+Ooo46KN998s9mxAwcOjAMPPDBmzZoV2223XVRWVsbgwYPjv/7rv5od19J06cbrzJs3L4444oiorq6OzTffPI499th45513mp2/bNmy+Na3vhW9evWK7t27x9ixY+Oll15q9v22ZtGiRXHUUUdFTU1N078hfvzjH0dDQ8MaffzRj34UF198cQwaNCi6d+8eu+22W8ydO3ed13jzzTfjxBNPjKFDh0b37t2jpqYm/vVf/zV+//vfr/PcgQMHxrx582LOnDlN31vjjIVipum39e/pkUceGXV1dTFz5sx1tkn+KLxhPZ111lmxcOHC+O///u+44oor4m9/+1uMGzcu6uvrm4659tprY999942qqqq46qqr4qabbopevXrFfvvt1+o/rMaOHds0tfHrX/96PPzww2tMdfy4+++/P3bfffdYtmxZXH755XH77bfH9ttvH4cddtg6B5Qrr7wyjjnmmNhmm23i1ltvjXPOOScuuOCCNaZQAtB2pRwjIiLefffd2GOPPeJnP/tZHHPMMfHrX/86Lr/88hgyZEgsXrw4IiJWrFgRo0aNiquvvjpOO+20uPPOO+Ooo46KqVOnxte+9rWmth5++OE47LDDYvDgwTFz5sy4884743vf+16sWrUqIiJ23HHHmDFjRkREnHPOOU1j0nHHHRcREQcccEA88cQTMXXq1Jg9e3ZMnz49dthhh1i2bFmr7+F3v/tdfPTRRzFq1Kimfa2Nf88991yMHDky5s2bF//1X/8Vv/rVr2Lo0KExYcKEmDp16hrtf/e7341FixbF5ZdfHr/+9a+jpqamxX7MnDkzTjzxxNhzzz1j1qxZcdttt8Wpp57adK/u2LFj4/vf/35EREybNq2pT2PHjo2I4r/Hr371q/G5z30ubrnllpg8eXLcdtttsd9++8VHH33U7Linn346TjnllDj11FNj1qxZMXLkyDj55JPjRz/6Uaufa6ODDz44hgwZErfeemuceeaZcf3118epp57a9OcNDQ0xbty4uP766+OMM86IWbNmxa677rrG/fZr8+abb8bIkSPjd7/7XVxwwQVxxx13xN577x2nn356nHTSSWscP23atJg9e3Zccsklcd1118Xy5cvjgAMOWOOXAZ+0dOnSiIiYNGlS3HnnnTFjxowYPHhw7LXXXutcs2bWrFkxePDg2GGHHZq+t1mzZrXp/TUq5vvt06dPbL311nHnnXcWdQ1yIgHWMH78+GSTTTZp8c/uv//+JCKSAw44oNn+m266KYmI5OGHH06SJEmWL1+e9OrVKxk3blyz4+rr65MRI0Yku+yyyzr7ERHJxIkTW7z+/fff37Rv6623TnbYYYfko48+anbsgQcemPTt2zepr69v8dz6+vqkX79+yY477pg0NDQ0nffyyy8nnTt3Tmpra9fZR4C82RjGiPPPPz+JiGT27NlrPebyyy9PIiK56aabmu2/6KKLkohIfve73yVJkiQ/+tGPkohIli1btta2HnvssSQikhkzZjTb/9ZbbyURkVxyySWt9rclJ5xwQtK1a9dm40+jlsa/ww8/PKmoqEgWLVrUbP+YMWOSbt26NfW/8Tv40pe+1KZ+nHTSSUnPnj1bPebmm29eY+xNkuK+x0mTJiURkZx66qnNjr3uuuuSiEiuvfbapn21tbVJoVBInn766WbH7rPPPklVVVWyfPnyJEmSZMGCBWt8L43XmTp1arNzTzzxxKSysrLp877zzjuTiEimT5/e7LgpU6YkEZFMmjSp1c/kzDPPTCIieeSRR5rtP+GEE5JCoZA899xzzfo4fPjwZNWqVU3HPfroo0lEJDfccEOr1/mkVatWJR999FEyevTo5Ktf/eo6j992222TPffcc439LX12M2bMSCIiWbBgQZIk6/f39Mgjj0w233zzot4T+SDxhvX05S9/udnr7bbbLiIiFi5cGBERDz30UCxdujTGjx8fq1atatoaGhpi//33j8ceeyyVlS9feOGF+Otf/xpHHnlkRESzax1wwAGxePHiFqc3RqxOD1577bX4xje+0WwqX21tbYwcOXKD+waQV6UeI37729/GkCFDYu+9917rMffdd19ssskm8fWvf73Z/sZFphrTusZp5IceemjcdNNN8eqrr7b5ffbq1Su23HLL+OEPfxgXX3xxPPXUU82mGbfmtddei80226zZ+NOa++67L0aPHh0DBgxotn/ChAnx/vvvrzEz7OCDD25Tu7vsskssW7YsjjjiiLj99tvjrbfeatN5Eev3PTaO140OPfTQ6NSpU9x///3N9m+77bYxYsSIZvu+8Y1vRF1dXZsW8WvpZ3DFihWxZMmSiIiYM2dO0/U/7ogjjlhn2xGrv4+hQ4fGLrvs0mz/hAkTIkmSNWbOjR07Njp27NisPxH/+3eiNZdffnnsuOOOUVlZGZ06dYrOnTvHvffeG3/5y1/a1Nf1tT7fb01NTSxZsqRpxgg0UnjDeurdu3ez1xUVFRER8cEHH0TE6nvXIlZPlevcuXOz7aKLLookSZqmT22Ixuucfvrpa1znxBNPjIhY6z8i3n777YhYPTXqk1raB0DblHqMePPNN2OLLbZotQ9vv/129OnTZ43CtqamJjp16tQ0BnzpS1+K2267LVatWhXf/OY3Y4sttohhw4bFDTfcsM73WSgU4t5774399tsvpk6dGjvuuGNsttlm8R//8R/x7rvvtnruBx98EJWVleu8xsffT9++fdfY369fv6Y//7iWjm3J0UcfHb/85S9j4cKFcfDBB0dNTU3suuuuMXv27HWeuz7f4yfH106dOkXv3r3X6H9rY/Mnj23Jun4G33777ejUqVP06tWr2XGbb775OttuPL+Y72Nd/Vmbiy++OE444YTYdddd49Zbb425c+fGY489Fvvvv/86z91Q6/P9VlZWRpIksWLFipL2jexpX0s7wkZk0003jYiIn/70p2tdNbOtg1tbrvPd73632T17H7fVVlu1uL9xEHz99dfX+LOW9gGQjg0dIzbbbLP4+9//3uo1evfuHY888kgkSdKs+G5M4xr7EBFx0EEHxUEHHRQrV66MuXPnxpQpU+Ib3/hGDBw4MHbbbbdWr1NbWxu/+MUvIiLi+eefj5tuuikmT54cH374YVx++eVrPW/TTTct6vFrvXv3brp//eNee+21pvY+rq1JesTqReOOOeaYWL58eTz44IMxadKkOPDAA+P555+P2tratZ63Pt/j66+/Hv379296vWrVqnj77bfXKExbG5s/eez66N27d6xatSqWLl3arPhu6/hf7Pexvq699trYa6+9Yvr06c32r+sXO2lYn+936dKlUVFREd27dy95/8gWiTeUyO677x49e/aM+fPnx0477dTi1qVLlw2+zlZbbRWf//zn45lnnlnrdXr06LHWc/v27Rs33HBDJEnStH/hwoXx0EMPbXDfAGjZho4RY8aMieeff77VhTBHjx4d7733Xtx2223N9jc+uWL06NFrnFNRURF77rlnXHTRRRGx+jnbjfsj1p1ODhkyJM4555wYPnz4OovqrbfeOt5+++11Lq718fdz3333NRV2H38/3bp1S+XRUJtsskmMGTMmzj777Pjwww9j3rx5EbH2978+3+N1113X7PVNN90Uq1atir322qvZ/nnz5sUzzzzTbN/1118fPXr0iB133HGD3+uee+4ZERE33nhjs/1tXZF79OjRMX/+/DW+56uvvjoKhUKzRfM2RKFQaPr8G/3pT39qddHZj6uoqFjvZHx9vt+XXnophg4dul7Xo32TeEOJdO/ePX7605/G+PHjY+nSpfH1r389ampq4s0334xnnnkm3nzzzTV+e7u+fvazn8WYMWNiv/32iwkTJkT//v1j6dKl8Ze//CWefPLJuPnmm1s8r0OHDnHBBRfEcccdF1/96lfj+OOPj2XLlsXkyZNNNQcooQ0dI0455ZS48cYb46CDDoozzzwzdtlll/jggw9izpw5ceCBB8aoUaPim9/8ZkybNi3Gjx8fL7/8cgwfPjz+8Ic/xPe///044IADmu4P/973vhd///vfY/To0bHFFlvEsmXL4ic/+Ul07ty5qTjbcssto2vXrnHdddfFNttsE927d49+/frFW2+9FSeddFIccsgh8fnPfz66dOkS9913X/zpT3+KM888s9XPYK+99ookSeKRRx6Jfffdd52f2aRJk+I3v/lNjBo1Kr73ve9Fr1694rrrros777wzpk6dGtXV1UV8A//r+OOPj65du8buu+8effv2jddffz2mTJkS1dXVTfe/Dxs2LCIirrjiiujRo0dUVlbGoEGDonfv3kV/j7/61a+iU6dOsc8++8S8efPi3HPPjREjRqxxr3W/fv3iy1/+ckyePDn69u0b1157bcyePTsuuuiiVJ4R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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "data = {\n", + " 'board': [\n", + " [0, 1, 0],\n", + " [0, 1, 0],\n", + " [0, 0, 0]\n", + " ],\n", + " 'costs': [\n", + " [9, 9, 9],\n", + " [9, 0, 9],\n", + " [9, 0, 9]\n", + " ],\n", + " 'start_state': [0, 0],\n", + " 'end_state': [2, 2]\n", + "}\n", + "\n", + "problem = factory.create_problem_from_dict(data)\n", + "problem.visualize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interacting with problems\n", + "\n", + "Let's create a small problem named `maze`:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAIcCAYAAAAAOnYgAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAARV5JREFUeJzt3XucVWW9P/DvZoAZkJkh0JGLNICFiuItL4mmEt4QycqyTA20/J2j0zlq2U/NC6hHTSqzk4jpKbyLpqGWvzTUxEox7xmUZl4wRTEJR1EUZtbvD2SOIzDMHtd2s/Z6v1+v9aq9WOtZz5oNPnz5POtZhSRJkgAAAABKolu5OwAAAACVTOENAAAAJaTwBgAAgBJSeAMAAEAJKbwBAACghBTeAAAAUEIKbwAAACghhTcAAACUkMIbAAAASkjhDRFRKBQ6td19991x9913R6FQiBtuuKHk/XrkkUdijz32iPr6+igUCnHBBRe0Xf/uu+8uur1izp00aVIMHTq06GsAVJr1dYz4ML344osxZcqUePTRR1Nv+8gjj4z99tuv3b41jX/lNH/+/JgyZUo8++yzZe3H+z377LNRKBTisssuK3dX1lsXXXTRGn8+a/rZXXbZZVEoFLr0Pf/0pz+NwYMHx9KlS7veWSpa93J3ANYH9913X7vPZ511Vvz2t7+Nu+66q93+kSNHxsMPP/yh9evII4+MpUuXxsyZM+MjH/lIDB06NHr37h333XdfjBw58kPrB0Cera9jxIfpxRdfjDPOOCOGDh0a2267bWrtPvLII3H55ZfH/fff327/msa/cpo/f36cccYZseeee5a9L+81cODAuO+++2LTTTctd1fWWxdddFFsuOGGMWnSpHb70/7ZTZw4Mc4777yYOnVqnHHGGam0SWVReENEfPKTn2z3eaONNopu3bqttv/D9uc//zmOOuqoGDduXLv95e4XQJ6sr2NEJfjud78bO+20U+ywww7t9q9t/Ouq5cuXR6FQiO7dK+uvvtXV1X4fdlHaP7vu3bvHv/3bv8VZZ50VJ554YvTu3Tu1tqkMpppDFy1fvjxOOeWUGDRoUNTV1cVee+0VTzzxxGrH3XHHHTF27Nioq6uL3r17x6677hp33nlnh22vmuq0YsWKmD59ets0xoi1Txd/8MEH4zOf+Uz069cvampqYrvttovrr7++U/dy2WWXxWabbRbV1dWxxRZbxBVXXNG5HwIAa1TKMWKVJUuWxLe+9a0YPnx4VFdXR0NDQ+y///7x17/+te2YxYsXxzHHHBODBw+Onj17xvDhw+OUU06Jt99+u11bP//5z2PnnXeO+vr66N27dwwfPjyOPPLIiFg57uy4444REXHEEUe0jUlTpkyJiIinn346vvzlL8egQYOiuro6Nt544xg7duw6p6W//PLLMWvWrDj88MPb9nU0/kWsLMgPPPDA+MhHPhI1NTWx7bbbxuWXX96u3VXj5JVXXhnf+ta3YvDgwVFdXR1PPfXUWvsyffr02GabbaJPnz5RW1sbm2++eXznO99p69MXv/jFiIgYM2ZMW5/eO0W5M9/jlClTolAoxCOPPBKf//zno66uLurr6+Owww6LV155pd2xQ4cOjQMOOCBmzZoVW2+9ddTU1MTw4cPjv//7v9sdt6bp0quuM2/evDjkkEOivr4+Nt544zjyyCPjtddea3f+kiVL4mtf+1r069cv+vTpE+PHj4+nn3663ffbkQULFsRhhx0WDQ0NbX+H+MEPfhCtra2r9fH73/9+nH/++TFs2LDo06dP7LLLLjF37tx1XuOVV16JY445JkaOHBl9+vSJhoaG+PSnPx2/+93v1nnu0KFDY968eTFnzpy2723VjIVipul39s/poYceGs3NzTFz5sx1tkn+KLyhi77zne/Ec889F//zP/8Tl1xySfztb3+LCRMmREtLS9sxV111Veyzzz5RV1cXl19+eVx//fXRr1+/2HfffTv8i9X48ePbpjZ+4QtfiPvuu2+1qY7v9dvf/jZ23XXXWLJkSVx88cVx8803x7bbbhtf+tKX1jmgXHbZZXHEEUfEFltsETfeeGOceuqpcdZZZ602hRKAzivlGBER8frrr8duu+0WP/nJT+KII46IX/7yl3HxxRfHiBEjYuHChRERsWzZshgzZkxcccUV8c1vfjNuvfXWOOyww2Lq1Knx+c9/vq2t++67L770pS/F8OHDY+bMmXHrrbfG6aefHitWrIiIiO233z5mzJgRERGnnnpq25j09a9/PSIi9t9//3jooYdi6tSpMXv27Jg+fXpst912sWTJkg7v4Te/+U0sX748xowZ07avo/HviSeeiNGjR8e8efPiv//7v+MXv/hFjBw5MiZNmhRTp05drf2TTz45FixYEBdffHH88pe/jIaGhjX2Y+bMmXHMMcfEHnvsEbNmzYqbbropjj/++LZndcePHx/nnHNORERMmzatrU/jx4+PiOK/x8997nPxsY99LG644YaYMmVK3HTTTbHvvvvG8uXL2x336KOPxnHHHRfHH398zJo1K0aPHh3HHntsfP/73+/w57rKQQcdFCNGjIgbb7wxTjrppLjmmmvi+OOPb/v11tbWmDBhQlxzzTVx4oknxqxZs2LnnXde7Xn7tXnllVdi9OjR8Zvf/CbOOuusuOWWW2KvvfaKE044Ib7xjW+sdvy0adNi9uzZccEFF8TVV18dS5cujf3333+1fwx4v8WLF0dExOTJk+PWW2+NGTNmxPDhw2PPPfdc55o1s2bNiuHDh8d2223X9r3NmjWrU/e3SjHf74ABA2LzzTePW2+9tahrkBMJsJqJEycmG2ywwRp/7be//W0SEcn+++/fbv/111+fRERy3333JUmSJEuXLk369euXTJgwod1xLS0tyTbbbJPstNNO6+xHRCRNTU1rvP5vf/vbtn2bb755st122yXLly9vd+wBBxyQDBw4MGlpaVnjuS0tLcmgQYOS7bffPmltbW0779lnn0169OiRNDY2rrOPAHmzPowRZ555ZhIRyezZs9d6zMUXX5xERHL99de323/eeeclEZH85je/SZIkSb7//e8nEZEsWbJkrW098MADSUQkM2bMaLf/n//8ZxIRyQUXXNBhf9fk6KOPTnr16tVu/FllTePfl7/85aS6ujpZsGBBu/3jxo1Levfu3db/Vd/B7rvv3ql+fOMb30j69u3b4TE///nPVxt7k6S473Hy5MlJRCTHH398u2OvvvrqJCKSq666qm1fY2NjUigUkkcffbTdsXvvvXdSV1eXLF26NEmSJHnmmWdW+15WXWfq1Kntzj3mmGOSmpqatp/3rbfemkREMn369HbHnXvuuUlEJJMnT+7wZ3LSSSclEZHcf//97fYfffTRSaFQSJ544ol2fRw1alSyYsWKtuP++Mc/JhGRXHvttR1e5/1WrFiRLF++PBk7dmzyuc99bp3Hb7nllskee+yx2v41/exmzJiRRETyzDPPJEnStT+nhx56aLLxxhsXdU/kg8Qbuugzn/lMu89bb711REQ899xzERFx7733xuLFi2PixImxYsWKtq21tTX222+/eOCBB1JZ+fKpp56Kv/71r3HooYdGRLS71v777x8LFy5c4/TGiJXpwYsvvhhf+cpX2k3la2xsjNGjR3/gvgHkVanHiF//+tcxYsSI2GuvvdZ6zF133RUbbLBBfOELX2i3f9UiU6vSulXTyA8++OC4/vrr44UXXuj0ffbr1y823XTT+N73vhfnn39+PPLII+2mGXfkxRdfjI022qjd+NORu+66K8aOHRtDhgxpt3/SpEnx5ptvrjYz7KCDDupUuzvttFMsWbIkDjnkkLj55pvjn//8Z6fOi+ja97hqvF7l4IMPju7du8dvf/vbdvu33HLL2Gabbdrt+8pXvhLNzc2dWsRvTb8Hly1bFosWLYqIiDlz5rRd/70OOeSQdbYdsfL7GDlyZOy0007t9k+aNCmSJFlt5tz48eOjqqqqXX8i/vfPREcuvvji2H777aOmpia6d+8ePXr0iDvvvDP+8pe/dKqvXdWV77ehoSEWLVrUNmMEVlF4Qxf179+/3efq6uqIiHjrrbciYuWzaxErp8r16NGj3XbeeedFkiRt06c+iFXXOeGEE1a7zjHHHBMRsda/RLz66qsRsXJq1PutaR8AnVPqMeKVV16JTTbZpMM+vPrqqzFgwIDVCtuGhobo3r172xiw++67x0033RQrVqyIr371q7HJJpvEVlttFddee+0677NQKMSdd94Z++67b0ydOjW233772GijjeI///M/4/XXX+/w3LfeeitqamrWeY333s/AgQNX2z9o0KC2X3+vNR27Jocffnj87Gc/i+eeey4OOuigaGhoiJ133jlmz569znO78j2+f3zt3r179O/ff7X+dzQ2v//YNVnX78FXX301unfvHv369Wt33MYbb7zOtledX8z3sa7+rM35558fRx99dOy8885x4403xty5c+OBBx6I/fbbb53nflBd+X5ramoiSZJYtmxZSftG9lTW0o6wHtlwww0jIuLHP/7xWlfN7Ozg1pnrnHzyye2e2XuvzTbbbI37Vw2CL7300mq/tqZ9AKTjg44RG220UfzjH//o8Br9+/eP+++/P5IkaVd8r0rjVvUhIuLAAw+MAw88MN5+++2YO3dunHvuufGVr3wlhg4dGrvsskuH12lsbIyf/vSnERHx5JNPxvXXXx9TpkyJd955Jy6++OK1nrfhhhsW9fq1/v37tz2//l4vvvhiW3vv1dkkPWLlonFHHHFELF26NO65556YPHlyHHDAAfHkk09GY2PjWs/ryvf40ksvxeDBg9s+r1ixIl599dXVCtOOxub3H9sV/fv3jxUrVsTixYvbFd+dHf+L/T666qqrroo999wzpk+f3m7/uv5hJw1d+X4XL14c1dXV0adPn5L3j2yReEOJ7LrrrtG3b9+YP39+7LDDDmvcevbs+YGvs9lmm8XHP/7xeOyxx9Z6ndra2rWeO3DgwLj22msjSZK2/c8991zce++9H7hvAKzZBx0jxo0bF08++WSHC2GOHTs23njjjbjpppva7V/15oqxY8eudk51dXXssccecd5550XEyvdsr9ofse50csSIEXHqqafGqFGj1llUb7755vHqq6+uc3Gt997PXXfd1VbYvfd+evfuncqroTbYYIMYN25cnHLKKfHOO+/EvHnzImLt99+V7/Hqq69u9/n666+PFStWxJ577tlu/7x58+Kxxx5rt++aa66J2tra2H777T/wve6xxx4REXHddde129/ZFbnHjh0b8+fPX+17vuKKK6JQKLRbNO+DKBQKbT//Vf70pz91uOjse1VXV3c5Ge/K9/v000/HyJEju3Q9KpvEG0qkT58+8eMf/zgmTpwYixcvji984QvR0NAQr7zySjz22GPxyiuvrPavt131k5/8JMaNGxf77rtvTJo0KQYPHhyLFy+Ov/zlL/Hwww/Hz3/+8zWe161btzjrrLPi61//enzuc5+Lo446KpYsWRJTpkwx1RyghD7oGHHcccfFddddFwceeGCcdNJJsdNOO8Vbb70Vc+bMiQMOOCDGjBkTX/3qV2PatGkxceLEePbZZ2PUqFHx+9//Ps4555zYf//9254PP/300+Mf//hHjB07NjbZZJNYsmRJ/OhHP4oePXq0FWebbrpp9OrVK66++urYYostok+fPjFo0KD45z//Gd/4xjfii1/8Ynz84x+Pnj17xl133RV/+tOf4qSTTurwZ7DnnntGkiRx//33xz777LPOn9nkyZPjV7/6VYwZMyZOP/306NevX1x99dVx6623xtSpU6O+vr6Ib+B/HXXUUdGrV6/YddddY+DAgfHSSy/FueeeG/X19W3Pv2+11VYREXHJJZdEbW1t1NTUxLBhw6J///5Ff4+/+MUvonv37rH33nvHvHnz4rTTTottttlmtWetBw0aFJ/5zGdiypQpMXDgwLjqqqti9uzZcd5556Xyjuj99tsvdt111/jWt74Vzc3N8YlPfCLuu+++tn+Y6dat43zu+OOPjyuuuCLGjx8fZ555ZjQ2Nsatt94aF110URx99NExYsSID9zHiIgDDjggzjrrrJg8eXLsscce8cQTT8SZZ54Zw4YN69Rz1KNGjYqZM2fGddddF8OHD4+ampoYNWpUp65d7J/T1tbW+OMf/xhf+9rXuny/VLCyLesG67HOrFj785//vN3+Na2OmSRJMmfOnGT8+PFJv379kh49eiSDBw9Oxo8fv9r5axKdXNU8SZLkscceSw4++OCkoaEh6dGjRzJgwIDk05/+dHLxxRev89z/+Z//ST7+8Y8nPXv2TEaMGJH87Gc/SyZOnGhVc4A1WF/GiH/961/Jsccem3z0ox9NevTokTQ0NCTjx49P/vrXv7Yd8+qrryb//u//ngwcODDp3r170tjYmJx88snJsmXL2o751a9+lYwbNy4ZPHhw0rNnz6ShoSHZf//9k9/97nftrnfttdcmm2++edKjR4+2Va9ffvnlZNKkScnmm2+ebLDBBkmfPn2SrbfeOvnhD3/YbgXrNWlpaUmGDh2aHHPMMav92prGvyRJkscffzyZMGFCUl9fn/Ts2TPZZpttVvuZru07WJvLL788GTNmTLLxxhsnPXv2TAYNGpQcfPDByZ/+9Kd2x11wwQXJsGHDkqqqqtW+y858j6tWG3/ooYeSCRMmJH369Elqa2uTQw45JHn55ZfbXauxsTEZP358csMNNyRbbrll0rNnz2To0KHJ+eef3+64jlY1f+WVV9od+/4Vu5MkSRYvXpwcccQRSd++fZPevXsne++9dzJ37twkIpIf/ehH6/zZPffcc8lXvvKVpH///kmPHj2SzTbbLPne977X9jaV9/bxe9/73mrnRydWT3/77beTE044IRk8eHBSU1OTbL/99slNN93U6b+nPPvss8k+++yT1NbWJhHRdk5nVjVfpbN/Tu+888627xjer5Ak75lfCgAAH5If/OAHcfbZZ8cLL7wQvXr1Knd3SmrKlClxxhlnxCuvvLLO55+HDh0aW221VfzqV7/6kHr3v6655po49NBD4w9/+IM3nBTp8MMPj6effjr+8Ic/lLsrrIdMNQcAoCyampriwgsvjGnTpsUJJ5xQ7u7kzrXXXhsvvPBCjBo1Krp16xZz586N733ve7H77rsruov097//Pa677roO110g3xTeAACURU1NTVx55ZVti7jx4aqtrY2ZM2fGf/3Xf8XSpUtj4MCBMWnSpPiv//qvcnctcxYsWBAXXnhh7LbbbuXuCuspU80BAACghLxODADK4J577okJEybEoEGDolAorPbKJwBg/fD666/HcccdF42NjdGrV68YPXp0PPDAA0W1ofAGgDJYunRpbLPNNnHhhReWuysAQAe+/vWvx+zZs+PKK6+Mxx9/PPbZZ5/Ya6+94oUXXuh0G6aaA0CZFQqFmDVrVnz2s58td1cAgPd46623ora2Nm6++eYYP3582/5tt902DjjggE6viVD04mqtra3x4osvRm1tbRQKhWJPB4CySpKk7XU+3bqlO/ErSZLVxsbq6uqorq5O9TqdYbwGIOtKNWYXM16vWLEiWlpaoqampt3+Xr16xe9///uiLtopF154YbLFFlskm266aRIRNpvNZrPZ3rf16dNntX2TJ09e5xgbEcmsWbM6OyQbr202m82Wm21AQ1XqbRY7Xu+yyy7JHnvskbzwwgvJihUrkiuvvDIpFArJiBEjOj0+Fz3V/LXXXou+ffvG888/H3V1dcWcCkAG1dfXl7sLmfL+8bEziXcpppqvGq93i/2je/RIrV1Kq2qLj5e7CyXx4qf7lbsLJTHorsXl7kLJtPzlb+XuQkm83LRzubtQMrULWsrdhdS989Zr8egd349nHmqMutp0Eu/m11tj2CeeK2q8/vvf/x5HHnlk3HPPPVFVVRXbb799jBgxIh5++OGYP39+p65b9FTzVZF8XV2dwhuATEtzCvaqf8deX8bHVffWPXpE94LCOyuqqj78xxI+DFXVNes+KIO6V+j3FRFRqND/blTq78WIiO49Kq/wblm+LCIi6mq7pVZ4r1LMeL3pppvGnDlzYunSpdHc3BwDBw6ML33pSzFs2LBOX6/owhsAKkGhUEj92eciJ5EBAJ3QkrRGS0pDbEvS2uVzN9hgg9hggw3iX//6V9x+++0xderUTp+r8AaAMnjjjTfiqaeeavv8zDPPxKOPPhr9+vWLj370o2XsGQDwXrfffnskSRKbbbZZPPXUU/Htb387NttsszjiiCM63YbCG4BcKnfi/eCDD8aYMWPaPn/zm9+MiIiJEyfGZZddlmq/ACDLWiOJ1kgn8u5KO6+99lqcfPLJ8Y9/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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "maze = factory.generate_problem('maze', 8, rng)\n", + "maze.visualize()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Practical Hints:**\n", + "- Use 'maze.get_start_node()' to get the node with the start state.\n", + "- Use 'maze.is_end(node)' to check whether 'node' is the node with the end state.\n", + "- Use 'maze.successors(node)' to get a list of nodes containing successor states. Successors will be returned in the order that they can be reached using the following actions: R(ight), U(p), D(own), L(eft) (omitting actions that will cause the agent to bump into the wall). \n", + "\n", + "In the following these methods are shown for a problem named `maze`:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Node(id:139885768164176, parent:139886059131184, state:(0, 0), action:None, cost:0, depth:0)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# - use 'problem.get_start_node()' to get the node with the start state\n", + "maze.get_start_node()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# - use 'problem.is_end(node)' to check whether 'node' is the node with the end state\n", + "maze.is_end(maze.get_start_node())" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Node(id:139885138912480, parent:139885138912720, state:(1, 0), action:R, cost:1, depth:1)]" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# - use 'problem.successors(node)' to get a list of nodes containing successor states\n", + "maze.successors(maze.get_start_node())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Provided data structures\n", + "\n", + "

Usage of provided datatypes:

\n", + "\n", + "*Queue:*\n", + "- to put an item in the the Queue, use `fringe.put()`\n", + "- to get an item out of the Queue, use `fringe.get()`\n", + "- to check whether there are any elements in the Queue, use `fringe.has_elements()`\n", + "\n", + "*Stack:*\n", + "- to put an item in the the Stack, use `fringe.put()`\n", + "- to get an item out of the Stack, use `fringe.get()`\n", + "- to check whether there are any elements in the Stack, use `fringe.has_elements()`\n", + "\n", + "*PriorityQueue:*\n", + "- to put an item into the PriorityQueue, use `fringe.put(, )`\n", + "- to get an item out of the PriorityQueue, use `fringe.get()`\n", + "- to check whether there are any elements in the PriorityQueue, use `fringe.has_elements()`\n", + "\n", + "Let's have a look at a queue:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pig_lite.datastructures.queue import Queue\n", + "\n", + "queue = Queue()\n", + "\n", + "# does the newly created queue have elements?\n", + "queue.has_elements()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# let's store the start node of the previously created maze in the queue\n", + "queue.put(maze.get_start_node())\n", + "\n", + "# does the queue have elements now?\n", + "queue.has_elements()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Node(id:139881658566416, parent:139886059131184, state:(0, 0), action:None, cost:0, depth:0)\n" + ] + } + ], + "source": [ + "# let's retrieve an item from the queue\n", + "item = queue.get()\n", + "\n", + "# (and print it for the fun of it)\n", + "print(item)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# does the queue have elements now?\n", + "queue.has_elements()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Implementing a search algorithm\n", + "\n", + "Here we demonstrate how a search algorithm will look like based on a very dumb search, namely *Random Search*." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "class RS(object):\n", + " def solve(self, problem: Problem):\n", + " current = problem.get_start_node()\n", + "\n", + " while not problem.is_end(current): # as long as the current node is not the end\n", + " nodes = problem.successors(current) # get the successor nodes for the current node\n", + " current = random.choice(nodes) # choose a random successor node\n", + " return current\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the following we instantiate the search algorithm `RS` and call the function `solve()`." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "rs_search = RS()\n", + "rs_solution = rs_search.solve(maze)\n", + "\n", + "# Since this random search is not very smart and might revisit previous states,\n", + "# you might receive a warning \"overflow encountered in scalar add node.cost + cost\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We provide you with a method to print information about the found solution:" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "state (6, 6) was reached following the sequence ['R', 'R', 'R', 'R', 'R', 'R', 'D', 'D', 'D', 'D', 'U', 'U', 'U', 'D', 'U', 'D', 'U', 'U', 'D', 'U', 'D', 'U', 'L', 'R', 'L', 'L', 'R', 'L', 'R', 'R', 'L', 'L', 'L', 'R', 'R', 'R', 'L', 'R', 'L', 'L', 'R', 'L', 'L', 'R', 'L', 'L', 'L', 'R', 'D', 'U', 'L', 'L', 'R', 'R', 'D', 'D', 'D', 'U', 'U', 'U', 'D', 'D', 'U', 'U', 'D', 'U', 'L', 'R', 'L', 'R', 'L', 'R', 'D', 'U', 'D', 'D', 'L', 'D', 'L', 'D', 'D', 'U', 'U', 'U', 'D', 'D', 'U', 'D', 'D', 'U', 'U', 'R', 'L', 'R', 'U', 'D', 'U', 'D', 'L', 'U', 'D', 'R', 'L', 'D', 'U', 'U', 'R', 'D', 'U', 'R', 'L', 'R', 'L', 'R', 'U', 'D', 'D', 'U', 'L', 'L', 'R', 'R', 'L', 'D', 'L', 'U', 'R', 'R', 'U', 'D', 'L', 'R', 'L', 'R', 'L', 'R', 'U', 'D', 'U', 'U', 'L', 'R', 'D', 'U', 'D', 'D', 'U', 'U', 'D', 'U', 'L', 'L', 'R', 'L', 'R', 'L', 'R', 'R', 'D', 'D', 'U', 'U', 'R', 'L', 'L', 'R', 'L', 'L', 'R', 'R', 'D', 'D', 'L', 'D', 'L', 'U', 'R', 'D', 'L', 'U', 'D', 'U', 'R', 'L', 'R', 'D', 'L', 'D', 'U', 'D', 'D', 'D', 'U', 'D', 'U', 'U', 'D', 'U', 'D', 'D', 'U', 'D', 'U', 'U', 'U', 'U', 'D', 'D', 'D', 'U', 'U', 'D', 'D', 'U', 'D', 'D', 'U', 'D', 'U', 'U', 'D', 'U', 'D', 'U', 'D', 'U', 'D', 'U', 'D', 'U', 'D', 'D', 'U', 'U', 'D', 'U', 'U', 'U', 'D', 'R', 'R', 'D', 'U', 'D', 'U', 'D', 'D', 'R', 'U', 'D', 'U', 'R', 'U', 'U', 'D', 'D', 'L', 'D', 'U', 'L', 'D', 'U', 'D', 'R', 'D', 'R', 'L', 'L', 'R', 'U', 'L', 'U', 'R', 'R', 'L', 'L', 'R', 'L', 'D', 'D', 'U', 'D', 'U', 'R', 'U', 'L', 'U', 'D', 'U', 'L', 'R', 'D', 'R', 'R', 'L', 'D', 'D', 'R', 'R', 'L', 'L', 'L', 'U', 'R', 'D', 'U', 'D', 'L', 'U', 'D', 'R', 'U', 'U', 'R', 'U', 'U', 'D', 'D', 'U', 'U', 'D', 'D', 'U', 'U', 'D', 'D', 'L', 'D', 'U', 'D', 'D', 'U', 'L', 'U', 'U', 'U', 'U', 'U', 'D', 'D', 'L', 'L', 'D', 'D', 'D', 'U', 'U', 'D', 'U', 'U', 'D', 'R', 'U', 'R', 'D', 'U', 'D', 'L', 'L', 'D', 'D', 'U', 'D', 'D', 'U', 'U', 'U', 'U', 'R', 'D', 'U', 'L', 'R', 'L', 'D', 'R', 'L', 'U', 'R', 'D', 'R', 'D', 'U', 'D', 'D', 'R', 'U', 'L', 'R', 'R', 'U', 'U', 'D', 'U', 'D', 'D', 'L', 'R', 'L', 'D', 'D', 'R', 'L', 'U', 'D', 'L', 'U', 'R', 'U', 'R', 'L', 'L', 'U', 'D', 'R', 'L', 'D', 'U', 'U', 'D', 'D', 'R', 'U', 'R', 'U', 'D', 'L', 'D', 'U', 'D', 'L', 'U', 'R', 'L', 'U', 'U', 'U', 'U', 'L', 'L', 'R', 'R', 'D', 'D', 'U', 'U', 'L', 'L', 'R', 'L', 'R', 'L', 'R', 'R', 'D', 'U', 'D', 'U', 'D', 'U', 'D', 'D', 'U', 'U', 'D', 'D', 'L', 'D', 'U', 'D', 'R', 'L', 'U', 'L', 'D', 'R', 'U', 'L', 'D', 'U', 'R', 'D', 'R', 'L', 'L', 'R', 'R', 'U', 'L', 'R', 'D', 'U', 'L', 'R', 'U', 'U', 'D', 'U', 'R', 'L', 'D', 'U', 'R', 'R', 'L', 'R', 'L', 'L', 'D', 'D', 'U', 'U', 'R', 'R', 'R', 'L', 'R', 'L', 'L', 'L', 'D', 'U', 'L', 'L', 'R', 'L', 'R', 'R', 'D', 'D', 'U', 'D', 'U', 'U', 'D', 'U', 'R', 'R', 'R', 'R', 'D', 'D', 'D', 'U', 'D', 'U', 'U', 'U', 'L', 'R', 'D', 'D', 'U', 'D', 'D', 'D', 'U', 'U', 'U', 'U', 'D', 'D', 'D', 'D', 'U', 'D', 'U', 'U', 'D', 'U', 'D', 'U', 'U', 'U', 'L', 'L', 'L', 'L', 'L', 'R', 'D', 'D', 'L', 'D', 'L', 'R', 'R', 'L', 'U', 'D', 'L', 'R', 'R', 'U', 'L', 'L', 'R', 'L', 'R', 'R', 'U', 'U', 'D', 'U', 'D', 'D', 'U', 'D', 'D', 'L', 'R', 'U', 'L', 'L', 'D', 'U', 'R', 'R', 'D', 'L', 'R', 'L', 'U', 'R', 'L', 'D', 'L', 'R', 'U', 'D', 'U', 'D', 'L', 'D', 'D', 'U', 'D', 'U', 'U', 'R', 'U', 'L', 'R', 'L', 'R', 'D', 'U', 'L', 'D', 'R', 'L', 'D', 'D', 'D', 'U', 'U', 'U', 'R', 'L', 'D', 'D', 'D', 'U', 'U', 'U', 'R', 'L', 'R', 'U', 'R', 'L', 'D', 'U', 'L', 'R', 'L', 'D', 'U', 'R', 'D', 'U', 'R', 'U', 'D', 'L', 'R', 'L', 'L', 'R', 'D', 'R', 'L', 'R', 'L', 'L', 'R', 'L', 'U', 'D', 'U', 'R', 'D', 'U', 'L', 'D', 'D', 'D', 'D', 'U', 'U', 'D', 'U', 'U', 'D', 'D', 'D', 'U', 'U', 'D', 'U', 'U', 'R', 'L', 'U', 'R', 'R', 'L', 'D', 'U', 'R', 'U', 'U', 'R', 'L', 'D', 'D', 'D', 'D', 'U', 'L', 'U', 'L', 'D', 'U', 'R', 'L', 'R', 'D', 'L', 'R', 'L', 'R', 'L', 'U', 'R', 'R', 'L', 'L', 'D', 'U', 'D', 'U', 'R', 'D', 'U', 'L', 'R', 'D', 'U', 'R', 'D', 'D', 'U', 'D', 'R', 'R', 'U', 'D', 'L', 'R', 'U', 'D', 'U', 'U', 'D', 'U', 'D', 'U', 'D', 'U', 'D', 'D', 'L', 'R', 'L', 'D', 'L', 'R', 'L', 'D', 'R', 'R', 'L', 'L', 'R', 'R', 'R', 'R'] (cost: 108, depth: 808)\n" + ] + } + ], + "source": [ + "rs_solution.pretty_print()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualizing the solution\n", + "\n", + "Now you can also visualize the found path on top of the maze:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/quirinecker/Downloads/tmp/notebook0/pig_lite/problem/simple_2d.py:306: RuntimeWarning: overflow encountered in scalar add\n", + " node.cost + cost,\n" + ] + }, + { + "data": { + "image/png": "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YcOGad++fRVmhp1//vkhtdulSxft2rVLl156qV555RX997//Dek8qXrvY/l4Xe6iiy5SQkKC3n333aD9bdq0UYcOHYL2XXbZZSouLg5pEb/KPoMlJSXatm2bJGnhwoWB/g936aWXHrVtqez9yMvLU5cuXYL2Dxs2TMaYCjPnBg0apPj4+KB4pP/9TFRlypQpOvnkk5WcnKyEhAQlJibq7bff1pdffhlSrNVVnfc3KytL27ZtC8wYAcqReAPVVLdu3aDXXq9XkrR//35JZfeuSWVT5RITE4O2Bx54QMaYwPSpY1Hez0033VShn5EjR0rSEX+J+OmnnySVTY36tcr2AQBCE+kxYvv27TrhhBOqjOGnn35SgwYNKiS2WVlZSkhICIwBp59+ul5++WWVlpbqqquu0gknnKC2bdvqhRdeOOp1ejwevf322zrzzDM1YcIEnXzyyapXr57+9Kc/ac+ePVWeu3//fiUnJx+1j8OvJzs7u8L+nJycwNcPV9mxlbnyyiv1r3/9Sxs2bND555+vrKwsde3aVfPnzz/qudV5H389viYkJKhu3boV4q9qbP71sZU52mfwp59+UkJCgurUqRN0XP369Y/advn54bwfR4vnSB5++GFdd9116tq1q2bPnq0lS5Zo2bJlOuuss4567rGqzvubnJwsY4xKSkoiGhucJ7aWdgRqkOOPP16S9Oijjx5x1cxQB7dQ+rntttuC7tk7XMuWLSvdXz4Ibt26tcLXKtsHALDGsY4R9erV0w8//FBlH3Xr1tXSpUtljAlKvsurceUxSNI555yjc845RwcOHNCSJUs0fvx4XXbZZWrSpIm6detWZT+5ubl66qmnJElr167VrFmzVFBQoIMHD2rKlClHPO/4448P6/FrdevWDdy/frjNmzcH2jtcqJV0qWzRuOHDh2vv3r167733NHbsWA0ePFhr165Vbm7uEc+rzvu4detWNWzYMPC6tLRUP/30U4XEtKqx+dfHVkfdunVVWlqqHTt2BCXfoY7/4b4f1fXss8+qd+/emjx5ctD+o/1hxwrVeX937Nghr9ertLS0iMcHZ6HiDURIjx49VLt2ba1evVqnnnpqpVtSUtIx99OyZUuddNJJ+vTTT4/YT3p6+hHPzc7O1gsvvCBjTGD/hg0btGjRomOODQBQuWMdIwYMGKC1a9dWuRBm37599fPPP+vll18O2l/+5Iq+fftWOMfr9apXr1564IEHJJU9Z7t8v3T06mSLFi00ZswYtWvX7qhJdatWrfTTTz8ddXGtw6/nnXfeCSR2h19PamqqJY+GqlWrlgYMGKA77rhDBw8e1KpVqyQd+fqr8z4+99xzQa9nzZql0tJS9e7dO2j/qlWr9Omnnwbte/7555Wenq6TTz75mK+1V69ekqSZM2cG7Q91Re6+fftq9erVFd7np59+Wh6PJ2jRvGPh8XgC3/9yn332WZWLzh7O6/VWuzJenff3u+++U15eXrX6Q2yj4g1ESFpamh599FENHTpUO3bs0AUXXKCsrCxt375dn376qbZv317hr7fV9fjjj2vAgAE688wzNWzYMDVs2FA7duzQl19+qY8//lgvvvhipefFxcVp3Lhxuvbaa3XuuedqxIgR2rVrlwoKCphqDgARdKxjxKhRozRz5kydc845uvXWW9WlSxft379fCxcu1ODBg9WnTx9dddVVKiws1NChQ7V+/Xq1a9dOH3zwge677z4NHDgwcH/4XXfdpR9++EF9+/bVCSecoF27dunvf/+7EhMTA8lZ8+bNlZKSoueee06tW7dWWlqacnJy9N///lfXX3+9LrzwQp100klKSkrSO++8o88++0y33nprld+D3r17yxijpUuXqn///kf9no0dO1avv/66+vTpo7vuukt16tTRc889pzlz5mjChAnKzMwM4x34nxEjRiglJUU9evRQdna2tm7dqvHjxyszMzNw/3vbtm0lSU888YTS09OVnJyspk2bqm7dumG/j//+97+VkJCgfv36adWqVbrzzjvVoUOHCvda5+Tk6Oyzz1ZBQYGys7P17LPPav78+XrggQcseUb0WWedpR49eujPf/6ziouLdcopp2jx4sWBP8zExVVdnxs9erSefvppDRo0SPfcc49yc3M1Z84cPfbYY7ruuuvUokWLY45RkgYPHqxx48Zp7Nix6tWrl9asWaN77rlHTZs2Dek+6nbt2mnGjBmaOXOmmjVrpuTkZLVr1y6kvsP9OfX7/froo490zTXXVPt6EcNsW9YNqMFCWbH2xRdfDNpf2eqYxhizcOFCM2jQIFOnTh2TmJhoGjZsaAYNGlTh/MooxFXNjTHm008/NRdddJHJysoyiYmJpkGDBua3v/2tmTJlylHP/ec//2lOOukkk5SUZFq0aGH+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+/XUU0+pTZs2ys3NrTQWv9+vAQMGaNGiRXr22We1evVq3X///UHPyfZ4PJo2bVrI17du3Tpt3bpV/fv3D+zzer3q1atXINZQrhkAgHAwXjNeAzUN93gDYTjxxBM1YcKEwOv//Oc/atCggc444wwlJiaqcePG6tKlS8jtpaamqmXLlkpMTAzsmzlzpi6++GLVrVtXCQkJSk1NVVFRkZo3by6p7P6qrKysCm1lZWVp69atkqT09HQtWLBA55xzjsaNGydJatGihebNm6eEhMp/7N966y199NFH+vLLL9WiRdn9dM2aNQs6pmXLlsrMzAz5+srjqV+/ftD++vXra8OGDSFfMwAA4WC8ZrwGahoq3kAYTj311KDXF154ofbv369mzZppxIgRKioqUmlpacjtdenSRV999ZUaNmwY2DdmzBjt3LlTb731lpYvX64bb7xRF154oT7//PPAMZU9F9MYE9i/f/9+XX311erRo4eWLFmiDz/8UG3atNHAgQO1f//+SmNZuXKlTjjhhMAgXpmvvvpK5557bsjXd6R4D49VCu2aAQAIFeM14zVQ01DxBsJQq1atoNeNGjXSmjVrNH/+fL311lsaOXKkHnzwQS1cuDDor+Kh+vbbbzVp0iR98cUXgVVEO3TooPfff1+FhYWaMmWKGjRooB9//LHCudu3bw/8pfr555/X+vXrtXjxYsXFxQX2HXfccXrllVd0ySWXVDg/JSUl7HiPpkGDBpLK/pKenZ0d2L9t27ZArKFcMwAA4WC8Dg/jNRB5VLyBY5SSkqKzzz5b//jHP7RgwQItXry42n/53bdvnyQFBt9y8fHx8vv9kqRu3bpp9+7d+uijjwJfX7p0qXbv3q3u3bsH2omLiwv6K3X56/J2fq19+/b64YcftHatdY+Jadq0qRo0aKD58+cH9h08eFALFy4MirU8vsMdfs0AABwrxusjY7wGIo/EGzgG06ZN01NPPaUvvvhC3333nZ555hmlpKQccUGUX/voo4/UqlUrbdq0SZLUqlUrnXjiifrDH/6gjz76SN9++60eeughzZ8/P/C8zdatW+uss87SiBEjtGTJEi1ZskQjRozQ4MGD1bJlS0lSv379tHPnTuXn5+vLL7/UqlWrNHz4cCUkJKhPnz6VxtKrVy+dfvrpOv/88zV//nytW7dOb7zxhubOnRs4plWrVioqKgq8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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "maze.visualize(sequences=[('rs', rs_solution.get_action_sequence())])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This also works for several sequences, e.g.: \n", + "```\n", + "maze.visualize(sequences=[('rs', rs_solution.get_action_sequence()), ('bfs', bfs_solution.get_action_sequence())])\n", + "```\n", + "or even a manually created one:" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/quirinecker/Downloads/tmp/notebook0/pig_lite/problem/simple_2d.py:306: RuntimeWarning: overflow encountered in scalar add\n", + " node.cost + cost,\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA94AAAJGCAYAAACtL12xAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjUsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvWftoOwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAY7lJREFUeJzt3Xd8VFX+//H3pE0S0ggQklBSwARCRxQpCiyCFLE3LAuy8t2fxl3FddeGgro2XF1cQVBXEWzYFhsrigVsdIkFlN6k90BCCJm5vz8ws46EMBPuzc2deT0fj7ububn3nM/NBE8+8zn3XJdhGIYAAAAAAIAlIuwOAAAAAACAUEbiDQAAAACAhUi8AQAAAACwEIk3AAAAAAAWIvEGAAAAAMBCJN4AAAAAAFiIxBsAAAAAAAuReAMAAAAAYCESbwAAAAAALETiDUhyuVwBbXPmzNGcOXPkcrn05ptvWh7X0qVL1atXLyUnJ8vlcmn8+PG+/ufMmRN0e8GcO3z4cGVnZwfdBwCEmro6RtSmLVu2aOzYsSoqKjK97REjRmjAgAF++6oa/+y0fPlyjR07VuvXr7c1jt9av369XC6XXnjhBbtDqbOeeuqpKn8+Vf3sXnjhBblcrhq9z88995yaNGmikpKSmgeLkBZldwBAXTBv3jy/1/fff78+++wzffrpp377CwoK9M0339RaXCNGjFBJSYmmT5+u+vXrKzs7W/Hx8Zo3b54KCgpqLQ4ACGd1dYyoTVu2bNG9996r7OxsdezY0bR2ly5dqqlTp2rBggV++6sa/+y0fPly3Xvvverdu7ftsfxaRkaG5s2bpxYtWtgdSp311FNPqWHDhho+fLjffrN/dsOGDdMjjzyicePG6d577zWlTYQWEm9A0hlnnOH3ulGjRoqIiDhmf2374YcfNHLkSA0cONBvv91xAUA4qatjRCh4+OGHdfrpp6tLly5++483/tXUkSNH5HK5FBUVWn/6ut1ufg9ryOyfXVRUlP74xz/q/vvv12233ab4+HjT2kZoYKo5UENHjhzRXXfdpczMTCUlJenss8/WihUrjjnu448/Vt++fZWUlKT4+Hj16NFDn3zySbVtV051qqio0KRJk3zTGKXjTxdfvHixzjvvPKWmpio2NladOnXS66+/HtC1vPDCC8rPz5fb7Vbr1q01bdq0wH4IAIAqWTlGVNq3b5/+8pe/KDc3V263W2lpaRo0aJB++ukn3zF79uzRDTfcoCZNmigmJka5ubm66667dPjwYb+23njjDXXt2lXJycmKj49Xbm6uRowYIenouHPaaadJkq699lrfmDR27FhJ0tq1a3XFFVcoMzNTbrdbjRs3Vt++fU84LX379u2aMWOGrrnmGt++6sY/6WhCfv7556t+/fqKjY1Vx44dNXXqVL92K8fJF198UX/5y1/UpEkTud1urV69+rixTJo0SR06dFBCQoISExPVqlUr3Xnnnb6YLr30UklSnz59fDH9eopyIO/j2LFj5XK5tHTpUl100UVKSkpScnKyrr76au3cudPv2OzsbJ177rmaMWOG2rdvr9jYWOXm5upf//qX33FVTZeu7GfZsmUaOnSokpOT1bhxY40YMUL79+/3O3/fvn36wx/+oNTUVCUkJGjw4MFau3at3/tbnY0bN+rqq69WWlqa72+Ixx57TF6v95gY//GPf+jxxx9XTk6OEhIS1K1bN82fP/+EfezcuVM33HCDCgoKlJCQoLS0NP3ud7/TF198ccJzs7OztWzZMs2dO9f3vlXOWAhmmn6g/06vuuoqFRcXa/r06SdsE+GHxBuooTvvvFMbNmzQv//9bz3zzDNatWqVhgwZIo/H4zvmpZdeUv/+/ZWUlKSpU6fq9ddfV2pqqs4555xq/7AaPHiwb2rjJZdconnz5h0z1fHXPvvsM/Xo0UP79u3T5MmT9c4776hjx466/PLLTzigvPDCC7r22mvVunVrvfXWWxo9erTuv//+Y6ZQAgACZ+UYIUkHDhxQz5499fTTT+vaa6/Ve++9p8mTJysvL09bt26VJJWVlalPnz6aNm2abrnlFs2cOVNXX321xo0bp4suusjX1rx583T55ZcrNzdX06dP18yZM3XPPfeooqJCktS5c2dNmTJFkjR69GjfmHTddddJkgYNGqQlS5Zo3Lhxmj17tiZNmqROnTpp37591V7DRx99pCNHjqhPnz6+fdWNfytWrFD37t21bNky/etf/9J//vMfFRQUaPjw4Ro3btwx7d9xxx3auHGjJk+erPfee09paWlVxjF9+nTdcMMN6tWrl2bMmKG3335bo0aN8t2rO3jwYD344IOSpIkTJ/piGjx4sKTg38cLL7xQLVu21JtvvqmxY8fq7bff1jnnnKMjR474HVdUVKSbb75Zo0aN0owZM9S9e3fddNNN+sc//lHtz7XSxRdfrLy8PL311lu6/fbb9corr2jUqFG+73u9Xg0ZMkSvvPKKbrvtNs2YMUNdu3Y95n7749m5c6e6d++ujz76SPfff7/effddnX322br11lt14403HnP8xIkTNXv2bI0fP14vv/yySkpKNGjQoGM+DPitPXv2SJLGjBmjmTNnasqUKcrNzVXv3r1PuGbNjBkzlJubq06dOvnetxkzZgR0fZWCeX/T09PVqlUrzZw5M6g+ECYMAMcYNmyYUa9evSq/99lnnxmSjEGDBvntf/311w1Jxrx58wzDMIySkhIjNTXVGDJkiN9xHo/H6NChg3H66aefMA5JRmFhYZX9f/bZZ759rVq1Mjp16mQcOXLE79hzzz3XyMjIMDweT5XnejweIzMz0+jcubPh9Xp9561fv96Ijo42srKyThgjAISbujBG3HfffYYkY/bs2cc9ZvLkyYYk4/XXX/fb/8gjjxiSjI8++sgwDMP4xz/+YUgy9u3bd9y2Fi1aZEgypkyZ4rd/165dhiRj/Pjx1cZbleuvv96Ii4vzG38qVTX+XXHFFYbb7TY2btzot3/gwIFGfHy8L/7K9+Css84KKI4bb7zRSElJqfaYN95445ix1zCCex/HjBljSDJGjRrld+zLL79sSDJeeukl376srCzD5XIZRUVFfsf269fPSEpKMkpKSgzDMIx169Yd875U9jNu3Di/c2+44QYjNjbW9/OeOXOmIcmYNGmS33EPPfSQIckYM2ZMtT+T22+/3ZBkLFiwwG//9ddfb7hcLmPFihV+MbZr186oqKjwHbdw4UJDkvHqq69W289vVVRUGEeOHDH69u1rXHjhhSc8vk2bNkavXr2O2V/Vz27KlCmGJGPdunWGYdTs3+lVV11lNG7cOKhrQnig4g3U0Hnnnef3un379pKkDRs2SJK+/vpr7dmzR8OGDVNFRYVv83q9GjBggBYtWmTKyperV6/WTz/9pKuuukqS/PoaNGiQtm7dWuX0Rulo9WDLli268sor/abyZWVlqXv37icdGwCEK6vHiA8++EB5eXk6++yzj3vMp59+qnr16umSSy7x21+5yFRlta5yGvlll12m119/XZs3bw74OlNTU9WiRQs9+uijevzxx7V06VK/acbV2bJlixo1auQ3/lTn008/Vd++fdWsWTO//cOHD1dpaekxM8MuvvjigNo9/fTTtW/fPg0dOlTvvPOOdu3aFdB5Us3ex8rxutJll12mqKgoffbZZ37727Rpow4dOvjtu/LKK1VcXBzQIn5V/Q6WlZVpx44dkqS5c+f6+v+1oUOHnrBt6ej7UVBQoNNPP91v//Dhw2UYxjEz5wYPHqzIyEi/eKT//ZuozuTJk9W5c2fFxsYqKipK0dHR+uSTT/Tjjz8GFGtN1eT9TUtL044dO3wzRoBKJN5ADTVo0MDvtdvtliQdOnRI0tF716SjU+Wio6P9tkceeUSGYfimT52Myn5uvfXWY/q54YYbJOm4f0Ts3r1b0tGpUb9V1T4AQGCsHiN27typpk2bVhvD7t27lZ6efkxim5aWpqioKN8YcNZZZ+ntt99WRUWFfv/736tp06Zq27atXn311RNep8vl0ieffKJzzjlH48aNU+fOndWoUSP9+c9/1oEDB6o999ChQ4qNjT1hH7++noyMjGP2Z2Zm+r7/a1UdW5VrrrlGzz//vDZs2KCLL75YaWlp6tq1q2bPnn3Cc2vyPv52fI2KilKDBg2Oib+6sfm3x1blRL+Du3fvVlRUlFJTU/2Oa9y48Qnbrjw/mPfjRPEcz+OPP67rr79eXbt21VtvvaX58+dr0aJFGjBgwAnPPVk1eX9jY2NlGIbKysosjQ3OE1pLOwJ1SMOGDSVJTz755HFXzQx0cAuknzvuuMPvnr1fy8/Pr3J/5SC4bdu2Y75X1T4AgDlOdoxo1KiRfv7552r7aNCggRYsWCDDMPyS78pqXGUMknT++efr/PPP1+HDhzV//nw99NBDuvLKK5Wdna1u3bpV209WVpaee+45SdLKlSv1+uuva+zYsSovL9fkyZOPe17Dhg2DevxagwYNfPev/9qWLVt87f1aoJV06eiicddee61KSkr0+eefa8yYMTr33HO1cuVKZWVlHfe8mryP27ZtU5MmTXyvKyoqtHv37mMS0+rG5t8eWxMNGjRQRUWF9uzZ45d8Bzr+B/t+1NRLL72k3r17a9KkSX77T/TBjhlq8v7u2bNHbrdbCQkJlscHZ6HiDVikR48eSklJ0fLly9WlS5cqt5iYmJPuJz8/X6eccoq+/fbb4/aTmJh43HMzMjL06quvyjAM3/4NGzbo66+/PunYAABVO9kxYuDAgVq5cmW1C2H27dtXBw8e1Ntvv+23v/LJFX379j3mHLfbrV69eumRRx6RdPQ525X7pRNXJ/Py8jR69Gi1a9fuhEl1q1attHv37hMurvXr6/n00099id2vryc+Pt6UR0PVq1dPAwcO1F133aXy8nItW7ZM0vGvvybv48svv+z3+vXXX1dFRYV69+7tt3/ZsmX69ttv/fa98sorSkxMVOfOnU/6Wnv16iVJeu211/z2B7oid9++fbV8+fJj3udp06bJ5XL5LZp3Mlwul+/nX+m7776rdtHZX3O73TWujNfk/V27dq0KCgpq1B9CGxVvwCIJCQl68sknNWzYMO3Zs0eXXHKJ0tLStHPnTn377bfauXPnMZ/e1tTTTz+tgQMH6pxzztHw4cPVpEkT7dmzRz/++KO++eYbvfHGG1WeFxERofvvv1/XXXedLrzwQo0cOVL79u3T2LFjmWoOABY62THi5ptv1muvvabzzz9ft99+u04//XQdOnRIc+fO1bnnnqs+ffro97//vSZOnKhhw4Zp/fr1ateunb788ks9+OCDGjRokO/+8HvuuUc///yz+vbtq6ZNm2rfvn164oknFB0d7UvOWrRoobi4OL388stq3bq1EhISlJmZqV27dunGG2/UpZdeqlNOOUUxMTH69NNP9d133+n222+v9mfQu3dvGYahBQsWqH///if8mY0ZM0bvv/+++vTpo3vuuUepqal6+eWXNXPmTI0bN07JyclBvAP/M3LkSMXFxalHjx7KyMjQtm3b9NBDDyk5Odl3/3vbtm0lSc8884wSExMVGxurnJwcNWjQIOj38T//+Y+ioqLUr18/LVu2THfffbc6dOhwzL3WmZmZOu+88zR27FhlZGTopZde0uzZs/XII4+Y8ozoAQMGqEePHvrLX/6i4uJinXrqqZo3b57vg5mIiOrrc6NGjdK0adM0ePBg3XfffcrKytLMmTP11FNP6frrr1deXt5JxyhJ5557ru6//36NGTNGvXr10ooVK3TfffcpJycnoPuo27Vrp+nTp+u1115Tbm6uYmNj1a5du4D6Dvbfqdfr1cKFC/WHP/yhxteLEGbbsm5AHRbIirVvvPGG3/6qVsc0DMOYO3euMXjwYCM1NdWIjo42mjRpYgwePPiY86uiAFc1NwzD+Pbbb43LLrvMSEtLM6Kjo4309HTjd7/7nTF58uQTnvvvf//bOOWUU4yYmBgj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APXu3Vs33nijbrnlFjVs2FD9+vWTdHQ6WvPmzeV2u5WZmak///nPJ9XPvHnz9Kc//Umnn366cnNzNXr0aKWkpOibb76RJP3444+aNWuW/v3vf6tbt27q1q2bnn32Wb3//vtasWKFJGnFihXau3ev7rvvPuXn56tNmzYaM2aMduzYoY0bNx6373379un//u//1LhxY8XGxqpt27Z6//33j3v8wIED9fe//10XXXRRld83DEPjx4/XXXfdpYsuukht27bV1KlTVVpaqldeeSXgawYAIFCM18divAbsR+INBGHq1KmKiorSV199paefflpvvvmm/vnPf+rpp5/WqlWr9Pbbb6tdu3YBtzdnzhy5XC6tX7/et69nz5567bXXtGfPHnm9Xk2fPl2HDx9W7969JR0d9JKTk9W1a1ffOWeccYaSk5P19ddfS5Ly8/PVsGFDPffccyovL9ehQ4f03HPPqU2bNsrKyqoyFq/Xq4EDB+rrr7/WSy+9pOXLl+vhhx/2e062y+XSCy+8EPD1rVu3Ttu2bVP//v19+9xut3r16uWLNZBrBgAgGIzXjNdAXcM93kAQWrZsqXHjxvle//e//1V6errOPvtsRUdHq3nz5jr99NMDbi8+Pl75+fmKjo727Xvttdd0+eWXq0GDBoqKilJ8fLxmzJihFi1aSDp6f1VaWtoxbaWlpWnbtm2SpMTERM2ZM0fnn3++7r//fklSXl6ePvzwQ0VFVf3P/uOPP9bChQv1448/Ki/v6P10ubm5fsfk5+crOTk54OurjKdx48Z++xs3bqwNGzYEfM0AAASD8ZrxGqhrqHgDQejSpYvf60svvVSHDh1Sbm6uRo4cqRkzZqiioiLg9k4//XT99NNPatKkiW/f6NGjtXfvXn388cdavHixbrnlFl166aX6/vvvfcdU9VxMwzB8+w8dOqQRI0aoR48emj9/vr766iu1adNGgwYN0qFDh6qMpaioSE2bNvUN4lX56aefdOGFFwZ8fceL99exSoFdMwAAgWK8ZrwG6hoq3kAQ6tWr5/e6WbNmWrFihWbPnq2PP/5YN9xwgx599FHNnTvX71PxQK1Zs0YTJkzQDz/84FtFtEOHDvriiy80ceJETZ48Wenp6dq+ffsx5+7cudP3SfUrr7yi9evXa968eYqIiPDtq1+/vt555x1dccUVx5wfFxcXdLwnkp6eLunoJ+kZGRm+/Tt27PDFGsg1AwAQDMbr4DBeA9aj4g2cpLi4OJ133nn617/+pTlz5mjevHk1/uS3tLRUknyDb6XIyEh5vV5JUrdu3bR//34tXLjQ9/0FCxZo//796t69u6+diIgIv0+pK19XtvNb7du3188//6yVK817TExOTo7S09M1e/Zs377y8nLNnTvXL9bK+H7t19cMAMDJYrw+PsZrwHok3sBJeOGFF/Tcc8/phx9+0Nq1a/Xiiy8qLi7uuAui/NbChQvVqlUrbd68WZLUqlUrtWzZUn/84x+1cOFCrVmzRo899phmz57te95m69atNWDAAI0cOVLz58/X/PnzNXLkSJ177rnKz8+XJPXr10979+5VYWGhfvzxRy1btkzXXnutoqKi1KdPnypj6dWrl8466yxdfPHFmj17ttatW6cPPvhAs2bN8h3TqlUrzZgxw/f64MGDKioqUlFRkaSji7MUFRX5VmJ1uVy6+eab9eCDD2rGjBn64YcfNHz4cMXHx+vKK68M+JoBADgZjNeM14DdSLyBk5CSkqJnn31WPXr0UPv27fXJJ5/ovffeU4MGDQI6v7S0VCtWrPA9qzM6Olr//e9/1ahRIw0ZMkTt27fXtGnTNHXqVA0aNMh33ssvv6x27dqpf//+6t+/v9q3b68XX3zR9/1WrVrpvffe03fffadu3brpzDPP1JYtWzRr1iy/KWS/9dZbb+m0007T0KFDVVBQoL/97W/yeDy+769YsUL79+/3vV68eLE6deqkTp06SZJuueUWderUSffcc4/vmL/97W+6+eabdcMNN6hLly7avHmzPvroIyUmJgZ1zQAA1BTjNeM1YDeXYRiG3UEgfJSVlWndunXKyclRbGys3eHAofg9AgDr8d9anCx+h4D/oeINAAAAAICFSLwBAAAAALAQiTcAAAAAABYi8QYAAAAAwEIk3gAAAAAAWIjEGwAAAAAAC5F4AwAAAABgIRJvAAAAAAAsROINAAAAAICFSLwBB+jdu7duvvlmu8MAAADVYLwGcDxRdgcAVLp8xhW12t9rF04P6vjevXurY8eOGj9+vGkxDB8+XPv27dPbb7990m3ddNNN+vLLL/XDDz+odevWKioqOuYYwzD02GOP6ZlnntGGDRuUlpam66+/XnfeeedJ919p+PDhmjp1qt++rl27av78+b7XzzzzjF555RV98803OnDggPbu3auUlBTTYgAAWIfx+uQwXgPhicQbCBGGYWjEiBFasGCBvvvuuyqPuemmm/TRRx/pH//4h9q1a6f9+/dr165dpscyYMAATZkyxfc6JibG7/ulpaUaMGCABgwYoDvuuMP0/gEAqKsYr4HwxFRzIADDhw/X3Llz9cQTT8jlcsnlcmn9+vWSpOXLl2vQoEFKSEhQ48aNdc011/gNjm+++abatWunuLg4NWjQQGeffbZKSko0duxYTZ06Ve+8846vzTlz5tQ4xn/9618qLCxUbm5uld//8ccfNWnSJL3zzjs677zzlJOTo44dO+rss8+utt2ff/5ZV1xxhVJTU1WvXj116dJFCxYsqPYct9ut9PR035aamur3/Ztvvlm33367zjjjjOAuEgCAajBeM14DdRWJNxCAJ554Qt26ddPIkSO1detWbd26Vc2aNdPWrVvVq1cvdezYUYsXL9asWbO0fft2XXbZZZKkrVu3aujQoRoxYoR+/PFHzZkzRxdddJEMw9Ctt96qyy67TAMGDPC12b1794DiGTt2rLKzs4O6hvfee0+5ubl6//33lZOTo+zsbF133XXas2fPcc85ePCgevXqpS1btujdd9/Vt99+q7/97W/yer2SpPXr11f5B8icOXOUlpamvLw8jRw5Ujt27AgqVgAAaoLxmvEaqKuYag4EIDk5WTExMYqPj1d6erpv/6RJk9S5c2c9+OCDvn3PP/+8mjVrppUrV+rgwYOqqKjQRRddpKysLElSu3btfMfGxcXp8OHDfm0GomHDhmrRokVQ56xdu1YbNmzQG2+8oWnTpsnj8WjUqFG65JJL9Omnn1Z5ziuvvKKdO3dq0aJFvk/BW7Zs6ft+dHS08vPzFR8f79s3cOBAXXrppcrKytK6det0991363e/+52WLFkit9sdVMwAAASD8ZrxGqirSLyBk7BkyRJ99tlnSkhIOOZ7a9asUf/+/dW3b1+1a9dO55xzjvr3769LLrlE9evXP6l+b7zxRt14441BneP1enX48GFNmzZNeXl5kqTnnntOp556qlasWKH8/PxjzikqKlKnTp2OmXpWqUmTJvrpp5/89l1++eW+r9u2basuXbooKytLM2fO1EUXXRRUzAAAmIHxmvEasBtTzYGT4PV6NWTIEBUVFfltq1at0llnnaXIyEjNnj1bH3zwgQoK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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "maze.visualize(sequences=[('rs', rs_solution.get_action_sequence()),\n", + " ('test', ['R', 'R', 'D', 'D', 'D', 'L', 'U', 'R', 'D', 'D', 'R', 'D', 'D', 'R', 'R', 'R'])])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tests Cases\n", + "\n", + "After the cell where you have to implement e.g., a search algorithm, there will be cells with some tests like the following. These tests let you test the behaviour and correctness of your algorithms (although they might not check for all possible edge cases!). " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# this is a testing cell, do not edit or delete\n", + "\n", + "assert(rs_solution is not None)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now you should know everything about the framework that you need to all future practical exercises.\n", + "\n", + "**Feel free to play around with the mazes and functions in this notebook!**" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/pig_lite/.gitignore b/pig_lite/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/pig_lite/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/pig_lite/.idea/.gitignore b/pig_lite/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/pig_lite/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/pig_lite/.idea/inspectionProfiles/Project_Default.xml b/pig_lite/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..879f166 --- /dev/null +++ b/pig_lite/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,24 @@ + + + + \ No newline at end of file diff --git a/pig_lite/.idea/inspectionProfiles/profiles_settings.xml b/pig_lite/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/pig_lite/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/pig_lite/.idea/misc.xml b/pig_lite/.idea/misc.xml new file mode 100644 index 0000000..9de2865 --- /dev/null +++ b/pig_lite/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/pig_lite/.idea/modules.xml b/pig_lite/.idea/modules.xml new file mode 100644 index 0000000..e45f676 --- /dev/null +++ b/pig_lite/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/pig_lite/.idea/pig_lite.iml b/pig_lite/.idea/pig_lite.iml new file mode 100644 index 0000000..8b8c395 --- /dev/null +++ b/pig_lite/.idea/pig_lite.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/pig_lite/.idea/vcs.xml b/pig_lite/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/pig_lite/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pig_lite/README.md b/pig_lite/README.md new file mode 100644 index 0000000..ed80324 --- /dev/null +++ b/pig_lite/README.md @@ -0,0 +1,3 @@ +# pig_lite + +This is PIG (=Problem Instance Generator) Lite, a simplified and cleaned up version of the framework previously used for the AI assignments. \ No newline at end of file diff --git a/pig_lite/bayesian_net/__init__.py b/pig_lite/bayesian_net/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pig_lite/bayesian_net/bayesian_net.py b/pig_lite/bayesian_net/bayesian_net.py new file mode 100644 index 0000000..1a8b584 --- /dev/null +++ b/pig_lite/bayesian_net/bayesian_net.py @@ -0,0 +1,154 @@ +import matplotlib +import numpy as np +import networkx as nx +import matplotlib.pyplot as plt + +#matplotlib.use('TkAgg') + + +class BayesianNode: + """ Building stone for BayesianNet class. Represents conditional probability distribution + for a boolean random variable, P(X | parents). """ + def __init__(self, X: str, parents: str, cpt: dict = None): + """ + X: String describing variable name + + parents: String containing parent variable names, separated with a whitespace + + cpt: dict that contains the distribution P(X=true | parent1=v1, parent2=v2...). + Dict should be structured as follows: {(v1, v2, ...): p, ...}, and each key must have + as many values as there are parents. Values (v1, v2, ...) must be True/False. + """ + if not isinstance(X, str) or not isinstance(parents, str): + raise ValueError("Use valid arguments - X and parents have to be strings (but at least one is not)!") + self.rand_var = X + self.parents = parents.split() + self.children = [] + + # in case of 0 or 1 parent, fix tuples first + if cpt and isinstance(cpt, (float, int)): + cpt = {(): cpt} + elif cpt and isinstance(cpt, dict): + if isinstance(list(cpt.keys())[0], bool): + # only one parent + cpt = {(k, ): v for k, v in cpt.items()} + elif cpt: + raise ValueError("Define cpt with a valid data type (dict, or int).") + # check format of cpt dict + if cpt: + for val, p in cpt.items(): + assert isinstance(val, tuple) and len(val) == len(self.parents) + assert all(isinstance(v, bool) for v in val) + assert 0 <= p <= 1 + + self.cpt = cpt + + def __repr__(self): + """ String representation of Bayesian Node. """ + return repr((self.rand_var, ' '.join(["parent(s):"] + self.parents))) + + def cond_probability(self, value: bool, event: dict): + """ + Returns conditional probability P(X=value | event) for an atomic event, + i.e. where each parent needs to be assigned a value. + value: bool (value of this random variable) + event: dict, assigning a value to each parent variable + """ + assert isinstance(value, bool) + if self.cpt: + prob_true = self.cpt[self.get_event_values(event)] + return prob_true if value else 1 - prob_true + + return None + + def get_event_values(self, event: dict): + """ Given an event (dict), returns tuple of values for all parents. """ + return tuple(event[p] for p in self.parents) + + +class BayesianNet: + """ Bayesian Network class for boolean random variables. Consists of BayesianNode-s. """ + def __init__(self, node_specs: list): + """ + Creates BayesianNet with given node_specs. Nodes should be in causal order (parents before children). + node_specs should be list of parameters for BayesianNode class. + """ + self.nodes = [] + self.rand_vars = [] + for spec in node_specs: + self.add_node(spec) + + def add_node(self, node_spec): + """ Creates a BayesianNode and adds it to the net, if the variable does *not*, and the parents do exist. """ + node = BayesianNode(*node_spec) + if node.rand_var in self.rand_vars: + raise ValueError("Variable {} already exists in network, cannot be defined twice!".format(node.rand_var)) + if not all((parent in self.rand_vars) for parent in node.parents): + raise ValueError("Parents do not all exist yet! Make sure to first add all parent nodes.") + self.nodes.append(node) + self.rand_vars.append(node.rand_var) + for parent in node.parents: + self.get_node_for_name(parent).children.append(node) + + def get_node_for_name(self, node_name): + """ Given the name of a random variable, returns the according BayesianNode of this network. """ + for n in self.nodes: + if n.rand_var == node_name: + return n + + raise ValueError("The variable {} does not exist in this network!".format(node_name)) + + def __repr__(self): + """ String representation of this Bayesian Network. """ + return "BayesianNet:\n{0!r}".format(self.nodes) + + def _get_depth(self, rand_var): + """ Given random variable, returns "depth" of node in graph for plotting. """ + node = self.get_node_for_name(rand_var) + if len(node.parents) == 0: + return 0 + + return max([self._get_depth(p) for p in node.parents]) + 1 + + def draw(self, title, save_path=None): + """ Draws the BN with networkx. Requires title for plot. """ + plt.figure(figsize=(14, 8)) + nx_bn = nx.DiGraph() + nx_bn.add_nodes_from(self.rand_vars) + pos = {rand_var: (10, 10) for rand_var in self.rand_vars} + for rand_var in self.rand_vars: + node = self.get_node_for_name(rand_var) + for c in node.children: + nx_bn.add_edge(rand_var, c.rand_var) + pos.update({c.rand_var: (pos[c.rand_var][0], pos[c.rand_var][1] - 3)}) + + depths = {rand_var: self._get_depth(rand_var) for rand_var in self.rand_vars} + _, counts = np.unique(list(depths.values()), return_counts=True) + xs = [list(np.linspace(6, 14, c)) if c > 1 else [10] for c in counts] + pos = {rand_var: (xs[depths[rand_var]].pop(), 10 - depths[rand_var] * 3) for rand_var in self.rand_vars} + + nx.set_node_attributes(nx_bn, pos, 'pos') + nx.draw_networkx(nx_bn, arrows=True, pos=nx.get_node_attributes(nx_bn, "pos"), + node_shape="o", node_color="white", node_size=7000, edgecolors="gray") + plt.title(title) + plt.box(False) + plt.margins(0.3) + plt.tight_layout() + if save_path: + plt.savefig(save_path, dpi=400) + else: + plt.show() + + +if __name__ == '__main__': + T = True + F = False + bn = BayesianNet([ + ('Burglary', '', 0.001), + ('Earthquake', '', {(): 0.002}), + ('Alarm', 'Burglary Earthquake', + {(T, T): 0.95, (T, F): 0.94, (F, T): 0.29, (F, F): 0.001}), + ('JohnCalls', 'Alarm', {T: 0.90, F: 0.05}), + ('MaryCalls', 'Alarm', {T: 0.70, F: 0.01}) + ]) + bn.draw("") diff --git a/pig_lite/datastructures/__init__.py b/pig_lite/datastructures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pig_lite/datastructures/priority_queue.py b/pig_lite/datastructures/priority_queue.py new file mode 100644 index 0000000..b5db414 --- /dev/null +++ b/pig_lite/datastructures/priority_queue.py @@ -0,0 +1,61 @@ +import heapq +from functools import total_ordering + + +# this annotation saves us some implementation work +@total_ordering +class Item(object): + def __init__(self, insertion, priority, value): + self.insertion = insertion + self.priority = priority + self.value = value + + def __lt__(self, other): + # if the decision "self < other" can be done + # based on the priority, do that + if self.priority < other.priority: + return True + elif self.priority == other.priority: + # in case the priorities are equal, we + # fall back on the insertion order, + # which establishes a total ordering + return self.insertion < other.insertion + return False + + def __eq__(self, other): + return self.priority == other.priority and self.insertion == other.insertion + + def __repr__(self): + return '({}, {}, {})'.format(self.priority, self.insertion, self.value) + + +class PriorityQueue(object): + def __init__(self): + self.insertion = 0 + self.heap = [] + + def has_elements(self): + return len(self.heap) > 0 + + def put(self, priority, value): + heapq.heappush(self.heap, Item(self.insertion, priority, value)) + self.insertion += 1 + + def get(self, include_priority=False): + item = heapq.heappop(self.heap) + if include_priority: + return item.priority, item.value + else: + return item.value + + def __iter__(self): + return iter([item.value for item in self.heap]) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return ('PriorityQueue [' + ','.join((str(item.value) for item in self.heap)) + ']') + + def __len__(self): + return len(self.heap) diff --git a/pig_lite/datastructures/queue.py b/pig_lite/datastructures/queue.py new file mode 100644 index 0000000..8b7ea03 --- /dev/null +++ b/pig_lite/datastructures/queue.py @@ -0,0 +1,27 @@ +from collections import deque + + +class Queue(object): + def __init__(self): + self.d = deque() + + def put(self, v): + self.d.append(v) + + def get(self): + return self.d.popleft() + + def has_elements(self): + return len(self.d) > 0 + + def __iter__(self): + return iter(self.d) + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return ('Queue [' + ','.join((str(item) for item in self.d)) + ']') + + def __len__(self): + return len(self.d) diff --git a/pig_lite/datastructures/stack.py b/pig_lite/datastructures/stack.py new file mode 100644 index 0000000..8dd970a --- /dev/null +++ b/pig_lite/datastructures/stack.py @@ -0,0 +1,21 @@ +from collections import deque + + +class Stack(object): + def __init__(self): + self.d = deque() + + def put(self, v): + self.d.append(v) + + def get(self): + return self.d.pop() + + def has_elements(self): + return len(self.d) > 0 + + def __iter__(self): + return iter(self.d) + + def __repr__(self): + return ('Stack [' + ','.join((str(item) for item in self.d)) + ']') diff --git a/pig_lite/decision_tree/dt_base.py b/pig_lite/decision_tree/dt_base.py new file mode 100644 index 0000000..c5ba853 --- /dev/null +++ b/pig_lite/decision_tree/dt_base.py @@ -0,0 +1,61 @@ +from pig_lite.decision_tree.dt_node import DecisionTreeNodeBase +import scipy.stats as stats + +def entropy(y: list): + """ + Compute the entropy of a binary label distribution. + + This function calculates the entropy of a binary classification label list `y` as a wrapper + around `scipy.stats.entropy`. It assumes the labels are binary (0 or 1) and computes the + proportion of positive labels (1s) to calculate the entropy. + + Parameters + ---------- + y : list + A list of binary labels (0 or 1). + + Returns + ------- + float + The entropy of the label distribution. If the list is empty, returns 0.0. + + Notes + ----- + - Entropy is calculated using the formula: + H = -p*log2(p) - (1-p)*log2(1-p) + where `p` is the proportion of positive labels (1s). + - If `y` is empty, entropy is defined as 0.0. + + Examples + -------- + >>> entropy([0, 0, 1, 1]) + 1.0 + + >>> entropy([1, 1, 1, 1]) + 0.0 + + >>> entropy([]) + 0.0 + """ + if len(y) == 0: return 0.0 + positive = sum(y) / len(y) + return stats.entropy([positive, 1 - positive], base=2) + +# these two dummy classes are only used so we can import them and load trees from a pickle file before they are implemented by the students +class DecisionTree(): + def __init__(self) -> None: + pass + + def get_height(self, node): + if node is None: + return 0 + return max(self.get_height(node.left_child), self.get_height(node.right_child)) + 1 + + def print(self): + if self.root is not None: + height = self.get_height(self.root) + self.root.print_tree(height) + +class DecisionTreeNode(DecisionTreeNodeBase): + def __init__(self) -> None: + pass diff --git a/pig_lite/decision_tree/dt_node.py b/pig_lite/decision_tree/dt_node.py new file mode 100644 index 0000000..83e976f --- /dev/null +++ b/pig_lite/decision_tree/dt_node.py @@ -0,0 +1,70 @@ +from pig_lite.datastructures.queue import Queue + +class DecisionTreeNodeBase(): + def __init__(self): + self.label = None + self.split_point = None + self.split_feature = None + self.left_child = None + self.right_child = None + + def print_node(self, height, level=1): + node_width = 10 + n_spaces = 2 ** (height - level - 1) * node_width - node_width // 2 + if n_spaces > 0: + text = " " * n_spaces + else: + text = "" + + if self.label is None and self.split_feature is None: + return f"{text} {text}" + + if self.label is not None: + text = f"{text}( {self.label} ){text}" + elif self.split_feature is not None: + text_snippet = f"(x{self.split_feature}:{self.split_point:.2f})" + if len(text_snippet) != node_width: + text_snippet = f" {text_snippet}" + text = f"{text}{text_snippet}{text}" + return text + + def __str__(self): + if self.label is not None: return f"({self.label})" + + str_value = f"{self.split_feature}:{self.split_point:.2f}|{self.left_child}{self.right_child}" + return str_value + + def print_tree(self, height): + visited = set() + frontier = Queue() + + lines = [''] + + previous_level = 1 + frontier.put((self, 1)) + + while frontier.has_elements(): + current, level = frontier.get() + if level > previous_level: + lines.append('') + previous_level = level + lines[-1] += current.print_node(height, level) + if current not in visited: + visited.add(current) + if current.left_child is not None: + frontier.put((current.left_child, level + 1)) + else: + if level < height: frontier.put((DecisionTreeNodeBase(), level + 1)) + if current.right_child is not None: + frontier.put((current.right_child, level + 1)) + else: + if level < height: frontier.put((DecisionTreeNodeBase(), level + 1)) + + for line in lines: + print(line) + return None + + def split(): + raise NotImplementedError() + + \ No newline at end of file diff --git a/pig_lite/decision_tree/training_set.py b/pig_lite/decision_tree/training_set.py new file mode 100644 index 0000000..f27e78c --- /dev/null +++ b/pig_lite/decision_tree/training_set.py @@ -0,0 +1,84 @@ +import json +import numpy as np +import matplotlib.pyplot as plt + +import matplotlib.pyplot as plt +import warnings + +class TrainingSet(): + def __init__(self, X, y): + self.X = X + self.y = y + + def to_json(self): + return json.dumps(dict( + type=self.__class__.__name__, + X=self.X.tolist(), + y=self.y.tolist() + )) + + @staticmethod + def from_json(jsonstring): + data = json.loads(jsonstring) + return TrainingSet.from_dict(data) + + @staticmethod + def from_dict(data): + return TrainingSet( + np.array(data['X']).squeeze(), + np.array(data['y']) + ) + + def plot_node_boundaries(self, node, limit_left, limit_right, limit_top, limit_bottom, max_depth, level=1): + + split_point = node.split_point + limit_left_updated = limit_left + limit_right_updated = limit_right + limit_top_updated = limit_top + limit_bottom_updated = limit_bottom + + if node.split_feature == 0: + if limit_bottom == limit_top: + warnings.warn('limit_bottom equals limit_top; extending by 0.1') + plt.plot([split_point, split_point], [limit_bottom - 0.1, limit_top + 0.1], color="purple", alpha=1 / level) + else: + plt.plot([split_point, split_point], [limit_bottom, limit_top], color="purple", alpha=1 / level) + limit_left_updated = split_point + limit_right_updated = split_point + + else: + if limit_left == limit_right: + warnings.warn('limit_left equals limit_right; extending by 0.1') + plt.plot([limit_left - 0.1, limit_right + 0.1], [split_point, split_point], color="purple", alpha=1 / level) + else: + plt.plot([limit_left, limit_right], [split_point, split_point], color="purple", alpha=1 / level) + limit_top_updated = split_point + limit_bottom_updated = split_point + + if level == max_depth: + return + if node.left_child is not None: self.plot_node_boundaries(node.left_child, limit_left, limit_right_updated, + limit_top_updated, limit_bottom, max_depth, level + 1) + if node.right_child is not None: self.plot_node_boundaries(node.right_child, limit_left_updated, limit_right, limit_top, + limit_bottom_updated, max_depth, level + 1) + + def visualize(self, tree=None, max_height=None): + symbols = [["x", "o"][index] for index in self.y] + for y in set(self.y): + X = self.X[self.y == y, :] + plt.scatter(X[:, 0], X[:, 1], + color=["red", "blue"][y], + marker=symbols[y], + label="class: {}".format(y)) + + if tree is not None: + tree_height = tree.get_height(tree.root) + if max_height is None or max_height > tree_height: + max_height = tree_height + self.plot_node_boundaries(tree.root, + limit_left=min(self.X[:, 0]), + limit_right=max(self.X[:, 0]), + limit_top=max(self.X[:, 1]), + limit_bottom=min(self.X[:, 1]), + max_depth=max_height) # TODO: make parameterizable + diff --git a/pig_lite/environment/__init__.py b/pig_lite/environment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pig_lite/environment/base.py b/pig_lite/environment/base.py new file mode 100644 index 0000000..7acfab1 --- /dev/null +++ b/pig_lite/environment/base.py @@ -0,0 +1,60 @@ +import json +import hashlib +import numpy as np + + +class Environment: + def step(self, action): + raise NotImplementedError() + + def reset(self): + raise NotImplementedError() + + def get_n_actions(self): + raise NotImplementedError() + + def get_n_states(self): + raise NotImplementedError() + + def get_flat_policy(self, policy): + flat_policy = [] + for state in range(self.get_n_states()): + for action in range(self.get_n_actions()): + flat_policy.append((state, action, policy[state, action])) + return flat_policy + + def get_policy_hash(self, outcome): + flat_policy = self.get_flat_policy(outcome.policy) + flat_policy_as_str = ','.join(map(str, flat_policy)) + flat_policy_hash = hashlib.sha256(flat_policy_as_str.encode('UTF-8')).hexdigest() + return flat_policy_hash + + +class Outcome: + def __init__(self, n_episodes, policy, V, Q): + self.n_episodes = n_episodes + self.policy = policy + self.V = V + self.Q = Q + + def get_n_episodes(self): + return self.n_episodes + + def to_json(self): + return json.dumps(dict( + type=self.__class__.__name__, + n_episodes=self.n_episodes, + policy=self.policy.tolist(), + V=self.V.tolist(), + Q=self.Q.tolist(), + )) + + @staticmethod + def from_json(jsonstring): + data = json.loads(jsonstring) + return Outcome( + data['n_episodes'], + np.array(data['policy']), + np.array(data['V']), + np.array(data['Q']) + ) diff --git a/pig_lite/environment/gridworld.py b/pig_lite/environment/gridworld.py new file mode 100644 index 0000000..7d8f13b --- /dev/null +++ b/pig_lite/environment/gridworld.py @@ -0,0 +1,360 @@ +import json +import numpy as np + +from pig_lite.environment.base import Environment + +DELTAS = [ + (-1, 0), + (+1, 0), + (0, -1), + (0, +1) +] +NAMES = [ + 'left', + 'right', + 'up', + 'down' +] + +def sample(rng, elements): + """ Samples an element of `elements` randomly. """ + csp = np.cumsum([elm[0] for elm in elements]) + idx = np.argmax(csp > rng.uniform(0, 1)) + return elements[idx] + + +class Gridworld(Environment): + def __init__(self, seed, dones, rewards, starts): + self.seed = seed + self.rng = np.random.RandomState(seed) + self.dones = dones + self.rewards = rewards + self.starts = starts + + self.__compute_P() + + def reset(self): + """ Resets the environment of this gridworld to a randomly sampled start state. """ + _, self.state = sample(self.rng, self.starts) + return self.state + + def step(self, action): + """ Performs the action on the gridworld, where next state of environment is sampled based on self.P. """ + _, self.state, reward, done = sample(self.rng, self.P[self.state][action]) + return self.state, reward, done + + def get_n_actions(self): + """ Returns the number of actions available in this gridworld. """ + return 4 + + def get_n_states(self): + """ Returns the number of states available in this gridworld. """ + return np.prod(self.dones.shape) + + def get_gamma(self): + """ Returns discount factor gamma for this gridworld. """ + return 0.99 + + def __compute_P(self): + """ Computes and stores the transitions for this gridworld. """ + w, h = self.dones.shape + + def inbounds(i, j): + """ Checks whether coordinates i and j are within the grid. """ + return i >= 0 and j >= 0 and i < w and j < h + + self.P = dict() + for i in range(0, w): + for j in range(0, h): + state = j * w + i + self.P[state] = dict() + + if self.dones[i, j]: + for action in range(self.get_n_actions()): + # make it absorbing + self.P[state][action] = [(1, state, 0, True)] + else: + for action, (dx, dy) in enumerate(DELTAS): + ortho_dir_probs = [ + (0.8, dx, dy), + (0.1, dy, dx), + (0.1, -dy, -dx) + ] + transitions = [] + for p, di, dj in ortho_dir_probs: + ni = i + di + nj = j + dj + if inbounds(ni, nj): + # we move + sprime = nj * w + ni + done = self.dones[ni, nj] + reward = self.rewards[ni, nj] + transitions.append((p, sprime, reward, done)) + else: + # stay in the same state, b/c we bounced + sprime = state + done = self.dones[i, j] + reward = self.rewards[i, j] + transitions.append((p, sprime, reward, done)) + + self.P[state][action] = transitions + + def to_json(self): + """ Converts and stores this gridworld to a JSON file. """ + return json.dumps(dict( + type=self.__class__.__name__, + seed=self.seed, + dones=self.dones.tolist(), + rewards=self.rewards.tolist(), + starts=self.starts.tolist() + )) + + @staticmethod + def from_json(jsonstring): + """ Loads given JSON file, and creates gridworld with information. """ + data = json.loads(jsonstring) + return Gridworld( + data['seed'], + np.array(data['dones']), + np.array(data['rewards']), + np.array(data['starts'], dtype=np.int64), + ) + + @staticmethod + def from_dict(data): + """ Creates gridworld with information in given data-dictionary. """ + return Gridworld( + data['seed'], + np.array(data['dones']), + np.array(data['rewards']), + np.array(data['starts'], dtype=np.int64), + ) + + @staticmethod + def get_random_instance(rng, size): + """ Given random generator and problem size, generates Gridworld instance. """ + dones, rewards, starts = Gridworld.__generate(rng, size) + return Gridworld(rng.randint(0, 2 ** 31), dones, rewards, starts) + + @staticmethod + def __generate(rng, size): + """ Helper function that retrieves dones, rewards, starts for Gridworld instance generation. """ + dones = np.full((size, size), False, dtype=bool) + rewards = np.zeros((size, size), dtype=np.int8) - 1 + + coordinates = [] + for i in range(1, size - 1): + for j in range(1, size - 1): + coordinates.append((i, j)) + indices = np.arange(len(coordinates)) + + chosen = rng.choice(indices, max(1, len(indices) // 10), replace=False) + + for c in chosen: + x, y = coordinates[c] + dones[x, y] = True + rewards[x, y] = -100 + + starts = np.array([[1, 0]]) + dones[-1, -1] = True + rewards[-1, -1] = 100 + + return dones, rewards, starts + + @staticmethod + def get_minimum_problem_size(): + return 3 + + def visualize(self, outcome, coords=None, grid=None): + """ Visualisation function for gridworld; plots environment, policy, Q. """ + policy = None + Q = None + V = None + if outcome is not None: + if outcome.policy is not None: + policy = outcome.policy + + if outcome.V is not None: + V = outcome.V + + if outcome.Q is not None: + Q = outcome.Q + + self._plot_environment_and_policy(policy, V, Q, show_coordinates=coords, show_grid=grid) + + def _plot_environment_and_policy(self, policy=None,V=None, Q=None, show_coordinates=False, + show_grid=False, plot_filename=None, debug_info=False): + """ Function that plots environment and policy. """ + import matplotlib.pyplot as plt + fig, axes = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True) + dones_ax = axes[0, 0] + rewards_ax = axes[0, 1] + V_ax = axes[1, 0] + Q_ax = axes[1, 1] + + dones_ax.set_title('Terminal States and Policy') + dones_ax.imshow(self.dones.T, cmap='gray_r', vmin=0, vmax=4) + + rewards_ax.set_title('Immediate Rewards') + rewards_ax.imshow(self.rewards.T, cmap='RdBu_r', vmin=-25, vmax=25) + + if len(policy) > 0: + self._plot_policy(dones_ax, policy) + + w, h = self.dones.shape + V_array = V.reshape(self.dones.shape).T + V_ax.set_title('State Value Function $V(s)$') + r = max(1e-13, np.max(np.abs(V_array))) + V_ax.imshow(V_array.T, cmap='RdBu_r', vmin=-r, vmax=r) + + if debug_info: + for s in range(len(V)): + sy, sx = divmod(s, w) + V_ax.text(sx, sy, f'{sx},{sy}:{s}', + color='w', fontdict=dict(size=6), + horizontalalignment='center', verticalalignment='center') + + Q_ax.set_title('State Action Value Function $Q(s, a)$') + poly_patches_q_values = self._draw_Q(Q_ax, Q, debug_info) + + def format_coord(x, y): + for poly_patch, q_value in poly_patches_q_values: + if poly_patch.contains_point(Q_ax.transData.transform((x, y))): + return f'x:{x:4.2f} y:{y:4.2f} {q_value}' + return f'x:{x:4.2f} y:{y:4.2f}' + + Q_ax.format_coord = format_coord + + for ax in [dones_ax, rewards_ax, V_ax, Q_ax]: + ax.tick_params( + top=show_coordinates, + left=show_coordinates, + labelleft=show_coordinates, + labeltop=show_coordinates, + right=False, + bottom=False, + labelbottom=False + ) + + # Major ticks + s = self.dones.shape[0] + ax.set_xticks(np.arange(0, s, 1)) + ax.set_yticks(np.arange(0, s, 1)) + + # Minor ticks + ax.set_xticks(np.arange(-.5, s, 1), minor=True) + ax.set_yticks(np.arange(-.5, s, 1), minor=True) + + if show_grid: + for color, ax in zip(['m', 'w', 'w'], [dones_ax, rewards_ax, V_ax]): + # Gridlines based on minor ticks + ax.grid(which='minor', color=color, linestyle='-', linewidth=1) + + plt.tight_layout() + if plot_filename is not None: + plt.savefig(plot_filename) + plt.close(fig) + else: + plt.show() + + def _plot_policy(self, ax, policy): + """ Function that plots policy. """ + w, h = self.dones.shape + xs = np.arange(w) + ys = np.arange(h) + xx, yy = np.meshgrid(xs, ys) + + # we need a quiver for each of the four action + quivers = list() + for a in range(self.get_n_actions()): + quivers.append(list()) + + # we parse the textual description of the lake + for s in range(self.get_n_states()): + y, x = divmod(s, w) + if self.dones[x, y]: + for a in range(self.get_n_actions()): + quivers[a].append((0., 0.)) + else: + for a in range(self.get_n_actions()): + wdx, wdy = DELTAS[a] + corrected = np.array([wdx, -wdy]) + quivers[a].append(corrected * policy[s, a]) + + # plot each quiver + for quiver in quivers: + q = np.array(quiver) + ax.quiver(xx, yy, q[:, 0], q[:, 1], units='xy', scale=1.5) + + def _draw_Q(self, ax, Q, debug_info): + """ Function that draws Q. """ + pattern = np.zeros(self.dones.shape) + ax.imshow(pattern, cmap='gray_r') + import matplotlib.pyplot as plt + from matplotlib.cm import ScalarMappable + from matplotlib.colors import Normalize + from matplotlib.patches import Rectangle, Polygon + w, h = self.dones.shape + + r = max(1e-13, np.max(np.abs(Q))) + norm = Normalize(vmin=-r, vmax=r) + cmap = plt.get_cmap('RdBu_r') + sm = ScalarMappable(norm, cmap) + + hover_polygons = [] + for state in range(len(Q)): + qs = Q[state] + # print('qs', qs) + y, x = divmod(state, w) + if self.dones[x, y]: + continue + y += 0.5 + x += 0.5 + + dx = 1 + dy = 1 + + ulx = (x - 1) * dx + uly = (y - 1) * dy + + rect = Rectangle( + xy=(ulx, uly), + width=dx, + height=dy, + edgecolor='k', + facecolor='none' + ) + ax.add_artist(rect) + + mx = (x - 1) * dx + dx / 2. + my = (y - 1) * dy + dy / 2. + + ul = ulx, uly + ur = ulx + dx, uly + ll = ulx, uly + dy + lr = ulx + dx, uly + dy + m = mx, my + + up = [ul, m, ur] + left = [ul, m, ll] + right = [ur, m, lr] + down = [ll, m, lr] + action_polys = [left, right, up, down] + for a, poly in enumerate(action_polys): + poly_patch = Polygon( + poly, + edgecolor='k', + linewidth=0.1, + facecolor=sm.to_rgba(qs[a]) + ) + if debug_info: + mmx = np.mean([x for x, y in poly]) + mmy = np.mean([y for x, y in poly]) + sss = '\n'.join(map(str, self.P[state][a])) + ax.text(mmx, mmy, f'{NAMES[a][0]}:{sss}', + fontdict=dict(size=5), horizontalalignment='center', + verticalalignment='center') + + hover_polygons.append((poly_patch, f'{NAMES[a]}:{qs[a]:4.2f}')) + ax.add_artist(poly_patch) + return hover_polygons diff --git a/pig_lite/game/__init__.py b/pig_lite/game/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pig_lite/game/base.py b/pig_lite/game/base.py new file mode 100644 index 0000000..a4e0a80 --- /dev/null +++ b/pig_lite/game/base.py @@ -0,0 +1,87 @@ +import hashlib + +class Node(object): + def __init__(self, parent, state, action, player, depth): + self.parent = parent + self.state = state + self.action = action + self.player = player + self.depth = depth + + def key(self): + # if state is composed of other stuff (dict, set, ...) + # make it a tuple containing hashable datatypes + # (this is supposed to be overridden by subclasses) + return tuple(self.state) + (self.player, ) + + def __hash__(self): + return hash(self.key()) + + def __eq__(self, other): + if type(self) == type(other): + return self.key() == other.key() + raise ValueError('cannot simply compare two different node types') + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return 'Node(id:{}, parent:{}, state:{}, action:{}, player:{}, depth:{})'.format( + id(self), + id(self.parent), + self.state, + self.action, + self.player, + self.depth + ) + + def get_move_sequence(self): + current = self + reverse_sequence = [] + while current.parent is not None: + reverse_sequence.append((current.player, current.action)) + current = current.parent + return list(reversed(reverse_sequence)) + + def get_move_sequence_hash(self): + move_sequence = self.get_move_sequence() + move_sequence_as_str = ';'.join(map(str, move_sequence)) + move_sequence_hash = hashlib.sha256(move_sequence_as_str.encode('UTF-8')).hexdigest() + return move_sequence_hash + +class Game(object): + def get_number_of_expanded_nodes(self): + raise NotImplementedError() + + def get_start_node(self): + raise NotImplementedError() + + def winner(self, node): + raise NotImplementedError() + + def successors(self, node): + raise NotImplementedError() + + def get_max_player(self): + raise NotImplementedError() + + def to_json(self): + raise NotImplementedError() + + def get_move_sequence(self, end: Node): + if end is None: + return list() + return end.get_move_sequence() + + def get_move_sequence_hash(self, end: Node): + if end is None: + return '' + return end.get_move_sequence_hash() + + @staticmethod + def from_json(jsonstring): + raise NotImplementedError() + + @staticmethod + def get_minimum_problem_size(): + raise NotImplementedError() diff --git a/pig_lite/game/tictactoe.py b/pig_lite/game/tictactoe.py new file mode 100644 index 0000000..1e49d34 --- /dev/null +++ b/pig_lite/game/tictactoe.py @@ -0,0 +1,371 @@ +import json +import numpy as np + +from copy import deepcopy +from pig_lite.game.base import Node, Game + + +class TTTNode(Node): + def key(self): + return tuple(self.state.flatten().tolist() + [self.player]) + + def __repr__(self): + return '"TTTNode(\nid:{}\nparent:{}\nboard:\n{}\nplayer:\n{}\naction:\n{}\ndepth:{})"'.format( + id(self), + id(self.parent), + # this needs to be printed transposed, so it fits together with + # how matplotlib's 'imshow' renders images + self.state.T, + self.player, + self.action, + self.depth + ) + + def pretty_print(self): + import matplotlib.pyplot as plt + from matplotlib.colors import ListedColormap + cm = ListedColormap(['tab:blue', 'lightgray', 'tab:orange']) + print('State of the board:') + plt.figure(figsize=(2, 2)) + plt.imshow(self.state.T, cmap=cm) + plt.axis('off') + plt.show() + print('Performed moves: {}'.format(self.depth)) + + +class TicTacToe(Game): + def __init__(self, rng=None, depth=None): + self.n_expands = 0 + self.play_randomly(rng, depth) + + def play_randomly(self, rng, depth): + """ Initialises self.start_node to be either empty board, or board at given depth after random playing. """ + empty_board = np.zeros((3, 3), dtype=int) + start_from_empty = TTTNode(None, empty_board, None, 1, 0) + if rng is None or depth is None or depth == 0: + self.start_node = start_from_empty + else: + # proceed playing randomly until either 'depth' is reached, + # or the node is a terminal node + nodes = [] + successors = [start_from_empty] + while True: + index = rng.randint(0, len(successors)) + current = successors[index] + + if current.depth == depth: + break + + nodes.append(current) + terminal, winner = self.outcome(current) + if terminal: + break + successors = self.successors(current) + + for node in successors: + nodes.append(node) + + self.start_node = TTTNode(None, current.state, None, current.player, 0) + + def get_start_node(self): + """ Returns start node of this Game. """ + return self.start_node + + def outcome(self, node): + """ Returns tuple stating whether game is finished or not, and winner (or None otherwise). """ + board = node.state + for player in [-1, 1]: + # checks rows and columns + for i in range(3): + if (board[i, :] == player).all() or (board[:, i] == player).all(): + return True, player + + # checks diagonals + if (np.diag(board) == player).all() or (np.diag(np.rot90(board)) == player).all(): + return True, player + + # if board is full, and none of the conditions above are true, + # nobody has won --- it's a draw + if (board != 0).all(): + return True, None + + # else, continue + return False, None + + def get_max_player(self): + """ Returns identifier of MAX player used in this game. """ + return 1 + + def successor(self, node, action): + """ Performs given action at given game node, and returns successor TTT node. """ + board = node.state + player = node.player + + next_board = board.copy() + next_board[action] = player + + if player == 1: + next_player = -1 + else: + next_player = 1 + + return TTTNode( + node, + next_board, + action, + next_player, + node.depth + 1 + ) + + def get_number_of_expanded_nodes(self): + return self.n_expands + + def successors(self, node): + """ Given a game node, returns all possible successor nodes based on all actions that can be performed. """ + self.n_expands += 1 + terminal, winner = self.outcome(node) + + if terminal: + return [] + else: + successor_nodes = [] + # iterate through all possible coordinates (==actions) + for action in zip(*np.nonzero(node.state == 0)): + successor_nodes.append(self.successor(node, action)) + return successor_nodes + + def to_json(self): + """ Converts and stores this TTT game to a JSON file. """ + return json.dumps(dict( + type=self.__class__.__name__, + start_state=self.start_node.state.tolist(), + start_player=self.start_node.player + )) + + @staticmethod + def from_json(jsonstring): + """ Loads given JSON file, and creates game with information. """ + data = json.loads(jsonstring) + + ttt = TicTacToe() + ttt.start_node = TTTNode( + None, + np.array(data['start_state'], dtype=int), + None, + data['start_player'], + 0 + ) + return ttt + + @staticmethod + def from_dict(data): + """ Creates game with information in given data-dictionary. """ + ttt = TicTacToe() + ttt.start_node = TTTNode( + None, + np.array(data['start_state'], dtype=int), + None, + data['start_player'], + 0 + ) + return ttt + + @staticmethod + def get_minimum_problem_size(): + return 0 + + def visualize(self, move_sequence, show_possible=False, tree_name=''): + game = deepcopy(self) + nodes = [] + current = game.get_start_node() + nodes.append(current) + for player, move in move_sequence: + if show_possible: + successors = game.successors(current) + nodes.extend(successors) + current = None + for succ in successors: + if succ.action == move: + current = succ + break + else: + current = game.successor(current, move) + nodes.append(current) + + try: + self.networkx_plot_game_tree(tree_name, nodes) + except ImportError: + print('#' * 30) + print('#' * 30) + print('starting position') + print(self.get_start_node()) + print('#' * 30) + print('#' * 30) + print('-' * 30) + print('sequence of nodes') + for node in nodes: + print('-' * 30) + print(node) + terminal, winner = game.outcome(node) + print('terminal {}, winner {}'.format(terminal, winner)) + + def networkx_plot_game_tree(self, title, nodes, highlight=None): + # TODO: this needs some serious refactoring + # use visitors for styling, for example, instead of cumbersome dicts + import networkx as nx + import matplotlib.pyplot as plt + from networkx.drawing.nx_pydot import graphviz_layout + from matplotlib.offsetbox import OffsetImage, AnnotationBbox, HPacker, VPacker, TextArea + + fig, tree_ax = plt.subplots() + tree_ax.set_title(title) + G = nx.DiGraph(ordering='out') + nodes_extra = dict() + edges_extra = dict() + + def sort_key(node): + if node.action is None: + return (-1, -1) + return node.action + + for node in sorted(nodes, key=sort_key): + G.add_node(id(node), search_node=node) + terminal, winner = self.outcome(node) + nodes_extra[id(node)] = dict( + board=node.state, + player=node.player, + depth=node.depth, + terminal=terminal, + winner=winner + ) + + for node in nodes: + if node.parent is not None: + edge = id(node.parent), id(node) + G.add_edge(*edge, parent_node=node.parent) + edges_extra[edge] = dict( + label='{}'.format(node.action), + parent_player=node.parent.player + ) + + node_size = 1000 + positions = graphviz_layout(G, prog='dot') + + from matplotlib.colors import Normalize, LinearSegmentedColormap + + blue_orange = LinearSegmentedColormap.from_list( + 'blue_orange', + ['tab:blue', 'lightgray', 'tab:orange'] + ) + + inf = float('Inf') + x_range = [inf, -inf] + y_range = [inf, -inf] + for id_node, pos in positions.items(): + x, y = pos + x_range = [min(x, x_range[0]), max(x, x_range[1])] + y_range = [min(y, y_range[0]), max(y, y_range[1])] + + player = nodes_extra[id_node]['player'] + text_player = 'p:{}'.format(player) + text_depth = 'd:{}'.format(nodes_extra[id_node]['depth']) + color_player = 'tab:blue' if player == -1 else 'tab:orange' + + frameon = False + bboxprops = None + if nodes_extra[id_node]['terminal']: + winner = nodes_extra[id_node]['winner'] + frameon = True + if winner is None: + edgecolor = 'tab:purple' + else: + edgecolor = 'tab:blue' if winner == -1 else 'tab:orange' + bboxprops = dict( + facecolor='none', + edgecolor=edgecolor + ) + color_player = 'k' + text_player = 'w:{}'.format(winner) + if winner is None: + text_player = '' + + # needs to be transposed b/c image coordinates etc ... + board = nodes_extra[id_node]['board'].T + textbox_player = TextArea(text_player, textprops=dict(size=6, color=color_player)) + textbox_depth = TextArea(text_depth, textprops=dict(size=6)) + + textbox_children = [textbox_player, textbox_depth] + + if highlight is not None: + if id_node in highlight: + if nodes_extra[id_node]['terminal']: + frameon = True + if nodes_extra[id_node]['winner'] is None: + edgecolor = 'tab:purple' + else: + edgecolor = 'tab:blue' if winner == -1 else 'tab:orange' + + bboxprops = dict( + facecolor='none', + edgecolor=edgecolor + ) + + if len(highlight[id_node]) > 0: + for key, value in highlight[id_node].items(): + textbox_children.append( + TextArea('{}:{}'.format(key, value), textprops=dict(size=6)) + ) + + imagebox = OffsetImage(board, zoom=5, cmap=blue_orange, norm=Normalize(vmin=-1, vmax=1)) + packed = HPacker( + align='center', + children=[ + imagebox, + VPacker( + align='center', + children=textbox_children, + sep=0.1, pad=0.1 + ) + ], + sep=0.1, pad=0.1 + ) + + ab = AnnotationBbox(packed, pos, xycoords='data', frameon=frameon, bboxprops=bboxprops) + tree_ax.add_artist(ab) + + def min_dist(a, b): + if a == b: + return [a - 1, b + 1] + else: + return [a - 0.9 * abs(a), b + 0.1 * abs(b)] + + x_range = min_dist(*x_range) + y_range = min_dist(*y_range) + tree_ax.set_xlim(x_range) + tree_ax.set_ylim(y_range) + + orange_edges = [] + blue_edges = [] + + for edge, extra in edges_extra.items(): + if extra['parent_player'] == -1: + blue_edges.append(edge) + else: + orange_edges.append(edge) + + for color, edgelist in [('tab:orange', orange_edges), ('tab:blue', blue_edges)]: + nx.draw_networkx_edges( + G, positions, + edgelist=edgelist, + edge_color=color, + arrowstyle='-|>', + arrowsize=10, + node_size=node_size, + ax=tree_ax + ) + edge_labels = {edge_id: edge['label'] for edge_id, edge in edges_extra.items()} + nx.draw_networkx_edge_labels(G, positions, edge_labels, ax=tree_ax, font_size=6) + + tree_ax.axis('off') + plt.tight_layout() + plt.show() \ No newline at end of file diff --git a/pig_lite/instance_generation/__init__.py b/pig_lite/instance_generation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pig_lite/instance_generation/enc.py b/pig_lite/instance_generation/enc.py new file mode 100644 index 0000000..5b9c7f0 --- /dev/null +++ b/pig_lite/instance_generation/enc.py @@ -0,0 +1,5 @@ +# this is the common encoding for different level tiles +WALL = 1 +SPACE = 0 +EXPOSED = -1 +UNDETERMINED = -2 diff --git a/pig_lite/instance_generation/problem_factory.py b/pig_lite/instance_generation/problem_factory.py new file mode 100644 index 0000000..6bed8e2 --- /dev/null +++ b/pig_lite/instance_generation/problem_factory.py @@ -0,0 +1,96 @@ +import json + +from pig_lite.problem.simple_2d import Simple2DProblem, MazeLevel, TerrainLevel, RoomLevel +from pig_lite.environment.gridworld import Gridworld +from pig_lite.game.tictactoe import TicTacToe +from pig_lite.decision_tree.training_set import TrainingSet + +# this is the common encoding for different level tiles +encoding = { + 'WALL': 1, + 'SPACE': 0, + 'EXPOSED': -1, + 'UNDETERMINED': -2 +} + +class ProblemFactory(): + def __init__(self) -> None: + pass + + @staticmethod + def generate_problem(problem_type, problem_size, rng): + if problem_type == 'maze': + level = MazeLevel(rng, size=problem_size) + return Simple2DProblem(level.get_field(), + level.get_costs(), + level.get_start(), + level.get_end()) + elif problem_type == 'terrain': + level = TerrainLevel(rng, size=problem_size) + return Simple2DProblem(level.get_field(), + level.get_costs(), + level.get_start(), + level.get_end()) + elif problem_type == 'rooms': + level = RoomLevel(rng, size=problem_size) + return Simple2DProblem(level.get_field(), + level.get_costs(), + level.get_start(), + level.get_end()) + elif problem_type == 'tictactoe': + return TicTacToe(rng, depth=problem_size) + elif problem_type == 'gridworld': + return Gridworld.get_random_instance(rng, size=problem_size) + elif problem_type =='trainset': + raise NotImplementedError(f'problem_type {problem_type} is not implemented yet') + else: + raise ValueError(f'unknown problem_type {problem_type}') + + + @staticmethod + def create_problem_from_json(json_path): + with open(json_path, 'r') as file: + data = json.load(file) + problem_type = data['type'] + + if problem_type == 'Simple2DProblem': + problem = Simple2DProblem.from_dict(data) + return problem + elif problem_type == 'TicTacToe': + problem = TicTacToe.from_dict(data) + return problem + elif problem_type == 'Gridworld': + problem = Gridworld.from_dict(data) + return problem + elif problem_type == 'TrainingSet': + problem = TrainingSet.from_dict(data) + return problem + else: + raise ValueError(f"Unknown problem type: {problem_type}") + + + @staticmethod + def create_problem_from_dict(data, problem_type='Simple2DProblem'): + import numpy as np + if problem_type == 'Simple2DProblem': + if not ('board' in data.keys() and 'costs' in data.keys() + and 'start_state' in data.keys() and 'end_state' in data.keys()): + raise ValueError('data dict must contain: "board", "costs", "start_state" and "end_state"') + if np.array(data['board']).shape != np.array(data['costs']).shape: + raise ValueError('data["board"] and data["costs"] must have same shape') + problem = Simple2DProblem.from_dict(data) + return problem + if problem_type == 'TicTacToe': + if not ('start_state' in data.keys() and 'start_player' in data.keys()): + raise ValueError('data dict must contain: "start_state", "start_player"') + problem = TicTacToe.from_dict(data) + return problem + if problem_type == 'Gridworld': + if not ('seed' in data.keys() and 'dones' in data.keys() + and 'rewards' in data.keys() and 'starts' in data.keys()): + raise ValueError('data dict must contain: "seed", "dones", "rewards", "starts"') + problem = Gridworld.from_dict(data) + return problem + else: + raise NotImplementedError(f'problem_type {problem_type} is not implemented yet') + diff --git a/pig_lite/problem/.ipynb_checkpoints/simple_2d-checkpoint.py b/pig_lite/problem/.ipynb_checkpoints/simple_2d-checkpoint.py new file mode 100644 index 0000000..101467b --- /dev/null +++ b/pig_lite/problem/.ipynb_checkpoints/simple_2d-checkpoint.py @@ -0,0 +1,529 @@ +from pig_lite.problem.base import Problem, Node +from pig_lite.instance_generation import enc +import json +import numpy as np +from collections import OrderedDict +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable +from matplotlib.colors import TABLEAU_COLORS, XKCD_COLORS + +class BaseLevel(): + def __init__(self, rng, size) -> None: + self.rng = rng + self.size = size + self.field = None + self.costs = None + self.start = None + self.end = None + + self.initialize_level() + + def initialize_level(self): + raise NotImplementedError() + + def get_field(self): + return self.field + + def get_costs(self): + return self.costs + + def get_start(self): + return self.start + + def get_end(self): + return self.end + + +class MazeLevel(BaseLevel): + # this method generates a random maze according to prim's randomized + # algorithm + # http://en.wikipedia.org/wiki/Maze_generation_algorithm#Randomized_Prim.27s_algorithm + + def __init__(self, rng, size): + super().__init__(rng, size) + + + def initialize_level(self): + + self.field = np.full((self.size, self.size), enc.WALL, dtype=np.int8) + self.costs = self.rng.randint(1, 5, self.field.shape, dtype=np.int8) + + self.start = (0, 0) + + self.deltas = [ + (0, 1), + (0, -1), + (1, 0), + (-1, 0) + ] + self.random_walk() + end = np.where(self.field == enc.SPACE) + self.end = (int(end[0][-1]), int(end[1][-1])) + + self.replace_walls_with_high_cost_tiles() + + def replace_walls_with_high_cost_tiles(self): + # select only coordinates of walls + walls = np.where(self.field == enc.WALL) + + n_walls = len(walls[0]) + + # replace about a tenth of the walls... + to_replace = self.rng.randint(0, n_walls, n_walls // 9) + + # ... with space, but very *costly* space (it's trap!) + for ri in to_replace: + x, y = walls[0][ri], walls[1][ri] + self.field[x, y] = enc.SPACE + self.costs[x, y] = 9 + + def random_walk(self): + frontier = list() + + sx, sy = self.start + self.field[sx, sy] = enc.SPACE + frontier.extend(self.get_walls(self.start)) + + while len(frontier) > 0: + current, opposing = frontier[self.rng.randint(len(frontier))] + + cx, cy = current + ox, oy = opposing + if self.field[ox, oy] == enc.WALL: + self.field[cx, cy] = enc.SPACE + self.field[ox, oy] = enc.SPACE + frontier.extend(self.get_walls(opposing)) + else: + frontier.remove((current, opposing)) + + def in_bounds(self, position): + x, y = position + return x >= 0 and y >= 0 and x < self.size and y < self.size + + def get_walls(self, position): + walls = [] + px, py = position + for dx, dy in self.deltas: + cx = px + dx + cy = py + dy + current = (cx, cy) + + ox = px + 2 * dx + oy = py + 2 * dy + opposing = (ox, oy) + + if (self.in_bounds(current) and self.field[cx, cy] == enc.WALL and self.in_bounds(opposing)): + walls.append((current, opposing)) + return walls + + +# this is code taken from +# https://github.com/dandrino/terrain-erosion-3-ways/blob/master/util.py +# Copyright (c) 2018 Daniel Andrino +# (project is MIT licensed) +def fbm(shape, p, lower=-np.inf, upper=np.inf): + freqs = tuple(np.fft.fftfreq(n, d=1.0 / n) for n in shape) + freq_radial = np.hypot(*np.meshgrid(*freqs)) + envelope = (np.power(freq_radial, p, where=freq_radial != 0) * + (freq_radial > lower) * (freq_radial < upper)) + envelope[0][0] = 0.0 + phase_noise = np.exp(2j * np.pi * np.random.rand(*shape)) + return np.real(np.fft.ifft2(np.fft.fft2(phase_noise) * envelope)) + + +class TerrainLevel(BaseLevel): + def __init__(self, rng, size): + super().__init__(rng, size) + + def initialize_level(self): + + self.field = np.full((self.size, self.size), enc.SPACE, dtype=np.int8) + + self.costs = fbm(self.field.shape, -2) + self.costs -= self.costs.min() + self.costs /= self.costs.max() + self.costs *= 9 + self.costs += 1 + self.costs = self.costs.astype(int) + + self.start = (0, 0) + self.end = (self.size - 1, self.size - 1) + + x = 0 + y = self.size - 1 + for i in range(0, self.size): + self.field[x, y] = enc.WALL + x += 1 + y -= 1 + + self.replace_one_or_more_walls() + + def replace_one_or_more_walls(self): + # select only coordinates of walls + walls = np.where(self.field == enc.WALL) + n_walls = len(walls[0]) + n_replace = self.rng.randint(1, max(2, n_walls // 5)) + to_replace = self.rng.randint(0, n_walls, n_replace) + + for ri in to_replace: + x, y = walls[0][ri], walls[1][ri] + self.field[x, y] = enc.SPACE + + +class RoomLevel(BaseLevel): + def __init__(self, rng, size): + super().__init__(rng, size) + + def initialize_level(self): + self.field = np.full((self.size, self.size), enc.SPACE, dtype=np.int8) + self.costs = np.ones_like(self.field, dtype=np.float32) + + k = 1 + self.subdivide(self.field.view(), self.costs.view(), k, 0, 0) + + # such a *crutch*! + # this 'repairs' dead ends. horrible stuff. + for x in range(1, self.size - 1): + for y in range(1, self.size - 1): + s = 0 + s += self.field[x - 1, y] + s += self.field[x + 1, y] + s += self.field[x, y - 1] + s += self.field[x, y + 1] + if self.field[x, y] == enc.SPACE and s >= 3: + self.field[x - 1, y] = enc.SPACE + self.field[x + 1, y] = enc.SPACE + self.field[x, y - 1] = enc.SPACE + self.field[x, y + 1] = enc.SPACE + + spaces = np.where(self.field == enc.SPACE) + n_spaces = len(spaces[0]) + + n_danger = self.rng.randint(3, 7) + dangers = self.rng.choice(range(n_spaces), n_danger, replace=False) + for di in dangers: + rx, ry = np.unravel_index(di, (self.size, self.size)) + const = max(1., self.rng.randint(self.size // 5, self.size // 2)) + for x in range(self.size): + for y in range(self.size): + distance = np.sqrt((rx - x) ** 2 + (ry - y) ** 2) + self.costs[x, y] = self.costs[x, y] + (1. / (const + distance)) + + self.costs = self.costs - self.costs.min() + self.costs = self.costs / self.costs.max() + self.costs = self.costs * 9 + self.costs = self.costs + 1 + self.costs = self.costs.astype(int) + + start_choice = 0 + end_choice = -1 + + self.start = (int(spaces[0][start_choice]), int(spaces[1][start_choice])) + self.end = (int(spaces[0][end_choice]), int(spaces[1][end_choice])) + + if self.start == self.end: + raise RuntimeError('should never happen') + + def subdivide(self, current, costs, k, d, previous_door): + w, h = current.shape + random_stop = self.rng.randint(0, 10) == 0 and d > 2 + if w <= 2 * k + 1 or h <= 2 * k + 1 or random_stop: + return + + split = previous_door + while split == previous_door: + split = self.rng.randint(k, w - k) + current[split, :] = enc.WALL + door = self.rng.randint(k, h - k) + current[split, door] = enc.SPACE + + self.subdivide( + current[:split, :].T, + costs[:split, :].T, + k, + d + 1, + door + ) + self.subdivide( + current[split + 1:, :].T, + costs[split + 1:, :].T, + k, + d + 1, + door + ) + + +class Simple2DProblem(Problem): + """ + the states are the positions on the board that the agent can walk on + """ + + ACTIONS_DELTA = OrderedDict([ + ('R', (+1, 0)), + ('U', (0, -1)), + ('D', (0, +1)), + ('L', (-1, 0)), + ]) + + def __init__(self, board, costs, start, end): + self.board = board + self.costs = costs + self.start_state = start + self.end_state = end + self.n_expands = 0 + + def get_start_node(self): + return Node(None, self.start_state, None, 0, 0) + + def get_end_node(self): + return Node(None, self.end_state, None, 0, 0) + + def is_end(self, node): + return node.state == self.end_state + + def action_cost(self, state, action): + # for the MazeProblem, the cost of any action + # is stored at the coordinates of the successor state, + # and represents the cost of 'stepping onto' this + # position on the board + sx, sy = self.__delta_state(state, action) + return self.costs[sx, sy] + + def successor(self, node, action): + # determine the next state + successor_state = self.__delta_state(node.state, action) + if successor_state is None: + return None + + # determine what it would cost to take this action in this state + cost = self.action_cost(node.state, action) + + # add the next state to the list of successor nodes + return Node( + node, + successor_state, + action, + node.cost + cost, + node.depth + 1 + ) + + def get_number_of_expanded_nodes(self): + return self.n_expands + + def reset(self): + self.n_expands = 0 + + def successors(self, node): + self.n_expands += 1 + successor_nodes = [] + for action in self.ACTIONS_DELTA.keys(): + succ = self.successor(node, action) + if succ is not None and succ != node: + successor_nodes.append(succ) + return successor_nodes + + def to_json(self): + return json.dumps(dict( + type=self.__class__.__name__, + board=self.board.tolist(), + costs=self.costs.tolist(), + start_state=self.start_state, + end_state=self.end_state + )) + + @staticmethod + def draw_nodes(fig, ax, name, node_collection, color, marker): + states = np.array([node.state for node in node_collection]) + if len(states) > 0: + ax.scatter(states[:, 0], states[:, 1], color=color, label=name, marker=marker) + + @staticmethod + def plot_nodes(fig, ax, nodes): + if len(nodes) > 0: + if len(nodes[0]) == 3: + for (name, marker, node_collection), color in zip(nodes, TABLEAU_COLORS): + if len(node_collection) > 0: + Simple2DProblem.draw_nodes(fig, ax, name, node_collection, color, marker) + else: + for name, marker, node_collection, color in nodes: + if len(node_collection) > 0: + Simple2DProblem.draw_nodes(fig, ax, name, node_collection, color, marker) + + ax.legend( + bbox_to_anchor=(0.5, -0.03), + loc='upper center', + ) + + def plot_sequences(self, fig, ax, sequences): + start_node = self.get_start_node() + for (name, action_sequence), color in zip(sequences, XKCD_COLORS): + self.draw_path(fig, ax, name, start_node, action_sequence, color) + + ax.legend( + bbox_to_anchor=(0.5, -0.03), + loc='upper center', + ) + + + def draw_path(self, fig, ax, name, start_node, action_sequence, color): + current = start_node + xs = [current.state[0]] + ys = [current.state[1]] + us = [0] + vs = [0] + + length = len(action_sequence) + cost = 0 + costs = [0] * length + for i, action in enumerate(action_sequence): + costs[i] = current.cost + xs.append(current.state[0]) + ys.append(current.state[1]) + current = self.successor(current, action) + dx, dy = self.ACTIONS_DELTA[action] + us.append(dx) + vs.append(-dy) + cost = current.cost + + quiv = ax.quiver( + xs, ys, us, vs, + color=color, + label='{} l:{} c:{}'.format(name, length, cost), + scale_units='xy', + units='xy', + scale=1, + headwidth=1, + headlength=1, + linewidth=1, + picker=5 + ) + return quiv + + def plot_field_and_costs_aux(self, fig, show_coordinates, show_grid, + field_ax=None, costs_ax=None): + + if field_ax is None: + ax = field_ax = plt.subplot(121) + else: + ax = field_ax + + ax.set_title('The field') + im = ax.imshow(self.board.T, cmap='gray_r') + + divider = make_axes_locatable(ax) + cax = divider.append_axes('right', size='5%', pad=0) + cbar = fig.colorbar(im, cax=cax, orientation='vertical') + cbar.set_ticks([0, 1]) + cbar.set_ticklabels([0, 1]) + + if costs_ax is None: + ax = costs_ax = plt.subplot(122, sharex=ax, sharey=ax) + else: + ax = costs_ax + + ax.set_title('The costs (for stepping on a tile)') + im = ax.imshow(self.costs.T, cmap='viridis') + divider = make_axes_locatable(ax) + cax = divider.append_axes('right', size='5%', pad=0) + cbar = fig.colorbar(im, cax=cax, orientation='vertical') + ticks = np.arange(self.costs.min(), self.costs.max() + 1) + cbar.set_ticks(ticks) + cbar.set_ticklabels(ticks) + + for ax in [field_ax, costs_ax]: + ax.tick_params( + top=show_coordinates, + left=show_coordinates, + labelleft=show_coordinates, + labeltop=show_coordinates, + right=False, + bottom=False, + labelbottom=False + ) + + # Major ticks + s = self.board.shape[0] + ax.set_xticks(np.arange(0, s, 1)) + ax.set_yticks(np.arange(0, s, 1)) + + # Minor ticks + ax.set_xticks(np.arange(-.5, s, 1), minor=True) + ax.set_yticks(np.arange(-.5, s, 1), minor=True) + + if show_grid: + for color, ax in zip(['m', 'w'], [field_ax, costs_ax]): + # Gridlines based on minor ticks + ax.grid(which='minor', color=color, linestyle='-', linewidth=1) + + return field_ax, costs_ax + + def visualize(self, sequences=None, show_coordinates=False, show_grid=False, plot_filename=None): + + nodes = [ + ('start', 'o', [self.get_start_node()]), + ('end', 'o', [self.get_end_node()]) + ] + + fig = plt.figure(figsize=(10, 7)) + field_ax, costs_ax = self.plot_field_and_costs_aux(fig, show_coordinates, show_grid) + if sequences is not None and len(sequences) > 0: + self.plot_sequences(fig, field_ax, sequences) + self.plot_sequences(fig, costs_ax, sequences) + + if nodes is not None and len(nodes) > 0: + Simple2DProblem.plot_nodes(fig, field_ax, nodes) + + plt.tight_layout() + if plot_filename is not None: + plt.savefig(plot_filename) + plt.close(fig) + else: + plt.show() + + + @staticmethod + def from_json(jsonstring): + data = json.loads(jsonstring) + return Simple2DProblem( + np.array(data['board']), + np.array(data['costs']), + tuple(data['start_state']), + tuple(data['end_state']) + ) + + @staticmethod + def from_dict(data): + return Simple2DProblem( + np.array(data['board']), + np.array(data['costs']), + tuple(data['start_state']), + tuple(data['end_state']) + ) + + def __delta_state(self, state, action): + # the old state's coordinates + x, y = state + + # the deltas for each coordinates + dx, dy = self.ACTIONS_DELTA[action] + + # compute the coordinates of the next state + sx = x + dx + sy = y + dy + + if self.__on_board(sx, sy) and self.__walkable(sx, sy): + # (sx, sy) is a *valid* state if it is on the board + # and there is no wall where we want to go + return sx, sy + else: + # EIEIEIEIEI. up until assignment 1, this returned None :/ + # this had no consequences on the correctness of the algorithms, + # but the explanations, and the self-edges were wrong + return x, y + + def __on_board(self, x, y): + size = len(self.board) # all boards are quadratic + return x >= 0 and x < size and y >= 0 and y < size + + def __walkable(self, x, y): + return self.board[x, y] != enc.WALL diff --git a/pig_lite/problem/base.py b/pig_lite/problem/base.py new file mode 100644 index 0000000..72bde76 --- /dev/null +++ b/pig_lite/problem/base.py @@ -0,0 +1,92 @@ +import hashlib + +class Node(object): + def __init__(self, parent, state, action, cost, depth): + self.parent = parent + self.state = state + self.action = action + self.cost = cost + self.depth = depth + + def key(self): + # if state is composed of other stuff (dict, set, ...) + # make it a tuple containing hashable datatypes + # (this is supposed to be overridden by subclasses) + return self.state + + def __hash__(self): + return hash(self.key()) + + def __eq__(self, other): + if type(self) == type(other): + return self.key() == other.key() + raise ValueError('cannot simply compare two different node types') + + def __str__(self): + return self.__repr__() + + def __repr__(self): + return 'Node(id:{}, parent:{}, state:{}, action:{}, cost:{}, depth:{})'.format( + id(self), + id(self.parent), + self.state, + self.action, + self.cost, + self.depth + ) + + def get_action_sequence(self): + current = self + reverse_sequence = [] + while current.parent is not None: + reverse_sequence.append(current.action) + current = current.parent + return list(reversed(reverse_sequence)) + + def get_action_sequence_hash(self): + action_sequence = self.get_action_sequence() + action_sequence_as_str = ','.join(map(str, action_sequence)) + action_sequence_hash = hashlib.sha256(action_sequence_as_str.encode('UTF-8')).hexdigest() # should solution node return hashcode? + return action_sequence_hash + + def pretty_print(self): + print(f"state {self.state} was reached following the sequence {self.get_action_sequence()} (cost: {self.cost}, depth: {self.depth})") + + +class Problem(object): + def get_number_of_expanded_nodes(self): + raise NotImplementedError() + + def get_start_node(self): + raise NotImplementedError() + + def get_end_node(self): + raise NotImplementedError() + + def is_end(self, node): + raise NotImplementedError() + + def action_cost(self, state, action): + raise NotImplementedError() + + def successors(self, node): + raise NotImplementedError() + + def to_json(self): + raise NotImplementedError() + + def visualize(self, **kwargs): + raise NotImplementedError() + + def get_action_sequence(self, end: Node): + if end is None: + return list() + return end.get_action_sequence() + + @staticmethod + def from_json(jsonstring): + raise NotImplementedError() + + @staticmethod + def get_minimum_problem_size(): + raise NotImplementedError() diff --git a/pig_lite/problem/simple_2d.py b/pig_lite/problem/simple_2d.py new file mode 100644 index 0000000..101467b --- /dev/null +++ b/pig_lite/problem/simple_2d.py @@ -0,0 +1,529 @@ +from pig_lite.problem.base import Problem, Node +from pig_lite.instance_generation import enc +import json +import numpy as np +from collections import OrderedDict +import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import make_axes_locatable +from matplotlib.colors import TABLEAU_COLORS, XKCD_COLORS + +class BaseLevel(): + def __init__(self, rng, size) -> None: + self.rng = rng + self.size = size + self.field = None + self.costs = None + self.start = None + self.end = None + + self.initialize_level() + + def initialize_level(self): + raise NotImplementedError() + + def get_field(self): + return self.field + + def get_costs(self): + return self.costs + + def get_start(self): + return self.start + + def get_end(self): + return self.end + + +class MazeLevel(BaseLevel): + # this method generates a random maze according to prim's randomized + # algorithm + # http://en.wikipedia.org/wiki/Maze_generation_algorithm#Randomized_Prim.27s_algorithm + + def __init__(self, rng, size): + super().__init__(rng, size) + + + def initialize_level(self): + + self.field = np.full((self.size, self.size), enc.WALL, dtype=np.int8) + self.costs = self.rng.randint(1, 5, self.field.shape, dtype=np.int8) + + self.start = (0, 0) + + self.deltas = [ + (0, 1), + (0, -1), + (1, 0), + (-1, 0) + ] + self.random_walk() + end = np.where(self.field == enc.SPACE) + self.end = (int(end[0][-1]), int(end[1][-1])) + + self.replace_walls_with_high_cost_tiles() + + def replace_walls_with_high_cost_tiles(self): + # select only coordinates of walls + walls = np.where(self.field == enc.WALL) + + n_walls = len(walls[0]) + + # replace about a tenth of the walls... + to_replace = self.rng.randint(0, n_walls, n_walls // 9) + + # ... with space, but very *costly* space (it's trap!) + for ri in to_replace: + x, y = walls[0][ri], walls[1][ri] + self.field[x, y] = enc.SPACE + self.costs[x, y] = 9 + + def random_walk(self): + frontier = list() + + sx, sy = self.start + self.field[sx, sy] = enc.SPACE + frontier.extend(self.get_walls(self.start)) + + while len(frontier) > 0: + current, opposing = frontier[self.rng.randint(len(frontier))] + + cx, cy = current + ox, oy = opposing + if self.field[ox, oy] == enc.WALL: + self.field[cx, cy] = enc.SPACE + self.field[ox, oy] = enc.SPACE + frontier.extend(self.get_walls(opposing)) + else: + frontier.remove((current, opposing)) + + def in_bounds(self, position): + x, y = position + return x >= 0 and y >= 0 and x < self.size and y < self.size + + def get_walls(self, position): + walls = [] + px, py = position + for dx, dy in self.deltas: + cx = px + dx + cy = py + dy + current = (cx, cy) + + ox = px + 2 * dx + oy = py + 2 * dy + opposing = (ox, oy) + + if (self.in_bounds(current) and self.field[cx, cy] == enc.WALL and self.in_bounds(opposing)): + walls.append((current, opposing)) + return walls + + +# this is code taken from +# https://github.com/dandrino/terrain-erosion-3-ways/blob/master/util.py +# Copyright (c) 2018 Daniel Andrino +# (project is MIT licensed) +def fbm(shape, p, lower=-np.inf, upper=np.inf): + freqs = tuple(np.fft.fftfreq(n, d=1.0 / n) for n in shape) + freq_radial = np.hypot(*np.meshgrid(*freqs)) + envelope = (np.power(freq_radial, p, where=freq_radial != 0) * + (freq_radial > lower) * (freq_radial < upper)) + envelope[0][0] = 0.0 + phase_noise = np.exp(2j * np.pi * np.random.rand(*shape)) + return np.real(np.fft.ifft2(np.fft.fft2(phase_noise) * envelope)) + + +class TerrainLevel(BaseLevel): + def __init__(self, rng, size): + super().__init__(rng, size) + + def initialize_level(self): + + self.field = np.full((self.size, self.size), enc.SPACE, dtype=np.int8) + + self.costs = fbm(self.field.shape, -2) + self.costs -= self.costs.min() + self.costs /= self.costs.max() + self.costs *= 9 + self.costs += 1 + self.costs = self.costs.astype(int) + + self.start = (0, 0) + self.end = (self.size - 1, self.size - 1) + + x = 0 + y = self.size - 1 + for i in range(0, self.size): + self.field[x, y] = enc.WALL + x += 1 + y -= 1 + + self.replace_one_or_more_walls() + + def replace_one_or_more_walls(self): + # select only coordinates of walls + walls = np.where(self.field == enc.WALL) + n_walls = len(walls[0]) + n_replace = self.rng.randint(1, max(2, n_walls // 5)) + to_replace = self.rng.randint(0, n_walls, n_replace) + + for ri in to_replace: + x, y = walls[0][ri], walls[1][ri] + self.field[x, y] = enc.SPACE + + +class RoomLevel(BaseLevel): + def __init__(self, rng, size): + super().__init__(rng, size) + + def initialize_level(self): + self.field = np.full((self.size, self.size), enc.SPACE, dtype=np.int8) + self.costs = np.ones_like(self.field, dtype=np.float32) + + k = 1 + self.subdivide(self.field.view(), self.costs.view(), k, 0, 0) + + # such a *crutch*! + # this 'repairs' dead ends. horrible stuff. + for x in range(1, self.size - 1): + for y in range(1, self.size - 1): + s = 0 + s += self.field[x - 1, y] + s += self.field[x + 1, y] + s += self.field[x, y - 1] + s += self.field[x, y + 1] + if self.field[x, y] == enc.SPACE and s >= 3: + self.field[x - 1, y] = enc.SPACE + self.field[x + 1, y] = enc.SPACE + self.field[x, y - 1] = enc.SPACE + self.field[x, y + 1] = enc.SPACE + + spaces = np.where(self.field == enc.SPACE) + n_spaces = len(spaces[0]) + + n_danger = self.rng.randint(3, 7) + dangers = self.rng.choice(range(n_spaces), n_danger, replace=False) + for di in dangers: + rx, ry = np.unravel_index(di, (self.size, self.size)) + const = max(1., self.rng.randint(self.size // 5, self.size // 2)) + for x in range(self.size): + for y in range(self.size): + distance = np.sqrt((rx - x) ** 2 + (ry - y) ** 2) + self.costs[x, y] = self.costs[x, y] + (1. / (const + distance)) + + self.costs = self.costs - self.costs.min() + self.costs = self.costs / self.costs.max() + self.costs = self.costs * 9 + self.costs = self.costs + 1 + self.costs = self.costs.astype(int) + + start_choice = 0 + end_choice = -1 + + self.start = (int(spaces[0][start_choice]), int(spaces[1][start_choice])) + self.end = (int(spaces[0][end_choice]), int(spaces[1][end_choice])) + + if self.start == self.end: + raise RuntimeError('should never happen') + + def subdivide(self, current, costs, k, d, previous_door): + w, h = current.shape + random_stop = self.rng.randint(0, 10) == 0 and d > 2 + if w <= 2 * k + 1 or h <= 2 * k + 1 or random_stop: + return + + split = previous_door + while split == previous_door: + split = self.rng.randint(k, w - k) + current[split, :] = enc.WALL + door = self.rng.randint(k, h - k) + current[split, door] = enc.SPACE + + self.subdivide( + current[:split, :].T, + costs[:split, :].T, + k, + d + 1, + door + ) + self.subdivide( + current[split + 1:, :].T, + costs[split + 1:, :].T, + k, + d + 1, + door + ) + + +class Simple2DProblem(Problem): + """ + the states are the positions on the board that the agent can walk on + """ + + ACTIONS_DELTA = OrderedDict([ + ('R', (+1, 0)), + ('U', (0, -1)), + ('D', (0, +1)), + ('L', (-1, 0)), + ]) + + def __init__(self, board, costs, start, end): + self.board = board + self.costs = costs + self.start_state = start + self.end_state = end + self.n_expands = 0 + + def get_start_node(self): + return Node(None, self.start_state, None, 0, 0) + + def get_end_node(self): + return Node(None, self.end_state, None, 0, 0) + + def is_end(self, node): + return node.state == self.end_state + + def action_cost(self, state, action): + # for the MazeProblem, the cost of any action + # is stored at the coordinates of the successor state, + # and represents the cost of 'stepping onto' this + # position on the board + sx, sy = self.__delta_state(state, action) + return self.costs[sx, sy] + + def successor(self, node, action): + # determine the next state + successor_state = self.__delta_state(node.state, action) + if successor_state is None: + return None + + # determine what it would cost to take this action in this state + cost = self.action_cost(node.state, action) + + # add the next state to the list of successor nodes + return Node( + node, + successor_state, + action, + node.cost + cost, + node.depth + 1 + ) + + def get_number_of_expanded_nodes(self): + return self.n_expands + + def reset(self): + self.n_expands = 0 + + def successors(self, node): + self.n_expands += 1 + successor_nodes = [] + for action in self.ACTIONS_DELTA.keys(): + succ = self.successor(node, action) + if succ is not None and succ != node: + successor_nodes.append(succ) + return successor_nodes + + def to_json(self): + return json.dumps(dict( + type=self.__class__.__name__, + board=self.board.tolist(), + costs=self.costs.tolist(), + start_state=self.start_state, + end_state=self.end_state + )) + + @staticmethod + def draw_nodes(fig, ax, name, node_collection, color, marker): + states = np.array([node.state for node in node_collection]) + if len(states) > 0: + ax.scatter(states[:, 0], states[:, 1], color=color, label=name, marker=marker) + + @staticmethod + def plot_nodes(fig, ax, nodes): + if len(nodes) > 0: + if len(nodes[0]) == 3: + for (name, marker, node_collection), color in zip(nodes, TABLEAU_COLORS): + if len(node_collection) > 0: + Simple2DProblem.draw_nodes(fig, ax, name, node_collection, color, marker) + else: + for name, marker, node_collection, color in nodes: + if len(node_collection) > 0: + Simple2DProblem.draw_nodes(fig, ax, name, node_collection, color, marker) + + ax.legend( + bbox_to_anchor=(0.5, -0.03), + loc='upper center', + ) + + def plot_sequences(self, fig, ax, sequences): + start_node = self.get_start_node() + for (name, action_sequence), color in zip(sequences, XKCD_COLORS): + self.draw_path(fig, ax, name, start_node, action_sequence, color) + + ax.legend( + bbox_to_anchor=(0.5, -0.03), + loc='upper center', + ) + + + def draw_path(self, fig, ax, name, start_node, action_sequence, color): + current = start_node + xs = [current.state[0]] + ys = [current.state[1]] + us = [0] + vs = [0] + + length = len(action_sequence) + cost = 0 + costs = [0] * length + for i, action in enumerate(action_sequence): + costs[i] = current.cost + xs.append(current.state[0]) + ys.append(current.state[1]) + current = self.successor(current, action) + dx, dy = self.ACTIONS_DELTA[action] + us.append(dx) + vs.append(-dy) + cost = current.cost + + quiv = ax.quiver( + xs, ys, us, vs, + color=color, + label='{} l:{} c:{}'.format(name, length, cost), + scale_units='xy', + units='xy', + scale=1, + headwidth=1, + headlength=1, + linewidth=1, + picker=5 + ) + return quiv + + def plot_field_and_costs_aux(self, fig, show_coordinates, show_grid, + field_ax=None, costs_ax=None): + + if field_ax is None: + ax = field_ax = plt.subplot(121) + else: + ax = field_ax + + ax.set_title('The field') + im = ax.imshow(self.board.T, cmap='gray_r') + + divider = make_axes_locatable(ax) + cax = divider.append_axes('right', size='5%', pad=0) + cbar = fig.colorbar(im, cax=cax, orientation='vertical') + cbar.set_ticks([0, 1]) + cbar.set_ticklabels([0, 1]) + + if costs_ax is None: + ax = costs_ax = plt.subplot(122, sharex=ax, sharey=ax) + else: + ax = costs_ax + + ax.set_title('The costs (for stepping on a tile)') + im = ax.imshow(self.costs.T, cmap='viridis') + divider = make_axes_locatable(ax) + cax = divider.append_axes('right', size='5%', pad=0) + cbar = fig.colorbar(im, cax=cax, orientation='vertical') + ticks = np.arange(self.costs.min(), self.costs.max() + 1) + cbar.set_ticks(ticks) + cbar.set_ticklabels(ticks) + + for ax in [field_ax, costs_ax]: + ax.tick_params( + top=show_coordinates, + left=show_coordinates, + labelleft=show_coordinates, + labeltop=show_coordinates, + right=False, + bottom=False, + labelbottom=False + ) + + # Major ticks + s = self.board.shape[0] + ax.set_xticks(np.arange(0, s, 1)) + ax.set_yticks(np.arange(0, s, 1)) + + # Minor ticks + ax.set_xticks(np.arange(-.5, s, 1), minor=True) + ax.set_yticks(np.arange(-.5, s, 1), minor=True) + + if show_grid: + for color, ax in zip(['m', 'w'], [field_ax, costs_ax]): + # Gridlines based on minor ticks + ax.grid(which='minor', color=color, linestyle='-', linewidth=1) + + return field_ax, costs_ax + + def visualize(self, sequences=None, show_coordinates=False, show_grid=False, plot_filename=None): + + nodes = [ + ('start', 'o', [self.get_start_node()]), + ('end', 'o', [self.get_end_node()]) + ] + + fig = plt.figure(figsize=(10, 7)) + field_ax, costs_ax = self.plot_field_and_costs_aux(fig, show_coordinates, show_grid) + if sequences is not None and len(sequences) > 0: + self.plot_sequences(fig, field_ax, sequences) + self.plot_sequences(fig, costs_ax, sequences) + + if nodes is not None and len(nodes) > 0: + Simple2DProblem.plot_nodes(fig, field_ax, nodes) + + plt.tight_layout() + if plot_filename is not None: + plt.savefig(plot_filename) + plt.close(fig) + else: + plt.show() + + + @staticmethod + def from_json(jsonstring): + data = json.loads(jsonstring) + return Simple2DProblem( + np.array(data['board']), + np.array(data['costs']), + tuple(data['start_state']), + tuple(data['end_state']) + ) + + @staticmethod + def from_dict(data): + return Simple2DProblem( + np.array(data['board']), + np.array(data['costs']), + tuple(data['start_state']), + tuple(data['end_state']) + ) + + def __delta_state(self, state, action): + # the old state's coordinates + x, y = state + + # the deltas for each coordinates + dx, dy = self.ACTIONS_DELTA[action] + + # compute the coordinates of the next state + sx = x + dx + sy = y + dy + + if self.__on_board(sx, sy) and self.__walkable(sx, sy): + # (sx, sy) is a *valid* state if it is on the board + # and there is no wall where we want to go + return sx, sy + else: + # EIEIEIEIEI. up until assignment 1, this returned None :/ + # this had no consequences on the correctness of the algorithms, + # but the explanations, and the self-edges were wrong + return x, y + + def __on_board(self, x, y): + size = len(self.board) # all boards are quadratic + return x >= 0 and x < size and y >= 0 and y < size + + def __walkable(self, x, y): + return self.board[x, y] != enc.WALL diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..af8ecd4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,15 @@ +{ + pkgs ? import { }, +}: + +pkgs.mkShell { + buildInputs = with pkgs; [ + python3 + python3Packages.notebook + python3Packages.numpy + python3Packages.matplotlib + graphviz + python3Packages.networkx + python3Packages.pydot + ]; +}