run

A module for running experiments that involve the execution of commands with different combinations of command line arguments.

Read on for an explanation with a bunch of examples, check out the reference documentation, or download the code.

Basic Concepts

  1. Generate Argument Combinations. Instead of specifying all combinations of command line arguments for your experiment explicitly, you specify the sets of values for each argument. A set of runs with one run for each combination is generated automatically.

  2. Everything is a Blob. At its core a blob is something that evaluates to a string based on the arguments of a single run. In most cases, a blob is simply a string that can contain wildcards for these arguments.

  3. Skip Unnecessary Runs. Runs are skipped if the produced output file already exists. This is particularly helpful if the experiment setup evolves over time.

  4. Postprocessing the Output. If the command prints its result to stdout, you can specify how it should be saved to a file. This lets you, e.g., add some reformatting procedure or handle timeouts.

Usage

Your experiment script should look somewhat like this:

# example.py
import run

run.add(...)
run.add(...)
...
run.run()

The method run.add() has three mandatory parameters and multiple keyword parameters. The first parameter specifies the name of the experiment. This is used so you can select which experiments to run, e.g., calling python example.py name1 name2 runs all experiments where the name was name1 or name2 (experiments can also be grouped). The second parameter specifies the command that should be called and the third parameter is a description of the arguments the command should be called with. Their usage is explained in the next section.

Generate Argument Combinations

Here is an easy example for an experiment that create some files by calling the touch command.

run.add(
    "create_files",
    "touch output/a=[[a]]_b=[[b]]_c=[[c]].txt",
    {'a': [1, 2], 'b': [1, 2, 3], 'c': 0}
)

The third parameter specifies the three arguments a, b, and c. As a and b are lists, one run is created for each combination of arguments. The second parameter specifies the command for each run. It is a blob, i.e., the wildcard of the form [[a]] is replaced with the corresponding value for a in each of the runs. Thus, the above experiment calls touch six times creating the following files.

$ ls output/
'a=1_b=1_c=0.txt'  'a=1_b=2_c=0.txt'  'a=1_b=3_c=0.txt'
'a=2_b=1_c=0.txt'  'a=2_b=2_c=0.txt'  'a=2_b=3_c=0.txt'

Note: In case your experiment involves some dependencies between the parameters that make some of the combinations invalid, you can filter those out.

Everything is a Blob

In the above example, the command (second parameter) is a blob, i.e., all wildcards are replaced with the appropriate values for each run. Besides being a string with wildcards, a blob can also be a function that returns a string with wildcards. In this case, it is called with the arguments of the current run as parameter, before the wildcards are replaced. See Blobs for more details.

Almost all parameters of run.add() are blobs and are thus evaluated based on the arguments of each individual run. Even the arguments themselves can be blobs based on earlier arguments. The next section contains an example for this. Moreover, there are some more details on blobs below.

Skip Unnecessary Runs

In the above example, run does not know that the runs create files. Thus, the touch commands is called even if the files already exist. You can use the optimal parameter creates_file to explicitly state which file is created by a run. It is a blob, so in the above example you could add creats_file="output/a=[[a]]_b=[[b]]_c=[[c]].txt". Equivalently, you can achieve the same in the following more elegant way, using that the arguments themselves are blobs.

run.add(
    "create_files",
    "touch [[file]]",
    {'a': [1, 2], 'b': [1, 2, 3], 'c': 0,
     'file': "output/a=[[a]]_b=[[b]]_c=[[c]].txt"},
    creates_file="[[file]]"
)

Now executing the script calls touch only for those files that do not already exist. For more details on when exactly a run is skipped, see Policy of Skipping Runs.

Note: Specifying creates_file is only necessary if the command itself creates the file. If instead the command's output to stdout is written to a file (see next section), run already knows which file is created.

Postprocessing the Output

If the parameter stdout_file is provided, the command's output to stdout is written to the corresponding file. This is illustrated by the following example.

run.add(
    "sum",
    "echo $(([[a]] + [[b]]))",
    {'a': [1, 2], 'b': [1, 2, 3]},
    stdout_file="output/[[a]]+[[b]].txt"
)

The echo command itself adds the two given values and writes them to standard output. The run module takes this result and writes it to the provided file instead. Thus, executing this example creates six files with different values for a and b, each of which contains the sum of a and b as content.

You can postprocess stdout (given as string) by providing the parameters stdout_mod (a function returning a blob) or stdout_res (a blob). If both are provided, they are applied in this order.

The function stdout_mod gets stdout as input its return value replaces stdout. This can serve as some kind of parser that, e.g., reformats the output from something human readable to csv. Additionally, if the function takes two parameters, the second parameter is the result of the call to subprocess.run(). This can be useful to do a more sophisticated reformatting not only based on stdout but also, e.g., on the return code or stderr.

If the blob stdout_res is given, this blob is written to the file instead of stdout itself. This blob is somewhat special as it allows for the additional argument stdout. The following example uses stdout_res with the wildcard [[stdout]].

run.add(
    "sum",
    "echo $(([[a]] + [[b]]))",
    {'a': [1, 2], 'b': [3, 4]},
    stdout_file="output/sums.txt",
    stdout_res="[[a]] + [[b]] = [[stdout]]"
)

This produces the following file.

$ cat output/sums.txt 
2 + 3 = 5
2 + 4 = 6
1 + 3 = 4
1 + 4 = 5

Note that the output of the individual runs was appended to the file, i.e., the runs were not skipped. This is the intended behavior to support these kind of experiment setups; see Policy of Skipping Runs for more details on when exactly a run is skipped.

