#!/bin/sh  
# \
exec oagtclsh "$0" "$@"

#
# mopsaOptimizer: cluster-based Multi-Objective Particle Swarm Algorithm Optimizer, M. Borland
# based on geneticOptimizer, by H. Shang and M. Borland
#

if ![info exists env(OAG_TOP_DIR)] {
    set env(OAG_TOP_DIR) /usr/local
}
set auto_path [linsert $auto_path 0  $env(OAG_TOP_DIR)/oag/apps/lib/$env(HOST_ARCH)]

APSStandardSetup

set usage {usage: mopsaOptimizer -input <filename> [-contin 1] [-reduce 0] [-help 1] [-maxRank <integer>] [-inertia <value>(0.1)] [-clocal <value>(0.1)] [-cglobal <value>(0.3)] -startGenerations <number> -swarmGenerations <number>] [-crowdingLimit <fraction>(0.1)] [-waitTime <seconds>(60)] [-errorFactor <value>(1.0)] [-debug 1]}

proc PrintHelp {} {
    global usage
    puts stdout "$usage\n"
    puts stdout "Description of commandline parameters:"
    puts stdout "input:  name of the input file, as described above."
    puts stdout "contin: if non-zero, then continue optimizing from a previous run"
    puts stdout "reduce: if non-zero, then unneeded trial jobs are deleted to save time and disk space."
    puts stdout "inertia: inertia factor: a higher value results in particle tending to keep the same velocity"
    puts stdout "clocal: local contribution to velocity increment, which sets the tendency of the particle"
    puts stdout "   to move toward one of its previous best positions." 
    puts stdout "cglobal: global contribution to velocity increment, which sets the tendency of the particle"
    puts stdout "   to move toward one of the globally-best positions." 
    puts stdout "topUp: if non-zero, then for continued runs the optimizer will start a sufficient number of jobs to ensure that"
    puts stdout "   the desired total number is running.  Use this only if you have changed you input file in the middle of"
    puts stdout "   running to increase the number of jobs and want to add jobs now, rather than waiting for processing to occur."
    puts stdout "maxRank: for multiobjective mode, maximum rank of solutions to include in breeding. Default is 1."
    puts stdout "startGenerations: number of generations of purely random starting points to seed the process."
    puts stdout "swarmGenerations: number of generations of swarming behavior."
    puts stdout "crowdingLimit: specify which fraction of top-ranked solutions to accept, preferring the least crowded solutions."
    puts stdout "waitTime: time to wait before checking for completed jobs again."
    puts stdout "errorFactor: factor by which to multiply error levels, to control randomness of positions."
    puts stdout ""
    puts stdout "Description of input file:"
    puts stdout "Parameters: "
    puts stdout "nTotalJobs:  maximum number of jobs to run"
    puts stdout "nParticles:  number of particles."
    puts stdout "maxRank:     overrides maxRank parameter on the commandline (optional)."
    puts stdout "staggerTime: seconds to sleep between submission of successive jobs,"
    puts stdout "    which can help reduce peak load on the system."
    puts stdout "generateRunScript:   script to submit the simulation job."
    puts stdout "  This script must create a file <rootname>.run immediately upon invocation." 
    puts stdout "  The submitted job/script must create a file <rootname>.done just before finishing."
    puts stdout "  It must also postprocess the data to make the <rootname>.proc file."
    puts stdout "  Any quantities in the <rootname>.proc file with names of the form"
    puts stdout "    <prefix>Penalty are interpreted as values to be minimized."
    puts stdout "  The script must accept the following arguments and syntax: "
    puts stdout "    -rootname <string>       rootname for the job."
    puts stdout "    -tagList <string>        list of tags for varied quantities."
    puts stdout "    -valueList <string>      list of values associated with tags."
    puts stdout "    -particle <integer>      particle number"
    puts stdout "    -generation <integer>    generation number"
    puts stdout "    The tag names are the same as the parameter names given in the"
    puts stdout "    parameterName column (see below)."
    puts stdout ""
    puts stdout "Columns:"
    puts stdout "parameterName: names of the parameters to vary in the optimization."
    puts stdout "  These should appear in the input file template in the form <name>."
    puts stdout "  parameter values as mutations."
    puts stdout "lowerLimit: smallest allowable values for the parameters"
    puts stdout "upperLimit: largest allowable values for the parameters."
    puts stdout "errorLevel: mutation level for each operation"
    puts stdout "startMode: \"error\" or \"range\""
}

