From 19250a94789d5a2d28d582cdb4b8078946b0604c Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 8 Jul 2025 22:13:58 -0400 Subject: [PATCH] Getting PyGAD 3.5.0 Ready --- docs/md/releases.md | 1 + docs/source/pygad_more.rst | 15 +++++++-- docs/source/releases.rst | 23 ++++++++++++-- example_multi_objective.py | 65 -------------------------------------- pygad/pygad.py | 16 ++++------ 5 files changed, 40 insertions(+), 80 deletions(-) delete mode 100644 example_multi_objective.py diff --git a/docs/md/releases.md b/docs/md/releases.md index 7a2c450..9fce35f 100644 --- a/docs/md/releases.md +++ b/docs/md/releases.md @@ -606,6 +606,7 @@ Release Date 08 July 2025 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. +18. Fix a bug retrieving in the `best_solution()` method when retrieving the best solution for multi-objective problems. https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/331 # PyGAD Projects at GitHub diff --git a/docs/source/pygad_more.rst b/docs/source/pygad_more.rst index 444f702..5334317 100644 --- a/docs/source/pygad_more.rst +++ b/docs/source/pygad_more.rst @@ -530,14 +530,23 @@ assigning a larger value for the ``sample_size`` parameter. 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. + + .. code:: python + + gene_constraint= + [ + lambda solution,values: [val for val in values if val10] + ] 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 ------------ diff --git a/docs/source/releases.rst b/docs/source/releases.rst index 254d458..8611130 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -1593,7 +1593,7 @@ Release Date 07 January 2025 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 +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 @@ -1630,11 +1630,12 @@ 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 + 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. @@ -1726,6 +1727,7 @@ Release Date 07 July 2025 called to ensure the candidate value is valid. Check the `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 @@ -1755,6 +1757,23 @@ Release Date 07 July 2025 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. + +18. Fix a bug retrieving in the ``best_solution()`` method when + retrieving the best solution for multi-objective problems. + https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/331 + PyGAD Projects at GitHub ======================== diff --git a/example_multi_objective.py b/example_multi_objective.py deleted file mode 100644 index b08db04..0000000 --- a/example_multi_objective.py +++ /dev/null @@ -1,65 +0,0 @@ -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.") -""" diff --git a/pygad/pygad.py b/pygad/pygad.py index 66e4b31..6eca219 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -2237,17 +2237,13 @@ def best_solution(self, pop_fitness=None): pop_fitness == numpy.max(pop_fitness))[0][0] elif pop_fitness_arr.ndim == 2: # Multi-objective optimization. - # Sort by all objectives in descending order. - # The first objective is the most important, then the second, etc. - sorted_indices = numpy.lexsort([ -pop_fitness_arr[:,i] for i in reversed(range(pop_fitness_arr.shape[1])) ]) - best_match_idx = sorted_indices[0] - maximum_fitness_value = pop_fitness_arr[best_match_idx] - - best_match_list = numpy.where( - pop_fitness == maximum_fitness_value) - - best_match_idx = best_match_list[0][0] # Get the first index of the best match. + # Use NSGA-2 to sort the solutions using the fitness. + # Set find_best_solution=True to avoid overriding the pareto_fronts instance attribute. + best_match_list = self.sort_solutions_nsga2(fitness=pop_fitness, + find_best_solution=True) + # Get the first index of the best match. + best_match_idx = best_match_list[0] best_solution = self.population[best_match_idx, :].copy() best_solution_fitness = pop_fitness[best_match_idx]