Skip to content

Commit 83f0bed

Browse files
authored
Merge pull request #333 from ahmedfgad/github-actions
GitHub actions
2 parents a5ef680 + eccdc6c commit 83f0bed

File tree

7 files changed

+193
-98
lines changed

7 files changed

+193
-98
lines changed

docs/md/pygad_more.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,9 +337,17 @@ Out of the range of *1000* numbers, all the 100 values might not be satisfying t
337337

338338
> PyGAD does not yet handle the **dependencies** among the genes in the `gene_constraint` parameter.
339339
>
340-
> For example, gene 0 might depend on gene 1. To efficiently enforce the constraints, the constraint for gene 1 must be enforced first (if not `None`) then the constraint for gene 0.
340+
> This is an example where gene 0 depends on gene 1. To efficiently enforce the constraints, the constraint for gene 1 must be enforced first (if not `None`) then the constraint for gene 0.
341341
>
342-
> PyGAD applies constraints sequentially, starting from the first gene to the last. To ensure correct behavior when genes depend on each other, structure your GA problem so that if gene X depends on gene Y, then gene Y appears earlier in the chromosome (solution) than gene X.
342+
> ```python
343+
> gene_constraint=
344+
> [
345+
> lambda solution,values: [val for val in values if val<solution[1]],
346+
> lambda solution,values: [val for val in values if val>10]
347+
> ]
348+
> ```
349+
>
350+
> PyGAD applies constraints sequentially, starting from the first gene to the last. To ensure correct behavior when genes depend on each other, structure your GA problem so that if gene X depends on gene Y, then gene Y appears earlier in the chromosome (solution) than gene X. As a result, its gene constraint will be earlier in the list.
343351

344352
## Full Example
345353

docs/md/releases.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,7 @@ Release Date 07 January 2025
566566
6. Created a new method called `unique_float_gene_from_range()` inside the `pygad.helper.unique.Unique` class to find a unique floating-point number from a range.
567567
7. Fix a bug in the `pygad.helper.unique.Unique.unique_gene_by_space()` method to return the numeric value only instead of a NumPy array.
568568
8. Refactoring the `pygad/helper/unique.py` script to remove duplicate codes and reformatting the docstrings.
569-
9. The plot_pareto_front_curve() method added to the pygad.visualize.plot.Plot class to visualize the Pareto front for multi-objective problems. It only supports 2 objectives. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/279
569+
9. The `plot_pareto_front_curve()` method added to the pygad.visualize.plot.Plot class to visualize the Pareto front for multi-objective problems. It only supports 2 objectives. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/279
570570
11. Fix a bug converting a nested NumPy array to a nested list. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/300
571571
12. The `Matplotlib` library is only imported when a method inside the `pygad/visualize/plot.py` script is used. This is more efficient than using `import matplotlib.pyplot` at the module level as this causes it to be imported when `pygad` is imported even when it is not needed. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/292
572572
13. Fix a bug when minus sign (-) is used inside the `stop_criteria` parameter (e.g. `stop_criteria=["saturate_10", "reach_-0.5"]`). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/296
@@ -575,9 +575,9 @@ Release Date 07 January 2025
575575

576576
## PyGAD 3.5.0
577577

578-
Release Date 07 July 2025
578+
Release Date 08 July 2025
579579

