// Copyright 2022 wanderer // SPDX-License-Identifier: GPL-3.0-or-later package algo import ( "fmt" "log" "time" "git.dotya.ml/wanderer/math-optim/report" "git.dotya.ml/wanderer/math-optim/stats" "git.dotya.ml/wanderer/math-optim/util" "gonum.org/v1/gonum/floats" "gonum.org/v1/plot" "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/plotutil" "gonum.org/v1/plot/vg" ) const preferredFontStyle = "Mono" func plotMeanVals(vals []stats.BenchRound, fes int) *plot.Plot { plotter.DefaultFont.Typeface = preferredFontStyle plotter.DefaultLineStyle.Width = vg.Points(1.5) p := plot.New() p.Title.Text = "Mean" p.X.Label.Text = "Generations" p.X.Label.TextStyle.Font.Variant = preferredFontStyle p.X.Label.TextStyle.Font.Weight = 1 // Medium p.X.Tick.Label.Font.Variant = preferredFontStyle p.Y.Label.Text = "CF value" p.Y.Label.TextStyle.Font.Variant = preferredFontStyle p.Y.Label.TextStyle.Font.Weight = 1 // Medium p.Y.Tick.Label.Font.Variant = preferredFontStyle p.Title.TextStyle.Font.Size = 11.5 p.Title.TextStyle.Font.Variant = "Sans" p.Title.TextStyle.Font.Weight = 2 // SemiBold p.Title.Padding = 5 * vg.Millimeter // get mean vals. meanVals := stats.GetMeanVals(vals, fes) // mark the end of the X axis with len(meanVals). p.X.Max = float64(len(meanVals)) p.Y.Min = floats.Min(meanVals) p.Y.Max = floats.Max(meanVals) pts := make(plotter.XYs, len(meanVals)) // fill the plotter with datapoints. for k, res := range meanVals { pts[k].X = float64(k) pts[k].Y = res } lines := make([]interface{}, 0) lines = append(lines, pts) err := plotutil.AddLines( p, lines..., ) if err != nil { log.Panic(err) } return p } // violating gocognit 30, TODO(me): still split this up. // nolint: gocognit func plotAllDims(algoStats []stats.Stats, fPrefix, fExt string, ch chan report.PicList) { start := time.Now() picsDir := report.GetPicsDir() // create picsDir (if not exists), fail on error, no point in going on // parsing algoStats and computing images if we cannot save them. if err := util.CreatePath(picsDir); err != nil { log.Fatalln("went to create picsDir, there was an issue: ", err) } pL := report.NewPicList() pics := make([]report.Pic, 0) // since the algoStats only contains results of a single algo, it's safe to // set the value like this. pL.Algo = algoStats[0].Algo pWidth := 13 * vg.Centimeter pHeight := 13 * vg.Centimeter plotter.DefaultFont.Typeface = preferredFontStyle plotter.DefaultLineStyle.Width = vg.Points(1.5) for _, s := range algoStats { p := plot.New() pic := report.NewPic() p.Title.Text = s.Algo + ", D=" + fmt.Sprint(s.Dimens) + ", func=" + s.BenchFuncStats[0].BenchName + ", G=" + fmt.Sprint(s.Generations) + ", I=" + fmt.Sprint(s.Iterations) pic.Caption = p.Title.Text // since a single stat slice of algoStats only contains results of a // single bench func, it's safe to set the value like this. pL.Bench = s.BenchFuncStats[0].BenchName p.X.Label.Text = "Generations" p.X.Label.TextStyle.Font.Variant = preferredFontStyle p.X.Label.TextStyle.Font.Weight = 1 // Medium p.X.Tick.Label.Font.Variant = preferredFontStyle p.Y.Label.Text = "CF value" p.Y.Label.TextStyle.Font.Variant = preferredFontStyle p.Y.Label.TextStyle.Font.Weight = 1 // Medium p.Y.Tick.Label.Font.Variant = preferredFontStyle p.Title.TextStyle.Font.Size = 11.5 p.Title.TextStyle.Font.Variant = "Sans" p.Title.TextStyle.Font.Weight = 2 // SemiBold p.Title.Padding = 5 * vg.Millimeter for _, dim := range s.BenchFuncStats { // infinite thanks to this SO comment for the interface "hack": // https://stackoverflow.com/a/44872993 lines := make([]interface{}, 0) for _, iter := range dim.Solution { // mark the end of the X axis with len(iter.Results). p.X.Max = float64(len(iter.Results)) if floats.Min(iter.Results) < p.Y.Min { p.Y.Min = floats.Min(iter.Results) } if floats.Max(iter.Results) > p.Y.Max { p.Y.Max = floats.Max(iter.Results) } pts := make(plotter.XYs, len(iter.Results)) // fill the plotter with datapoints. for k, res := range iter.Results { pts[k].X = float64(k) pts[k].Y = res } lines = append(lines, pts) } err := plotutil.AddLines( p, lines..., ) if err != nil { // panic (don't panic, I know) instead of a hard exit. log.Panic(err) } filename := picsDir + fPrefix + "-" + util.SanitiseFName(s.Algo) + "-" + util.SanitiseFName(dim.BenchName) + "-" + fmt.Sprint(s.Dimens) + "D-" + fmt.Sprint(s.Generations) + "G-" + fmt.Sprint(len(dim.Solution)) + "I" filenameMean := filename + util.SanitiseFName(" Mean") // NEVER EVER ATTEMPT TO INITIALISE THIS WITH `pic`! picMean := report.NewPic() picMean.Caption = "D:~" + fmt.Sprint(s.Dimens) + ", G:~" + fmt.Sprint(s.Generations) + ", I:~" + fmt.Sprint(s.Iterations) // set pic file path (later used in tmpl generation) pic.FilePath = filename picMean.FilePath = filenameMean pMean := plotMeanVals(dim.Solution, s.Generations) elapsed := time.Since(start) info := "saving img to file: " + filename + "(-Mean)" + fExt + " [generated in " + fmt.Sprint(elapsed) + "]" if s.Algo == "Random Search" { printRandomSearch(info) } else { printSHC(info) } // Save the plot to a file using the above-constructed 'filename'. if err := p.Save( pWidth, pHeight, filename+fExt, ); err != nil { panic(err) } // save pic. pics = append(pics, *pic) // save plot of pMean. if err := pMean.Save( pWidth, pHeight, filenameMean+fExt, ); err != nil { panic(err) } } } pL.Pics = pics ch <- *pL }