Select Git revision
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
dorun.go 10.12 KiB
// Aug 2021
package mcwork
import (
"bufio"
"bytes"
"fmt"
"io"
"math"
"math/rand"
"os"
"path/filepath"
"sync"
"example.com/ackley_mc/ackley"
)
var seedLocker sync.Mutex
// getSeed returns the random number seed, but we have to put a lock
// around it so multiple runs get consistent successive seeds
func getSeed() int {
seedLocker.Lock()
r := Seed
Seed++
seedLocker.Unlock()
return r
}
type withBytes interface {
Bytes() []byte
}
type cprm struct { // parameters calculated from input
rand *rand.Rand
coolMult float64 // multiplier for temperature
fOutRaw io.WriteCloser // the raw file handle for csv files
fOut *bufio.Writer // buffered version of fOutRaw
doTxtFval bool // Are we writing text files (csv) ?
plotnstp []float64 // Number of steps. float64 for the plot library
plotf []float64 // Function values for plotting
plotTmprtr []float64 // Temperature values for plotting
plotXtrj []float32 // for plotting trajectories
coolme bool // Cooling ? or just constant temperature ?
fplotWrt io.Writer // Where to write the plot of function values
xplotWrt io.Writer // Where to write the plot of x trajectories
}
// isNotSane checks for obviously silly parameters
func isNotSane(mcPrm *McPrm) error {
if mcPrm.fOutName == "" && mcPrm.fPltName == "" && mcPrm.xPltName == "" {
if false { // if there are no graphics compiled in, ..
return fmt.Errorf("All output files are empty. You would not see anything")
}
}
if mcPrm.FnlTmp > mcPrm.IniTmp {
const finalTempErr = "Final temp %g higher than initial temp %g"
return fmt.Errorf(finalTempErr, mcPrm.FnlTmp, mcPrm.IniTmp)
}
if mcPrm.IniTmp < 0 {
return fmt.Errorf("negative initial temperature %g", mcPrm.IniTmp)
}
return nil
}
// createFix is a wrapper around os.Create() with a long error message.
// On windows, this fails if I am in my "Documents" directory due to
// its ransomware protection.
func createFix(name string) (*os.File, error) {
emsg := `Could not create "%s". The error was
%w
This is probably a permission error. Specify a directory other than
%s
and try again. It might be your protected folder settings.`
var t string
var err, e2 error
var f *os.File
if f, err = os.Create(name); err == nil {
return f, nil // Most common path (no problem)
}
if t, e2 = filepath.Abs(name); e2 != nil {
return nil, fmt.Errorf("The path for %s is broken: \"%w\"", name, e2)
}
return nil, fmt.Errorf(emsg, name, err, filepath.Dir(t))
}
// setupRun does things like get the cooling rate, seed the random numbers,
// open files for writing output.
func setupRun(mcPrm *McPrm, cprm *cprm) error {
var err error
cprm.rand = rand.New(rand.NewSource(int64(getSeed())))
if mcPrm.IniTmp != mcPrm.FnlTmp { // cooling or constant temperature ?
var coolrate float64
cprm.coolme = true
nStep := float64(mcPrm.NStep)
fnlTmp, iniTmp := float64(mcPrm.FnlTmp), float64(mcPrm.IniTmp)
if fnlTmp == 0 { // avoid divide by zeroes
fnlTmp = math.SmallestNonzeroFloat32
}
coolrate = -1 / nStep
coolrate *= math.Log(fnlTmp / iniTmp)
cprm.coolMult = math.Exp(-coolrate)
}
if mcPrm.fOutName != "" { // We are going to write text, .csv
if mcPrm.fOutName, err = setSuffix(mcPrm.fOutName, ".csv"); err != nil {
return err
}
if cprm.fOutRaw, err = createFix(mcPrm.fOutName); err != nil {
return err
}
cprm.fOut = bufio.NewWriter(cprm.fOutRaw)
cprm.doTxtFval = true // other parts of code can see that we are writing to file
}
if mcPrm.fPltName != "" { // Function value plot to file
if mcPrm.fPltName, err = setSuffix(mcPrm.fPltName, ".png"); err != nil {
return fmt.Errorf("broke setting plot filename: %w", err)
}
if cprm.fplotWrt, err = os.Create(mcPrm.fPltName); err != nil {
return fmt.Errorf("Trying to create file: %w", err)
}
} else { // Must be writing to a screen
var w bytes.Buffer // could be fancier. Preallocate and use bytes.Newbuffer
cprm.fplotWrt = &w
}
n_alloc := mcPrm.NStep / 5 // About a fifth of the number of steps
cprm.plotnstp = make([]float64, 0, n_alloc)
cprm.plotf = make([]float64, 0, n_alloc)
if cprm.coolme {
cprm.plotTmprtr = make([]float64, 0, n_alloc)
}
if mcPrm.xPltName != "" {
if mcPrm.xPltName, err = setSuffix(mcPrm.xPltName, ".png"); err != nil {
return fmt.Errorf("broke setting plot filename: %w", err)
}
if cprm.xplotWrt, err = os.Create(mcPrm.xPltName); err != nil {
return err
}
} else { // Write to buffer, to plot on screen
var w bytes.Buffer
cprm.xplotWrt = &w // I think we can remove the preceding line
}
cprm.plotXtrj = make([]float32, 0, int(n_alloc)*len(mcPrm.XIni))
if err = isNotSane(mcPrm); err != nil {
return err
}
return nil
}
// newx provides a random step to give us new coordinates.
// An earlier version moved all dimensions at once, but required
// teeny moves.
func newx(xold []float32, xT []float32, rand *rand.Rand, xDlta float64) {
var iDim int
if (len(xold)) > 1 {
iDim = rand.Intn(len(xold)) // pick one dimension to change
}
t := 2.*rand.Float64() - 1
t *= xDlta
copy(xT, xold)
xT[iDim] = xT[iDim] + float32(t)
}
// printfVal is the loop to print out the function value and coordinates
// for later plotting.
func printfVal(fOut io.Writer, x []float32, n int, tmprtr float64, fOld float64) {
fmt.Fprintf(fOut, "%d,%.4g,%.5g", n, tmprtr, fOld)
for _, xx := range x {
fmt.Fprintf(fOut, ",%.3g", xx)
}
fmt.Fprintln(fOut)
}
// saveBest checks if we have found a best value for the function. If so,
// update the location where we store it.
var bestfval float64
var bestX []float32
var bestN int
func saveBest(x []float32, fTrial float64, n int) {
if bestX == nil {
bestX = make([]float32, len(x))
}
if fTrial < bestfval || n == 0 {
bestfval = fTrial
bestN = n
copy(bestX, x)
}
}
// saveStep is called when we accept a change. It writes or saves for
// later plotting. Every time we append values, we have to do it twice
// so we get a proper stepped appearance.
func saveStep(cprm *cprm, n int, tmprtr float64, x []float32, fTrial float64) {
if cprm.coolme { // Only keep the best values found if we are annealing
saveBest(x, fTrial, n)
}
fprev := fTrial
xprev := x
if n != 0 {
fprev = cprm.plotf[len(cprm.plotf)-1]
xprev = cprm.plotXtrj[len(cprm.plotXtrj)-len(x):]
}
cprm.plotnstp = append(cprm.plotnstp, float64(n), float64(n)) // num steps
if cprm.fplotWrt != nil { // Function values
cprm.plotf = append(cprm.plotf, fprev) // copy the last coordinates
cprm.plotf = append(cprm.plotf, fTrial) // and append them
cprm.plotTmprtr = append(cprm.plotTmprtr, tmprtr) // then the new ones
cprm.plotTmprtr = append(cprm.plotTmprtr, tmprtr) // and again
}
if cprm.xplotWrt != nil { // The trajectory
cprm.plotXtrj = append(cprm.plotXtrj, xprev...)
cprm.plotXtrj = append(cprm.plotXtrj, x...)
}
if cprm.doTxtFval {
printfVal(cprm.fOut, x, n, tmprtr, fTrial)
}
}
// doRun does a Monte Carlo run.
// We only need single precision for most things, except for function
// values, but the interface to the plot and interface packages mostly
// want double precision, so we have a lot of float64 that should be
// float32.
func singleRun(mcPrm *McPrm) (MCresult, error) {
var cprm cprm
broken := MCresult{}
if err := setupRun(mcPrm, &cprm); err != nil {
return broken, err
}
if cprm.doTxtFval { // The text file documenting the run
defer cprm.fOutRaw.Close()
defer cprm.fOut.Flush()
}
if c, ok := cprm.fplotWrt.(io.Closer); ok {
defer c.Close() // file handle for function values
}
if c, ok := cprm.xplotWrt.(io.Closer); ok {
defer c.Close() // file handle for X trajectories
}
var nAcc int // Counter, number of accepted moves
x := make([]float32, len(mcPrm.XIni)) // current position
xT := make([]float32, len(mcPrm.XIni)) // trial position
for i := range mcPrm.XIni { // Initial coordinates from
x[i] = float32(mcPrm.XIni[i]) // the control structure
}
fOld := ackley.Ackley(x) // Initial function value
fTrial := fOld
tmprtr := float64(mcPrm.IniTmp)
xDlta := mcPrm.XDlta
saveStep(&cprm, 0, tmprtr, x, fOld)
for n := 0; n < mcPrm.NStep; n++ {
var accept bool
newx(x, xT, cprm.rand, xDlta)
fTrial = ackley.Ackley(xT)
if fTrial <= fOld {
accept = true
} else {
delta := fTrial - fOld // Decision as to whether to
t := math.Exp(-delta / tmprtr) // accept or reject the trial
if cprm.rand.Float64() < t {
accept = true
}
}
if accept {
nAcc++
copy(x, xT)
fOld = fTrial
saveStep(&cprm, n+1, tmprtr, x, fTrial)
}
if cprm.coolme {
tmprtr *= cprm.coolMult
}
}
// On plots, we want the last step, even if nothing changed.
if fTrial != fOld { // last move was not accepted, so we have to add last step
saveStep(&cprm, mcPrm.NStep, tmprtr, x, fOld)
}
defer fmt.Println("n accepted:", nAcc, "of", mcPrm.NStep)
if bestX != nil {
s := "Best function value: %.2f at %.2v at step %d\n"
defer fmt.Printf(s, bestfval, bestX, bestN)
}
if err := plotfWrt(&cprm); err != nil {
return broken, err
}
if err := plotxWrt(&cprm, len(mcPrm.XIni)); err != nil {
return broken, err
}
var fdata, xdata []byte
if funcBuf, ok := cprm.fplotWrt.(withBytes); ok {
fdata = funcBuf.Bytes()
}
if xBuf, ok := cprm.xplotWrt.(withBytes); ok {
xdata = xBuf.Bytes()
}
return MCresult{
Fdata: fdata, Xdata: xdata,
NStep: mcPrm.NStep, NAcc: nAcc,
Bestfval: bestfval, BestX: bestX,
LastX: x,
}, nil
}
// DoRun will do a run with equilibration. It will call singleRun twice.
// On the first run, we make sure there is no cooling and output is
// not written or goes to the garbage (io.Discard).
func DoRun(mcPrm *McPrm) (MCresult, error) {
var tmpX []float32
if mcPrm.NEquil > 0 {
tmpX = make([]float32, len(mcPrm.XIni))
mcEquil := *mcPrm
mcEquil.FnlTmp = mcEquil.IniTmp
mcEquil.NStep = mcEquil.NEquil
mcEquil.NEquil = 0
mcEquil.fOutName = ""
mcEquil.fPltName, mcEquil.xPltName = "", ""
if eqResult, err := singleRun(&mcEquil); err != nil {
return eqResult, err
} else {
copy(tmpX, mcPrm.XIni)
copy(mcPrm.XIni, eqResult.LastX)
}
}
mcResult, err := singleRun(mcPrm)
if tmpX != nil {
copy(mcPrm.XIni, tmpX)
}
return mcResult, err
}