# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

import random
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# %matplotlib inline
plt.rcParams['figure.figsize'] = (15 , 10)
sns.set_style('darkgrid')

function_val_epoch_elitism = []
function_val_epoch_random_search = []
function_val_epoch_basic_genetic = []
function_val_epoch_diversity = []

# ## Using Elitism 
# ### Average Results: Minimum Value of fitness is between -8.7 and -9.50. Most of the time it is near -8.7. It also depends on the population size, with higher population size like >1000, it sometimes reached to a value of -9.7.

# +
function_val_epoch_elitism = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = 1000
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
CROSSOVER_PROB = 0.1
h = 1e-7
X_SIZE = 5

np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))


def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        global CROSSOVER_PROB
        child_chromosome = []
        
        for gp1 , gp2 in zip(self.chromosome , par2.chromosome):
            child_part_chromosome = []
#             print(gp1)
            for i in range(len(gp1)):
                probability_of_crossover = random.random()
                if (probability_of_crossover > CROSSOVER_PROB):
                    # do crossover
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                else:
                    # do mutation
                    child_part_chromosome.append(self.mutate(i))
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)

generation = 1

count = 1000

population = []
for _ in range(POPULATION_SIZE):
    gnome = Individual.create_gnome()
    population.append(Individual(gnome))

while count!=0:
    count-=1
    population = sorted(population , key = lambda x:x.fitness)


    # performing elitism
    new_generation = []

    s = int(0.10*POPULATION_SIZE)
    new_generation.extend(population[:s])

    s = int(0.90*POPULATION_SIZE)

    for _ in range(s):
        parent1 = random.choice(population[:POPULATION_SIZE//2])
        parent2 = random.choice(population[:POPULATION_SIZE//2])
        child = parent1.mate(parent2)
        new_generation.append(child)

    if generation % 5 ==0:
        population = new_generation
        get_num_arr = []
        for l in population[0].chromosome:
            get_num_arr.append(getNum(l))
        print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

    function_val_epoch_elitism.append(population[0].fitness)
    generation += 1

get_num_arr = []
for l in population[0].chromosome:
    get_num_arr.append(getNum(l))
print("Gen: {} X: {}\tMinimimum Value: {}".format(generation, get_num_arr, population[0].fitness))


plt.plot(range(len(function_val_epoch_elitism)) , function_val_epoch_elitism , "k--")
# -

# ## Using Basic Genetic Algorithm
# ### The minimum value of fitness achieved is between -8.4 to -9.0.

# +
function_val_epoch_basic_genetic = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = 1000
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
CROSSOVER_PROB = 0.1
h = 1e-7
X_SIZE = 5

np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))

def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        child_chromosome = []
        global CROSSOVER_PROB
        for gp1 , gp2 in zip(self.chromosome , par2.chromosome):
            child_part_chromosome = []
#             print(gp1)
            for i in range(len(gp1)):
                probability_of_crossover = random.random()
                if (probability_of_crossover > CROSSOVER_PROB):
                    # do crossover
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                else:
                    # do mutation
                    child_part_chromosome.append(self.mutate(i))
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)

generation = 1

count = 1000

population = []

for _ in range(POPULATION_SIZE):
    gnome = Individual.create_gnome()
    population.append(Individual(gnome))

while count!=0:
    count-=1
    population = sorted(population , key = lambda x:x.fitness)


    # performing elitism
    new_generation = []

    s = int(0.10*POPULATION_SIZE)
    new_generation.extend(population[:s])

    s = int(0.90*POPULATION_SIZE)

    for _ in range(s):
        # no elitism
        parent1 = random.choice(population[:POPULATION_SIZE])
        parent2 = random.choice(population[:POPULATION_SIZE])
        child = parent1.mate(parent2)
        new_generation.append(child)

    if generation % 5 ==0:
        population = new_generation
        get_num_arr = []
        for l in population[0].chromosome:
            get_num_arr.append(getNum(l))
        print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

    function_val_epoch_basic_genetic.append(population[0].fitness)
    generation += 1

get_num_arr = []
for l in population[0].chromosome:
    get_num_arr.append(getNum(l))
print("Gen: {} X: {}\tMinimimum Value: {}".format(generation, get_num_arr, population[0].fitness))


plt.plot(range(len(function_val_epoch_basic_genetic)) , function_val_epoch_basic_genetic , "k--")
# -

# ## Using Diversity:

# +
function_val_epoch_diversity = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = 1000
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
h = 1e-7
X_SIZE = 5
DIVERSITY_PERCENT = 50


np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))

