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 }