Skip to content

Commit

Permalink
Create genetic-operators.md
Browse files Browse the repository at this point in the history
  • Loading branch information
KRM7 committed Aug 9, 2023
1 parent 14407ff commit 7263efe
Showing 1 changed file with 220 additions and 0 deletions.
220 changes: 220 additions & 0 deletions docs/genetic-operators.md
Original file line number Diff line number Diff line change
@@ -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<RealGene>&, const Chromosome<RealGene>& 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<RealGene>&, const Candidate<RealGene>& parent1, const Candidate<RealGene>& parent2)
{
auto child1 = parent1;
auto child2 = parent2;

// perform the crossover ...

return CandidatePair<RealGene>{ std::move(child1), std::move(child2) };
});

ga.mutation_method([](const GA<RealGene>& ga, const Candidate<RealGene>& sol, Chromosome<RealGene>& 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<GeneType>` and
`mutation::Mutation<GeneType>` respectively. Crossovers must implement the
`crossover` method, while mutations must implement the `mutate` method:

```cpp
class MyCrossover : public crossover::Crossover<RealGene>
{
public:
using Crossover::Crossover;

CandidatePair<RealGene> crossover(const GA<RealGene>& ga, const Candidate<RealGene>& parent1, const Candidate<RealGene>& parent2) const override
{
// perform the crossover ...
}
};
```

```cpp
class MyMutation : public mutation::Mutation<RealGene>
{
public:
using Mutation::Mutation;

void mutate(const GA<RealGene>& ga, const Candidate<RealGene>& candidate, Chromosome<RealGene>& 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.

------------------------------------------------------------------------------------------------

0 comments on commit 7263efe

Please sign in to comment.