def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        global DIVERSITY_PERCENT
        tot = len(self.chromosome)
        diversity_idx_arr = np.random.choice(range(tot) , 
                                        replace=False , 
                                        size=int(DIVERSITY_PERCENT*tot / 100))
            
        child_chromosome = []
        
        for j , gp1 , gp2 in zip(range(tot) , self.chromosome , par2.chromosome):
            child_part_chromosome = []
            for i in range(len(gp1)):
                if (j*tot+i) in diversity_idx_arr:
                    child_part_chromosome.append(self.mutate(i))
                else:
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                        
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)

generation = 1

count = 1000

population = []

for _ in range(POPULATION_SIZE):
    gnome = Individual.create_gnome()
    population.append(Individual(gnome))

while count!=0:
    count-=1
    population = sorted(population , key = lambda x:x.fitness)


    # performing elitism
    new_generation = []

    s = int(0.10*POPULATION_SIZE)
    new_generation.extend(population[:s])

    s = int(0.90*POPULATION_SIZE)

    for _ in range(s):
        # no elitism
        parent1 = random.choice(population[:POPULATION_SIZE])
        parent2 = random.choice(population[:POPULATION_SIZE])
        child = parent1.mate(parent2)
        new_generation.append(child)

    if generation % 5 ==0:
        population = new_generation
        get_num_arr = []
        for l in population[0].chromosome:
            get_num_arr.append(getNum(l))
        print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

    function_val_epoch_diversity.append(population[0].fitness)
    generation += 1

get_num_arr = []
for l in population[0].chromosome:
    get_num_arr.append(getNum(l))
print("Gen: {} X: {}\tMinimimum Value: {}".format(generation, get_num_arr, population[0].fitness))


plt.plot(range(len(function_val_epoch_diversity)) , function_val_epoch_diversity , "k--")
# -

# ## Using Random Search

# +
function_val_epoch_random_search = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = 1000
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
h = 1e-7
X_SIZE = 5

np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))

def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        child_chromosome = []
        
        for gp1 , gp2 in zip(self.chromosome , par2.chromosome):
            child_part_chromosome = []
#             print(gp1)
            for i in range(len(gp1)):
                probability_of_crossover = random.random()
                if (probability_of_crossover > 0.1):
                    # do crossover
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                else:
                    # do mutation
                    child_part_chromosome.append(self.mutate(i))
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)

generation = 1

count = 1000

population = []

for _ in range(POPULATION_SIZE):
    gnome = Individual.create_gnome()
    population.append(Individual(gnome))

while count!=0:
    count-=1
    population = sorted(population , key = lambda x:x.fitness)


    # performing elitism
    new_generation = []

    s = int(0.10*POPULATION_SIZE)
    new_generation.extend(population[:s])

    s = int(0.90*POPULATION_SIZE)

    for _ in range(s):
        # Random Search
        gnome = Individual.create_gnome()
        new_generation.append(Individual(gnome))
        


    if generation % 5 ==0:
        population = new_generation
        get_num_arr = []
        for l in population[0].chromosome:
            get_num_arr.append(getNum(l))
        print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

    function_val_epoch_random_search.append(population[0].fitness)
    generation += 1

get_num_arr = []
for l in population[0].chromosome:
    get_num_arr.append(getNum(l))
print("Gen: {} X: {}\tMinimimum Value: {}".format(generation, get_num_arr, population[0].fitness))


plt.plot(range(len(function_val_epoch_random_search)) , function_val_epoch_random_search , "k--")
# -

# ## Comparison:

