Assignment 2: State-Space Search
CSE 415: Introduction to Artificial Intelligence
The University of Washington, Seattle, Autumn 2023
Due Monday, October 16, at 23:59 PM (but note early-bird bonus option, due Wednesday, October 11).
 
Introduction

This assignment covers state-space search in single-agent, deterministic environments. Later we will consider multi-agent search in environments with stochastic dynamics. We start by introducing a framework for the classical theory of problem solving. Then we examine heuristic search. Note that this assignment has two parts. Part 1 is an "individual work" sub-assignment, and must not be done in partnerships or groups. (When the graders find evidence of academic misconduct such as plagiarism, these cases are reported to the administration. Please do not copy, collaborate, or otherwise allow this to happen.) Part 2 is a "collaborative work" sub-assignment, in which you are encouraged to work with a single partner, i.e., in a team of two people. If you have a strong desire to be in a group of three, please email the lead TA (Teerapat) with a request and a good reason for the request, giving the names of all three proposed team members.

Along with various Python files, you'll be submitting two separate report files, one called BlindSearches.pdf (that you create in Part 1) and one called HeuristicSearches.pdf (that you create in Part 2).

Part 1 (Individual work)

Each student should do Part 1 without collaboration. This part is about basic problem formulation and basic search algorithms.

Although both Part 1 and Part 2 are due on Monday, October 16, there is an option to submit your solution for Part 1 early and receive a 5-point bonus. To get the bonus, you must do two things: submit your Part 1 files by 11:59 PM on Wednesday, October 11, to GradeScope, and you must submit the name of your partner for Part 2. If you do submit early, you retain the right to resubmit Part 1 later, but the bonus will not accrue in that case. Also, to get the bonus in Part 1, you have to have a partner in Part 2, name the partner in your early-bird submission, and follow through with the partner in Part 2.

1. First, download the starter code for this assignment. Unzip it with a utility such as Windows Compressed Folders, etc.

2. Examine each of the two problem formulation files provided. One is the Humans, Robots and Ferry problem. The other is the Towers of Hanoi problem. An additional file is provided, Farmer_Fox.py, and you will be writing one new problem formulation in that file.

In the given formulation files, pay particular attention to how the State class is defined and how the operators are established. We will be talking about this code in class.

3. An interactive solving client is provided. This is implemented in the file Int_Solv_Client.py. Optional but recommended: Try running the client with Towers of Hanoi by using the following command in a Linux, Darwin, or Cygwin command shell. To do this, issue the command:
python3 Int_Solv_Client.py TowersOfHanoi 3
If you are not comfortable working in Linux, Darwin, or Cygwin, you may wish to simply create a working folder with the Interactive Solving Client in it, and all the problem formulation files in it, and edit the importing code of the Interactive Solving Client so that it simply imports the problem formulation of your choice, and then run the program from IDLE, PyCharm, or other IDE you might be using.
4. Create your own problem formulation for the "Farmer, Fox, Chicken, and Grain" problem covered in class. Then produce a session transcript for your formulation. This transcript will be in the form of a log file. You can produce it by running the starter-code program ItrDFS.py and capturing the output and directing it to a file. The file should be named FFCG-log.txt. For your operators, implement the set of operators given in the solution to the worksheet (posted in ED) on the Farmer, Fox, Chicken and Grain problem. Define the operators in the same order that they appear in the presentation of the set of operators in the given solution. That will help ensure that solutions found by algorithms will match the expected solutions.
5. Using parts of the starter-code file ItrDFS.py, which implements a loop-based depth-first search method ("DFS" in the following), implement BFS (Breadth-First Search) as it is specified in the lecture slides. Put your implementation of BFS into the starter-template file ItrBFS.py. Make sure that these implementations keep track of "predecessor" links (also known as back links), and they can report a shortest path from start to goal, for whatever problem they are applied to. (You can implement these links using a hash table, i.e., dictionary, that maps each state other than the initial state to its parent state. The initial state should be mapped to a special value such as None, or -1, which is up to you.)
6. Compare DFS and BFS on the following problems: (i) Humans, Robots and Ferry, (ii) Farmer, Fox, Chicken, and Grain, and (iii) 4-Disk Towers of Hanoi. For each combination of algorithm and problem, report the following: (a) the path found from start to goal, (b) the length of the path, (c) the number of nodes expanded (i.e., removed from OPEN and had successors generated). Create a file BlindSearches.pdf that contains a table showing these results in a clear, easy-to-read manner. Your Towers-of-Hanoi paths can be shown outside the table, since they can be relatively long. This file can be created with Microsoft Word, Google Docs, or similar tool. Finally, just for the Towers-of-Hanoi problem with 4 disks, explain (i) why the maximum length of the OPEN list is more for one algorithm than the other, and (ii) why the solution PATH length is different for one algorithm from that of the other.
Part 2 (Collaborative work) In this part, you are encouraged to work with a partner in the class.