Note: There are several cases where stdout_mod and stdout_res can both be used to achieve the same thing. You can use this rule of thumb:

  • Use stdout_mod if you want to reformat stdout or if you need access to things like the return code.

  • Use stdout_res if you want to combine stdout with the input arguments.

  • Use both if you need both.

Note: When using stdout_file, one can additionally write a header to the first line of the file.

Details

When you execute your experiment script, the following steps are performed in this order.

  1. Generate Combinations. For each experiment, create the cross product of all argument lists provided in third parameter of run.add(). For each argument combination that is not rejected by combinations_filter, a single run is created. Each individual argument is a blob.

  2. Deblob. For each individual run, the blobs are turned into strings. This is first done for all arguments in the order they are given. Then all other blobs (except stdout_res) are deblobbed.

  3. Skipping Runs. For each run, it is decided whether to skip the run. A run is skipped if the file it would produce (specified by creates_file or stdout_file) already exists at this stage in the process.

  4. Filter Unselected Runs. A run counts as selected if its experiments name or group was used as command line parameter to execute your experiment script. All other runs are ignored.

  5. Run and Output Results. The runs are executed and, depending on the stdout settings and the header settings, the output is written.

Blobs

Deblobbing happens in three steps in the function run.deblob().

  1. If the blob is a function, it is called with one parameter args. args is a dictionary mapping from the argument names to the value. Note that this is done after generating the combinations, i.e., the argument values are no longer lists.

  2. The result of step 1. (or the blob itself if it is not a function) is converted into a string. Thus, it is ok for an argument to have, e.g., integer values or for the above function to return integer values.

  3. In the resulting string, each pattern of the form [[key]] is replaced with the corresponding value in args.

The following example is similar to one of the previous examples, extending the computation of the sum to also compute differences, products, and quotients. It gives an example of how to make use of the fact that blobs can be functions. Note how the output file name (stdout_file) of each run depends on the operator. As the argument combinations are generated before deblobbing, args['operator'] in the lambda function evaluates to one of the operators (and not to the list of all operators). Running this, creates the four files sum.txt, diff.txt, prod.txt, and quot.txt, each containing the respective calculations.

res_name = {'+': "sum", '-': "diff", '*': "prod", '/': "quot"}
run.add(
    "calculate",
    "echo $(([[a]] [[operator]] [[b]]))",
    {'a': [1, 2], 'b': [3, 4],
     'operator': ["+", "-", "*", "/"]
     },
    stdout_file=lambda args: "output/" + res_name[args['operator']] + ".txt",
    stdout_res="[[a]] [[operator]] [[b]] = [[stdout]]"
)

The next example highlights how the order of evaluation matters when using blobs for the arguments. Assume we have three arguments a, b, and c that depend on each other, i.e., we want to consider not all but only certain combinations of values. In this example, we want a, b, and c to be all combinations of natural numbers with a + b + c = 4. In the example below, this is done using the helper argument triple, which is a list of triples. Then the actual arguments a, b, and c use these triples. Note that blobs of a, b, and c are deblobbed (i.e., their lambda functions are called) after lists have been split into separate runs but before the triple argument was deblobbed. Thus, args['triple'] is a dictionary of the form {'a': a, 'b': b, 'c': c}. Moreover, a, b, and c are deblobbed before file, which makes them usable as wildcards.

run.add(
    "sum_of_squares",
    "echo $(([[a]] * [[a]] + [[b]] * [[b]] + [[c]] * [[c]]))",
    {'a': lambda args: args['triple']['a'],
     'b': lambda args: args['triple']['b'],
     'c': lambda args: args['triple']['c'],
     'triple': [{'a': a, 'b': b, 'c': c}
                for a in range(0, 5) for b in range(0, 5) for c in range(0, 5)
                if a + b + c == 4],
     'file': "output/sum_of_squares_[[a]]_[[b]]_[[c]].txt"
     },
    stdout_file="[[file]]"
)

Changing the order could have unwanted effects. Moving the argument triple before a would first deblob triple, before calling the lambda function, which means that args['triple'] would be the string representation of the dictionary instead of the dictionary itself. Moreover, having file before a would make the use of '[[a]]' invalid, as the argument a would not be a string but a function when trying to do the replacement.

As a rule of thumb: Arguments you use as wild cards should go before, arguments you want to use in their original form (not in their string representation) within a function should go after.

Note: In the above example one could of course remove the file argument and directly assign its blob to stdout_file, as the arguments are always deblobbed before any other blobs (like stdout_file).

Filtering the Combinations

The previous example showed a somewhat hacky way of only looking at those combinations of arguments a, b, and c where a + b + c = 4. This can be achieved much easier using the parameter combinations_filter. It is a function that gets called for every argument combination and decides whether it is valid (in which case a run is created) or not (in which case the combination is discarded).

run.add(
    "better_sum_of_squares",
    "echo $(([[a]] * [[a]] + [[b]] * [[b]] + [[c]] * [[c]]))",
    {'a': list(range(0, 5)), 'b': list(range(0, 5)), 'c': list(range(0, 5))},
    stdout_file="output/sum_of_squares_[[a]]_[[b]]_[[c]]_good.txt",
    combinations_filter=lambda args: args['a'] + args['b'] + args['c'] == 4
)

Policy of Skipping Runs

From the order of steps discussed above, it becomes implicitly clear that runs are only skipped if the result files already exist at the time the script is executed. Thus, if two runs have the same non-existent output file, they are both run, even if the file exists when the second run starts. To make this more explicit, here is the behavior of writing files when using the stdout_file parameter.

  1. If the file exists when adding the experiment via run.add() (i.e., before running any experiment), then the run is skipped.

  2. If the run is not skipped but the file exists when executing the run, stdout is added at the end of the file.

  3. If the file does not exist, it is created beginning with the header (if specified) and then stdout is added.