set args $argv
set contin 0
set input ""
set reduce 1
set inertia 0.1
set clocal 0.1
set cglobal 0.3
set help 0
set maxRank 1
set startGenerations 4
set swarmGenerations 100
set waitTime 60
set debug 0
set errorFactor 1.0
set crowdingLimit 0.1
if {[APSStrictParseArguments {contin startGenerations swarmGenerations input reduce help inertia clocal cglobal maxRank waitTime crowdingLimit errorFactor debug}] || \
        ![string length  $input]} {
    if $help {
        PrintHelp
        exit 
    }    
    return -code error "$usage"
}

if $startGenerations<2 {
    return -code error "startGenerations must be greater than 1"
}

if ![file exists $input] {
    return -code error "not found: $input"
}
set rootname [file rootname $input]

set preexistingJobs [llength [glob -nocomplain $rootname-??????.*]]
if {!$contin && $preexistingJobs} {
    return -code error "rootname in use: $rootname"
}

proc LoadAndCheckInputFile {} {
    global input inputParameterList inputColumnList
    global staggerTime
    global executionMode generateRunScript permissionToSubmitScript permissionToSubmitTries
    set staggerTime 0
    set generateRunScript ""
    set permissionToSubmitScript ""
    set permissionToSubmitTries -1
    if [catch {sdds load $input inputData} result] {
        return -code error "$result"
    }
    set inputParameterList $inputData(ParameterNames)
    foreach param $inputParameterList {
        global $param
        set $param [lindex $inputData(Parameter.$param) 0]
    }
    set inputColumnList $inputData(ColumnNames)
    foreach col $inputColumnList {
        global $col
        set $col [lindex $inputData(Column.$col) 0]
    }
    if [lsearch -exact $inputColumnList "startMode"]==-1 {
	global startMode
	lappend inputColumnList startMode
	set startMode [APSReplicateItem -item range -number [llength $col]]
    }

    foreach sm $startMode {
	if [lsearch -exact [list range error] $sm]==-1 {
	    puts stderr "Invalid startMode value: $sm"
	    exit 1
	}
    }

    foreach pN $parameterName lL $lowerLimit uL $upperLimit {
        if {$lL>$uL} {
            puts stderr "Error: $pN lower limit ($lL) is greater than the upper limit ($uL)."
            exit 1
        }
    }
}

