Creating a Custom Controller

[2]:
from evokit.core import Population
from evokit.evolvables.binstring import BinaryString

First create a Population. This example uses the binary string representation.

[3]:
pop : Population[BinaryString] = Population[BinaryString]()

pop.append(BinaryString(int('11111', 2), 5))
pop.append(BinaryString(int('11110', 2), 5))
pop.append(BinaryString(int('11100', 2), 5))
pop.append(BinaryString(int('11000', 2), 5))
pop.append(BinaryString(int('10000', 2), 5))
pop.append(BinaryString(int('00000', 2), 5))

print(pop)
['[1, 1, 1, 1, 1]', '[1, 1, 1, 1, 0]', '[1, 1, 1, 0, 0]', '[1, 1, 0, 0, 0]', '[1, 0, 0, 0, 0]', '[0, 0, 0, 0, 0]']

When an BinaryString is created, it has yet to be evaluated. Attempting to access its .fitness attribute raises an error.

[4]:
try:
    pop[0].fitness
except ValueError as e:
    print(f"An {type(e).__name__} is raised, with message \"{e}\"")
An ValueError is raised, with message "Score is accessed but null"

The selector only operates on individuals whose .fitness is defined. Import an evaluator for BinaryString to evaluate all items in pop.

[5]:
from evokit.evolvables.binstring import CountBits

CountBits().evaluate_population(pop)

for individual in pop:
    print(f"Fitness of {individual} is {individual.fitness}")
Fitness of [1, 1, 1, 1, 1] is 5
Fitness of [1, 1, 1, 1, 0] is 4
Fitness of [1, 1, 1, 0, 0] is 3
Fitness of [1, 1, 0, 0, 0] is 2
Fitness of [1, 0, 0, 0, 0] is 1
Fitness of [0, 0, 0, 0, 0] is 0

A selector selects from a Population into a strict subset of it. For each item selected into this subset, that item is removed from the original Population to prevent duplication of references.

This example uses a SimpleSelector, which simply selects a number of individuals with the highest .fitness. Set budget=3 to select 3 such individuals.

[6]:
from evokit.core import SimpleSelector
[7]:
sel = SimpleSelector(budget=3)
original_population = pop
parents = sel.select_to_population(original_population)

print(f"Original population has items {original_population}")
print(f"Output population has items {parents}")
Original population has items ['[1, 1, 0, 0, 0]', '[1, 0, 0, 0, 0]', '[0, 0, 0, 0, 0]']
Output population has items ['[1, 1, 1, 1, 1]', '[1, 1, 1, 1, 0]', '[1, 1, 1, 0, 0]']

A variator creates new individuals from existing individuals. Here, use MutateBits which randomly flips each bit with probability mutation_rate. Use 0.1 for now.

Apply the mutator to output_population to create a new population of offspring. Note that, unlike selectors, variators do not affect the Population that is given argument.

Notice that not all items in the survivor pool are changed. With mutation_rate=0.1, there is a probability of 0.6 that the variator does not affect an individual. The first and third individuals are duplicated exactly.

[8]:
import random
random.seed(24601)

from evokit.evolvables.binstring import MutateBits
variator = MutateBits(mutation_rate=0.1)

survivors = variator.vary_population(parents)
print (f"Parent:    {parents}")
print (f"Offspring: {survivors}")
Parent:    ['[1, 1, 1, 1, 1]', '[1, 1, 1, 1, 0]', '[1, 1, 1, 0, 0]']
Offspring: ['[1, 1, 1, 1, 1]', '[1, 1, 0, 1, 0]', '[1, 1, 1, 0, 0]']

Put everything together, define a Controller that automates this process. The algorithm can take one

[9]:
from evokit.core import Algorithm
from typing import override

from evokit.core import Evaluator, Selector, Variator

class ExampleAlgorithm(Algorithm):

    @override
    def __init__(self,
                 population: Population[BinaryString],
                 evaluator: Evaluator[BinaryString],
                 selector: Selector[BinaryString],
                 variator: Variator[BinaryString]) -> None:
        self.population = population
        self.evaluator = evaluator
        self.selector = selector
        self.variator = variator

    @override
    def step(self) -> None:
        self.population = self.variator.vary_population(self.population)
        self.evaluator.evaluate_population(self.population)
        self.population = \
            self.selector.select_to_population(self.population)

This time, create random individuals by calling BinaryString.random.

[10]:
another_pop = Population[BinaryString]()
for _ in range(5):
    another_pop.append(BinaryString.random(size=5))

print(f"Initial population: {another_pop}")

ctrl = ExampleAlgorithm(another_pop,
                         CountBits(),
                         SimpleSelector(budget=3),
                         MutateBits(mutation_rate=0.1))
Initial population: ['[0, 0, 1, 1, 0]', '[0, 1, 1, 1, 0]', '[0, 0, 1, 0, 1]', '[0, 0, 0, 1, 0]', '[0, 1, 0, 1, 1]']

Run the algorithm. Observe that each iteration results in a new population. This is because ExampleAlgorithm replaces the entirely of its population with the survivor pool. Doing so might, as is shown in the example, result in an decrease in fitness.

[11]:
for _ in range (10):
    ctrl.step()
    print(f"Current population: {ctrl.population}")
    print(f"Current fitnesses: {[ind.fitness for ind in ctrl.population]}")
Current population: ['[1, 1, 0, 1, 1]', '[0, 0, 1, 1, 1]', '[0, 1, 1, 1, 0]']
Current fitnesses: [4, 3, 3]
Current population: ['[1, 1, 0, 1, 1]', '[0, 0, 1, 1, 1]', '[0, 1, 1, 1, 0]']
Current fitnesses: [4, 3, 3]
Current population: ['[1, 1, 0, 1, 1]', '[0, 0, 1, 1, 1]', '[0, 1, 1, 0, 0]']
Current fitnesses: [4, 3, 2]
Current population: ['[1, 1, 0, 1, 1]', '[0, 1, 1, 0, 0]', '[0, 0, 0, 1, 0]']
Current fitnesses: [4, 2, 1]
Current population: ['[1, 1, 0, 1, 1]', '[0, 1, 1, 0, 0]', '[0, 0, 0, 1, 0]']
Current fitnesses: [4, 2, 1]
Current population: ['[1, 1, 0, 1, 1]', '[0, 1, 1, 0, 0]', '[0, 0, 0, 1, 0]']
Current fitnesses: [4, 2, 1]
Current population: ['[1, 1, 0, 1, 1]', '[0, 1, 0, 0, 0]', '[0, 0, 0, 1, 0]']
Current fitnesses: [4, 1, 1]
Current population: ['[0, 1, 0, 1, 1]', '[1, 1, 0, 0, 0]', '[0, 0, 0, 0, 0]']
Current fitnesses: [3, 2, 0]
Current population: ['[0, 1, 0, 0, 1]', '[1, 1, 0, 0, 0]', '[0, 0, 0, 1, 0]']
Current fitnesses: [2, 2, 1]
Current population: ['[1, 1, 1, 0, 0]', '[0, 1, 0, 0, 0]', '[0, 0, 0, 1, 0]']
Current fitnesses: [3, 1, 1]