RIGEL's purpose
RIGEL is a simple framework to code the most common flavors of evolutionary algorithms. It aims to include the most comprehensive range of evolutionary tools, while remaining simple, fast and efficient. Priority has been given to easy access, over refinement of scheme and strategies.
RIGEL's structures
Evolutionary data has been split into two conceptual entities:
search-space specific, and generational-specific. The first set of data,
stored in a
RIGEL_Interface
structure, actually informs RIGEL of the
genome encoding and the genetic operators selected. The second set,
stored in a
RIGEL_Parameters
object, contains the genetic parameters
pertaining to the populations dynamics and operators probabilities.
Note the subtle difference we have made between structure and object:
the first is a simple C
struct,
while the second is a pointer toward a
user-allocated chunk of memory containing a C
struct.
This terminology
will be used a few times in this tutorial, as we have -- rarely --
to deal with memory issues.
Genome
When creating a new evolutionary project with RIGEL, the first step you
have to take is to build a structure to hold the genome. You need to
reserve space to store all the data relevant to individuals you wish to
transmit to descendant, or need to use in genetic operators.
For our demonstration, let us take the example of the well known OneMax
optimisation algorithm. We realise the genome as a C structure, that is:
#define SIZE 50 // The size of the bit vector
struct S_Genome {
char x[SIZE] ; // The bits
} ;
The second step is to inform RIGEL of our choice of a genome structure.
To this end, we define, before including RIGEL headers, the macro
RIGEL_Genotype_Object
#define RIGEL_Genotype_Object struct S_Genome
The RIGEL_Genotype
type will then be
struct S_Genome *
The structure RIGEL_Interface
will let us specify handlers to set up the contents of the genome and the genetic operators.
These are:
- | a constructor / initializer | RIGEL_Genotype _Iface_Init(RIGEL_Parameters param)
|
- | a mutation operator | RIGEL_Genotype _Iface_Mutn(RIGEL_Genotype indiv, RIGEL_Parameters param)
|
- | a crossover operator | RIGEL_Genotype _Iface_Xovr(RIGEL_Genotype indi1, RIGEL_Genotype indi2, RIGEL_Parameters param)
|
- | a migration operator | RIGEL_Genotype _Iface_Migr(RIGEL_Genotype indiv, RIGEL_Parameters param)
|
- | a copy constructor | RIGEL_Genotype _Iface_Copy(RIGEL_Genotype indiv)
|
- | a delete method | void _Iface_Free(RIGEL_Genotype indiv)
|
- | an evaluation method | double _Iface_Eval(RIGEL_Genotype indiv)
|
- | a read and a write method | long _Iface_Write(RIGEL_Genotype indiv, char * str, unsigned long len)
|
Whichever component you do not wish to define can be set to
NULL,
except the initializer and the evaluator. In our example, these
functions (or methods, if you see the genome as an object) would be:
RIGEL_Genotype _Iface_Init(RIGEL_Parameters param) {
unsigned long i ;
Genome g ;
g = malloc(sizeof(struct S_Genome)) ;
i = 0 ;
while(i<SIZE) {
// Use param->rate_init as a bias vs ones
if(rand()<(RAND_MAX*param->rate_init)) g->x[i] = 1 ;
else g->x[i] = 0 ;
i ++ ;
}
return g ;
}
void _Iface_Free(RIGEL_Genotype indiv) {
free(indiv) ;
}
RIGEL_Genotype _Iface_Copy(RIGEL_Genotype indiv) {
unsigned long i ;
Genome g ;
g = malloc(sizeof(struct S_Genome)) ;
i = 0 ;
while(i<SIZE) {
g->x[i] = indiv->x[i] ;
i ++ ;
}
return g ;
}
RIGEL_Genotype _Iface_Mutn(RIGEL_Genotype indiv, RIGEL_Parameters param) {
unsigned long i = 0 ;
// Use param->rate_mutn as the rate to flip bits
while(i<SIZE) {
if(rand()<(RAND_MAX*param->rate_mutn)) indiv->x[i] = !(indiv->x[i]) ;
i ++ ;
}
return indiv ;
}
RIGEL_Genotype _Iface_Xovr(RIGEL_Genotype indi1, RIGEL_Genotype indi2, RIGEL_Parameters param) {
unsigned long i = 0 ;
unsigned char t ;
Genome g ;
g = malloc(sizeof(struct S_Genome)) ;
// Use param->rate_xovr as the rate to change sides
t = (rand()/((double)RAND_MAX))>0.5?1:0 ;
while(i<SIZE) {
if(t) g->x[i] = indi1->x[i] ;
else g->x[i] = indi2->x[i] ;
if(rand()<(RAND_MAX*param->rate_xovr)) t = 1-t ;
i ++ ;
}
return g ;
}
double _Iface_Eval(RIGEL_Genotype indiv) {
unsigned long i = 0 ;
double s = 0 ;
while(i<SIZE) {
if(indiv->x[i]) s ++ ;
i ++ ;
}
return s ;
}
long _Iface_Write(RIGEL_Genotype indiv, char * str, unsigned long len) {
unsigned long i = 0 ;
while(i<SIZE && i<len) {
str[i] = (indiv->x[i])?'1':'0' ;
i ++ ;
}
str[i] = '\0' ;
return SIZE ;
}
We now have all the parts ready to complete the interface:
RIGEL_Interface OneMaxInterface = {
RIGEL_PARADIGM_MUTALLOC | RIGEL_PARADIGM_XOVALLOC, // The paradigm flag
_Iface_Init, // Initialisation constructor
_Iface_Mutn, // Mutation
_Iface_Xovr, // Crossover
_Iface_Copy, // Copy constructor
_Iface_Free, // Destructor
_Iface_Eval, // Evaluator
NULL, // _Iface_Read, unneeded and thus not defined
_Iface_Write // Writer
} ;
Note that the order of the fields is important, since we do not refer to
them by name, but by position. An other way to do this would be:
RIGEL_Interface OneMaxInterface ;
OneMaxInterface.Paradigm = RIGEL_PARADIGM_MUTALLOC | RIGEL_PARADIGM_XOVALLOC ;
OneMaxInterface.Initialize = _Iface_Init ;
OneMaxInterface.Mutate = _Iface_Mutn ;
OneMaxInterface.CrossOver = _Iface_Xovr ;
OneMaxInterface.Duplicate = _Iface_Copy ;
OneMaxInterface.Destroy = _Iface_Free ;
OneMaxInterface.Evaluate = _Iface_Eval ;
OneMaxInterface.Read = NULL ;
OneMaxInterface.Write = _Iface_Write ;
Before starting the actual optimisation work, we need to reate and
fill the genetic parameters list, that will be used to regulate the
inner workings of genetic operators, and the populations dynamics. A
convenient method parses all operators from the command line (the argc
and argv parameters of the main function), so we just have to set a few
sensible defaults:
RIGEL_Parameters par ;
par = RIGEL_ParametersSetDefaults(NULL) ;
par->rate_init = 0.5 ;
par->rate_mutn = 0.1 ;
par->rate_xovr = 0.1 ;
par->popu_size = 10 ;
par->popu_flag = RIGEL_PARAM_SELPROPN | RIGEL_PARAM_SORTDESC ;
par->popu_elit = 3 ;
par->popu_parn = 5 ;
par->popu_chld = 5 ;
par->popu_init = 0 ;
RIGEL_ParametersParseArgs(par,&argc,&argv) ;
We now have to build the main EA loop, that is building the population for generation 0, and iterating the generation builder until the stop criterion is reached.
Building a first population is done by:
RIGEL_Population pop ;
pop = RIGEL_Initialize(OneMaxInterface,par) ;
Since a Write method has been defined, you can write it on the standard output by:
printf("Ident Eval Zone Rate Conf Indiv\n") ;
RIGEL_FileSave(pop,"") ;
And now the main loop:
unsigned long gen = 0 ;
gen = 0 ;
while(pop->locs[0].eval<SIZE) {
RIGEL_Population nwpop ;
gen ++ ;
nwpop = RIGEL_Generation(pop) ;
RIGEL_Terminate(pop) ;
pop = nwpop ;
printf("Generation %lu\n",gen) ;
printf("Ident Eval Zone Rate Conf Indiv\n") ;
RIGEL_FileSave(pop,"") ;
}
We have specified the stop criterion by hand, directly in the while loop,
since we know that pop->locs[0] will always contain the best individual.