11import numpy
22
3- population = 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- def non_dominant_sorting (curr_set ):
13- # List of the members of the current dominant front/set.
14- dominant_set = []
15- # List of the non-members of the current dominant front/set.
16- non_dominant_set = []
17- for idx1 , sol1 in enumerate (curr_set ):
18- # Flag indicates whether the solution is a member of the current dominant set.
19- is_dominant = True
20- for idx2 , sol2 in enumerate (curr_set ):
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 ]])
11+
12+ def get_non_dominated_set (curr_solutions ):
13+ """
14+ Get the set of non-dominated solutions from the current set of solutions.
15+
16+ Parameters
17+ ----------
18+ curr_solutions : TYPE
19+ The set of solutions to find its non-dominated set.
20+
21+ Returns
22+ -------
23+ dominated_set : TYPE
24+ A set of the dominated solutions.
25+ non_dominated_set : TYPE
26+ A set of the non-dominated set.
27+
28+ """
29+ # List of the members of the current dominated pareto front/set.
30+ dominated_set = []
31+ # List of the non-members of the current dominated pareto front/set.
32+ non_dominated_set = []
33+ for idx1 , sol1 in enumerate (curr_solutions ):
34+ # Flag indicates whether the solution is a member of the current dominated set.
35+ is_dominated = True
36+ for idx2 , sol2 in enumerate (curr_solutions ):
2137 if idx1 == idx2 :
2238 continue
2339 # Zipping the 2 solutions so the corresponding genes are in the same list.
2440 # The returned array is of size (N, 2) where N is the number of genes.
25- b = numpy .array (list (zip (sol1 , sol2 )))
26-
41+ two_solutions = numpy .array (list (zip (sol1 [ 1 ] , sol2 [ 1 ] )))
42+
2743 #TODO Consider repacing < by > for maximization problems.
2844 # Checking for if any solution dominates the current solution by applying the 2 conditions.
29- # le_eq: All elements must be True.
30- # le: Only 1 element must be True.
31- le_eq = b [:, 1 ] <= b [:, 0 ]
32- le = b [:, 1 ] < b [:, 0 ]
45+ # le_eq (less than or equal) : All elements must be True.
46+ # le (less than) : Only 1 element must be True.
47+ le_eq = two_solutions [:, 1 ] <= two_solutions [:, 0 ]
48+ le = two_solutions [:, 1 ] < two_solutions [:, 0 ]
3349
3450 # If the 2 conditions hold, then a solution dominates the current solution.
35- # The current solution is not considered a member of the dominant set.
51+ # The current solution is not considered a member of the dominated set.
3652 if le_eq .all () and le .any ():
37- # print(f"{sol2} dominates {sol1}")
38- # Set the is_dominant flag to False to not insert the current solution in the current dominant set.
39- # Instead, insert it into the non-dominant set.
40- is_dominant = False
41- non_dominant_set .append (sol1 )
53+ # Set the is_dominated flag to False to NOT insert the current solution in the current dominated set.
54+ # Instead, insert it into the non-dominated set.
55+ is_dominated = False
56+ non_dominated_set .append (sol1 )
4257 break
4358 else :
44- # Reaching here means the solution does not dominant the current solution.
45- # print(f"{sol2} does not dominate {sol1}")
59+ # Reaching here means the solution does not dominate the current solution.
4660 pass
4761
4862 # If the flag is True, then no solution dominates the current solution.
49- if is_dominant :
50- dominant_set .append (sol1 )
63+ if is_dominated :
64+ dominated_set .append (sol1 )
65+
66+ # Return the dominated and non-dominated sets.
67+ return dominated_set , non_dominated_set
68+
69+ def non_dominated_sorting (population_fitness ):
70+ """
71+ Apply the non-dominant sorting over the population_fitness to create sets of non-dominaned solutions.
72+
73+ Parameters
74+ ----------
75+ population_fitness : TYPE
76+ An array of the population fitness across all objective function.
5177
52- # Return the dominant and non-dominant sets.
53- return dominant_set , non_dominant_set
78+ Returns
79+ -------
80+ non_dominated_sets : TYPE
81+ An array of the non-dominated sets.
5482
55- dominant_set = []
56- non_dominant_set = population .copy ()
57- while len (non_dominant_set ) > 0 :
58- d1 , non_dominant_set = non_dominant_sorting (non_dominant_set )
59- dominant_set .append (numpy .array (d1 ))
83+ """
84+ # A list of all non-dominated sets.
85+ non_dominated_sets = []
6086
61- for i , s in enumerate (dominant_set ):
62- print (f'Dominant Front Set { i + 1 } :\n { s } ' )
87+ # The remaining set to be explored for non-dominance.
88+ # Initially it is set to the entire population.
89+ # The solutions of each non-dominated set are removed after each iteration.
90+ remaining_set = population_fitness .copy ()
91+
92+ # Zipping the solution index with the solution's fitness.
93+ # This helps to easily identify the index of each solution.
94+ # Each element has:
95+ # 1) The index of the solution.
96+ # 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 ))
99+ while len (remaining_set ) > 0 :
100+ # Get the current non-dominated set of solutions.
101+ d1 , remaining_set = get_non_dominated_set (curr_solutions = remaining_set )
102+ non_dominated_sets .append (numpy .array (d1 , dtype = object ))
103+ return non_dominated_sets
104+
105+ def crowding_distance (pareto_front ):
106+ """
107+ Calculate the crowding dstance for all solutions in the current pareto front.
108+
109+ Parameters
110+ ----------
111+ pareto_front : TYPE
112+ The set of solutions in the current pareto front.
113+
114+ Returns
115+ -------
116+ obj_crowding_dist_list : TYPE
117+ A nested list of the values for all objectives alongside their crowding distance.
118+ crowding_dist_sum : TYPE
119+ A list of the sum of crowding distances across all objectives for each solution.
120+ """
121+
122+ # Each solution in the pareto front has 2 elements:
123+ # 1) The index of the solution in the population.
124+ # 2) A list of the fitness values for all objectives of the solution.
125+ # Before proceeding, remove the indices from each solution in the pareto front.
126+ pareto_front = numpy .array ([pareto_front [idx ] for idx in range (pareto_front .shape [0 ])])
127+
128+ # If there is only 1 solution, then return empty arrays for the crowding distance.
129+ if pareto_front .shape [0 ] == 1 :
130+ return numpy .array ([]), numpy .array ([])
63131
64- print ("\n \n \n --------------------" )
65- def crowding_distance (front ):
66132 # An empty list holding info about the objectives of each solution. The info includes the objective value and crowding distance.
67133 obj_crowding_dist_list = []
68134 # Loop through the objectives to calculate the crowding distance of each solution across all objectives.
69- for obj_idx in range (front .shape [1 ]):
70- obj = front [:, obj_idx ]
135+ for obj_idx in range (pareto_front .shape [1 ]):
136+ obj = pareto_front [:, obj_idx ]
71137 # This variable has a nested list where each child list zip the following together:
72138 # 1) The index of the objective value.
73139 # 2) The objective value.
@@ -79,8 +145,8 @@ def crowding_distance(front):
79145 obj_sorted = sorted (obj , key = lambda x : x [1 ])
80146
81147 # Get the minimum and maximum values for the current objective.
82- obj_min_val = min (population [:, obj_idx ])
83- obj_max_val = max (population [:, obj_idx ])
148+ obj_min_val = min (population_fitness [:, obj_idx ])
149+ obj_max_val = max (population_fitness [:, obj_idx ])
84150 denominator = obj_max_val - obj_min_val
85151 # To avoid division by zero, set the denominator to a tiny value.
86152 if denominator == 0 :
@@ -93,7 +159,7 @@ def crowding_distance(front):
93159 # crowding_distance[-1] = inf_val
94160 obj_sorted [- 1 ][2 ] = inf_val
95161
96- # If there are only 2 solutions in the current front, then do not proceed.
162+ # If there are only 2 solutions in the current pareto front, then do not proceed.
97163 # The crowding distance for such 2 solutions is infinity.
98164 if len (obj_sorted ) <= 2 :
99165 break
@@ -113,11 +179,24 @@ def crowding_distance(front):
113179 crowding_dist = numpy .array ([obj_crowding_dist_list [idx , :, 2 ] for idx in range (len (obj_crowding_dist_list ))])
114180 crowding_dist_sum = numpy .sum (crowding_dist , axis = 0 )
115181
182+ # An array of the sum of crowding distances across all objectives.
183+ # Each row has 2 elements:
184+ # 1) The index of the solution.
185+ # 2) The sum of all crowding distances for all objective of the solution.
186+ crowding_dist_sum = numpy .array (list (zip (obj_crowding_dist_list [0 , :, 0 ], crowding_dist_sum )))
187+ crowding_dist_sum = sorted (crowding_dist_sum , key = lambda x : x [1 ], reverse = True )
188+
116189 return obj_crowding_dist_list , crowding_dist_sum
117190
118- # Fetch the current front.
119- front = dominant_set [1 ]
120- obj_crowding_distance_list , crowding_distance_sum = crowding_distance (front )
191+ non_dominated_sets = non_dominated_sorting (population_fitness )
192+
193+ # for i, s in enumerate(non_dominated_sets):
194+ # print(f'dominated Pareto Front Set {i+1}:\n{s}')
195+ # print("\n\n\n--------------------")
196+
197+ # Fetch the current pareto front.
198+ pareto_front = non_dominated_sets [1 ][:, 1 ]
199+ obj_crowding_distance_list , crowding_distance_sum = crowding_distance (pareto_front )
121200
122201print (obj_crowding_distance_list )
123- print (f'\n Sum of Crowd Dists\n { crowding_distance_sum } ' )
202+ print (f'\n Sorted Sum of Crowd Dists\n { crowding_distance_sum } ' )
0 commit comments