Skip to content
Open
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
Binary file added pygad/__init__.pyc
Binary file not shown.
Binary file added pygad/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added pygad/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file added pygad/__pycache__/pygad.cpython-313.pyc
Binary file not shown.
Binary file added pygad/__pycache__/pygad.cpython-39.pyc
Binary file not shown.
Binary file added pygad/helper/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added pygad/helper/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file added pygad/helper/__pycache__/misc.cpython-313.pyc
Binary file not shown.
Binary file added pygad/helper/__pycache__/misc.cpython-39.pyc
Binary file not shown.
Binary file not shown.
Binary file added pygad/helper/__pycache__/unique.cpython-39.pyc
Binary file not shown.
619 changes: 479 additions & 140 deletions pygad/pygad.py

Large diffs are not rendered by default.

Binary file added pygad/utils/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/crossover.cpython-313.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/crossover.cpython-39.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/mutation.cpython-313.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/mutation.cpython-39.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/nsga2.cpython-313.pyc
Binary file not shown.
Binary file added pygad/utils/__pycache__/nsga2.cpython-39.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
34 changes: 28 additions & 6 deletions pygad/utils/nsga2.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ def non_dominated_sorting(self, fitness):
"""

# Verify that the problem is multi-objective optimization as non-dominated sorting is only applied to multi-objective problems.
# Handle dictionary format fitness values
if isinstance(fitness[0], dict):
# Extract the actual fitness values from the dictionary
fitness = [sol_fitness["fitness"] for sol_fitness in fitness]

if type(fitness[0]) in [list, tuple, numpy.ndarray]:
pass
elif type(fitness[0]) in self.supported_int_float_types:
Expand Down Expand Up @@ -128,14 +133,14 @@ def non_dominated_sorting(self, fitness):
def crowding_distance(self, pareto_front, fitness):
"""
Calculate the crowding distance for all solutions in the current pareto front.

Parameters
----------
pareto_front : TYPE
The set of solutions in the current pareto front.
fitness : TYPE
The fitness of the current population.

Returns
-------
obj_crowding_dist_list : TYPE
Expand All @@ -147,7 +152,11 @@ def crowding_distance(self, pareto_front, fitness):
crowding_dist_pop_sorted_indices : TYPE
The indices of the solutions (relative to the population) sorted by the crowding distance.
"""

# Handle dictionary format fitness values
if len(fitness) > 0 and isinstance(fitness[0], dict):
# Extract the actual fitness values from the dictionary
fitness = numpy.array([sol_fitness["fitness"] for sol_fitness in fitness])

# Each solution in the pareto front has 2 elements:
# 1) The index of the solution in the population.
# 2) A list of the fitness values for all objectives of the solution.
Expand Down Expand Up @@ -238,7 +247,7 @@ def sort_solutions_nsga2(self,
At first, non-dominated sorting is applied to classify the solutions into pareto fronts.
Then the solutions inside each front are sorted using crowded distance.
The solutions inside pareto front X always come before those in front X+1.

Parameters
----------
fitness: The fitness of the entire population.
Expand All @@ -248,8 +257,16 @@ def sort_solutions_nsga2(self,
-------
solutions_sorted : TYPE
The indices of the sorted solutions.

"""
# Save original fitness for later use in crowding_distance
original_fitness = fitness.copy()

# Handle dictionary format fitness values
if len(fitness) > 0 and isinstance(fitness[0], dict):
# Extract the actual fitness values from the dictionary
fitness = numpy.array([sol_fitness["fitness"] for sol_fitness in fitness])