Thus, instead of writing one file per run, one can also have multiple runs write their output into a single file. This can be the intended behavior if, e.g., the output of the same algorithm on multiple instances should appear in the same csv file. The disadvantage of this is that either all or none of the experiments creating this file are skipped, so extending the experiment would require to rerun it (which might be ok, depending on your use case).

Headers

When using stdout_file, one can use the parameter header_string (a blob) to specify a header that should appear in the first line of the file. Alternatively, one can specify header_command (again, a blob), which is a command that is executed and its output (to stdout) is used as header. The resulting header can be modified by specifying the parameter header_mod. It should be a function that takes the unmodified header as input and outputs the actual (modified) header.

Using header_command instead of simply header_string is advantageous in the following scenario. Assume you have an executable called algo that runs a certain algorithm on an input file and outputs some statistics using comma-separated values. Moreover, assume algo is capable of printing the corresponding header by calling algo --only-header. Then the following example first writes the header (obtained by calling algo --only-header) followed by the results of running algo on three input files.

run.add(
    "algo",
    "algo [[input]]",
    {'input': ["file1", "file2", "file3"]},
    stdout_file="output.csv",
    header_command="algo --only-header"
)

The advantage of this is that the knowledge about the output of algo is concentrated at the algo executable, i.e., the experiment script does not have to change if the statistics printed by algo change. Moreover, using header_command over header_string becomes even more advantageous, when running multiple commands, e.g., algo1 and algo2, with the same command line interface (but potentially very different outputs) on the same set of files. This is illustrated by the following example (note the usage of the wildcard [[algo]] in the command (second parameter) and the header_command).

run.add(
    "algos",
    "[[algo]] [[input]]",
    {'algo': ["algo1", "algo2"],
     'input': ["file1", "file2", "file3"]},
    stdout_file="output_[[algo]].csv",
    header_command="[[algo]] --only-header"
)

Error Handling

This section is only about errors that happen when executing a run. If you have an error in your experiment specification, then you probably just get a python error you have to deal with.

You can specify the allowed return codes of an experiment via the parameter allowed_return_codes. The default is [0]. If the return code of the command is not in the list of allowed return codes, a warning is printed and the run is aborted, meaning that stdout is not written to the file given by stdout_file. You can set allowed_return_codes to [] to indicate that every return code should be accepted (you probably should not do this).

In case different return codes are possible, you probably want to postprocess the result by using stdout_mod or stdout_res. With the former, you have access to the return code; with the latter you only have the output of the command; see the section about postprocessing the standard output.

In the following example, sleep [[time]] && echo waking up waits for 0 to 4 seconds and then outputs waking up. The command timeout 2 aborts this after 2 seconds and returns with code 124 and no output to stdout. The return codes 0 and 124 are allowed by setting allowed_return_codes=[0, 124]. Moreover, the blob stdout_res specifies the output depending on whether there was a timeout.

run.add(
    "timeouts",
    "timeout 2 sleep [[time]] && echo waking up",
    {'time': [0, 1, 2, 3, 4]},
    stdout_file="output/timeouts.txt",
    allowed_return_codes=[0, 124],
    stdout_res=lambda args: (
        "sleeping [[time]]s -> [[stdout]]" if args['stdout'] != "" else
        "sleeping [[time]]s -> timeout")
)

This produces the following file.

$ cat output/timeouts.txt 
sleeping 0s -> waking up
sleeping 1s -> waking up
sleeping 3s -> timeout
sleeping 4s -> timeout
sleeping 2s -> timeout

The above example relies on the fact that the standard output is the empty string in case of an error. The following yields the same result but explicitly checks for the return code using stdout_mod instead of stdout_res.

run.add(
    "timeouts",
    "timeout 2 sleep [[time]] && echo waking up",
    {"time": [0, 1, 2, 3, 4]},
    stdout_file="output/timeouts.txt",
    allowed_return_codes=[0, 124],
    stdout_mod=lambda out, res: (
        "sleeping [[time]]s -> [[stdout]]"
        if res.returncode == 0
        else "sleeping [[time]]s -> timeout"
    ),
)

In the above example, one could also use the parameter out directly instead of using the wildcard [[stdout]].

Miscellaneous Features

There are some features that are not at the core of run in the sense that they are independent of how you specify your experiments. Nonetheless they are pretty useful and should be mentioned here.

Experiment Names and Groups

When running the experiment script with all examples from above by calling python example.py create_files calculate, the output looks somewhat like this:

Example Run 1

You can see that a bunch of experiments are available. The two selected ones are highlighted in green (here create_files and calculate), all others are red.

To select which experiments to run more conveniently, you can group experiments using the run.group() function as follows.

# example.py
import run

run.group("group_name_a")
run.add("exp1", ...)
run.add("exp2", ...)
...
run.group("group_name_b")
run.add("exp3", ...)
run.add("exp4", ...)
...
run.run()

By running the script with a group name as parameter, all experiments in that group are run, e.g., python example.py group_name_a runs the experiments "exp1" and "exp2".

Experiment names are of course also blobs, so you can do the following to create experiments with four different names in one group by calling run.add() just once.

res_name = {'+': "sum", '-': "diff", '*': "prod", '/': "quot"}
run.group("calculations")
run.add(
    "calculate_[[op_name]]",
    "echo $(([[a]] [[operator]] [[b]]))",
    {'a': [1, 2], 'b': [3, 4],
     'operator': ["+", "-", "*", "/"],
     'op_name': lambda args: res_name[args['operator']]
     },
    stdout_file="output/result_[[op_name]].txt",1
    stdout_res="[[a]] [[operator]] [[b]] = [[stdout]]"
)

