EvoLP was designed with extensibility in mind, by using Julia's dynamic dispatch ability to keep things simple.
We have 3 main functions (
mutate) which will perform differently depending on the parameters passed. By passing a specific type, we ensure that the selection/recombination/mutation acts how we expect to. The available type hierarchy in EvoLP.jl looks like the following:
In the image above, the abstract types are highlighted in bold font. The composite types (those you can instantiate) are marked with dotted lines, and those with parameters are bounded in a grey box.
If you want to experiment with new building blocks, you can create your own by using the abstract block types as a supertype of your own selectors, mutators and recombinators. These abstract blocks are the following:
Abstract Mutator for binary individuals.
Abstract Mutator for real-valued individuals.
Abstract continuous recombinator.
Abstract Mutation Method.
Abstract numeric recombinator.
Abstract Parent Selector
Abstract Mutator for permutation individuals.
Abstract permutation recombinator.
Abstract Selector for either Parent or Deme selection methods.
The abstract types are not exported in EvoLP, which means that you will not see them if you used
using EvoLP in your code. You need to access them directly, by writing
Once you have your own type, you need to explicitly code how the block should operate. This is done by creating a new method of the
mutate functions, depending on what your abstract supertype is.
After that, you can use the new blocks in your algorithms like any of the other built-in blocks.
You can use the corresponding
AbstractType to create a new mutation method called
MyCrazyMutation. This new mutator would work on a real-valued vector, so it should be a sub-type of the
ContinuousMutator abstract type:
struct MyCrazyMutation <: EvoLP.ContinuousMutator
Then, we can code a new method for the
mutate function, which will look something like the following:
function mutate(M::MyCrazyMutation, ind; rng=Random.GLOBAL_RNG)
mutant = deepcopy(ind)
if rand(rng) < param3
mutant = mutant + M.param1
mutant = mutant - M.param2
MyCrazyMutation method will then operate on a hypothetical 2-dimensional individual
ind, and will change its first dimension by its
param1, and the second dimension using
param2 if a random number is less than its
Then, you can use it in your algorithms as if it was any other block. Here is a fake snippet for illustration purposes:
M = MyCrazyMutation(p1, p2, p3)
If using random numbers (for example in crossover or mutation operators) it is always a good idea to pass to your function a random number generator instance. In this way, your code can be used both for unit testing as well as for constructing shareable examples that are reproducible for the sake of science.