{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Search for AIMA 4th edition\n",
    "\n",
    "Implementation of search algorithms and search problems for AIMA.\n",
    "\n",
    "We start by defining the abstract class for a `Problem`; problem domains will subclass this, and then you can create individual problems with specific initial states and goals. We also ddefine a `Node` in a search tree, and some functions on nodes: `expand` to generate successors, and `path_actions`, `path_states` and `path` to recover aspects of the path from the node.  Finally, a `PriorityQueue`, which allows you to keep a collection of items, and continually remove from it the item with minimum `f(item)` score."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import heapq\n",
    "import math\n",
    "import sys\n",
    "from collections import defaultdict, deque, Counter\n",
    "\n",
    "class Problem(object):\n",
    "    \"\"\"The abstract class for a formal problem. You should subclass this,\n",
    "    overriding `actions` and `results`, and other methods if necessary.\n",
    "    Note: a problem can specify a default heuristic if desired. By default, \n",
    "    the heuristic is 0 for all states, and the step cost is 1 for all actions.\"\"\"\n",
    "\n",
    "    def __init__(self, initial=None, goal=None, **other_keywords):\n",
    "        \"\"\"Specify the initial and goal states.\n",
    "        Subclasses can use other keywords if they want.\"\"\"\n",
    "        self.__dict__.update(initial=initial, goal=goal, **other_keywords) \n",
    "\n",
    "    def actions(self, state):           raise NotImplementedError\n",
    "    def result(self, state, action):    raise NotImplementedError\n",
    "    def is_goal(self, state):           return state == self.goal\n",
    "    def step_cost(self, s, action, s1): return 1\n",
    "    def h(self, node):                  return 0\n",
    "    \n",
    "\n",
    "class Node:\n",
    "    \"A Node in a search tree.\"\n",
    "    def __init__(self, state, parent=None, action=None, path_cost=0):\n",
    "        self.__dict__.update(state=state, parent=parent, action=action, path_cost=path_cost)\n",
    "\n",
    "    def __repr__(self): return '<{}>'.format(self.state)\n",
    "    def __len__(self): return 0 if self.parent is None else (1 + len(self.parent))\n",
    "    def __lt__(self, other): return self.state < other.state\n",
    "    \n",
    "    \n",
    "def expand(problem, node):\n",
    "    \"Expand a node, generating the children nodes.\"\n",
    "    s = node.state\n",
    "    for action in problem.actions(s):\n",
    "        s1 = problem.result(s, action)\n",
    "        cost = node.path_cost + problem.step_cost(s, action, s1)\n",
    "        yield Node(s1, node, action, cost)\n",
    "        \n",
    "\n",
    "def path_actions(node):\n",
    "    \"The sequence of actions to get to this node.\"\n",
    "    return [] if node.parent is None else path_actions(node.parent) + [node.action]\n",
    "\n",
    "\n",
    "def path_states(node):\n",
    "    \"The sequence of states to get to this node.\"\n",
    "    return ([] if node.parent is None else path_states(node.parent) ) + [node.state]\n",
    "\n",
    "\n",
    "def path(node):\n",
    "    \"Alternating states and actions to get to this node.\"\n",
    "    return ([] if node.parent is None else path(node.parent) + [node.action] ) + [node.state]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Queues"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "FIFOQueue = list\n",
    "\n",
    "LIFOQueue = deque\n",
    "\n",
    "class PriorityQueue:\n",
    "    \"\"\"A queue in which the item with minimum f(item) is always popped first.\"\"\"\n",
    "\n",
    "    def __init__(self, items=(), key=lambda x: x): \n",
    "        self.key = key\n",
    "        self.items = []\n",
    "        for item in items:\n",
    "            self.add(item)\n",
    "         \n",
    "    def add(self, item):\n",
    "        \"\"\"Add item to the queuez.\"\"\"\n",
    "        heapq.heappush(self.items, (self.key(item), item))\n",
    "\n",
    "    def pop(self):\n",
    "        \"\"\"Pop and return the item with min f(item) value.\"\"\"\n",
    "        return heapq.heappop(self.items)[1]\n",
    "\n",
    "    def __len__(self): return len(self.items)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Search Algorithms\n",
    "\n",
    "Here are the six major state-space search algorithms covered in the book:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "def breadth_first_search(problem):\n",
    "    \"Search shallowest nodes in the search tree first.\"\n",
    "    frontier = LIFOQueue([Node(problem.initial)])\n",
    "    reached = set()\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if problem.is_goal(node.state):\n",
    "            return node\n",
    "        for child in expand(problem, node):\n",
    "            s = child.state\n",
    "            if s not in reached:\n",
    "                reached.add(s)\n",
    "                frontier.appendleft(child)\n",
    "    return failure\n",
    "\n",
    "def depth_limited_search(problem, limit=5):\n",
    "    \"Search deepest nodes in the search tree first.\"\n",
    "    frontier = FIFOQueue([Node(problem.initial)])\n",
    "    solution = failure\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if len(node) > limit:\n",
    "            solution = cutoff\n",
    "        else:\n",
    "            for child in expand(problem, node):\n",
    "                if problem.is_goal(child.state):\n",
    "                    return child\n",
    "                frontier.append(child)\n",
    "    return solution\n",
    "\n",
    "def iterative_deepening_search(problem):\n",
    "    \"Do depth-limited search with increasing depth limits.\"\n",
    "    for limit in range(1, sys.maxsize):\n",
    "        result = depth_limited_search(problem, limit)\n",
    "        if result != cutoff:\n",
    "            return result\n",
    "\n",
    "\n",
    "def best_first_search(problem, f):\n",
    "    \"Search nodes with minimum f(node) value first.\"\n",
    "    frontier = PriorityQueue([Node(problem.initial)], key=f)\n",
    "    reached = {}\n",
    "    while frontier:\n",
    "        node = frontier.pop()\n",
    "        if problem.is_goal(node.state):\n",
    "            return node\n",
    "        for child in expand(problem, node):\n",
    "            s = child.state\n",
    "            if s not in reached or child.path_cost < reached[s].path_cost:\n",
    "                reached[s] = child\n",
    "                frontier.add(child)\n",
    "    return failure\n",
    "\n",
    "\n",
    "def uniform_cost_search(problem):\n",
    "    \"Search niodes with minimum  path cost first.\"\n",
    "    return best_first_search(problem, lambda node: node.path_cost)\n",
    "\n",
    "\n",
    "def astar_search(problem, h=None):\n",
    "    \"\"\"Search niodes with minimum f(n) = g(n) + h(n).\"\"\"\n",
    "    h = h or problem.h\n",
    "    return best_first_search(problem, lambda node: node.path_cost + h(node))\n",
    "\n",
    "failure = Node('failure', path_cost=math.inf) # Indicates an algorithm couldn't find a solution.\n",
    "cutoff  = Node('cutoff', path_cost=math.inf)  # Indicates iterative deeepening search was cut off."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Problem Domains\n",
    "\n",
    "Now we turn our attention to defining some problem domains.\n",
    "\n",
    "# Water Pouring Problems"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "class PourProblem(Problem):\n",
    "    \"\"\"Problem about pouring water between jugs to achieve some water level.\n",
    "    Each state is a tuples of water levels. In the initialization, provide a tuple of \n",
    "    sizes, e.g. PourProblem((2, 4, 3), 7, sizes=(8, 16, 32)), \n",
    "    which means three jugs of sizes (8, 16, 32), initially filled with (2, 4, 3) units of \n",
    "    water, respectively, and the goal is to get a level of 7 in any one of the jugs.\"\"\"\n",
    "    \n",
    "    def actions(self, state):\n",
    "        \"\"\"The actions executable in this state.\"\"\"\n",
    "        jugs = range(len(state))\n",
    "        return ([('Fill', i)    for i in jugs if state[i] < self.sizes[i]] +\n",
    "                [('Dump', i)    for i in jugs if state[i]] +\n",
    "                [('Pour', i, j) for i in jugs if state[i] for j in jugs if i != j])\n",
    "\n",
    "    def result(self, state, action):\n",
    "        \"\"\"The state that results from executing this action in this state.\"\"\"\n",
    "        result = list(state)\n",
    "        act, i, *_ = action\n",
    "        if act == 'Fill':   # Fill i to capacity\n",
    "            result[i] = self.sizes[i]\n",
    "        elif act == 'Dump': # Empty i\n",
    "            result[i] = 0\n",
    "        elif act == 'Pour': # Pour from i into j\n",
    "            j = action[2]\n",
    "            amount = min(state[i], self.sizes[j] - state[j])\n",
    "            result[i] -= amount\n",
    "            result[j] += amount\n",
    "        return tuple(result)\n",
    "\n",
    "    def is_goal(self, state):\n",
    "        \"\"\"True if the goal level is in any one of the jugs.\"\"\"\n",
    "        return self.goal in state\n",
    "    \n",
    "    \n",
    "class GreenPourProblem(PourProblem): \n",
    "    \"\"\"A PourProblem in which we count not the number of steps, but the amount of water used.\"\"\"\n",
    "    def step_cost(self, state, action, result=None):\n",
    "        \"The cost is the amount of water used in a fill.\"\n",
    "        act, i, *_ = action\n",
    "        return self.sizes[i] - state[i] if act == 'Fill' else 0"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Route Finding Problems"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "class RouteProblem(Problem):\n",
    "    \"\"\"A problem to find a route between places on a map.\n",
    "    Use RouteProblem('S', 'G', map=Map(...)})\"\"\"\n",
    "    \n",
    "    def actions(self, state): \n",
    "        \"\"\"The places neighboring `state`. (Action names are same as place names.)\"\"\"\n",
    "        return self.map.neighbors[state]\n",
    "    \n",
    "    def result(self, state, action):\n",
    "        \"\"\"Go to the `action` place, if the map says that is possible.\"\"\"\n",
    "        return action if action in self.map.neighbors[state] else state\n",
    "    \n",
    "    def step_cost(self, s, action, s1):\n",
    "        \"\"\"The actual distance between s and s1.\"\"\"\n",
    "        return self.map.distances[s, s1]\n",
    "    \n",
    "    def h(self, node):\n",
    "        \"Straight-line distance between state and the goal.\"\n",
    "        locs = self.map.locations\n",
    "        s, g = locs[node.state], locs[self.goal]\n",
    "        return abs(complex(*s) - complex(*g))\n",
    "\n",
    "class Map:\n",
    "    \"\"\"Builds an undirected graph of {vertex: [neighbors...]}, with two additional annotations:\n",
    "       neighbors:\n",
    "       distances: a dict of {(v1, v2): number} giving the distance from v1 to v2;\n",
    "       locations: a dict of {v: (x, y)} giving the (x, y) location of each vertex.\"\"\"\n",
    "    def __init__(self, distances, locations=()):\n",
    "        self.neighbors = undirected_graph(distances)\n",
    "        self.distances = distances\n",
    "        self.locations = locations or defaultdict(lambda: (0, 0))\n",
    "        for (v1, v2) in list(distances):\n",
    "            distances[v2, v1] = distances[v1, v2]\n",
    "            \n",
    "def undirected_graph(pairs):\n",
    "    \"Given {(v1, v2)...} pairs, return a graph of {v1: [v2,...], v2:[v1,...]}.\"\n",
    "    graph = defaultdict(tuple)\n",
    "    for (v1, v2) in pairs:\n",
    "        graph[v1] += (v2,)\n",
    "        graph[v2] += (v1,)\n",
    "    return dict(graph)\n",
    "\n",
    "romania = Map(distances={\n",
    "    ('O', 'Z'): 71, ('O', 'S'): 151, ('A', 'Z'): 75, ('A', 'S'): 140, ('A', 'T'): 118, \n",
    "    ('L', 'T'): 111, ('L', 'M'): 70, ('D', 'M'): 75, ('C', 'D'): 120, ('C', 'R'): 146, \n",
    "    ('C', 'P'): 138, ('R', 'S'): 80, ('F', 'S'): 99, ('B', 'F'): 211, ('B', 'P'): 101, \n",
    "    ('B', 'G'): 90, ('B', 'U'): 85, ('H', 'U'): 98,  ('E', 'H'): 86, ('U', 'V'): 142, \n",
    "    ('I', 'V'): 92, ('I', 'N'): 87, ('P', 'R'): 97},\n",
    "    locations=dict(\n",
    "    A=(91, 492), B=(400, 327), C=(253, 288), D=(165, 299), E=(562, 293), F=(305, 449),\n",
    "    G=(375, 270), H=(534, 350), I=(473, 506), L=(165, 379), M=(168, 339), N=(406, 537),\n",
    "    O=(131, 571), P=(320, 368), R=(233, 410), S=(207, 457), T=(94, 410), U=(456, 350),\n",
    "    V=(509, 444), Z=(108, 531)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Z', 'S', 'T')"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "romania.neighbors['A'] # Neighbors of "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "75"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "romania.distances['A', 'Z']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(91, 492)"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "romania.locations['A']"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 8 Puzzle Problems\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "class EightPuzzle(Problem):\n",
    "    \"\"\" The problem of sliding tiles numbered from 1 to 8 on a 3x3 board,\n",
    "    where one of the squares is a blank, trying to reach a goal configuration.\n",
    "    A board state is represented as a tuple of length 9, where the element at index i \n",
    "    represents the tile number at index i, or 0 if for the empty square, e.g. the goal:\n",
    "        1 2 3\n",
    "        4 5 6 ==> (1, 2, 3, 4, 5, 6, 7, 8, 0)\n",
    "        7 8 _\n",
    "    \"\"\"\n",
    "    \n",
    "    def actions(self, state):\n",
    "        \"\"\"The numbers of the squares that the blank can move to.\"\"\"\n",
    "        moves = ((1, 3),    (0, 2, 4),    (1, 5),\n",
    "                 (0, 4, 6), (1, 3, 5, 7), (2, 4, 8),\n",
    "                 (3, 7),    (4, 6, 8),    (7, 5))\n",
    "        blank = state.index(0)\n",
    "        return moves[blank]\n",
    "    \n",
    "    def result(self, state, action):\n",
    "        \"\"\"Swap the blank with the square numbered `action`.\"\"\"\n",
    "        s = list(state)\n",
    "        blank = state.index(0)\n",
    "        s[action], s[blank] = s[blank], s[action]\n",
    "        return tuple(s)\n",
    "    \n",
    "    def h(self, node):\n",
    "        \"\"\"The misplaced tiles heuristic.\"\"\"\n",
    "        return sum(s != g for (s, g) in zip(node.state, self.goal))\n",
    "    \n",
    "    \n",
    "def board8(board, fmt=(3 * '{} {} {}\\n')):\n",
    "    \"A string representing an 8-puzzle board\"\n",
    "    return fmt.format(*board).replace('0', '_')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Specific Problems and Solutions\n",
    "\n",
    "Now that we have some domains, we can make specific problems in those domains, and solve them:\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "p1 = PourProblem((1, 1, 1), 13, sizes=(2, 16, 32))\n",
    "p2 = PourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "p3 = PourProblem((0, 0), 8, sizes=(7,9))\n",
    "p4 = PourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "\n",
    "g1 = GreenPourProblem((1, 1, 1), 13, sizes=(2, 16, 32))\n",
    "g2 = GreenPourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "g3 = GreenPourProblem((0, 0), 8, sizes=(7,9))\n",
    "g4 = GreenPourProblem((0, 0, 0), 21, sizes=(8, 11, 31))\n",
    "\n",
    "r1 = RouteProblem('A', 'B', map=romania)\n",
    "r2 = RouteProblem('N', 'L', map=romania)\n",
    "r3 = RouteProblem('E', 'T', map=romania)\n",
    "r4 = RouteProblem('O', 'M', map=romania)\n",
    "\n",
    "goal = (1, 2, 3, 4, 5, 6, 7, 8, 0)\n",
    "e1 = EightPuzzle((4, 0, 2, 5, 1, 3, 7, 8, 6), goal)\n",
    "e2 = EightPuzzle((1, 4, 2, 0, 7, 5, 3, 6, 8), goal)\n",
    "e3 = EightPuzzle((2, 5, 8, 1, 4, 7, 0, 3, 6), goal)\n",
    "e4 = EightPuzzle((0, 1, 2, 3, 4, 5, 6, 7, 8), goal)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['A', 'S', 'R', 'P', 'B']"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Solve a problem (which gives a node) and recover the sequence of states in that node's path\n",
    "path_states(astar_search(r1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['A', 'S', 'F', 'B']"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Breadth first search finds a solution with fewer steps, but in this case higher path cost\n",
    "path_states(breadth_first_search(r1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[(1, 1, 1),\n",
       " ('Fill', 1),\n",
       " (1, 16, 1),\n",
       " ('Pour', 1, 0),\n",
       " (2, 15, 1),\n",
       " ('Dump', 0),\n",
       " (0, 15, 1),\n",
       " ('Pour', 1, 0),\n",
       " (2, 13, 1)]"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Solve a problem and recover the path of alternating states and actions\n",
    "path(breadth_first_search(p1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "4 _ 2\n",
      "5 1 3\n",
      "7 8 6\n",
      "\n",
      "4 1 2\n",
      "5 _ 3\n",
      "7 8 6\n",
      "\n",
      "4 1 2\n",
      "_ 5 3\n",
      "7 8 6\n",
      "\n",
      "_ 1 2\n",
      "4 5 3\n",
      "7 8 6\n",
      "\n",
      "1 _ 2\n",
      "4 5 3\n",
      "7 8 6\n",
      "\n",
      "1 2 _\n",
      "4 5 3\n",
      "7 8 6\n",
      "\n",
      "1 2 3\n",
      "4 5 _\n",
      "7 8 6\n",
      "\n",
      "1 2 3\n",
      "4 5 6\n",
      "7 8 _\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Solve an 8 puzzle problem and print out each state\n",
    "\n",
    "for s in path_states(astar_search(e1)):\n",
    "    print(board8(s))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Reporting Metrics\n",
    "\n",
    "Now let's gather some metrics on how well each algorithm does.  We'll use `CountCalls` to wrap a `Problem` object in such a way that calls to its methods are delegated, but each call increments a counter. Once we've solved the problem, we print out summary statistics."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "class CountCalls:\n",
    "    \"\"\"Delegate all attribute accesses to the object, and count them in ._counts\"\"\"\n",
    "    def __init__(self, obj):\n",
    "        self._object = obj\n",
    "        self._counts = Counter()\n",
    "        \n",
    "    def __getattr__(self, attr):\n",
    "        self._counts[attr] += 1\n",
    "        return getattr(self._object, attr)\n",
    "        \n",
    "def report(searchers, problems):\n",
    "    \"Show metrics for each searcher on each problem.\"\n",
    "    for searcher in searchers:\n",
    "        print(searcher.__name__ + ':')\n",
    "        total_counts = Counter()\n",
    "        for p in problems:\n",
    "            prob = CountCalls(p)\n",
    "            soln = searcher(prob)\n",
    "            cts  = prob._counts; \n",
    "            cts.update(len=len(path_actions(soln)), cost=soln.path_cost)\n",
    "            total_counts += cts\n",
    "            report_line(cts, type(p).__name__)\n",
    "        report_line(total_counts, 'TOTAL\\n')\n",
    "        \n",
    "def report_line(counts, name):\n",
    "    \"Print one line of the report.\"\n",
    "    print('{:7,d} Exp |{:7,d} Gen |{:7,d} Goal |{:5.0f} cost |{:3d} len | {}'\n",
    "          .format(counts['actions'], counts['result'], counts['is_goal'], \n",
    "                  counts['cost'], counts['len'], name))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "astar_search:\n",
      "    150 Exp |  1,325 Gen |    151 Goal |    4 cost |  4 len | PourProblem\n",
      "    378 Exp |  3,381 Gen |    379 Goal |    9 cost |  9 len | PourProblem\n",
      "    528 Exp |  4,706 Gen |    530 Goal |   13 cost | 13 len | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Here's a tiny report\n",
    "report([astar_search], [p1, p2])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The last line says that, over the two problems `[p1, p2]`, the `astar_search` algorithm expanded 528 nodes, generating 4,706 nodes and doing 530 goal checks. Together, the two solutions had a path cost of 13 and also a total length of 13 (since step cost was 1 in these problems). \n",
    "\n",
    "Now let's do a bigger report, concentrating first on the easier problems, then harder ones:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "astar_search:\n",
      "    150 Exp |  1,325 Gen |    151 Goal |    4 cost |  4 len | PourProblem\n",
      "    185 Exp |  1,646 Gen |    186 Goal |   10 cost | 12 len | GreenPourProblem\n",
      "      5 Exp |     15 Gen |      6 Goal |  418 cost |  4 len | RouteProblem\n",
      "     15 Exp |     35 Gen |     16 Goal |  910 cost |  9 len | RouteProblem\n",
      "     14 Exp |     34 Gen |     15 Goal |  805 cost |  8 len | RouteProblem\n",
      "      9 Exp |     22 Gen |     10 Goal |  445 cost |  5 len | RouteProblem\n",
      "     11 Exp |     29 Gen |     12 Goal |    7 cost |  7 len | EightPuzzle\n",
      "    389 Exp |  3,106 Gen |    396 Goal | 2599 cost | 49 len | TOTAL\n",
      "\n",
      "uniform_cost_search:\n",
      "    150 Exp |  1,325 Gen |    151 Goal |    4 cost |  4 len | PourProblem\n",
      "    185 Exp |  1,646 Gen |    186 Goal |   10 cost | 12 len | GreenPourProblem\n",
      "     13 Exp |     33 Gen |     14 Goal |  418 cost |  4 len | RouteProblem\n",
      "     19 Exp |     43 Gen |     20 Goal |  910 cost |  9 len | RouteProblem\n",
      "     20 Exp |     45 Gen |     21 Goal |  805 cost |  8 len | RouteProblem\n",
      "     12 Exp |     32 Gen |     13 Goal |  445 cost |  5 len | RouteProblem\n",
      "    124 Exp |    335 Gen |    125 Goal |    7 cost |  7 len | EightPuzzle\n",
      "    523 Exp |  3,459 Gen |    530 Goal | 2599 cost | 49 len | TOTAL\n",
      "\n",
      "breadth_first_search:\n",
      "    127 Exp |  1,116 Gen |    128 Goal |    4 cost |  4 len | PourProblem\n",
      "    127 Exp |  1,116 Gen |    128 Goal |   15 cost |  4 len | GreenPourProblem\n",
      "     11 Exp |     29 Gen |     12 Goal |  450 cost |  3 len | RouteProblem\n",
      "     20 Exp |     45 Gen |     21 Goal | 1085 cost |  9 len | RouteProblem\n",
      "     18 Exp |     41 Gen |     19 Goal |  837 cost |  7 len | RouteProblem\n",
      "     15 Exp |     38 Gen |     16 Goal |  445 cost |  5 len | RouteProblem\n",
      "    143 Exp |    397 Gen |    144 Goal |    7 cost |  7 len | EightPuzzle\n",
      "    461 Exp |  2,782 Gen |    468 Goal | 2843 cost | 39 len | TOTAL\n",
      "\n",
      "iterative_deepening_search:\n",
      "    981 Exp |  7,610 Gen |  7,610 Goal |    4 cost |  4 len | PourProblem\n",
      "    981 Exp |  7,610 Gen |  7,610 Goal |   15 cost |  4 len | GreenPourProblem\n",
      "     10 Exp |     27 Gen |     27 Goal |  450 cost |  3 len | RouteProblem\n",
      "    547 Exp |  1,308 Gen |  1,308 Goal |  910 cost |  9 len | RouteProblem\n",
      "    172 Exp |    406 Gen |    406 Goal |  837 cost |  7 len | RouteProblem\n",
      "     63 Exp |    175 Gen |    175 Goal |  572 cost |  5 len | RouteProblem\n",
      "    742 Exp |  2,108 Gen |  2,108 Goal |    7 cost |  7 len | EightPuzzle\n",
      "  3,496 Exp | 19,244 Gen | 19,244 Goal | 2795 cost | 39 len | TOTAL\n",
      "\n",
      "depth_limited_search:\n",
      "    472 Exp |  3,522 Gen |  3,522 Goal |    6 cost |  6 len | PourProblem\n",
      "    472 Exp |  3,522 Gen |  3,522 Goal |   16 cost |  6 len | GreenPourProblem\n",
      "     29 Exp |     69 Gen |     69 Goal |  686 cost |  5 len | RouteProblem\n",
      "     28 Exp |     59 Gen |     59 Goal |  inf cost |  0 len | RouteProblem\n",
      "     40 Exp |    100 Gen |    100 Goal |  inf cost |  0 len | RouteProblem\n",
      "     47 Exp |    139 Gen |    139 Goal |  661 cost |  6 len | RouteProblem\n",
      "    292 Exp |    803 Gen |    803 Goal |  inf cost |  0 len | EightPuzzle\n",
      "  1,380 Exp |  8,214 Gen |  8,214 Goal |  inf cost | 23 len | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "easy = (p1, g1, r1, r2, r3, r4, e1)\n",
    "hard = (g2, p2, g3, p3, g4, p4, e2, e3, e4)\n",
    "\n",
    "report((astar_search, uniform_cost_search,  breadth_first_search, \n",
    "        iterative_deepening_search, depth_limited_search), easy)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "One thing to notice: on three of the problems, `depth_limited_search` had a path cost of `inf`, meaning that the search was cut off, so it reported an infinite cost.\n",
    "\n",
    "If we look at the whole `cost` column, we see that the optimal algorithms, `astar_search` and `uniform_cost_search`, give the best results, while `breadth_first_search` and `iterative_deepening_search` have non-optimal costs on some problems, because they find a solution with the minimal number of steps, but not the minimal path cost. We see that `astar_search` has fewer expansions, generated nodes, and goal tests that `uniform_cost_search`, which means the heuristic helps (if only by 10% or so).\n",
    "\n",
    "Next I'll try some harder problems; I won't even try the tree search algorithms on these problems; too many redundant paths."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "astar_search:\n",
      "    451 Exp |  4,048 Gen |    452 Goal |   21 cost | 19 len | GreenPourProblem\n",
      "    378 Exp |  3,381 Gen |    379 Goal |    9 cost |  9 len | PourProblem\n",
      "     30 Exp |    126 Gen |     31 Goal |   35 cost | 16 len | GreenPourProblem\n",
      "     30 Exp |    126 Gen |     31 Goal |   14 cost | 14 len | PourProblem\n",
      "    451 Exp |  4,048 Gen |    452 Goal |   21 cost | 19 len | GreenPourProblem\n",
      "    378 Exp |  3,381 Gen |    379 Goal |    9 cost |  9 len | PourProblem\n",
      " 10,338 Exp | 27,461 Gen | 10,339 Goal |   23 cost | 23 len | EightPuzzle\n",
      " 14,119 Exp | 37,562 Gen | 14,120 Goal |   24 cost | 24 len | EightPuzzle\n",
      "  5,989 Exp | 15,951 Gen |  5,990 Goal |   22 cost | 22 len | EightPuzzle\n",
      " 32,164 Exp | 96,084 Gen | 32,173 Goal |  178 cost |155 len | TOTAL\n",
      "\n",
      "uniform_cost_search:\n",
      "    451 Exp |  4,048 Gen |    452 Goal |   21 cost | 19 len | GreenPourProblem\n",
      "    378 Exp |  3,381 Gen |    379 Goal |    9 cost |  9 len | PourProblem\n",
      "     30 Exp |    126 Gen |     31 Goal |   35 cost | 16 len | GreenPourProblem\n",
      "     30 Exp |    126 Gen |     31 Goal |   14 cost | 14 len | PourProblem\n",
      "    451 Exp |  4,048 Gen |    452 Goal |   21 cost | 19 len | GreenPourProblem\n",
      "    378 Exp |  3,381 Gen |    379 Goal |    9 cost |  9 len | PourProblem\n",
      "103,882 Exp |279,376 Gen |103,883 Goal |   23 cost | 23 len | EightPuzzle\n",
      "121,025 Exp |325,288 Gen |121,026 Goal |   24 cost | 24 len | EightPuzzle\n",
      " 76,710 Exp |206,476 Gen | 76,711 Goal |   22 cost | 22 len | EightPuzzle\n",
      "303,335 Exp |826,250 Gen |303,344 Goal |  178 cost |155 len | TOTAL\n",
      "\n",
      "breadth_first_search:\n",
      "    422 Exp |  3,840 Gen |    423 Goal |   32 cost |  9 len | GreenPourProblem\n",
      "    422 Exp |  3,840 Gen |    423 Goal |    9 cost |  9 len | PourProblem\n",
      "     30 Exp |    126 Gen |     31 Goal |   36 cost | 14 len | GreenPourProblem\n",
      "     30 Exp |    126 Gen |     31 Goal |   14 cost | 14 len | PourProblem\n",
      "    422 Exp |  3,840 Gen |    423 Goal |   32 cost |  9 len | GreenPourProblem\n",
      "    422 Exp |  3,840 Gen |    423 Goal |    9 cost |  9 len | PourProblem\n",
      "118,340 Exp |316,026 Gen |118,341 Goal |   23 cost | 23 len | EightPuzzle\n",
      "131,021 Exp |350,990 Gen |131,022 Goal |   24 cost | 24 len | EightPuzzle\n",
      " 80,968 Exp |218,918 Gen | 80,969 Goal |   22 cost | 22 len | EightPuzzle\n",
      "332,077 Exp |901,546 Gen |332,086 Goal |  201 cost |133 len | TOTAL\n",
      "\n"
     ]
    }
   ],
   "source": [
    "report((astar_search, uniform_cost_search, breadth_first_search), hard)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This time we see that A* is an order of magnitude more efficient than the two uninformed algorithm. Note that again,  uniform cost is optimal, but breadth-first is not: it optimized for path length, not path cost."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
