Skip to content

GitHub actions #333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/md/pygad_more.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,17 @@ Out of the range of *1000* numbers, all the 100 values might not be satisfying t

> PyGAD does not yet handle the **dependencies** among the genes in the `gene_constraint` parameter.
>
> 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.
> 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.
>
> 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.
> ```python
> gene_constraint=
> [
> lambda solution,values: [val for val in values if val<solution[1]],
> lambda solution,values: [val for val in values if val>10]
> ]
> ```
>
> 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.

## Full Example

Expand Down
10 changes: 6 additions & 4 deletions docs/md/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ Release Date 07 January 2025
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.
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.
8. Refactoring the `pygad/helper/unique.py` script to remove duplicate codes and reformatting the docstrings.
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
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
11. Fix a bug converting a nested NumPy array to a nested list. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/300
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
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
Expand All @@ -575,9 +575,9 @@ Release Date 07 January 2025

## PyGAD 3.5.0

Release Date 07 July 2025
Release Date 08 July 2025

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
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
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
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
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.
Expand All @@ -598,12 +598,14 @@ Release Date 07 July 2025
11. `filter_gene_values_by_constraint()`: Receives a list of values for a gene. Then it filters such values using the gene constraint.
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()`.
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.
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.
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
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.
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.
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.
14. More tests are created.
15. More examples are created.
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.
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.

# PyGAD Projects at GitHub

Expand Down
2 changes: 1 addition & 1 deletion docs/md/utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ It has the following helper methods:
The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II. The methods inside this class are:

1. `non_dominated_sorting()`: Returns all the pareto fronts by applying non-dominated sorting over the solutions.
2. `get_non_dominated_set()`: Returns the set of non-dominated solutions from the passed solutions.
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.
3. `crowding_distance()`: Calculates the crowding distance for all solutions in the current pareto front.
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.

Expand Down
65 changes: 65 additions & 0 deletions example_multi_objective.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pygad
import numpy

"""
Given these 2 functions:
y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6
y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12
where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50
and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30
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.
This is a multi-objective optimization problem.

PyGAD considers the problem as multi-objective if the fitness function returns:
1) List.
2) Or tuple.
3) Or numpy.ndarray.
"""

function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs.
function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs.
desired_output1 = 50 # Function 1 output.
desired_output2 = 30 # Function 2 output.

def fitness_func(ga_instance, solution, solution_idx):
output1 = numpy.sum(solution*function_inputs1)
output2 = numpy.sum(solution*function_inputs2)
fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001)
fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001)
return [fitness1, fitness2]

num_generations = 1 # Number of generations.
num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool.

sol_per_pop = 10 # Number of solutions in the population.
num_genes = len(function_inputs1)

ga_instance = pygad.GA(num_generations=num_generations,
num_parents_mating=num_parents_mating,
sol_per_pop=sol_per_pop,
num_genes=num_genes,
fitness_func=fitness_func,
random_seed=3,
parent_selection_type='tournament_nsga2')

# Running the GA to optimize the parameters of the function.
ga_instance.run()

"""
ga_instance.plot_fitness(label=['Obj 1', 'Obj 2'])
ga_instance.plot_pareto_front_curve()

# Returning the details of the best solution.
solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness)
print(f"Parameters of the best solution : {solution}")
print(f"Fitness value of the best solution = {solution_fitness}")
print(f"Index of the best solution : {solution_idx}")

prediction = numpy.sum(numpy.array(function_inputs1)*solution)
print(f"Predicted output 1 based on the best solution : {prediction}")
prediction = numpy.sum(numpy.array(function_inputs2)*solution)
print(f"Predicted output 2 based on the best solution : {prediction}")

if ga_instance.best_solution_generation != -1:
print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.")
"""
60 changes: 36 additions & 24 deletions pygad/utils/nsga2.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class NSGA2:

def __init__():
def __init__(self):
pass

def get_non_dominated_set(self, curr_solutions):
Expand All @@ -26,10 +26,11 @@ def get_non_dominated_set(self, curr_solutions):
# 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.
# The non-dominated set is the 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
is_not_dominated = True
for idx2, sol2 in enumerate(curr_solutions):
if idx1 == idx2:
continue
Expand All @@ -45,28 +46,30 @@ def get_non_dominated_set(self, curr_solutions):
gr_eq = two_solutions[:, 1] >= two_solutions[:, 0]
gr = 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 the 2 conditions hold, then a solution (sol2) dominates the current solution (sol1).
# The current solution (sol1) is not considered a member of the non-dominated set.
if gr_eq.all() and gr.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)
# Set the is_not_dominated flag to False because another solution dominates the current solution (sol1)
is_not_dominated = False
# DO NOT insert the current solution in the current non-dominated set.
# Instead, insert it into the dominated set.
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)
# Insert the current solution (sol1) into the non-dominated set.
if is_not_dominated:
non_dominated_set.append(sol1)

# Return the dominated and non-dominated sets.
return dominated_set, non_dominated_set

def non_dominated_sorting(self, fitness):
"""
Apply non-dominant sorting over the fitness to create the pareto fronts based on non-dominaned sorting of the solutions.
Apply non-dominant sorting over the fitness to create the pareto fronts based on non-dominated sorting of the solutions.

Parameters
----------
Expand Down Expand Up @@ -101,7 +104,6 @@ def non_dominated_sorting(self, fitness):
# 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.
Expand All @@ -112,15 +114,15 @@ def non_dominated_sorting(self, fitness):
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)
remaining_set, pareto_front = 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):
Expand Down Expand Up @@ -200,7 +202,7 @@ def crowding_distance(self, pareto_front, fitness):
# 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.
# Sort the objective by the original index at index 0 of each child list.
obj_sorted = sorted(obj_sorted, key=lambda x: x[0])
obj_crowding_dist_list.append(obj_sorted)

Expand All @@ -225,8 +227,10 @@ def crowding_distance(self, pareto_front, fitness):
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):

def sort_solutions_nsga2(self,
fitness,
find_best_solution=False):
"""
Sort the solutions based on the fitness.
The sorting procedure differs based on whether the problem is single-objective or multi-objective optimization.
Expand All @@ -237,9 +241,9 @@ def sort_solutions_nsga2(self, fitness):

Parameters
----------
fitness : TYPE
The fitness of the entire population.
fitness: The fitness of the entire population.
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.

Returns
-------
solutions_sorted : TYPE
Expand All @@ -251,7 +255,13 @@ def sort_solutions_nsga2(self, fitness):
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()
if find_best_solution:
# Do not edit the pareto_fronts instance attribute when just getting the best solution.
pass
else:
# The method is called within the regular GA lifecycle.
# We have to edit the pareto_fronts to be assigned the latest pareto front.
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(),
Expand All @@ -264,5 +274,7 @@ def sort_solutions_nsga2(self, fitness):
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()

else:
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')

return solutions_sorted
Loading