580-
1. Fix a bug when minus sign (-) is used inside the `stop_criteria` parameter for multi-objective problems. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/314
580+
1. Fix a bug when minus sign (-) is used inside the `stop_criteria` parameter for multi-objective problems. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/314 https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/323
581581
2. Fix a bug when the `stop_criteria` parameter is passed as an iterable (e.g. list) for multi-objective problems (e.g. `['reach_50_60', 'reach_20, 40']`). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/314
582582
3. Call the `get_matplotlib()` function from the `plot_genes()` method inside the `pygad.visualize.plot.Plot` class to import the matplotlib library. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/315
583583
4. Create a new helper method called `select_unique_value()` inside the `pygad/helper/unique.py` script to select a unique gene from an array of values.
@@ -598,12 +598,14 @@ Release Date 07 July 2025
598598
11. `filter_gene_values_by_constraint()`: Receives a list of values for a gene. Then it filters such values using the gene constraint.
599599
12. `get_valid_gene_constraint_values()`: Selects one valid gene value that satisfy the gene constraint. It simply calls `generate_gene_value()` to generate some gene values then it filters such values using `filter_gene_values_by_constraint()`.
600600
9. Create a new helper method called `mutation_process_random_value()` inside the `pygad/utils/mutation.py` script that generates constrained random values for mutation. It calls either `generate_gene_value()` or `get_valid_gene_constraint_values()` based on whether the `gene_constraint` parameter is used or not.
601-
10. A new parameter called `gene_constraint` is added. It accepts a list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/pygad_more.html#gene-constraint) section for more information.
601+
10. A new parameter called `gene_constraint` is added. It accepts a list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/pygad_more.html#gene-constraint) section for more information. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/119
602602
11. A new parameter called `sample_size` is added. To select a gene value that respects a constraint, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. An instance attribute of the same name is created in the instances of the `pygad.GA` class. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information.
603603
12. Use the `sample_size` parameter instead of `num_trials` in the methods `solve_duplicate_genes_randomly()` and `unique_float_gene_from_range()` inside the `pygad/helper/unique.py` script. It is the maximum number of values to generate as the search space when looking for a unique float value out of a range.
604604
13. Fixed a bug in population initialization when `allow_duplicate_genes=False`. Previously, gene values were checked for duplicates before rounding, which could allow near-duplicates like 7.61 and 7.62 to pass. After rounding (e.g., both becoming 7.6), this resulted in unintended duplicates. The fix ensures gene values are now rounded before duplicate checks, preventing such cases.
605605
14. More tests are created.
606606
15. More examples are created.
607+
16. Edited the `sort_solutions_nsga2()` method in the `pygad/utils/nsga2.py` script to accept an optional parameter called `find_best_solution` when calling this method just to find the best solution.
608+
17. Fixed a bug while applying the non-dominated sorting in the `get_non_dominated_set()` method inside the `pygad/utils/nsga2.py` script. It was swapping the non-dominated and dominated sets. In other words, it used the non-dominated set as if it is the dominated set and vice versa. All the calls to this method were edited accordingly. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/320.
607609

608610
# PyGAD Projects at GitHub
609611

docs/md/utils.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ It has the following helper methods:
190190
The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II. The methods inside this class are:
191191

192192
1. `non_dominated_sorting()`: Returns all the pareto fronts by applying non-dominated sorting over the solutions.
193-
2. `get_non_dominated_set()`: Returns the set of non-dominated solutions from the passed solutions.
193+
2. `get_non_dominated_set()`: Returns the 2 sets of non-dominated solutions and dominated solutions from the passed solutions. Note that the Pareto front consists of the solutions in the non-dominated set.
194194
3. `crowding_distance()`: Calculates the crowding distance for all solutions in the current pareto front.
195195
4. `sort_solutions_nsga2()`: Sort the solutions. If the problem is single-objective, then the solutions are sorted by sorting the fitness values of the population. If it is multi-objective, then non-dominated sorting and crowding distance are applied to sort the solutions.
196196

