github.com/RH12503/Triangula@v1.2.0/algorithm/modifiedgenetic.go (about) 1 package algorithm 2 3 import ( 4 "github.com/RH12503/Triangula/algorithm/evaluator" 5 "github.com/RH12503/Triangula/fitness" 6 "github.com/RH12503/Triangula/mutation" 7 "github.com/RH12503/Triangula/normgeom" 8 "github.com/panjf2000/ants/v2" 9 "sort" 10 "time" 11 ) 12 13 // An modifiedGenetic is a genetic algorithm with a modified crossover process. 14 // It runs in parallel, scaling well across multiple cores. 15 // 16 // Simply put, during each generation a set number (the cutoff value) of members of the population with the highest fitnesses are guaranteed to survive. 17 // These members are called "bases", and all other members are mutated versions of the bases. 18 // At the end, for each base, a copy is created and mutated with all the beneficial mutations found for that base. 19 // 20 // For a full explanation on how the algorithm works see: https://github.com/RH12503/Triangula/wiki/Explanation-of-the-algorithm. 21 type modifiedGenetic struct { 22 evaluator evaluator.Evaluator // Contains the fitness function(s) used to calculate fitnesses. 23 mutator mutation.Method // Used in newGeneration to mutate members of the population. 24 25 population []normgeom.NormPointGroup // The population of the algorithm. 26 newPopulation []normgeom.NormPointGroup // Used in newGeneration to generate a new generation from the previous population. 27 28 fitnesses []FitnessData // fitnesses[i] is the fitness of population[i]. 29 30 mutations [][]mutation.Mutation // Stores the mutations made in newGeneration so beneficial mutations can later be combined in combineMutations. 31 32 beneficialMutations []MutationsData // Stores the beneficial mutations for each base. 33 34 best normgeom.NormPointGroup // The member of the population with the highest fitness. 35 36 cutoff int // The number of members that are guaranteed to survive to the next generation. 37 38 stats Stats // Simple statistics relating to the algorithm. 39 } 40 41 func (g *modifiedGenetic) Step() { 42 t := time.Now() // Measure the time taken for the generation 43 44 // Fill the population with new members 45 g.newGeneration() 46 47 // Calculate the fitnesses of the new population and combine all beneficial mutations made 48 49 g.calculateFitnesses() 50 g.combineMutations() 51 52 // Update fitnesses in preparation for the next generation 53 g.updateFitnesses() 54 55 g.stats.Generation++ 56 g.stats.TimeForGen = time.Since(t) 57 } 58 59 // newGeneration populates the generation with new members. 60 func (g *modifiedGenetic) newGeneration() { 61 i := 0 62 63 // newPopulation is filled with new members, and then is swapped with population (the old population) 64 65 // The bases are guaranteed to survive without any mutations 66 for ; i < g.cutoff; i++ { 67 g.newPopulation[i].Set(g.population[i]) 68 g.mutations[i] = g.mutations[i][:0] 69 } 70 71 // Fill the rest of the algorithm with mutated versions of the bases 72 // 73 // The population is only filled to len(g.population)-g.cutoff because there needs to be a member 74 // At the end for each base in order to combine beneficial mutations 75 for i < len(g.population)-g.cutoff { 76 // i = the member, j = the base of the member 77 for j := 0; j < g.cutoff && i < len(g.population)-g.cutoff; j++ { 78 g.mutations[i] = g.mutations[i][:0] // clear all previous mutations 79 g.newPopulation[i].Set(g.population[j]) 80 81 // The evaluator need to know the base of each member and any mutations made 82 83 g.evaluator.SetBase(i, j) 84 g.mutator.Mutate(g.newPopulation[i], func(mut mutation.Mutation) { 85 g.mutations[i] = append(g.mutations[i], mut) 86 }) 87 i++ 88 } 89 } 90 91 for i := range g.beneficialMutations { 92 g.beneficialMutations[i].Clear() // Clear all previous beneficial mutations 93 } 94 95 g.population, g.newPopulation = g.newPopulation, g.population 96 } 97 98 // calculateFitnesses calculates the fitnesses of the current generation. 99 func (g *modifiedGenetic) calculateFitnesses() { 100 ch := make(chan FitnessData, len(g.population)) // Buffered channel for performance 101 102 for i := 0; i < len(g.population)-g.cutoff; i++ { 103 i := i 104 p := g.population[i] 105 e := g.evaluator.Get(i) 106 // Workers calculate the fitness of each member 107 ants.Submit( 108 func() { 109 fit := e.Calculate(fitness.PointsData{ 110 Points: p, 111 Mutations: g.mutations[i], 112 }) 113 ch <- FitnessData{ 114 I: i, 115 Fitness: fit, 116 } 117 }, 118 ) 119 g.fitnesses[i].I = i // Assign an index to each fitness so it can be found after being sorted 120 } 121 122 g.evaluator.Prepare() 123 124 done := 0 125 for d := range ch { 126 g.fitnesses[d.I].Fitness = d.Fitness 127 g.evaluator.Update(d.I) 128 129 // If the new fitness of a member is higher than its base, that means its mutations were beneficial 130 if d.Fitness > g.fitnesses[g.getBase(d.I)].Fitness { 131 g.setBeneficial(d.I) 132 } 133 134 done++ 135 if done == len(g.population)-g.cutoff { // Wait till all the fitnesses are calculated 136 close(ch) 137 } 138 } 139 140 } 141 142 // setBeneficial adds the mutations of population[index] to beneficialMutations. 143 func (g *modifiedGenetic) setBeneficial(index int) { 144 base := g.getBase(index) 145 146 for _, m := range g.mutations[index] { 147 // Check if a mutation already exists for a given point (we can't have 2 mutations on one point) 148 found := false 149 foundIndex := -1 150 for i, o := range g.beneficialMutations[base].Mutations { 151 if m.Index == o.Index { 152 found = true 153 foundIndex = i 154 break 155 } 156 } 157 if !found { 158 // If there are no existing mutation, add it to the list of beneficial mutations 159 g.beneficialMutations[base].Mutations = append(g.beneficialMutations[base].Mutations, m) 160 g.beneficialMutations[base].Indexes = append(g.beneficialMutations[base].Indexes, index) 161 } else { 162 // If there is a duplicate mutation, check to see which one is more beneficial, and replace the other 163 // with this one if this one is more beneficial 164 other := g.beneficialMutations[base].Indexes[foundIndex] 165 if g.fitnesses[index].Fitness > g.fitnesses[other].Fitness { 166 g.beneficialMutations[base].Mutations[foundIndex] = m 167 g.beneficialMutations[base].Indexes[foundIndex] = index 168 } 169 } 170 } 171 } 172 173 // combineMutations combines all the beneficial mutations found for each base. 174 func (g *modifiedGenetic) combineMutations() { 175 // The members with the combined mutations are at the end 176 for i := len(g.population) - g.cutoff; i < len(g.population); i++ { 177 g.mutations[i] = g.mutations[i][:0] 178 base := g.getBase(i) 179 g.fitnesses[i].I = i 180 181 if g.beneficialMutations[base].Count() > 0 { 182 // If there are any beneficial mutations, set the member to its base and perform all the mutations 183 g.population[i].Set(g.population[base]) 184 g.evaluator.SetBase(i, base) 185 186 for _, m := range g.beneficialMutations[base].Mutations { 187 g.population[i][m.Index].X = m.New.X 188 g.population[i][m.Index].Y = m.New.Y 189 g.mutations[i] = append(g.mutations[i], m) 190 } 191 192 // Calculate the fitness of the new member 193 e := g.evaluator.Get(i) 194 fit := e.Calculate(fitness.PointsData{ 195 Points: g.population[i], 196 Mutations: g.mutations[i], 197 }) 198 g.fitnesses[i].Fitness = fit 199 g.evaluator.Update(i) 200 } else { 201 // If there aren't any beneficial mutations, leave the member out of the next generation by setting 202 // its fitness to 0 203 g.fitnesses[i].Fitness = 0 204 } 205 } 206 } 207 208 // updateFitnesses prepares the members with calculated fitnesses for the next generation. 209 func (g *modifiedGenetic) updateFitnesses() { 210 // Sort the population by fitness so g.population[0] has the highest fitness 211 sort.Sort(g) 212 213 g.best.Set(g.population[0]) 214 g.stats.BestFitness = g.fitnesses[0].Fitness 215 } 216 217 // getBase returns the base of a member given the index of that member. 218 func (g modifiedGenetic) getBase(index int) int { 219 return index % g.cutoff 220 } 221 222 func (g modifiedGenetic) Best() normgeom.NormPointGroup { 223 return g.best 224 } 225 226 func (g modifiedGenetic) Stats() Stats { 227 return g.stats 228 } 229 230 // Functions for sorting. 231 232 func (g modifiedGenetic) Len() int { 233 return len(g.fitnesses) 234 } 235 236 func (g modifiedGenetic) Less(i, j int) bool { 237 return g.fitnesses[i].Fitness > g.fitnesses[j].Fitness 238 } 239 240 func (g *modifiedGenetic) Swap(i, j int) { 241 g.fitnesses[i], g.fitnesses[j] = g.fitnesses[j], g.fitnesses[i] 242 g.population[i], g.population[j] = g.population[j], g.population[i] 243 g.evaluator.Swap(i, j) 244 } 245 246 // NewModifiedGenetic returns a new modifiedGenetic algorithm. 247 func NewModifiedGenetic(newPointGroup func() normgeom.NormPointGroup, size int, cutoff int, 248 newEvaluators func(n int) evaluator.Evaluator, mutator mutation.Method) *modifiedGenetic { 249 250 var algo modifiedGenetic 251 252 // Fill the population with point groups 253 for i := 0; i < size; i++ { 254 pg := newPointGroup() 255 algo.population = append(algo.population, pg) 256 algo.newPopulation = append(algo.newPopulation, pg.Copy()) 257 } 258 259 algo.best = algo.population[0].Copy() // Set a random best member to start off 260 261 algo.evaluator = newEvaluators(size) 262 263 algo.fitnesses = make([]FitnessData, len(algo.population)) 264 265 algo.mutations = make([][]mutation.Mutation, len(algo.population)) 266 algo.beneficialMutations = make([]MutationsData, cutoff) 267 268 algo.mutator = mutator 269 270 algo.cutoff = cutoff 271 272 // Calculate and update fitnesses in preparation for the first generation 273 algo.calculateFitnesses() 274 algo.updateFitnesses() 275 276 return &algo 277 }