import math
import random
import statistics
from abc import ABC, abstractmethod
from copy import deepcopy

###############################################################
#                           Gene                              #
###############################################################


class Gene(ABC):
    def __init__(self):
        self.bit = None

    @abstractmethod
    def mutate(self):
        pass

    @abstractmethod
    def __str__(self):
        pass

    @abstractmethod
    def __repr__(self):
        pass


###############################################################
#                       Individual                            #
###############################################################


class Individual(ABC):
    def __init__(self, parameters, empty=False):
        self.sequence = []
        self.parameters = parameters

    def __getitem__(self, key):
        return self.sequence[key]

    def __setitem__(self, key, value):
        self.sequence[key] = value
        return value

    @abstractmethod
    def fitness(self):
        pass

    @abstractmethod
    def crossover(self, other):
        """
        crossover used if you use the crossover 'individual
        :param other: the second parent for the crossover
        :type other: Individual
        :return: the
        """

    @abstractmethod
    def mutate(self):
        """
        mutation called if you use the mutation 'individual'
        if you need to mutate on the individual instead of the gene
        :return: None
        """

    @abstractmethod
    def __eq__(self, other):
        pass

    def __str__(self):
        return repr(self)

    @abstractmethod
    def __repr__(self):
        pass

    @abstractmethod
    def __hash__(self):
        pass


class IndividualPermutation(Individual, ABC):
    @abstractmethod
    def basic_order(self):
        pass


###############################################################
#                       Population                            #
###############################################################


