def select_elite(self, population):
		scores_for_chromosomes = []
		for i in range(0, len(population)):
			chromosome = population[i]
			result = brainfuck.evaluate(chromosome)
			score = self.fitness_score(result, self.target)
			if score == self.max_fitness_score:
				current_time = time.time()
				print("\nFOUND SOLUTION: " + chromosome + " for: " + repr(self.target) +  " in: " + str(int((current_time-self.start_time)/60)) + " minutes")
				self.best_fitness_scores.append(self.max_fitness_score)
				self.update_fitness_plot()
				exit()
			scores_for_chromosomes.append((chromosome, score))
		scores_for_chromosomes.sort(key=lambda x: x[1])
		scores = [x[1] for x in scores_for_chromosomes]
		print("population: " + "(min: " + str(min(scores)) + ", avg: " + str(mean(scores)) + ", max: " + str(max(scores)) + ")")

		top_performers = scores_for_chromosomes[-TOP_PERFORMERS_COUNT:]
		top_scores = [x[1] for x in top_performers]
		print("elite " + str(round(1.0-SELECTION_RATE, 2)) + ": " + "(min: " + str(min(top_scores)) + ", avg: " + str(mean(top_scores)) + ", max: " + str(max(top_scores)) + ")")
		
		chromosome = top_performers[-1][0]
		result = brainfuck.evaluate(chromosome)
		best_fitness_score = self.fitness_score(result, self.target)
		print("best: " + chromosome + ", result: " + repr(result) + ", score: " + str(best_fitness_score) + "/" + str(self.max_fitness_score))

		self.best_fitness_scores.append(best_fitness_score)
		self.update_fitness_plot()
		
		return top_performers
	def genetic_evolution(self):
		self.population = self.generate_population(self.population)
		while True:
			print("\ngeneration: " + str(self.generation) + ", population: " + str(len(self.population)) + ", mutation_rate: " + str(MUTATION_RATE))
			
			# 1. Selection
			elite = self.select_elite(self.population)

			# 2. Crossover (Roulette selection)
			pairs = self.generate_pairs(elite)
			selected_offsprings = []
			for pair in pairs:
				offsprings = self.crossover(pair[0][0], pair[1][0])
				selected_offsprings.append(offsprings[random.randint(0, 1)])

			# 3. Mutation
			mutated_population = self.mutation(selected_offsprings)

			# 4. Validation (We don't want syntactically incorrect programs)
			valid_population = []
			for chromosome in mutated_population:
				if brainfuck.evaluate(chromosome) is not None:
					valid_population.append(chromosome)
			print("propagated to next generation: " + str(len(valid_population)))
			self.population = self.generate_population(valid_population)
			self.generation += 1
 def mutation(self, selected_offsprings):
     offsprings = []
     for offspring in selected_offsprings:
         valid = False
         mutation_attempts = 0
         offspring_mutation = copy.deepcopy(offspring)
         while not valid and mutation_attempts < MAX_MUTATION_ATTEMPTS:
             for i in range(0, len(offspring_mutation)):
                 if np.random.choice([True, False],
                                     p=[MUTATION_RATE, 1 - MUTATION_RATE]):
                     action_type = random.randint(0, 2)
                     if action_type == 0 and len(
                             offspring_mutation
                     ) < PROGRAM_LENGTH_UPPER_BOUND:
                         # Inserting random value at index
                         offspring_mutation = offspring_mutation[:i] + random.choice(
                             AVAILABLE_OPS) + offspring_mutation[i:]
                     elif action_type == 1 and len(
                             offspring_mutation
                     ) > PROGRAM_LENGTH_LOWER_BOUND:
                         # Removing value at index
                         offspring_mutation = offspring_mutation[:
                                                                 i] + offspring_mutation[
                                                                     i + 1:]
                     else:
                         # Setting random value at index
                         offspring_mutation = self.set_value_at_index(
                             offspring_mutation,
                             random.choice(AVAILABLE_OPS), i)
             if brainfuck.evaluate(offspring_mutation) is not None:
                 valid = True
                 offsprings.append(offspring_mutation)
             mutation_attempts += 1
     return offsprings
	def generate_population(self, population):
		while len(population) < POPULATION:
			length = random.randint(PROGRAM_LENGTH_LOWER_BOUND, PROGRAM_LENGTH_UPPER_BOUND)
			chromosome = ""
			for i in range(0, length):
				chromosome += random.choice(AVAILABLE_OPS)
			if brainfuck.evaluate(chromosome) is not None: # We don't want programs that are syntactically incorrect
				print(chromosome)
				exit()
				population.append(chromosome)
		return population