proc StartInitialJobs {args} {
    set generation 0
    APSStrictParseArguments {generation}
    global inputParameterList inputColumnList 
    eval global $inputParameterList 
    eval global $inputColumnList
    global rootname template jobsStarted staggerTime executionMode generateRunScript inertia clocal cglobal

    for {set particle 0} {$particle<$nParticles} {incr particle} {
        if ![WaitForPermissionToSubmit] return;
        set runName [format ${rootname}-%06d-%06d $particle $generation]
        set origOpt <rootname>
        set replOpt $runName
        set valueList ""
        set procOpt ""
        foreach pN $parameterName iV $initialValue lL $lowerLimit uL $upperLimit err $errorLevel sm $startMode {
            lappend origOpt <$pN>
            if {$generation==0 && $particle==0} {
                set value $iV
            } else {
		switch $sm {
		    error {
			set value [expr $iV + (2*rand()-1)*$err]
		    }
		    range -
		    default {
			set value [expr rand()*($uL-$lL)+$lL]
		    }
		}
            }
            lappend replOpt $value
            lappend valueList $value
            lappend procOpt -define=column,$pN,$value,type=double
        }
        catch {eval exec sddssequence -pipe -define=dummy,type=short -sequence=begin=0,end=0,number=1 \
                   | sddsprocess -pipe=in $runName.inp \
                   $procOpt \
                   -print=column,runName,$runName \
                   -define=column,particle,$particle,type=long \
                   -define=column,generation,$generation,type=long} result
        if [string compare [exec sddscheck $runName.inp] "ok"]!=0 {
            return -code error "$result"
        }
        
        puts stderr "Generating and submitting script $generateRunScript"
        catch {eval exec $generateRunScript -rootname $runName -tagList \"$parameterName\" \
                   -valueList \"$valueList\" -particle $particle -generation $generation} result
        puts stderr "$generateRunScript returns: $result"
        if $staggerTime {
            after [expr int($staggerTime*1000)]
        }
    }
}

proc StartNewJobs {} {
    global inputParameterList inputColumnList 
    eval global $inputParameterList 
    eval global $inputColumnList
    global rootname template jobsToStart jobsStarted jobsRunning staggerTime generateRunScript inertia clocal cglobal
    global maxRank reduce sortOptionList crowdingLimit startGenerations debug errorFactor

    set newParticleList ""
    set lastGenerationList ""
    for {set particle 0} {$particle<$nParticles} {incr particle} {
        set prootname [format $rootname-%06d $particle]
        set nSubmitted [llength [glob -nocomplain $prootname-??????.run]]
        set nDone      [llength [glob -nocomplain $prootname-??????.done]]
        if $nSubmitted!=0 {
            if $nSubmitted>$nDone {
                continue
            }
            set processedJobList [lsort [glob -nocomplain $prootname-??????.proc]]
            puts stderr "nProcessed = [llength $processedJobList]"
            if [llength $processedJobList]!=$nDone {
                puts stderr "Warning: missing processed job for $prootname"
		puts stderr "nSubmitted = $nSubmitted, nDone = $nDone, nProcessed = [llength $processedJobList]"
                continue
            }
            puts stderr "Completed job detected for $prootname: $processedJobList"
            
            # Update history files with latest data
            if ![file exists $prootname.all] {
                if [llength $processedJobList]<2 {
                    puts stderr "Need more jobs for $prootname..."
                    continue
                }
                eval exec sddscombine $processedJobList $prootname.all -merge 
            } else {
                puts stderr "$prootname.all found---adding to it"
                eval exec sddscombine $prootname.all $processedJobList -merge -pipe=out \
                    | sddssort -pipe=in -column=generation -unique $prootname.all.new
                file rename -force $prootname.all $prootname.all.old
                file rename $prootname.all.new $prootname.all
            }
            foreach item $processedJobList {
                set runName [file rootname $item]
                eval exec mv [glob $runName.*] ${rootname}SavedRuns
            }

            if [llength $sortOptionList]==0 {
                set columns [exec sddsquery -column $prootname.all]
                foreach col $columns {
                    if [string match *Penalty $col] {
                        lappend sortOptionList -col=${col}
                    }
                }
            }
        }
        lappend lastGenerationList [exec sddsprocess $prootname.all -pipe=out -clip=0,1,invert | sdds2stream -pipe -column=generation]
        lappend newParticleList $particle
    }
    if [llength $newParticleList]==0 {
        return 0
    }

    if [llength $sortOptionList]==0 {
        set columns [exec sddsquery -column $rootname.all]
        foreach col $columns {
            if [string match *Penalty $col] {
                lappend sortOptionList -col=${col}
            }
        }
    }
    
    # Make master history files
    puts stderr "Making new master history file."
    eval exec sddscombine [lsort [glob ${rootname}-??????.all]] -merge -overwrite ${rootname}.all
    if [llength $sortOptionList]==1 {
        eval exec sddssort $sortOptionList ${rootname}.all -pipe=out \
            | sddsprocess -pipe=in ${rootname}.sort \
            "{-define=col,Rank,i_row 1 +,type=long}" \
            "-define=col,CrowdingDistance,1" 
    } else {
        eval exec sddssort -nondominate $sortOptionList ${rootname}.all ${rootname}.sort
    }

    set maxGeneration -1
    foreach particle $newParticleList lastGeneration $lastGenerationList {
        if $debug {
            puts stderr "Working on new position for particle $particle"
        }
        if $lastGeneration>$maxGeneration {
            set maxGeneration $lastGeneration
        }
        set prootname [format $rootname-%06d $particle]
        set generation [expr $lastGeneration+1]

        # Decide new trajectory for this particle
        # Determine previous velocity
        if $debug {
            puts stderr "Determining previous velocity"
        }
        exec sddsconvert $prootname.all -pipe=out -retain=column,[join $parameterName ,] -delete=param,* \
            | sddsprocess -pipe -clip=0,2,invert \
            | sddstranspose -pipe -oldColumnNames=parameterName -root=Value \
            | sddsprocess -pipe=in $prootname.vel \
            "-define=column,previousVelocity,Value001 Value000 - $lastGeneration $startGenerations > ? 1 : 0 $ * " \
            "-define=column,previousPosition,Value001"
        # Determine distance from local "best"
        if $debug {
            puts stderr "Determining distance from local best, sortOptionList = $sortOptionList (length=[llength $sortOptionList])"
        }
        if [llength $sortOptionList]==1 {
            if [catch {eval exec sddssort $sortOptionList $prootname.all -pipe=out \
                           | sddsprocess -pipe=in $prootname.sort \
                           "{-define=col,Rank,i_row 1 +,type=long}" \
                           "-define=col,CrowdingDistance,1" } result] {
                puts stderr "$result"
                exit 1
            }
        } elseif [catch {eval exec sddssort -nondominate $sortOptionList $prootname.all $prootname.sort} result] {
            puts stderr "$result"
            exit 1
        }
        if [catch {exec sddsprocess $prootname.sort -filter=col,Rank,0,$maxRank -pipe=out \
                       | sddssort -pipe -column=CrowdingDistance,decr \
                       | sddsprocess -pipe=in $prootname.best \
                       "-test=column,n_rows 1.2 $crowdingLimit / > ? i_row n_rows / $crowdingLimit / int : 0 $ 0 == " } result] {
            puts stderr "$result"
            exit 1
        }
        exec sddsprocess $prootname.best -pipe=out -define=col,RN,rnd \
            | sddssort -pipe -column=RN \
            | sddsprocess -pipe=in $prootname.best1 -clip=0,1,invert 
        exec sddsprocess $prootname.all $prootname.last -clip=0,1,invert 
        exec sddscombine $prootname.best1 $prootname.last -pipe=out -merge -retain=column,[join $parameterName ,] \
            | sddstranspose -pipe -oldColumnNames=parameterName -root=Value \
            | sddsprocess -pipe=in $prootname.dxlocal \
            "-define=column,dxLocal,Value000 Value001 -" 
        # Determine distance from global "best"
        if $debug {
            puts stderr "Determining distance from global best"
        }
        exec sddsprocess $rootname.sort -pipe=out -filter=col,Rank,0,$maxRank \
	    | tee $rootname.best \
            | sddssort -pipe -column=CrowdingDistance,decr \
            | sddsprocess -pipe \
            "-test=column,n_rows 2 $crowdingLimit / > ? i_row n_rows / $crowdingLimit / int : 0 $ 0 == " -define=col,RN,rnd \
            | sddssort -pipe -column=RN \
            | sddsprocess -pipe=in $rootname.best1 -clip=0,1,invert 
        exec sddsprocess $prootname.all $prootname.last -clip=0,1,invert 
        exec sddscombine $rootname.best1 $prootname.last -pipe=out -merge -retain=column,[join $parameterName ,] \
            | sddstranspose -pipe -oldColumnNames=parameterName -root=Value \
            | sddsprocess -pipe=in $prootname.dxglobal \
            "-define=column,dxGlobal,Value000 Value001 -" 
        # Compute new position
        if $debug {
            puts stderr "Determining new position"
        }
        set value0List [exec sddsxref $prootname.vel $prootname.dxlocal $prootname.dxglobal -take=* -pipe=out \
                            | sddsprocess -pipe \
                            "-redefine=parameter,w,$inertia" "-redefine=parameter,c1,$clocal" "-redefine=parameter,c2,$cglobal"  \
                            "-redefine=parameter,r1,rnd" "-redefine=parameter,r2,rnd"  \
                            "-redefine=column,newPosition,previousPosition w previousVelocity * + dxLocal c1 * r1 * + dxGlobal c2 * r2 * +" \
                            | tee $prootname.calc \
                            | sdds2stream -pipe -column=newPosition]

        set procOpt ""
        set valueList ""
        foreach pN $parameterName value0 $value0List err $errorLevel lL $lowerLimit uL $upperLimit {
            set value [expr $value0 + (2*rand()-1)*$err*$errorFactor]
            if [expr $value>$uL] {
                set value $uL
            }
            if [expr $value<$lL] {
                set value $lL
            }
            lappend valueList $value
            lappend procOpt -redefine=column,$pN,$value,type=double
        }

        if $debug {
            puts stderr "Making input file"
        }
        set runName [format ${prootname}-%06d $generation]
        catch {eval exec sddsmakedataset -pipe=out \
                   -column=generation,type=long -data=$generation \
                   -column=particle,type=long -data=$particle \
                   | sddsprocess -pipe=in $runName.inp \
                   $procOpt -print=column,runName,$runName} result
        if [string compare [exec sddscheck $runName.inp] "ok"]!=0 {
            return -code error "$result"
        }

        # Submit the job
        puts stderr "Generating and submitting script for $runName"
        puts stderr "tagList: [join $parameterName ,]; valueList: [join $valueList ,]"
        catch {eval exec $generateRunScript -rootname $runName -tagList [join $parameterName ,] -valueList [join $valueList ,] \
            -particle $particle -generation $generation} result
        puts stderr "$generateRunScript returns: $result"

        if $reduce {
            # Determine whether to remove the previous job files and then do so if needed
            # Remove jobs that are neither a local best nor a global best
        }

	if $staggerTime {
	    after [expr int($staggerTime*1000)]
	}
    }
    return $maxGeneration
}

proc WaitForPermissionToSubmit {} {
    global permissionToSubmitScript permissionToSubmitTries 
    if [string length $permissionToSubmitScript] { 
	set triesLeft $permissionToSubmitTries
	while {$triesLeft!=0} {
	    catch {exec $permissionToSubmitScript} result
	    switch $result {
		1 -
		yes {
		    return 1
		}
		default -
		0 - 
		no {
		    puts stderr "[clock format [clock seconds]]: Job submission not permitted now"
		    incr triesLeft -1
		    after 5000
		}
	    }
	}
	puts stderr "[clock format [clock seconds]]: Job submission wait timeout"
	return 0
    } else {
	return 1
    }
}

LoadAndCheckInputFile 
puts stderr "Input file read and checked."

if {!$contin} {
    if {[file exists $rootname.all]} {
        puts stderr "Error: $rootname.all file found but -contin 1 not given"
        exit 1
    }
    WaitForPermissionToSubmit
    for {set generation 0} {$generation<$startGenerations} {incr generation} {
        StartInitialJobs -generation $generation
    }
    if ![file exists ${rootname}SavedRuns] {
        file mkdir ${rootname}SavedRuns
    }
} else {
    if {![file exists $rootname.all]} {
        puts stderr "Error: $rootname.all file not found but -contin 1 given"
        exit 1
    }
    set generation [exec sddsprocess $rootname.all -pipe=out -process=generation,max,%sMax | sdds2stream -pipe -parameter=generationMax]
}

set sortOptionList ""
            
set previousGeneration -1
while {$generation<=$swarmGenerations} {
    WaitForPermissionToSubmit
    set generation [StartNewJobs]
    if $generation>0 {
        puts stderr "\nNew jobs started for generation $generation"
    } else {
        set message "[clock format [clock seconds]]: Waiting for jobs to complete."
        puts -nonewline stderr $message
        for {set i 0} {$i<[string length $message]} {incr i} {
	    puts -nonewline stderr "\b"
        }
    }
    after [expr int($waitTime)*60]
}

