Course Home |
Assignments |
Schedule |
Submit
|
Computer Science 462
Artificial Intelligence
Spring 2010 |
|
Assignment
03
Bidirectional Searches
Contents:
For this assignment, you are allowed to work with one other
student if you wish (in fact, we suggest that you do so).
If any student wishes to have a partner but
has not been able to locate one, please let the instructor know so
that we can match up partners.
Please make sure you adhere to the policies on
academic integrity in this regard.
Overview
Topic: Bidirectional Searches
Related Reading: Ch. 3
Due:
Wednesday, 24 February 2010, 11:59pm
Your goal is to add support for bidirectional searching to our
framework for general searches. We will explain the details below,
but for those interested in reading more about the technical details
of bidirectional searching, here are some research articles.
Source Code
The source code for this project can be found at
turing:/Public/goldwasser/462/puzzles/current/ (the 09Feb2010
version). The top-level file solvers.py contains all of the
logic for performing the various search algorithms on our puzzle
framework, including a full implementation of forward or backward
searches using metrics such as BFS, uniform-cost or A*.
Your task for this assignment is to add support for
bidirectional searches. To provide a more clean division of code, we
have stubbed out a file bidirectional.py with an initial (but
incomplete) definition of a bidirectional search. You are to submit a
correct version of bidirectional.py while leaving all other
source files unchanged. This file has a function search that
should return a solution, when found. The form of a solution is a
complete list of (action,resultingState) pairs that lead in a forward
direction from the initial state to the goal state.
You may test test program using one of the specific puzzles types
found in the demos/ subdirectory. In particular, the
shortestpath and tiles problems support bidirectional reasoning.
Relevant classes
Within the solver.py file, we define three relevant classes.
A GenericSolver class is used to manage an overall search of
a problem space. It keeps track of significant parameters such as the
memory model being used, the precise evaluation function for tree
nodes, and it provides supplemental support for register listeners.
You will not need to know much about the details of this class; it is
simply needed because you must pass an instance as a parameter when
creating a new search front.
The nested class _SearchFront has been designed to model the
expansion of a search tree (or graph) during a search of the problem
space. It has all of the low-level code already for doing things such
as expanding nodes and adding neighboring actions to the frontier. It
also can be configured to do either a forward or a backward search.
You need not go through all of the code for that class. But you will
need to know about the following methods that it supports:
-
The constructor takes parameters:
- solver
a reference to a GenericSolver instance, used for
retrieving parameter settings and for announcing key
events to the listeners.
- limit
Can be used to place a maximum limit on the depth/cost
to be considered by a search front. This is used to
support iterative deepening, but for this assignment you should
always set it to None.
- forward
True (the default) designates forward reasoning.
False designates backward reasoning.
-
empty determines whether the current frontier is empty
-
__len__ determines the number of nodes currently on the frontier
-
peek returns the node that is currently at the front of
the frontier's priority queue. This does not remove
that node nor expand it.
-
pop removes and returns the node that is currently at
the front of the frontier's priority queue (without expansion).
-
expandOnce pops a node from the frontier and formally
expands it, causing any of its neighbors to be considered for
inclusion on the frontier.
-
getNode(state) is a function that determines whether
the search front currently has a node corresponding to a given
state. If so, it returns a reference to the TreeNode
instance (see below). Otherwise it returns None. Please note
that the node may either be on the frontier or already explored.
-
getFrontierNode(state). This is similar to
getNode, however it only returns the node when it is a
frontier node.
-
getExploredNode(state). Akin to
getFrontierNode but only returning a node when it is explored.
Finally, nested within GenericSolver._SearchFront is a
TreeNode class. Each instance represents a single node in a
particular search front. Of particular interest, that class supports:
-
getState returns a Problem.State instance
designating the state associated with this node.
-
getActon returns the action that would lead to
(resp. from) this state.
-
getDepth returns the number of steps from the initial
(resp. goal) state to the given node's state.
-
getPathCost returns the path cost to this node (rather
than the depth)
-
getEstimate returns the heuristic estimate from this
node's state to the goal (resp. initial) state.
-
getValue returns the actual value being used by a node
within the priority queue. This depends upon the search style
(e.g., bfs, cost, astar)
-
computeFullPath(forward) is a utility function that can be used
to reconstruct the entire path from the root of a tree to the
given node. More specifically this produces a list of
(action, resultingState) pairs, including for a forward search
front an initial pair of the form (None, initialState). Note
that in the case of a reverse search, designated with
forward=False, The resulting list is oriented so that the
sequence of actions is portrayed in the forward direction.
Bidirectional Algorithm
Our stubbed version of the solve function in
bidirectional.py demonstrates how two separate search fronts
can be instantiated, and it contains a loop showing how to expand a
single node from a single front at a time, alternating moves between
the two fronts. However, our initial implementation has an infinite
loop.
The key to completing this assignment is developing a better rule
for terminating the loop, while still guaranteeing that a solution
that is returned is optimal. To ensure proper
termination, you must maintain a list of all nodes that are known to
be explored on one front and either explored or on the frontier of the
opposing front. The proper rules for this process depend
upon which type of search is being performed. We summarize the correct
algorithm as follows.
When about to explore the node currently at the front of a search
front, the following rules should be used.
-
If the state is neither explored nor on the frontier of the
opposing front, then expandOnce should be called to
perform a standard expansion.
-
If the state is found to be on the frontier of the opposing
front, then the expansion of the given node causes the creation
of a new "explored/frontier" combination. The node which is
being considered as explored should be added to a list of
candidate nodes that may result in the optimal path. However,
the search should continue and expandOnce should be
called to perform expansion on this node
-
If the state for the node that is about to be explored is found
to be already explored on the opposing frontier, this results in
a new "explored/explored" combination. This state should be
added to the list of candidate states for finding an optimal
solution.
In this case, the node should not be formally expanded,
as there is no need to further explore the paths crossing the
frontiers at this point.
In the case of a BFS or cost search, the overall search can be
stopped at this point. In the case of A*, it may be necessary
to continue with the next expansion from a search front.
The final termination condition is as follows. For BFS or cost, the
loop can be terminated as soon as an "explored/explored" pair occurs.
However, that state is not necessarily used in the optimal solution.
Instead, for that state and all earlier designated explored/frontier
states, a computation should be performed to determine the total path
cost of going from the initial to goal state through this state. The
lowest of all such path costs should be reported as the solution.
In the case of A*, it is not sufficient to stop the loop when finding
the first explored/explored combination. Instead, it is necessary to
continue considering items from a search front until considering an
items whose "value" (pathCost + estimate) is greater than or equal to
the best known path cost.
Finally, we note that our initial implementation alternately expands a
node from each front. However, we wish to have you always do the
following. If you consider the assigned value for the two nodes at
the front of the respective search fronts, we want you to choose to
explore at the one that has smaller value.
Determining the frontier metric (e.g., bfs, cost, astar)
The logic for a correct bidirectional search depends upon the frontier
style, but the function signature for the search does not explicitly
state which style of search is being performed. That said, you can
determine it as follows. Each TreeNode instance supports
methods getDepth, getPathCost, getEstimate,
and getValue. It also has a member _discover which
is a unique ID designating the order in which nodes were created.
For a given node, the relationship between
those values can be used to determine the search style. In
particular, for BFS the value is simply the _discover field
(as this gets us FIFO order). For cost, the
value is precisely the path cost. For A*, the value is the sum of the
pathcost and estimate.
If implemented properly, a bidirectional search should produce a
solution that has the same quality as that produced by a
unidirectional search. So for particular examples, you should be able
to run a forward reasoning version and compare that solution to the
one produces by your bidirectional implementation.
Automated
Although you will want to examine individual cases with
demos/shortestpath/euclidean.py, we are providing you with a
test harness in the form of demos/shortestpath/test.py.
This script allows you to do perform an arbitrary number of random
trials, where for each situation it runs the forward, backward, and
bidirectional form of a search, for each of the bfs, cost, and astar
formations. The test.py allows for similar command line
arguments to control the parameters for generating random instances.
Furthermore, when running multiple trials, it echos a seed that was
used for reach trial, so that you can repeat a single example by using
that seed with the original euclidean.py program.
Detailed Traces
When looking at individual examples, we remind you that our solver
allows for the option -t all to provide a verbose textual trace of the
entire process. You may want to redirect that output to a file so
that you can later analyze the progress of your searches on an example.
Pen and Paper
There are many subtle cases to consider in correctly implementing a
bidirectional search. The only way I was able to find some bugs was
to sit down with the detailed trace data and a piece of graph paper,
and to draw the relevant portion of the geometry.
Note: You may click on any of these images to see a larger version.
BFS with seed=4
Optimal Solution: 18 edges
|
forward
|
backward
|
bidirectional
|
|
|
|
|
explored nodes
|
598
|
860
|
448
|
Uniform-cost with seed=4
Optimal Path-cost: 1347.85
|
forward
|
backward
|
bidirectional
|
|
|
|
|
explored nodes
|
541
|
816
|
488
|
A* with seed=4
Optimal Path-cost: 1347.85
|
forward
|
backward
|
bidirectional
|
|
|
|
|
explored nodes
|
92
|
81
|
146
|
BFS with seed=4, wall=0.6
Optimal Solution: 24 edges
|
forward
|
backward
|
bidirectional
|
|
|
|
|
explored nodes
|
749
|
802
|
630
|
Uniform-cost with seed=4, wall=0.6
Optimal Path-cost: 1912.42
|
forward
|
backward
|
bidirectional
|
|
|
|
|
explored nodes
|
765
|
827
|
649
|
A* with seed=4, wall=0.6
Optimal Path-cost: 1912.42
|
forward
|
backward
|
bidirectional
|
|
|
|
|
explored nodes
|
354
|
220
|
450
|
All of your code should be placed into the file
bidirectional.py that we have stubbed. Do not modify any
other files from our project.
You should also submit a separate 'readme' text file. If you worked
as a pair, please make this clear and briefly describe the
contributions of each person in the effort.
Please see details regarding the submission
process from the general programming web page, as well as a
discussion of the late policy.
The assignment is worth 50 points.
Michael Goldwasser
CSCI 462, Spring 2010
Last modified: Monday, 22 February 2010
Course Home |
Assignments |
Schedule |
Submit