Skip to content

Commit 94fda8c

Browse files
authored
Create GA.pyx
Cythonized implementation of the GA.pyx file that achieves x18 speed compared to its Python counterpart GA.py file
1 parent 4d53266 commit 94fda8c

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

Cython/GA.pyx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import numpy
2+
cimport numpy
3+
import time
4+
import cython
5+
from libc.stdlib cimport rand, RAND_MAX
6+
7+
cdef double DOUBLE_RAND_MAX = RAND_MAX # a double variable holding the maximum random integer in C
8+
9+
@cython.wraparound(False)
10+
@cython.cdivision(True)
11+
@cython.nonecheck(False)
12+
@cython.boundscheck(False)
13+
cpdef cal_pop_fitness(numpy.ndarray[numpy.double_t, ndim=1] equation_inputs, numpy.ndarray[numpy.double_t, ndim=2] pop):
14+
# Calculating the fitness value of each solution in the current population.
15+
# The fitness function calulates the sum of products between each input and its corresponding weight.
16+
cdef numpy.ndarray[numpy.double_t, ndim=1] fitness
17+
fitness = numpy.zeros(pop.shape[0])
18+
# fitness = numpy.sum(pop*equation_inputs, axis=1) # slower than looping.
19+
for i in range(pop.shape[0]):
20+
for j in range(pop.shape[1]):
21+
fitness[i] += pop[i, j]*equation_inputs[j]
22+
return fitness
23+
24+
@cython.wraparound(False)
25+
@cython.cdivision(True)
26+
@cython.nonecheck(False)
27+
@cython.boundscheck(False)
28+
cpdef numpy.ndarray[numpy.double_t, ndim=2] select_mating_pool(numpy.ndarray[numpy.double_t, ndim=2] pop, numpy.ndarray[numpy.double_t, ndim=1] fitness, int num_parents):
29+
# Selecting the best individuals in the current generation as parents for producing the offspring of the next generation.
30+
cdef numpy.ndarray[numpy.double_t, ndim=2] parents
31+
cdef int parent_num, max_fitness_idx, min_val, max_fitness, a
32+
33+
min_val = -99999999999
34+
35+
parents = numpy.empty((num_parents, pop.shape[1]))
36+
for parent_num in range(num_parents):
37+
max_fitness_idx = 0
38+
# numpy.where(fitness == numpy.max(fitness)) # slower than argmax() by 150 ms.
39+
# max_fitness_idx = numpy.argmax(fitness) # slower than looping by 100 ms.
40+
for a in range(1, fitness.shape[0]):
41+
if fitness[a] > fitness[max_fitness_idx]:
42+
max_fitness_idx = a
43+
44+
# parents[parent_num, :] = pop[max_fitness_idx, :] # slower han looping by 20 ms
45+
for a in range(parents.shape[1]):
46+
parents[parent_num, a] = pop[max_fitness_idx, a]
47+
fitness[max_fitness_idx] = min_val
48+
return parents
49+
50+
@cython.wraparound(True)
51+
@cython.cdivision(True)
52+
@cython.nonecheck(False)
53+
@cython.boundscheck(False)
54+
cpdef numpy.ndarray[numpy.double_t, ndim=2] crossover(numpy.ndarray[numpy.double_t, ndim=2] parents, tuple offspring_size):
55+
cdef numpy.ndarray[numpy.double_t, ndim=2] offspring
56+
offspring = numpy.empty(offspring_size)
57+
# The point at which crossover takes place between two parents. Usually, it is at the center.
58+
cdef int k, parent1_idx, parent2_idx
59+
cdef numpy.int_t crossover_point
60+
crossover_point = (int) (offspring_size[1]/2)
61+
62+
for k in range(offspring_size[0]):
63+
# Index of the first parent to mate.
64+
parent1_idx = k%parents.shape[0]
65+
# Index of the second parent to mate.
66+
parent2_idx = (k+1)%parents.shape[0]
67+
68+
for m in range(crossover_point):
69+
# The new offspring will have its first half of its genes taken from the first parent.
70+
offspring[k, m] = parents[parent1_idx, m]
71+
for m in range(crossover_point-1, -1):
72+
# The new offspring will have its second half of its genes taken from the second parent.
73+
offspring[k, m] = parents[parent2_idx, m]
74+
75+
# The next 2 lines are slower than using the above loops because they run with C speed.
76+
# offspring[k, 0:crossover_point] = parents[parent1_idx, 0:crossover_point]
77+
# offspring[k, crossover_point:] = parents[parent2_idx, crossover_point:]
78+
return offspring
79+
80+
@cython.wraparound(False)
81+
@cython.cdivision(True)
82+
@cython.nonecheck(False)
83+
@cython.boundscheck(False)
84+
cpdef numpy.ndarray[numpy.double_t, ndim=2] mutation(numpy.ndarray[numpy.double_t, ndim=2] offspring_crossover, int num_mutations=1):
85+
cdef int idx, mutation_num, gene_idx
86+
cdef double random_value
87+
cdef Py_ssize_t mutations_counter
88+
mutations_counter = (int) (offspring_crossover.shape[1] / num_mutations) # using numpy.uint8() is slower than using (int)
89+
# Mutation changes a number of genes as defined by the num_mutations argument. The changes are random.
90+
cdef double rand_num
91+
for idx in range(offspring_crossover.shape[0]):
92+
gene_idx = mutations_counter - 1
93+
for mutation_num in range(num_mutations):
94+
# The random value to be added to the gene.
95+
# random_value = numpy.random.uniform(-1.0, 1.0, 1) # Slower by 300 ms compared to native C rand() function.
96+
rand_double = rand()/DOUBLE_RAND_MAX
97+
random_value = rand_double * (1.0 - (-1.0)) + (-1.0); # The new range is from 1.0 to -1.0.
98+
offspring_crossover[idx, gene_idx] = offspring_crossover[idx, gene_idx] + random_value
99+
gene_idx = gene_idx + mutations_counter
100+
return offspring_crossover
101+
102+
@cython.wraparound(False)
103+
@cython.cdivision(True)
104+
@cython.nonecheck(False)
105+
@cython.boundscheck(False)
106+
cpdef optimize():
107+
# Inputs of the equation.
108+
cdef numpy.ndarray equation_inputs, parents, new_population, fitness, offspring_crossover, offspring_mutation
109+
cdef int num_weights, sol_per_pop, num_parents_mating, num_generations
110+
cdef list pop_size
111+
cdef double t1, t2, t
112+
113+
equation_inputs = numpy.array([4,-2,3.5,5,-11,-4.7])
114+
num_weights = equation_inputs.shape[0]
115+
116+
# Number of the weights we are looking to optimize.
117+
sol_per_pop = 8
118+
num_weights = equation_inputs.shape[0]
119+
num_parents_mating = 4
120+
121+
# Defining the population size.
122+
pop_size = [sol_per_pop,num_weights] # The population will have sol_per_pop chromosome where each chromosome has num_weights genes.
123+
#Creating the initial population.
124+
new_population = numpy.random.uniform(low=-4.0, high=4.0, size=pop_size)
125+
126+
num_generations = 1000000
127+
t1 = time.time()
128+
for generation in range(num_generations):
129+
# Measuring the fitness of each chromosome in the population.
130+
fitness = cal_pop_fitness(equation_inputs, new_population)
131+
132+
# Selecting the best parents in the population for mating.
133+
parents = select_mating_pool(new_population, fitness,
134+
num_parents_mating)
135+
136+
# Generating next generation using crossover.
137+
offspring_crossover = crossover(parents,
138+
offspring_size=(pop_size[0]-parents.shape[0], num_weights))
139+
140+
# Adding some variations to the offspring using mutation.
141+
offspring_mutation = mutation(offspring_crossover, num_mutations=2)
142+
143+
# Creating the new population based on the parents and offspring.
144+
new_population[0:parents.shape[0], :] = parents
145+
new_population[parents.shape[0]:, :] = offspring_mutation
146+
t2 = time.time()
147+
t = t2-t1
148+
print("Total Time %.20f" % t)
149+
print(cal_pop_fitness(equation_inputs, new_population))

0 commit comments

Comments
 (0)