plt.plot(range(len(function_val_epoch_elitism)) , function_val_epoch_elitism , "k--")
plt.plot(range(len(function_val_epoch_basic_genetic)) , function_val_epoch_basic_genetic , "b--")
plt.plot(range(len(function_val_epoch_diversity)) , function_val_epoch_diversity , "r--")
plt.plot(range(len(function_val_epoch_random_search)) , function_val_epoch_random_search , "g--")
plt.legend(["with elitism" , "basic-genetic" , "with diversity" , "random search"])

# ### Clearly, Random Search is worst approach for this kind of the problems, it's complete luck.

# ## Elitism: with different sample count

# +
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = [50 ,100 , 500 , 1000]
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
CROSSOVER_PROB = 0.1
h = 1e-7
X_SIZE = 5

np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))


def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        global CROSSOVER_PROB
        child_chromosome = []
        
        for gp1 , gp2 in zip(self.chromosome , par2.chromosome):
            child_part_chromosome = []
#             print(gp1)
            for i in range(len(gp1)):
                probability_of_crossover = random.random()
                if (probability_of_crossover > CROSSOVER_PROB):
                    # do crossover
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                else:
                    # do mutation
                    child_part_chromosome.append(self.mutate(i))
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

color = ["k--" , "b--" , "r--" , "g--" , "y--"]
legend = []
for N, c in zip(POPULATION_SIZE , color):
    np.random.seed(np.random.randint(low=0 , high=100))
    random.seed(np.random.randint(low=0 , high=100))
    function_val_epoch_elitism = []
    generation = 1

    count = 1000

    population = []
    for _ in range(N):
        gnome = Individual.create_gnome()
        population.append(Individual(gnome))

    while count!=0:
        count-=1
        population = sorted(population , key = lambda x:x.fitness)


        # performing elitism
        new_generation = []

        s = int(0.10*N)
        new_generation.extend(population[:s])

        s = int(0.90*N)

        for _ in range(s):
            parent1 = random.choice(population[:N//2])
            parent2 = random.choice(population[:N//2])
            child = parent1.mate(parent2)
            new_generation.append(child)

        if generation % 5 ==0:
            population = new_generation
            get_num_arr = []
            for l in population[0].chromosome:
                get_num_arr.append(getNum(l))
            #print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

        function_val_epoch_elitism.append(population[0].fitness)
        generation += 1

    get_num_arr = []
    for l in population[0].chromosome:
        get_num_arr.append(getNum(l))
    print("Population: {} X: {}\tMinimimum Value: {}".format(N, get_num_arr, population[0].fitness))
    plt.plot(range(len(function_val_epoch_elitism)) , function_val_epoch_elitism , c)
    legend.append("N = {}".format(N))
plt.legend(legend)
# -

# The result above is as expected for a genetic algorithm with elitism. The more the size of the population the diversity and the fitness is maintained at the same time, which in principle yields (most of the time) a better result on increasing the size of the population with a suitable number of epochs.

# ## Basic Genetic Algorithm : with different sample count

# +
function_val_epoch_basic_genetic = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = [50, 100, 500, 1000]
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
CROSSOVER_PROB = 0.1
h = 1e-7
X_SIZE = 5

def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        child_chromosome = []
        global CROSSOVER_PROB
        for gp1 , gp2 in zip(self.chromosome , par2.chromosome):
            child_part_chromosome = []
#             print(gp1)
            for i in range(len(gp1)):
                probability_of_crossover = random.random()
                if (probability_of_crossover > CROSSOVER_PROB):
                    # do crossover
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                else:
                    # do mutation
                    child_part_chromosome.append(self.mutate(i))
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)
color = ["k--" , "b--" , "r--" , "g--" , "y--"]
legend = []
for N, c in zip(POPULATION_SIZE , color):
    np.random.seed(np.random.randint(low=0 , high=100))
    random.seed(np.random.randint(low=0 , high=100))
    
    function_val_epoch_basic_genetic = []

    generation = 1

    count = 1000

    population = []

    for _ in range(N):
        gnome = Individual.create_gnome()
        population.append(Individual(gnome))

    while count!=0:
        count-=1
        population = sorted(population , key = lambda x:x.fitness)


        # performing elitism
        new_generation = []

        s = int(0.10*N)
        new_generation.extend(population[:s])

        s = int(0.90*N)

        for _ in range(s):
            # no elitism
            parent1 = random.choice(population[:N])
            parent2 = random.choice(population[:N])
            child = parent1.mate(parent2)
            new_generation.append(child)

        if generation % 5 ==0:
            population = new_generation
            get_num_arr = []
            for l in population[0].chromosome:
                get_num_arr.append(getNum(l))
            #print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

        function_val_epoch_basic_genetic.append(population[0].fitness)
        generation += 1

    get_num_arr = []
    for l in population[0].chromosome:
        get_num_arr.append(getNum(l))

    print("Population: {} X: {}\tMinimimum Value: {}".format(N, get_num_arr, population[0].fitness))
    plt.plot(range(len(function_val_epoch_basic_genetic)) , function_val_epoch_basic_genetic , c)
    legend.append("N = {}".format(N))

plt.legend(legend)
# -

# Unlike genetic algorithm with elitism, in basic genetic algorithm the complete population get the chance to mate(crossover and mutation) which may or may not improve the results on increasing the size of the population, because increasing the size of the population also expose us to the risk that elite members will not get chance to mate.

# ## Diversity: with the different sample counts

# +
function_val_epoch_diversity = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = [50 , 100 , 500 , 1000]
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
h = 1e-7
X_SIZE = 5
DIVERSITY_PERCENT = 50


np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))

def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        global DIVERSITY_PERCENT
        tot = len(self.chromosome)
        diversity_idx_arr = np.random.choice(range(tot) , 
                                        replace=False , 
                                        size=int(DIVERSITY_PERCENT*tot / 100))
            
        child_chromosome = []
        
        for j , gp1 , gp2 in zip(range(tot) , self.chromosome , par2.chromosome):
            child_part_chromosome = []
            for i in range(len(gp1)):
                if (j*tot+i) in diversity_idx_arr:
                    child_part_chromosome.append(self.mutate(i))
                else:
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                        
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)
color = ["k--" , "b--" , "r--" , "g--" , "y--"]
legend = []
for N, c in zip(POPULATION_SIZE , color):
    np.random.seed(np.random.randint(low=0 , high=100))
    random.seed(np.random.randint(low=0 , high=100))
    
    function_val_epoch_diversity = []

    generation = 1

    count = 1000

    population = []

    for _ in range(N):
        gnome = Individual.create_gnome()
        population.append(Individual(gnome))

    while count!=0:
        count-=1
        population = sorted(population , key = lambda x:x.fitness)


        # performing elitism
        new_generation = []

        s = int(0.10*N)
        new_generation.extend(population[:s])

        s = int(0.90*N)

        for _ in range(s):
            # no elitism
            parent1 = random.choice(population[:N])
            parent2 = random.choice(population[:N])
            child = parent1.mate(parent2)
            new_generation.append(child)

        if generation % 5 ==0:
            population = new_generation
            get_num_arr = []
            for l in population[0].chromosome:
                get_num_arr.append(getNum(l))
            #print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

        function_val_epoch_diversity.append(population[0].fitness)
        generation += 1

    get_num_arr = []
    for l in population[0].chromosome:
        get_num_arr.append(getNum(l))
    print("Population: {} X: {}\tMinimimum Value: {}".format(N, get_num_arr, population[0].fitness))
    plt.plot(range(len(function_val_epoch_diversity)) , function_val_epoch_diversity , c)
    legend.append("N = {}".format(N))
