Skip to content

Commit 03b112a

Browse files
committed
Fix numpy.reshape bug and add tests
1 parent 0f6c46a commit 03b112a

File tree

9 files changed

+241
-16
lines changed

9 files changed

+241
-16
lines changed

docs/md/pygad.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -863,13 +863,13 @@ import functools
863863
import operator
864864

865865
def img2chromosome(img_arr):
866-
return numpy.reshape(a=img_arr, newshape=(functools.reduce(operator.mul, img_arr.shape)))
866+
return numpy.reshape(img_arr, (functools.reduce(operator.mul, img_arr.shape)))
867867

868868
def chromosome2img(vector, shape):
869869
if len(vector) != functools.reduce(operator.mul, shape):
870870
raise ValueError(f"A vector of length {len(vector)} into an array of shape {shape}.")
871871

872-
return numpy.reshape(a=vector, newshape=shape)
872+
return numpy.reshape(vector, shape)
873873
```
874874

875875
### Create an Instance of the `pygad.GA` Class

docs/source/pygad.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,13 +1727,13 @@ its code is listed below.
17271727
import operator
17281728
17291729
def img2chromosome(img_arr):
1730-
return numpy.reshape(a=img_arr, newshape=(functools.reduce(operator.mul, img_arr.shape)))
1730+
return numpy.reshape(img_arr, (functools.reduce(operator.mul, img_arr.shape)))
17311731
17321732
def chromosome2img(vector, shape):
17331733
if len(vector) != functools.reduce(operator.mul, shape):
17341734
raise ValueError(f"A vector of length {len(vector)} into an array of shape {shape}.")
17351735
1736-
return numpy.reshape(a=vector, newshape=shape)
1736+
return numpy.reshape(vector, shape)
17371737
17381738
.. _create-an-instance-of-the-pygadga-class-2:
17391739

pygad/cnn/cnn.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def layers_weights_as_matrix(model, vector_weights):
126126

127127
weights_vector=vector_weights[start:start + layer_weights_size]
128128
# matrix = pygad.nn.DenseLayer.to_array(vector=weights_vector, shape=layer_weights_shape)
129-
matrix = numpy.reshape(weights_vector, newshape=(layer_weights_shape))
129+
matrix = numpy.reshape(weights_vector, (layer_weights_shape))
130130
network_weights.append(matrix)
131131

132132
start = start + layer_weights_size
@@ -163,11 +163,11 @@ def layers_weights_as_vector(model, initial=True):
163163
if type(layer) in [Conv2D, Dense]:
164164
# If the 'initial' parameter is True, append the initial weights. Otherwise, append the trained weights.
165165
if initial == True:
166-
vector = numpy.reshape(layer.initial_weights, newshape=(layer.initial_weights.size))
166+
vector = numpy.reshape(layer.initial_weights, (layer.initial_weights.size))
167167
# vector = pygad.nn.DenseLayer.to_vector(matrix=layer.initial_weights)
168168
network_weights.extend(vector)
169169
elif initial == False:
170-
vector = numpy.reshape(layer.trained_weights, newshape=(layer.trained_weights.size))
170+
vector = numpy.reshape(layer.trained_weights, (layer.trained_weights.size))
171171
# vector = pygad.nn.DenseLayer.to_vector(array=layer.trained_weights)
172172
network_weights.extend(vector)
173173
else:

pygad/kerasga/kerasga.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def model_weights_as_vector(model):
2323
if layer.trainable:
2424
layer_weights = layer.get_weights()
2525
for l_weights in layer_weights:
26-
vector = numpy.reshape(l_weights, newshape=(l_weights.size))
26+
vector = numpy.reshape(l_weights, (l_weights.size))
2727
weights_vector.extend(vector)
2828

2929
return numpy.array(weights_vector)
@@ -57,7 +57,7 @@ def model_weights_as_matrix(model, weights_vector):
5757
layer_weights_size = l_weights.size
5858

5959
layer_weights_vector = weights_vector[start:start + layer_weights_size]
60-
layer_weights_matrix = numpy.reshape(layer_weights_vector, newshape=(layer_weights_shape))
60+
layer_weights_matrix = numpy.reshape(layer_weights_vector, (layer_weights_shape))
6161
weights_matrix.append(layer_weights_matrix)
6262

6363
start = start + layer_weights_size

pygad/nn/nn.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,11 @@ def layers_weights_as_vector(last_layer, initial=True):
5656
while "previous_layer" in layer.__init__.__code__.co_varnames:
5757
# If the 'initial' parameter is True, append the initial weights. Otherwise, append the trained weights.
5858
if initial == True:
59-
vector = numpy.reshape(layer.initial_weights, newshape=(layer.initial_weights.size))
59+
vector = numpy.reshape(layer.initial_weights, (layer.initial_weights.size))
6060
# vector = DenseLayer.to_vector(matrix=layer.initial_weights)
6161
network_weights.extend(vector)
6262
elif initial == False:
63-
vector = numpy.reshape(layer.trained_weights, newshape=(layer.trained_weights.size))
63+
vector = numpy.reshape(layer.trained_weights, (layer.trained_weights.size))
6464
# vector = DenseLayer.to_vector(array=layer.trained_weights)
6565
network_weights.extend(vector)
6666
else:
@@ -98,7 +98,7 @@ def layers_weights_as_matrix(last_layer, vector_weights):
9898

9999
weights_vector=vector_weights[start:start + layer_weights_size]
100100
# matrix = DenseLayer.to_array(vector=weights_vector, shape=layer_weights_shape)
101-
matrix = numpy.reshape(weights_vector, newshape=(layer_weights_shape))
101+
matrix = numpy.reshape(weights_vector, (layer_weights_shape))
102102
network_weights.append(matrix)
103103

104104
start = start + layer_weights_size
@@ -338,7 +338,7 @@ def to_vector(array):
338338
"""
339339
if not (type(array) is numpy.ndarray):
340340
raise TypeError(f"An input of type numpy.ndarray is expected but an input of type {type(array)} found.")
341-
return numpy.reshape(array, newshape=(array.size))
341+
return numpy.reshape(array, (array.size))
342342

343343
def to_array(vector, shape):
344344
"""
@@ -357,7 +357,7 @@ def to_array(vector, shape):
357357
raise ValueError(f"A 1D NumPy array is expected but an array of {vector.ndim} dimensions found.")
358358
if vector.size != functools.reduce(lambda x,y:x*y, shape, 1): # (operator.mul == lambda x,y:x*y
359359
raise ValueError(f"Mismatch between the vector length and the array shape. A vector of length {vector.size} cannot be converted into a array of shape ({shape}).")
360-
return numpy.reshape(vector, newshape=shape)
360+
return numpy.reshape(vector, shape)
361361

362362
class InputLayer:
363363
"""

pygad/torchga/torchga.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def model_weights_as_vector(model):
1010
# cpu() is called for making shore the data is moved from GPU to cpu
1111
# numpy() is called for converting the tensor into a NumPy array.
1212
curr_weights = curr_weights.cpu().detach().numpy()
13-
vector = numpy.reshape(curr_weights, newshape=(curr_weights.size))
13+
vector = numpy.reshape(curr_weights, (curr_weights.size))
1414
weights_vector.extend(vector)
1515

1616
return numpy.array(weights_vector)
@@ -28,7 +28,7 @@ def model_weights_as_dict(model, weights_vector):
2828
layer_weights_size = w_matrix.size
2929

3030
layer_weights_vector = weights_vector[start:start + layer_weights_size]
31-
layer_weights_matrix = numpy.reshape(layer_weights_vector, newshape=(layer_weights_shape))
31+
layer_weights_matrix = numpy.reshape(layer_weights_vector, (layer_weights_shape))
3232
weights_dict[key] = torch.from_numpy(layer_weights_matrix)
3333

3434
start = start + layer_weights_size

tests/test_gann.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pygad.gann
2+
import pygad.nn
3+
import numpy
4+
5+
def test_gann_regression():
6+
"""Test GANN for a simple regression problem."""
7+
# Data
8+
data_inputs = numpy.array([[0.02, 0.1, 0.15],
9+
[0.7, 0.6, 0.8],
10+
[1.5, 1.2, 1.7],
11+
[3.2, 2.9, 3.1]])
12+
data_outputs = numpy.array([0.1, 0.6, 1.3, 2.5])
13+
14+
# GANN architecture
15+
num_inputs = data_inputs.shape[1]
16+
num_classes = 1 # Regression
17+
18+
gann_instance = pygad.gann.GANN(num_solutions=10,
19+
num_neurons_input=num_inputs,
20+
num_neurons_hidden_layers=[5],
21+
num_neurons_output=num_classes,
22+
hidden_activations="relu",
23+
output_activation="None")
24+
25+
# The number of genes is the total number of weights in the network.
26+
# We can get it by converting the weights of any network in the population into a vector.
27+
num_genes = len(pygad.nn.layers_weights_as_vector(last_layer=gann_instance.population_networks[0]))
28+
29+
def fitness_func(ga_instance, solution, solution_idx):
30+
# Update the weights of the network associated with the current solution.
31+
# GANN.update_population_trained_weights expects weights for ALL solutions.
32+
# To avoid updating all, we can update just the one we need.
33+
34+
# However, for simplicity and to test GANN's intended flow:
35+
population_matrices = pygad.gann.population_as_matrices(num_networks=ga_instance.sol_per_pop,
36+
population_vectors=ga_instance.population)
37+
gann_instance.update_population_trained_weights(population_trained_weights=population_matrices)
38+
39+
predictions = pygad.nn.predict(last_layer=gann_instance.population_networks[solution_idx],
40+
data_inputs=data_inputs)
41+
42+
# Mean Absolute Error
43+
abs_error = numpy.mean(numpy.abs(predictions.flatten() - data_outputs)) + 0.00000001
44+
fitness = 1.0 / abs_error
45+
return fitness
46+
47+
ga_instance = pygad.GA(num_generations=5,
48+
num_parents_mating=4,
49+
fitness_func=fitness_func,
50+
sol_per_pop=10,
51+
num_genes=num_genes,
52+
random_seed=42,
53+
suppress_warnings=True)
54+
55+
ga_instance.run()
56+
assert ga_instance.run_completed
57+
print("test_gann_regression passed.")
58+
59+
def test_nn_direct_usage():
60+
"""Test pygad.nn layers directly."""
61+
input_layer = pygad.nn.InputLayer(num_inputs=3)
62+
dense_layer = pygad.nn.DenseLayer(num_neurons=2, previous_layer=input_layer, activation_function="relu")
63+
output_layer = pygad.nn.DenseLayer(num_neurons=1, previous_layer=dense_layer, activation_function="sigmoid")
64+
65+
data_inputs = numpy.array([[0.1, 0.2, 0.3]])
66+
predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs)
67+
68+
assert predictions.shape == (1, 1)
69+
assert 0 <= predictions[0, 0] <= 1
70+
print("test_nn_direct_usage passed.")
71+
72+
if __name__ == "__main__":
73+
test_gann_regression()
74+
test_nn_direct_usage()
75+
print("\nAll GANN/NN tests passed!")

tests/test_operators.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import pygad
2+
import numpy
3+
import random
4+
5+
# Global constants for testing
6+
num_generations = 5
7+
num_parents_mating = 4
8+
sol_per_pop = 10
9+
num_genes = 10
10+
random_seed = 42
11+
12+
def fitness_func(ga_instance, solution, solution_idx):
13+
return numpy.sum(solution)
14+
15+
def fitness_func_multi(ga_instance, solution, solution_idx):
16+
return [numpy.sum(solution), numpy.sum(solution**2)]
17+
18+
def run_ga_with_params(parent_selection_type='sss', crossover_type='single_point', mutation_type='random', multi_objective=False):
19+
if multi_objective:
20+
f = fitness_func_multi
21+
else:
22+
f = fitness_func
23+
24+
ga_instance = pygad.GA(num_generations=num_generations,
25+
num_parents_mating=num_parents_mating,
26+
fitness_func=f,
27+
sol_per_pop=sol_per_pop,
28+
num_genes=num_genes,
29+
parent_selection_type=parent_selection_type,
30+
crossover_type=crossover_type,
31+
mutation_type=mutation_type,
32+
random_seed=random_seed,
33+
suppress_warnings=True)
34+
ga_instance.run()
35+
return ga_instance
36+
37+
def test_selection_operators():
38+
operators = ['sss', 'rws', 'sus', 'rank', 'random', 'tournament']
39+
for op in operators:
40+
ga = run_ga_with_params(parent_selection_type=op)
41+
# Verify parents were selected
42+
assert ga.last_generation_parents.shape == (num_parents_mating, num_genes)
43+
print(f"Selection operator '{op}' passed.")
44+
45+
def test_crossover_operators():
46+
operators = ['single_point', 'two_points', 'uniform', 'scattered']
47+
for op in operators:
48+
ga = run_ga_with_params(crossover_type=op)
49+
# Verify population shape
50+
assert ga.population.shape == (sol_per_pop, num_genes)
51+
print(f"Crossover operator '{op}' passed.")
52+
53+
def test_mutation_operators():
54+
operators = ['random', 'swap', 'inversion', 'scramble']
55+
for op in operators:
56+
ga = run_ga_with_params(mutation_type=op)
57+
# Verify population shape
58+
assert ga.population.shape == (sol_per_pop, num_genes)
59+
print(f"Mutation operator '{op}' passed.")
60+
61+
def test_multi_objective_selection():
62+
# NSGA-II is usually used for multi-objective
63+
ga = run_ga_with_params(parent_selection_type='nsga2', multi_objective=True)
64+
assert ga.last_generation_parents.shape == (num_parents_mating, num_genes)
65+
print("Multi-objective selection (nsga2) passed.")
66+
67+
# Tournament NSGA-II
68+
ga = run_ga_with_params(parent_selection_type='tournament_nsga2', multi_objective=True)
69+
assert ga.last_generation_parents.shape == (num_parents_mating, num_genes)
70+
print("Multi-objective selection (tournament_nsga2) passed.")
71+
72+
if __name__ == "__main__":
73+
test_selection_operators()
74+
test_crossover_operators()
75+
test_mutation_operators()
76+
test_multi_objective_selection()
77+
print("\nAll operator tests passed!")

tests/test_parallel.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import pygad
2+
import numpy
3+
import time
4+
5+
# Global constants for testing
6+
num_generations = 5
7+
num_parents_mating = 4
8+
sol_per_pop = 10
9+
num_genes = 3
10+
random_seed = 42
11+
12+
def fitness_func(ga_instance, solution, solution_idx):
13+
# Simulate some work
14+
# time.sleep(0.01)
15+
return numpy.sum(solution**2)
16+
17+
def fitness_func_batch(ga_instance, solutions, indices):
18+
return [numpy.sum(s**2) for s in solutions]
19+
20+
def test_parallel_thread():
21+
"""Test parallel_processing with 'thread' mode."""
22+
ga_instance = pygad.GA(num_generations=num_generations,
23+
num_parents_mating=num_parents_mating,
24+
fitness_func=fitness_func,
25+
sol_per_pop=sol_per_pop,
26+
num_genes=num_genes,
27+
parallel_processing=["thread", 2],
28+
random_seed=random_seed,
29+
suppress_warnings=True
30+
)
31+
ga_instance.run()
32+
assert ga_instance.run_completed
33+
print("test_parallel_thread passed.")
34+
35+
def test_parallel_process():
36+
"""Test parallel_processing with 'process' mode."""
37+
# Note: 'process' mode might be tricky in some environments (e.g. Windows without if __name__ == '__main__':)
38+
# But for a CI environment it should be tested.
39+
ga_instance = pygad.GA(num_generations=num_generations,
40+
num_parents_mating=num_parents_mating,
41+
fitness_func=fitness_func,
42+
sol_per_pop=sol_per_pop,
43+
num_genes=num_genes,
44+
parallel_processing=["process", 2],
45+
random_seed=random_seed,
46+
suppress_warnings=True
47+
)
48+
ga_instance.run()
49+
assert ga_instance.run_completed
50+
print("test_parallel_process passed.")
51+
52+
def test_parallel_thread_batch():
53+
"""Test parallel_processing with 'thread' mode and batch fitness."""
54+
ga_instance = pygad.GA(num_generations=num_generations,
55+
num_parents_mating=num_parents_mating,
56+
fitness_func=fitness_func_batch,
57+
sol_per_pop=sol_per_pop,
58+
num_genes=num_genes,
59+
parallel_processing=["thread", 2],
60+
fitness_batch_size=2,
61+
random_seed=random_seed,
62+
suppress_warnings=True
63+
)
64+
ga_instance.run()
65+
assert ga_instance.run_completed
66+
print("test_parallel_thread_batch passed.")
67+
68+
if __name__ == "__main__":
69+
# For 'process' mode to work on Windows/macOS, we need this guard
70+
test_parallel_thread()
71+
test_parallel_process()
72+
test_parallel_thread_batch()
73+
print("\nAll parallel tests passed!")

0 commit comments

Comments
 (0)