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 ...
# 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
# 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
}