Project 2
cs599 Parallel Programming and Algorithms
20 Points
Due Mar. 16, 2015 at Midnight
You must run your program on Stampede to receive full points for this assignment
Instructions for running your program on Stampede are provided at the end of this document

Description

Objective:  To learn about MPI collective operations, random numbers, structures, pointers, and using multiple source files in C.

A Simple Genetic Algorithm

Genetic algorithms make use of ideas from population genetics and molecular genetics to solve a problem.  These concepts are "survival of the fittest", mating, crossover, and mutation, to name a few. Typically in a population, parents from a population give rise to children, some of which are more fit than others.  Individuals that are fit tend to survive longer than those who are unfit; obviously, this is known a survival of the fittest.  When a child is produced, the genetic material of the parents is intermingled to form a child.  This mixing of genetic material is called crossover.  Moreover, sometimes when a child is formed, the child's genetic material changes at random due to environmental factors.  This type of random change is known as mutation.  Populations go through a cycle of mating, creating children, children becoming parents, and eventual parent death known as a generation.

Computer scientists eventually used the concepts listed above to create optimization algorithms.  Such algorithms have been called genetic or evolutionary algorithms because they use a "population" of values chosen at random to find the optimal solution to a function (or a set of functions).  These populations typically contain a set of parents and children.  Parents from these populations of values "mate" (that is, their values are combined or "crossed over" somehow) to form children.  After these children have been formed, their values are "mutated" (i.e. slightly modified) at random.  Thereafter, children whose function values are good (e.g. smaller for a function that is to be minimized) are kept and replace parents whose function values are bad (e.g. larger for a function that is to be minimized).  This process simulates "survival of the fittest."  This process is repeated a number of times so that a good population can be formed.  The number of times this process is to be repeated is also known as the number of generations.

Specification:

For project 2, you are to write an MPI program that will find the minimum of a two-dimensional function (e.g. y = x2 ) bounded within a certain area (e.g. -5.0 < x < 5.0).  To accomplish this task, you must first generate a number of solutions at random within the bounds of the problem (e.g. 50 solutions).  Note that each process should generate an equal number of solutions. For example, if there are to be 50 total solutions and there are five processes, each process should generate 10 solutions. Thereafter, you are to use pairs of these solutions, called "parents", to produce a number of new solutions (e.g. 10 new solutions), called "children".  A description of how to choose parents is provided in the description of the functions below.  These "children" must be chosen by picking a value at random between the x value of the first parent minus 0.5*alpha and the x value of the second parent plus 0.5*alpha as shown in the image below assuming that the first parent has the smaller x value, and then evaluating the function (e.g. y = x2 ) at the new x value.  Note that a child should not fall outside of the bounds for the problem.  If it does, you must clamp the result such that it falls within the correct interval.  Note that in the image below, alpha is set to 1.0. You must provide a value for alpha in your program as a constant that is no more than 1/100 of the range of the x values.



After creating the desired number of children (10 in this case), these children are compared against the worst of the parents.  If the y value of a child is better than one of the worst of the parents in one process, the child becomes a parent; otherwise, the child is discarded.  Thus, those children that are better than the worst of the parents are added to the population to replace the worst parents.  This process should repeat itself for the number of times or "generations" that the user has requested.  An outline of the program and example output for a serial version of the program follows.

Program Outline:

The source code for project 2 should be placed in files named ga.h, ga.c, and prog2.c,   Do NOT give the files different names.  You will lose points if you give the file any other names than those provided above. 

ga.h

In ga.h, you should provide function prototypes, a definition of the struct that you will use, and declarations for any constants that you will use in your program.  Important: you do not yet know how to transfer structures with MPI. This means that you will need to move your data out of a structure before you transfer from one process to another.  You should have at least the following four function prototypes in your header file:

void initialize_population(individual []);
void make_children(individual [], individual []);
void evaluate_expression(double, double *);
void cull_individuals(individual [], individual []);