7. The starter-code file UCS.py, provides an implemention of the Uniform-Cost Search algorithm. This file also provides you with an implementation of a special kind of priority queue which supports not only the standard insert and delete_min operations, but also a method called contains, which returns True if a given element is in the priority queue. It also allows accessing and updating the priority value of any element in the priority queue. See the documentation for more information. This priority queue is not just adequate for implementing Uniform-Cost Search, but is particularly helpful in implementing the A* Search algorithm.
The main difference between Breadth-First Search and Uniform-Cost Search (UCS) is that UCS works with problem formulations that have different costs associated with different moves. In terms of graph search, there are edge costs, and they are not necessarily assumed to be all equal to 1 as they essentially are in Breadth-First Search.

The starter code includes a new problem formulation for you to use with UCS; it's the French Cities network in which the problem is to find a shortest route from the initial-state city to the goal-state city. These are, by default, RENNES and AVIGNON. (These could easily be changed by editing the formulation file's line defining STARTING_CITY, etc.) This formulation is in the file FranceWithCosts.py.
The State class in this formulation file has a method edge_distance(s2), which is used to get the edge cost from the current state to s2. You'll be setting up the UCS.py program to use these cost values to determine the cost of the lowest-cost path from the starting state to any newly reached state.
When the UCS algorithm reaches a goal state, it produces a backtrace of "backlinks" (sometimes called predecessor links) to represent the actual lowest-cost path from the start state to the goal state found. The file UCS.py includes a method runUCS.

For the default initial state and goal state, the results are

['Rennes', 'Nantes', 'Limoges', 'Lyon', 'Avignon']
for the self.PATH variable and
1041.0
for the self.TOTAL_COST variable.

Page 184 in the reading shows the state space for this problem, with the distances on the graph edges. When you run the code you should get a solution path of 4 edges (5 cities) and total distance 1041: [’Rennes’, ’Nantes’, ’Limoges’, ’Lyon’, ’Avignon’]. It might be formatted differently in the printout. The map is also shown below:

Map showing French cities (which serve as the states of this problem) and distance values on roads (represented as graph edges) between them.

Implement an A* search program, putting your code into the starting template file AStar.py. You may wish to cut and paste from the UCS.py file, since most of the implementation will be identical.

Develop your A* program to work with the heuristic function defined in the file FranceWithDXHeuristic.py. Your program will be operated as follows:

python3 AStar.py FranceWithDXHeuristic
When it is working, it should find the same path that UCS does, but it should have only expanded 12 states instead of 15.

Here are some hints about how to modify UCS.py to implement A*. Change the names of the file and the algorithm in the code and the comments. After the code that imports the Problem, assign self.h = Problem.h, so that you can call the heuristic function easily in your algorithm. Within the search algorithm, any time UCS uses gs or new_g as a priority value, you should compute f (or call it fs or new_f, depending on the context), and use the f value as the priority.

When checking the CLOSED list, don't necessarily delete the new state if the same city is already on CLOSED, but look at the priority, and if the new state has a lowever priority value, delete the state on CLOSED and put the new one on OPEN.

Make sure that the first five CAPITALIZED variables in class AStar get assigned the values they need (as described in the corresponding comments), so that you can receive credit on autograder tests. These include self.COUNT through self.TOTAL_COST. The solution path should be set as the value of self.PATH, and it must be a list of strings, such as ['Rennes', 'Nantes', 'Limoges', 'Lyon', 'Avignon']. Also, be sure to assign proper values in the dictionary for self.g.

8. The Eight Puzzle is a simplified version of the popular Fifteen Puzzle. A photo of one commercial model of the Fifteen Puzzle is given below on the left, and a picture of the Eight Puzzle is on the right. (For image credits, see the acknowledgments at the bottom of this page.)

       

Try to solve some instances of the Eight Puzzle using UCS.py. There is a default instance you should try first. You can do it with the command line:

python3 UCS.py EightPuzzle
There is an easy way to try more instances. Simply provide a string on the command line that represents the initial state in list form.
python3 UCS.py EightPuzzle '[[3, 1, 2], [4, 5, 0], [6, 7, 8]]'
Another way is to create a file that first imports EightPuzzle's definitions and then overwrites the definition of CREATE_INITIAL_STATE. This is illustrated in the file puzzle_rotate_2.py. To run UCS.py on such a puzzle, type this:
python3 UCS.py puzzle_rotate_2
This instance of the Eight Puzzle is named this way, because the solution requires moving all the outside pieces "around" the middle square a distance of two steps each. The lowest-cost path for this instance should have total cost 14.0, since there are seven tiles that move, and they each move twice. Note, if you prefer to do your testing by creating extra files like puzzle_rotate_2, DO NOT turn those files in with your solutions. Just report the results you get as part of your report file HeuristicSearches.pdf.

Example puzzles to try:

# Puzzle A. (should be very fast)
python3 UCS.py EightPuzzle '[[3,0,1],[6,4,2],[7,8,5]]'

# Puzzle B. (should not take long)
python3 UCS.py EightPuzzle '[[3,1,2],[6,8,7],[5,4,0]]'

# Puzzle C. (May take a few minutes)
python3 UCS.py EightPuzzle '[[4,5,0],[1,2,8],[3,7,6]]'

