# 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:

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

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

[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]]