Adding some more groups and calling python example.py sum sum_of_squares calculations yields the following output.

Example Run 1

Wildcard matching for experiment names

When specifying the names of experiments to be run, it is also possible to use shell wildcards (* and ?). Note that in most shells the names need to be put into quotes in order to not be evaluated before being passed to python.

Example Wildcard-Run 1

Parallelization

Runs from the same experiment are run in parallel and you can choose how many threads to use by calling run.use_cores(nr_cores). By default, 4 cores are used.

Note: There is no parallelization between different experiments, i.e., in the above example, all runs from sum have to finish before the runs of sum_of_squares start.

Note: Files are locked for writing, i.e., it is ok for multiple runs of the same experiment to write to the same file. It is also made sure that the header is written only once. If you abort an experiment by interrupting it with ctrl + c there might be a leftover *.lock file, which you have to remove manually.

Return String

The method run.add() can return a list of strings, one for each run. This can, e.g., be helpful if each run of the experiment generates a file and you want to perform an additional task on each of these files in a later experiment.

To use this feature, set the parameter return_string of run.add(). It is a blob.

Dry Run

If you execute your experiment script with dry_run as parameter, the runs are not executed. Instead it is printed to stdout which runs would have been called.

Creating Directories

If you use stdout_file and the directory where the output file should be stored does not exist, it is created.

Multiple Runs in One Script

You can call run.run() multiple times in one script. Each call after the first considers only the runs added after the previous call. This is useful when there are some experiments that create files and then later experiments do something for each file created earlier. This is demonstrated in the following example, where create_files creates some .txt files while copy_files copies all .txt files that are found by glob.glob("output/*.txt").

run.add(
    "create_files",
    "touch [[file]]",
    {"a": [1, 2], "b": [1, 2, 3], "c": 0, "file": "output/a=[[a]]_b=[[b]]_c=[[c]].txt"},
    creates_file="[[file]]",
)
run.run()

import glob
files = glob.glob("output/*.txt")

run.add(
    "copy_files",
    "cp [[file]] [[copy]]",
    {"file": files, "copy": "[[file]].copy"},
    creates_file="[[copy]]",
)
run.run()

Without the first call to run.run(), glob.glob() would not find the files created by create_file as they are not yet created. Thus, one would have to call the script twice to do both experiments. With the additional call to run.run(), one can run create_files and copy_files in one execution of the script.

Section Headlines

You can call run.section() to print a section title (e.g., to structure the output in case of multiple run calls).