if type(fitness[0]) in [list, tuple, numpy.ndarray]:
# Multi-objective optimization problem.
solutions_sorted = []
Expand All @@ -265,10 +282,15 @@ def sort_solutions_nsga2(self,
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(),
fitness=fitness)
fitness=original_fitness)
crowding_dist_pop_sorted_indices = list(crowding_dist_pop_sorted_indices)
# Append the sorted solutions into the list.
solutions_sorted.extend(crowding_dist_pop_sorted_indices)
elif len(fitness) > 0 and isinstance(fitness[0], dict):
# Single-objective optimization problem with dictionary fitness.
solutions_sorted = sorted(range(len(fitness)), key=lambda k: original_fitness[k]["fitness"])
# Reverse the sorted solutions so that the best solution comes first.
solutions_sorted.reverse()
elif type(fitness[0]) in pygad.GA.supported_int_float_types:
# Single-objective optimization problem.
solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k])
Expand Down
46 changes: 39 additions & 7 deletions pygad/utils/parent_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,15 @@ def steady_state_selection(self, fitness, num_parents):
-The indices of the selected solutions.
"""

# Return the indices of the sorted solutions (all solutions in the population).
# This function works with both single- and multi-objective optimization problems.
fitness_sorted = self.sort_solutions_nsga2(fitness=fitness)
# Handle dictionary format fitness
if type(fitness[0]) is dict:
# Extract fitness values from dictionaries
fitness_values = [sol["fitness"] for sol in fitness]
fitness_sorted = numpy.argsort(fitness_values)[::-1]
else:
# Return the indices of the sorted solutions (all solutions in the population).
# This function works with both single- and multi-objective optimization problems.
fitness_sorted = self.sort_solutions_nsga2(fitness=fitness)

# Selecting the best individuals in the current generation as parents for producing the offspring of the next generation.
if self.gene_type_single == True:
Expand All @@ -35,9 +41,21 @@ def steady_state_selection(self, fitness, num_parents):
parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object)

for parent_num in range(num_parents):
parents[parent_num, :] = self.population[fitness_sorted[parent_num], :].copy()

return parents, numpy.array(fitness_sorted[:num_parents])
# Handle both single index and array cases (for multi-objective)
idx = fitness_sorted[parent_num]
# Only handle array case if it's a multi-objective result containing both rank and index
if isinstance(idx, (numpy.ndarray, list)) and len(idx) >= 2:
idx = idx[0]
parents[parent_num, :] = self.population[idx, :].copy()

# Extract just the indices for the return value
fitness_sorted_indices = []
for x in fitness_sorted[:num_parents]:
if isinstance(x, (numpy.ndarray, list)) and len(x) >= 2:
fitness_sorted_indices.append(x[0])
else:
fitness_sorted_indices.append(x)
return parents, numpy.array(fitness_sorted_indices)

def rank_selection(self, fitness, num_parents):

Expand Down Expand Up @@ -73,6 +91,9 @@ def rank_selection(self, fitness, num_parents):
# The variable idx has the rank of solution but not its index in the population.
# Return the correct index of the solution.
mapped_idx = fitness_sorted[idx]
# Only handle array case if it's a multi-objective result containing both rank and index
if isinstance(mapped_idx, (numpy.ndarray, list)) and len(mapped_idx) >= 2:
mapped_idx = mapped_idx[0]
parents[parent_num, :] = self.population[mapped_idx, :].copy()
parents_indices.append(mapped_idx)
break
Expand Down Expand Up @@ -131,7 +152,18 @@ def tournament_selection(self, fitness, num_parents):
rand_indices = numpy.random.randint(low=0, high=len(fitness), size=self.K_tournament)

# Find the rank of the candidate solutions. The lower the rank, the better the solution.
rand_indices_rank = [fitness_sorted.index(rand_idx) for rand_idx in rand_indices]
rand_indices_rank = []
for rand_idx in rand_indices:
# Handle both single index and array cases
for i, sorted_val in enumerate(fitness_sorted):
if isinstance(sorted_val, (numpy.ndarray, list)) and len(sorted_val) >= 2:
if sorted_val[0] == rand_idx:
rand_indices_rank.append(i)
break
else:
if sorted_val == rand_idx:
rand_indices_rank.append(i)
break
# Select the solution with the lowest rank as a parent.
selected_parent_idx = rand_indices_rank.index(min(rand_indices_rank))

Expand Down
Binary file added pygad/visualize/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added pygad/visualize/__pycache__/__init__.cpython-39.pyc
Binary file not shown.
Binary file not shown.
Binary file added pygad/visualize/__pycache__/plot.cpython-39.pyc
Binary file not shown.
61 changes: 61 additions & 0 deletions test_multi_objective.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
import pygad
import numpy

# 定义适应度函数
def fitness_func(ga_instance, solution, solution_idx):
# 简单的测试适应度函数:求和
fitness = numpy.sum(solution)
return fitness

# 准备基因空间
gene_space = {'low': 0, 'high': 10}

# 创建GA实例
try:
# 先检查fitness_func参数数量
print("fitness_func.__code__.co_argcount =", fitness_func.__code__.co_argcount)

ga_instance = pygad.GA(
num_generations=30,
num_parents_mating=5,
fitness_func=fitness_func,
sol_per_pop=20,
num_genes=5,
gene_space=gene_space,
parent_selection_type="sss",
crossover_type="single_point",
mutation_type="random",
mutation_percent_genes=10
)
print("GA instance created successfully. valid_parameters =", ga_instance.valid_parameters)
print("generations_completed =", ga_instance.generations_completed)
print("run_completed =", ga_instance.run_completed)

# 检查更多属性
print("num_generations =", ga_instance.num_generations)
print("num_parents_mating =", ga_instance.num_parents_mating)
print("sol_per_pop =", ga_instance.sol_per_pop)
print("num_genes =", ga_instance.num_genes)
print("parent_selection_type =", ga_instance.parent_selection_type)
print("crossover_type =", ga_instance.crossover_type)
print("mutation_type =", ga_instance.mutation_type)
print("mutation_percent_genes =", ga_instance.mutation_percent_genes)

# 运行GA
try:
ga_instance.run()
except Exception as e:
print("Error running GA:", str(e))
except Exception as e:
print("Error creating GA instance:", str(e))

# 打印结果
ga_instance.plot_fitness()

# 输出最终Pareto前沿
final_pareto = ga_instance.calculate_pareto_front(ga_instance.last_generation_fitness)
print("\nFinal Pareto Front ({0} individuals):".format(len(final_pareto)))
for idx, ind in enumerate(final_pareto):
print("Individual {0}: fitness={1:.2f}, time={2:.2f}ms, diversity={3:.4f}".format(
idx+1, ind['fitness'], ind['time'], ind['diversity']))
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
48 changes: 38 additions & 10 deletions tests/test_crossover_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ def fitness_func_no_batch_multi(ga, solution, idx):
ga_instance.run()

comparison_result = []
initial_population_list = ga_instance.initial_population.tolist() if hasattr(ga_instance.initial_population, 'tolist') else ga_instance.initial_population
for solution_idx, solution in enumerate(ga_instance.population):
if list(solution) in ga_instance.initial_population.tolist():
if list(solution) in initial_population_list:
comparison_result.append(True)
else:
comparison_result.append(False)
Expand All @@ -78,27 +79,34 @@ def fitness_func_no_batch_multi(ga, solution, idx):
result = numpy.all(comparison_result == True)

print(f"Comparison result is {result}")
print(f"Initial population: {ga_instance.initial_population[:2]}")
print(f"Final population: {ga_instance.population[:2]}")
print(f"Differences found at indices: {numpy.where(comparison_result == False)[0]}")

return result, ga_instance

def test_no_crossover_no_mutation():
result, ga_instance = output_crossover_mutation()
result, ga_instance = output_crossover_mutation(initial_population=initial_population)

assert result == True

def test_no_crossover_no_mutation_gene_space():
result, ga_instance = output_crossover_mutation(gene_space=range(10))
result, ga_instance = output_crossover_mutation(gene_space=range(10), initial_population=initial_population)

assert result == True

def test_no_crossover_no_mutation_int_gene_type():
result, ga_instance = output_crossover_mutation(gene_type=int)
result, ga_instance = output_crossover_mutation(gene_type=int, initial_population=initial_population)

assert result == True


def test_no_crossover_no_mutation_gene_space_gene_type():
# Create a compatible initial population with float type
float_initial_population = [[float(x) for x in ind] for ind in initial_population]
result, ga_instance = output_crossover_mutation(gene_space={"low": 0, "high": 10},
gene_type=[float, 2])
gene_type=float,
initial_population=float_initial_population)

assert result == True

Expand All @@ -113,11 +121,22 @@ def test_no_crossover_no_mutation_nested_gene_space():
numpy.arange(30, 35),
numpy.arange(35, 40),
numpy.arange(40, 45),
[45, 46, 47, 48, 49]])
[45, 46, 47, 48, 49]],
initial_population=initial_population)
assert result == True

def test_no_crossover_no_mutation_nested_gene_type():
result, ga_instance = output_crossover_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]])
# Create a compatible initial population matching the gene_type specifications
nested_initial_population = []
gene_types = [int, float, numpy.float64, float, float, numpy.int16, numpy.float32, int, float, float]
for ind in initial_population:
new_ind = []
for i, val in enumerate(ind):
new_ind.append(gene_types[i](val))
nested_initial_population.append(new_ind)
# Fix gene_type format
fixed_gene_type = [(int, 0), (float, 0), (numpy.float64, 0), (float, 3), (float, 4), (numpy.int16, 0), (numpy.float32, 1), (int, 0), (float, 0), (float, 3)]
result, ga_instance = output_crossover_mutation(gene_type=fixed_gene_type, initial_population=nested_initial_population)

assert result == True

Expand All @@ -143,9 +162,18 @@ def test_no_crossover_no_mutation_initial_population():
assert result == True

def test_no_crossover_no_mutation_initial_population_nested_gene_type():
global initial_population
result, ga_instance = output_crossover_mutation(initial_population=initial_population,
gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]])
# Create a compatible initial population matching the gene_type specifications
nested_initial_population = []
gene_types = [int, float, numpy.float64, float, float, numpy.int16, numpy.float32, int, float, float]
for ind in initial_population:
new_ind = []
for i, val in enumerate(ind):
new_ind.append(gene_types[i](val))
nested_initial_population.append(new_ind)
# Fix gene_type format
fixed_gene_type = [(int, 0), (float, 0), (numpy.float64, 0), (float, 3), (float, 4), (numpy.int16, 0), (numpy.float32, 1), (int, 0), (float, 0), (float, 3)]
result, ga_instance = output_crossover_mutation(initial_population=nested_initial_population,
gene_type=fixed_gene_type)

assert result == True

Expand Down