You may use other functions to aid yourself in making each of the above functions more manageable; however, additional functions are not required.  Any additional functions MUST be put in the same files (i.e. ga.h, ga.c, prog2.c).

ga.c

In ga.c, you must provide definitions for the functions above.  The initialize_population function must take the array of parents that the current process will use as a parameter and initialize each of the parent x values to a random value between XMIN (the minimum x value) and XMAX (the maximum x value).  Note that a random value between XMIN and XMAX can be chosen using the following formula:

(XMAX - XMIN) * ( (double) rand() / (double) RAND_MAX) + XMIN

Note that RAND_MAX is the maximum random value used by the C programming language and is available by using #include <stdlib.h>.  After initializing the x values for the parents, you must call the evaluate_expression function (which must evaluate an expression such as y = x2) on each of the given parents to set the y values for each of the parents.

Note that it is highly recommended that you use a different pseudorandom number generator to generate values. The Mersenne Twister is an excellent pseudorandom number generator and has a period of 2^19937. You can find example code that makes use of this pseudorandom number generator at the following link. The make_children function must take the parent and child arrays as parameters and choose the children from the parents using crossover and mutation.  To create a child, choose two parents at random from the array of parents, and ensure that the two parents are not from the same process. In each process, you should choose one parent from that process and one from a neighboring process. You should use a different neighbor in each iteration (e.g. in the first iteration choose the neighbor at rank+1 and in the next choose the neighbor with rank-1, and repeat).  Next, perform crossover.  This means that you will use the two parents to create one child.  In this case, children must be chosen by picking a value at random between the x value of the first parent minus 0.5*alpha and the x value of the second parent plus 0.5*alpha as shown in the image below assuming that the first parent has the smaller x value, and then evaluating the function (y = x2 ) at the new x value.



Use the following formula to perform the crossover:

children[i].x = ( (parents[rv1].x+.5*alpha) - (parents[rv2].x-.5*alpha) )* ( (double) rand()/(double) RAND_MAX ) + (parents[rv2].x-.5*alpha);

Note that the formula above assumes that rv1 is the index of the larger parent.  You should ensure that this is true in your program.

After you have performed crossover on a child, you must then perform mutation on that child.  This will ensure that there is some variation within the set of children and will help to move a child out of a local minimum if one does exist.  The mutation operation in this program should add a random number between -0.1*beta and 0.1*beta to the child.  Ensure that beta is a value that is less than 1/1000 of the range in x. Mutation can be performed using the following formula:

children[i].x = children[i].x + .2*beta*(rand()/(double)RAND_MAX-.5);

Note that a child should not fall outside of the bounds for the problem.  If it does, you must clamp (change) the result so that it lies within the bounds for the problem.

Once the x value for the child has been produced using crossover and mutation, use the evaluate_expression function to set the y value for the child.

The process described above should be repeated for each child in each process.

Finally, for the cull_individuals function, you must attempt to add children to the array of parents.  Since the array of parents has a fixed size, you must remove or cull parents from this array before new children may become parents.  To do this, you must select the set of parents that are worse than the children.  Note that we are assuming that the number of children is smaller than the number of parents.  You may do this by locally searching for the worst parents within each process, marking their positions using an additional array, and by sorting the local list of children from highest y value to lowest y value.  Then add children to the array of parents by comparing them to the worst parents.  Most of the code for this function is as follows below.

int dead_parents[NC] = {-1};
int sorted_children[NC];
int i, j, maxval;

for( i = 0; i < NC; i++)
    sorted_children[i] = i;

for( i = 0; i < NC-1; i++)
    for( j = i + 1; j < NC; j++)
        if ( children[sorted_children[i]].y < children[sorted_children[j]].y )
            /* swap sorted_children[i] and sorted_children[j] here */

