def print(self): """Print a helpful report about the experiment grid.""" print('=' * DIV_LINE_WIDTH) # Prepare announcement at top of printing. If the ExperimentGrid has a # short name, write this as one line. If the name is long, break the # announcement over two lines. base_msg = 'ExperimentGrid %s runs over parameters:\n' name_insert = '[' + self._name + ']' if len(base_msg % name_insert) <= 80: msg = base_msg % name_insert else: msg = base_msg % (name_insert + '\n') print(colorize(msg, color='green', bold=True)) # List off parameters, shorthands, and possible values. for k, v, sh in zip(self.keys, self.vals, self.shs): color_k = colorize(k.ljust(40), color='cyan', bold=True) print('', color_k, '[' + sh + ']' if sh is not None else '', '\n') for i, val in enumerate(v): print('\t' + str(convert_json(val))) print() # Count up the number of variants. The number counting seeds # is the total number of experiments that will run; the number not # counting seeds is the total number of otherwise-unique configs # being investigated. nvars_total = int(np.prod([len(v) for v in self.vals])) if 'seed' in self.keys: num_seeds = len(self.vals[self.keys.index('seed')]) nvars_seedless = int(nvars_total / num_seeds) else: nvars_seedless = nvars_total print(' Variants, counting seeds: '.ljust(40), nvars_total) print(' Variants, not counting seeds: '.ljust(40), nvars_seedless) print() print('=' * DIV_LINE_WIDTH)
def call_experiment(exp_name, thunk, seed=0, num_cpu=1, data_dir=None, datestamp=False, **kwargs): """ Run a function (thunk) with hyperparameters (kwargs), plus configuration. This wraps a few pieces of functionality which are useful when you want to run many experiments in sequence, including logger configuration and splitting into multiple processes for MPI. There's also a SpinningUp-specific convenience added into executing the thunk: if ``env_name`` is one of the kwargs passed to call_experiment, it's assumed that the thunk accepts an argument called ``env_fn``, and that the ``env_fn`` should make a gym environment with the given ``env_name``. The way the experiment is actually executed is slightly complicated: the function is serialized to a string, and then ``run_entrypoint.py`` is executed in a subprocess call with the serialized string as an argument. ``run_entrypoint.py`` unserializes the function call and executes it. We choose to do it this way---instead of just calling the function directly here---to avoid leaking state between successive experiments. Args: exp_name (string): Name for experiment. thunk (callable): A python function. seed (int): Seed for random number generators. num_cpu (int): Number of MPI processes to split into. Also accepts 'auto', which will set up as many procs as there are cpus on the machine. data_dir (string): Used in configuring the logger, to decide where to store experiment results. Note: if left as None, data_dir will default to ``DEFAULT_DATA_DIR`` from ``spinup/user_config.py``. **kwargs: All kwargs to pass to thunk. """ # Determine number of CPU cores to run on num_cpu = psutil.cpu_count(logical=False) if num_cpu == 'auto' else num_cpu # Send random seed to thunk kwargs['seed'] = seed # Be friendly and print out your kwargs, so we all know what's up print(colorize('Running experiment:\n', color='cyan', bold=True)) print(exp_name + '\n') print(colorize('with kwargs:\n', color='cyan', bold=True)) kwargs_json = convert_json(kwargs) print( json.dumps(kwargs_json, separators=(',', ':\t'), indent=4, sort_keys=True)) print('\n') # # Set up logger output directory # if 'logger_kwargs' not in kwargs: # kwargs['logger_kwargs'] = setup_logger_kwargs(exp_name, seed, data_dir, datestamp) # else: # print('Note: Call experiment is not handling logger_kwargs.\n') def thunk_plus(): # Make 'env_fn' from 'env_name' if 'env_name' in kwargs: import gym env_name = kwargs['env_name'] kwargs['env_fn'] = lambda: gym.make(env_name) del kwargs['env_name'] # Fork into multiple processes mpi_fork(num_cpu) # Run thunk thunk(**kwargs) # Prepare to launch a script to run the experiment pickled_thunk = cloudpickle.dumps(thunk_plus) encoded_thunk = base64.b64encode( zlib.compress(pickled_thunk)).decode('utf-8') entrypoint = osp.join(osp.abspath(osp.dirname(__file__)), 'run_entrypoint.py') cmd = [ sys.executable if sys.executable else 'python', entrypoint, encoded_thunk ] try: subprocess.check_call(cmd, env=os.environ) except CalledProcessError: err_msg = '\n' * 3 + '=' * DIV_LINE_WIDTH + '\n' + dedent(""" There appears to have been an error in your experiment. Check the traceback above to see what actually went wrong. The traceback below, included for completeness (but probably not useful for diagnosing the error), shows the stack leading up to the experiment launch. """) + '=' * DIV_LINE_WIDTH + '\n' * 3 print(err_msg) raise # # Tell the user about where results are, and how to check them # logger_kwargs = kwargs['logger_kwargs'] # # plot_cmd = 'python -m spinup.run plot ' + logger_kwargs['output_dir'] # plot_cmd = colorize(plot_cmd, 'green') # # test_cmd = 'python -m spinup.run test_policy ' + logger_kwargs['output_dir'] # test_cmd = colorize(test_cmd, 'green') # # output_msg = '\n' * 5 + '=' * DIV_LINE_WIDTH + '\n' + dedent("""\ # End of experiment. # # # Plot results from this run with: # # %s # # # Watch the trained agent with: # # %s # # # """ % (plot_cmd, test_cmd)) + '=' * DIV_LINE_WIDTH + '\n' * 5 print(output_msg)
def run(self, thunk, num_cpu=1, data_dir=None, datestamp=False): """ Run each variant in the grid with function 'thunk'. Note: 'thunk' must be either a callable function, or a string. If it is a string, it must be the name of a parameter whose values are all callable functions. Uses ``call_experiment`` to actually launch each experiment, and gives each variant a name using ``self.variant_name()``. Maintenance note: the args for ExperimentGrid.run should track closely to the args for call_experiment. However, ``seed`` is omitted because we presume the user may add it as a parameter in the grid. """ # Make the list of all variants. variants = self.variants() # Print variant names for the user. var_names = set([self.variant_name(var) for var in variants]) var_names = sorted(list(var_names)) line = '=' * DIV_LINE_WIDTH preparing = colorize('Preparing to run the following experiments...', color='green', bold=True) joined_var_names = '\n'.join(var_names) announcement = f"\n{preparing}\n\n{joined_var_names}\n\n{line}" print(announcement) if WAIT_BEFORE_LAUNCH > 0: delay_msg = colorize(dedent(""" Launch delayed to give you a few seconds to review your experiments. To customize or disable this behavior, change WAIT_BEFORE_LAUNCH in spinup/user_config.py. """), color='cyan', bold=True) + line print(delay_msg) wait, steps = WAIT_BEFORE_LAUNCH, 100 prog_bar = trange( steps, desc='Launching in...', leave=False, ncols=DIV_LINE_WIDTH, mininterval=0.25, bar_format='{desc}: {bar}| {remaining} {elapsed}') for _ in prog_bar: time.sleep(wait / steps) # Run the variants. for var in variants: exp_name = self.variant_name(var) # Figure out what the thunk is. if isinstance(thunk, str): # Assume one of the variant parameters has the same # name as the string you passed for thunk, and that # variant[thunk] is a valid callable function. thunk_ = var[thunk] del var[thunk] else: # Assume thunk is given as a function. thunk_ = thunk call_experiment(exp_name, thunk_, num_cpu=num_cpu, data_dir=data_dir, datestamp=datestamp, **var)