def __init__(self, problem_instance, params=default_params, run=0, log_name="temp", log_dir="./log/"):
        self._text              = ""
        self._generation        = 0
        self._run               = run
        self._fittest           = None
        self._problem_instance  = problem_instance
        self._population        = None
        self._observers         = []
        self._best_solution     = None

        self._parse_params(params)

        self._logger = GeneticAlgorithmLogger(log_dir, log_name, run)

        self._observers = []
class GeneticAlgorithm:
    """
    A genetic algorithm is a search heuristic that is inspired by Charles Darwin’s theory of 
    natural evolution. This algorithm reflects the process of natural selection where the fittest 
    individuals are selected for reproduction in order to produce offspring of the next generation.

    The process of natural selection starts with the selection of fittest individuals from a population. 
    They produce offspring which inherit the characteristics of the parents and will be added to the 
    next generation. If parents have better fitness, their offspring will be better than parents and 
    have a better chance at surviving. This process keeps on iterating and at the end, a generation with 
    the fittest individuals will be found.
    
    This notion can be applied for a search problem. We consider a set of solutions for a problem and 
    select the set of best ones out of them.
    
    Six phases are considered in a genetic algorithm.

    1. Initial population
    2. Fitness function
    3. Selection
    4. Crossover
    5. Mutation
    6. Replacement
    """
    # Constructor
    # ---------------------------------------------------------------------------------------------
    def __init__(self, problem_instance, params=default_params, run=0, log_name="temp", log_dir="./log/"):
        self._text              = ""
        self._generation        = 0
        self._run               = run
        self._fittest           = None
        self._problem_instance  = problem_instance
        self._population        = None
        self._observers         = []
        self._best_solution     = None

        self._parse_params(params)

        self._logger = GeneticAlgorithmLogger(log_dir, log_name, run)

        self._observers = []


    
    # search
    # ---------------------------------------------------------------------------------------------
    def search(self):
        """
            Genetic Algorithm - Search Algorithm
            1. Initial population

            2. Repeat n generations )(#1 loop )
                
                2.1. Repeat until generate the next generation (#2 loop )
                    1. Selection
                    2. Try Apply Crossover (depends on the crossover probability)
                    3. Try Apply Mutation (depends on the mutation probability)
                
                2.2. Replacement

            3. Return the best solution    
        """
        problem         = self._problem_instance
        select          = self._selection_approach
        cross           = self._crossover_approach
        mutate          = self._mutation_approach
        replace         = self._replacement_approach
        is_admissible   = self._problem_instance.is_admissible

        self._generation = 0
        self._notify(message="Genetic Algorithm")

        # 1. Initial population
        self._population = self._initialize(problem, self._population_size)
        self._fittest = self._population.fittest
        self._best_solution = self._fittest


        self._notify()

        #2. Repeat n generations )(#1 loop )
        for self._generation in range(1, self._number_of_generations + 1):
            new_population = Population(problem=problem, maximum_size=self._population_size, solution_list=[])
            i = 0

            # 2.1. Repeat until generate the next generation (#2 loop )
            while new_population.has_space:
                # 2.1.1. Selection
                parent1, parent2 = select(self._population, problem.objective, self._params)
                offspring1 = deepcopy(parent1)  # parent1.clone()
                offspring2 = deepcopy(parent2)  # parent2.clone()
                # 2.1.2. Try Apply Crossover (depends on the crossover probability)
                if self.apply_crossover: 
                    offspring1, offspring2 = cross(problem, parent1, parent2)
                    offspring1.id = [self._generation, i]
                    i += 1
                    offspring2.id = [self._generation, i]
                    #i += 2
                    i += 1

                # 2.1.3. Try Apply Mutation (depends on the mutation probability)
                if self.apply_mutation: 
                    offspring1 = mutate(problem, offspring1)
                    offspring1.id = [self._generation, i]
                    i += 1
                if self.apply_mutation: 
                    offspring2 = mutate(problem, offspring2)
                    offspring2.id = [self._generation, i]
                    i += 1

                # in our opinion the id's should be added after applying crossover and/or mutation, but we will not
                # change because it is not relevant

                # add the offsprings in the new population (New Generation)
                if new_population.has_space and is_admissible(offspring1):
                    problem.evaluate_solution(offspring1)
                    new_population.solutions.append(offspring1)
                
                if new_population.has_space and is_admissible(offspring2):
                    problem.evaluate_solution(offspring2)
                    new_population.solutions.append(offspring2)
                    #print(f'Added O2 - {offspring2.id}-{offspring2.representation}')

            self._population = replace(problem, self._population, new_population)

            self._fittest = self._population.fittest

            self.find_best_solution()

            self._notify()

        self._notify(message="Fittest Solution")
        return self._fittest    

    def __str__(self):
        self._text = ""


    @property
    def apply_crossover(self):
        chance = random()
        return chance < self._crossover_probability

    @property
    def apply_mutation(self):
        chance = random()
        return chance < self._mutation_probability

    @property
    def best_solution(self):
        return self._best_solution

    # initialize
    # ---------------------------------------------------------------------------------------------
    def _parse_params(self, params):
        self._params = params
        self._text = ""

        self._population_size = 10
        if "Population-Size" in params:
            self._population_size = params["Population-Size"]
        self._text += "\nPopulation-Size " + str(self._population_size)

        self._crossover_probability = 0.8
        if "Crossover-Probability" in params:
            self._crossover_probability = params["Crossover-Probability"]
        self._text += "\nCrossover-Probability " + str(self._crossover_probability)

        self._mutation_probability = 0.5
        if "Mutation-Probability" in params:
            self._mutation_probability = params["Mutation-Probability"]

        self._number_of_generations = 5
        if "Number-of-Generations" in params:
            self._number_of_generations = params["Number-of-Generations"]
        
        # Initialization signature: <method_name>( problem, population_size ):
        self._initialize = None
        if "Initialization-Approach" in params:
            self._initialize = params["Initialization-Approach"]
        else:
            print("Undefined Initialization approach. The default will be used.")
            self._initialize = initialize_using_random
        
        # Selection
        self._selection_approach = None
        if "Selection-Approach" in params:
            self._selection_approach = params["Selection-Approach"]
        else:
            print("Undefined Selection approach. The default will be used.")
            parent_selection = tournament_selection()
            self._selection_approach = parent_selection.select
        
        # tournament size
        self._tournament_size = 5
        if "Tournament-Size" in params:
            self._tournament_size = params["Tournament-Size"]

        # Crossover
        self._crossover_approach = None
        if "Crossover-Approach" in params:
            self._crossover_approach = params["Crossover-Approach"]
        else:
            print("Undefined Crossover-Approach. The default will be used.")
            self._crossover_approach = singlepoint_crossover

        # Mutation
        self._mutation_approach = None
        if "Mutation-Approach" in params:
            self._mutation_approach = params["Mutation-Approach"]
        else:
            print("Undefined Mutation-Approach. The default will be used.")
            self._mutation_approach = single_point_mutation

        # Replacement
        self._replacement_approach = None
        if "Replacement-Approach" in params:
            self._replacement_approach = params["Replacement-Approach"]
        else:
            print("Undefined Replacement-Approach. The default will be used.")
            self._replacement_approach = elitism_replacement

        self._notify(message="Configuration", content=self._text)


    # register observer
    #----------------------------------------------------------------------------------------------
    def register_observer(self, observer):
        self._observers.append(observer)

    # unregister observer
    #----------------------------------------------------------------------------------------------
    def unregister_observer(self, observer):
        self._observers.remove(observer)

    # notify
    #----------------------------------------------------------------------------------------------
    def _notify(self,  message="", content=""):

        self._state = {
            "iteration" : self._generation,
            "message"   : message,
            "content"   : content,
            "fittest"   : self._fittest
        }

        if message == "":
            self._logger.add( 
                generation = self._generation, 
                solution = self._fittest 
            )
        
        #if message == "Fittest Solution":
        #    self._logger.save()

        for observer in self._observers:
            observer.update()

    # get state
    #----------------------------------------------------------------------------------------------
    def get_state(self):
        return self._state

    
    def save_log(self):
        self._logger.save()
    
    
    #def calc_weights(self, solution):
    #    weights = self._problem_instance.decision_variables["Weights"] 
    #
    #    weight = 0
    #    for  i in range(0, len( weights )):
    #        if solution.representation[ i ] == 1:
    #            weight += weights[ i ]
    #
    #    return weight

    # find the best solution for each generation in each run
    def find_best_solution(self):
        if self._problem_instance.objective == ProblemObjective.Minimization:
            if self._fittest.fitness < self._best_solution.fitness:
                self._best_solution = deepcopy(self._fittest)
        elif self._problem_instance.objective == ProblemObjective.Maximization:
            if self._fittest.fitness > self._best_solution.fitness:
                self._best_solution = deepcopy(self._fittest)
        else:
            print('The code does not handle multiobjective problems yet.')
            exit(code=1)
