A3: A*, IDS, and Effective Branching Factor

For this assignment, implement the Recursive Best-First Search implementation of the A* algorithm given in class. Name this function Astar_search. Also in this notebook include your iterative_deepening_search functions. Define a new function named effective_branching_factor that returns an estimate of the effective branching factor for a search algorithm applied to a search problem.

So, the required functions are

  • Astar_search(start_state, actions_f, take_action_f, goal_test_f, h_f)
  • iterative_deepening_search(start_state, goal_state, actions_f, take_action_f, max_depth)
  • effective_branching_factor(n_nodes, depth, precision=0.01), returns the effective branching factor, given the number of nodes expanded and depth reached during a search.

Apply iterative_deepening_search and Astar_search to several eight-tile sliding puzzle problems. For this you must include your implementations of these functions from Assignment 2. Here we are renaming these functions to not include _f, just for simplicity.

  • actions_8p(state): returns a list of up to four valid actions that can be applied in state. With each action include a step cost of 1. For example, if all four actions are possible from this state, return [('left', 1), ('right', 1), ('up', 1), ('down', 1)].
  • take_action_8p(state, action): return the state that results from applying action in state and the cost of the one step,

plus the following function for the eight-tile puzzle:

  • goal_test_8p(state, goal)

Compare their results by displaying solution path depth, number of nodes generated, and the effective branching factor, and discuss the results. Do this by defining the following function that prints the table as shown in the example below.

  • run_experiment(goal_state_1, goal_state_2, goal_state_3, [h1, h2, h3])

Define this function so it takes any number of $h$ functions in the list that is the fourth argument.

Heuristic Functions

For Astar_search use the following two heuristic functions, plus one more of your own design, for a total of three heuristic functions.

  • h1_8p(state, goal): $h(state, goal) = 0$, for all states $state$ and all goal states $goal$,
  • h2_8p(state, goal): $h(state, goal) = m$, where $m$ is the Manhattan distance that the blank is from its goal position,
  • h3_8p(state, goal): $h(state, goal) = ?$, that you define. It must be admissible, and not constant for all states.

Comparison

Apply all four algorithms (iterative_deepening_search plus Astar_search with the three heuristic functions) to three eight-tile puzzle problems with start state

$$ \begin{array}{ccc} 1 & 2 & 3\\ 4 & 0 & 5\\ 6 & 7 & 8 \end{array} $$

and these three goal states.

$$ \begin{array}{ccccccccccc} 1 & 2 & 3 & ~~~~ & 1 & 2 & 3 & ~~~~ & 1 & 0 & 3\\ 4 & 0 & 5 & & 4 & 5 & 8 & & 4 & 5 & 8\\ 6 & 7 & 8 & & 6 & 0 & 7 & & 2 & 6 & 7 \end{array} $$

Print a well-formatted table like the following. Try to match this format. If you have time, you might consider learning a bit about the DataFrame class in the pandas package. When displayed in jupyter notebooks, pandas.DataFrame objects are nicely formatted in html.

       [1, 2, 3, 4, 0, 5, 6, 7, 8]    [1, 2, 3, 4, 5, 8, 6, 0, 7]    [1, 0, 3, 4, 5, 8, 2, 6, 7] 
Algorithm    Depth  Nodes  EBF              Depth  Nodes  EBF              Depth  Nodes  EBF          
     IDS       0      0  0.000                3     43  3.086               11 225850  2.954         
    A*h1       0      0  0.000                3    116  4.488               11 643246  3.263         
    A*h2       0      0  0.000                3     51  3.297               11 100046  2.733         

Of course you will have one more line for h3.

First, some example output for the effective_branching_factor function. During execution, this example shows debugging output which is the low and high values passed into a recursive helper function.

In [2]:
effective_branching_factor(10, 3)
1 10
1 5.5
1 3.25
1 2.125
1.5625 2.125
1.5625 1.84375
1.5625 1.703125
1.6328125 1.703125
1.6328125 1.66796875
1.650390625 1.66796875
1.6591796875 1.66796875
1.6591796875 1.66357421875
Out[2]:
1.661376953125

The smallest argument values should be a depth of 0, and 1 node.

