diff --git a/pygad/pygad.py b/pygad/pygad.py index bedca73..436237b 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -1211,7 +1211,7 @@ def __init__(self, self.valid_parameters = False raise ValueError(f"In the 'stop_criteria' parameter, the supported stop words are {self.supported_stop_words} but '{stop_word}' found.") - if number.replace(".", "").isnumeric(): + if number.replace(".", "").replace("-", "").isnumeric(): number = float(number) else: self.valid_parameters = False @@ -1306,6 +1306,8 @@ def __init__(self, # A list holding the offspring after applying mutation in the last generation. self.last_generation_offspring_mutation = None # Holds the fitness values of one generation before the fitness values saved in the last_generation_fitness attribute. Added in PyGAD 2.16.2. + # They are used inside the cal_pop_fitness() method to fetch the fitness of the parents in one generation before the latest generation. + # This is to avoid re-calculating the fitness for such parents again. self.previous_generation_fitness = None # Added in PyGAD 2.18.0. A NumPy array holding the elitism of the current generation according to the value passed in the 'keep_elitism' parameter. It works only if the 'keep_elitism' parameter has a non-zero value. self.last_generation_elitism = None @@ -1640,14 +1642,12 @@ def cal_pop_fitness(self): # 'last_generation_parents_as_list' is the list version of 'self.last_generation_parents' # It is used to return the parent index using the 'in' membership operator of Python lists. This is much faster than using 'numpy.where()'. if self.last_generation_parents is not None: - last_generation_parents_as_list = [ - list(gen_parent) for gen_parent in self.last_generation_parents] + last_generation_parents_as_list = self.last_generation_parents.tolist() # 'last_generation_elitism_as_list' is the list version of 'self.last_generation_elitism' # It is used to return the elitism index using the 'in' membership operator of Python lists. This is much faster than using 'numpy.where()'. if self.last_generation_elitism is not None: - last_generation_elitism_as_list = [ - list(gen_elitism) for gen_elitism in self.last_generation_elitism] + last_generation_elitism_as_list = self.last_generation_elitism.tolist() pop_fitness = ["undefined"] * len(self.population) if self.parallel_processing is None: @@ -1659,6 +1659,12 @@ def cal_pop_fitness(self): # Make sure that both the solution and 'self.solutions' are of type 'list' not 'numpy.ndarray'. # if (self.save_solutions) and (len(self.solutions) > 0) and (numpy.any(numpy.all(self.solutions == numpy.array(sol), axis=1))) # if (self.save_solutions) and (len(self.solutions) > 0) and (numpy.any(numpy.all(numpy.equal(self.solutions, numpy.array(sol)), axis=1))) + + # Make sure self.best_solutions is a list of lists before proceeding. + # Because the second condition expects that best_solutions is a list of lists. + if type(self.best_solutions) is numpy.ndarray: + self.best_solutions = self.best_solutions.tolist() + if (self.save_solutions) and (len(self.solutions) > 0) and (list(sol) in self.solutions): solution_idx = self.solutions.index(list(sol)) fitness = self.solutions_fitness[solution_idx] @@ -1867,13 +1873,13 @@ def run(self): # self.best_solutions: Holds the best solution in each generation. if type(self.best_solutions) is numpy.ndarray: - self.best_solutions = list(self.best_solutions) + self.best_solutions = self.best_solutions.tolist() # self.best_solutions_fitness: A list holding the fitness value of the best solution for each generation. if type(self.best_solutions_fitness) is numpy.ndarray: self.best_solutions_fitness = list(self.best_solutions_fitness) # self.solutions: Holds the solutions in each generation. if type(self.solutions) is numpy.ndarray: - self.solutions = list(self.solutions) + self.solutions = self.solutions.tolist() # self.solutions_fitness: Holds the fitness of the solutions in each generation. if type(self.solutions_fitness) is numpy.ndarray: self.solutions_fitness = list(self.solutions_fitness) @@ -1913,34 +1919,8 @@ def run(self): self.best_solutions.append(list(best_solution)) for generation in range(generation_first_idx, generation_last_idx): - if not (self.on_fitness is None): - on_fitness_output = self.on_fitness(self, - self.last_generation_fitness) - if on_fitness_output is None: - pass - else: - if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]: - on_fitness_output = numpy.array(on_fitness_output) - if on_fitness_output.shape == self.last_generation_fitness.shape: - self.last_generation_fitness = on_fitness_output - else: - raise ValueError(f"Size mismatch between the output of on_fitness() {on_fitness_output.shape} and the expected fitness output {self.last_generation_fitness.shape}.") - else: - raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.") - - # Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute. - self.best_solutions_fitness.append(best_solution_fitness) - - # Appending the solutions in the current generation to the solutions list. - if self.save_solutions: - # self.solutions.extend(self.population.copy()) - population_as_list = self.population.copy() - population_as_list = [list(item) - for item in population_as_list] - self.solutions.extend(population_as_list) - - self.solutions_fitness.extend(self.last_generation_fitness) + self.run_loop_head(best_solution_fitness) # Call the 'run_select_parents()' method to select the parents. # It edits these 2 instance attributes: @@ -1964,7 +1944,6 @@ def run(self): # 1) population: A NumPy array of the population of solutions/chromosomes. self.run_update_population() - # The generations_completed attribute holds the number of the last completed generation. self.generations_completed = generation + 1 @@ -2078,6 +2057,9 @@ def run(self): # Converting the 'best_solutions' list into a NumPy array. self.best_solutions = numpy.array(self.best_solutions) + # Update previous_generation_fitness because it is used to get the fitness of the parents. + self.previous_generation_fitness = self.last_generation_fitness.copy() + # Converting the 'solutions' list into a NumPy array. # self.solutions = numpy.array(self.solutions) except Exception as ex: @@ -2085,6 +2067,35 @@ def run(self): # sys.exit(-1) raise ex + def run_loop_head(self, best_solution_fitness): + if not (self.on_fitness is None): + on_fitness_output = self.on_fitness(self, + self.last_generation_fitness) + + if on_fitness_output is None: + pass + else: + if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]: + on_fitness_output = numpy.array(on_fitness_output) + if on_fitness_output.shape == self.last_generation_fitness.shape: + self.last_generation_fitness = on_fitness_output + else: + raise ValueError(f"Size mismatch between the output of on_fitness() {on_fitness_output.shape} and the expected fitness output {self.last_generation_fitness.shape}.") + else: + raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.") + + # Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute. + self.best_solutions_fitness.append(best_solution_fitness) + + # Appending the solutions in the current generation to the solutions list. + if self.save_solutions: + # self.solutions.extend(self.population.copy()) + population_as_list = self.population.copy() + population_as_list = [list(item) for item in population_as_list] + self.solutions.extend(population_as_list) + + self.solutions_fitness.extend(self.last_generation_fitness) + def run_select_parents(self, call_on_parents=True): """ This method must be only callled from inside the run() method. It is not meant for use by the user. @@ -2333,6 +2344,7 @@ def best_solution(self, pop_fitness=None): -best_solution_fitness: Fitness value of the best solution. -best_match_idx: Index of the best solution in the current population. """ + try: if pop_fitness is None: # If the 'pop_fitness' parameter is not passed, then we have to call the 'cal_pop_fitness()' method to calculate the fitness of all solutions in the lastest population. diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py index 3ddeaff..3265de3 100644 --- a/pygad/visualize/plot.py +++ b/pygad/visualize/plot.py @@ -3,9 +3,18 @@ """ import numpy -import matplotlib.pyplot +# import matplotlib.pyplot import pygad +def get_matplotlib(): + # Importing matplotlib.pyplot at the module scope causes performance issues. + # This causes matplotlib.pyplot to be imported once pygad is imported. + # An efficient approach is to import matplotlib.pyplot only when needed. + # Inside each function, call get_matplotlib() to return the library object. + # If a function called get_matplotlib() once, then the library object is reused. + import matplotlib.pyplot as matplt + return matplt + class Plot: def __init__(): @@ -43,7 +52,9 @@ def plot_fitness(self, self.logger.error("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") raise RuntimeError("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") - fig = matplotlib.pyplot.figure() + matplt = get_matplotlib() + + fig = matplt.figure() if type(self.best_solutions_fitness[0]) in [list, tuple, numpy.ndarray] and len(self.best_solutions_fitness[0]) > 1: # Multi-objective optimization problem. if type(linewidth) in pygad.GA.supported_int_float_types: @@ -70,18 +81,18 @@ def plot_fitness(self, # Return the fitness values for the current objective function across all best solutions acorss all generations. fitness = numpy.array(self.best_solutions_fitness)[:, objective_idx] if plot_type == "plot": - matplotlib.pyplot.plot(fitness, + matplt.plot(fitness, linewidth=current_linewidth, color=current_color, label=current_label) elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(len(fitness)), + matplt.scatter(range(len(fitness)), fitness, linewidth=current_linewidth, color=current_color, label=current_label) elif plot_type == "bar": - matplotlib.pyplot.bar(range(len(fitness)), + matplt.bar(range(len(fitness)), fitness, linewidth=current_linewidth, color=current_color, @@ -89,29 +100,29 @@ def plot_fitness(self, else: # Single-objective optimization problem. if plot_type == "plot": - matplotlib.pyplot.plot(self.best_solutions_fitness, + matplt.plot(self.best_solutions_fitness, linewidth=linewidth, color=color) elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(len(self.best_solutions_fitness)), + matplt.scatter(range(len(self.best_solutions_fitness)), self.best_solutions_fitness, linewidth=linewidth, color=color) elif plot_type == "bar": - matplotlib.pyplot.bar(range(len(self.best_solutions_fitness)), + matplt.bar(range(len(self.best_solutions_fitness)), self.best_solutions_fitness, linewidth=linewidth, color=color) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) # Create a legend out of the labels. - matplotlib.pyplot.legend() + matplt.legend() if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig @@ -166,21 +177,23 @@ def plot_new_solution_rate(self, generation_num_unique_solutions = len_after - len_before num_unique_solutions_per_generation.append(generation_num_unique_solutions) - fig = matplotlib.pyplot.figure() + matplt = get_matplotlib() + + fig = matplt.figure() if plot_type == "plot": - matplotlib.pyplot.plot(num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.plot(num_unique_solutions_per_generation, linewidth=linewidth, color=color) elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.scatter(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) elif plot_type == "bar": - matplotlib.pyplot.bar(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) + matplt.bar(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig @@ -251,7 +264,7 @@ def plot_genes(self, if num_cols == 0: figsize = (10, 8) # There is only a single gene - fig, ax = matplotlib.pyplot.subplots(num_rows, figsize=figsize) + fig, ax = matplt.subplots(num_rows, figsize=figsize) if plot_type == "plot": ax.plot(solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) elif plot_type == "scatter": @@ -260,7 +273,7 @@ def plot_genes(self, ax.bar(range(self.generations_completed + 1), solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) ax.set_xlabel(0, fontsize=font_size) else: - fig, axs = matplotlib.pyplot.subplots(num_rows, num_cols) + fig, axs = matplt.subplots(num_rows, num_cols) if num_cols == 1 and num_rows == 1: fig.set_figwidth(5 * num_cols) @@ -297,10 +310,10 @@ def plot_genes(self, gene_idx += 1 fig.suptitle(title, fontsize=font_size, y=1.001) - matplotlib.pyplot.tight_layout() + matplt.tight_layout() elif graph_type == "boxplot": - fig = matplotlib.pyplot.figure(1, figsize=(0.7*self.num_genes, 6)) + fig = matplt.figure(1, figsize=(0.7*self.num_genes, 6)) # Create an axes instance ax = fig.add_subplot(111) @@ -323,10 +336,10 @@ def plot_genes(self, for cap in boxeplots['caps']: cap.set(color=color, linewidth=linewidth) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - matplotlib.pyplot.tight_layout() + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + matplt.tight_layout() elif graph_type == "histogram": # num_rows will always be >= 1 @@ -337,12 +350,12 @@ def plot_genes(self, if num_cols == 0: figsize = (10, 8) # There is only a single gene - fig, ax = matplotlib.pyplot.subplots(num_rows, + fig, ax = matplt.subplots(num_rows, figsize=figsize) ax.hist(solutions_to_plot[:, 0], color=fill_color) ax.set_xlabel(0, fontsize=font_size) else: - fig, axs = matplotlib.pyplot.subplots(num_rows, num_cols) + fig, axs = matplt.subplots(num_rows, num_cols) if num_cols == 1 and num_rows == 1: fig.set_figwidth(4 * num_cols) @@ -375,13 +388,13 @@ def plot_genes(self, gene_idx += 1 fig.suptitle(title, fontsize=font_size, y=1.001) - matplotlib.pyplot.tight_layout() + matplt.tight_layout() if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig @@ -449,12 +462,14 @@ def plot_pareto_front_curve(self, # Sort the Pareto front solutions (optional but can make the plot cleaner) sorted_pareto_front = sorted(zip(pareto_front_x, pareto_front_y)) + matplt = get_matplotlib() + # Plotting - fig = matplotlib.pyplot.figure() + fig = matplt.figure() # First, plot the scatter of all points (population) all_points_x = [self.last_generation_fitness[i][0] for i in range(self.sol_per_pop)] all_points_y = [self.last_generation_fitness[i][1] for i in range(self.sol_per_pop)] - matplotlib.pyplot.scatter(all_points_x, + matplt.scatter(all_points_x, all_points_y, marker=marker, color=color_fitness, @@ -463,7 +478,7 @@ def plot_pareto_front_curve(self, # Then, plot the Pareto front as a curve pareto_front_x_sorted, pareto_front_y_sorted = zip(*sorted_pareto_front) - matplotlib.pyplot.plot(pareto_front_x_sorted, + matplt.plot(pareto_front_x_sorted, pareto_front_y_sorted, marker=marker, label=label, @@ -471,17 +486,17 @@ def plot_pareto_front_curve(self, color=color, linewidth=linewidth) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - matplotlib.pyplot.legend() + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + matplt.legend() - matplotlib.pyplot.grid(grid) + matplt.grid(grid) if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig