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
357 changes: 357 additions & 0 deletions pygad.egg-info/PKG-INFO

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions pygad.egg-info/SOURCES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
LICENSE
README.md
pyproject.toml
setup.py
pygad/__init__.py
pygad/pygad.py
pygad.egg-info/PKG-INFO
pygad.egg-info/SOURCES.txt
pygad.egg-info/dependency_links.txt
pygad.egg-info/requires.txt
pygad.egg-info/top_level.txt
pygad/cnn/__init__.py
pygad/cnn/cnn.py
pygad/gacnn/__init__.py
pygad/gacnn/gacnn.py
pygad/gann/__init__.py
pygad/gann/gann.py
pygad/helper/__init__.py
pygad/helper/misc.py
pygad/helper/unique.py
pygad/kerasga/__init__.py
pygad/kerasga/kerasga.py
pygad/nn/__init__.py
pygad/nn/nn.py
pygad/torchga/__init__.py
pygad/torchga/torchga.py
pygad/utils/__init__.py
pygad/utils/crossover.py
pygad/utils/engine.py
pygad/utils/mutation.py
pygad/utils/nsga2.py
pygad/utils/parent_selection.py
pygad/utils/validation.py
pygad/visualize/__init__.py
pygad/visualize/plot.py
tests/test_adaptive_mutation.py
tests/test_allow_duplicate_genes.py
tests/test_best_solution.py
tests/test_constrained_nsga2.py
tests/test_crossover_mutation.py
tests/test_gann.py
tests/test_gene_constraint.py
tests/test_gene_space.py
tests/test_gene_space_allow_duplicate_genes.py
tests/test_gene_type.py
tests/test_lifecycle_callbacks_calls.py
tests/test_number_fitness_function_calls.py
tests/test_operators.py
tests/test_parallel.py
tests/test_save_solutions.py
tests/test_stop_criteria.py
tests/test_summary.py
tests/test_visualize.py
1 change: 1 addition & 0 deletions pygad.egg-info/dependency_links.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

7 changes: 7 additions & 0 deletions pygad.egg-info/requires.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
numpy
matplotlib
cloudpickle

[deep_learning]
keras
torch
1 change: 1 addition & 0 deletions pygad.egg-info/top_level.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pygad
39 changes: 34 additions & 5 deletions pygad/utils/nsga2.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,28 @@ def get_non_dominated_set(self, curr_solutions, constraint_violations=None):

return dominated_set, non_dominated_set

def _get_constraint_violations(self):
def _get_constraint_violations(self, solutions=None):
"""
Helper method to get constraint violations if constraint_func is defined.

Parameters
----------
solutions : numpy.ndarray, optional
The solutions to calculate constraint violations for.
If None, uses self.population.

Returns
-------
constraint_violations : numpy.ndarray or None
Total constraint violations for each solution, or None if no constraint_func.
"""
if hasattr(self, 'constraint_func') and self.constraint_func is not None:
if hasattr(self, 'population') and self.population is not None:
return self.calculate_constraint_violations(self.constraint_func, self.population)[0]
if solutions is None:
if hasattr(self, 'population') and self.population is not None:
solutions = self.population
else:
return None
return self.calculate_constraint_violations(self.constraint_func, solutions)[0]
return None

def non_dominated_sorting(self, fitness, constraint_violations=None):
Expand Down Expand Up @@ -257,8 +267,27 @@ def sort_solutions_nsga2(self,
crowding_dist_pop_sorted_indices = list(crowding_dist_pop_sorted_indices)
solutions_sorted.extend(crowding_dist_pop_sorted_indices)
elif type(fitness[0]) in pygad.GA.supported_int_float_types:
solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k])
solutions_sorted.reverse()
if constraint_violations is None:
constraint_violations = self._get_constraint_violations()

if constraint_violations is not None:
# Constraint domination for single-objective:
# 1. Feasible solutions (cv <= 0) always come before infeasible ones
# 2. Between feasible solutions: higher fitness is better
# 3. Between infeasible solutions: smaller constraint violation is better
# Sort key: (is_infeasible, secondary_key)
# - is_infeasible: 0 for feasible, 1 for infeasible
# - secondary_key: for feasible, -fitness (higher first); for infeasible, cv (smaller first)
solutions_sorted = sorted(
range(len(fitness)),
key=lambda k: (
constraint_violations[k] > 0, # 0 for feasible (first), 1 for infeasible (second)
-fitness[k] if constraint_violations[k] <= 0 else constraint_violations[k]
)
)
else:
solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k])
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')

Expand Down
117 changes: 117 additions & 0 deletions tests/test_constrained_nsga2.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,121 @@ def test_feasible_pareto_domination():
return True