plt.legend(legend)
# -

# I don't even need to state that this is the best results by far we have got. Just like the elitism it is also affected by increasing the population size and the overall trend is that the performance(on an average) increases.

# ## Random Search : with different sample counts

# +
function_val_epoch_random_search = []
RANGE_OF_X = [-2.04 , 2.04]
POPULATION_SIZE = [50, 100, 500, 1000]
GENES = ["01" , "012" , "0123456789", "0123456789"]
TARGET_LENGTH = 4
h = 1e-7
X_SIZE = 5

np.random.seed(np.random.randint(low=0 , high=100))
random.seed(np.random.randint(low=0 , high=100))

def f1(X):
    return np.sum(np.square(X))

def f2(X):
    return np.sum(np.floor(X))

def f3(X):
    return np.sum(np.multiply(np.arange(len(X)) , np.power(X , 4) ) ) + np.random.standard_normal(1)[0]

def g(X):
    return f1(X) + f2(X) + f3(X)

def determine_target_length(range_of_x):
    n = max(range_of_x)
    return int(np.ceil(np.log(n)/np.log(2)))

def getNum(l):
    num_str=""
    if l[0] == "1":
        num_str+="-"
    num_str += "{}.{}{}".format(l[1] , l[2], l[3])
    return float(num_str)