example_multi_objective.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import pygad
2+
import numpy
3+
4+
"""
5+
Given these 2 functions:
6+
y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6
7+
y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12
8+
where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50
9+
and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30
10+
What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions.
11+
This is a multi-objective optimization problem.
12+
13+
PyGAD considers the problem as multi-objective if the fitness function returns:
14+
1) List.
15+
2) Or tuple.
16+
3) Or numpy.ndarray.
17+
"""
18+
19+
function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs.
20+
function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs.
21+
desired_output1 = 50 # Function 1 output.
22+
desired_output2 = 30 # Function 2 output.
23+
24+
def fitness_func(ga_instance, solution, solution_idx):
25+
output1 = numpy.sum(solution*function_inputs1)
26+
output2 = numpy.sum(solution*function_inputs2)
27+
fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001)
28+
fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001)
29+
return [fitness1, fitness2]
30+
31+
num_generations = 1 # Number of generations.
32+
num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool.
33+
34+
sol_per_pop = 10 # Number of solutions in the population.
35+
num_genes = len(function_inputs1)
36+
37+
ga_instance = pygad.GA(num_generations=num_generations,
38+
num_parents_mating=num_parents_mating,
39+
sol_per_pop=sol_per_pop,
40+
num_genes=num_genes,
41+
fitness_func=fitness_func,
42+
random_seed=3,
43+
parent_selection_type='tournament_nsga2')
44+
45+
# Running the GA to optimize the parameters of the function.
46+
ga_instance.run()
47+
48+
"""
49+
ga_instance.plot_fitness(label=['Obj 1', 'Obj 2'])
50+
ga_instance.plot_pareto_front_curve()
51+
52+
# Returning the details of the best solution.
53+
solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness)
54+
print(f"Parameters of the best solution : {solution}")
55+
print(f"Fitness value of the best solution = {solution_fitness}")
56+
print(f"Index of the best solution : {solution_idx}")
57+
58+
prediction = numpy.sum(numpy.array(function_inputs1)*solution)
59+
print(f"Predicted output 1 based on the best solution : {prediction}")
60+
prediction = numpy.sum(numpy.array(function_inputs2)*solution)
61+
print(f"Predicted output 2 based on the best solution : {prediction}")
62+
63+
if ga_instance.best_solution_generation != -1:
64+
print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.")
65+
"""

pygad/utils/nsga2.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
class NSGA2:
55

6-
def __init__():
6+
def __init__(self):
77
pass
88

99
def get_non_dominated_set(self, curr_solutions):
@@ -26,10 +26,11 @@ def get_non_dominated_set(self, curr_solutions):
2626
# List of the members of the current dominated pareto front/set.
2727
dominated_set = []
2828
# List of the non-members of the current dominated pareto front/set.
29+
# The non-dominated set is the pareto front set.
2930
non_dominated_set = []
3031
for idx1, sol1 in enumerate(curr_solutions):
3132
# Flag indicates whether the solution is a member of the current dominated set.
32-
is_dominated = True
33+
is_not_dominated = True
3334
for idx2, sol2 in enumerate(curr_solutions):
3435
if idx1 == idx2:
3536
continue
@@ -45,28 +46,30 @@ def get_non_dominated_set(self, curr_solutions):
4546
gr_eq = two_solutions[:, 1] >= two_solutions[:, 0]
4647
gr = two_solutions[:, 1] > two_solutions[:, 0]
4748

48-
# If the 2 conditions hold, then a solution dominates the current solution.
49-
# The current solution is not considered a member of the dominated set.
49+
# If the 2 conditions hold, then a solution (sol2) dominates the current solution (sol1).
50+
# The current solution (sol1) is not considered a member of the non-dominated set.
5051
if gr_eq.all() and gr.any():
51-
# Set the is_dominated flag to False to NOT insert the current solution in the current dominated set.
52-
# Instead, insert it into the non-dominated set.
53-
is_dominated = False
54-
non_dominated_set.append(sol1)
52+
# Set the is_not_dominated flag to False because another solution dominates the current solution (sol1)
53+
is_not_dominated = False
54+
# DO NOT insert the current solution in the current non-dominated set.
55+
# Instead, insert it into the dominated set.
56+
dominated_set.append(sol1)
5557
break
5658
else:
5759
# Reaching here means the solution does not dominate the current solution.
5860
pass
5961

6062
# If the flag is True, then no solution dominates the current solution.
61-
if is_dominated:
62-
dominated_set.append(sol1)
63+
# Insert the current solution (sol1) into the non-dominated set.
64+
if is_not_dominated:
65+
non_dominated_set.append(sol1)
6366