Interface Documentation

  1'''
  2A module for running experiments that involve the execution of
  3commands with different combinations of command line arguments.
  4
  5Read on for an explanation with a bunch of examples, check out the
  6[reference documentation](./run.html), or download the
  7[code](https://github.com/thobl/run).
  8
  9# Basic Concepts
 10
 111. **Generate Argument Combinations.** Instead of specifying all
 12   combinations of command line arguments for your experiment
 13   explicitly, you specify the sets of values for each argument.  A
 14   set of runs with one run for each combination is generated
 15   automatically.
 16
 171. **Everything is a Blob.** At its core a blob is something that
 18   evaluates to a string based on the arguments of a single run.  In
 19   most cases, a blob is simply a string that can contain wildcards
 20   for these arguments.
 21
 221. **Skip Unnecessary Runs.** Runs are skipped if the produced output
 23   file already exists.  This is particularly helpful if the
 24   experiment setup evolves over time.
 25
 261. **Postprocessing the Output.** If the command prints its result to
 27   ``stdout``, you can specify how it should be saved to a file.  This
 28   lets you, e.g., add some reformatting procedure or handle timeouts.
 29
 30# Usage
 31
 32Your experiment script should look somewhat like this:
 33
 34```python
 35# example.py
 36import run
 37
 38run.add(...)
 39run.add(...)
 40...
 41run.run()
 42```
 43
 44The method ``run.add()`` has three mandatory parameters and multiple
 45keyword parameters.  The first parameter specifies the name of the
 46experiment.  This is used so you can select which experiments to run,
 47e.g., calling ``python example.py name1 name2`` runs all experiments
 48where the name was ``name1`` or ``name2`` (experiments can also be
 49[grouped](#experiment-names-and-groups)).  The second parameter
 50specifies the command that should be called and the third parameter is
 51a description of the arguments the command should be called with.
 52Their usage is explained in the [next
 53section](#generate-argument-combinations).
 54
 55## Generate Argument Combinations
 56
 57Here is an easy example for an experiment that create some files by
 58calling the ``touch`` command.
 59
 60```python
 61run.add(
 62    "create_files",
 63    "touch output/a=[[a]]_b=[[b]]_c=[[c]].txt",
 64    {'a': [1, 2], 'b': [1, 2, 3], 'c': 0}
 65)
 66```
 67
 68The third parameter specifies the three arguments ``a``, ``b``, and
 69``c``.  As ``a`` and ``b`` are lists, one run is created for each
 70combination of arguments.  The second parameter specifies the command
 71for each run.  It is a blob, i.e., the wildcard of the form ``[[a]]``
 72is replaced with the corresponding value for ``a`` in each of the
 73runs.  Thus, the above experiment calls ``touch`` six times creating
 74the following files.
 75
 76```shell
 77$ ls output/
 78'a=1_b=1_c=0.txt'  'a=1_b=2_c=0.txt'  'a=1_b=3_c=0.txt'
 79'a=2_b=1_c=0.txt'  'a=2_b=2_c=0.txt'  'a=2_b=3_c=0.txt'
 80```
 81
 82**Note:** In case your experiment involves some dependencies between
 83the parameters that make some of the combinations invalid, you can
 84[filter those out](#filtering-the-combinations).
 85
 86## Everything is a Blob
 87
 88In the above example, the command (second parameter) is a blob, i.e.,
 89all wildcards are replaced with the appropriate values for each run.
 90Besides being a string with wildcards, a blob can also be a function
 91that returns a string with wildcards.  In this case, it is called with
 92the arguments of the current run as parameter, before the wildcards
 93are replaced.  See [Blobs](#blobs) for more details.
 94
 95Almost all parameters of `run.add()` are blobs and are thus evaluated
 96based on the arguments of each individual run.  Even the arguments
 97themselves can be blobs based on earlier arguments.  The [next
 98section](#skip-unnecessary-runs) contains an example for this.
 99Moreover, there are some more details on blobs [below](#blobs).
100
101## Skip Unnecessary Runs
102
103In the above example, ``run`` does not know that the runs create
104files.  Thus, the ``touch`` commands is called even if the files
105already exist.  You can use the optimal parameter ``creates_file`` to
106explicitly state which file is created by a run.  It is a blob, so in
107the above example you could add
108``creats_file="output/a=[[a]]_b=[[b]]_c=[[c]].txt"``.  Equivalently,
109you can achieve the same in the following more elegant way, using that
110the arguments themselves are blobs.
111
112```python
113run.add(
114    "create_files",
115    "touch [[file]]",
116    {'a': [1, 2], 'b': [1, 2, 3], 'c': 0,
117     'file': "output/a=[[a]]_b=[[b]]_c=[[c]].txt"},
118    creates_file="[[file]]"
119)
120```
121
122Now executing the script calls ``touch`` only for those files that do
123not already exist.  For more details on when exactly a run is skipped,
124see [Policy of Skipping Runs](#policy-of-skipping-runs).
125
126**Note:** Specifying ``creates_file`` is only necessary if the command
127itself creates the file.  If instead the command's output to ``stdout``
128is written to a file (see [next section](#postprocessing-the-output)),
129``run`` already knows which file is created.
130
131## Postprocessing the Output
132
133If the parameter ``stdout_file`` is provided, the command's output to
134``stdout`` is written to the corresponding file.  This is illustrated
135by the following example.
136
137```python
138run.add(
139    "sum",
140    "echo $(([[a]] + [[b]]))",
141    {'a': [1, 2], 'b': [1, 2, 3]},
142    stdout_file="output/[[a]]+[[b]].txt"
143)
144```
145
146The ``echo`` command itself adds the two given values and writes them
147to standard output.  The run module takes this result and writes it
148to the provided file instead.  Thus, executing this example creates
149six files with different values for a and b, each of which contains
150the sum of a and b as content.
151
152You can postprocess ``stdout`` (given as string) by providing the
153parameters ``stdout_mod`` (a function returning a blob) or
154``stdout_res`` (a blob).  If both are provided, they are applied in
155this order.
156
157The function ``stdout_mod`` gets ``stdout`` as input its return value
158replaces ``stdout``.  This can serve as some kind of parser that,
159e.g., reformats the output from something human readable to csv.
160Additionally, if the function takes two parameters, the second
161parameter is the result of the call to
162[``subprocess.run()``](https://docs.python.org/3/library/subprocess.html#subprocess.run).
163This can be useful to do a more sophisticated reformatting not only
164based on ``stdout`` but also, e.g., on the return code or stderr.
165
166If the blob ``stdout_res`` is given, this blob is written to the file
167instead of ``stdout`` itself.  This blob is somewhat special as it
168allows for the additional argument ``stdout``.  The following example
169uses ``stdout_res`` with the wildcard ``[[stdout]]``.
170
171```python
172run.add(
173    "sum",
174    "echo $(([[a]] + [[b]]))",
175    {'a': [1, 2], 'b': [3, 4]},
176    stdout_file="output/sums.txt",
177    stdout_res="[[a]] + [[b]] = [[stdout]]"
178)
179```
180
181This produces the following file.
182
183``` shell
184$ cat output/sums.txt 
1852 + 3 = 5
1862 + 4 = 6
1871 + 3 = 4
1881 + 4 = 5
189```
190
191Note that the output of the individual runs was appended to the file,
192i.e., the runs were not skipped.  This is the intended behavior to
193support these kind of experiment setups; see [Policy of Skipping
194Runs](#policy-of-skipping-runs) for more details on when exactly a run
195is skipped.
196
197**Note:** There are several cases where ``stdout_mod`` and
198``stdout_res`` can both be used to achieve the same thing.  You can
199use this rule of thumb:
200
201 * Use ``stdout_mod`` if you want to reformat ``stdout`` or if you
202   need access to things like the return code.
203
204 * Use ``stdout_res`` if you want to combine ``stdout`` with the input
205   arguments.
206
207 * Use both if you need both.
208 
209**Note:** When using ``stdout_file``, one can additionally [write a
210header](#headers) to the first line of the file.
211
212# Details
213
214When you execute your experiment script, the following steps are
215performed in this order.
216
2171. **Generate Combinations.** For each experiment, create the cross
218   product of all argument lists provided in third parameter of
219   ``run.add()``.  For each argument combination that is not rejected
220   by [``combinations_filter``](#filtering-the-combinations), a single
221   run is created.  Each individual argument is a blob.
222
2231. **Deblob.** For each individual run, the [blobs are turned into
224   strings](#blobs).  This is first done for all arguments in the
225   order they are given.  Then all other blobs (except ``stdout_res``)
226   are deblobbed.
227
2281. **Skipping Runs.** For each run, it is decided whether to skip the
229   run.  A run is skipped if the file it would produce (specified by
230   ``creates_file`` or ``stdout_file``) already exists at this stage
231   in the process.
232
2331. **Filter Unselected Runs.** A run counts as selected if its
234   experiments [name or group](#experiment-names-and-groups) was used
235   as command line parameter to execute your experiment script.  All
236   other runs are ignored.
237   
2381. **Run and Output Results.** The runs are executed and, depending on
239   the [stdout settings](#postprocessing-the-output) and the [header
240   settings](#headers), the output is written.
241
242
243## Blobs
244
245Deblobbing happens in three steps in the function ``run.deblob()``.
246
2471. If the blob is a function, it is called with one parameter
248   ``args``.  ``args`` is a dictionary mapping from the argument names
249   to the value.  Note that this is done after generating the
250   combinations, i.e., the argument values are no longer lists.
251
2521. The result of step 1. (or the blob itself if it is not a function)
253   is converted into a string.  Thus, it is ok for an argument to
254   have, e.g., integer values or for the above function to return
255   integer values.
256
2571. In the resulting string, each pattern of the form ``[[key]]`` is
258   replaced with the corresponding value in ``args``.
259
260The following example is similar to one of the previous examples,
261extending the computation of the sum to also compute differences,
262products, and quotients.  It gives an example of how to make use of
263the fact that blobs can be functions.  Note how the output file name
264(``stdout_file``) of each run depends on the operator.  As the
265argument combinations are generated before deblobbing,
266``args['operator']`` in the lambda function evaluates to one of the
267operators (and not to the list of all operators).  Running this,
268creates the four files ``sum.txt``, ``diff.txt``, ``prod.txt``, and
269``quot.txt``, each containing the respective calculations.
270
271```python
272res_name = {'+': "sum", '-': "diff", '*': "prod", '/': "quot"}
273run.add(
274    "calculate",
275    "echo $(([[a]] [[operator]] [[b]]))",
276    {'a': [1, 2], 'b': [3, 4],
277     'operator': ["+", "-", "*", "/"]
278     },
279    stdout_file=lambda args: "output/" + res_name[args['operator']] + ".txt",
280    stdout_res="[[a]] [[operator]] [[b]] = [[stdout]]"
281)
282```
283
284The next example highlights how the order of evaluation matters when
285using blobs for the arguments.  Assume we have three arguments ``a``,
286``b``, and ``c`` that depend on each other, i.e., we want to consider
287not all but only certain combinations of values.  In this example, we
288want ``a``, ``b``, and ``c`` to be all combinations of natural numbers
289with ``a + b + c = 4``.  In the example below, this is done using the
290helper argument ``triple``, which is a list of triples.  Then the
291actual arguments ``a``, ``b``, and ``c`` use these triples.  Note that
292blobs of ``a``, ``b``, and ``c`` are deblobbed (i.e., their lambda
293functions are called) after lists have been split into separate runs
294but before the ``triple`` argument was deblobbed.  Thus,
295``args['triple']`` is a dictionary of the form ``{'a': a, 'b': b, 'c':
296c}``.  Moreover, ``a``, ``b``, and ``c`` are deblobbed before
297``file``, which makes them usable as wildcards.
298
299```python
300run.add(
301    "sum_of_squares",
302    "echo $(([[a]] * [[a]] + [[b]] * [[b]] + [[c]] * [[c]]))",
303    {'a': lambda args: args['triple']['a'],
304     'b': lambda args: args['triple']['b'],
305     'c': lambda args: args['triple']['c'],
306     'triple': [{'a': a, 'b': b, 'c': c}
307                for a in range(0, 5) for b in range(0, 5) for c in range(0, 5)
308                if a + b + c == 4],
309     'file': "output/sum_of_squares_[[a]]_[[b]]_[[c]].txt"
310     },
311    stdout_file="[[file]]"
312)
313```
314
315Changing the order could have unwanted effects.  Moving the argument
316``triple`` before ``a`` would first deblob ``triple``, before calling
317the lambda function, which means that ``args['triple']`` would be the
318string representation of the dictionary instead of the dictionary
319itself.  Moreover, having ``file`` before ``a`` would make the use of
320``'[[a]]'`` invalid, as the argument ``a`` would not be a string but a
321function when trying to do the replacement.
322
323As a rule of thumb: Arguments you use as wild cards should go before,
324arguments you want to use in their original form (not in their string
325representation) within a function should go after.
326
327**Note:** In the above example one could of course remove the ``file``
328argument and directly assign its blob to ``stdout_file``, as the
329arguments are always deblobbed before any other blobs (like
330``stdout_file``).
331
332## Filtering the Combinations
333
334The previous example showed a somewhat hacky way of only looking at
335those combinations of arguments ``a``, ``b``, and ``c`` where ``a +
336b + c = 4``.  This can be achieved much easier using the parameter
337``combinations_filter``.  It is a function that gets called for every
338argument combination and decides whether it is valid (in which case a
339run is created) or not (in which case the combination is discarded).
340
341```python
342run.add(
343    "better_sum_of_squares",
344    "echo $(([[a]] * [[a]] + [[b]] * [[b]] + [[c]] * [[c]]))",
345    {'a': list(range(0, 5)), 'b': list(range(0, 5)), 'c': list(range(0, 5))},
346    stdout_file="output/sum_of_squares_[[a]]_[[b]]_[[c]]_good.txt",
347    combinations_filter=lambda args: args['a'] + args['b'] + args['c'] == 4
348)
349```
350
351## Policy of Skipping Runs
352
353From the [order of steps](#details) discussed above, it becomes
354implicitly clear that runs are only skipped if the result files
355already exist at the time the script is executed.  Thus, if two runs
356have the same non-existent output file, they are both run, even if the
357file exists when the second run starts.  To make this more explicit,
358here is the behavior of writing files when using the ``stdout_file``
359parameter.
360
3611. If the file exists when adding the experiment via ``run.add()``
362   (i.e., before running any experiment), then the run is skipped.
363
3641. If the run is not skipped but the file exists when executing the
365   run, ``stdout`` is added at the end of the file.
366
3671. If the file does not exist, it is created beginning with the
368   [header](#headers) (if specified) and then ``stdout`` is added.
369   
370Thus, instead of writing one file per run, one can also have multiple
371runs write their output into a single file.  This can be the intended
372behavior if, e.g., the output of the same algorithm on multiple
373instances should appear in the same csv file.  The disadvantage of
374this is that either all or none of the experiments creating this file
375are skipped, so extending the experiment would require to rerun it
376(which might be ok, depending on your use case).
377			   
378## Headers
379
380When using ``stdout_file``, one can use the parameter
381``header_string`` (a blob) to specify a header that should appear in
382the first line of the file.  Alternatively, one can specify
383``header_command`` (again, a blob), which is a command that is
384executed and its output (to ``stdout``) is used as header.  The
385resulting header can be modified by specifying the parameter
386``header_mod``.  It should be a function that takes the unmodified
387header as input and outputs the actual (modified) header.
388
389Using ``header_command`` instead of simply ``header_string`` is
390advantageous in the following scenario.  Assume you have an executable
391called ``algo`` that runs a certain algorithm on an input file and
392outputs some statistics using comma-separated values.  Moreover,
393assume ``algo`` is capable of printing the corresponding header by
394calling ``algo --only-header``.  Then the following example first
395writes the header (obtained by calling ``algo --only-header``)
396followed by the results of running ``algo`` on three input files.
397
398```python
399run.add(
400    "algo",
401    "algo [[input]]",
402    {'input': ["file1", "file2", "file3"]},
403    stdout_file="output.csv",
404    header_command="algo --only-header"
405)
406```
407
408The advantage of this is that the knowledge about the output of
409``algo`` is concentrated at the ``algo`` executable, i.e., the
410experiment script does not have to change if the statistics printed by
411``algo`` change.  Moreover, using ``header_command`` over
412``header_string`` becomes even more advantageous, when running
413multiple commands, e.g., ``algo1`` and ``algo2``, with the same
414command line interface (but potentially very different outputs) on the
415same set of files.  This is illustrated by the following example (note
416the usage of the wildcard ``[[algo]]`` in the command (second
417parameter) and the ``header_command``).
418
419```python
420run.add(
421    "algos",
422    "[[algo]] [[input]]",
423    {'algo': ["algo1", "algo2"],
424     'input': ["file1", "file2", "file3"]},
425    stdout_file="output_[[algo]].csv",
426    header_command="[[algo]] --only-header"
427)
428```
429
430## Error Handling
431
432This section is only about errors that happen when executing a run.
433If you have an error in your experiment specification, then you
434probably just get a python error you have to deal with.
435
436You can specify the allowed return codes of an experiment via the
437parameter ``allowed_return_codes``.  The default is ``[0]``.  If the
438return code of the command is not in the list of allowed return codes,
439a warning is printed and the run is aborted, meaning that ``stdout``
440is **not** written to the file given by ``stdout_file``.  You can set
441``allowed_return_codes`` to ``[]`` to indicate that every return code
442should be accepted (you probably should not do this).
443
444In case different return codes are possible, you probably want to
445postprocess the result by using ``stdout_mod`` or ``stdout_res``.
446With the former, you have access to the return code; with the latter
447you only have the output of the command; see the section about
448[postprocessing the standard output](#postprocessing-the-output).
449
450In the following example, ``sleep [[time]] && echo waking up`` waits
451for 0 to 4 seconds and then outputs ``waking up``.  The command
452``timeout 2`` aborts this after 2 seconds and returns with code
453``124`` and no output to ``stdout``.  The return codes ``0`` and
454``124`` are allowed by setting ``allowed_return_codes=[0, 124]``.
455Moreover, the blob ``stdout_res`` specifies the output depending on
456whether there was a timeout.
457
458```python
459run.add(
460    "timeouts",
461    "timeout 2 sleep [[time]] && echo waking up",
462    {'time': [0, 1, 2, 3, 4]},
463    stdout_file="output/timeouts.txt",
464    allowed_return_codes=[0, 124],
465    stdout_res=lambda args: (
466        "sleeping [[time]]s -> [[stdout]]" if args['stdout'] != "" else
467        "sleeping [[time]]s -> timeout")
468)
469```
470This produces the following file.
471
472```shell
473$ cat output/timeouts.txt 
474sleeping 0s -> waking up
475sleeping 1s -> waking up
476sleeping 3s -> timeout
477sleeping 4s -> timeout
478sleeping 2s -> timeout
479```
480
481The above example relies on the fact that the standard output is the
482empty string in case of an error.  The following yields the same
483result but explicitly checks for the return code using ``stdout_mod``
484instead of ``stdout_res``.
485
486```python
487run.add(
488    "timeouts",
489    "timeout 2 sleep [[time]] && echo waking up",
490    {"time": [0, 1, 2, 3, 4]},
491    stdout_file="output/timeouts.txt",
492    allowed_return_codes=[0, 124],
493    stdout_mod=lambda out, res: (
494        "sleeping [[time]]s -> [[stdout]]"
495        if res.returncode == 0
496        else "sleeping [[time]]s -> timeout"
497    ),
498)
499```
500
501In the above example, one could also use the parameter ``out``
502directly instead of using the wildcard ``[[stdout]]``.
503
504# Miscellaneous Features
505
506There are some features that are not at the core of `run` in the sense
507that they are independent of how you specify your experiments.
508Nonetheless they are pretty useful and should be mentioned here.
509
510## Experiment Names and Groups
511
512When running the experiment script with all examples from above by
513calling ``python example.py create_files calculate``, the output looks
514somewhat like this:
515
516![Example Run 1](example_run_1.png)
517
518You can see that a bunch of experiments are available.  The two
519selected ones are highlighted in green (here ``create_files `` and
520``calculate``), all others are red.
521
522To select which experiments to run more conveniently, you can group
523experiments using the ``run.group()`` function as follows.
524
525```python
526# example.py
527import run
528
529run.group("group_name_a")
530run.add("exp1", ...)
531run.add("exp2", ...)
532...
533run.group("group_name_b")
534run.add("exp3", ...)
535run.add("exp4", ...)
536...
537run.run()
538```
539
540By running the script with a group name as parameter, all experiments
541in that group are run, e.g., ``python example.py group_name_a`` runs
542the experiments "exp1" and "exp2".
543
544Experiment names are of course also blobs, so you can do the following
545to create experiments with four different names in one group by
546calling ``run.add()`` just once.
547
548```python
549res_name = {'+': "sum", '-': "diff", '*': "prod", '/': "quot"}
550run.group("calculations")
551run.add(
552    "calculate_[[op_name]]",
553    "echo $(([[a]] [[operator]] [[b]]))",
554    {'a': [1, 2], 'b': [3, 4],
555     'operator': ["+", "-", "*", "/"],
556     'op_name': lambda args: res_name[args['operator']]
557     },
558    stdout_file="output/result_[[op_name]].txt",1
559    stdout_res="[[a]] [[operator]] [[b]] = [[stdout]]"
560)
561```
562
563Adding some more groups and calling ``python example.py sum
564sum_of_squares calculations`` yields the following output.
565
566![Example Run 1](example_run_2.png)
567
568## Wildcard matching for experiment names
569
570When specifying the names of experiments to be run, it is also possible to use
571shell wildcards (* and ?).  Note that in most shells the names need to be put
572into quotes in order to not be evaluated before being passed to python.
573
574![Example Wildcard-Run 1](example_run_wildcards.png)
575
576## Parallelization
577
578Runs from the same experiment are run in parallel and you can choose
579how many threads to use by calling ``run.use_cores(nr_cores)``.  By
580default, ``4`` cores are used.
581
582**Note:** There is no parallelization between different experiments,
583i.e., in the above example, all runs from ``sum`` have to finish
584before the runs of ``sum_of_squares`` start.
585
586**Note:** Files are locked for writing, i.e., it is ok for multiple
587runs of the same experiment to write to the same file.  It is also
588made sure that the header is written only once.  If you abort an
589experiment by interrupting it with ``ctrl + c`` there might be a
590leftover ``*.lock`` file, which you have to remove manually.
591
592## Return String
593
594The method ``run.add()`` can return a list of strings, one for each
595run.  This can, e.g., be helpful if each run of the experiment
596generates a file and you want to perform an additional task on each of
597these files in a later experiment.
598
599To use this feature, set the parameter ``return_string`` of
600``run.add()``.  It is a blob.
601
602## Dry Run
603
604If you execute your experiment script with ``dry_run`` as parameter,
605the runs are not executed.  Instead it is printed to ``stdout`` which
606runs would have been called.
607
608## Creating Directories
609
610If you use ``stdout_file`` and the directory where the output file
611should be stored does not exist, it is created.
612
613## Multiple Runs in One Script
614
615You can call ``run.run()`` multiple times in one script.  Each call
616after the first considers only the runs added after the previous call.
617This is useful when there are some experiments that create files and
618then later experiments do something for each file created earlier.
619This is demonstrated in the following example, where ``create_files``
620creates some ``.txt`` files while ``copy_files`` copies all ``.txt``
621files that are found by
622[``glob.glob("output/*.txt")``](https://docs.python.org/3/library/glob.html#glob.glob).
623
624```python
625run.add(
626    "create_files",
627    "touch [[file]]",
628    {"a": [1, 2], "b": [1, 2, 3], "c": 0, "file": "output/a=[[a]]_b=[[b]]_c=[[c]].txt"},
629    creates_file="[[file]]",
630)
631run.run()
632
633import glob
634files = glob.glob("output/*.txt")
635
636run.add(
637    "copy_files",
638    "cp [[file]] [[copy]]",
639    {"file": files, "copy": "[[file]].copy"},
640    creates_file="[[copy]]",
641)
642run.run()
643```
644
645Without the first call to ``run.run()``,
646[``glob.glob()``](https://docs.python.org/3/library/glob.html#glob.glob)
647would not find the files created by ``create_file`` as they are not
648yet created.  Thus, one would have to call the script twice to do both
649experiments.  With the additional call to ``run.run()``, one can run
650``create_files`` and ``copy_files`` in one execution of the script.
651
652## Section Headlines
653
654You can call ``run.section()`` to print a section title (e.g., to
655structure the output in case of multiple run calls).
656
657# [Interface Documentation](./run.html)
658
659'''