CS 188, Spring 2005, Introduction to Artificial Intelligence
Assignment 2, due Feb 24, total value 8% of grade




This assignment should be done in pairs. 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 AIMA code on which it builds.

100 points total




In this assignment, you will implement two basic versions (plus variants) of a system for designing model railway tracks: The assignment is somewhat open-ended, in that the underlying goal is to produce the most useful track-designing tool you can. 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). 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 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/tile-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:

> (setq p (make-tile-puzzle :n 3))
#<a TILE-PUZZLE>
> (problem-initial-state p)
 . 5 3
 1 6 2
 8 4 7
Then, one invokes one of the search algorithms to solve it. Here, we apply A* graph search. This returns a node, so we also look at the solution sequence:
> (setq n (a*-graph-search p))
#<NODE f(20) = g(20) + h(0) state:
 . 1 2
 3 4 5
 6 7 8
> (solution-actions n)
(DOWN RIGHT RIGHT UP LEFT DOWN RIGHT DOWN LEFT LEFT ...)

Railway tracks

The code for defining and displaying tracks is in the following files: These files will 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 defstructure 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 includes 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 search/domains/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/code-188/search/domains/random44.track")
NIL
> (setq random44copy (read-track "~cs188/code-188/search/domains/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/code-188/search/domains/weak44.track")))

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

Your mission

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.

Now go through the following steps:

1. (10) 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. (5) 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. Measure the run time as a function of track size, up to the largest track you can construct in about a minute. You may find it helpful to display 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. (35) Now, formulate and solve the no-loose-ends, unlimited track design problem as a CSP. (For now, ignore WCCs.) Do this by the following two methods:

7. (5) What difficulties might arise in the reduction to enumerated CSPs for the track design problem where solutions must have exactly one WCC, or where there is a fixed supply of track as in Q.5? How might you overcome these difficulties?

8. (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 6), 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/code-188/search/domains/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.

9. (Not part of the assignment, but 199 credit is available; due last day of classes) 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.