// Copyright 2023 wanderer // SPDX-License-Identifier: GPL-3.0-or-later package de import ( "log" "os" "sort" "time" "golang.org/x/exp/rand" "git.dotya.ml/wanderer/math-optim/bench" "git.dotya.ml/wanderer/math-optim/stats" "gonum.org/v1/gonum/stat/distuv" ) // JDE is a holder for the settings of an instance of a self-adapting // differential evolution (jDE) algorithm. type JDE struct { // Generations denotes the number of generations the population evolves // for. Special value -1 disables limiting the number of generations. Generations int // BenchMinIters is the number of iterations the bench function will be re-run (for statistical purposes). BenchMinIters int // Dimensions to solve the problem for. Dimensions []int // F is the differential weight (mutation/weighting factor). F float64 // CR is the crossover probability constant. CR float64 // MutationStrategy selects the mutation strategy, i.e. the variant of the // jDE algorithm (0..17), see mutationStrategies.go for more details. MutationStrategy int // AdptScheme is the parameter self-adaptation scheme (0..1). AdptScheme int // NP is the initial population size. NP int // BenchName is a name of the problem to optimise. BenchName string // ch is a channel for writing back computed results. ch chan []stats.Stats // chAlgoMeans is a channel for writing back algo means. chAlgoMeans chan *stats.AlgoBenchMean // initialised denotes the initialisation state of the struct. initialised bool } const ( // jDEMinNP is the minimum size of the initial population for jDE. jDEMinNP = 4 // fMin is the minimum allowed value of the differential weight. fMin = 0.5 // fMax is the maximum allowed value of the differential weight. fMax = 2.0 // crMin is the minimum allowed value of the crossover probability constant. crMin = 0.2 // crMax is the maximum allowed value of the crossover probability constant. crMax = 0.9 ) // jDELogger declares and initialises a "custom" jDE logger. var jDELogger = log.New(os.Stderr, " *** δ jDE:", log.Ldate|log.Ltime|log.Lshortfile) // Init initialises the jDE algorithm, performs sanity checks on the inputs. func (j *JDE) Init(generations, benchMinIters, mutStrategy, adptScheme, np int, f, cr float64, dimensions []int, bench string, ch chan []stats.Stats, chAlgoMeans chan *stats.AlgoBenchMean) { if j == nil { jDELogger.Fatalln("jDE needs to be initialised before calling RunjDE, exiting...") } // check input parameters. switch { case generations == 0: jDELogger.Fatalln("Generations cannot be 0, got", generations) case generations == -1: jDELogger.Println("Generations is '-1', disabling generation limits..") case benchMinIters < 1: jDELogger.Fatalln("Minimum bench iterations cannot be less than 1, got:", benchMinIters) case mutStrategy < 0 || mutStrategy > 17: jDELogger.Fatalln("Mutation strategy needs to be from the interval <0; 17>, got", mutStrategy) case adptScheme < 0 || adptScheme > 1: jDELogger.Fatalln("Parameter self-adaptation scheme needs to be from the interval <0; 1>, got", adptScheme) case np < jDEMinNP: jDELogger.Fatalf("NP cannot be less than %d, got: %d\n.", jDEMinNP, np) case f < fMin || f > fMax: jDELogger.Fatalf("F needs to be from the interval <%f;%f>, got: %f\n.", fMin, fMax, f) case cr < crMin || cr > crMax: jDELogger.Fatalf("CR needs to be from the interval <%f;>%f, got: %f\n.", crMin, crMax, cr) case len(dimensions) == 0: jDELogger.Fatalf("Dimensions cannot be empty, got: %+v\n", dimensions) case bench == "": jDELogger.Fatalln("Bench cannot be empty, got:", bench) } j.Generations = generations j.BenchMinIters = benchMinIters j.MutationStrategy = mutStrategy j.AdptScheme = adptScheme j.NP = np j.F = f j.CR = cr j.Dimensions = dimensions j.BenchName = bench j.ch = ch j.chAlgoMeans = chAlgoMeans j.initialised = true } // InitAndRun initialises the jDE algorithm, performs sanity checks on the // inputs and calls the Run method. func (j *JDE) InitAndRun(generations, benchMinIters, mutStrategy, adptScheme, np int, f, cr float64, dimensions []int, bench string, ch chan []stats.Stats, chAlgoMeans chan *stats.AlgoBenchMean) { if j == nil { jDELogger.Fatalln("jDE is nil, NewjDE() needs to be called first. exiting...") } j.Init(generations, benchMinIters, mutStrategy, adptScheme, np, f, cr, dimensions, bench, ch, chAlgoMeans) j.Run() } // Run self-adapting differential evolution algorithm. func (j *JDE) Run() { if j == nil { jDELogger.Fatalln("jDE is nil, NewjDE() needs to be called first. exiting...") } if !j.initialised { jDELogger.Fatalln("jDE needs to be initialised before calling Run(), exiting...") } var jDEStats []stats.Stats jDEMeans := &stats.AlgoBenchMean{ Algo: "jDE", BenchMeans: make([]stats.BenchMean, 0, len(j.Dimensions)), } // run evolve for for all dimensions. for _, dim := range j.Dimensions { maxFES := bench.GetGAMaxFES(dim) fesPerIter := int(float64(maxFES / j.NP)) jDEStatDimX := &stats.Stats{ Algo: "jDE", Dimens: dim, Iterations: j.BenchMinIters, Generations: maxFES, NP: j.NP, F: j.F, CR: j.CR, } funcStats := &stats.FuncStats{BenchName: j.BenchName} dimXMean := &stats.BenchMean{ Bench: j.BenchName, Dimens: dim, Iterations: j.BenchMinIters, Generations: maxFES, Neighbours: -1, } benchFuncParams := bench.FunctionParams[j.BenchName] uniDist := distuv.Uniform{ Min: benchFuncParams.Min(), Max: benchFuncParams.Max(), } // jDELogger.Printf("running bench \"%s\" for %dD, maxFES: %d\n", // j.BenchName, dim, maxFES, // ) funcStats.BenchResults = make([]stats.BenchRound, j.BenchMinIters) // rand.Seed(uint64(time.Now().UnixNano())) // create and seed a source of preudo-randomness src := rand.NewSource(uint64(rand.Int63())) // track execution duration. start := time.Now() for iter := 0; iter < j.BenchMinIters; iter++ { jDELogger.Printf("run: %d, bench: %s, %dD, started at %s", iter, j.BenchName, dim, start, ) funcStats.BenchResults[iter].Iteration = iter uniDist.Src = src // create a population with known params. pop := newPopulation(j.BenchName, j.NP, dim) // set population seed. pop.Seed = uint64(time.Now().UnixNano()) // initialise the population. pop.Init() // jDELogger.Printf("%+v\n", pop.Population) var bestResult float64 // the core. for i := 0; i < fesPerIter; i++ { r := j.evaluate(pop) // distinguish the first or any of the subsequent iterations. switch i { case 0: bestResult = r default: // call evolve where jDE runs. j.evolve(pop, &uniDist) // take measurements of current population fitness. r = j.evaluate(pop) // save if better. if r < bestResult { bestResult = r } } funcStats.BenchResults[iter].Results = append( funcStats.BenchResults[iter].Results, bestResult, ) // this block makes sure we properly count func evaluations for // the purpose of correctly comparable plot comparison. i.e. // append the winning (current best) value NP-1 (the first best // is already saved at this point) times to represent the fact // that while evaluating (and comparing) other population // individuals to the current best value is taking place in the // background, the current best value itself is kept around and // symbolically saved as the best of the Generation. for x := 0; x < j.NP-1; x++ { funcStats.BenchResults[iter].Results = append( funcStats.BenchResults[iter].Results, bestResult, ) } } } elapsed := time.Since(start) jDELogger.Printf("completed: bench: %s, %dD, computing took %s\n", j.BenchName, dim, elapsed, ) // get mean vals. dimXMean.MeanVals = stats.GetMeanVals(funcStats.BenchResults, maxFES) funcStats.MeanVals = dimXMean.MeanVals jDEStatDimX.BenchFuncStats = append( jDEStatDimX.BenchFuncStats, *funcStats, ) jDEStats = append(jDEStats, *jDEStatDimX) jDEMeans.BenchMeans = append(jDEMeans.BenchMeans, *dimXMean) } sort.Sort(jDEMeans) j.chAlgoMeans <- jDEMeans j.ch <- jDEStats } // evaluate evaluates the fitness of current population. func (j *JDE) evaluate(pop *Population) float64 { f := bench.Functions[pop.Problem] bestIndividual := pop.Population[pop.GetBestIdx()].CurX bestSolution := f(bestIndividual) return bestSolution } // evolve evolves a population by running the jDE (self-adapting Differential // Evolution) algorithm on the passed population until termination conditions // are met. // nolint: gocognit func (j *JDE) evolve(pop *Population, uniDist *distuv.Uniform) { popCount := len(pop.Population) for i, currentIndividual := range pop.Population { idcs := make([]int, 0, popCount-1) // gather indices. for k := 0; k < popCount; k++ { if k != i { idcs = append(idcs, k) } } // randomly choose 3 of those idcs. selectedIdcs := make([]int, 0) selectedA := false selectedB := false selectedC := false for !selectedA { candidateA := rand.Intn(len(idcs)) % len(idcs) if candidateA != i { selectedIdcs = append(selectedIdcs, candidateA) selectedA = true } } for !selectedB { a := selectedIdcs[0] candidateB := rand.Intn(len(idcs)) % len(idcs) if candidateB != i && candidateB != a { selectedIdcs = append(selectedIdcs, candidateB) selectedB = true } } for !selectedC { a := selectedIdcs[0] b := selectedIdcs[1] candidateC := rand.Intn(len(idcs)) % len(idcs) if candidateC != i && candidateC != a && candidateC != b { selectedIdcs = append(selectedIdcs, candidateC) selectedC = true } } // selected contains the selected population individuals. selected := make([]PopulationIndividual, 0) // select individuals for rand/1/bin for _, idx := range selectedIdcs { for k := 0; k < popCount; k++ { if k == idx { selected = append(selected, pop.Population[idx]) } } } mutant := make([]float64, pop.Dimen) // mutate. for k := 0; k < pop.Dimen; k++ { // mutant = a + mut * (b - c) mutant[k] = selected[0].CurX[k] + (j.F * (selected[1].CurX[k] - selected[2].CurX[k])) // clip values to <0;1>. switch { case mutant[k] < 0: mutant[k] = 0 case mutant[k] > 1: mutant[k] = 1 } } crossPoints := make([]bool, pop.Dimen) // prepare crossover points (binomial crossover). for k := 0; k < pop.Dimen; k++ { if v := uniDist.Rand(); v < j.CR { crossPoints[k] = true } else { crossPoints[k] = false } } trial := make([]float64, pop.Dimen) // recombine using crossover points. for k := 0; k < pop.Dimen; k++ { if crossPoints[k] { trial[k] = currentIndividual.CurX[k] } } f := bench.Functions[pop.Problem] currentFitness := f(currentIndividual.CurX) trialFitness := f(trial) // replace if better. if trialFitness < currentFitness { pop.Population[i].CurX = trial } } } // NewjDE returns a pointer to a new, uninitialised jDE instance. func NewjDE() *JDE { return &JDE{} } // LogPrintln wraps the jDE logger's Println func. func LogPrintln(v ...any) { jDELogger.Println(v...) } // LogPrintf wraps the jDE logger's Printf func. func LogPrintf(s string, v ...any) { jDELogger.Printf(s, v...) } // LogFatalln wraps the jDE logger's Fatalln func. func LogFatalln(s string) { jDELogger.Fatalln(s) } // LogFatalf wraps the jDE logger's Fatalf func. func LogFatalf(s string, v ...any) { jDELogger.Fatalf(s, v...) }