pymoo
Latest Version: pymoo==0.3.2

Custom Variable Type

In the following, we describe a custom variable problem. The variable is a string with a fixed length in our case. However, we formulate the problem to be easily extended to have a variable length. The objective function looks as follows:

\begin{align} \begin{split} \max f_1(x) & = & \, \# a \\[2mm] \max f_2(x) & = & \, \# b \end{split} \end{align}

The first objective is the number of a’s in the string and the second the number of b’s. For instance, for the variable “abdfgdgabb” the \(f_1(x)=2\) and \(f_2(x)=3\).

[1]:
import numpy as np
from pymoo.model.problem import Problem

class MyProblem(Problem):

    def __init__(self, n_characters=10):
        super().__init__(n_var=1, n_obj=2, n_constr=0, elementwise_evaluation=True)
        self.n_characters = n_characters
        self.ALPHABET = [c for c in string.ascii_lowercase]

    def _evaluate(self, x, out, *args, **kwargs):
        n_a, n_b = 0, 0
        for c in x[0]:
            if c == 'a':
                n_a += 1
            elif c == 'b':
                n_b += 1

        out["F"] = np.array([- n_a, - n_b], dtype=np.float)


The problem definition above define a problem with just one variable. This variable can be considered as a complex object which is in our case a string. The same principle can be used to use other data structure such as trees or lists with variable lengths. Because both objectives have to be maximized, we are minimizing their negative values.

To solve the problem the evolutionary operators sampling, crossover, mutation and duplication check needs to be implemented. Each of the modules will be shown in the following.

Sampling

Our sampling method just generates a random string which is equivalent of choosing a random letter from the alphabet (only lower case). Because of the implementation of having only one variable, we return a matrix with the shape (n,1).

[2]:
from pymoo.model.sampling import Sampling

class MySampling(Sampling):

    def _do(self, problem, n_samples, **kwargs):
        X = np.full((n_samples, 1), None, dtype=np.object)

        for i in range(n_samples):
            X[i, 0] = "".join([np.random.choice(problem.ALPHABET) for _ in range(problem.n_characters)])

        return X

Crossover

The crossover operator combines parents to create offsprings. In our framework the crossover operator retrieves the input already with predefined matings. Our crossover randomly picks a character from the first or from the second parent.

[3]:
from pymoo.model.crossover import Crossover

class MyCrossover(Crossover):
    def __init__(self):

        # define the crossover: number of parents and number of offsprings
        super().__init__(2, 2)

    def _do(self, problem, X, **kwargs):

        # The input of has the following shape (n_parents, n_matings, n_var)
        _, n_matings, n_var = X.shape

        # The output owith the shape (n_offsprings, n_matings, n_var)
        # Because there the number of parents and offsprings are equal it keeps the shape of X
        Y = np.full_like(X, None, dtype=np.object)

        # for each mating provided
        for k in range(n_matings):

            # get the first and the second parent
            a, b = X[0, k, 0], X[1, k, 0]

            # prepare the offsprings
            off_a = ["_"] * problem.n_characters
            off_b = ["_"] * problem.n_characters

            for i in range(problem.n_characters):
                if np.random.random() < 0.5:
                    off_a[i] = a[i]
                    off_b[i] = b[i]
                else:
                    off_a[i] = b[i]
                    off_b[i] = a[i]

            # join the character list and set the output
            Y[0, k, 0], Y[1, k, 0] = "".join(off_a), "".join(off_b)

        return Y

Mutation

The mutation needs to be implemented for our string object as well. We either change the order of the string (40%), randomly pick a new character with a given probability (40%) or leave the string unmodified (20%).

[4]:
from pymoo.model.mutation import Mutation

class MyMutation(Mutation):
    def __init__(self):
        super().__init__()

    def _do(self, problem, X, **kwargs):

        # for each individual
        for i in range(len(X)):

            r = np.random.random()

            # with a probabilty of 40% - change the order of characters
            if r < 0.4:
                perm = np.random.permutation(problem.n_characters)
                X[i, 0] = "".join(np.array([e for e in X[i, 0]])[perm])

            # also with a probabilty of 40% - change a character randomly
            elif r < 0.8:
                prob = 1 / problem.n_characters
                mut = [c if np.random.random() > prob
                       else np.random.choice(problem.ALPHABET) for c in X[i, 0]]
                X[i, 0] = "".join(mut)

        return X

Duplicates

Moreover, do not underestimate the importance of filtering out duplicates during the evolution. This helps to make sure a mating produces an offspring that does not already exist in the current population. If it does another mating process is triggered until unique individual do exist.

[5]:
# the input is the current population and a list of other populations.
# the function returns if an individual in pop is equal to any other individual
# in any other population.
def func_is_duplicate(pop, *other, **kwargs):
    if len(other) == 0:
        return np.full(len(pop), False)

    # value to finally return
    is_duplicate = np.full(len(pop), False)

    H = set()
    for e in other:
        for val in e:
            H.add(val.X[0])

    for i, (val, ) in enumerate(pop.get("X")):
        if val in H:
            is_duplicate[i] = True
        H.add(val)

    return is_duplicate


Optimize

Finally, we create an algorithm object with all the modules defined above.

[6]:
import string
import numpy as np

from pymoo.algorithms.nsga2 import NSGA2
from pymoo.optimize import minimize


algorithm = NSGA2(pop_size=11,
                  sampling=MySampling(),
                  crossover=MyCrossover(),
                  mutation=MyMutation(),
                  eliminate_duplicates=func_is_duplicate)

res = minimize(MyProblem(),
               algorithm,
               ('n_gen', 100),
               seed=1,
               verbose=False)

[7]:
from pymoo.visualization.scatter import Scatter
Scatter().add(res.F).show()
../_images/tutorial_custom_19_0.png
[8]:
results = res.X[np.argsort(res.F[:, 0])]
count = [np.sum([e == "a" for e in r]) for r in results[:, 0]]
print(np.column_stack([results, count]))
[['aaaaaaaaaa' 10]
 ['aaabaaaaaa' 9]
 ['aaabaabaaa' 8]
 ['bbaaaaaaab' 7]
 ['aabbaababa' 6]
 ['babbbaabaa' 5]
 ['babbbaabab' 4]
 ['bbbbbabbaa' 3]
 ['ababbbbbbb' 2]
 ['bbbbabbbbb' 1]
 ['bbbbbbbbbb' 0]]