Skip to content

Commit 0ea3674

Browse files
committed
Support of NSGA-II
1 parent 912a1d1 commit 0ea3674

8 files changed

+506
-103
lines changed

NSGA-II/non_dominant_sorting.py

Lines changed: 0 additions & 62 deletions
This file was deleted.

NSGA-II/non_dominant_sorting_crowding_distance.py

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
11
import numpy
22

3-
population_fitness = numpy.array([[20, 2.2],
4-
[60, 4.4],
5-
[65, 3.5],
6-
[15, 4.4],
7-
[55, 4.5],
8-
[50, 1.8],
9-
[80, 4.0],
10-
[25, 4.6]])
3+
fitness = numpy.array([[20, 2.2],
4+
[60, 4.4],
5+
[65, 3.5],
6+
[15, 4.4],
7+
[55, 4.5],
8+
[50, 1.8],
9+
[80, 4.0],
10+
[25, 4.6]])
11+
12+
# fitness = numpy.array([20,
13+
# 60,
14+
# 65,
15+
# 15,
16+
# 55,
17+
# 50,
18+
# 80,
19+
# 25])
20+
21+
# fitness = numpy.array([[20],
22+
# [60],
23+
# [65],
24+
# [15],
25+
# [55],
26+
# [50],
27+
# [80],
28+
# [25]])
1129

1230
def get_non_dominated_set(curr_solutions):
1331
"""
@@ -66,13 +84,13 @@ def get_non_dominated_set(curr_solutions):
6684
# Return the dominated and non-dominated sets.
6785
return dominated_set, non_dominated_set
6886

69-
def non_dominated_sorting(population_fitness):
87+
def non_dominated_sorting(fitness):
7088
"""
71-
Apply the non-dominant sorting over the population_fitness to create the pareto fronts based on non-dominaned sorting of the solutions.
89+
Apply the non-dominant sorting over the fitness to create the pareto fronts based on non-dominaned sorting of the solutions.
7290
7391
Parameters
7492
----------
75-
population_fitness : TYPE
93+
fitness : TYPE
7694
An array of the population fitness across all objective function.
7795
7896
Returns
@@ -87,15 +105,15 @@ def non_dominated_sorting(population_fitness):
87105
# The remaining set to be explored for non-dominance.
88106
# Initially it is set to the entire population.
89107
# The solutions of each non-dominated set are removed after each iteration.
90-
remaining_set = population_fitness.copy()
108+
remaining_set = fitness.copy()
91109

92110
# Zipping the solution index with the solution's fitness.
93111
# This helps to easily identify the index of each solution.
94112
# Each element has:
95113
# 1) The index of the solution.
96114
# 2) An array of the fitness values of this solution across all objectives.
97-
# remaining_set = numpy.array(list(zip(range(0, population_fitness.shape[0]), non_dominated_set)))
98-
remaining_set = list(zip(range(0, population_fitness.shape[0]), remaining_set))
115+
# remaining_set = numpy.array(list(zip(range(0, fitness.shape[0]), non_dominated_set)))
116+
remaining_set = list(zip(range(0, fitness.shape[0]), remaining_set))
99117

100118
# A list mapping the index of each pareto front to the set of solutions in this front.
101119
solutions_fronts_indices = [-1]*len(remaining_set)
@@ -116,14 +134,16 @@ def non_dominated_sorting(population_fitness):
116134

117135
return pareto_fronts, solutions_fronts_indices
118136

