Repeatedly running a program

Arjen Markus (26 november 2008) A quick-and-dirty version of what I hope will become a useful program. The background is given in the comments ...


Example of input

# Trivial example of the use of the runner program
#
proc add {} {
    set infile [open "example.inp"]
    set first  [gets $infile]
    set second [gets $infile]
    close $infile
    set outfile [open "example.rep" w]
    puts $outfile "Sum of $first and $second is [expr {$first+$second}]"
    close $outfile
}

numberCases 10
randomInteger FIRST    0 10
randomInteger SECOND -10 20

runProcedure add

templateFiles example.inp
sourceDirectory "example"
destinationDirectory "work"
runSequentially   yes
useSubdirectories yes 

and the template input file for the add procedure (put it in a subdirectory "example"):

FIRST
SECOND

# Very simple, trivial example of an input file:
# Just add the two numbers and present the result 

The code

# runner.tcl --
#     Application to prepare the input for a numerical program and
#     run it a number of times.
#     The idea:
#     Often you want to run a numerical program with different
#     parameters to see the effect on the results, for instance with
#     sensitivity analysis. Preparing the input and running the program
#     manually is tedious and error prone.
#
#     This tedious part can be automated: the runner program does this
#     by reading in a number of commands that together set up the
#     parameter space from which to select the values, set up the run
#     procedure and so on.
#
#     The tedious part is thus taken care of and you can concentrate on
#     the more interesting parts: analysing the results
#
#     Potential commands:
#     randomUniform var min max
#     randomNormal var mean stdev
#     randomExponential var mean stdev
#     randomPoisson var mean stdev
#     randomfromList var list-of-values
#     generateFromFile filename
#     numberCases n
#     templateFiles list-of-files
#     saveFiles list-of-files
#     sourceDirectory dir
#     targetDirectory dir
#     useSubdirectories yesno
#     runSequentially yesno
#     runCommand cmd
#     runProcedure proc
#     maximumJobsAtOnce n
#
#     Different ways of running the program:
#     - prepare the input and run sequentially
#     - prepare the input and let others take care
#     - prepare the input and put them in a queue
#
#     submitCommand
#     queryCommand
#     onlyGenerateInput
#     onlyStartRuns
#
#     Extensions:
#     - use of orthogonal arrays
#     - analysing results
#     - optimisation
#

# global variables --
#
set varNames    {}
array set var   {}
array set files {
    number     10
    filename   ""
    sequential 1
    subdirs    1
    runcmd     ""
    runproc    ""
    templates  ""
    sourcedir  ""
    destdir    ""
}

# prepareCaseAndRun --
#     Prepare a new case and run the program immediately
#
# Arguments:
#     None
#
# Note:
#     Be careful with the directories
#
# To do:
#     Protect against endless recursion if the source directory
#     contains the destination directory
#
proc prepareCaseAndRun {} {
    global files

    if { ! [file exists $files(destdir)] } {
        file mkdir $files(destdir)
    }

    if { $files(subdirs) } {
        set casedir [file join $files(destdir) $files(casecount)]
        file mkdir $casedir
    } else {
        set casedir $files(destdir)
    }

    set curdir [pwd]
    cd $casedir
    set casedir [pwd]
    cd $curdir

    cd $files(sourcedir)
    copyInput $casedir
    cd $curdir

    cd $casedir
    if { $files(runproc) != "" } {
        $files(runproc)
    } else {
        eval exec $files(runcmd)
    }
    cd $curdir

    #
    # Administration ...
    #
    incr files(casecount)
    if { $files(casecount) >= $files(number) } {
        set files(continue) 0
    }
}

# copyInput --
#     Copy the input files and replace any variables by their values
#
# Arguments:
#     destdir         Destination directory
#
proc copyInput {destdir} {
    global files

    foreach file [glob -nocomplain *] {
        file copy -force $file $destdir
    }

    cd $destdir
    foreach template $files(templates) {
        substituteInput $template
    }
}

# substituteInput --
#     Substitute the values of all registered variables
#
# Arguments:
#     filename        Name of the file to be treated
#
proc substituteInput {filename} {
    global varNames

    set infile [open $filename]
    set contents [read $infile]
    close $infile

    set substitute {}
    foreach v $varNames {
        lappend substitute $v [randomValue $v]
    }

    set outfile [open $filename w]
    puts -nonewline $outfile [string map $substitute $contents]
    close $outfile
}

