How to Write Your Own Wrapper

If you want to write your own first wrapper, we would like to show you how we would proceed to set up a wrapper for the SAT solver Spear based on our 'generic wrapper' written in Python.

Preface

Our generic wrapper handles already the following points and you do not have to worry about it: * wrapping and execution of a target algorithm (e.g., Spear) with the runsolver to limit the usage of memory and runtime * recognizes timeouts and memouts of target algorithms * provides a mapping from parameter name to parameter value * prints the result for SMAC (or ParamILS) in the appropriated format

1. Set up files

The file genericWrapper.py is an abstract class where only two methods are open for implementation, i.e., get_command_line_args() and process_results() (details are covered later on).

First, we have to prepare a directory with all required files.

  1. mkdir spear-generic-wrapper
  2. cd spear-generic-wrapper
  3. wget aclib.net/smac/tutorial/genericwrapper/wrapperScripts.tar.gz
  4. tar xvfz wrapperScripts.tar.gz

You will find three files:

2. Empty Wrapper

The empty wrapper is a simple class in python which inherits the framework of the 'AbstractWrapper' defined in genericWrapper.py. It has a lot of boilerplate text to explain what is happening:

#!/usr/bin/python
# encoding: utf-8

from genericWrapper import AbstractWrapper

class EmptyWrapper(AbstractWrapper):

    def get_command_line_args(self, runargs, config):
        '''
        Returns the command line call string to execute the target algorithm (here: Spear).
        Args:
            runargs: a map of several optional arguments for the execution of the target algorithm.
                    {
                      "instance": <instance>,
                      "specifics" : <extra data associated with the instance>,
                      "cutoff" : <runtime cutoff>,
                      "runlength" : <runlength cutoff>,
                      "seed" : <seed>
                    }
            config: a mapping from parameter name to parameter value
        Returns:
            A command call list to execute the target algorithm.
        '''
        cmd = "<TODO: fill in your binary> %s" %(runargs["instance"])       
        # TODO: add the paramters in <config> to your cmd
        return cmd

    def process_results(self, filepointer, out_args):
        '''
        Parse a results file to extract the runs status (SUCCESS/CRASHED/etc) and other optional results.

        Args:
            filepointer: a pointer to the file containing the solver execution standard out.
            out_args : a map with {"exit_code" : exit code of target algorithm}
        Returns:
            A map containing the standard AClib run results. The current standard result map as of AClib 2.06 is:
            {
                "status" : <"SUCCESS"/"SAT"/"UNSAT"/"TIMEOUT"/"CRASHED"/"ABORT">,
                "runtime" : <runtime of target algrithm>,
                "quality" : <a domain specific measure of the quality of the solution [optional]>,
                "misc" : <a (comma-less) string that will be associated with the run [optional]>
            }
            ATTENTION: The return values (i.e., status and runtime) will overwrite the measured results of the runsolver (if runsolver was used). 
        '''
        resultMap = {}
        #TODO: parse the output of your solver which can be found in the filepointer <filepointer>
        return resultMap

if __name__ == "__main__":
    wrapper = EmptyWrapper()
    wrapper.main()