Example #3
0
class GeneticAlgorithm:
    """
    A genetic algorithm is a search heuristic that is inspired by Charles Darwin’s theory of 
    natural evolution. This algorithm reflects the process of natural selection where the fittest 
    individuals are selected for reproduction in order to produce offspring of the next generation.

    The process of natural selection starts with the selection of fittest individuals from a population. 
    They produce offspring which inherit the characteristics of the parents and will be added to the 
    next generation. If parents have better fitness, their offspring will be better than parents and 
    have a better chance at surviving. This process keeps on iterating and at the end, a generation with 
    the fittest individuals will be found.
    
    This notion can be applied for a search problem. We consider a set of solutions for a problem and 
    select the set of best ones out of them.
    
    Six phases are considered in a genetic algorithm.

    1. Initial population
    2. Fitness function
    3. Selection
    4. Crossover
    5. Mutation
    6. Replacement
    """

    # Constructor
    # ---------------------------------------------------------------------------------------------
    def __init__(self,
                 problem_instance,
                 params=default_params,
                 run=0,
                 log_name="temp"):
        self._text = ""
        self._generation = 0
        self._run = run
        self._fittest = None
        self._problem_instance = problem_instance
        self._population = None
        self._observers = []

        self._parse_params(params)

        self._logger = GeneticAlgorithmLogger(log_name, run)

        self._observers = []

    # search
    # ---------------------------------------------------------------------------------------------
    def search(self):
        """
            Genetic Algorithm - Search Algorithm
            1. Initial population

            2. Repeat n generations )(#1 loop )
                
                2.1. Repeat until generate the next generation (#2 loop )
                    1. Selection
                    2. Try Apply Crossover (depends on the crossover probability)
                    3. Try Apply Mutation (depends on the mutation probability)
                
                2.2. Replacement

            3. Return the best solution    
        """
        problem = self._problem_instance
        select = self._selection_approach
        cross = self._crossover_approach
        mutate = self._mutation_approach
        replace = self._replacement_approach
        is_admissible = self._problem_instance.is_admissible

        self._generation = 0
        self._notify(message="Genetic Algorithm")

        # 1. Initial population
        self._population = self._initialize(problem, self._population_size)
        self._fittest = self._population.fittest

        self._notify()
        self._store_population()

        # 2. Repeat n generations )(#1 loop )
        for self._generation in range(1, self._number_of_generations + 1):

            new_population = Population(problem=problem,
                                        maximum_size=self._population_size,
                                        solution_list=[])
            i = 0

            # 2.1. Repeat until generate the next generation (#2 loop )
            while new_population.has_space:
                # 2.1.1. Selection
                parent1, parent2 = select(self._population, problem.objective,
                                          self._params)
                offspring1 = problem.fast_solution_copy(
                    parent1.representation)  # parent1.clone()
                offspring2 = problem.fast_solution_copy(
                    parent2.representation)  # parent1.clone()
                # offspring1 = problem.build_solution(parent1.representation[:]) # parent2.clone()
                # offspring2 = problem.build_solution(parent2.representation[:]) # parent2.clone()
                # 2.1.2. Try Apply Crossover (depends on the crossover probability)
                if self.apply_crossover:
                    offspring1, offspring2 = cross(
                        problem, offspring1, offspring2
                    )  # I guess this was wrong here, I've replaced 'solution' by 'offspring'
                    offspring1.id = [self._generation, i]
                    i += 1
                    offspring2.id = [self._generation, i]
                    i += 1

                # 2.1.3. Try Apply Mutation (depends on the mutation probability)
                if self.apply_mutation:
                    offspring1 = mutate(problem, offspring1)
                    offspring1.id = [self._generation, i]
                    i += 1
                if self.apply_mutation:
                    offspring2 = mutate(problem, offspring2)
                    offspring2.id = [self._generation, i]
                    i += 1

                # # Don't allow Twins
                # solReps = [x.representation for x in new_population.solutions]
                # if offspring1.representation in solReps:
                #     offspring1 = problem.get_single_neighbor(offspring1, problem)

                # if offspring2.representation in solReps:
                #     offspring2 = problem.get_single_neighbor(offspring2, problem)

                # add the offsprings in the new population (New Generation)
                if new_population.has_space and is_admissible(offspring1):
                    problem.evaluate_solution(offspring1)
                    new_population.solutions.append(offspring1)

                if new_population.has_space and is_admissible(offspring2):
                    problem.evaluate_solution(offspring2)
                    new_population.solutions.append(offspring2)
                    # print(f'Added O2 - {offspring2.id}-{offspring2.representation}')

            self._population = replace(problem, self._population,
                                       new_population)

            self._fittest = self._population.fittest
            self._notify()
            self._store_population()

        self._notify(message="Fittest Solution")
        return self._fittest

    def __str__(self):
        self._text = ""

    @property
    def apply_crossover(self):
        chance = random()
        return chance < self._crossover_probability

    @property
    def apply_mutation(self):
        chance = random()
        return chance < self._mutation_probability

    # initialize
    # ---------------------------------------------------------------------------------------------
    def _parse_params(self, params):
        self._params = params
        self._text = ""

        self._population_size = 10
        if "Population-Size" in params:
            self._population_size = params["Population-Size"]
        self._text += "\nPopulation-Size " + str(self._population_size)

        self._crossover_probability = 0.8
        if "Crossover-Probability" in params:
            self._crossover_probability = params["Crossover-Probability"]
        self._text += "\nCrossover-Probability " + str(
            self._crossover_probability)

        self._mutation_probability = 0.5
        if "Mutation-Probability" in params:
            self._mutation_probability = params["Mutation-Probability"]

        self._number_of_generations = 5
        if "Number-of-Generations" in params:
            self._number_of_generations = params["Number-of-Generations"]

        # Initialization signature: <method_name>( problem, population_size ):
        self._initialize = None
        if "Initialization-Approach" in params:
            self._initialize = params["Initialization-Approach"]
        else:
            print(
                "Undefined Initialization approach. The default will be used.")
            self._initialize = initialize_randomly

        # Selection
        self._selection_approach = None
        if "Selection-Approach" in params:
            self._selection_approach = params["Selection-Approach"]
        else:
            print("Undefined Selection approach. The default will be used.")
            parent_selection = TournamentSelection()
            self._selection_approach = parent_selection.select

        # tournament size
        self._tournament_size = 5
        if "Tournament-Size" in params:
            self._tournament_size = params["Tournament-Size"]

        # Crossover
        self._crossover_approach = None
        if "Crossover-Approach" in params:
            self._crossover_approach = params["Crossover-Approach"]
        else:
            print("Undefined Crossover-Approach. The default will be used.")
            self._crossover_approach = singlepoint_crossover

        # Mutation
        self._mutation_approach = None
        if "Mutation-Aproach" in params:
            self._mutation_approach = params["Mutation-Aproach"]
        else:
            print("Undefined Mutation-Aproach. The default will be used.")
            self._mutation_approach = single_point_mutation

        # Replacement
        self._replacement_approach = None
        if "Replacement-Approach" in params:
            self._replacement_approach = params["Replacement-Approach"]
        else:
            print("Undefined Replacement-Approach. The default will be used.")
            self._replacement_approach = elitism_replacement

        self._notify(message="Configuration", content=self._text)

    # register observer
    # ----------------------------------------------------------------------------------------------
    def register_observer(self, observer):
        self._observers.append(observer)

    # unregister observer
    # ----------------------------------------------------------------------------------------------
    def unregister_observer(self, observer):
        self._observers.remove(observer)

    # notify
    # ----------------------------------------------------------------------------------------------
    def _notify(self, message="", content=""):

        self._state = {
            "iteration": self._generation,
            "message": message,
            "content": content,
            "fittest": self._fittest,
        }

        if message == "":
            self._logger.add(generation=self._generation,
                             solution=self._fittest)

        # if message == "Fittest Solution" :
        #    self._logger.save()

        for observer in self._observers:
            observer.update()

    # get state
    # ----------------------------------------------------------------------------------------------
    def get_state(self):
        return self._state

    def save_log(self):
        self._logger.save()

    def get_best_fits(self):
        return self._logger.get_fitnesses()

    def _store_population(self):
        self._logger.add_population(self._population)

    def get_populations(self):
        return self._logger.get_populations()