def inRange(l , range_of_x):
    num = getNum(l)
    return min(range_of_x)<= num <= max(range_of_x)

class Individual(object):

    def __init__(self,chromosome):
        self.chromosome = chromosome
        self.fitness = self.calculate_fitness()

    @classmethod 
    def mutate(self , digit_num:int):
        global GENES
        return random.choice(GENES[digit_num])
    
    @classmethod
    def create_gnome(self):
        global TARGET_LENGTH
        global RANGE_OF_X
        global X_SIZE
        gnome = []
        for i in range(X_SIZE):
            while True:
                l = [self.mutate(i) for i in range(TARGET_LENGTH)]
                if (inRange(l , RANGE_OF_X)):
                    gnome.append(l)
                    break
        return gnome


    def mate(self , par2):
        child_chromosome = []
        
        for gp1 , gp2 in zip(self.chromosome , par2.chromosome):
            child_part_chromosome = []
#             print(gp1)
            for i in range(len(gp1)):
                probability_of_crossover = random.random()
                if (probability_of_crossover > 0.1):
                    # do crossover
                    probability_of_p1_gene = random.random()
                    if probability_of_p1_gene > 0.5:
                        child_part_chromosome.append(gp1[i])
                    else:
                        child_part_chromosome.append(gp2[i])
                else:
                    # do mutation
                    child_part_chromosome.append(self.mutate(i))
            child_chromosome.append(child_part_chromosome)
        return Individual(child_chromosome)

    def calculate_fitness(self):
        global TARGET_LENGTH
        X = []
        for s in self.chromosome:
            #print(s)
            #s = ''.join(map(str, self.chromosome))
            x = getNum(s)
            X.append(x)
        return g(X)
        


global POPULATION_SIZE
global TARGET_LENGTH
global RANGE_OF_X

# TARGET_LENGTH = determine_target_length(RANGE_OF_X)
color = ["k--" , "b--" , "r--" , "g--" , "y--"]
legend = []
for N, c in zip(POPULATION_SIZE , color):
    np.random.seed(np.random.randint(low=0 , high=100))
    random.seed(np.random.randint(low=0 , high=100))
    
    function_val_epoch_random_search = []
    generation = 1

    count = 1000

    population = []

    for _ in range(N):
        gnome = Individual.create_gnome()
        population.append(Individual(gnome))

    while count!=0:
        count-=1
        population = sorted(population , key = lambda x:x.fitness)


        # performing elitism
        new_generation = []

        s = int(0.10*N)
        new_generation.extend(population[:s])

        s = int(0.90*N)

        for _ in range(s):
            # Random Search
            gnome = Individual.create_gnome()
            new_generation.append(Individual(gnome))



        if generation % 5 ==0:
            population = new_generation
            get_num_arr = []
            for l in population[0].chromosome:
                get_num_arr.append(getNum(l))
            # print("Gen: {} X: {} Fit: {}".format(generation, get_num_arr, population[0].fitness))

        function_val_epoch_random_search.append(population[0].fitness)
        generation += 1

    get_num_arr = []
    for l in population[0].chromosome:
        get_num_arr.append(getNum(l))
    print("Population: {} X: {}\tMinimimum Value: {}".format(N, get_num_arr, population[0].fitness))
    plt.plot(range(len(function_val_epoch_random_search)) , function_val_epoch_random_search , c)
    legend.append("N = {}".format(N))
plt.legend(legend)
# -

# Clearly, Random Search is completely out of this league. It doesn't stand a chance while comparing with the other algorithms. I won't even try it again. It's complete brute force. If you are very lucky(which is impossible) then you have a chance of getting a good result through random search.



StarcoderdataJupyterScriptsDedupFiltered