In [3]:
effective_branching_factor(1, 0)
1 1
Out[3]:
1.0
In [4]:
effective_branching_factor(2, 1)
1 2
1 1.5
1 1.25
1 1.125
1 1.0625
1 1.03125
1 1.015625
Out[4]:
1.0078125
In [5]:
effective_branching_factor(2, 1, precision=0.000001)
1 2
1 1.5
1 1.25
1 1.125
1 1.0625
1 1.03125
1 1.015625
1 1.0078125
1 1.00390625
1 1.001953125
1 1.0009765625
1 1.00048828125
1 1.000244140625
1 1.0001220703125
1 1.00006103515625
1 1.000030517578125
1 1.0000152587890625
1 1.0000076293945312
1 1.0000038146972656
1 1.0000019073486328
Out[5]:
1.0000009536743164
In [6]:
effective_branching_factor(200000, 5)
1 200000
1 100000.5
1 50000.75
1 25000.875
1 12500.9375
1 6250.96875
1 3125.984375
1 1563.4921875
1 782.24609375
1 391.623046875
1 196.3115234375
1 98.65576171875
1 49.827880859375
1 25.4139404296875
1 13.20697021484375
7.103485107421875 13.20697021484375
10.155227661132812 13.20697021484375
10.155227661132812 11.681098937988281
10.918163299560547 11.681098937988281
10.918163299560547 11.299631118774414
11.10889720916748 11.299631118774414
11.204264163970947 11.299631118774414
11.25194764137268 11.299631118774414
11.25194764137268 11.275789380073547
11.263868510723114 11.275789380073547
11.26982894539833 11.275789380073547
11.272809162735939 11.275789380073547
11.274299271404743 11.275789380073547
11.275044325739145 11.275789380073547
11.275416852906346 11.275789380073547
11.275416852906346 11.275603116489947
11.275509984698147 11.275603116489947
11.275556550594047 11.275603116489947
11.275579833541997 11.275603116489947
11.275591475015972 11.275603116489947
11.275591475015972 11.27559729575296
11.275594385384466 11.27559729575296
11.275595840568712 11.27559729575296
11.275596568160836 11.27559729575296
Out[6]:
11.275596931956898
In [7]:
effective_branching_factor(200000, 50)
1 200000
1 100000.5
1 50000.75
1 25000.875
1 12500.9375
1 6250.96875
1 3125.984375
1 1563.4921875
1 782.24609375
1 391.623046875
1 196.3115234375
1 98.65576171875
1 49.827880859375
1 25.4139404296875
1 13.20697021484375
1 7.103485107421875
1 4.0517425537109375
1 2.5258712768554688
1 1.7629356384277344
1 1.3814678192138672
1.1907339096069336 1.3814678192138672
1.1907339096069336 1.2861008644104004
1.1907339096069336 1.238417387008667
1.2145756483078003 1.238417387008667
1.2264965176582336 1.238417387008667
1.2324569523334503 1.238417387008667
1.2324569523334503 1.2354371696710587
1.2339470610022545 1.2354371696710587
1.2346921153366566 1.2354371696710587
1.2346921153366566 1.2350646425038576
1.2346921153366566 1.234878378920257
1.2347852471284568 1.234878378920257
1.2347852471284568 1.234831813024357
1.234808530076407 1.234831813024357
1.234808530076407 1.234820171550382
1.2348143508133944 1.234820171550382
1.2348172611818882 1.234820171550382
1.234818716366135 1.234820171550382
1.234818716366135 1.2348194439582585
1.2348190801621968 1.2348194439582585
1.2348190801621968 1.2348192620602276
1.2348191711112122 1.2348192620602276
1.23481921658572 1.2348192620602276
1.2348192393229738 1.2348192620602276
1.2348192393229738 1.2348192506916007
1.2348192450072872 1.2348192506916007
1.234819247849444 1.2348192506916007
Out[7]:
1.2348192492705223

Here is a simple example using our usual simple graph search.

In [1]:
def actions_simple(state):
    succs = {'a': ['b', 'c'], 'b':['a'], 'c':['h'], 'h':['i'], 'i':['j', 'k', 'l'], 'k':['z']}
    return [(s, 1) for s in succs.get(state, [])]

def take_action_simple(state, action):
    return action

def goal_test_simple(state, goal):
    return state == goal

def h_simple(state, goal):
    return 1
