Due: Friday, February 22nd by 11:59 PM

Conway's Game of Life

Your task is to implement sequential and parallel programs to simulate Conway's Game of Life.

Getting started

Download CS365_Assign2.zip on the cluster head node and unzip it.

Copy the files grid.h and grid.c from your implementation of Assignment 1 into the CS365_Assign2 directory.

The rules

In Conway's game of life, each cell value in the next generation is computed based on how many neighbors the cell had in the previous generation, and whether or not the cell was alive in the previous generation.

If a cell is alive in the current generation, it will be alive in the next generation if and only if it has either 2 or 3 neighbors in the current generation.

If a cell is dead in the current generation, it will be alive in the next generation if any only if it has exactly 3 neighbors in the current generation.

life.h, life.c

The header and source files life.h and life.c define several functions that can be used in an implementation (sequential or parallel) of Conway's game of life.

Here are the function prototypes as defined in life.h. The commend for each function describes how it should work:

//
// Load the game board from given file handle.
// The file consists of a series of integers.
// The first two integers specify the number of rows
// and columns, respectively.
// The remaining integers specify cell values (either 0 or 1)
// for all of the cells on the game board, in row-major order.
//
// The Grid returned by the function should have the contents of
// the file as the current generation.
//
Grid *life_load_board(FILE *fp, const char *filename);

//
// In given Grid, compute the next generation based on the
// current generation.
//
void life_compute_next_gen(Grid *grid);

//
// Save the contents of the current generation of the given Grid
// by writing them to the given file handle.
// The file format should be the same as the one expected
// by life_load_board().
//
void life_save_board(FILE *fp, Grid *grid);

The life_compute_next_gen function implements one time step of the simulation: based on the values of the cells in the current generation, the cells in the next generation are computed, following the rules of the game.

The life_load_board and life_save_board functions load a game board from a file and write a game board to a file, respectively.

You will need to provide implementations of these functions in life.c.

Note: life_load_board and life_save_board can use fscanf and fprintf to read from and write to the file handles (FILE pointer values) passed to the functions.

Sequential version

The source file life_seq.c contains a main function implementing a sequential simulation of Conway's Game of Life.

The program takes two command line arguments:

  • filename, which is a file specifying the initial generation of cells
  • numgens, which is an integer specifying how many generations should be simulated

The code in life_seq.c implements the computation by calling the functions you implemented in life.c. You may modify this code if you want to, but you will probably not need to.

Testing the sequential version

The file testin.dat contains an data file with an initial configuration of a grid of 5 rows and 10 columns. The initial configuration is a glider.

Here is a transcript showing the expected output when running the sequential program on this input file (user commands in bold):

[dhovemey@cscluster]$ ./life_seq testin.dat 0
5 10
0 0 1 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0 0
0 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
[dhovemey@cscluster]$ ./life_seq testin.dat 1
5 10
0 0 0 0 0 0 0 0 0 0
0 1 0 1 0 0 0 0 0 0
0 0 1 1 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
[dhovemey@cscluster]$ ./life_seq testin.dat 2
5 10
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0 0
0 1 0 1 0 0 0 0 0 0
0 0 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
[dhovemey@cscluster]$ ./life_seq testin.dat 3
5 10
0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0
0 0 0 1 1 0 0 0 0 0
0 0 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
[dhovemey@cscluster]$ ./life_seq testin.dat 4
5 10
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0
0 0 1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0

Make sure your sequential implementation produces similar output.

Parallel version

Your parallel version should use MPI to parallelize the computation. The source life_par.c contains the main function of the parallel version. It receives four command-line arguments, filename, numgens, N, and M. The first two, filename and numgens, have the same meaning as in the sequential version. N and M specify how the computation should be parallelized: the overall game grid should be subdivided into N rows and M columns of smaller grids.

For example, let's say the overall game grid has 120 rows and 300 columns, and that N = 2 and M = 3. Here is the overall game grid:

figures/gamegrid.png

Here is how the game grid would be subdivided into 2 rows and 3 columns of smaller grids:

figures/gamegrid-subdivided.png

The computation within each smaller grid will be handled by a separate parallel process.

The runpar script can be used to run the program. It takes the four arguments (filename, numgens, N, and M) indicated earlier. So, for example, the command

./runpar testin.dat 10 2 3

would run the simulation with the initial configuration specified by testin.dat, for 10 generations, dividing the overall game grid into 2 rows and 3 columns of smaller grids.

Parallel implementaton hints

Each processor should create a grid for just its local computation, with one row/column of padding on each side for cells whose values will be computed by other processors.

You may find it useful to add a grid_set_current function to your grid datatype.

Develop incrementally. For example:

  • First, get each process to do its local computation without any communication
  • Start the communication by having processes exchange a single value (for example, each process with a left neighbor sends its upper-left cell, then each process with a right neighbor receives that value)
  • If exchanges of a single value work, change to receiving an entire column or row
  • Once one communication (e.g., send left/receive right) is working, add another

Once the program is working well (but not before!), you can consider making the communication more efficient. For example, rather than looping over each value in a row or column and sending it individually, send all of the values using a single send. (Note: you can define a derived datatype to send values in a column.)

Grading criteria

  • Sequential computation: 20%
  • Dividing work between processes: 20%
  • Local computation: 20%
  • Communication: 30%
  • Efficiency of communication: 10%

Submitting

Run the command make submit. Enter your Marmoset username and password when prompted.