# randomValue --
#     Generate a random value for a registered variable
#
# Arguments:
#     name            Name of the variable
#
proc randomValue {name} {
    global var

    switch -- $var($name,type) {
        "Integer" {
            set value [expr {int( rand() * ($var($name,second)-$var($name,first)) +
                                            $var($name,first))}]
        }
        default {
            error "Random type not implemented yet: $var($name,type)"
        }
    }
}

# randomUniform, randomNormal, ...
#     Define the random variables
#
# Arguments:
#     string          Literal string that will be replaced by the random value
#     first           First parameter (minimum or mean)
#     second          Second parameter (maximum or standard deviation)
#
foreach p {Uniform Normal Exponential Poisson Integer} {
    proc random$p {string first second} [string map [list TYPE $p] {
        global var
        global varNames

        lappend varNames $string
        set var($string,type)  TYPE
        set var($string,first) $first
        set var($string,second) $second
    }]
}

proc randomFromList {string values} {
    global var
    global varNames

    lappend varNames $string
    set var($string,type)   List
    set var($string,values) $values
}

# numberCases --
#     Register the number of cases to generate
#
# Arguments:
#     number      Number to generate
#
# Note:
#     Used only if there is no file from which to read the cases
#     (no generateFromFile command)
#
proc numberCases {number} {
    global files

    set files(number) $number
}

# generateFromFile --
#     Register the input file from which to generate the cases
#
# Arguments:
#     filename    Name of an existing file
#
proc generateFromFile {filename} {
    global files

    set files(filename) $filename
    if { ![file exists $filename] } {
        errorMessage prepare "Input file with cases does not exist: $filename"
    }
}

# runSequentially --
#     Register whether to run one case after another or not
#
# Arguments:
#     yesno       Whether to run sequentially or not
#
proc runSequentially {yesno} {
    global files

    if { $yesno } {
        set files(sequential) 1
    } else {
        set files(sequential) 0
    }
}

# useSubdirectories --
#     Register whether to store each case in a separate directory or not
#
# Arguments:
#     yesno       Whether to use subdirectories or not
#
proc useSubdirectories {yesno} {
    global files

    if { $yesno } {
        set files(subdirs) 1
    } else {
        set files(subdirs) 0
    }
}

# runCommand --
#     Register the command to be run
#
# Arguments:
#     cmd         Command to be run (no arguments will be added)
#
proc runCommand {cmd} {
    global files

    set files(runcmd) $cmd
}

# runProcedure --
#     Register a Tcl proc to be run
#
# Arguments:
#     cmd         Procedure to be run (no arguments will be added)
#
# Note:
#     If both are given, the procedure takes precedence
#
proc runProcedure {cmd} {
    global files

    set files(runproc) $cmd
}

# templateFiles --
#     Register the files in the source directory serving as templates
#
# Arguments:
#     list        List of file names
#
proc templateFiles {list} {
    global files

    set files(templates) $list
}

# sourceDirectory --
#     Register the source directory
#
# Arguments:
#     dirname     Name of an existing directory
#
proc sourceDirectory {dirname} {
    global files

    if { [file exists $dirname] && [file isdirectory $dirname] } {
        set files(sourcedir) $dirname
    } else {
        errorMessage prepare "Source directory does not exist: $dirname"
    }
}

# destinationDirectory --
#     Register the destination directory
#
# Arguments:
#     dirname     Name of a directory that will contain the input files
#
# Note:
#     The directory must exist at run time!
#
proc destinationDirectory {dirname} {
    global files

    set files(destdir) $dirname

#   if { ! [file exists $dirname] || ! [file isdirectory $dirname] } {
#       errorMessage run "Destination directory does not exist: $dirname"
#   }
}

# main --
#     Read the input, generate the cases and run the program
#
#     TODO: safe interpreter
#
source [lindex $argv 0]

if { $files(sequential) } {
    set files(continue)  1
    set files(casecount) 0
    while { $files(continue) } {
        prepareCaseAndRun
    }
} else {
    prepareCases
    runProgram
}