8000 Merge pull request #241 from ahmedfgad/github-actions · madprog/GeneticAlgorithmPython@adf7758 · GitHub
[go: up one dir, main page]

Skip to content

Commit adf7758

Browse files
authored
Merge pull request ahmedfgad#241 from ahmedfgad/github-actions
Testing stop_criteria
2 parents 9137dd4 + 210c345 commit adf7758

File tree

2 files changed

+286
-13
lines changed

2 files changed

+286
-13
lines changed

pygad/pygad.py

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,23 +1171,40 @@ def __init__(self,
11711171
# reach_{target_fitness}: Stop if the target fitness value is reached.
11721172
# saturate_{num_generations}: Stop if the fitness value does not change (saturates) for the given number of generations.
11731173
criterion = stop_criteria.split("_")
1174-
if len(criterion) == 2:
1175-
stop_word = criterion[0]
1176-
number = criterion[1]
1177-
1178-
if stop_word in self.supported_stop_words:
1179-
pass
1180-
else:
1181-
self.valid_parameters = False
1182-
raise ValueError(f"In the 'stop_criteria' parameter, the supported stop words are '{self.supported_stop_words}' but '{stop_word}' found.")
1174+
stop_word = criterion[0]
1175+
# criterion[1] might be a single or multiple numbers.
1176+
number = criterion[1:]
1177+
if stop_word in self.supported_stop_words:
1178+
pass
1179+
else:
1180+
self.valid_parameters = False
1181+
raise ValueError(f"In the 'stop_criteria' parameter, the supported stop words are '{self.supported_stop_words}' but '{stop_word}' found.")
11831182

1183+
if len(criterion) == 2:
1184+
# There is only a single number.
1185+
number = number[0]
11841186
if number.replace(".", "").isnumeric():
11851187
number = float(number)
11861188
else:
11871189
self.valid_parameters = False
11881190
raise ValueError(f"The value following the stop word in the 'stop_criteria' parameter must be a number but the value ({number}) of type {type(number)} found.")
11891191

11901192
self.stop_criteria.append([stop_word, number])
1193+
elif len(criterion) > 2:
1194+
if stop_word == 'reach':
1195+
pass
1196+
else:
1197+
self.valid_parameters = False
1198+
raise ValueError(f"Passing multiple numbers following the keyword in the 'stop_criteria' parameter is expected only with the 'reach' keyword but the keyword ({stop_word}) found.")
1199+
1200+
for idx, num in enumerate(number):
1201+
if num.replace(".", "").isnumeric():
1202+
number[idx] = float(num)
1203+
else:
1204+
self.valid_parameters = False
1205+
raise ValueError(f"The value(s) following the stop word in the 'stop_criteria' parameter must be numeric but the value ({num}) of type {type(num)} found.")
1206+
1207+
self.stop_criteria.append([stop_word] + number)
11911208

11921209
else:
11931210
self.valid_parameters = False
@@ -2133,15 +2150,56 @@ def run(self):
21332150
if not self.stop_criteria is None:
21342151
for criterion in self.stop_criteria:
21352152
if criterion[0] == "reach":
2136-
if max(self.last_generation_fitness) >= criterion[1]:
2153+
# Single-objective problem.
2154+
if type(self.last_generation_fitness[0]) in GA.supported_int_float_types:
2155+
if max(self.last_generation_fitness) >= criterion[1]:
2156+
stop_run = True
2157+
break
2158+
# Multi-objective problem.
2159+
elif type(self.last_generation_fitness[0]) in [list, tuple, numpy.ndarray]:
2160+
# Validate the value passed to the criterion.
2161+
if len(criterion[1:]) == 1:
2162+
# There is a single value used across all the objectives.
2163+
pass
2164+
elif len(criterion[1:]) > 1:
2165+
# There are multiple values. The number of values must be equal to the number of objectives.
2166+
if len(criterion[1:]) == len(self.last_generation_fitness[0]):
2167+
pass
2168+
else:
2169+
self.valid_parameters = False
2170+
raise ValueError(f"When the the 'reach' keyword is used with the 'stop_criteria' parameter for solving a multi-objective problem, then the number of numeric values following the keyword can be:\n1) A single numeric value to be used across all the objective functions.\n2) A number of numeric values equal to the number of objective functions.\nBut the value {criterion} found with {len(criterion)-1} numeric values which is not equal to the number of objective functions {len(self.last_generation_fitness[0])}.")
2171+
21372172
stop_run = True
2138-
break
2173+
for obj_idx in range(len(self.last_generation_fitness[0])):
2174+
# Use the objective index to return the proper value for the criterion.
2175+
2176+
if len(criterion[1:]) == len(self.last_generation_fitness[0]):
2177+
reach_fitness_value = criterion[obj_idx + 1]
2178+
elif len(criterion[1:]) == 1:
2179+
reach_fitness_value = criterion[1]
2180+
2181+
if max(self.last_generation_fitness[:, obj_idx]) >= reach_fitness_value:
2182+
pass
2183+
else:
2184+
stop_run = False
2185+
break
21392186
elif criterion[0] == "saturate":
21402187
criterion[1] = int(criterion[1])
21412188
if (self.generations_completed >= criterion[1]):
2142-
if (self.best_solutions_fitness[self.generations_completed - criterion[1]] - self.best_solutions_fitness[self.generations_completed - 1]) == 0:
2189+
# Single-objective problem.
2190+
if type(self.last_generation_fitness[0]) in GA.supported_int_float_types:
2191+
if (self.best_solutions_fitness[self.generations_completed - criterion[1]] - self.best_solutions_fitness[self.generations_completed - 1]) == 0:
2192+
stop_run = True
2193+
break
2194+
# Multi-objective problem.
2195+
elif type(self.last_generation_fitness[0]) in [list, tuple, numpy.ndarray]:
21432196
stop_run = True
2144-
break
2197+
for obj_idx in range(len(self.last_generation_fitness[0])):
2198+
if (self.best_solutions_fitness[self.generations_completed - criterion[1]][obj_idx] - self.best_solutions_fitness[self.generations_completed - 1][obj_idx]) == 0:
2199+
pass
2200+
else:
2201+
stop_run = False
2202+
break
21452203

21462204
if stop_run:
21472205
break

tests/test_stop_criteria.py

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import pygad
2+
import numpy
3+
4+
actual_num_fitness_calls_default_keep = 0
5+
actual_num_fitness_calls_no_keep = 0
6+
actual_num_fitness_calls_keep_elitism = 0
7+
actual_num_fitness_calls_keep_parents = 0
8+
9+
num_generations = 100
10+
sol_per_pop = 10
11+
num_parents_mating = 5
12+
13+
def multi_objective_problem(keep_elitism=1,
14+
keep_parents=-1,
15+
fitness_batch_size=None,
16+
stop_criteria=None,
17+
parent_selection_type='sss',
18+
mutation_type="random",
19+
mutation_percent_genes="default",
20+
multi_objective=False):
21+
22+
function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs.
23+
function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs.
24+
desired_output1 = 50 # Function 1 output.
25+
desired_output2 = 30 # Function 2 output.
26+
27+
def fitness_func_batch_multi(ga_instance, solution, solution_idx):
28+
f = []
29+
for sol in solution:
30+
output1 = numpy.sum(sol*function_inputs1)
31+
output2 = numpy.sum(sol*function_inputs2)
32+
fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001)
33+
fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001)
34+
f.append([fitness1, fitness2])
35+
return f
36+
37+
def fitness_func_no_batch_multi(ga_instance, solution, solution_idx):
38+
output1 = numpy.sum(solution*function_inputs1)
39+
output2 = numpy.sum(solution*function_inputs2)
40+
fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001)
41+
fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001)
42+
return [fitness1, fitness2]
43+
44+
def fitness_func_batch_single(ga_instance, solution, solution_idx):
45+
f = []
46+
for sol in solution:
47+
output = numpy.sum(solution*function_inputs1)
48+
fitness = 1.0 / (numpy.abs(output - desired_output1) + 0.000001)
49+
f.append(fitness)
50+
return f
51+
52+
def fitness_func_no_batch_single(ga_instance, solution, solution_idx):
53+
output = numpy.sum(solution*function_inputs1)
54+
fitness = 1.0 / (numpy.abs(output - desired_output1) + 0.000001)
55+
return fitness
56+
57+
if fitness_batch_size is None or (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size == 1):
58+
if multi_objective == True:
59+
fitness_func = fitness_func_no_batch_multi
60+
else:
61+
fitness_func = fitness_func_no_batch_single
62+
elif (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size > 1):
63+
if multi_objective == True:
64+
fitness_func = fitness_func_batch_multi
65+
else:
66+
fitness_func = fitness_func_batch_single
67+
68+
ga_optimizer = pygad.GA(num_generations=num_generations,
69+
sol_per_pop=sol_per_pop,
70+
num_genes=6,
71+
num_parents_mating=num_parents_mating,
72+
fitness_func=fitness_func,
73+
fitness_batch_size=fitness_batch_size,
74+
mutation_type=mutation_type,
75+
mutation_percent_genes=mutation_percent_genes,
76+
keep_elitism=keep_elitism,
77+
keep_parents=keep_parents,
78+
stop_criteria=stop_criteria,
79+
parent_selection_type=parent_selection_type,
80+
suppress_warnings=True)
81+
82+
ga_optimizer.run()
83+
84+
return ga_optimizer.generations_completed, ga_optimizer.best_solutions_fitness, ga_optimizer.last_generation_fitness, stop_criteria
85+
86+
def test_number_calls_fitness_function_default_keep():
87+
multi_objective_problem()
88+
89+
def test_number_calls_fitness_function_stop_criteria_reach(multi_objective=False,
90+
fitness_batch_size=None,
91+
num=10):
92+
generations_completed, best_solutions_fitness, last_generation_fitness 1241 , stop_criteria = multi_objective_problem(multi_objective=multi_objective,
93+
fitness_batch_size=fitness_batch_size,
94+
stop_criteria=f"reach_{num}")
95+
# Verify that the GA stops when meeting the criterion.
96+
criterion = stop_criteria.split('_')
97+
stop_word = criterion[0]
98+
if generations_completed < num_generations:
99+
if stop_word == 'reach':
100+
if len(criterion) > 2:
101+
# multi-objective problem.
102+
for idx, num in enumerate(criterion[1:]):
103+
criterion[idx + 1] = float(num)
104+
else:
105+
criterion[1] = float(criterion[1])
106+
107+
# Single-objective
108+
if type(last_generation_fitness[0]) in pygad.GA.supported_int_float_types:
109+
assert max(last_generation_fitness) >= criterion[1]
110+
# Multi-objective
111+
elif type(last_generation_fitness[0]) in [list, tuple, numpy.ndarray]:
112+
# Validate the value passed to the criterion.
113+
if len(criterion[1:]) == 1:
114+
# There is a single value used across all the objectives.
115+
pass
116+
elif len(criterion[1:]) > 1:
117+
# There are multiple values. The number of values must be equal to the number of objectives.
118+
if len(criterion[1:]) == len(last_generation_fitness[0]):
119+
pass
120+
else:
121+
raise ValueError("Error")
122+
123+
for obj_idx in range(len(last_generation_fitness[0])):
124+
# Use the objective index to return the proper value for the criterion.
125+
if len(criterion[1:]) == len(last_generation_fitness[0]):
126+
reach_fitness_value = criterion[obj_idx + 1]
127+
elif len(criterion[1:]) == 1:
128+
reach_fitness_value = criterion[1]
129+
130+
assert max(last_generation_fitness[:, obj_idx]) >= reach_fitness_value
131+
132+
def test_number_calls_fitness_function_stop_criteria_saturate(multi_objective=False,
133+
fitness_batch_size=None,
134+
num=5):
135+
generations_completed, best_solutions_fitness, last_generation_fitness, stop_criteria = multi_objective_problem(multi_objective=multi_objective,
136+
fitness_batch_size=fitness_batch_size,
137+
stop_criteria=f"saturate_{num}")
138+
# Verify that the GA stops when meeting the criterion.
139+
criterion = stop_criteria.split('_')
140+
stop_word = criterion[0]
141+
number = criterion[1]
142+
if generations_completed < num_generations:
143+
if stop_word == 'saturate':
144+
number = int(number)
145+
if type(last_generation_fitness[0]) in pygad.GA.supported_int_float_types:
146+
assert best_solutions_fitness[generations_completed - number] == best_solutions_fitness[generations_completed - 1]
147+
elif type(last_generation_fitness[0]) in [list, tuple, numpy.ndarray]:
148+
for obj_idx in range(len(best_solutions_fitness[0])):
149+
assert best_solutions_fitness[generations_completed - number][obj_idx] == best_solutions_fitness[generations_completed - 1][obj_idx]
150+
151+
if __name__ == "__main__":
152+
#### Single-objective problem with a single numeric value with stop_criteria.
153+
print()
154+
test_number_calls_fitness_function_default_keep()
155+
print()
156+
test_number_calls_fitness_function_stop_criteria_reach()
157+
print()
158+
test_number_calls_fitness_function_stop_criteria_reach(num=2)
159+
print()
160+
test_number_calls_fitness_function_stop_criteria_saturate()
161+
print()
162+
test_number_calls_fitness_function_stop_criteria_saturate(num=2)
163+
print()
164+
test_number_calls_fitness_function_stop_criteria_reach(fitness_batch_size=4)
165+
print()
166+
test_number_calls_fitness_function_stop_criteria_reach(fitness_batch_size=4,
167+
num=2)
168+
print()
169+
test_number_calls_fitness_function_stop_criteria_saturate(fitness_batch_size=4)
170+
print()
171+
test_number_calls_fitness_function_stop_criteria_saturate(fitness_batch_size=4,
172+
num=2)
173+
print()
174+
175+
176+
#### Multi-objective problem with a single numeric value with stop_criteria.
177+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True)
178+
print()
179+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True,
180+
num=2)
181+
print()
182+
test_number_calls_fitness_function_stop_criteria_saturate(multi_objective=True)
183+
print()
184+
test_number_calls_fitness_function_stop_criteria_saturate(multi_objective=True,
185+
num=2)
186+
print()
187+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True,
188+
fitness_batch_size=4)
189+
print()
190+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True,
191+
fitness_batch_size=4,
192+
num=2)
193+
print()
194+
test_number_calls_fitness_function_stop_criteria_saturate(multi_objective=True,
195+
fitness_batch_size=4)
196+
print()
197+
test_number_calls_fitness_function_stop_criteria_saturate(multi_objective=True,
198+
fitness_batch_size=4,
199+
num=50)
200+
print()
201+
202+
203+
#### Multi-objective problem with multiple numeric values with stop_criteria.
204+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True)
205+
print()
206+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True,
207+
num="2_5")
208+
print()
209+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True,
210+
fitness_batch_size=4)
211+
print()
212+
test_number_calls_fitness_function_stop_criteria_reach(multi_objective=True,
213+
fitness_batch_size=4,
214+
num="10_20")
215+

0 commit comments

Comments
 (0)
0