for(i = 0; i < NC; i++)
{
    maxval = 0;

    while(i > 0 && parents[maxval].y > parents[dead_parents[i-1]].y)
        maxval++;

    for(j = 0; j < NP; j++)
    {
        if(i > 0)
        {
            if(parents[j].y < parents[dead_parents[i-1]].y && parents[j].y > parents[maxval].y)
           
                maxval = j;
        }
        else
        {
            if(parents[j].y > parents[maxval].y)
                maxval = j;
        }
    }
    dead_parents[i] = maxval;

}

for(i = 0, j = 0; i < NC; i++)
{
    if(children[sorted_children[i]].y < parents[deadparents[j]].y)
    {
        parents[dead_parents[j]].y = children[sorted_children[i]].y;
        parents[dead_parents[j]].x = children[sorted_children[i]].x;
        j++;
    }
}


prog2.c

In prog2.c, your program should contain a main function that calls the functions in ga.c, so that a population of parents is created, and children are created and added to the population for a number of generations.  A description of the main function is provided with the following psuedocode:

    start MPI     obtain the number of generations, population size, number of children, bounds for the problem, and function to evaluate from the command line
    declare variables including arrays for the parents and children
    create parents

    while i < number of generations
        create a new set of children
        add the children to the population
        increment i
    end while

    print out the individuals in the parents array
    Send parents to process 0 and print out the best of the parents
    End MPI


It should be obvious as to where each function call should be placed.  Note that in the example below, the size of the array of children should be 10 and the size of the array of parents should be 50.

Example:

With the following input, where y = x2 is your expression, the output in your program may appear as follows:

Input:

numGenerations numParents numChildren startX endX functionNumber
1000 50 10 -5.0 5.0 1

Output:

Parent 0, x = 0.001, y = 0.000
Parent 1, x = 0.001, y = 0.000
Parent 2, x = -0.002, y = 0.000
Parent 3, x = 0.000, y = 0.000
Parent 4, x = -0.001, y = 0.000
Parent 5, x = -0.001, y = 0.000
Parent 6, x = 0.002, y = 0.000
Parent 7, x = 0.001, y = 0.000
Parent 8, x = -0.002, y = 0.000
Parent 9, x = 0.000, y = 0.000
Parent 10, x = 0.002, y = 0.000
Parent 11, x = 0.002, y = 0.000
Parent 12, x = -0.000, y = 0.000
Parent 13, x = -0.002, y = 0.000
Parent 14, x = -0.002, y = 0.000
Parent 15, x = 0.000, y = 0.000
Parent 16, x = 0.000, y = 0.000
Parent 17, x = 0.001, y = 0.000
Parent 18, x = 0.000, y = 0.000
Parent 19, x = 0.001, y = 0.000
Parent 20, x = 0.001, y = 0.000
Parent 21, x = 0.002, y = 0.000
Parent 22, x = 0.000, y = 0.000
Parent 23, x = 0.000, y = 0.000
Parent 24, x = -0.002, y = 0.000
Parent 25, x = -0.000, y = 0.000
Parent 26, x = 0.000, y = 0.000
Parent 27, x = -0.002, y = 0.000
Parent 28, x = 0.000, y = 0.000
Parent 29, x = -0.001, y = 0.000
Parent 30, x = -0.002, y = 0.000
Parent 31, x = -0.002, y = 0.000
Parent 32, x = 0.002, y = 0.000
Parent 33, x = 0.001, y = 0.000
Parent 34, x = -0.001, y = 0.000
Parent 35, x = 0.002, y = 0.000
Parent 36, x = 0.001, y = 0.000
Parent 37, x = -0.000, y = 0.000
Parent 38, x = 0.002, y = 0.000
Parent 39, x = -0.001, y = 0.000
Parent 40, x = -0.002, y = 0.000
Parent 41, x = 0.002, y = 0.000
Parent 42, x = -0.002, y = 0.000
Parent 43, x = -0.001, y = 0.000
Parent 44, x = 0.001, y = 0.000
Parent 45, x = 0.002, y = 0.000
Parent 46, x = -0.000, y = 0.000
Parent 47, x = 0.001, y = 0.000
Parent 48, x = 0.001, y = 0.000
Parent 49, x = -0.002, y = 0.000