119-
def crowding_distance(pareto_front):
137+
def crowding_distance(pareto_front, fitness):
120138
"""
121139
Calculate the crowding dstance for all solutions in the current pareto front.
122140
123141
Parameters
124142
----------
125143
pareto_front : TYPE
126144
The set of solutions in the current pareto front.
145+
fitness : TYPE
146+
The fitness of the current population.
127147
128148
Returns
129149
-------
@@ -164,8 +184,8 @@ def crowding_distance(pareto_front):
164184
obj_sorted = sorted(obj, key=lambda x: x[1])
165185

166186
# Get the minimum and maximum values for the current objective.
167-
obj_min_val = min(population_fitness[:, obj_idx])
168-
obj_max_val = max(population_fitness[:, obj_idx])
187+
obj_min_val = min(fitness[:, obj_idx])
188+
obj_max_val = max(fitness[:, obj_idx])
169189
denominator = obj_max_val - obj_min_val
170190
# To avoid division by zero, set the denominator to a tiny value.
171191
if denominator == 0:
@@ -217,9 +237,11 @@ def crowding_distance(pareto_front):
217237
return obj_crowding_dist_list, crowding_dist_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices
218238

219239
def tournament_selection_nsga2(self,
220-
pareto_fronts,
221-
solutions_fronts_indices,
222-
num_parents):
240+
fitness,
241+
num_parents
242+
# pareto_fronts,
243+
# solutions_fronts_indices,
244+
):
223245

224246
"""
225247
Select the parents using the tournament selection technique for NSGA-II.
@@ -231,9 +253,10 @@ def tournament_selection_nsga2(self,
231253
Later, the selected parents will mate to produce the offspring.
232254
233255
It accepts 2 parameters:
256+
-fitness: The fitness values for the current population.
257+
-num_parents: The number of parents to be selected.
234258
-pareto_fronts: A nested array of all the pareto fronts. Each front has its solutions.
235259
-solutions_fronts_indices: A list of the pareto front index of each solution in the current population.
236-
-num_parents: The number of parents to be selected.
237260
238261
It returns an array of the selected parents alongside their indices in the population.
239262
"""
@@ -246,6 +269,11 @@ def tournament_selection_nsga2(self,
246269
# The indices of the selected parents.
247270
parents_indices = []
248271

272+
# TODO If there is only a single objective, each pareto front is expected to have only 1 solution.
273+
# TODO Make a test to check for that behaviour.
274+
# Find the pareto fronts and the solutions' indicies in each front.
275+
pareto_fronts, solutions_fronts_indices = non_dominated_sorting(fitness)
276+
249277
# Randomly generate pairs of indices to apply for NSGA-II tournament selection for selecting the parents solutions.
250278
rand_indices = numpy.random.randint(low=0.0,
251279
high=len(solutions_fronts_indices),
@@ -284,7 +312,8 @@ def tournament_selection_nsga2(self,
284312
# Reaching here means the pareto front has more than 1 solution.
285313

286314
# Calculate the crowding distance of the solutions of the pareto front.
287-
obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front.copy())
315+
obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front=pareto_front.copy(),
316+
fitness=fitness)
288317

289318
# This list has the sorted front-based indices for the solutions in the current pareto front.
290319
crowding_dist_front_sorted_indices = list(crowding_dist_front_sorted_indices)
@@ -333,9 +362,11 @@ def tournament_selection_nsga2(self,
333362
return parents, numpy.array(parents_indices)
334363

335364
def nsga2_selection(self,
336-
pareto_fronts,
337-
solutions_fronts_indices,
338-
num_parents):
365+
fitness,
366+
num_parents
367+
# pareto_fronts,
368+
# solutions_fronts_indices
369+
):
339370

340371
"""
341372
Select the parents using the Non-Dominated Sorting Genetic Algorithm II (NSGA-II).
@@ -348,9 +379,10 @@ def nsga2_selection(self,
348379
Later, the selected parents will mate to produce the offspring.
349380
350381
It accepts 2 parameters:
382+
-fitness: The fitness values for the current population.
383+
-num_parents: The number of parents to be selected.
351384
-pareto_fronts: A nested array of all the pareto fronts. Each front has its solutions.
352385
-solutions_fronts_indices: A list of the pareto front index of each solution in the current population.
353-
-num_parents: The number of parents to be selected.
354386
355387
It returns an array of the selected parents alongside their indices in the population.
356388
"""
@@ -363,6 +395,11 @@ def nsga2_selection(self,
363395
# The indices of the selected parents.
364396
parents_indices = []
365397

398+
# TODO If there is only a single objective, each pareto front is expected to have only 1 solution.
399+
# TODO Make a test to check for that behaviour.
400+
# Find the pareto fronts and the solutions' indicies in each front.
401+
pareto_fronts, solutions_fronts_indices = non_dominated_sorting(fitness)
402+
366403
# The number of remaining parents to be selected.
367404
num_remaining_parents = num_parents
368405

@@ -387,7 +424,8 @@ def nsga2_selection(self,
387424
# If only a subset of the front is needed, then use the crowding distance to sort the solutions and select only the number needed.
388425

389426
# Calculate the crowding distance of the solutions of the pareto front.
390-
obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(current_pareto_front.copy())
427+
obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front=current_pareto_front.copy(),
428+
fitness=fitness)
391429

392430
for selected_solution_idx in crowding_dist_pop_sorted_indices[0:num_remaining_parents]:
393431
# Insert the parent into the parents array.
@@ -404,8 +442,10 @@ def nsga2_selection(self,
404442
# Make sure the parents indices is returned as a NumPy array.
405443
return parents, numpy.array(parents_indices)
406444

407-
408-
pareto_fronts, solutions_fronts_indices = non_dominated_sorting(population_fitness)
445+
# TODO If there is only a single objective, each pareto front is expected to have only 1 solution.
446+
# TODO Make a test to check for that behaviour.
447+
# Find the pareto fronts and the solutions' indicies in each front.
448+
pareto_fronts, solutions_fronts_indices = non_dominated_sorting(fitness)
409449
# # print('\nsolutions_fronts_indices\n', solutions_fronts_indices)
410450
# for i, s in enumerate(pareto_fronts):
411451
# # print(f'Dominated Pareto Front Set {i+1}:\n{s}')
@@ -422,27 +462,33 @@ class Object(object):
422462
obj.K_tournament = 2
423463

424464
parents, parents_indices = tournament_selection_nsga2(self=obj,
425-
pareto_fronts=pareto_fronts,
426-
solutions_fronts_indices=solutions_fronts_indices,
427-
num_parents=40)
465+
fitness=fitness,
466+
num_parents=4
467+
# pareto_fronts=pareto_fronts,
468+
# solutions_fronts_indices=solutions_fronts_indices,
469+
)
428470
print(f'Tournament Parent Selection for NSGA-II - Indices: \n{parents_indices}')
429471

430472
parents, parents_indices = nsga2_selection(self=obj,
431-
pareto_fronts=pareto_fronts,
432-
solutions_fronts_indices=solutions_fronts_indices,
433-
num_parents=40)
473+
fitness=fitness,
474+
num_parents=4
475+
# pareto_fronts=pareto_fronts,
476+
# solutions_fronts_indices=solutions_fronts_indices,
477+
)
434478
print(f'NSGA-II Parent Selection - Indices: \n{parents_indices}')
435479

436480
# for idx in range(len(pareto_fronts)):
437481
# # Fetch the current pareto front.
438482
# pareto_front = pareto_fronts[idx]
439-
# obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front.copy())
483+
# obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front=pareto_front.copy(),
484+
# fitness=fitness)
440485
# print('Front IDX', crowding_dist_front_sorted_indices)
441486
# print('POP IDX ', crowding_dist_pop_sorted_indices)
442487
# print(f'Sorted Sum of Crowd Dists\n{crowding_distance_sum}')
443488

444489
# # Fetch the current pareto front.
445490
# pareto_front = pareto_fronts[0]
446-
# obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front.copy())
491+
# obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = crowding_distance(pareto_front=pareto_front.copy(),
492+
# fitness=fitness)
447493
# print('\n', crowding_dist_pop_sorted_indices)
448494
# print(f'Sorted Sum of Crowd Dists\n{crowding_distance_sum}')

pygad/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .pygad import * # Relative import.
22

3-
__version__ = "3.1.1"
3+
__version__ = "3.2.0"

pygad/helper/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from pygad.helper import unique
1+
from pygad.helper import unique, nsga2
22

3-
__version__ = "1.1.0"
3+
__version__ = "1.2.0"

0 commit comments

Comments
 (0)