// Copyright 2022 wanderer // SPDX-License-Identifier: GPL-3.0-or-later package algo import ( "fmt" "log" "os" "git.dotya.ml/wanderer/math-optim/bench" "git.dotya.ml/wanderer/math-optim/stats" "gonum.org/v1/gonum/stat/distuv" ) func getRandomSearchLogPrefix() string { return " ***  random search:" } func fmtRandomSearchOut(input string) string { return getRandomSearchLogPrefix() + " " + input } func printRandomSearch(input string) { if _, err := fmt.Fprintln(os.Stderr, fmtRandomSearchOut(input)); err != nil { fmt.Fprintf( os.Stdout, getRandomSearchLogPrefix(), "error while printing to stderr: %q\n * original message was: %q", err, input, ) } } func genValsRandomSearch(dimens uint, vals []float64, uniform *distuv.Uniform) { for i := uint(0); i < dimens; i++ { // using Uniform.Rand from gonum's stat/distuv package. // https://pkg.go.dev/gonum.org/v1/gonum/stat/distuv#Uniform.Rand // boundaries are already set at this point. vals[i] = uniform.Rand() } } // singleRandomSearch performs a single iteration of the 'RandomSearch' algorithm. // it takes a couple of arguments: // * dimens uint: number of dimensions of the objective function // * f func([]float64) float64: bench func to execute (see Functions map in // bench/functions.go) // * min/max float64: the upper/lower limit of the uniform distribution span, // which is relevant to the objective function. func singleRandomSearch(dimens uint, f func([]float64) float64, min, max float64) ([]float64, float64) { vals := make([]float64, dimens) // create a continuous uniform distribution representation within min/max bounds uniformDist := distuv.Uniform{Min: min, Max: max} genValsRandomSearch(dimens, vals, &uniformDist) // result of the bench function. res := f(vals) return vals, res } func RandomSearchNG(maxFES, benchMinIters int, theD []int, benchFunc string) []stats.Stats { // perform basic sanity checks. if maxFES <= 0 { log.Fatalln(fmtRandomSearchOut("maxFES cannot be <= 0, bailing")) } else if benchMinIters <= 0 { log.Fatalln(fmtRandomSearchOut("benchMinIters cannot be <= 0, bailing")) } else if _, ok := bench.Functions[benchFunc]; !ok { log.Fatalln(fmtRandomSearchOut( "unknown benchFunc used: '" + benchFunc + "', bailing", )) } for i := range theD { if theD[i] <= 0 { log.Fatalln(fmtRandomSearchOut(" no dimension in D can be <= 0, bailing")) } } // use func-local vars. var ( fes int localD []int minIters int randomSearchStats []stats.Stats ) fes = maxFES localD = theD minIters = benchMinIters // iterate over whatever was passed to us with theD - dimens slice. for _, dimens := range localD { randomSearchStatDimX := &stats.Stats{ Algo: "Random Search", Dimens: dimens, Iterations: minIters, Generations: fes, } funcStats := &stats.FuncStats{BenchName: benchFunc} benchFuncParams := bench.FunctionParams[benchFunc] funcStats.Solution = make([]stats.BenchRound, minIters) // perform the while dance 'minIters' times for "statistical relevance" for iter := 0; iter < minIters; iter++ { // have a fresh bestResult for each of the 'minIters' runs. var bestResult float64 // set current iteration in funcStats. funcStats.Solution[iter].Iteration = iter // run the benchmarking function 'fes' times. for i := 0; i < fes; i++ { _, r := singleRandomSearch( uint(dimens), bench.Functions[benchFunc], benchFuncParams.Min(), benchFuncParams.Max(), ) // is a switch efficient, or should an if statement be used..? // TODO(me): perhaps benchmark this... switch i { case 0: // if it's our first, bestResult is the result we just got. bestResult = r default: // since we're minimising, the lower 'r' is the better. if r < bestResult { bestResult = r } } // save the 'best' result, since we only care about those. funcStats.Solution[iter].Results = append( funcStats.Solution[iter].Results, bestResult, ) } } // save cumulative results of 'minIters' runs. randomSearchStatDimX.BenchFuncStats = append( randomSearchStatDimX.BenchFuncStats, *funcStats, ) // save stats for each dimension to a stats slice. randomSearchStats = append(randomSearchStats, *randomSearchStatDimX) } return randomSearchStats }