# Puzzle D. (May take several minutes)
python3 UCS.py EightPuzzle '[[0,8,2],[1,7,4],[3,6,5]]'
9. Implement the following two pre-defined (i.e., fairly standard) heuristics for the Eight Puzzle. (a) Hamming Distance (count the number of tiles out of place, but not the blank, in order to maintain admissibility), and (b) Total of Manhattan distances for the 8 tiles. Each of these should be in a separate file. If you look at the starter code file FranceWithDXHeuristic.py, it shows you how your file should import the basic problem formulation module (EightPuzzle) and then define the heuristic function h. When you are ready to test your Hamming distance heuristic, for example, you will type something like this to test it out.
# Test with Puzzle A:
python3 AStar.py EightPuzzleWithHamming '[[3,0,1],[6,4,2],[7,8,5]]'
If your A* algorithm and heuristics are working correctly, then you should see some rather noticeable improvements in the search speed and statistics.
10. Comparing Heuristics and Writing the Report: Test these heuristics on the four previously given instances of the Eight Puzzle, and record the results. Also, compare them with "no heuristic" (straight Uniform Cost Search).

You'll be turning in a report for this part of the assignment in addition to your code (see below). Create a section of your report called "Heuristics for the Eight Puzzle". Compare the performance of these heuristics on each of the example puzzles (given above). For each puzzle-heuristic pair, report (a) whether the puzzle was successfully solved, (b) length of the solution path found, in number of edges, (c) total cost of the path found, (d) number of states expanded, and (d) MAX_OPEN_LENGTH. Put this information into the table, described below.

The table should have a row for each puzzle instance and heuristic (e.g., "(Puzzle A, Hamming)", and columns for the following: puzzle instance permutation (e.g., [3,0,1,6,4,2,7,8,5]; this flattened list is OK here), success (yes/no), count of expanded nodes, aborted (yes/no). If it takes more than 5 minutes to solve a particular problem with a particular heuristic then note that; you may abort such runs. You are welcome to use the format for your report: shown in the starter-code file named HeuristicSearches-template.txt.

Keep in Mind

Here are some ideas to keep in mind for this assignment. The problem format we are using not only permits the "manual solving" that you do with the Interactive Solving Client. It also permits automatic solving, such as with DFS, and BFS. Thus it is important that you follow the problem structure correctly. Certain methods are required in the State class, including __init__, __eq__, __hash__, __str__, and copy. The operators must have the correct structure, as well. The nice thing about the Interactive Solving Client is that it provides a pretty good means to test whether your formulation will be OK.

Even if you are familiar with Python, there may be some constructs in the formulation files that you are not familiar with. You can read up on list comprehensions (p.31 of the first reading) and lambda expressions (pp. 47-52 of that reading). We'll also cover them in class.

Parts of your code will be graded by an autograder. To do this, your files will be imported as modules by the autograder. Your search should not automatically start running when the module is imported. The starter template file AStar.py comes with some code at the end of the file that takes care of this. This code is: "if __name__ == '__main__' " which avoids running the search automatically if it is being imported, but still runs it automatically if it is the main program. The autograder will be comparing the solutions and statistics found by your code with expected solutions, by calling functions in your code and examining the values of certain variables in your code. These variables are generally already in place for you in the starter code UCS.py. So you should not rename variables like self.PATH or any of the other variables written with all CAPITAL letters.

Commenting your Code

Each of your Python files should begin with a multiline string (more details are shown below.)

Follow reasonable commenting practice in your code. The comments in the starter code can serve as examples of the commenting style that we consider appropriate.

What to Turn In

For Part 1, turn in the following files to GradeScope:

partnership-plan.txt
Farmer_Fox.py
FFCG-log.txt
ItrBFS.py
BlindSearches.pdf
Here partnership-plan.txt will tell us whom you are working
with in Part 2.  Each partner in a planned team needs to provide this information
along with an early submission of Part 1 files in order to qualify for an early-bird bonus.

For Part 2, turn in the following files to GradeScope:

partnership-retrospective.txt
AStar.py
EightPuzzleWithHamming.py
EightPuzzleWithManhattan.py
HeuristicSearches.pdf
Here partnership-retrospective.txt will contain (a) the names of the people in your team, and (b) how you divided up the work in Part 2.

Each of the Python files should start with a multiline comment in the following format with your own information rather than the sample information given here.

'''Farmer_Fox.py
by John Nathan Smith
UWNetID: jnsmith98
Student number: 9876543

Assignment 2, Part 1, in CSE 415, Autumn 2023.
 
This file contains my problem formulation for the problem of
the Farmer, Fox, Chicken, and Grain.
'''

The turn-in method is via file uploads to GradeScope. Be sure to upload your Part 1 files to the A2-Part-1 assignment, and upload your Part 2 files to the A2-Part-2 assignment. We expect to grade this assignment semi-automatically, and having files in the wrong places might lead to unnecessary losses in points.

Updates and Corrections
 

If needed, updates and corrections will be posted here, and/or in ED.