Course Home | Assignments | Schedule | Submit

Saint Louis University

Computer Science 462
Artificial Intelligence

Michael Goldwasser

Spring 2010

Dept. of Math & Computer Science

Assignment 03

Bidirectional Searches


Contents:


Collaboration Policy

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.


Details

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:

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:

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.

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.

Testing

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.

Examples

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

Submitting Your Assignment

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.


Grading Standards

The assignment is worth 50 points.


Michael Goldwasser
CSCI 462, Spring 2010
Last modified: Monday, 22 February 2010
Course Home | Assignments | Schedule | Submit