diff --git a/docs/genetic-operators.md b/docs/genetic-operators.md new file mode 100644 index 00000000..9e3613b8 --- /dev/null +++ b/docs/genetic-operators.md @@ -0,0 +1,220 @@ + +1. [Introduction](introduction.md) +2. [Fitness functions](fitness-functions.md) +3. [Encodings](encodings.md) +4. [Algorithms](algorithms.md) +5. **Genetic operators** +6. [Stop conditions](stop-conditions.md) +7. [Metrics](metrics.md) +8. [Miscellaneous](miscellaneous.md) + +------------------------------------------------------------------------------------------------ + +# Genetic operators + +The genetic operators are used to create new candidate solutions from the existing +ones in the population, thus providing the basic search mechanism of the genetic +algorithms. The 2 main operators are the crossover and mutation, but the library +also allows for specifying a repair function. + +The selection method is considered to be a part of the `Algorithm` in the library, +so it will not be discussed here. + +The library contains implementations of several crossover and mutation methods +that can be used. These can be found in the `gapp::crossover` and +`gapp::mutation` namespaces respectively. + +As the genetic operators operate on candidate solutions, their +implementations depend on the encoding type used for the GA. A given crossover or +mutation method can only be used with the encoding types it is implemented for. +Because of this, the implemented crossover and mutation methods are further broken +up into multiple namespaces based on the encoding type they can be used with. +For example, the crossover operators are in the following namespaces: + + - `crossover::binary` + - `crossover::real` + - `crossover::perm` + - `crossover::integer` + +Crossover methods in the `binary` namespace can only be used for the `BinaryGA`, +methods in the `real` namespace can only be used for the `RCGA`, and so on. +The mutation methods are organized similarly. + +The library doesn't provide any repair functions since their use in the GAs +is optional. These always have to be defined by the user when they are used. + +## Crossover + +The crossover operator is responsible for generating new solutions from existing +ones. The operator takes 2 solutions selected from the population, and performs +some operation on them with a given probability to generate 2 new solutions. +When the operation is not performed, it returns copies of the parent solutions. + +The crossover operator used by the GA can be set either in the constructor or by +using the `crossover_method` method: + +```cpp +PermutationGA GA; +GA.crossover_method(crossover::perm::Edge{}); +``` + +The probability of performing the crossover operation is a general parameter +of the crossovers and it can be set for all of the crossover operators either +in their constructors or by using the `crossover_rate` method: + +```cpp +PermutationGA GA; +GA.crossover_method(crossover::perm::Edge{ /* crossover_rate = */ 0.8 }); +``` + +The GA classes also provide a `crossover_rate` method that can be used to set +the crossover probability for the current crossover operator used by the GA: + +```cpp +PermutationGA GA; +GA.crossover_method(crossover::perm::Edge{}); +GA.crossover_rate(0.8); +``` + +Some crossover operators may also have additional parameters that are specific +to the given operator. + +## Mutation + +The mutation operator is applied to each of the solutions generated by the +crossovers in order to promote diversity in the population. This help the GA +with exploring more of the search space and avoiding convergence to local +optima. + +The mutation operator used by the GAs can be set similar to the crossover operators, +either in the constructor or by using the `mutation_method` method: + +```cpp +RCGA GA; +GA.mutation_method(mutation::real::NonUniform{}); +``` + +Similar to the crossovers, the mutation operators also all have a +mutation probability parameter, but how this probability is interpreted +(either on a per-gene or per-solution basis) depends on the specific +operator. + +The mutation probability can be set similar to the crossover probability, either +in the constructor of the mutation operator, or by using the `mutation_rate` +method: + +```cpp +RCGA GA; +GA.mutation_method(mutation::real::NonUniform{ /* mutation_rate = */ 0.1 }); +``` + +The GA classes also provide a `mutation_rate` method that can be used to set +the mutation probability for the current mutation operator of the GA: + +```cpp +RCGA GA; +GA.mutation_method(mutation::real::NonUniform{}); +GA.mutation_rate(0.1); +``` + +Similar to the crossovers, some mutation operators may have additional parameters +that are specific to the particular operator. + +## Repair + +The repair function is an additional operator that will be applied to each +solution after the mutations. Using a repair function is optional, and they are +not used in the GAs by default. + +Repair functions can be specified using the `repair_function` method of the GAs: + +```cpp +ga.repair_function([](const GA&, const Chromosome& chrom) +{ + auto new_chrom = chrom; + // do something with new_chrom ... + return new_chrom; +}); +``` + +If a repair function has been set previously, it can be cleared by passing +a nullptr to the setter: + +```cpp +GA.repair_function(nullptr); +``` + +## Custom genetic operators (crossover and mutation) + +In addition to the operators already implemented in the library, +user defined crossover and mutation operators can also be used in the GAs. + +The simplest way to do this is to use a lambda function: + +```cpp +RCGA ga; + +ga.crossover_method([](const GA&, const Candidate& parent1, const Candidate& parent2) +{ + auto child1 = parent1; + auto child2 = parent2; + + // perform the crossover ... + + return CandidatePair{ std::move(child1), std::move(child2) }; +}); + +ga.mutation_method([](const GA& ga, const Candidate& sol, Chromosome& chrom) +{ + for (RealGene& gene : chrom) + { + if (rng::randomReal() < ga.mutation_rate()) + { + // modify the gene ... + } + } +}); +``` + +Alternatively, crossover and mutation operators can also be implemented as +classes derived from `crossover::Crossover` and +`mutation::Mutation` respectively. Crossovers must implement the +`crossover` method, while mutations must implement the `mutate` method: + +```cpp +class MyCrossover : public crossover::Crossover +{ +public: + using Crossover::Crossover; + + CandidatePair crossover(const GA& ga, const Candidate& parent1, const Candidate& parent2) const override + { + // perform the crossover ... + } +}; +``` + +```cpp +class MyMutation : public mutation::Mutation +{ +public: + using Mutation::Mutation; + + void mutate(const GA& ga, const Candidate& candidate, Chromosome& chromosome) const override + { + // perform the mutation on chromosome ... + } +}; +``` + +There are a few things that should be kept in mind for the implementations +of these operators regardless of how they are defined: + + - The crossover implementation shouldn't take the crossover rate into account. + This is done elsewhere. + - The mutation implementation must take the mutation rate into account, as how + the mutation rate is interpreted depends on the specific mutation method. + - The mutation modifies the `chrom` parameter, and does not return anything. + - The implementations should be thread-safe. + +------------------------------------------------------------------------------------------------