CS 188, Fall 2005, Introduction to Artificial Intelligence
Assignment 2, due Oct 9, total value 8% of grade




This assignment should be done in pairs. Working in pairs does not mean that each does half. It means both work together on the whole thing! Don't leave it to the last minute! The total amount of coding required is relatively small but it may take some time to understand the code on which it builds.

100 points total


When you are done with the assignment, you should use submit a2 to turn it in. This will pick up two files: a writeup (a2.txt or a2.pdf) explaining what you did for each of the following steps (and incorporating transcripts as needed), and an a2.lisp file with all your code.


In this assignment, you will work on two tasks: The assignment is somewhat open-ended, in that the underlying goal is to produce the most useful track-designing tool and sudoku solver. Extra credit will be available for creative extensions of the assigned tasks.

Solving problems using the AIMA code

The first thing you need to do is load all the AIMA code in the usual way: load aima.lisp and then do (aima-load 'search). Be sure to use the current version; there have been several recent additions, changes, and bug fixes. The AIMA code includes a general facility for defining search problems. You should look in particular at the following files in the AIMA code directory: These files differ from the standard WWW AIMA code distribution, so make sure you use the latest CS188 versions, either from the class web page or the ~cs188 directory on the instructional machines.

Note in particular the default definition (in search/domains/problems.lisp) of the successors method in terms of an actions method, which returns all possible actions in a given state, and a result method, which computes the new state resulting from a given action in a given state. (Examples of their use appear in search/domains/nqueens-problem.lisp and search/domains/sliding-block-puzzle.lisp.) For local search algorithms, the random-successor method, defined in terms of a random-action method, is also useful. To construct and solve a problem, once the methods for a particular problem type are defined, one first makes a problem instance. Here, we make an 8-puzzle (3x3) and look at its initial state:

> (problem-initial-state (setq p (make-sliding-block-puzzle :n 3)))
 6 3 5
 7 2 1
 4 8 .
Then, one invokes one of the search algorithms to solve it. Here, we apply A* graph search. This returns the solution sequence:
> (pprint (setq soln (a*-graph-search p)))
(LEFT LEFT UP UP RIGHT DOWN RIGHT UP LEFT DOWN DOWN LEFT UP UP)
Just to be sure, we can look at the outcome of this sequence:
> (sequence-result p soln (problem-initial-state p))

 . 1 2
 3 4 5
 6 7 8
There are many more examples of how to solve problems in search/test-search.lisp.

Railway tracks

The code for defining and displaying tracks is in the following files: I suggest copying these files into your own directory. You may want to add them to the defsystem for search in your aima.lisp so that they compile and load automatically when you run (aima-compile 'search).

Track pieces, orientations, and connections

In this assignment, railway tracks are composed of square tiles that carry the track elements. There is a defstruct for the general type track-piece, and there are seven subtypes of usable pieces. Their ASCII appearances (with asterisks added for visual separation) are as follows:
   cross     curve   straight  twocurve    blank    lsplit    rsplit
*    |    *    |    *    |    *    |    *         *    |    *    |    *
*    |    *     \   *    |    *     \   *         *    |    *    |    *
*----|----*      `--*    |    *--.   `--*         *--. |    *    | ,--*
*    |    *         *    |    *   \     *         *   \|    *    |/   *
*    |    *         *    |    *    |    *         *    |    *    |    *
In addition, there is a barrier-piece subtype that surrounds the track (see below) and an unknown-piece subtype for parts of the track that are not yet defined.

Any track piece p is an instance of one of these subtypes. As such, it has an orientation (track-piece-orientation p) which is an integer. Each orientation gives the piece a different appearance; the appearances shown above are for orientation 0. In general, the possible orientations are 0, 1, 2, 3, each rotated 90 degrees clockwise from the previous. Because of symmetries, some pieces have fewer distinct orientations. For example, the blank and cross pieces just have orientation 0 and the straight, curve, and two-curve pieces have orientations 0 and 1. For any piece, the number of distinct orientations is given by (track-piece-d p). The track subtype definitions include appearances for each distinct orientation, but you need not worry about this because print-track takes care of them automatically (see below).

The appearance of a track piece is purely cosmetic (i.e., it's just for the purposes of printing out tracks so you can see them). What really matters is the set of connections, i.e., the ways in which the tracks connect the edges of the piece. The edges of any piece are called *top*, *right*, *bottom*, and *left*, which are defined as global parameters with values 0, 1, 2, 3 respectively. (Hence, you can enumerate over edges by looping from *top* to *left*.) Which edges are connected to which depends on the orientation of the piece. Consider, for example, the lsplit-piece, which is shown above in orientation 0:

Notice that the *left* edge is not connected directly to the *top* edge, because trains cannot turn sharp corners. (They can go into reverse, so there is an indirect connection.) Similarly, the *left* edge of a cross-piece is not connected to its *top* edge, and so on.

The connections for orientation 0 are recorded in the track-piece-connections field of each subtype. In general, you won't want to mess with this field directly. Instead, you will use the following access functions:

Notice that the barrier, blank, and unknown pieces have no connections. They have different functions, however: barriers go around the outside of the track; a blank piece is a real track piece with no track; an unknown piece simply indicates that no real track piece has been chosen for this square.

Tracks

A track is an array whose elements are track pieces. An MxN track is actually an (M+2)x(N+2) array whose outside squares are filled with barrier pieces. This way, you can enumerate over the "real" squares of a track by looping from 1 to M and 1 to N. Furthermore, every real square will have neighbours on all four sides.

The print-track function prints out a track as plain text characters. The display-track function displays a track in a separate window in a much nicer format. (See the track-display.lisp file for further instructions.)

IMPORTANT: As with all the arrays in AIMA, we use the Cartesian convention: (1,1) is the bottom left corner, (M,1) is the bottom right corner, (1,N) is the top left corner, (M,N) is the top right corner. This convention also applies when calculating, for example, the square adjacent to the top edge of another square.

IMPORTANT: As a general rule it is best to have each square of a track contain a distinct track piece instance. Putting the same instance in more than one square causes obvious problems when the piece orientation is modified!

The basic function for making a track is (make-full-track width height pieces), which makes a track with the given dimensions, adding barriers around the outside, and fills the squares from the list of pieces, in the given order. It returns the track (and a list of left-over pieces if any).

make-empty-track calls make-full-track with a list of unknown pieces:

> (print-track (make-empty-track 4 4))
  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
                                                      
         . . . . .. . . . .. . . . .. . . . .         
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
  |   |  . . . . .. . . . .. . . . .. . . . .  |   |  
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
         : : : : :: : : : :: : : : :: : : : :         
         . . . . .. . . . .. . . . .. . . . .         
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
  |   |  . . . . .. . . . .. . . . .. . . . .  |   |  
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
         : : : : :: : : : :: : : : :: : : : :         
         . . . . .. . . . .. . . . .. . . . .         
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
  |   |  . . . . .. . . . .. . . . .. . . . .  |   |  
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
         : : : : :: : : : :: : : : :: : : : :         
         . . . . .. . . . .. . . . .. . . . .         
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
  |   |  . . . . .. . . . .. . . . .. . . . .  |   |  
  -----  . . . . .. . . . .. . . . .. . . . .  -----  
         : : : : :: : : : :: : : : :: : : : :         
                                                      
  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  

make-random-track-unlimited calls make-full-track with a list of pieces generated randomly according to the *piece-probs* distribution (as if there were an unlimited supply of each piece type):

> (print-track (setq random44 (make-random-track-unlimited 4 4)))
  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
                                                      
             |                 |        |             
  -----      |                 |       /       -----  
  |   |      | ,----.------    |    --'   ,--  |   |  
  -----      |/      \         |         /     -----  
             |        |        |        |             
                      |        |        |             
  -----              /         |         \     -----  
  |   |  --.      --'   ,------|------.   `--  |   |  
  -----     \          /       |       \       -----  
             |        |        |        |             
                               |        |             
  -----                       /|         \     -----  
  |   |  --.      --.--------' |          `--  |   |  
  -----     \        \         |               -----  
             |        |        |                      
             |        |                 |             
  -----       \      /                   \     -----  
  |   |  ------`----'------------,--------`--  |   |  
  -----                         /              -----  
                               |                      
                                                      
  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
                                                      

One useful function for accessing track squares is (adjacent-square x y edge), which returns the square adjacent to the given edge of square (x,y) in the track. For example,

> (adjacent-square 1 1 *right*)
(2 1)
The function (adjacent-piece track x y edge) extracts the corresponding piece in the given track. Thus, if random44 is bound to the track above:
> (adjacent-piece random44 1 1 *right*)
#.(make 'LSPLIT-PIECE :orientation 1)
Notice the somewhat odd way we print out pieces: this makes it possible to print out a track to a file and read it back in again:
> (save-track random44 "~cs188/a2/random44.track")
NIL
> (setq random44copy (read-track "~cs188/a2/random44.track"))
#2A((#.(make 'BARRIER-PIECE :orientation 0)
     #.(make 'BARRIER-PIECE :orientation 0)
     etc., etc.

Well-formed tracks

The randomly generated track shown above is ill-formed: it has various disconnected stretches of track and many loose ends, some of which run off the edge. Define a weakly connected component (WCC) as a maximal set of track elements such that from every point in the WCC it is possible to reach every other point. (Notice that an individual twocurve-piece or cross-piece by itself has two WCCs.) For example, random44 above has 6 WCCs. (Not 5 - remember that tracks crossing at a cross-piece are not joined!) We would prefer tracks with no loose ends and just one WCC. Here is an example of such a track:
> (print-track (setq weak44 (read-track "~cs188/a2/weak44.track")))

  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
                                                      
                                                      
  -----                                        -----  
  |   |        ,----.            ,----.        |   |  
  -----       /      \          /      \       -----  
             |        |        |        |             
             |        |        |        |             
  -----       \      /|        |        |      -----  
  |   |        `----' |        |        |      |   |  
  -----               |        |        |      -----  
                      |        |        |             
                      |        |        |             
  -----               |\      /        /|      -----  
  |   |               | `----'   ,----' |      |   |  
  -----               |         /       |      -----  
                      |        |        |             
                      |        |        |             
  -----                \      /        /       -----  
  |   |                 `----'--------'        |   |  
  -----                                        -----  
                                                      
                                                      
  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
Later we will see a more stringent definition of strongly connected component.

Your mission

Now go through the following steps:

1. (5) First, to help you become familiar with tracks and pieces, write a function (count-loose-ends track), which should return the number of loose ends in the track. For random44 you should get 20; for weak44 you should get 0.

2. (2) If a track has no loose ends, what does that imply about the numbers of lsplit and rsplit pieces? Explain.

3. (20) Now, write a function (count-wccs track), which should return the number of WCCs in the track. For random44 you should get 6; for weak44 you should get 1. [Hint: your first instinct may be to view WCCs in terms of collections of squares; but this won't work because of the twocurve and cross pieces, each of which can be part of two distinct WCCs. Instead, consider a WCC as a set of edges. For an MxN track, there are MxNx4 edges. To find a WCC, start from any non-empty edge not already noted as belonging to a WCC, note it as belonging to a new WCC, then recursively explore and note all edges connected to it (including the adjacent edge of the neighboring square). Start with a small track for debugging purposes and print out useful debugging information as the exploration proceeds.]

4. (25) Now, formulate and solve the local search problem type, unlimited-track-local-problem, where the goal is to construct a track with as few loose ends and WCCs as possible. In such problems, we start with a randomly filled track and make changes to improve it, with no limitations on the pieces that can be used. You will need to define the actions or random-action method and the result, h-cost and goal-test methods. Then construct a suitable problem instance with a random initial state and apply a suitable algorithm from local-search.lisp. (Note: for most of the algorithms, you will need to play with the parameters, such as number of restarts, temperature schedule, etc., to get good results.) Measure the run time as a function of track size, up to the largest track you can construct in a reasonable amount of time (say, a minute or two). While experimenting with the algorithms, you may find it helpful to insert code that displays the current track at each iteration.

5. (10 extra credit, optional) Formulate and solve the fixed-track-local-problem, where the track must be constructed from a fixed set of pieces that exactly fills the track. (Hence, actions can rotate or swap pieces, but cannot introduce new ones.) The fixed set can be specified as an association list, e.g., for a 4x4 track one might specify

((blank-piece . 1) (cross-piece . 1) (straight-piece . 3) (curve-piece . 6) 
 (twocurve-piece . 1) (lsplit-piece . 2) (rsplit-piece . 2))
Experiment with various different track sizes and sets of pieces. Is this problem harder or easier than the unlimited problem? Why? Which kinds of pieces seem to cause difficulties?

6. (8) Explain (in words) what variables, domains, and constraints you would need to formulate the track design problem as a CSP. Do this for (a) the unlimited-track-local-problem, where there are no constraints on the pieces used, and (b) the fixed-track-local-problem, where the set of pieces is fixed.

7. (10 extra credit, optional) The notion of WCC doesn't quite capture the full requirements of running trains on tracks. In the weak44 track given earlier, a train running north at (2,2) runs into the loop in the upper left corner and never escapes unless it reverses. A simple circular track is even worse: a train running forwards in the clockwise direction can never run forwards in the anticlockwise direction unless it is picked up bodily and placed facing the other way. According to Gordon (age 7), a track is only good if the train can traverse every part of it in both directions without stopping or reversing. Here is an example of such a strongly connected track:

> (print-track (setq strong44 (read-track "~cs188/a2/strong44.track")))

  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
                                                      
                                                      
  -----                                        -----  
  |   |        ,----.------------,----.        |   |  
  -----       /      \          /      \       -----  
             |        |        |        |             
             |        |        |        |             
  -----      |\       |        |        |      -----  
  |   |      | `----. |        |        |      |   |  
  -----      |       \|        |        |      -----  
             |        |        |        |             
             |        |        |        |             
  -----      |        |\      /        /|      -----  
  |   |      |        | `----'   ,----' |      |   |  
  -----      |        |         /       |      -----  
             |        |        |        |             
             |        |        |        |             
  -----       \        \      /        /       -----  
  |   |        `--------`----'--------'        |   |  
  -----                                        -----  
                                                      
                                                      
  -----    -----    -----    -----    -----    -----  
  |   |    |   |    |   |    |   |    |   |    |   |  
  -----    -----    -----    -----    -----    -----  
Write a predicate (strongly-connected? track), which should return t iff the track is strongly connected, and use it with any of your track design methods to make some large, strongly connected tracks.

8. (Not part of the assignment, but 199 credit is available; due last day of classes. You might be able to make a startup company out of this.) Tracks made from square tiles are hardly the real thing. Look into the problems involved in designing real Brio and/or Scalextric tracks. Propose a solution approach, implement and test the approach, and do a short write-up.


Formulating and solving CSPs

For the next part of the assignment, we will be using the AIMA CSP code: In reading the code, you will see two basic ways to describe constraints. First, a constraint may be defined as an enumerated-constraint, in which case the allowed tuples of values for the variables are listed explicitly and a generic "allowed?" method used to check any particular set of values for consistency (e.g., in australia-csp.lisp). Second, a constraint may be defined by an "allowed?" method for that type of constraint that checks consistency using lisp code (e.g., neq-constraints in nqueens-csp.lisp).

To solve a CSP, first make one and then apply backtracking to it:

> (setq p (make-nqueens-csp :n 8))
#S(NQUEENS-CSP :VARIABLES (1 2 3 4 5 6 7 8)
               :DOMAINS
               ((1 1 2 3 4 5 6 7 8) (2 1 2 3 4 5 6 7 8)
                (3 1 2 3 4 5 6 7 8) (4 1 2 3 4 5 6 7 8)
                (5 1 2 3 4 5 6 7 8) (6 1 2 3 4 5 6 7 8)
                (7 1 2 3 4 5 6 7 8) (8 1 2 3 4 5 6 7 8))
               :CONSTRAINTS
               (#S(NQUEENS-CONSTRAINT :VARIABLES #)
                #S(NQUEENS-CONSTRAINT :VARIABLES #)
                #S(NQUEENS-CONSTRAINT :VARIABLES #) #S(...) ...)
               :N 8)
> (print-nqueens-assignment (backtracking-search p))

. . Q . . . . . 
. . . . . Q . . 
. . . Q . . . . 
. Q . . . . . . 
. . . . . . . Q 
. . . . Q . . . 
. . . . . . Q . 
Q . . . . . . . 
((8 . 4) (7 . 2) (6 . 7) (5 . 3) (4 . 6) (3 . 8) (2 . 5) (1 . 1))
You can get the backtracking algorithm to use variable and value ordering heuristics by supplying keyword arguments:
> (backtracking-search p :select-unassigned-variable #'minimum-remaining-values)
((8 . 4) (6 . 7) (5 . 3) (7 . 2) (4 . 6) (3 . 8) (2 . 5) (1 . 1))

Sudoku

Sudoku is a fairly old puzzle that is now a worldwide phenomenon. You can type "sudoku" into Google, or go to www.sudoku.com for the basics (see "How to Solve") and the Wikipedia article to get more information than you could possibly imagine. Key facts about standard Sudoku puzzles:

In addition to the rules, many web sites offer extensive discussion of methods that humans can use to solve Sudoku without using trial-and-error search.

We supply some useful code and examples:

Your mission

Now go through the following steps:

9. (10) Write code defining Sudoku as a CSP, such that the call (make-sudoku-csp) returns a Sudoku CSP for the case where no initial digits are supplied. Your CSP should have (9+9+9)*(9*8/2) = 972 binary constraints. Notice that the initial-state constructor initial-csp-state lets you supply an initial assignment. This is by far the easiest way to handle the initial digits that define any particular Sudoku puzzle. For example, you will be able to do the following:

> (setq p (make-sudoku-csp))
[no peeking!!]
> (print-sudoku (backtracking-search p (initial-csp-state p *easy-sudoku-01*)))
+=======================+
| 6 7 1 | 4 5 3 | 8 9 2 |
| 4 5 8 | 7 9 2 | 1 6 3 |
| 2 9 3 | 6 1 8 | 5 4 7 |
| ===================== |
| 9 3 5 | 2 8 4 | 7 1 6 |
| 8 4 6 | 1 7 5 | 2 3 9 |
| 1 2 7 | 3 6 9 | 4 5 8 |
| ===================== |
| 3 1 9 | 5 2 7 | 6 8 4 |
| 7 6 4 | 8 3 1 | 9 2 5 |
| 5 8 2 | 9 4 6 | 3 7 1 |
+=======================+
(((9 9) . 2) ((9 8) . 3) ((9 6) . 6) ((9 5) . 9) ((9 2) . 5)
 ((9 1) . 1) ((8 9) . 9) ((8 7) . 4) ((8 4) . 5) ((8 3) . 8) ...)

You will need to decide what the variables should be, what their domains are, and what the constraints are. The code in nqueens-csp.lisp is a good guide for what's needed. Apply plain backtracking to your CSP and show that it works.

10. (5) Now instrument backtracking-search so that it counts the total number of guesses made. Whenever the backtracking algorithm prepares to loop through a list of k>0 possible values, we will say that k-1 guesses are made. (This way, the algorithm is still charged for guessing even if it is lucky and its first value choice succeeds.) Show the numbers of guesses made for each of the 40 instances in sudoku-puzzles.lisp (use *all-sudokus*), using plain backtracking and backtracking with MRV.

11. (25) We would like our solver never to resort to guessing. For this to happen, we will need better inference methods. In this context, an inference method is a function that examines the current state of the CSP and deduces additional facts about the remaining unassigned variables -- either ruling out one or more values for a variable or asserting a particular value. The method can then modify the current state accordingly. Notice that every time a method modifies the current state, other methods (or indeed the method itself) may become applicable again. A waterfall is a set of methods that is applied repeatedly, until all the methods fail to do anything useful.

Modify backtracking-search so that it applies a waterfall to modify the current state right after the assignment-complete? check. (Be sure to have the program keep track of what the waterfall does so that it can be undone!)

Initially, your waterfall list contains no inference methods, so nothing will change. Now, find some Sudoku inference methods on the web (ones written in English, of course, not programs!). For each, describe how it works with an example; explain whether it is already covered by the standard operation of keeping track of legal values as in delete-inconsistent-values and using MRV; and, if it is not covered, implement it and add it to your waterfall. After each addition, check the number of guesses for each of the 40 puzzles. Ideally, you will be able to get the numbers to zero for all puzzles. If not, it may be possible to define your own inference methods by examining the current state at each point where a guess is made, to see what a smart human would do at that point (if one happens to be available).

12. (10 extra credit, optional) Design, implement, and test a Sudoku puzzle generator. For extra fun, ho ho, pass the puzzles to your friends to see if their solvers can solve them without guessing.