IBM ILOG Solver User's Manual > Evolutionary Algorithms > Introduction and Basic Concepts > Features of the library > Genetic operations

After selection of parents, a GA then typically applies genetic operators to those parents to produce new solutions: the offspring. The EA framework comes with various built-in mutation and combination operators, operating on arrays of constrained integer variables. All genetic operators provided with the EA framework are randomized and you should follow this tenet if you write your own operators. During the execution of a genetic algorithm, the same operator may be applied to the same solution or solutions more than once. If operators where not randomized, search would quickly stagnate, unable to provide new different solutions.

All genetic operators are represented by the class IloPoolOperator, which can be seen as a special type of pool processor; in fact an automatic cast exists between a pool operator and pool processor. An operator depends on an instance of IloSolver to produce its solutions and works as follows:

  1. The operator asks for the solutions it needs to perform the operation. Typically, it asks the processor immediately "upstream" of a >> operator.
  2. The operator instantiates the constrained variables in the solver according to the definition of the operator, and the values of the input solutions. For example, imagine that the operator was to mutate 0-1 variables uniformly. To take advantage of constraint programming, the operator could launch a goal search where, at each node, a choice point (IlcOr) is created which instantiates the variable in question to either 0 or 1. With high probability, the left branch would concur with the value in the input solution, and with low probability (the mutation probability), the branches would be reversed.
  3. The operator looks for a prototype solution. This can be given explicitly by the user to the operator, but if not, one of the input solutions is used as a prototype.
  4. The prototype is cloned, is stored using solution.store(solver) and transferred to the output of the operator.

Steps 1, 3, and 4 are automated and are common to all operators. They comprise the demand for input, the creation of a new solution, the transfer of solution values from the solver to the solution, and the transfer of the new solution to the output of the processor. Only Step 2 need vary from operator to operator, and it is this step that you will be responsible for if you want to write your own genetic operator. (Some information for Step 1 is also required--the number of solutions that the operator needs as input to properly function.)

The principle of Step 2 of the above process is to place the decision variables of the problem in some desired state congruent with the definition of the operator. A goal can also be seen as performing this job. As such, it is possible to produce an operator from a standard goal, so long as a solution prototype is given explicitly. This approach can be used for creating initial populations as follows:

IloIntVarArray vars = ...;
IloObjective obj = ...;
IloSolution prototype(env);
prototype.add(vars);
prototype.add(obj);
IloGoal generate = IloRandomPerturbation(env, stdGoal, 0.05);

IloSolutionPool population(env);
IloPoolProc genProc = IloSource(env, generate, prototype)(50) >> population;
solver.solve(IloExecuteProcessor(env, genProc));

The IloSource operator requests no inputs, runs goal generate, clones prototype, stores the resulting solution, and places it in the output pool. Setting the output size to 50 ensures that this process is repeated until IloSource has gathered 50 solutions in its output pool.

The genetic operators provided in the EA framework are generated by a factory class IloEAOperatorFactory. This class allows built-in operators to be generated as well as configured by solution prototypes and search limits. For example, the following code creates a factory which will generate operators with a search limit of 100 fails in which to generate a solution and a prototype of prototype:

IloSolutionPool population(env);
IloEAOperatorFactory fact(env, vars);

// Prototype
fact.setPrototype(prototype);

// All operators generated by the factor will have this limit applied.
fact.setSearchLimit(IloFailLimit(env, 100));

// Generate an initial random population of 30 solutions
solver.solve(IloExecuteProcessor(env, fact.randomize()(30) >> population));

// Mutation operator, with 1% probability.
IloPoolProc mut = fact.mutate(0.01);

Note that if the operator could not produce a solution in the number of fails given as a limit, it will be repeatedly called until it produces such a solution. Each time this happens, new input solutions are requested before the operator is re-applied.

The IIM EA framework also supports dynamic pool processor selection. That is, a pool processor can be defined to be a dynamic selection among a set of such processors.

This is an elegant way of specifying composite processors. Imagine a processor which when invoked, behaves as one of n subordinate processors, the choice being made at random. This behavior can be specified quite simply in the EA framework as follows:

IloPoolProc op1 = ...;
IloPoolProc op2 = ...;
IloTournamentSelector<IloSolution,IloSolutionPool> tsel(
  env, 3, IloBestSolutionComparator(env)
);
IloPoolProc tourSelect = IloSelectSolutions(env, tsel);
IloPoolProc rndSelect = IloSelectSolutions(
  env, IloRandomSelector<IloSolution,IloSolutionPool>(env)
);

// Build an array of processors.
IloPoolProcArray ops(env);
ops.add(op1);
ops.add(op2);

// A composite operator which executes a randomly selected operator.
IloPoolProc composite = IloSelectProcessor(
  env, ops, IloRandomSelector<IloPoolProc,IloPoolProcArray>(env)
);

// Generate 30 offspring.
IloSolutionPool offspring(env);
IloPoolProc breed = population >> tourSelect >> composite(30) >> offspring;
solver.solve(IloExecuteProcessor(env, breed));

Notice the function IloSelectProcessor which uses a selection of a pool processor from an array of pool processors to pick the ``sub-processor'' to execute each time processor composite is executed.

The result is that the offspring pool will be filled by solutions which have been generated by a non-deterministic interleaving of the executions of op1 and op2. One additional point to note in the above is that op1 and op2 need not be consistent in the numbers of solutions that they require as input. The tournament selector tourSelect is only asked for the correct number of solutions to supply after composite has decided on the exact subprocessor to execute. Thus, every time composite is invoked, it may demand a different number of solutions from the tournament selector.