diff --git a/pygad/helper/__init__.py b/pygad/helper/__init__.py index 3eebdb79..e781d27f 100644 --- a/pygad/helper/__init__.py +++ b/pygad/helper/__init__.py @@ -1,3 +1,3 @@ -from pygad.helper import unique, nsga2 +from pygad.helper import unique -__version__ = "1.2.0" \ No newline at end of file +__version__ = "1.1.0" \ No newline at end of file diff --git a/pygad/helper/nsga2.py b/pygad/helper/nsga2.py deleted file mode 100644 index 8f7401e4..00000000 --- a/pygad/helper/nsga2.py +++ /dev/null @@ -1,252 +0,0 @@ -import numpy -import pygad - -def get_non_dominated_set(curr_solutions): - """ - Get the set of non-dominated solutions from the current set of solutions. - - Parameters - ---------- - curr_solutions : TYPE - The set of solutions to find its non-dominated set. - - Returns - ------- - dominated_set : TYPE - A set of the dominated solutions. - non_dominated_set : TYPE - A set of the non-dominated set. - - """ - # List of the members of the current dominated pareto front/set. - dominated_set = [] - # List of the non-members of the current dominated pareto front/set. - non_dominated_set = [] - for idx1, sol1 in enumerate(curr_solutions): - # Flag indicates whether the solution is a member of the current dominated set. - is_dominated = True - for idx2, sol2 in enumerate(curr_solutions): - if idx1 == idx2: - continue - # Zipping the 2 solutions so the corresponding genes are in the same list. - # The returned array is of size (N, 2) where N is the number of genes. - two_solutions = numpy.array(list(zip(sol1[1], sol2[1]))) - - #TODO Consider repacing < by > for maximization problems. - # Checking for if any solution dominates the current solution by applying the 2 conditions. - # le_eq (less than or equal): All elements must be True. - # le (less than): Only 1 element must be True. - le_eq = two_solutions[:, 1] >= two_solutions[:, 0] - le = two_solutions[:, 1] > two_solutions[:, 0] - - # If the 2 conditions hold, then a solution dominates the current solution. - # The current solution is not considered a member of the dominated set. - if le_eq.all() and le.any(): - # Set the is_dominated flag to False to NOT insert the current solution in the current dominated set. - # Instead, insert it into the non-dominated set. - is_dominated = False - non_dominated_set.append(sol1) - break - else: - # Reaching here means the solution does not dominate the current solution. - pass - - # If the flag is True, then no solution dominates the current solution. - if is_dominated: - dominated_set.append(sol1) - - # Return the dominated and non-dominated sets. - return dominated_set, non_dominated_set - -def non_dominated_sorting(fitness): - """ - Apply the non-dominant sorting over the fitness to create the pareto fronts based on non-dominaned sorting of the solutions. - - Parameters - ---------- - fitness : TYPE - An array of the population fitness across all objective function. - - Returns - ------- - pareto_fronts : TYPE - An array of the pareto fronts. - - """ - # A list of all non-dominated sets. - pareto_fronts = [] - - # The remaining set to be explored for non-dominance. - # Initially it is set to the entire population. - # The solutions of each non-dominated set are removed after each iteration. - remaining_set = fitness.copy() - - # Zipping the solution index with the solution's fitness. - # This helps to easily identify the index of each solution. - # Each element has: - # 1) The index of the solution. - # 2) An array of the fitness values of this solution across all objectives. - # remaining_set = numpy.array(list(zip(range(0, fitness.shape[0]), non_dominated_set))) - remaining_set = list(zip(range(0, fitness.shape[0]), remaining_set)) - - # A list mapping the index of each pareto front to the set of solutions in this front. - solutions_fronts_indices = [-1]*len(remaining_set) - solutions_fronts_indices = numpy.array(solutions_fronts_indices) - - # Index of the current pareto front. - front_index = -1 - while len(remaining_set) > 0: - front_index += 1 - - # Get the current non-dominated set of solutions. - pareto_front, remaining_set = get_non_dominated_set(curr_solutions=remaining_set) - pareto_front = numpy.array(pareto_front, dtype=object) - pareto_fronts.append(pareto_front) - - solutions_indices = pareto_front[:, 0].astype(int) - solutions_fronts_indices[solutions_indices] = front_index - - return pareto_fronts, solutions_fronts_indices - -def crowding_distance(pareto_front, fitness): - """ - Calculate the crowding dstance for all solutions in the current pareto front. - - Parameters - ---------- - pareto_front : TYPE - The set of solutions in the current pareto front. - fitness : TYPE - The fitness of the current population. - - Returns - ------- - obj_crowding_dist_list : TYPE - A nested list of the values for all objectives alongside their crowding distance. - crowding_dist_sum : TYPE - A list of the sum of crowding distances across all objectives for each solution. - crowding_dist_front_sorted_indices : TYPE - The indices of the solutions (relative to the current front) sorted by the crowding distance. - crowding_dist_pop_sorted_indices : TYPE - The indices of the solutions (relative to the population) sorted by the crowding distance. - """ - - # Each solution in the pareto front has 2 elements: - # 1) The index of the solution in the population. - # 2) A list of the fitness values for all objectives of the solution. - # Before proceeding, remove the indices from each solution in the pareto front. - pareto_front_no_indices = numpy.array([pareto_front[:, 1][idx] for idx in range(pareto_front.shape[0])]) - - # If there is only 1 solution, then return empty arrays for the crowding distance. - if pareto_front_no_indices.shape[0] == 1: - # There is only 1 index. - return numpy.array([]), numpy.array([]), numpy.array([0]), pareto_front[:, 0].astype(int) - - # An empty list holding info about the objectives of each solution. The info includes the objective value and crowding distance. - obj_crowding_dist_list = [] - # Loop through the objectives to calculate the crowding distance of each solution across all objectives. - for obj_idx in range(pareto_front_no_indices.shape[1]): - obj = pareto_front_no_indices[:, obj_idx] - # This variable has a nested list where each child list zip the following together: - # 1) The index of the objective value. - # 2) The objective value. - # 3) Initialize the crowding distance by zero. - obj = list(zip(range(len(obj)), obj, [0]*len(obj))) - obj = [list(element) for element in obj] - # This variable is the sorted version where sorting is done by the objective value (second element). - # Note that the first element is still the original objective index before sorting. - obj_sorted = sorted(obj, key=lambda x: x[1]) - - # Get the minimum and maximum values for the current objective. - obj_min_val = min(fitness[:, obj_idx]) - obj_max_val = max(fitness[:, obj_idx]) - denominator = obj_max_val - obj_min_val - # To avoid division by zero, set the denominator to a tiny value. - if denominator == 0: - denominator = 0.0000001 - - # Set the crowding distance to the first and last solutions (after being sorted) to infinity. - inf_val = float('inf') - # crowding_distance[0] = inf_val - obj_sorted[0][2] = inf_val - # crowding_distance[-1] = inf_val - obj_sorted[-1][2] = inf_val - - # If there are only 2 solutions in the current pareto front, then do not proceed. - # The crowding distance for such 2 solutions is infinity. - if len(obj_sorted) <= 2: - obj_crowding_dist_list.append(obj_sorted) - break - - for idx in range(1, len(obj_sorted)-1): - # Calculate the crowding distance. - crowding_dist = obj_sorted[idx+1][1] - obj_sorted[idx-1][1] - crowding_dist = crowding_dist / denominator - # Insert the crowding distance back into the list to override the initial zero. - obj_sorted[idx][2] = crowding_dist - - # Sort the objective by the original index at index 0 of the each child list. - obj_sorted = sorted(obj_sorted, key=lambda x: x[0]) - obj_crowding_dist_list.append(obj_sorted) - - obj_crowding_dist_list = numpy.array(obj_crowding_dist_list) - crowding_dist = numpy.array([obj_crowding_dist_list[idx, :, 2] for idx in range(len(obj_crowding_dist_list))]) - crowding_dist_sum = numpy.sum(crowding_dist, axis=0) - - # An array of the sum of crowding distances across all objectives. - # Each row has 2 elements: - # 1) The index of the solution. - # 2) The sum of all crowding distances for all objective of the solution. - crowding_dist_sum = numpy.array(list(zip(obj_crowding_dist_list[0, :, 0], crowding_dist_sum))) - crowding_dist_sum = sorted(crowding_dist_sum, key=lambda x: x[1], reverse=True) - - # The sorted solutions' indices by the crowding distance. - crowding_dist_front_sorted_indices = numpy.array(crowding_dist_sum)[:, 0] - crowding_dist_front_sorted_indices = crowding_dist_front_sorted_indices.astype(int) - # Note that such indices are relative to the front, NOT the population. - # It is mandatory to map such front indices to population indices before using them to refer to the population. - crowding_dist_pop_sorted_indices = pareto_front[:, 0] - crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices[crowding_dist_front_sorted_indices] - crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices.astype(int) - - return obj_crowding_dist_list, crowding_dist_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices - -def sort_solutions_nsga2(fitness): - """ - Sort the solutions based on the fitness. - The sorting procedure differs based on whether the problem is single-objective or multi-objective optimization. - If it is multi-objective, then non-dominated sorting and crowding distance are applied. - At first, non-dominated sorting is applied to classify the solutions into pareto fronts. - Then the solutions inside each front are sorted using crowded distance. - The solutions inside pareto front X always come before those in front X+1. - - Parameters - ---------- - fitness : TYPE - The fitness of the entire population. - - Returns - ------- - solutions_sorted : TYPE - The indices of the sorted solutions. - - """ - if type(fitness[0]) in [list, tuple, numpy.ndarray]: - # Multi-objective optimization problem. - solutions_sorted = [] - # Split the solutions into pareto fronts using non-dominated sorting. - pareto_fronts, solutions_fronts_indices = non_dominated_sorting(fitness) - for pareto_front in pareto_fronts: - # Sort the solutions in the front using crowded distance. - _, _, _, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front=pareto_front.copy(), - fitness=fitness) - crowding_dist_pop_sorted_indices = list(crowding_dist_pop_sorted_indices) - # Append the sorted solutions into the list. - solutions_sorted.extend(crowding_dist_pop_sorted_indices) - elif type(fitness[0]) in pygad.GA.supported_int_float_types: - # Single-objective optimization problem. - solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - # Reverse the sorted solutions so that the best solution comes first. - solutions_sorted.reverse() - - return solutions_sorted diff --git a/pygad/pygad.py b/pygad/pygad.py index 97205f9e..fc3e5b55 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -11,9 +11,11 @@ from pygad import visualize import sys +# Extend all the classes so that they can be referenced by just the `self` object of the `pygad.GA` class. class GA(utils.parent_selection.ParentSelection, utils.crossover.Crossover, utils.mutation.Mutation, + utils.nsga2.NSGA2, helper.unique.Unique, visualize.plot.Plot): diff --git a/pygad/utils/__init__.py b/pygad/utils/__init__.py index 3b8450be..95bf6e5d 100644 --- a/pygad/utils/__init__.py +++ b/pygad/utils/__init__.py @@ -1,5 +1,6 @@ from pygad.utils import parent_selection from pygad.utils import crossover from pygad.utils import mutation +from pygad.utils import nsga2 -__version__ = "1.0.1" \ No newline at end of file +__version__ = "1.1.0" \ No newline at end of file diff --git a/pygad/utils/crossover.py b/pygad/utils/crossover.py index 6cc9a27e..b03d361c 100644 --- a/pygad/utils/crossover.py +++ b/pygad/utils/crossover.py @@ -6,6 +6,10 @@ import random class Crossover: + + def __init__(): + pass + def single_point_crossover(self, parents, offspring_size): """ diff --git a/pygad/utils/mutation.py b/pygad/utils/mutation.py index ef0e85f5..2d6bed92 100644 --- a/pygad/utils/mutation.py +++ b/pygad/utils/mutation.py @@ -9,6 +9,9 @@ class Mutation: + def __init__(): + pass + def random_mutation(self, offspring): """ diff --git a/pygad/utils/nsga2.py b/pygad/utils/nsga2.py new file mode 100644 index 00000000..8b24f300 --- /dev/null +++ b/pygad/utils/nsga2.py @@ -0,0 +1,257 @@ +import numpy +import pygad + +class NSGA2: + def __init__(): + pass + + def get_non_dominated_set(self, curr_solutions): + """ + Get the set of non-dominated solutions from the current set of solutions. + + Parameters + ---------- + curr_solutions : TYPE + The set of solutions to find its non-dominated set. + + Returns + ------- + dominated_set : TYPE + A set of the dominated solutions. + non_dominated_set : TYPE + A set of the non-dominated set. + + """ + # List of the members of the current dominated pareto front/set. + dominated_set = [] + # List of the non-members of the current dominated pareto front/set. + non_dominated_set = [] + for idx1, sol1 in enumerate(curr_solutions): + # Flag indicates whether the solution is a member of the current dominated set. + is_dominated = True + for idx2, sol2 in enumerate(curr_solutions): + if idx1 == idx2: + continue + # Zipping the 2 solutions so the corresponding genes are in the same list. + # The returned array is of size (N, 2) where N is the number of genes. + two_solutions = numpy.array(list(zip(sol1[1], sol2[1]))) + + #TODO Consider repacing < by > for maximization problems. + # Checking for if any solution dominates the current solution by applying the 2 conditions. + # le_eq (less than or equal): All elements must be True. + # le (less than): Only 1 element must be True. + le_eq = two_solutions[:, 1] >= two_solutions[:, 0] + le = two_solutions[:, 1] > two_solutions[:, 0] + + # If the 2 conditions hold, then a solution dominates the current solution. + # The current solution is not considered a member of the dominated set. + if le_eq.all() and le.any(): + # Set the is_dominated flag to False to NOT insert the current solution in the current dominated set. + # Instead, insert it into the non-dominated set. + is_dominated = False + non_dominated_set.append(sol1) + break + else: + # Reaching here means the solution does not dominate the current solution. + pass + + # If the flag is True, then no solution dominates the current solution. + if is_dominated: + dominated_set.append(sol1) + + # Return the dominated and non-dominated sets. + return dominated_set, non_dominated_set + + def non_dominated_sorting(self, fitness): + """ + Apply the non-dominant sorting over the fitness to create the pareto fronts based on non-dominaned sorting of the solutions. + + Parameters + ---------- + fitness : TYPE + An array of the population fitness across all objective function. + + Returns + ------- + pareto_fronts : TYPE + An array of the pareto fronts. + + """ + # A list of all non-dominated sets. + pareto_fronts = [] + + # The remaining set to be explored for non-dominance. + # Initially it is set to the entire population. + # The solutions of each non-dominated set are removed after each iteration. + remaining_set = fitness.copy() + + # Zipping the solution index with the solution's fitness. + # This helps to easily identify the index of each solution. + # Each element has: + # 1) The index of the solution. + # 2) An array of the fitness values of this solution across all objectives. + # remaining_set = numpy.array(list(zip(range(0, fitness.shape[0]), non_dominated_set))) + remaining_set = list(zip(range(0, fitness.shape[0]), remaining_set)) + + # A list mapping the index of each pareto front to the set of solutions in this front. + solutions_fronts_indices = [-1]*len(remaining_set) + solutions_fronts_indices = numpy.array(solutions_fronts_indices) + + # Index of the current pareto front. + front_index = -1 + while len(remaining_set) > 0: + front_index += 1 + + # Get the current non-dominated set of solutions. + pareto_front, remaining_set = self.get_non_dominated_set(curr_solutions=remaining_set) + pareto_front = numpy.array(pareto_front, dtype=object) + pareto_fronts.append(pareto_front) + + solutions_indices = pareto_front[:, 0].astype(int) + solutions_fronts_indices[solutions_indices] = front_index + + return pareto_fronts, solutions_fronts_indices + + def crowding_distance(self, pareto_front, fitness): + """ + Calculate the crowding dstance for all solutions in the current pareto front. + + Parameters + ---------- + pareto_front : TYPE + The set of solutions in the current pareto front. + fitness : TYPE + The fitness of the current population. + + Returns + ------- + obj_crowding_dist_list : TYPE + A nested list of the values for all objectives alongside their crowding distance. + crowding_dist_sum : TYPE + A list of the sum of crowding distances across all objectives for each solution. + crowding_dist_front_sorted_indices : TYPE + The indices of the solutions (relative to the current front) sorted by the crowding distance. + crowding_dist_pop_sorted_indices : TYPE + The indices of the solutions (relative to the population) sorted by the crowding distance. + """ + + # Each solution in the pareto front has 2 elements: + # 1) The index of the solution in the population. + # 2) A list of the fitness values for all objectives of the solution. + # Before proceeding, remove the indices from each solution in the pareto front. + pareto_front_no_indices = numpy.array([pareto_front[:, 1][idx] for idx in range(pareto_front.shape[0])]) + + # If there is only 1 solution, then return empty arrays for the crowding distance. + if pareto_front_no_indices.shape[0] == 1: + # There is only 1 index. + return numpy.array([]), numpy.array([]), numpy.array([0]), pareto_front[:, 0].astype(int) + + # An empty list holding info about the objectives of each solution. The info includes the objective value and crowding distance. + obj_crowding_dist_list = [] + # Loop through the objectives to calculate the crowding distance of each solution across all objectives. + for obj_idx in range(pareto_front_no_indices.shape[1]): + obj = pareto_front_no_indices[:, obj_idx] + # This variable has a nested list where each child list zip the following together: + # 1) The index of the objective value. + # 2) The objective value. + # 3) Initialize the crowding distance by zero. + obj = list(zip(range(len(obj)), obj, [0]*len(obj))) + obj = [list(element) for element in obj] + # This variable is the sorted version where sorting is done by the objective value (second element). + # Note that the first element is still the original objective index before sorting. + obj_sorted = sorted(obj, key=lambda x: x[1]) + + # Get the minimum and maximum values for the current objective. + obj_min_val = min(fitness[:, obj_idx]) + obj_max_val = max(fitness[:, obj_idx]) + denominator = obj_max_val - obj_min_val + # To avoid division by zero, set the denominator to a tiny value. + if denominator == 0: + denominator = 0.0000001 + + # Set the crowding distance to the first and last solutions (after being sorted) to infinity. + inf_val = float('inf') + # crowding_distance[0] = inf_val + obj_sorted[0][2] = inf_val + # crowding_distance[-1] = inf_val + obj_sorted[-1][2] = inf_val + + # If there are only 2 solutions in the current pareto front, then do not proceed. + # The crowding distance for such 2 solutions is infinity. + if len(obj_sorted) <= 2: + obj_crowding_dist_list.append(obj_sorted) + break + + for idx in range(1, len(obj_sorted)-1): + # Calculate the crowding distance. + crowding_dist = obj_sorted[idx+1][1] - obj_sorted[idx-1][1] + crowding_dist = crowding_dist / denominator + # Insert the crowding distance back into the list to override the initial zero. + obj_sorted[idx][2] = crowding_dist + + # Sort the objective by the original index at index 0 of the each child list. + obj_sorted = sorted(obj_sorted, key=lambda x: x[0]) + obj_crowding_dist_list.append(obj_sorted) + + obj_crowding_dist_list = numpy.array(obj_crowding_dist_list) + crowding_dist = numpy.array([obj_crowding_dist_list[idx, :, 2] for idx in range(len(obj_crowding_dist_list))]) + crowding_dist_sum = numpy.sum(crowding_dist, axis=0) + + # An array of the sum of crowding distances across all objectives. + # Each row has 2 elements: + # 1) The index of the solution. + # 2) The sum of all crowding distances for all objective of the solution. + crowding_dist_sum = numpy.array(list(zip(obj_crowding_dist_list[0, :, 0], crowding_dist_sum))) + crowding_dist_sum = sorted(crowding_dist_sum, key=lambda x: x[1], reverse=True) + + # The sorted solutions' indices by the crowding distance. + crowding_dist_front_sorted_indices = numpy.array(crowding_dist_sum)[:, 0] + crowding_dist_front_sorted_indices = crowding_dist_front_sorted_indices.astype(int) + # Note that such indices are relative to the front, NOT the population. + # It is mandatory to map such front indices to population indices before using them to refer to the population. + crowding_dist_pop_sorted_indices = pareto_front[:, 0] + crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices[crowding_dist_front_sorted_indices] + crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices.astype(int) + + return obj_crowding_dist_list, crowding_dist_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices + + def sort_solutions_nsga2(self, fitness): + """ + Sort the solutions based on the fitness. + The sorting procedure differs based on whether the problem is single-objective or multi-objective optimization. + If it is multi-objective, then non-dominated sorting and crowding distance are applied. + At first, non-dominated sorting is applied to classify the solutions into pareto fronts. + Then the solutions inside each front are sorted using crowded distance. + The solutions inside pareto front X always come before those in front X+1. + + Parameters + ---------- + fitness : TYPE + The fitness of the entire population. + + Returns + ------- + solutions_sorted : TYPE + The indices of the sorted solutions. + + """ + if type(fitness[0]) in [list, tuple, numpy.ndarray]: + # Multi-objective optimization problem. + solutions_sorted = [] + # Split the solutions into pareto fronts using non-dominated sorting. + pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness) + self.pareto_fronts = pareto_fronts.copy() + for pareto_front in pareto_fronts: + # Sort the solutions in the front using crowded distance. + _, _, _, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=pareto_front.copy(), + fitness=fitness) + crowding_dist_pop_sorted_indices = list(crowding_dist_pop_sorted_indices) + # Append the sorted solutions into the list. + solutions_sorted.extend(crowding_dist_pop_sorted_indices) + elif type(fitness[0]) in pygad.GA.supported_int_float_types: + # Single-objective optimization problem. + solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) + # Reverse the sorted solutions so that the best solution comes first. + solutions_sorted.reverse() + + return solutions_sorted diff --git a/pygad/utils/parent_selection.py b/pygad/utils/parent_selection.py index 464dcbd0..4016ca1a 100644 --- a/pygad/utils/parent_selection.py +++ b/pygad/utils/parent_selection.py @@ -3,9 +3,12 @@ """ import numpy -from ..helper import nsga2 class ParentSelection: + + def __init__(): + pass + def steady_state_selection(self, fitness, num_parents): """ @@ -23,7 +26,7 @@ def steady_state_selection(self, fitness, num_parents): # Return the indices of the sorted solutions (all solutions in the population). # This function works with both single- and multi-objective optimization problems. - fitness_sorted = nsga2.sort_solutions_nsga2(fitness=fitness) + fitness_sorted = self.sort_solutions_nsga2(fitness=fitness) # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. if self.gene_type_single == True: @@ -51,7 +54,7 @@ def rank_selection(self, fitness, num_parents): # Return the indices of the sorted solutions (all solutions in the population). # This function works with both single- and multi-objective optimization problems. - fitness_sorted = nsga2.sort_solutions_nsga2(fitness=fitness) + fitness_sorted = self.sort_solutions_nsga2(fitness=fitness) # Rank the solutions based on their fitness. The worst is gives the rank 1. The best has the rank N. rank = numpy.arange(1, self.sol_per_pop+1) @@ -114,7 +117,7 @@ def tournament_selection(self, fitness, num_parents): # Return the indices of the sorted solutions (all solutions in the population). # This function works with both single- and multi-objective optimization problems. - fitness_sorted = nsga2.sort_solutions_nsga2(fitness=fitness) + fitness_sorted = self.sort_solutions_nsga2(fitness=fitness) if self.gene_type_single == True: parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) @@ -312,7 +315,7 @@ def tournament_selection_nsga2(self, If 2 solutions are in the same pareto front, then crowding distance is calculated. The solution with the higher crowding distance is selected. If the 2 solutions are in the same pareto front and have the same crowding distance, then a solution is randomly selected. Later, the selected parents will mate to produce the offspring. - + It accepts 2 parameters: -fitness: The fitness values for the current population. -num_parents: The number of parents to be selected. @@ -332,10 +335,11 @@ def tournament_selection_nsga2(self, # The indices of the selected parents. parents_indices = [] - # TODO If there is only a single objective, each pareto front is expected to have only 1 solution. + # If there is only a single objective, each pareto front is expected to have only 1 solution. # TODO Make a test to check for that behaviour and add it to the GitHub actions tests. - pareto_fronts, solutions_fronts_indices = nsga2.non_dominated_sorting(fitness) - + pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness) + self.pareto_fronts = pareto_fronts.copy() + # Randomly generate pairs of indices to apply for NSGA-II tournament selection for selecting the parents solutions. rand_indices = numpy.random.randint(low=0.0, high=len(solutions_fronts_indices), @@ -362,7 +366,7 @@ def tournament_selection_nsga2(self, # Fetch the current pareto front. pareto_front = pareto_fronts[parent_fronts_indices[0]] # Index 1 can also be used. - + # If there is only 1 solution in the pareto front, just return it without calculating the crowding distance (it is useless). if pareto_front.shape[0] == 1: selected_parent_idx = current_indices[0] # Index 1 can also be used. @@ -370,7 +374,7 @@ def tournament_selection_nsga2(self, # Reaching here means the pareto front has more than 1 solution. # Calculate the crowding distance of the solutions of the pareto front. - obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = nsga2.crowding_distance(pareto_front=pareto_front.copy(), + obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=pareto_front.copy(), fitness=fitness) # This list has the sorted front-based indices for the solutions in the current pareto front. @@ -452,10 +456,11 @@ def nsga2_selection(self, # The indices of the selected parents. parents_indices = [] - - # TODO If there is only a single objective, each pareto front is expected to have only 1 solution. + + # If there is only a single objective, each pareto front is expected to have only 1 solution. # TODO Make a test to check for that behaviour. - pareto_fronts, solutions_fronts_indices = nsga2.non_dominated_sorting(fitness) + pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness) + self.pareto_fronts = pareto_fronts.copy() # The number of remaining parents to be selected. num_remaining_parents = num_parents @@ -485,8 +490,8 @@ def nsga2_selection(self, # If only a subset of the front is needed, then use the crowding distance to sort the solutions and select only the number needed. # Calculate the crowding distance of the solutions of the pareto front. - obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = nsga2.crowding_distance(pareto_front=current_pareto_front.copy(), - fitness=fitness) + obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=current_pareto_front.copy(), + fitness=fitness) for selected_solution_idx in crowding_dist_pop_sorted_indices[0:num_remaining_parents]: # Insert the parent into the parents array.