def test_single_objective_constraint_domination():
"""
Test constraint domination for single-objective optimization.

For single-objective:
1. Feasible solutions always come before infeasible ones.
2. Between feasible solutions: higher fitness is better.
3. Between infeasible solutions: smaller constraint violation is better.
"""
print("\n" + "="*60)
print("Testing: Single-Objective Constraint Domination")
print("="*60)

nsga2 = pygad.utils.nsga2.NSGA2()
nsga2.supported_int_float_types = [int, float, numpy.float64, numpy.int64]

# Test case:
# Solution 0: feasible (cv=0), fitness=10 (best feasible)
# Solution 1: feasible (cv=0), fitness=5 (worse feasible)
# Solution 2: infeasible (cv=1), fitness=100 (good fitness but violates constraint
# Solution 3: infeasible (cv=5), fitness=1000 (best fitness but worst violation)

fitness = numpy.array([10.0, 5.0, 100.0, 1000.0])
constraint_violations = numpy.array([0.0, 0.0, 1.0, 5.0])

# Test: sort_solutions_nsga2 with single-objective fitness
sorted_indices = nsga2.sort_solutions_nsga2(
fitness,
constraint_violations=constraint_violations
)

print(f"\nFitness values:")
for i in range(len(fitness)):
status = "feasible" if constraint_violations[i] <= 0 else "infeasible"
print(f" Solution {i}: fitness={fitness[i]}, violation={constraint_violations[i]} ({status})")

print(f"\nExpected order: [0, 1, 2, 3]")
print(f" - Solutions 0,1 (feasible) come before 2,3 (infeasible)")
print(f" - Between feasible: 0 (fitness=10) > 1 (fitness=5)")
print(f" - Between infeasible: 2 (cv=1) < 3 (cv=5)")
print(f"\nActual sorted indices: {sorted_indices}")

# Assertions
feasible_indices = [i for i in sorted_indices if constraint_violations[i] <= 0]
infeasible_indices = [i for i in sorted_indices if constraint_violations[i] > 0]

print(f"\nFeasible in sorted order: {feasible_indices}")
print(f"Infeasible in sorted order: {infeasible_indices}")

# All feasible should come before infeasible
all_feasible_first = all(
sorted_indices.index(f) < sorted_indices.index(inf)
for f in feasible_indices
for inf in infeasible_indices
)
assert all_feasible_first, "All feasible solutions should come before infeasible ones"
print("\n✓ All feasible solutions come before infeasible ones")

# Between feasible: higher fitness first
if len(feasible_indices) >= 2:
assert fitness[feasible_indices[0]] >= fitness[feasible_indices[1]], \
"Between feasible solutions, higher fitness should come first"
print("✓ Between feasible solutions, higher fitness comes first")

# Between infeasible: smaller violation first
if len(infeasible_indices) >= 2:
assert constraint_violations[infeasible_indices[0]] <= constraint_violations[infeasible_indices[1]], \
"Between infeasible solutions, smaller violation should come first"
print("✓ Between infeasible solutions, smaller violation comes first")

return True


def test_get_constraint_violations_with_solutions():
"""
Test that _get_constraint_violations accepts solutions parameter.
"""
print("\n" + "="*60)
print("Testing: _get_constraint_violations with solutions parameter")
print("="*60)

nsga2 = pygad.utils.nsga2.NSGA2()
nsga2.supported_int_float_types = [int, float, numpy.float64, numpy.int64]

# Define constraint_func: sum of genes should be <= 0
def constraint_func(solution, solution_idx):
return [numpy.sum(solution)]

nsga2.constraint_func = constraint_func

# Test with custom solutions
custom_solutions = numpy.array([
[-1.0, -2.0, -3.0], # sum = -6 (feasible)
[1.0, 2.0, 3.0], # sum = 6 (infeasible)
[0.0, 0.0, 0.0], # sum = 0 (feasible)
])

# Without solutions parameter (should use self.population, but it's None)
result_none = nsga2._get_constraint_violations()
print(f"\nWithout solutions parameter (self.population is None): {result_none}")
assert result_none is None, "Should return None when no solutions available"

# With solutions parameter
result_with_solutions = nsga2._get_constraint_violations(solutions=custom_solutions)
print(f"With solutions parameter: {result_with_solutions}")

expected = numpy.array([0.0, 6.0, 0.0]) # Only positive violations are summed
print(f"Expected: {expected}")

numpy.testing.assert_array_almost_equal(result_with_solutions, expected)
print("\n✓ _get_constraint_violations correctly uses provided solutions")

return True


def test_backward_compatibility():
"""
Test that NSGA-II works without constraint_func (backward compatibility).
Expand Down Expand Up @@ -262,6 +377,8 @@ def fitness_func(ga_instance, solution, solution_idx):
test_feasible_vs_infeasible_domination()
test_infeasible_vs_infeasible_domination()
test_feasible_pareto_domination()
test_single_objective_constraint_domination()
test_get_constraint_violations_with_solutions()

print("\n" + "="*60)
print("Running full GA test with constraints...")
Expand Down