6467
# Return the dominated and non-dominated sets.
6568
return dominated_set, non_dominated_set
66-
69+
6770
def non_dominated_sorting(self, fitness):
6871
"""
69-
Apply non-dominant sorting over the fitness to create the pareto fronts based on non-dominaned sorting of the solutions.
72+
Apply non-dominant sorting over the fitness to create the pareto fronts based on non-dominated sorting of the solutions.
7073
7174
Parameters
7275
----------
@@ -101,7 +104,6 @@ def non_dominated_sorting(self, fitness):
101104
# Each element has:
102105
# 1) The index of the solution.
103106
# 2) An array of the fitness values of this solution across all objectives.
104-
# remaining_set = numpy.array(list(zip(range(0, fitness.shape[0]), non_dominated_set)))
105107
remaining_set = list(zip(range(0, fitness.shape[0]), remaining_set))
106108

107109
# A list mapping the index of each pareto front to the set of solutions in this front.
@@ -112,15 +114,15 @@ def non_dominated_sorting(self, fitness):
112114
front_index = -1
113115
while len(remaining_set) > 0:
114116
front_index += 1
115-
117+
116118
# Get the current non-dominated set of solutions.
117-
pareto_front, remaining_set = self.get_non_dominated_set(curr_solutions=remaining_set)
119+
remaining_set, pareto_front = self.get_non_dominated_set(curr_solutions=remaining_set)
118120
pareto_front = numpy.array(pareto_front, dtype=object)
119121
pareto_fronts.append(pareto_front)
120122

121123
solutions_indices = pareto_front[:, 0].astype(int)
122124
solutions_fronts_indices[solutions_indices] = front_index
123-
125+
124126
return pareto_fronts, solutions_fronts_indices
125127

126128
def crowding_distance(self, pareto_front, fitness):
@@ -200,7 +202,7 @@ def crowding_distance(self, pareto_front, fitness):
200202
# Insert the crowding distance back into the list to override the initial zero.
201203
obj_sorted[idx][2] = crowding_dist
202204

203-
# Sort the objective by the original index at index 0 of the each child list.
205+
# Sort the objective by the original index at index 0 of each child list.
204206
obj_sorted = sorted(obj_sorted, key=lambda x: x[0])
205207
obj_crowding_dist_list.append(obj_sorted)
206208

@@ -225,8 +227,10 @@ def crowding_distance(self, pareto_front, fitness):
225227
crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices.astype(int)
226228

227229
return obj_crowding_dist_list, crowding_dist_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices
228-
229-
def sort_solutions_nsga2(self, fitness):
230+
231+
def sort_solutions_nsga2(self,
232+
fitness,
233+
find_best_solution=False):
230234
"""
231235
Sort the solutions based on the fitness.
232236
The sorting procedure differs based on whether the problem is single-objective or multi-objective optimization.
@@ -237,9 +241,9 @@ def sort_solutions_nsga2(self, fitness):
237241
238242
Parameters
239243
----------
240-
fitness : TYPE
241-
The fitness of the entire population.
242-
244+
fitness: The fitness of the entire population.
245+
find_best_solution: Whether the method is called only to find the best solution or as part of the PyGAD lifecycle. This is to decide whether the pareto_fronts instance attribute is edited or not.
246+
243247
Returns
244248
-------
245249
solutions_sorted : TYPE
@@ -251,7 +255,13 @@ def sort_solutions_nsga2(self, fitness):
251255
solutions_sorted = []
252256
# Split the solutions into pareto fronts using non-dominated sorting.
253257
pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness)
254-
self.pareto_fronts = pareto_fronts.copy()
258+
if find_best_solution:
259+
# Do not edit the pareto_fronts instance attribute when just getting the best solution.
260+
pass
261+
else:
262+
# The method is called within the regular GA lifecycle.
263+
# We have to edit the pareto_fronts to be assigned the latest pareto front.
264+
self.pareto_fronts = pareto_fronts.copy()
255265
for pareto_front in pareto_fronts:
256266
# Sort the solutions in the front using crowded distance.
257267
_, _, _, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=pareto_front.copy(),
@@ -264,5 +274,7 @@ def sort_solutions_nsga2(self, fitness):
264274
solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k])
265275
# Reverse the sorted solutions so that the best solution comes first.
266276
solutions_sorted.reverse()
267-
277+
else:
278+
raise TypeError(f'Each element in the fitness array must be of a number of an iterable (list, tuple, numpy.ndarray). But the type {type(fitness[0])} found')
279+
268280
return solutions_sorted

0 commit comments

Comments
 (0)