So what happens here:

  1. Line 4 imports the 'AbstractWrapper' class from genericWrapper.py
  2. Line 6 defines our class 'EmptyWrapper' which inherits from 'AbstractWrapper'
  3. From Line 8 to 26, we find the method ' get_command_line_args() ' which you have to implement to define how your target algorithm has to be called on the command line the most simple code is already given: In Line 25, the binary path is required; In Line 26, we build the callstring of our target algorithm by concatenate the binary with the problem instance and in the last line, we return the command line string ('cmd')
  4. From Line 28 to 46, we find the method ' process_results() ' which you also have to implement to define how the output of your target algorithm can be interpreted (the real code is only Line 45-46)
  5. The last three lines (48-50) simply create an object of 'EmptyWrapper' and call its method main() which is defined in genericWrapper.py (you don't have to care about it)

3. Example of Spear Wrapper

To create the spearWrapper.py, we simply copied the emptyWrapper.py first:

cp emptyWrapper.py spearWrapper.py

Then we have to make some small changes in spearWrapper.py (see code below)

  1. We 'rename the class' from 'EmptyWrapper' to 'SpearWrapper' in Lines 7 and 61
  2. To 'complete the get_command_line_args() ', we added the following functionalty:
    1. We add the path to our binary of Spear in Line 24
    2. Besides, the binary and the instance, the Spear gets also a random seed which can be found in runargs["seed"] which is also added in Line 25
    3. In Line 26 and 27, we simply added some lines to iterate over all tuples of parameter name and value and it will be added to the command line string (cmd)
  3. To 'complete the process_result() ' method, we added the following functionality:
    1. The package for regular expressions is imported
    2. The complete file is read into the variable data
    3. A map with the results is initialized (resultMap)
    4. With the help of regular expressions, we look whether the string "s SATISFIABLE" or "s UNSATISFIABLE" is in the output of Spear; if it is the case, the status of in the resultMap is set to "SUCCESS" because spear was able to solve the given problem instance successfully; if resultMap does not specify the "status" of the target algorithm, the wrapper assumes the status is timeout, memout or crashed (depending on the runsolver output).
    5. The resultMap is returned by the function


#!/usr/bin/python
# encoding: utf-8

from genericWrapper import AbstractWrapper

class SpearWrapper(AbstractWrapper):

    def get_command_line_args(self, runargs, config):
        '''
        Returns the command line call string to execute the target algorithm (here: Spear).
        Args:
            runargs: a map of several optional arguments for the execution of the target algorithm.
                    {
                      "instance": <instance>,
                      "specifics" : <extra data associated with the instance>,
                      "cutoff" : <runtime cutoff>,
                      "runlength" : <runlength cutoff>,
                      "seed" : <seed>
                    }
            config: a mapping from parameter name to parameter value
        Returns:
            A command call list to execute the target algorithm.
        '''
        binary_path = "./spear"
        cmd = "%s --seed %d --model-stdout --dimacs %s" %(binary_path, runargs["seed"], runargs["instance"])       
        for name, value in config.items():
            cmd += " -%s %s" %(name,  value)
        return cmd

    def process_results(self, filepointer, out_args):
        '''
        Parse a results file to extract the runs status (SUCCESS/CRASHED/etc) and other optional results.

        Args:
            filepointer: a pointer to the file containing the solver execution standard out.
            out_args : a map with {"exit_code" : exit code of target algorithm}
        Returns:
            A map containing the standard AClib run results. The current standard result map as of AClib 2.06 is:
            {
                "status" : <"SUCCESS"/"SAT"/"UNSAT"/"TIMEOUT"/"CRASHED"/"ABORT">,
                "runtime" : <runtime of target algrithm>,
                "quality" : <a domain specific measure of the quality of the solution [optional]>,
                "misc" : <a (comma-less) string that will be associated with the run [optional]>
            }
            ATTENTION: The return values will overwrite the measured results of the runsolver (if runsolver was used). 
        '''
        import re

        data = filepointer.read()
        resultMap = {}

        if (re.search('s SATISFIABLE', data)) or (re.search('s UNSATISFIABLE', data)):
            resultMap['status'] = 'SUCCESS'

        return resultMap

if __name__ == "__main__":
    wrapper = SpearWrapper()
    wrapper.main()    

4. Requirements

The generic wrapper framework uses runsolver from Olivier Roussel to limit the runtime and memory. examples/generic-wrapper/ has already an precompiled version of the runsolver. However, we strongly recommend to rebuild the runsolver because runsolver uses OS dependent system calls. You can find the sources of runsolver at

http://www.cril.univ-artois.fr/~roussel/runsolver/

5. Usage of Spear Wrapper

Our generic wrapper has some own options:

python emptyWrapper.py --help

For instance, an important option is '--runsolver-path RUNSOLVER' to specify where the wrapper can find the binary of the runsolver.

SMAC calls a wrapper in the following way:

<algo> <instance> <specifics> <runtime cutoff> <runlength> <seed> [solver parameters]

So a possible call of our Spear wrapper could look like:

python spearWrapper.py --runsolver-path runsolver/runsolver example_scenarios/spear/instances/train/qcplin2006.10085.cnf "" 30.0 2147483647 1234 -sp-var-dec-heur 16 -sp-learned-clause-sort-heur 5

with:

5. Further Notes

Our example only touches the surface of what is possible with a wrapper based on our generic wrapper. For instances, as you can find in the docstring of process_results(), you can return a lot more detailed information, i.e., runtime of your algorithm, quality of the solution and some misc information for logging purposes.

ATTENTION: Every information returned by process_results() will overwrite information extracted in the runsolver output, i.e., timeouts or memouts, and runtime.

Furthermore, we strongly recommend to add some functionality in process_results() which verifies the output. SMAC will probably try configurations of your target algorithm, which you never considered before in any experiment. In our experience, bugs are often exposed in such configurations. In the best case, your target algorithm will only crash (which is also not favorable for the configuration process and can decrease the performance of the configuration process). In the worst case, SMACs selects a final incumbent with a bug.

For example, in the case of a SAT solver, the returned assignment of a satisfiable instance can be easily verified with an additional function which has to be integrated in process_results(). (This is an easy example and not the most efficient code):

def _verify_SAT(self, model, solver_output):
    with open(self._instance) as fp:
        for line in fp:
            if line.startswith("c"):
                continue
            if line.startswith("p"):
                continue
            clause = map(int, line.split(" ")[:-1])
            satisfied = False
            for lit in clause:
                if lit in model:
                    satisfied = True
            if not satisfied:
                return False
    return True