In [2]:
actions = actions_simple('a')
actions
Out[2]:
[('b', 1), ('c', 1)]
In [3]:
take_action_simple('a', actions[0])
Out[3]:
('b', 1)
In [11]:
goal_test_simple('a', 'a')
Out[11]:
True
In [12]:
h_simple('a', 'z')
Out[12]:
1
In [13]:
iterative_deepening_search('a', 'z', actions_simple, take_action_simple, 10)
Out[13]:
['a', 'c', 'h', 'i', 'k', 'z']
In [14]:
Astar_search('a',actions_simple, take_action_simple,
            lambda s: goal_test_simple(s, 'z'),
            lambda s: h_simple(s, 'z'))
Out[14]:
(['a', 'c', 'h', 'i', 'k', 'z'], 5)

Grading

Download A3grader.tar and extract A3grader.py from it.

In [2]:
%run -i A3grader.py
======================= Code Execution =======================

Extracting python code from notebook named 'Anderson-A3.ipynb' and storing in notebookcode.py
Removing all statements that are not function or class defs or import statements.

Testing actions_8p([1, 2, 3, 4, 5, 6, 7, 0, 8])

--- 5/5 points. Your actions_8p correctly returned [('left', 1), ('right', 1), ('up', 1)]

Testing take_action_8p([1, 2, 3, 4, 5, 6, 7, 0, 8], (up, 1))

--- 5/5 points. Your take actions_8p correctly returned ([1, 2, 3, 4, 0, 6, 7, 5, 8], 1)

Testing goal_test_8p([1, 2, 3, 4, 5, 6, 7, 0, 8], [1, 2, 3, 4, 5, 6, 7, 0, 8])

--- 5/5 points. Your goal_test_8p correctly True

Testing Astar_search(1, 2, 3, 4, 5, 6, 7, 0, 8],
                     actions_8p, take_action_8p,
                     lambda s: goal_test_8p(s, [0, 2, 3, 1, 4,  6, 7, 5, 8]),
                     lambda s: h1_8p(s, [0, 2, 3, 1, 4,  6, 7, 5, 8]))

--- 20/20 points. Your search correctly returned ([[1, 2, 3, 4, 5, 6, 7, 0, 8], [1, 2, 3, 4, 0, 6, 7, 5, 8], [1, 2, 3, 0, 4, 6, 7, 5, 8], [0, 2, 3, 1, 4, 6, 7, 5, 8]], 3)

Testing iterative_deepening_search([1, 2, 3, 4, 5, 6, 7, 0, 8], 
                                 [0, 2, 3, 1, 4,  6, 7, 5, 8],
                                 actions_8p, take_action_8p, 10)

--- 15/15 points. Your search correctly returned [[1, 2, 3, 4, 5, 6, 7, 0, 8], [1, 2, 3, 4, 0, 6, 7, 5, 8], [1, 2, 3, 0, 4, 6, 7, 5, 8], [0, 2, 3, 1, 4, 6, 7, 5, 8]]

Testing iterative_deepening_search([5, 2, 8, 0, 1, 4, 3, 7, 6], 
                                 [0, 2, 3, 1, 4,  6, 7, 5, 8],
                                 actions_8p, take_action_8p, 10)

--- 15/15 points. Your search correctly returned cutoff

Testing effective_branching_factor(200, 6, 0.1)

--- 15/15 points. Your call to effective_branching_factor correctly returned 2.185373306274414

======================================================================
A3 Execution Grade is 80 / 80
======================================================================

___ / 10 points.  At least 6 meaningful sentences describing your third heuristic function.
                  Describe what it calculates and argue why you think it is admissible.

___ / 10 points.  At least 6 more sentences that discuss the similarities and differences in your
                  results for each search method and heuristic function.

======================================================================
A3 Additional Grade is __ / 20
======================================================================

======================================================================
A3 FINAL GRADE is  _  / 100
======================================================================

Extra Credit: Earn one point of extra credit for adding the computation time to your results table
and a discussion of the timing results.

A3 EXTRA CREDIT is 0 / 1

Extra Credit

Add a third column for each result (from running runExperiment) that is the number of seconds each search required. You may get the total run time when running a function by doing

 import time

 start_time = time.time()

 < do some python stuff >

 end_time = time.time()
 print('This took', end_time - start_time, 'seconds.')