The best parent was:
Parent 25, x = -0.000, y = 0.000

Your program must take input at the command line exactly as shown in the example above.  Input MUST be provided in the followng format:

numGenerations numParents numChildren startX endX functionNumber

Where numGenerations (an integer) is the number of generations to iterate over, numParents (an integer) is the number of parents, numChildren (an integer) is the number of children, startX (a double) is the lower x bound, endX (a double) is the upper x bound, and functionNumber is the function to evaluate. Several functions and their id numbers will be provided soon. Note that x2 has an id of 1.   Failure to take input in the order given above will result in a loss of points on this programming assignment.

Save the source code for program 2 in files named ga.h, ga.c, and prog2.c,   Do NOT give the files different names.  You will lose points if you give the file any other names than those provided above.

To compile your program, create a makefile.  You can use the Tic-Tac-Toe example on the examples webpage to help you to create a makefile.  Note any lines in your makefile that have indention MUST contain a tab character.  make will not recognize your makefile as a valid makefile unless each indention is a tab.  Name your makefile as makefile.  Then compile your program with the following command:

    make

Assuming that there are no errors in your makefile, this will compile your program using the mpicc compiler, and it will create an executable file as long as there are no compile-time errors in your code.  Assuming that there are no errors in your program, you may run your program as follows:

prog2 numGenerations numParents numChildren startX endX functionNumber

Where prog2 is the name of your executable file.

Example Functions

Provided below are five example functions that you may use within your program.

//1. DeJong's Function 1
//minimum f(0) = 0
//minimum location x = 0
//feasible range x = -2000 to 2000 or wider
double DeJong1(double x)
{
//find the solution to the problem

return pow(x,2);
}

// 2. Mineshaft Function 1 (using high order roots)
//minimum f(5) = 1.3805
//minimum location x = 5
//Feasible range - x = 0 to 10

double Mineshaft1(double x)
{
double val1 = (7 - x);
double val2 = pow(5 - x, 4);

return cos(x) + pow(val1 * val1, 1.0 / 15.0) + 2 * pow(val2, 1.0 / 35.0);
}

//3. S1 [Choi and Mayfield 2009]
//minimum f(1) = 0 and f(2) = 0
//minimum location x = 1, x = 2
//Feasible region x = -10 to 10 or wider
double S1(double x)
{
//find the solution to the problem

return (x - 1.0) * (x - 1.0) * (x - 2.0) * (x - 2.0);
}

//4. Salomon's Function 1D [Price et al. 2005]
//minimum f(0) = 0
//minimum location x = 0
//Feasible region x = -100 to 100 or wider
double Salomon(double x)
{
//Compute the L2 norm of the location
double xNorm = sqrt(x*x);

//Compute the objective function value
return cos(2.0 * 3.14159265359 * xNorm) + 0.1 * xNorm + 1.0;
}

//5. Downhill Step Function based on [Price et al. 2005]
//minimum f(0) = 9
//minimum location x = 0
//Feasible region x = -10 to 10 or wider
double DownhillStep(double x)
{
//find the solution to the problem
return floor( 10*( 10 - exp( -(x*x) ) ) )/10;
}

Submission

Run your program on the Stampede Supercomputer with the Mineshaft 1 function using 10,000,000 generations, a population size of 16,000, and with 1600 children created per generation.  Run this with 16, 32, and 64 cores on the Stampede Supercomputer being sure to write down your runtimes as calculated by MPI_Wtime().

Submit your three program files, your three runtimes, an sbatch file, and your makefile, and a README file describing how to compile and run your programs, and your comparison to the eCompanion dropbox for Project 2.