Path: blob/main/Lessons/Lesson 07 - Global Optimization 2/GAUtilities.py
871 views
import importlib1import numpy as np23def computeFitness(f, pop, **kwargs):4'''5Computes fitness based on passed in function.67Parameters:8f (function, required): the function used to evaluate fitness9pop (numpy array, required): the population on which to evaluate fitness - individuals in rows.10**kwargs (named arguments, optional): additional arguments that will be passed through to the fitness function1112Returns a numpy array of computed fitnesses.13'''14#get sizes from pop15pop_size, ind_size = pop.shape[0], pop.shape[1]1617#create the fitness array of zeros18fitness = np.zeros(pop_size)19#fill the fitness array20for j in range(pop_size):21fitness[j] = f(pop[j], **kwargs)2223return fitness2425# create population26def createPop(pop_size, ind_size, type = "float", low = 0, high = 10):27'''28Creates a population of random individuals as rows in a numpy array. Bounds are ignored for boolean and permuation types.2930Parameters:31pop_size (integer, required): number of individuals32ind_size (integer, required): number of genes in each individual33type (string, default is float): must be permutation, boolean, integer, or float34low (float or integer, default is 0): lower bound for integer or float genes35high (float or integer, default is 10): upper bound for integer or float genes (contains upper bound for ints)36'''3738if type == "float":39pop = np.random.uniform(low, high, size = (pop_size, ind_size))40elif type == "integer":41pop = np.random.randint(low, high + 1, size = (pop_size, ind_size), dtype = int)42elif type == "boolean":43pop = np.random.randint(0, 2, size = (pop_size, ind_size) ).astype(bool)44elif type == "permutation":45pop = np.zeros( (pop_size, ind_size), dtype = 'int')46for k in range(pop_size):47pop[k] = np.random.permutation( ind_size )48else:49print("type must be integer, boolean, permutation, or float")50pop = NaN5152return pop5354# Sort population55def sortPop(pop, fitness):56'''57Sorts a population with minimal fitness in first place.5859Parameters:60pop (numpy array, required): The population to sort, individuals in rows61fitness (numpy array, required): The values used to sort the population6263Returns:64Sorted numpy array of population65'''66#sort by increasing fitness67sorted_pos = fitness.argsort()68fitness = fitness[sorted_pos]69pop = pop[sorted_pos]70return pop.copy()7172####################################73# Selection operators74####################################7576# tournament selection77def tournamentSelection(pop, tourn_size, debug=False):78'''79Implements tournameent selection on a population.8081Parameters:82pop (numpy array, required): The sorted population from which selections will be drawn.83tourn_size (between 2 and population size, required): The number of individuals that will compete in each tournament84debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.8586Returns:87Numpy Array of the selected population.8889'''90#get the population size91pop_size, ind_size = pop.shape[0], pop.shape[1]9293# initialize selected population94select_pop = np.zeros((pop_size,ind_size))95for j in range(pop_size):96subset_pos = np.random.choice(pop_size,tourn_size,replace=False) # select without replacement97smallest_pos = np.min(subset_pos) # choose index corresponding to lowest fitness98if debug:99print('Individuals in tournament:', subset_pos)100print('Individual selected:', smallest_pos)101select_pop[j] = pop[smallest_pos]102return select_pop103104####################################105# Crossover operators106####################################107108def orderedCrossover(pop, cx_prob, debug=False):109'''110Peforms ordered crossover on permutation populations.111112Parameters:113pop (numpy array of permutations, required): The population of permutations, individuals as rows114cx_prob (real between 0 and 1, required): The probability that any two individuals will mate115debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.116117Returns:118Population of integers119'''120#get the sizes from population121pop_size, ind_size = pop.shape[0], pop.shape[1]122cx_pop = np.zeros((pop_size,ind_size)) # initialize crossover population123for j in range(int(pop_size/2)): # pop_size must be even124parent1, parent2 = pop[2*j], pop[2*j+1]125child1, child2 = parent2.copy(), parent1.copy()126if np.random.uniform() < cx_prob: # crossover occurs127swap_idx = np.sort(np.random.randint(0,ind_size,2))128hole = np.full( ind_size, False, dtype = bool)129hole[swap_idx[0]:swap_idx[1]+1] = True130child1[~hole] = np.array([x for x in parent1 if x not in parent2[hole]])131child2[~hole] = np.array([x for x in parent2 if x not in parent1[hole]])132if debug:133print("Crossover happened for individual", j)134print('Parent 1', parent1)135print('Parent 2', parent2)136print('Swap Index:', swap_idx)137print('Hole', hole)138print('Child1', child1)139print('Child2', child2)140cx_pop[2*j] = child1141cx_pop[2*j+1] = child2142return cx_pop.astype(int)143144145def onePointCrossover(pop, cx_prob, debug=False):146'''147Peforms one-point crossover on integer, boolean or real populations.148149Parameters:150pop (numpy array, required): The population, individuals as rows151cx_prob (real between 0 and 1, required): The probability that any two individuals will mate152debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.153154Returns:155Population (will return as reals, should be cast if integer or boolean is desired)156'''157# one-point crossover (mating)158#get the sizes from pop159pop_size, ind_size = pop.shape[0], pop.shape[1]160cx_pop = np.zeros((pop_size,ind_size)) # initialize crossover population161for j in range(int(pop_size/2)): # pop_size must be even162parent1, parent2 = pop[2*j], pop[2*j+1]163child1, child2 = parent1.copy(), parent2.copy()164if np.random.uniform() < cx_prob: # crossover occurs165cx_point = np.random.randint(1,ind_size) # crossover point between 1 and ind_size166if debug:167print('Crossover happened between Individuals', 2*j, 'and', 2*j+1, 'at point', cx_point)168child1[0:cx_point], child2[0:cx_point] = parent2[0:cx_point], parent1[0:cx_point]169cx_pop[2*j] = child1170cx_pop[2*j+1] = child2171return cx_pop172173def blendedCrossover(pop, cx_prob, alpha, bounds, debug=False):174'''175Peforms blended crossover on real populations.176177Parameters:178pop (numpy array, required): The population, individuals as rows179cx_prob (real between 0 and 1, required): The probability that any two individuals will mate180alpha (real, required): the amount of expansion181bounds (list, required): the [lower, upper] bounds of possible values for each gene182debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.183184Returns:185Population of real variables186'''187#get individual size and population size188pop_size, ind_size = pop.shape[0], pop.shape[1]189cx_pop = np.zeros((pop_size,ind_size)) # initialize crossover population190for j in range(int(pop_size/2)): # pop_size must be even191parent1, parent2 = pop[2*j], pop[2*j+1]192child1, child2 = parent1.copy(), parent2.copy()193if np.random.uniform() < cx_prob: # crossover occurs194if debug:195print('Crossover occurred between', 2*j, 'and', 2*j+1)196for i, (x1, x2) in enumerate(zip(child1, child2)):197l = min(x1,x2)198r = max(x1,x2)199bb = np.clip(np.random.uniform(l-alpha*(x2-x1),r+alpha*(x2-x1),size=2),bounds[0],bounds[1])200child1[i] = bb[0]201child2[i] = bb[1]202cx_pop[2*j] = child1203cx_pop[2*j+1] = child2204return cx_pop205206207208####################################209# Mutation operators210####################################211def gaussianMutation(pop, mut_prob, ind_prob, bounds, sigma, debug=False):212'''213Peforms gaussian mutation on real populations.214215Parameters:216pop (numpy array, required): The population, individuals as rows217mut_prob (real between 0 and 1, required): The probability that any individual will mutate218ind_prob (real between 0 and 1, required): The probability that a gene will mutate219bounds (list, required): the [lower, upper] bounds of possible values for each gene220sigma (real, required): standard deviation used to generate new mutations221debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.222223Returns:224Population of real variables225'''226#get individual size and population size227pop_size, ind_size = pop.shape[0], pop.shape[1]228mut_pop = np.zeros((pop_size,ind_size)) # initialize mutation population229for j in range(pop_size):230individual = pop[j].copy() # copy is necessary to avoid conflicts in memory231if np.random.uniform()<mut_prob:232if debug:233print("Mutation probability met for individual", j)234individual = individual + np.random.normal(0,sigma,ind_size)*(np.random.uniform(size=ind_size)<ind_prob)235individual = np.maximum(individual,bounds[0]) # clip to lower bound236individual = np.minimum(individual,bounds[1]) # clip to upper bound237mut_pop[j] = individual.copy() # copy is necessary to avoid conflicts in memory238return mut_pop239240241def uniformIntMutation(pop, mut_prob, ind_prob, bounds, debug=False):242'''243Peforms uniform integer mutation on integer populations.244245Parameters:246pop (numpy array, required): The population, individuals as rows247mut_prob (real between 0 and 1, required): The probability that any individual will mutate248ind_prob (real between 0 and 1, required): The probability that a gene will mutate249bounds (list, required): the [lower, upper] bounds of possible values for each gene250debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.251252Returns:253Population of integer variables254'''255mut_pop = pop.copy()256pop_size, ind_size = pop.shape[0], pop.shape[1]257for j in range(pop_size):258if np.random.uniform()<mut_prob:259new_assign = mut_pop[j].copy()260for i in range(ind_size):261if np.random.uniform() < ind_prob:262if debug:263print('Gene', i, 'in Individual', j, 'mutated.')264while new_assign[i] == mut_pop[j][i]: # loops until new and old are different265new_assign[i] = np.random.randint(bounds[0], bounds[1])266mut_pop[j] = new_assign267return mut_pop.astype(int)268269270def bitFlipMutation(pop, mut_prob, ind_prob, debug=False):271'''272Peforms bit-flipping mutation on boolean populations.273274Parameters:275pop (numpy array, required): The population, individuals as rows276mut_prob (real between 0 and 1, required): The probability that any individual will mutate277ind_prob (real between 0 and 1, required): The probability that a gene will mutate278debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.279280Returns:281Population of boolean variables282'''283284#get sizes285pop_size, ind_size = pop.shape[0], pop.shape[1]286mut_pop = np.zeros((pop_size,ind_size),dtype=bool) # initialize mutation population287for j in range(pop_size):288individual = pop[j].copy() # copy is necessary to avoid conflicts in memory289if np.random.uniform()<mut_prob:290for i in range(ind_size):291if np.random.uniform() < ind_prob:292if debug:293print('Gene', i, 'in Individual', j, 'flipped.')294individual[i] = ~individual[i]295mut_pop[j] = individual.copy() # copy is necessary to avoid conflicts in memory296return mut_pop.astype(bool)297298def shuffleMutation(pop, mut_prob, ind_prob, debug=False):299'''300Peforms index shuffling mutation on permutation populations.301302Parameters:303pop (numpy array, required): The population, individuals as rows304mut_prob (real between 0 and 1, required): The probability that any individual will mutate305ind_prob (real between 0 and 1, required): The probability that a gene will mutate306debug (boolean, optional, default=False): Flag to indicate whether to output debugging print statements.307308Returns:309Population of permutation integer variables310'''311#get sizes312pop_size, ind_size = pop.shape[0], pop.shape[1]313if debug:314print('Pop size:', pop_size)315print('Individual size:', ind_size)316mut_pop = np.zeros((pop_size,ind_size),dtype=int) # initialize mutation population317for j in range(pop_size):318individual = pop[j].copy() # copy is necessary to avoid conflicts in memory319if np.random.uniform()<mut_prob:320for k in range(ind_size):321if np.random.uniform() < ind_prob:322swap = np.random.randint(ind_size)323if debug:324print('Gene', k, 'in Individual', j, 'swapped with', swap)325print('Individual before\n', individual)326individual[k],individual[swap] = individual[swap],individual[k]327if debug:328print('Individual after\n', individual)329mut_pop[j] = individual.copy() # copy is necessary to avoid conflicts in memory330return mut_pop.astype(int)331332333def flipSegmentsMutation(pop, mut_prob, ind_prob, debug=False):334'''335Performs multiple random segment reversal on permutation populations336337Parameters:338pop (numpy array, required): The population, individuals339mut_prob (real between 0 and 1, required): The probability an individual will mutate340ind_prob (real between 0 and 1, required): The probability a gene will be part of a segment reversal341'''342pop_size, ind_size = pop.shape[0], pop.shape[1]343mut_pop = np.zeros((pop_size,ind_size),dtype=int)344for k in range(pop_size):345individual = pop[k].copy() # make a copy to avoid conflicts346if np.random.uniform() < mut_prob:347num_swaps = np.random.binomial(ind_size,ind_prob)348for m in range(num_swaps): # choose now many swaps349i, j = np.sort(np.random.choice(ind_size, 2, replace=False))350individual = np.concatenate((individual[0:i], individual[j:-ind_size + i - 1:-1],351individual[j + 1:ind_size]))352mut_pop[k] = individual.copy()353return mut_pop.astype(int)354355# Elitism356def addElitism(initPop, mutPop, num_elite):357'''358Peforms elitism by pulling in num_elite best individuals from initPop to mutPop.359360Parameters:361initPop (numpy array, required): The population from the beginning of the loop, individuals as rows362mutPop (numpy array, required): The population post-mutation.363364Returns:365Population numpy array population366'''367pop_size = initPop.shape[0]368initPop[(num_elite):pop_size] = mutPop[(num_elite):pop_size].copy()369return initPop370371###############################372# Stats Tracking373###############################374375def initStats(fitness, pop, num_iter):376'''377Sets up initial stats tracking378379Parameters:380fitness (numpy array, required): The current fitness at the start of the algorithm381pop (numpy array, required): The population for which we are tracking fitness382num_iter (integer, required): The number of iterations we'll track.383384Returns:385stats (numpy array)386best_fitness (real)387best_x (individual)388'''389stats = np.zeros((num_iter+1,3)) # for collecting statistics390#get the initial best fitness391best_fitness = min(fitness)392min_fitness = min(fitness) # best for this iteration393index = np.argmin(fitness)394#set the initial best_x395best_x = pop[index]396# initialize stats and output397stats[0,:] = np.array([0,best_fitness, best_fitness])398print('Iteration | Best this iter | Best ever')399return stats, best_fitness, best_x400401402def updateStats(stats, fitness, best_x, pop, iter, update_iter):403'''404Updates stats tracking405406Parameters:407stats (numpy array, required): The stats that have been collected so far408fitness (numpy array, required): The current fitness at the start of the algorithm409best_x (numpy array individual, required): The current best individual410pop (numpy array, required): The population for which we are tracking fitness411iter (integer, required): The current iteration we are on.412update_iter (integer, required): How often to display stats413414Returns:415stats (numpy array)416best_fitness (real)417best_x (individual)418'''419# collect stats and output to screen420min_fitness = min(fitness) # best for this iteration421422#get stats less than this iteration423snipped_stats = stats[0:iter]424if len(snipped_stats) > 0:425index = np.argmin(snipped_stats[:,2])426best_fitness = min(snipped_stats[:,1])427else:428best_fitness = min_fitness429index = np.argmin(fitness)430best_x = pop[index]431432if min_fitness < best_fitness: # best for all iterations433best_fitness = min_fitness434index = np.argmin(fitness)435best_x = pop[index]436437stats[iter+1,:] = np.array([iter+1,min_fitness, best_fitness])438if (iter+1) % update_iter == 0 or (iter+1) ==1 :439print(f"{stats[iter+1,0]:9.0f} | {stats[iter+1,1]:14.3e} | {stats[iter+1,2]:12.3e}")440441442return stats, best_fitness, best_x443444445446447