class PSO(Timer, object): """A particle swarm class that contains methods for handling the population over iterations Attributes: There are not attributes for this class. All settings/attributes are read in from pso_settings.py which should be located in the same directory as this file """ def __init__(self, settings, function): # TODO add settings parameter super(self.__class__, self).__init__() # read in settings num_dims = settings['number_of_dimensions'] population_size = settings['population_size'] bounds = settings['bounds'] if settings['velocity_type'] == 'constriction': phi = max(settings['cp'] + settings['cg'], 4.0) self.k = 2.0 / abs(2.0 - phi - sqrt(phi * phi - 4.0 * phi)) else: self.k = 1 # check to make sure num_dims and number of bounds provided match if len(bounds) != num_dims: raise ValueError( "Number of dimensions doesn't match number of bounds provided") # set instance variables self.settings = settings self.function = function # initialize population self.population = PSO.__gen_population(bounds, population_size, function) self.total_population = population_size self.best_x = PSO.__get_best_particle(self.population) self.num_iterations = 1 if settings['plot']: try: self.plotutils = PlotUtils(num_dims, bounds, function) self.__plot_state() except ValueError: print("Can not plot more than 2 dimensions") settings['plot'] = False if settings['print_iterations']: self.__display_state() if settings['step_through']: oa_utils.pause() @staticmethod def __gen_particle(id, bounds, function): # use gen_random_numbers to get a list of positions within the bounds return Particle(id, oa_utils.gen_random_numbers(bounds), function) @staticmethod def __gen_population(bounds, size, function): b = bounds f = function # generate a list of organisms p = [PSO.__gen_particle(i + 1, b, f) for i in range(0, size)] return p ########################### ### PSO steps and loop ### ########################### @staticmethod def __update_velocity(population, velocity_type, print_actions, gbest, cp, cg, k, w): for p in population: if (velocity_type == 'normal'): p.velocity = PSO.__get_velocity(1, cp, cg, gbest, p, 1) elif (velocity_type == 'inertia'): p.velocity = PSO.__get_velocity(k, cp, cg, gbest, p, w) elif (velocity_type == 'constriction'): p.velocity = PSO.__get_velocity(k, cp, cg, gbest, p, 1) return population @staticmethod def __get_velocity(k, c1, c2, gbest, p, w): velocity_array = [] for i, v in enumerate(p.velocity): velocity_array.append( k * (w * v + c1 * random.random() * (p.pbest[i] - p.pos[i]) + c2 * random.random() * (gbest[i] - p.pos[i]))) return velocity_array @staticmethod def __update_position( population): # TODO put bounds on what position can be updated to for p in population: p.pos = list(map(add, p.pos, p.velocity)) p.fval = p.get_fval() return population @staticmethod def __get_best_particle(population): return copy.deepcopy(min(population, key=attrgetter('fval'))) def __display_state(self): print("The best organism in generation %d is %s" \ % (self.num_generations, str(self.get_best_x()))) def __plot_state(self): pts = [(organism.pos[0], organism.pos[1]) for organism in self.population] self.plotutils.plot(pts) def __str__(self): return "Best Fitness: %8.4f by particle %s" % \ (self.get_best_f(), str(self.get_best_x())) #################################### # These are the only methods that # # should be called outside of this # # class # #################################### def get_best_x(self): return self.best_x def get_best_f(self): return self.best_x.fval def do_loop(self): population = self.population population = PSO.__update_velocity(population, \ self.settings['velocity_type'], \ self.settings['print_actions'], \ self.get_best_x().pos, \ self.settings['cp'], \ self.settings['cg'], \ self.k, \ self.settings['weight']) if self.settings['cg_plus']: self.settings['cg'] += 0.1 phi = max(self.settings['cp'] + self.settings['cg'], 4.0) self.k = 2.0 / abs(2.0 - phi - sqrt(phi * phi - 4.0 * phi)) population = PSO.__update_position(population) self.num_iterations += 1 self.population = population current_best = PSO.__get_best_particle(self.population) if current_best.get_fval() < self.best_x.get_fval(): self.best_x = current_best if self.settings['plot']: self.__plot_state() if self.settings['print_iterations']: self.__display_state() if self.settings['step_through']: oa_utils.pause() def run(self): # iterate over generations while self.settings['num_iterations'] > self.num_iterations: self.do_loop() time.sleep(self.settings['time_delay']) @staticmethod def get_name(): return "Particle Swarm"
class GA(Timer, object): """A genetic algorithm class that contains methods for handling the population over generations/iterations Attributes: There are not attributes for this class. All settings/attributes are read in from ga_settings.py which should be located in the same directory as this file NOTE: The GA methods assume the population array is sorted """ def __init__(self, settings, function): # TODO add settings parameter super(self.__class__, self).__init__() # read in settings num_dims = settings['number_of_dimensions'] population_size = settings['population_size'] bounds = settings['bounds'] # check to make sure num_dims and number of bounds provided match if len(bounds) != num_dims: raise ValueError("Number of dimensions doesn't match number of bounds provided") # set instance variables self.settings = settings self.function = function # initialize population self.population = GA.__gen_population(bounds, population_size, function) self.total_organisms = len(self.population) self.best_x = self.population[0] self.num_generations = 1 # stopping criteria variables self.func_val_improvement = 0 self.num_iter_since_improvement = 0 if settings['plot']: try: self.plotutils = PlotUtils(num_dims, bounds, function) self.__plot_state() except ValueError: print("Can not plot more than 2 dimensions") settings['plot'] = False if settings['print_iterations']: self.__display_state() if settings['step_through']: oa_utils.pause() # def __del__(self): # del(self.plotutils) # @staticmethod def __gen_organism(id, bounds, function): # use gen_random_numbers to get a list of positions within the bounds return Organism(id, oa_utils.gen_random_numbers(bounds), function) @staticmethod def __gen_population(bounds, size, function): b = bounds f = function # generate a list of organisms p = [GA.__gen_organism(i+1, b, f) for i in range(0, size)] return GA.__sort_population(p) @staticmethod def __sort_population(p): return sorted(p, key=lambda o: o.fitness) ########################### ### GA steps and loop ### ########################### ''' Three possible ways of doing this. 1. have a setting that says we kill of last 20% of array or population 2. the further you are down the array the higher your probability of dieing 3. kill off the worst based on their distance from the best TODO write a test for this. simple 10 population w/ .5 cutoff test will do ''' @staticmethod def __selection(population, cutoff, print_action=False): size = len(population) max_f = population[0].fitness min_f = population[size-1].fitness # denominator in probability of surviving den = (max_f - min_f) # if den == 0: # print("Every organism has same objective function value.") for (i, organism) in enumerate(population): f = organism.fitness # check for division by zero if den == 0: normalized_f = 0 else: # get normalized value normalized_f = float(f - min_f) / den if normalized_f > cutoff: # delete the organism from the population del population[i] if print_action: print("Selection: Deleting organism %s" % str(organism)) return population @staticmethod def __get_parent_index(cdf_value, arr): norm_sum = 0 for i, o in enumerate(arr): norm_sum += o['probability'] if norm_sum >= cdf_value: return i return -1 @staticmethod def __mate_parents(id, parent1, parent2, function): n = len(parent1.pos) # randomly choose split position split = random.randint(0, n-1) # split parent positions pos1 = parent1.pos[0:split] + parent2.pos[split:] pos2 = parent2.pos[0:split] + parent1.pos[split:] # get id numbers id1 = id + 1 id2 = id + 2 # return the two newly created organisms return (Organism(id1, pos1, function), Organism(id2, pos2, function)) """ population: population size: size that the population should be after crossover NOTE: population must be sorted. crossover will return an unsorted array of the new population. """ @staticmethod def __crossover(id, population, size, function, print_action=False): new_population = [] length = len(population) max_f = population[length-1].fitness min_f = population[0].fitness den = max_f - min_f # if size is odd if size % 2 == 1: raise ValueError("Populations with an odd size hasn't been implemented. Talk to Jesse") # get inversed normalized values of fitness # normalized value of 1 is the best. 0 is the worst probabilities = [] normalized_sum = 0.0 for o in population: if den == 0: normalized_f = 1 else: normalized_f = (max_f - o.fitness)/den normalized_sum += normalized_f probabilities.append({'normalized_f': normalized_f}) # calculate weight of each normalized value for i, p in enumerate(probabilities): probabilities[i]['probability'] = probabilities[i]['normalized_f']/normalized_sum # generate new population while len(new_population) < size: # get cdf input values cdf1 = random.random() cdf2 = random.random() # get index of parent from output of cdf i = GA.__get_parent_index(cdf1, probabilities) j = GA.__get_parent_index(cdf2, probabilities) # mate parents child1, child2 = GA.__mate_parents(id, population[i], population[j], function) id += 2 # append children to new_population new_population.extend((child1, child2)) if print_action: for organism in new_population: print("Crossover: New oganism %s" % str(organism)) return new_population @staticmethod def __mutation(population, bounds, rate, max_mutation_amount, print_action=False): for organism in population: if random.random() < rate: new_pos = [] # for each dimension for i in range(0, len(bounds)): # take some percentage of the max mutation amount x = random.uniform(0.01, 1.00) delta_pos = (-1.0*log(1-x))*max_mutation_amount # should we go positive or negative if random.randint(0,1) == 1: delta_pos = -1.0*delta_pos new_dim_pos = organism.pos[i] + delta_pos # cap where we can go if we are beyond the bounds of the design space if new_dim_pos < bounds[i][0]: new_dim_pos = bounds[i][0] elif new_dim_pos > bounds[i][1]: new_dim_pos = bounds[i][1] new_pos.append(new_dim_pos) if print_action: new_pos_str = "[" for x in new_pos: new_pos_str += "%6.3f " % x new_pos_str += "]" print("Mutation: Moving organism %s to %s" % \ (str(organism), new_pos_str)) organism.pos = new_pos organism.fitness = organism.get_fval() return population def __display_state(self): print("The best organism in generation %d is %s" \ % (self.num_generations, str(self.get_best_x()))) def __plot_state(self): pts = [(organism.pos[0], organism.pos[1]) for organism in self.population] self.plotutils.plot(pts) def __str__(self): return "Iteration %d Best Fitness: %8.4f by organism %s" % \ (self.num_generations, self.get_best_f(), str(self.get_best_x())) #################################### # These are the only methods that # # should be called outside of this # # class # #################################### def get_best_x(self): return self.best_x def get_best_f(self): return self.best_x.fitness def do_loop(self): population = self.population population = GA.__selection(population, \ self.settings['selection_cutoff'], \ self.settings['print_actions']) population = GA.__crossover(self.total_organisms, \ population, \ self.settings['population_size'], \ self.function, \ self.settings['print_actions']) self.total_organisms += len(population) population = GA.__mutation(population, \ self.settings['bounds'], \ self.settings['mutation_rate'], \ self.settings['max_mutation_amount'], \ self.settings['print_actions']) self.population = GA.__sort_population(population) self.num_generations += 1 if self.population[0].fitness < self.best_x.fitness: # add on the improvement in function value self.func_val_improvement += (self.best_x.fitness - self.population[0].fitness) self.best_x = self.population[0] if self.settings['plot']: self.__plot_state() if self.settings['print_iterations']: self.__display_state() if self.settings['step_through']: oa_utils.pause() def run(self): # iterate over generations while self.settings['num_iterations'] > self.num_generations: self.do_loop() # check if we've improved our function value if self.func_val_improvement > self.settings['stopping_criteria']: self.func_val_improvement = 0 self.num_iter_since_improvement = 0 else: self.num_iter_since_improvement += 1 # check if we haven't improved at all in num of stopping criteria steps if self.num_iter_since_improvement > self.settings['num_iter_stop_criteria']: if self.settings['print_actions'] or self.settings['print_iterations']: print("Stopping criteria met after %d number of iterations" % self.num_generations) break # pause for a bit if setting is set time.sleep(self.settings['time_delay']) if self.num_generations > self.settings['num_iterations']: if self.settings['print_actions'] or self.settings['print_iterations']: print("Maximum number of iterations hit (%d)" % self.num_generations) @staticmethod def get_name(): return "Genetic Algorithm"
# you could import settings from a separate file like so from settings import settings # plot util variable. probably make this an instance # variable in a class plotutils = None if settings['plot']: try: # Create PlotUtils instance # 2 params. number of dimensions and an array of 2D lists with # the bounds for each dimension. ex [(-10,10), (-10,10)] plotutils = PlotUtils(settings['num_dims'], settings['bounds']) except ValueError: print("Can not plot more than 2 dimensions!") # set this to false so that we don't try to use # the plotutils variable later on settings['plot'] = False # data should be an array of 2D lists with x1 and x2 data data = [(1, 1), (8, 4), (-4, -9)] # open or update plot if settings['plot']: plotutils.plot(data) # you can put plotutils.plot(data) in a loop and continually update # the plot. Here I just wait for you to press enter, after which the # plot will close raw_input("Press Enter to Exit") # Python 2 user input