#!/usr/bin/env python # coding: utf-8 # # Informed Search # Also known as "heuristic" search, because the search is informed by an # estimate of the total path cost through each node, and the next # unexpanded node with the lowest estimated cost is expanded next. # # At some intermediate node, the # estimated cost of the solution path = # the sum of the step costs so far from the start node to this node # + # an estimate of the sum of the remaining step costs to a goal # # Let's label these as # # * $f(n) =$ estimated cost of the solution path through node $n$ # * $g(n) =$ the sum of the step costs so far from the start node to this node # * $h(n) =$ an estimate of the sum of the remaining step costs to a goal # # *heuristic function*: $h(n) =$ estimated cost of the cheapest path from state at node $n$ to a goal state. # # # # Should we explore under Node a or b? # # # # A* algorithm # ## Non-recursive # So, now you know enough python to try to implement A\*, at least a non-recursive form. Start with your graph search algorithm from Assignment 1. Modify it so that the next node selected is based on its `f` value. # # For a given problem, define `start_state`, `actions_f`, `take_action_f`, `goal_test_f`, and a heuristic function `heuristic_f`. `actions_f` must return valid actions paired with the single step cost, and `take_action_f` must return the pair containing the new state and the cost of the single step given by the action. We can use the `Node` class to hold instances of nodes. However, since this is not a recursive algorithm, `Node` must be extended to include the node's parent node, to be able to generate the solution path once the search finds the goal. # # Now the A* algorithm can be written as follows # * Initialize `expanded` to be an empty dictionary # * Initialize `un_expanded` to be a list containing the start_state node. Its `h` value is calculated using `heuristic_f`, its `g` value is 0, and its `f` value is `g+h`. # * If `start_state` is the `goal_state`, return the list containing just `start_state` and its `f` value to show the cost of the solution path. # * Repeat the following steps while `un_expanded` is not empty: # * Pop from the front of `un_expanded` to get the best (lowest f value) node to expand. # * Generate the `children` of this `node`. # * Update the `g` value of each child by adding the action's single step cost to this node's `g` value. # * Calculate `heuristic_f` of each child. # * Set `f = g + h` of each child. # * Add the node to the `expanded` dictionary, indexed by its state. # * Remove from `children` any nodes that are already either in `expanded` or `un_expanded`, unless the node in `children` has a lower f value. # * If `goal_state` is in `children`: # * Build the solution path as a list starting with `goal_state`. # * Use the parent stored with each node in the `expanded` dictionary to construct the path. # * Reverse the solution path list and return it. # * Insert the modified `children` list into the `un_expanded` list and ** sort by `f` values.** # ## Recursive # Our authors provide the Recursive Best-First Search algorithm, which # is A\* in a recursive, iterative-deepening form, where depth is now # given by the $f$ value. Other differences from just # iterative-deepening A\* are: # - depth-limit determined by $f$ value of best alternative to node being explored, so will stop when alternative at the node's level looks better; # - $f$ value of a node is replaced by best $f$ value of its children, so any future decision to try expanding this node again is more informed. # # It is a bit difficult to translate their pseudo-code into python. Here is my version. Let's step through it. # In[32]: get_ipython().run_cell_magic('writefile', 'a_star_search.py', '# Recursive Best First Search (Figure 3.26, Russell and Norvig)\n# Recursive Iterative Deepening form of A*, where depth is replaced by f(n)\n\nclass Node:\n\n def __init__(self, state, f=0, g=0, h=0):\n self.state = state\n self.f = f\n self.g = g\n self.h = h\n\n def __repr__(self):\n return f\'Node({self.state}, f={self.f}, g={self.g}, h={self.h})\'\n\ndef a_star_search(start_state, actions_f, take_action_f, goal_test_f, heuristic_f):\n h = heuristic_f(start_state)\n start_node = Node(state=start_state, f=0 + h, g=0, h=h)\n return a_star_search_helper(start_node, actions_f, take_action_f, \n goal_test_f, heuristic_f, float(\'inf\'))\n\ndef a_star_search_helper(parent_node, actions_f, take_action_f, \n goal_test_f, heuristic_f, f_max):\n\n if goal_test_f(parent_node.state):\n return ([parent_node.state], parent_node.g)\n \n ## Construct list of children nodes with f, g, and h values\n actions = actions_f(parent_node.state)\n if not actions:\n return (\'failure\', float(\'inf\'))\n \n children = []\n for action in actions:\n (child_state, step_cost) = take_action_f(parent_node.state, action)\n h = heuristic_f(child_state)\n g = parent_node.g + step_cost\n f = max(h + g, parent_node.f)\n child_node = Node(state=child_state, f=f, g=g, h=h)\n children.append(child_node)\n \n while True:\n # find best child\n children.sort(key = lambda n: n.f) # sort by f value\n best_child = children[0]\n if best_child.f > f_max:\n return (\'failure\', best_child.f)\n # next lowest f value\n alternative_f = children[1].f if len(children) > 1 else float(\'inf\')\n # expand best child, reassign its f value to be returned value\n result, best_child.f = a_star_search_helper(best_child, actions_f,\n take_action_f, goal_test_f,\n heuristic_f,\n min(f_max,alternative_f))\n if result != \'failure\': # g\n result.insert(0, parent_node.state) # / \n return (result, best_child.f) # d\n # / \\ \nif __name__ == "__main__": # b h \n # / \\ \n successors = {\'a\': [\'b\',\'c\'], # a e \n \'b\': [\'d\',\'e\'], # \\ \n \'c\': [\'f\'], # c i\n \'d\': [\'g\', \'h\'], # \\ / \n \'f\': [\'i\',\'j\']} # f \n # \\\n def actions_f(state): # j \n try:\n ## step cost of each action is 1\n return [(succ, 1) for succ in successors[state]]\n except KeyError:\n return []\n\n def take_action_f(state, action):\n return action\n\n def goal_test_f(state):\n return state == goal\n\n def h1(state):\n return 0\n\n start = \'a\'\n goal = \'h\'\n result = a_star_search(start, actions_f, take_action_f, goal_test_f, h1)\n\n print(f\'Path from a to h is {result[0]} for a cost of {result[1]}\')\n') # Running this shows # In[33]: run a_star_search.py # In[34]: actions_f('a') valid_ones = actions_f('a') valid_ones # In[35]: take_action_f('a', valid_ones[0]) # Actually, there is in error in this code. Try using it to search for a goal that does not exist!