class Population:
    def __init__(self, parameters, description=False):
        self.individuals = []
        self.parameters = parameters
        self.stats = {
            "max_fitness": [],
            "min_fitness": [],
            "mean_fitness": [],
            "fitness_diversity": [],
            "total_fitness": [],
            "diversity": [],
            "max_age": [],
            "mean_age": [],
            "parameters": parameters,
            "utility": [],
        }
        self.selected = []
        self.crossed = []
        self.mutated = []
        self.nb_turns = 0

        if "function_each_turn" in self.parameters:
            self.each_turn_fct = self.parameters["function_each_turn"]
        else:
            self.each_turn_fct = None

        if "function_end" in self.parameters:
            self.end_fct = self.parameters["function_end"]
        else:
            self.end_fct = None

        if "termination_condition" in self.parameters:
            self.final_condition = self.parameters["termination_condition"]
        else:
            self.final_condition = lambda pop: not (
                pop.nb_turns == pop.parameters["nb turn max"]
            )

        self.individual_class = parameters["individual"]

        individuals = []
        for _ in range(self.parameters["population size"]):
            individuals.append(self.individual_class(parameters))

        fits = [i.fitness() for i in individuals]
        self.individuals = [
            (i, f, 0) for i, f in sorted(zip(individuals, fits), key=lambda z: z[1])
        ]
        if self.parameters["proportion selection"] > 1:
            self.nb_select = self.parameters["proportion selection"]
        else:
            self.nb_select = int(
                len(self.individuals) * self.parameters["proportion selection"]
            )
        if description:
            description = (
                f"Population parameters :"
                f"\n\tIndividuals : {self.individual_class}"
                f'\n\tSize of an individual : {self.parameters["chromosome size"]}'
                f'\n\tSize of the population : {self.parameters["population size"]}'
                f'\n\tnb turn max : {self.parameters["nb turn max"]}'
                f"\n\tNumber of individuals selected each turns : {self.nb_select}"
                f'\n\tSelection : {self.parameters["selection"]}'
                f'\n\tCrossover : {self.parameters["crossover"]} ('
                f'{self.parameters["proportion crossover"] * 100}%)'
                f'\n\tMutation : {self.parameters["mutation"]} ('
                f'{self.parameters["proportion mutation"] * 100}%)'
                f'\n\tInsertion : {self.parameters["insertion"]}'
            )
            print(description)
        self.method_switch = dict()
        self.method_switch["selection"] = {
            "select_random": self.select_random,
            "select_best": self.select_best,
            "select_tournament": self.select_tournament,
            "select_wheel": self.select_wheel,
        }
        self.method_switch["crossover"] = {
            "individual": self.crossover_individual,
            "order 1": self.crossover_order_1,
            "pmx": self.crossover_pmx,
            "mono-point": self.crossover_monopoint,
            "multipoint": self.crossover_multipoint,
            "uniforme": self.crossover_uniforme,
        }
        self.method_switch["mutation"] = {
            "individual": self.mutation_individu,
            "insert": self.mutation_insert,
            "swap": self.mutation_swap,
            "inversion": self.mutation_inversion,
            "scramble": self.mutation_scramble,
            "1-flip": self.mutation_1fip,
            "3-flip": self.mutation_3fip,
            "5-flip": self.mutation_5fip,
            "bit-flip": self.mutation_bitfip,
        }

    def sort_individuals_fitness(self):
        self.individuals.sort(key=lambda i: i[1], reverse=True)

    def sort_individuals_age(self):
        self.individuals.sort(key=lambda i: i[2], reverse=False)

    def population_get_older(self):
        self.nb_turns += 1
        self.individuals = [(i, f, (a + 1)) for i, f, a in self.individuals]

    def __repr__(self):
        s = f"Population size : {len(self.individuals)}"
        for i in self.individuals:
            s += "\n" + str(i)
        return s

    def start(self):
        self.statistic()
        while self.final_condition(self):
            if self.each_turn_fct:
                self.each_turn_fct(self)
            self.population_get_older()
            self.sort_individuals_fitness()
            self.turn()
            self.statistic()
        if self.end_fct:
            self.end_fct(self)

    def turn(self):
        self.selection()
        self.crossover()
        self.mutation()
        self.insertion()

    ###############################################################
    #                       adaptative                            #
    ###############################################################

    def adaptative(self, method):
        switch = {
            "fixed roulette wheel": self.adaptative_fixe,
            "adaptive roulette wheel": self.adaptive_roulette_wheel,
            "adaptive pursuit": self.adaptative_poursuit,
            "UCB": self.adaptative_ucb,
        }
        switch[self.parameters[method][1]](method)

    def adaptative_fixe(self, method):
        proba, methode = list(map(list, zip(*self.parameters[method][2])))
        selected_method = random.choices(range(len(methode)), proba, k=1)[0]
        self.method_switch[method][self.parameters[method][2][selected_method][1]]()

    def adaptive_roulette_wheel(self, method):
        pmin = self.parameters[method][3]
        switch = {
            "selection": 0,
            "crossover": 1,
            "mutation": 2,
        }
        rank = switch[method]
        utility = list(map(list, zip(*self.stats["utility"][:-1])))
        utility = utility[-20:]
        n = len(self.parameters[method][2])
        if len(utility) >= rank:
            sum_uk = sum([u for u, _ in utility[rank]])
            for i in range(len(self.parameters[method][2])):
                m = self.parameters[method][2][i][1]
                u_n = sum([u for u, me in utility[rank] if m == me])
                self.parameters[method][2][i][0] = pmin + (1 - n * pmin) * (
                    u_n / sum_uk if sum_uk != 0 else 1
                )
        self.adaptative_fixe(method)

    def adaptative_poursuit(self, method):
        switch = {
            "selection": 0,
            "crossover": 1,
            "mutation": 2,
        }
        rank = switch[method]
        utility = list(map(list, zip(*self.stats["utility"][:-1])))
        if len(utility) % 10 == 0:
            for i in range(len(self.parameters[method][2])):
                self.parameters[method][2][i][0] = 1 / len(self.parameters[method][2])
        utility = utility[-20:]
        if len(utility) >= rank:
            pmin = self.parameters[method][3]
            pmax = 1 - (len(self.parameters[method][2]) - 1) * pmin
            beta = self.parameters[method][4]
            past_selected = utility[rank]
            utilities = []
            for i in range(len(self.parameters[method][2])):
                methode = self.parameters[method][2][i][1]
                utilities.append(sum([u for u, me in past_selected if methode in me]))
            maxi = max(utilities)
            indices = [i for i, v in enumerate(utilities) if v == maxi]
            best = random.choice(indices)
            for i in range(len(self.parameters[method][2])):
                tminus1 = self.parameters[method][2][i][0]
                if i == best:
                    self.parameters[method][2][i][0] = tminus1 + beta * (pmax - tminus1)
                else:
                    self.parameters[method][2][i][0] = tminus1 + beta * (pmin - tminus1)
        self.adaptative_fixe(method)

    def adaptative_ucb(self, method):
        switch = {
            "selection": 0,
            "crossover": 1,
            "mutation": 2,
        }
        rank = switch[method]
        utility = list(map(list, zip(*self.stats["utility"][:-1])))
        if len(utility) >= rank:
            past_selected = utility[rank]
            exploitations = []
            explorations = []
            for i in range(len(self.parameters[method][2])):
                methode = self.parameters[method][2][i][1]
                nb_i = [m[1] for m in past_selected].count(methode)
                exploitations.append(
                    sum([u for u, me in past_selected if methode in me])
                )
                explorations.append(
                    math.sqrt(2 * math.log(len(past_selected)) / (nb_i + 1))
                )
            for i in range(len(self.parameters[method][2])):
                self.parameters[method][2][i][0] = exploitations[i] + explorations[i]
        maxi = max(self.parameters[method][2], key=lambda e: e[0])[0]
        indices = [i for i, v in enumerate(self.parameters[method][2]) if v[0] == maxi]
        choosen = random.choice(indices)
        self.method_switch[method][self.parameters[method][2][choosen][1]]()

    ###############################################################
    #                       Selection                             #
    ###############################################################

    def selection(self):
        """
        chose the type of selection to use and select individuals
        :return:
        """
        self.selected = []
        if self.parameters["selection"][0] == "adaptative":
            self.adaptative("selection")
        else:
            switch = {
                "select_random": self.select_random,
                "select_best": self.select_best,
                "select_tournament": self.select_tournament,
                "select_wheel": self.select_wheel,
            }
            switch[self.parameters["selection"][0]]()
        self.stats["utility"][-1][0][0] = statistics.mean(
            self.stats["utility"][-1][0][0]
        )

    def select_random(self):
        selected = random.sample(self.individuals, self.nb_select)
        self.stats["utility"].append([[[f for _, f, _ in selected], "select_random"]])
        self.selected = deepcopy([i for i, _, _ in selected])

    def select_best(self):
        selected = self.individuals[0 : self.nb_select]
        self.stats["utility"].append([[[f for _, f, _ in selected], "select_best"]])
        self.selected = deepcopy([i for i, _, _ in selected])

    def select_tournament(self):
        nb_winners = 2
        nb_players = 5
        self.stats["utility"].append([[[], "select_tournament"]])
        while len(self.selected) < self.nb_select:
            selected = random.sample(self.individuals, nb_players)
            selected.sort(key=lambda i: i[1], reverse=True)
            selected = selected[0:nb_winners]
            self.stats["utility"][-1][0][0].extend([f for _, f, _ in selected])
            self.selected.extend(deepcopy([i for i, _, _ in selected]))
        self.selected = self.selected[0 : self.nb_select]

    def select_wheel(self):
        individuals, fits, _ = list(zip(*self.individuals))
        total = sum(fits)
        wheel = [1] * len(fits) if total == 0 else [f / total for f in fits]
        probabilities = [sum(wheel[: i + 1]) for i in range(len(wheel))]
        self.stats["utility"].append([[[], "select_wheel"]])
        for n in range(self.nb_select):
            r = random.random()
            for (i, individual) in enumerate(individuals):
                if r <= probabilities[i]:
                    self.stats["utility"][-1][0][0].append(individual.fitness())
                    self.selected.append(deepcopy(individual))
                    break

    ###############################################################
    #                       Crossover                             #
    ###############################################################

    def crossover(self):
        self.crossed = []
        if self.parameters["crossover"][0] == "adaptative":
            self.adaptative("crossover")
        else:
            switch = {
                "individual": self.crossover_individual,
                "mono-point": self.crossover_monopoint,
                "multipoint": self.crossover_multipoint,
                "uniforme": self.crossover_uniforme,
                "order 1": self.crossover_order_1,
                "pmx": self.crossover_pmx,
            }
            switch[self.parameters["crossover"][0]]()

    def crossover_individual(self):
        self.stats["utility"][-1].append([[], "individual"])
        for i in range(0, len(self.selected), 2):
            first_child, second_child = None, None
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                first_child, second_child = i1.crossover(i2)
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_monopoint(self):
        self.stats["utility"][-1].append([[], "mono-point"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                rand = random.randint(1, len(i1.sequence))
                first_child[::] = i1[0:rand] + i2[rand:]
                second_child[::] = i2[0:rand] + i1[rand:]
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_multipoint(self):
        self.stats["utility"][-1].append([[], "multipoint"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                rand1 = random.randint(1, len(i1.sequence) - 2)
                rand2 = random.randint(rand1, len(i1.sequence))
                first_child[::] = (
                    deepcopy(i1[0:rand1])
                    + deepcopy(i2[rand1:rand2])
                    + deepcopy(i1[rand2:])
                )
                second_child[::] = (
                    deepcopy(i2[0:rand1])
                    + deepcopy(i1[rand1:rand2])
                    + deepcopy(i2[rand2:])
                )
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_uniforme(self):
        self.stats["utility"][-1].append([[], "uniforme"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                for j in range(self.parameters["chromosome size"]):
                    first_child[j], second_child[j] = (
                        (i1[j], i2[j]) if random.random() <= 0.5 else (i2[j], i1[j])
                    )
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_order_1(self):
        """ """
        # prendre deux val random, la 2nd plus grande que la 1ere
        # copie du parent 1 dans le child 1
        self.stats["utility"][-1].append([[], "order 1"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters, empty=True)
            second_child = self.individual_class(self.parameters, empty=True)
            if random.random() <= self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                size = len(first_child.sequence)
                rand1 = random.randint(0, size - 2)
                rand2 = random.randint(rand1, size)
                first_child[rand1:rand2] = deepcopy(i1[rand1:rand2])
                second_child[rand1:rand2] = deepcopy(i2[rand1:rand2])
                k = rand2
                j = rand2
                i = 0
                while None in first_child.sequence:
                    i += 1
                    k %= size
                    j %= size
                    if not i2[k] in first_child.sequence:
                        first_child[j] = deepcopy(i2[k])
                        j += 1
                    k += 1
                i = 0
                k = rand2
                j = rand2
                while None in second_child.sequence:
                    i += 1
                    k %= size
                    j %= size
                    if not i1[k] in second_child.sequence:
                        second_child[j] = deepcopy(i1[k])
                        j += 1
                    k += 1
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_pmx(self):
        self.stats["utility"][-1].append([[], "pmx"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            if random.random() <= self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]

                size = len(first_child.sequence)
                rand1 = random.randint(0, size - 2)
                rand2 = random.randint(rand1 + 1, size - 1)
                indexes = i1.basic_order()
                l1 = [indexes.index(i1[j]) for j in range(size)]
                l2 = [indexes.index(i2[j]) for j in range(size)]

                p1 = [None] * size
                p2 = [None] * size
                for j in range(size):
                    p1[l1[j]] = j
                    p2[l2[j]] = j
                for j in range(rand1, rand2):
                    val1 = l1[j]
                    val2 = l2[j]
                    l1[j], l1[p1[val2]] = val2, val1
                    p1[val1], p1[val2] = p1[val2], p1[val1]
                    l2[j], l2[p2[val1]] = val1, val2
                    p2[val1], p2[val2] = p2[val2], p2[val1]

                c1 = [indexes[l1[j]] for j in range(size)]
                c2 = [indexes[l2[j]] for j in range(size)]
                first_child.sequence = deepcopy(c1)
                second_child.sequence = deepcopy(c2)
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    ###############################################################
    #                       Mutation                              #
    ###############################################################

    def mutation(self):
        self.mutated = []
        if self.parameters["mutation"][0] == "adaptative":
            self.adaptative("mutation")
        else:
            switch = {
                "individual": self.mutation_individu,
                "1-flip": self.mutation_1fip,
                "3-flip": self.mutation_3fip,
                "5-flip": self.mutation_5fip,
                "bit-flip": self.mutation_bitfip,
                "insert": self.mutation_insert,
                "swap": self.mutation_swap,
                "inversion": self.mutation_inversion,
                "scramble": self.mutation_scramble,
            }
            switch[self.parameters["mutation"][0]]()

    def mutation_individu(self):
        self.stats["utility"][-1].append([[], "individual"])
        for indiv in self.crossed:
            indiv.mutate()
            self.mutated.append(indiv)

    def mutation_1fip(self):
        self.stats["utility"][-1].append([[], "1-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                for i in random.sample(range(len(indiv.sequence)), 1):
                    indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_3fip(self):
        self.stats["utility"][-1].append([[], "3-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                for i in random.sample(range(len(indiv.sequence)), 3):
                    indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_5fip(self):
        self.stats["utility"][-1].append([[], "5-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                for i in random.sample(range(len(indiv.sequence)), 5):
                    indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_bitfip(self):
        self.stats["utility"][-1].append([[], "bit-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                p = 1 / length
                for i in range(length):
                    if random.random() <= p:
                        indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_insert(self):
        """
        prendre deux points, le second devient le suivant du premier
        """
        self.stats["utility"][-1].append([[], "insert"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                indiv[rand1 + 1], indiv[rand2] = indiv[rand2], indiv[rand1 + 1]
            self.mutated.append(indiv)

    def mutation_swap(self):
        """
        prendre deux points et les echanger
        """
        self.stats["utility"][-1].append([[], "swap"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                indiv[rand1], indiv[rand2] = indiv[rand2], indiv[rand1]
            self.mutated.append(indiv)

    def mutation_inversion(self):
        """
        inverser toutes les valeurs entre deux points
        """
        self.stats["utility"][-1].append([[], "invertion"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                indiv[rand1:rand2] = indiv[rand1:rand2][::-1]
            self.mutated.append(indiv)

    def mutation_scramble(self):
        """
        melanger toutes les valeurs entre deux points
        """
        self.stats["utility"][-1].append([[], "scramble"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                randlist = indiv[rand1:rand2]
                random.shuffle(randlist)
                indiv[rand1:rand2] = randlist
            self.mutated.append(indiv)

    ###############################################################
    #                       Insertion                             #
    ###############################################################

    def insertion(self):
        if self.parameters["insertion"] == "fitness":
            self.sort_individuals_fitness()
        elif self.parameters["insertion"] == "age":
            self.sort_individuals_age()
        if len(self.mutated):
            self.individuals = self.individuals[: -len(self.mutated)]
            for i in self.mutated:
                f = i.fitness()
                self.stats["utility"][-1][-1][0].append(f)
                self.individuals.append((i, f, 0))
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )
        selection = self.stats["utility"][-1][0][0]
        crossover = self.stats["utility"][-1][1][0]
        mutation = self.stats["utility"][-1][2][0]
        u_selection = (mutation - selection) / self.parameters["chromosome size"]
        u_crossover = (crossover - selection) / self.parameters["chromosome size"]
        u_mutation = (mutation - crossover) / self.parameters["chromosome size"]
        self.stats["utility"][-1][0][0] = u_selection
        self.stats["utility"][-1][1][0] = u_crossover
        self.stats["utility"][-1][2][0] = u_mutation

    ###############################################################
    #                       Statistic                             #
    ###############################################################

    def statistic(self):
        self.sort_individuals_fitness()
        i = [i for i, _, _ in self.individuals]
        fits = [f for _, f, _ in self.individuals]
        ages = [a for _, _, a in self.individuals]
        self.stats["total_fitness"].append(sum(fits))
        self.stats["max_fitness"].append(fits[0])
        self.stats["min_fitness"].append(fits[-1])
        self.stats["mean_fitness"].append(statistics.mean(fits))
        self.stats["fitness_diversity"].append(len(set(i)))
        self.stats["diversity"].append(len(set(self.individuals)))
        self.stats["max_age"].append(max(ages))
        self.stats["mean_age"].append(statistics.mean(ages))


def mainpermut():
    number_of_team = 6
    from algo_gen.individuals.STS import IndividualSTS

    def final_condition(population):
        if population.stats["max_fitness"][-1] == 0:
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    parameters = {
        "configuration name": "config1",
        "individual": IndividualSTS,
        "only weeks": False,
        "number of team": number_of_team,
        "population size": 100,
        "chromosome size": (number_of_team - 1) * (number_of_team // 2),
        "termination_condition": final_condition,
        "nb turn max": 10000,
        # 'stop after no change': 10000,
        "selection": ["select_tournament"],
        "proportion selection": 0.2,  # 0.04,  # 2 / population_size
        "crossover": ["pmx"],
        "proportion crossover": 1,
        "mutation":
        # ['inversion'],
        [
            "adaptative",
            "adaptive pursuit",
            # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB'
            [[0.25, "insert"], [0.25, "swap"], [0.25, "inversion"], [0.25, "scramble"]],
            0.1,
            0.3,
        ],
        # 'insert'
        # 'swap'
        # 'inversion'
        # 'scramble'
        "proportion mutation": 0.5,  # 0.1 0.2 0.5 0.8
        "insertion": "age",  # 'age' 'fitness'
    }
    crossover = [["order 1"], ["pmx"]]
    mutation = [["insert"], ["swap"], ["inversion"], ["scramble"]]
    for cross in crossover:
        parameters["crossover"] = cross
        for mut in mutation:
            parameters["mutation"] = mut
            population = Population(parameters)
            population.start()
            from algo_gen.tools.plot import show_stats

            show_stats(population.stats)

    utility = list(map(list, zip(*population.stats["utility"])))
    val = list(map(list, zip(*utility[2])))[1]

    count = {
        "insert": [0],
        "swap": [0],
        "inversion": [0],
        "scramble": [0],
    }
    for v in val:
        for m in ["insert", "swap", "inversion", "scramble"]:
            count[m].append(count[m][-1] + (1 if v == m else 0))

    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(figsize=(10, 10))

    for m in count.keys():
        ax.plot(list(range(len(count[m]))), count[m], label=m)
    plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)
    import textwrap

    plt.title(
        "\n".join(
            textwrap.wrap(str(population.stats["parameters"]["mutation"][1]), 120)
        )
    )
    plt.show()


def main():
    from algo_gen.individuals.onemax import IndividualOneMax

    def final_condition(population):
        if (
            population.stats["max_fitness"][-1]
            == population.parameters["chromosome size"]
        ):
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    # def final_condition(population):
    #     if population.stats['max_fitness'][-1] == population.parameters['chromosome size']:
    #         return False
    #     if population.nb_turns >= population.parameters['stop after no change']:
    #         last_max = population.stats['max_fitness'][-population.parameters['stop after no change']:]
    #         # last_min = self.stats['min_fitness'][-self.parameters['stop after no change']:]
    #         max_change = not all(x >= y for x, y in zip(last_max, last_max[1:]))
    #         # min_change = not all(x >= y for x, y in zip(last_min, last_min[1:]))
    #         return (not (population.nb_turns == population.parameters['nb turn max'])) and max_change
    #         # and not (not max_change and not min_change)
    #     else:
    #         return not (population.nb_turns == population.parameters['nb turn max'])

    # def function_each_turn(population):
    #     pass
    #
    # def function_end(population):
    #     print(f'fitness max : {population.stats["max_fitness"][-1]}')

    param = {
        "configuration name": "config1",
        "individual": IndividualOneMax,
        "population size": 100,  # 100 200 500
        "chromosome size": 100,  # 5 10 50 100
        "termination_condition": final_condition,
        # 'function_each_turn': function_each_turn,
        # 'function_end': function_end,
        "nb turn max": 5000,
        # 'stop after no change': 5000000,  # int(config['nb turn max']*0.10),
        "selection": ["select_tournament"],
        #     ['adaptative',
        #      'UCB',
        #      [
        #          [0.25, 'select_random'],
        #          [0.25, 'select_best'],
        #          [0.25, 'select_tournament'],
        #          [0.25, 'select_wheel']
        #      ]],
        "proportion selection": 0.02,  # 0.04,  # 2 / population_size
        "crossover": ["uniforme"],
        #     ['adaptative',
        #      'UCB',
        #      [
        #          [0.25, 'mono-point'],
        #          [0.25, 'uniforme'],
        #      ],
        #      0.5],
        "proportion crossover": 1,
        "mutation": ["individual"],
        #     ['adaptative',
        #      'UCB',
        #      # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB'
        #      [
        #          [0.25, '1-flip'],
        #          [0.25, '3-flip'],
        #          [0.25, '5-flip'],
        #          [0.25, 'bit-flip']
        #      ],
        #      0.05,  # pmin for adaptive roulette wheel and adaptive poursuite
        #      0.5,  # beta for adaptive poursuit
        #      ],
        "proportion mutation": 1,  # 0.1 0.2 0.5 0.8
        "insertion": "fitness",  # 'age' 'fitness'
    }
    # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB' 'DMAB'

    # pmin = 0.15
    # beta = 1.5
    diff_mutation = [
        # ['adaptative', 'fixed roulette wheel',
        #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        # ['adaptative', 'adaptive pursuit',
        #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], pmin, beta, ],
        # ['adaptative', 'UCB',
        #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], ],
    ]

    for mu in diff_mutation:
        param["mutation"] = mu
        population = Population(param)
        population.start()
        print(population.parameters["mutation"])
        utility = list(map(list, zip(*population.stats["utility"])))
        val = list(map(list, zip(*utility[2])))[1]

        count = {
            "1-flip": [0],
            "3-flip": [0],
            "5-flip": [0],
            "bit-flip": [0],
        }
        for v in val:
            for m in ["1-flip", "3-flip", "5-flip", "bit-flip"]:
                count[m].append(count[m][-1] + (1 if v == m else 0))

        import matplotlib.pyplot as plt

        fig, ax = plt.subplots(figsize=(10, 10))

        for m in count.keys():
            ax.plot(list(range(len(count[m]))), count[m], label=m)
        plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)

        plt.title(population.stats["parameters"]["mutation"][1])
        plt.show()

        from algo_gen.tools.plot import show_stats

        show_stats(population.stats)


def main_meilleur():
    from algo_gen.individuals.onemax import IndividualOneMax

    def final_condition(population):
        if (
            population.stats["max_fitness"][-1]
            == population.parameters["chromosome size"]
        ):
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    parametres = {
        "individual": IndividualOneMax,
        "population size": 100,
        "chromosome size": 100,
        "termination_condition": final_condition,
        "nb turn max": 10000,
        "selection": ["select_best"],
        "proportion selection": 0.60,
        "crossover": ["uniforme"],
        "proportion crossover": 1,
        "mutation": [
            "adaptative",
            "UCB",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
        ],
        "proportion mutation": 1,
        "insertion": "fitness",
    }
    population = Population(parametres)
    population.start()
    # utility = list(map(list, zip(*population.stats['utility'])))
    # val = list(map(list, zip(*utility[2])))[1]
    #
    # count = {'1-flip': [0],
    #          '3-flip': [0],
    #          '5-flip': [0],
    #          'bit-flip': [0],
    #          }
    # for v in val:
    #     for m in ['1-flip', '3-flip', '5-flip', 'bit-flip']:
    #         count[m].append(count[m][-1] + (1 if v == m else 0))
    #
    # import matplotlib.pyplot as plt
    #
    # fig, ax = plt.subplots(figsize=(10, 10))
    #
    # for m in count.keys():
    #     ax.plot(list(range(len(count[m]))), count[m], label=m)
    # plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
    # import textwrap
    #
    # plt.title("\n".join(textwrap.wrap(str(population.stats['parameters']['mutation'][1]), 120)))
    # plt.show()
    from algo_gen.tools.plot import show_stats

    show_stats(population.stats)


def main_meilleur_perm():
    from algo_gen.individuals.STS import IndividualSTS

    def final_condition(population):
        if population.stats["max_fitness"][-1] == 0:
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    number_of_team = 14

    parametres = {
        "individual": IndividualSTS,
        "only weeks": True,
        "number of team": number_of_team,
        "population size": 100,
        "chromosome size": (number_of_team - 1) * (number_of_team // 2),
        "termination_condition": final_condition,
        "nb turn max": 5000,
        "selection": ["select_tournament"],
        "proportion selection": 0.2,
        "crossover": ["pmx"],
        "proportion crossover": 1,
        "mutation": ["swap"],
        "proportion mutation": 0.2,
        "insertion": "age",
    }
    population = Population(parametres)
    population.start()

    from algo_gen.tools.plot import show_stats

    show_stats(population.stats)


if __name__ == "__main__":
    import time

    start_time = time.time()
    main_meilleur_perm()
    # mainpermut()
    print("--- %s seconds ---" % (time.time() - start_time))

import math
import random
import statistics
from abc import ABC, abstractmethod
from copy import deepcopy

###############################################################
#                           Gene                              #
###############################################################


class Gene(ABC):
    def __init__(self):
        self.bit = None

    @abstractmethod
    def mutate(self):
        pass

    @abstractmethod
    def __str__(self):
        pass

    @abstractmethod
    def __repr__(self):
        pass


###############################################################
#                       Individual                            #
###############################################################


class Individual(ABC):
    def __init__(self, parameters, empty=False):
        self.sequence = []
        self.parameters = parameters

    def __getitem__(self, key):
        return self.sequence[key]

    def __setitem__(self, key, value):
        self.sequence[key] = value
        return value

    @abstractmethod
    def fitness(self):
        pass

    @abstractmethod
    def crossover(self, other):
        """
        crossover used if you use the crossover 'individual
        :param other: the second parent for the crossover
        :type other: Individual
        :return: the
        """

    @abstractmethod
    def mutate(self):
        """
        mutation called if you use the mutation 'individual'
        if you need to mutate on the individual instead of the gene
        :return: None
        """

    @abstractmethod
    def __eq__(self, other):
        pass

    def __str__(self):
        return repr(self)

    @abstractmethod
    def __repr__(self):
        pass

    @abstractmethod
    def __hash__(self):
        pass


class IndividualPermutation(Individual, ABC):
    @abstractmethod
    def basic_order(self):
        pass


###############################################################
#                       Population                            #
###############################################################


class Population:
    def __init__(self, parameters, description=False):
        self.individuals = []
        self.parameters = parameters
        self.stats = {
            "max_fitness": [],
            "min_fitness": [],
            "mean_fitness": [],
            "fitness_diversity": [],
            "total_fitness": [],
            "diversity": [],
            "max_age": [],
            "mean_age": [],
            "parameters": parameters,
            "utility": [],
        }
        self.selected = []
        self.crossed = []
        self.mutated = []
        self.nb_turns = 0

        if "function_each_turn" in self.parameters:
            self.each_turn_fct = self.parameters["function_each_turn"]
        else:
            self.each_turn_fct = None

        if "function_end" in self.parameters:
            self.end_fct = self.parameters["function_end"]
        else:
            self.end_fct = None

        if "termination_condition" in self.parameters:
            self.final_condition = self.parameters["termination_condition"]
        else:
            self.final_condition = lambda pop: not (
                pop.nb_turns == pop.parameters["nb turn max"]
            )

        self.individual_class = parameters["individual"]

        individuals = []
        for _ in range(self.parameters["population size"]):
            individuals.append(self.individual_class(parameters))

        fits = [i.fitness() for i in individuals]
        self.individuals = [
            (i, f, 0) for i, f in sorted(zip(individuals, fits), key=lambda z: z[1])
        ]
        if self.parameters["proportion selection"] > 1:
            self.nb_select = self.parameters["proportion selection"]
        else:
            self.nb_select = int(
                len(self.individuals) * self.parameters["proportion selection"]
            )
        if description:
            description = (
                f"Population parameters :"
                f"\n\tIndividuals : {self.individual_class}"
                f'\n\tSize of an individual : {self.parameters["chromosome size"]}'
                f'\n\tSize of the population : {self.parameters["population size"]}'
                f'\n\tnb turn max : {self.parameters["nb turn max"]}'
                f"\n\tNumber of individuals selected each turns : {self.nb_select}"
                f'\n\tSelection : {self.parameters["selection"]}'
                f'\n\tCrossover : {self.parameters["crossover"]} ('
                f'{self.parameters["proportion crossover"] * 100}%)'
                f'\n\tMutation : {self.parameters["mutation"]} ('
                f'{self.parameters["proportion mutation"] * 100}%)'
                f'\n\tInsertion : {self.parameters["insertion"]}'
            )
            print(description)
        self.method_switch = dict()
        self.method_switch["selection"] = {
            "select_random": self.select_random,
            "select_best": self.select_best,
            "select_tournament": self.select_tournament,
            "select_wheel": self.select_wheel,
        }
        self.method_switch["crossover"] = {
            "individual": self.crossover_individual,
            "order 1": self.crossover_order_1,
            "pmx": self.crossover_pmx,
            "mono-point": self.crossover_monopoint,
            "multipoint": self.crossover_multipoint,
            "uniforme": self.crossover_uniforme,
        }
        self.method_switch["mutation"] = {
            "individual": self.mutation_individu,
            "insert": self.mutation_insert,
            "swap": self.mutation_swap,
            "inversion": self.mutation_inversion,
            "scramble": self.mutation_scramble,
            "1-flip": self.mutation_1fip,
            "3-flip": self.mutation_3fip,
            "5-flip": self.mutation_5fip,
            "bit-flip": self.mutation_bitfip,
        }

    def sort_individuals_fitness(self):
        self.individuals.sort(key=lambda i: i[1], reverse=True)

    def sort_individuals_age(self):
        self.individuals.sort(key=lambda i: i[2], reverse=False)

    def population_get_older(self):
        self.nb_turns += 1
        self.individuals = [(i, f, (a + 1)) for i, f, a in self.individuals]

    def __repr__(self):
        s = f"Population size : {len(self.individuals)}"
        for i in self.individuals:
            s += "\n" + str(i)
        return s

    def start(self):
        self.statistic()
        while self.final_condition(self):
            if self.each_turn_fct:
                self.each_turn_fct(self)
            self.population_get_older()
            self.sort_individuals_fitness()
            self.turn()
            self.statistic()
        if self.end_fct:
            self.end_fct(self)

    def turn(self):
        self.selection()
        self.crossover()
        self.mutation()
        self.insertion()

    ###############################################################
    #                       adaptative                            #
    ###############################################################

    def adaptative(self, method):
        switch = {
            "fixed roulette wheel": self.adaptative_fixe,
            "adaptive roulette wheel": self.adaptive_roulette_wheel,
            "adaptive pursuit": self.adaptative_poursuit,
            "UCB": self.adaptative_ucb,
        }
        switch[self.parameters[method][1]](method)

    def adaptative_fixe(self, method):
        proba, methode = list(map(list, zip(*self.parameters[method][2])))
        selected_method = random.choices(range(len(methode)), proba, k=1)[0]
        self.method_switch[method][self.parameters[method][2][selected_method][1]]()

    def adaptive_roulette_wheel(self, method):
        pmin = self.parameters[method][3]
        switch = {
            "selection": 0,
            "crossover": 1,
            "mutation": 2,
        }
        rank = switch[method]
        utility = list(map(list, zip(*self.stats["utility"][:-1])))
        utility = utility[-20:]
        n = len(self.parameters[method][2])
        if len(utility) >= rank:
            sum_uk = sum([u for u, _ in utility[rank]])
            for i in range(len(self.parameters[method][2])):
                m = self.parameters[method][2][i][1]
                u_n = sum([u for u, me in utility[rank] if m == me])
                self.parameters[method][2][i][0] = pmin + (1 - n * pmin) * (
                    u_n / sum_uk if sum_uk != 0 else 1
                )
        self.adaptative_fixe(method)

    def adaptative_poursuit(self, method):
        switch = {
            "selection": 0,
            "crossover": 1,
            "mutation": 2,
        }
        rank = switch[method]
        utility = list(map(list, zip(*self.stats["utility"][:-1])))
        if len(utility) % 10 == 0:
            for i in range(len(self.parameters[method][2])):
                self.parameters[method][2][i][0] = 1 / len(self.parameters[method][2])
        utility = utility[-20:]
        if len(utility) >= rank:
            pmin = self.parameters[method][3]
            pmax = 1 - (len(self.parameters[method][2]) - 1) * pmin
            beta = self.parameters[method][4]
            past_selected = utility[rank]
            utilities = []
            for i in range(len(self.parameters[method][2])):
                methode = self.parameters[method][2][i][1]
                utilities.append(sum([u for u, me in past_selected if methode in me]))
            maxi = max(utilities)
            indices = [i for i, v in enumerate(utilities) if v == maxi]
            best = random.choice(indices)
            for i in range(len(self.parameters[method][2])):
                tminus1 = self.parameters[method][2][i][0]
                if i == best:
                    self.parameters[method][2][i][0] = tminus1 + beta * (pmax - tminus1)
                else:
                    self.parameters[method][2][i][0] = tminus1 + beta * (pmin - tminus1)
        self.adaptative_fixe(method)

    def adaptative_ucb(self, method):
        switch = {
            "selection": 0,
            "crossover": 1,
            "mutation": 2,
        }
        rank = switch[method]
        utility = list(map(list, zip(*self.stats["utility"][:-1])))
        if len(utility) >= rank:
            past_selected = utility[rank]
            exploitations = []
            explorations = []
            for i in range(len(self.parameters[method][2])):
                methode = self.parameters[method][2][i][1]
                nb_i = [m[1] for m in past_selected].count(methode)
                exploitations.append(
                    sum([u for u, me in past_selected if methode in me])
                )
                explorations.append(
                    math.sqrt(2 * math.log(len(past_selected)) / (nb_i + 1))
                )
            for i in range(len(self.parameters[method][2])):
                self.parameters[method][2][i][0] = exploitations[i] + explorations[i]
        maxi = max(self.parameters[method][2], key=lambda e: e[0])[0]
        indices = [i for i, v in enumerate(self.parameters[method][2]) if v[0] == maxi]
        choosen = random.choice(indices)
        self.method_switch[method][self.parameters[method][2][choosen][1]]()

    ###############################################################
    #                       Selection                             #
    ###############################################################

    def selection(self):
        """
        chose the type of selection to use and select individuals
        :return:
        """
        self.selected = []
        if self.parameters["selection"][0] == "adaptative":
            self.adaptative("selection")
        else:
            switch = {
                "select_random": self.select_random,
                "select_best": self.select_best,
                "select_tournament": self.select_tournament,
                "select_wheel": self.select_wheel,
            }
            switch[self.parameters["selection"][0]]()
        self.stats["utility"][-1][0][0] = statistics.mean(
            self.stats["utility"][-1][0][0]
        )

    def select_random(self):
        selected = random.sample(self.individuals, self.nb_select)
        self.stats["utility"].append([[[f for _, f, _ in selected], "select_random"]])
        self.selected = deepcopy([i for i, _, _ in selected])

    def select_best(self):
        selected = self.individuals[0 : self.nb_select]
        self.stats["utility"].append([[[f for _, f, _ in selected], "select_best"]])
        self.selected = deepcopy([i for i, _, _ in selected])

    def select_tournament(self):
        nb_winners = 2
        nb_players = 5
        self.stats["utility"].append([[[], "select_tournament"]])
        while len(self.selected) < self.nb_select:
            selected = random.sample(self.individuals, nb_players)
            selected.sort(key=lambda i: i[1], reverse=True)
            selected = selected[0:nb_winners]
            self.stats["utility"][-1][0][0].extend([f for _, f, _ in selected])
            self.selected.extend(deepcopy([i for i, _, _ in selected]))
        self.selected = self.selected[0 : self.nb_select]

    def select_wheel(self):
        individuals, fits, _ = list(zip(*self.individuals))
        total = sum(fits)
        wheel = [1] * len(fits) if total == 0 else [f / total for f in fits]
        probabilities = [sum(wheel[: i + 1]) for i in range(len(wheel))]
        self.stats["utility"].append([[[], "select_wheel"]])
        for n in range(self.nb_select):
            r = random.random()
            for (i, individual) in enumerate(individuals):
                if r <= probabilities[i]:
                    self.stats["utility"][-1][0][0].append(individual.fitness())
                    self.selected.append(deepcopy(individual))
                    break

    ###############################################################
    #                       Crossover                             #
    ###############################################################

    def crossover(self):
        self.crossed = []
        if self.parameters["crossover"][0] == "adaptative":
            self.adaptative("crossover")
        else:
            switch = {
                "individual": self.crossover_individual,
                "mono-point": self.crossover_monopoint,
                "multipoint": self.crossover_multipoint,
                "uniforme": self.crossover_uniforme,
                "order 1": self.crossover_order_1,
                "pmx": self.crossover_pmx,
            }
            switch[self.parameters["crossover"][0]]()

    def crossover_individual(self):
        self.stats["utility"][-1].append([[], "individual"])
        for i in range(0, len(self.selected), 2):
            first_child, second_child = None, None
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                first_child, second_child = i1.crossover(i2)
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_monopoint(self):
        self.stats["utility"][-1].append([[], "mono-point"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                rand = random.randint(1, len(i1.sequence))
                first_child[::] = i1[0:rand] + i2[rand:]
                second_child[::] = i2[0:rand] + i1[rand:]
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_multipoint(self):
        self.stats["utility"][-1].append([[], "multipoint"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                rand1 = random.randint(1, len(i1.sequence) - 2)
                rand2 = random.randint(rand1, len(i1.sequence))
                first_child[::] = (
                    deepcopy(i1[0:rand1])
                    + deepcopy(i2[rand1:rand2])
                    + deepcopy(i1[rand2:])
                )
                second_child[::] = (
                    deepcopy(i2[0:rand1])
                    + deepcopy(i1[rand1:rand2])
                    + deepcopy(i2[rand2:])
                )
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_uniforme(self):
        self.stats["utility"][-1].append([[], "uniforme"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            rand_cross = random.random()
            if rand_cross < self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                for j in range(self.parameters["chromosome size"]):
                    first_child[j], second_child[j] = (
                        (i1[j], i2[j]) if random.random() <= 0.5 else (i2[j], i1[j])
                    )
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_order_1(self):
        """ """
        # prendre deux val random, la 2nd plus grande que la 1ere
        # copie du parent 1 dans le child 1
        self.stats["utility"][-1].append([[], "order 1"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters, empty=True)
            second_child = self.individual_class(self.parameters, empty=True)
            if random.random() <= self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]
                size = len(first_child.sequence)
                rand1 = random.randint(0, size - 2)
                rand2 = random.randint(rand1, size)
                first_child[rand1:rand2] = deepcopy(i1[rand1:rand2])
                second_child[rand1:rand2] = deepcopy(i2[rand1:rand2])
                k = rand2
                j = rand2
                i = 0
                while None in first_child.sequence:
                    i += 1
                    k %= size
                    j %= size
                    if not i2[k] in first_child.sequence:
                        first_child[j] = deepcopy(i2[k])
                        j += 1
                    k += 1
                i = 0
                k = rand2
                j = rand2
                while None in second_child.sequence:
                    i += 1
                    k %= size
                    j %= size
                    if not i1[k] in second_child.sequence:
                        second_child[j] = deepcopy(i1[k])
                        j += 1
                    k += 1
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    def crossover_pmx(self):
        self.stats["utility"][-1].append([[], "pmx"])
        for i in range(0, len(self.selected), 2):
            first_child = self.individual_class(self.parameters)
            second_child = self.individual_class(self.parameters)
            if random.random() <= self.parameters["proportion crossover"]:
                if len(self.selected) <= i + 1:
                    rand = random.choice(self.selected[0:-1])
                    i1 = self.selected[-1]
                    i2 = rand
                else:
                    i1 = self.selected[i]
                    i2 = self.selected[i + 1]

                size = len(first_child.sequence)
                rand1 = random.randint(0, size - 2)
                rand2 = random.randint(rand1 + 1, size - 1)
                indexes = i1.basic_order()
                l1 = [indexes.index(i1[j]) for j in range(size)]
                l2 = [indexes.index(i2[j]) for j in range(size)]

                p1 = [None] * size
                p2 = [None] * size
                for j in range(size):
                    p1[l1[j]] = j
                    p2[l2[j]] = j
                for j in range(rand1, rand2):
                    val1 = l1[j]
                    val2 = l2[j]
                    l1[j], l1[p1[val2]] = val2, val1
                    p1[val1], p1[val2] = p1[val2], p1[val1]
                    l2[j], l2[p2[val1]] = val1, val2
                    p2[val1], p2[val2] = p2[val2], p2[val1]

                c1 = [indexes[l1[j]] for j in range(size)]
                c2 = [indexes[l2[j]] for j in range(size)]
                first_child.sequence = deepcopy(c1)
                second_child.sequence = deepcopy(c2)
            else:
                first_child.sequence = deepcopy(self.selected[i][::])
                # the second child will be a copy of a random parents from the selected parents if the number
                # of parents is odd
                sc = (
                    random.randrange(len(self.selected[0:-1]))
                    if len(self.selected) <= i + 1
                    else i + 1
                )
                second_child.sequence = deepcopy(self.selected[sc].sequence)
            self.stats["utility"][-1][-1][0].extend(
                [first_child.fitness(), second_child.fitness()]
            )
            self.crossed.extend([first_child, second_child])
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )

    ###############################################################
    #                       Mutation                              #
    ###############################################################

    def mutation(self):
        self.mutated = []
        if self.parameters["mutation"][0] == "adaptative":
            self.adaptative("mutation")
        else:
            switch = {
                "individual": self.mutation_individu,
                "1-flip": self.mutation_1fip,
                "3-flip": self.mutation_3fip,
                "5-flip": self.mutation_5fip,
                "bit-flip": self.mutation_bitfip,
                "insert": self.mutation_insert,
                "swap": self.mutation_swap,
                "inversion": self.mutation_inversion,
                "scramble": self.mutation_scramble,
            }
            switch[self.parameters["mutation"][0]]()

    def mutation_individu(self):
        self.stats["utility"][-1].append([[], "individual"])
        for indiv in self.crossed:
            indiv.mutate()
            self.mutated.append(indiv)

    def mutation_1fip(self):
        self.stats["utility"][-1].append([[], "1-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                for i in random.sample(range(len(indiv.sequence)), 1):
                    indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_3fip(self):
        self.stats["utility"][-1].append([[], "3-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                for i in random.sample(range(len(indiv.sequence)), 3):
                    indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_5fip(self):
        self.stats["utility"][-1].append([[], "5-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                for i in random.sample(range(len(indiv.sequence)), 5):
                    indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_bitfip(self):
        self.stats["utility"][-1].append([[], "bit-flip"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                p = 1 / length
                for i in range(length):
                    if random.random() <= p:
                        indiv.sequence[i].mutate()
            self.mutated.append(indiv)

    def mutation_insert(self):
        """
        prendre deux points, le second devient le suivant du premier
        """
        self.stats["utility"][-1].append([[], "insert"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                indiv[rand1 + 1], indiv[rand2] = indiv[rand2], indiv[rand1 + 1]
            self.mutated.append(indiv)

    def mutation_swap(self):
        """
        prendre deux points et les echanger
        """
        self.stats["utility"][-1].append([[], "swap"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                indiv[rand1], indiv[rand2] = indiv[rand2], indiv[rand1]
            self.mutated.append(indiv)

    def mutation_inversion(self):
        """
        inverser toutes les valeurs entre deux points
        """
        self.stats["utility"][-1].append([[], "invertion"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                indiv[rand1:rand2] = indiv[rand1:rand2][::-1]
            self.mutated.append(indiv)

    def mutation_scramble(self):
        """
        melanger toutes les valeurs entre deux points
        """
        self.stats["utility"][-1].append([[], "scramble"])
        for indiv in self.crossed:
            if random.random() <= self.parameters["proportion mutation"]:
                length = self.parameters["chromosome size"]
                rand1 = random.randint(0, length - 3)
                rand2 = random.randint(rand1 + 1, length - 1)
                randlist = indiv[rand1:rand2]
                random.shuffle(randlist)
                indiv[rand1:rand2] = randlist
            self.mutated.append(indiv)

    ###############################################################
    #                       Insertion                             #
    ###############################################################

    def insertion(self):
        if self.parameters["insertion"] == "fitness":
            self.sort_individuals_fitness()
        elif self.parameters["insertion"] == "age":
            self.sort_individuals_age()
        if len(self.mutated):
            self.individuals = self.individuals[: -len(self.mutated)]
            for i in self.mutated:
                f = i.fitness()
                self.stats["utility"][-1][-1][0].append(f)
                self.individuals.append((i, f, 0))
        self.stats["utility"][-1][-1][0] = statistics.mean(
            self.stats["utility"][-1][-1][0]
        )
        selection = self.stats["utility"][-1][0][0]
        crossover = self.stats["utility"][-1][1][0]
        mutation = self.stats["utility"][-1][2][0]
        u_selection = (mutation - selection) / self.parameters["chromosome size"]
        u_crossover = (crossover - selection) / self.parameters["chromosome size"]
        u_mutation = (mutation - crossover) / self.parameters["chromosome size"]
        self.stats["utility"][-1][0][0] = u_selection
        self.stats["utility"][-1][1][0] = u_crossover
        self.stats["utility"][-1][2][0] = u_mutation

    ###############################################################
    #                       Statistic                             #
    ###############################################################

    def statistic(self):
        self.sort_individuals_fitness()
        i = [i for i, _, _ in self.individuals]
        fits = [f for _, f, _ in self.individuals]
        ages = [a for _, _, a in self.individuals]
        self.stats["total_fitness"].append(sum(fits))
        self.stats["max_fitness"].append(fits[0])
        self.stats["min_fitness"].append(fits[-1])
        self.stats["mean_fitness"].append(statistics.mean(fits))
        self.stats["fitness_diversity"].append(len(set(i)))
        self.stats["diversity"].append(len(set(self.individuals)))
        self.stats["max_age"].append(max(ages))
        self.stats["mean_age"].append(statistics.mean(ages))


def mainpermut():
    number_of_team = 6
    from algo_gen.individuals.STS import IndividualSTS

    def final_condition(population):
        if population.stats["max_fitness"][-1] == 0:
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    parameters = {
        "configuration name": "config1",
        "individual": IndividualSTS,
        "only weeks": False,
        "number of team": number_of_team,
        "population size": 100,
        "chromosome size": (number_of_team - 1) * (number_of_team // 2),
        "termination_condition": final_condition,
        "nb turn max": 10000,
        # 'stop after no change': 10000,
        "selection": ["select_tournament"],
        "proportion selection": 0.2,  # 0.04,  # 2 / population_size
        "crossover": ["pmx"],
        "proportion crossover": 1,
        "mutation":
        # ['inversion'],
        [
            "adaptative",
            "adaptive pursuit",
            # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB'
            [[0.25, "insert"], [0.25, "swap"], [0.25, "inversion"], [0.25, "scramble"]],
            0.1,
            0.3,
        ],
        # 'insert'
        # 'swap'
        # 'inversion'
        # 'scramble'
        "proportion mutation": 0.5,  # 0.1 0.2 0.5 0.8
        "insertion": "age",  # 'age' 'fitness'
    }
    crossover = [["order 1"], ["pmx"]]
    mutation = [["insert"], ["swap"], ["inversion"], ["scramble"]]
    for cross in crossover:
        parameters["crossover"] = cross
        for mut in mutation:
            parameters["mutation"] = mut
            population = Population(parameters)
            population.start()
            from algo_gen.tools.plot import show_stats

            show_stats(population.stats)

    utility = list(map(list, zip(*population.stats["utility"])))
    val = list(map(list, zip(*utility[2])))[1]

    count = {
        "insert": [0],
        "swap": [0],
        "inversion": [0],
        "scramble": [0],
    }
    for v in val:
        for m in ["insert", "swap", "inversion", "scramble"]:
            count[m].append(count[m][-1] + (1 if v == m else 0))

    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(figsize=(10, 10))

    for m in count.keys():
        ax.plot(list(range(len(count[m]))), count[m], label=m)
    plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)
    import textwrap

    plt.title(
        "\n".join(
            textwrap.wrap(str(population.stats["parameters"]["mutation"][1]), 120)
        )
    )
    plt.show()


def main():
    from algo_gen.individuals.onemax import IndividualOneMax

    def final_condition(population):
        if (
            population.stats["max_fitness"][-1]
            == population.parameters["chromosome size"]
        ):
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    # def final_condition(population):
    #     if population.stats['max_fitness'][-1] == population.parameters['chromosome size']:
    #         return False
    #     if population.nb_turns >= population.parameters['stop after no change']:
    #         last_max = population.stats['max_fitness'][-population.parameters['stop after no change']:]
    #         # last_min = self.stats['min_fitness'][-self.parameters['stop after no change']:]
    #         max_change = not all(x >= y for x, y in zip(last_max, last_max[1:]))
    #         # min_change = not all(x >= y for x, y in zip(last_min, last_min[1:]))
    #         return (not (population.nb_turns == population.parameters['nb turn max'])) and max_change
    #         # and not (not max_change and not min_change)
    #     else:
    #         return not (population.nb_turns == population.parameters['nb turn max'])

    # def function_each_turn(population):
    #     pass
    #
    # def function_end(population):
    #     print(f'fitness max : {population.stats["max_fitness"][-1]}')

    param = {
        "configuration name": "config1",
        "individual": IndividualOneMax,
        "population size": 100,  # 100 200 500
        "chromosome size": 100,  # 5 10 50 100
        "termination_condition": final_condition,
        # 'function_each_turn': function_each_turn,
        # 'function_end': function_end,
        "nb turn max": 5000,
        # 'stop after no change': 5000000,  # int(config['nb turn max']*0.10),
        "selection": ["select_tournament"],
        #     ['adaptative',
        #      'UCB',
        #      [
        #          [0.25, 'select_random'],
        #          [0.25, 'select_best'],
        #          [0.25, 'select_tournament'],
        #          [0.25, 'select_wheel']
        #      ]],
        "proportion selection": 0.02,  # 0.04,  # 2 / population_size
        "crossover": ["uniforme"],
        #     ['adaptative',
        #      'UCB',
        #      [
        #          [0.25, 'mono-point'],
        #          [0.25, 'uniforme'],
        #      ],
        #      0.5],
        "proportion crossover": 1,
        "mutation": ["individual"],
        #     ['adaptative',
        #      'UCB',
        #      # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB'
        #      [
        #          [0.25, '1-flip'],
        #          [0.25, '3-flip'],
        #          [0.25, '5-flip'],
        #          [0.25, 'bit-flip']
        #      ],
        #      0.05,  # pmin for adaptive roulette wheel and adaptive poursuite
        #      0.5,  # beta for adaptive poursuit
        #      ],
        "proportion mutation": 1,  # 0.1 0.2 0.5 0.8
        "insertion": "fitness",  # 'age' 'fitness'
    }
    # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB' 'DMAB'

    # pmin = 0.15
    # beta = 1.5
    diff_mutation = [
        # ['adaptative', 'fixed roulette wheel',
        #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        [
            "adaptative",
            "adaptive roulette wheel",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
            0.10,
        ],
        # ['adaptative', 'adaptive pursuit',
        #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], pmin, beta, ],
        # ['adaptative', 'UCB',
        #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], ],
    ]

    for mu in diff_mutation:
        param["mutation"] = mu
        population = Population(param)
        population.start()
        print(population.parameters["mutation"])
        utility = list(map(list, zip(*population.stats["utility"])))
        val = list(map(list, zip(*utility[2])))[1]

        count = {
            "1-flip": [0],
            "3-flip": [0],
            "5-flip": [0],
            "bit-flip": [0],
        }
        for v in val:
            for m in ["1-flip", "3-flip", "5-flip", "bit-flip"]:
                count[m].append(count[m][-1] + (1 if v == m else 0))

        import matplotlib.pyplot as plt

        fig, ax = plt.subplots(figsize=(10, 10))

        for m in count.keys():
            ax.plot(list(range(len(count[m]))), count[m], label=m)
        plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left", borderaxespad=0.0)

        plt.title(population.stats["parameters"]["mutation"][1])
        plt.show()

        from algo_gen.tools.plot import show_stats

        show_stats(population.stats)


def main_meilleur():
    from algo_gen.individuals.onemax import IndividualOneMax

    def final_condition(population):
        if (
            population.stats["max_fitness"][-1]
            == population.parameters["chromosome size"]
        ):
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    parametres = {
        "individual": IndividualOneMax,
        "population size": 100,
        "chromosome size": 100,
        "termination_condition": final_condition,
        "nb turn max": 10000,
        "selection": ["select_best"],
        "proportion selection": 0.60,
        "crossover": ["uniforme"],
        "proportion crossover": 1,
        "mutation": [
            "adaptative",
            "UCB",
            [[0.25, "1-flip"], [0.25, "3-flip"], [0.25, "5-flip"], [0.25, "bit-flip"]],
        ],
        "proportion mutation": 1,
        "insertion": "fitness",
    }
    population = Population(parametres)
    population.start()
    # utility = list(map(list, zip(*population.stats['utility'])))
    # val = list(map(list, zip(*utility[2])))[1]
    #
    # count = {'1-flip': [0],
    #          '3-flip': [0],
    #          '5-flip': [0],
    #          'bit-flip': [0],
    #          }
    # for v in val:
    #     for m in ['1-flip', '3-flip', '5-flip', 'bit-flip']:
    #         count[m].append(count[m][-1] + (1 if v == m else 0))
    #
    # import matplotlib.pyplot as plt
    #
    # fig, ax = plt.subplots(figsize=(10, 10))
    #
    # for m in count.keys():
    #     ax.plot(list(range(len(count[m]))), count[m], label=m)
    # plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.)
    # import textwrap
    #
    # plt.title("\n".join(textwrap.wrap(str(population.stats['parameters']['mutation'][1]), 120)))
    # plt.show()
    from algo_gen.tools.plot import show_stats

    show_stats(population.stats)


def main_meilleur_perm():
    from algo_gen.individuals.STS import IndividualSTS

    def final_condition(population):
        if population.stats["max_fitness"][-1] == 0:
            return False
        else:
            return not (population.nb_turns == population.parameters["nb turn max"])

    number_of_team = 14

    parametres = {
        "individual": IndividualSTS,
        "only weeks": True,
        "number of team": number_of_team,
        "population size": 100,
        "chromosome size": (number_of_team - 1) * (number_of_team // 2),
        "termination_condition": final_condition,
        "nb turn max": 5000,
        "selection": ["select_tournament"],
        "proportion selection": 0.2,
        "crossover": ["pmx"],
        "proportion crossover": 1,
        "mutation": ["swap"],
        "proportion mutation": 0.2,
        "insertion": "age",
    }
    population = Population(parametres)
    population.start()

    from algo_gen.tools.plot import show_stats

    show_stats(population.stats)


if __name__ == "__main__":
    import time

    start_time = time.time()
    main_meilleur_perm()
    # mainpermut()
    print("--- %s seconds ---" % (time.time() - start_time))

############################################################### #                           Gene                              # ############################################################### ############################################################### #                       Individual                            # ############################################################### crossover used if you use the crossover 'individual         :param other: the second parent for the crossover         :type other: Individual         :return: the mutation called if you use the mutation 'individual'         if you need to mutate on the individual instead of the gene         :return: None ############################################################### #                       Population                            # ############################################################### ############################################################### #                       adaptative                            # ############################################################### ############################################################### #                       Selection                             # ############################################################### chose the type of selection to use and select individuals         :return: ############################################################### #                       Crossover                             # ############################################################### # the second child will be a copy of a random parents from the selected parents if the number # of parents is odd # the second child will be a copy of a random parents from the selected parents if the number # of parents is odd # the second child will be a copy of a random parents from the selected parents if the number # of parents is odd # the second child will be a copy of a random parents from the selected parents if the number # of parents is odd # prendre deux val random, la 2nd plus grande que la 1ere # copie du parent 1 dans le child 1 # the second child will be a copy of a random parents from the selected parents if the number # of parents is odd # the second child will be a copy of a random parents from the selected parents if the number # of parents is odd ############################################################### #                       Mutation                              # ############################################################### prendre deux points, le second devient le suivant du premier prendre deux points et les echanger inverser toutes les valeurs entre deux points melanger toutes les valeurs entre deux points ############################################################### #                       Insertion                             # ############################################################### ############################################################### #                       Statistic                             # ############################################################### # 'stop after no change': 10000, # 0.04,  # 2 / population_size # ['inversion'], # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB' # 'insert' # 'swap' # 'inversion' # 'scramble' # 0.1 0.2 0.5 0.8 # 'age' 'fitness' # def final_condition(population): #     if population.stats['max_fitness'][-1] == population.parameters['chromosome size']: #         return False #     if population.nb_turns >= population.parameters['stop after no change']: #         last_max = population.stats['max_fitness'][-population.parameters['stop after no change']:] #         # last_min = self.stats['min_fitness'][-self.parameters['stop after no change']:] #         max_change = not all(x >= y for x, y in zip(last_max, last_max[1:])) #         # min_change = not all(x >= y for x, y in zip(last_min, last_min[1:])) #         return (not (population.nb_turns == population.parameters['nb turn max'])) and max_change #         # and not (not max_change and not min_change) #     else: #         return not (population.nb_turns == population.parameters['nb turn max']) # def function_each_turn(population): #     pass # # def function_end(population): #     print(f'fitness max : {population.stats["max_fitness"][-1]}') # 100 200 500 # 5 10 50 100 # 'function_each_turn': function_each_turn, # 'function_end': function_end, # 'stop after no change': 5000000,  # int(config['nb turn max']*0.10), #     ['adaptative', #      'UCB', #      [ #          [0.25, 'select_random'], #          [0.25, 'select_best'], #          [0.25, 'select_tournament'], #          [0.25, 'select_wheel'] #      ]], # 0.04,  # 2 / population_size #     ['adaptative', #      'UCB', #      [ #          [0.25, 'mono-point'], #          [0.25, 'uniforme'], #      ], #      0.5], #     ['adaptative', #      'UCB', #      # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB' #      [ #          [0.25, '1-flip'], #          [0.25, '3-flip'], #          [0.25, '5-flip'], #          [0.25, 'bit-flip'] #      ], #      0.05,  # pmin for adaptive roulette wheel and adaptive poursuite #      0.5,  # beta for adaptive poursuit #      ], # 0.1 0.2 0.5 0.8 # 'age' 'fitness' # 'fixed roulette wheel' 'adaptive roulette wheel' 'adaptive pursuit' 'UCB' 'DMAB' # pmin = 0.15 # beta = 1.5 # ['adaptative', 'fixed roulette wheel', #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], ], # ['adaptative', 'adaptive pursuit', #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], pmin, beta, ], # ['adaptative', 'UCB', #  [[0.25, '1-flip'], [0.25, '3-flip'], [0.25, '5-flip'], [0.25, 'bit-flip']], ], # utility = list(map(list, zip(*population.stats['utility']))) # val = list(map(list, zip(*utility[2])))[1] # # count = {'1-flip': [0], #          '3-flip': [0], #          '5-flip': [0], #          'bit-flip': [0], #          } # for v in val: #     for m in ['1-flip', '3-flip', '5-flip', 'bit-flip']: #         count[m].append(count[m][-1] + (1 if v == m else 0)) # # import matplotlib.pyplot as plt # # fig, ax = plt.subplots(figsize=(10, 10)) # # for m in count.keys(): #     ax.plot(list(range(len(count[m]))), count[m], label=m) # plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.) # import textwrap # # plt.title("\n".join(textwrap.wrap(str(population.stats['parameters']['mutation'][1]), 120))) # plt.show() # mainpermut()