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  }