Example #1
0
def action_VAR(name, title, action_params, _):
    """Load a variable.

    This action is used for parameters without constraints. If one configuration
    element is given, the parameter doesn't have limits. If three are given, the last two
    specify the low and upper limits. Parameter is set to not constant.

    """
    # Take numerical value or load from file
    if ':' in action_params[0]:  # We want to load a fit result
        from analysis.fit.result import FitResult
        fit_name, var_name = action_params[0].split(':')
        result = FitResult.from_yaml_file(fit_name)
        try:
            value, _, _, _ = result.get_fit_parameter(var_name)
        except KeyError:
            value = result.get_const_parameter(var_name)
    else:
        try:
            value = float(action_params[0])
        except ValueError:
            print("error, action params[0]", action_params[0])
    parameter = ROOT.RooRealVar(name, title, value)
    parameter.setConstant(False)
    if len(action_params) > 1:
        try:
            _, min_val, max_val = action_params
        except ValueError:
            raise ValueError(
                "Wrongly specified var (need to give 1 or 3 arguments) "
                "-> {}".format(action_params))
        parameter.setMin(float(min_val))
        parameter.setMax(float(max_val))
        parameter.setConstant(False)
    return parameter, None
Example #2
0
def action_BLINDRATIO(name, title, action_params, external_vars):
    """Configure a ratio between two variables.

    The first is the numerator, the second the denominator. At least needs to be a shared variable,
    and currently two constrained variables are not possible.
    The third is a randomization string; the fourth a mean and the fifth a width.

    """
    from analysis.utils.pdf import load_pdf_by_name
    Ratio = load_pdf_by_name('RooRatio')

    if len(action_params) != 5:
        raise ValueError(
            "Wrong number of arguments for BLINDRATIO -> {}".format(
                action_params))
    if not any(v.startswith('@') for v in action_params):
        raise ValueError(
            "At least one parameter of a BLINDRATIO must be a reference")
    constraint = None
    processed_vars = []
    for variable in action_params[:2]:
        if variable.startswith('@'):
            processed_variable, const = external_vars[variable[1:]]
            if const:
                if not constraint:
                    constraint = const
                else:
                    raise NotImplementedError(
                        "Two constrained variables in SCALE are not allowed")
        elif ':' in variable:
            from analysis.fit.result import FitResult
            fit_name, var_name = variable.split(':')
            result = FitResult.from_yaml_file(fit_name)
            try:
                value = result.get_fit_parameter(var_name)[0]
            except KeyError:
                value = result.get_const_parameter(var_name)
            processed_variable = ROOT.RooFit.RooConst(value)
        else:
            processed_variable = ROOT.RooFit.RooConst(float(variable))
        processed_vars.append(processed_variable)
    parameter = Ratio(name, title, *processed_vars)
    blind_str, blind_central, blind_sigma = action_params[2:]
    ref_var = deepcopy(parameter)
    parameter = ROOT.RooUnblindPrecision(name + "_blind", title + "_blind",
                                         blind_str, float(blind_central),
                                         float(blind_sigma), ref_var)
    return parameter, constraint
Example #3
0
def action_SCALE(name, title, action_params, external_vars):
    """Configure a constant scaling to a variable.

    The first param must be a shared variable, the second can be a number or a shared variable.

    """
    try:
        ref_var, second_var = action_params
    except ValueError:
        raise ValueError(
            "Wrong number of arguments for SCALE -> {}".format(action_params))
    try:
        if ref_var.startswith('@'):
            ref_var = ref_var[1:]
        else:
            raise ValueError(
                "The first value for a SCALE must be a reference.")
        ref_var, constraint = external_vars[ref_var]
        if second_var.startswith('@'):
            second_var = second_var[1:]
            second_var, const = external_vars[second_var]
            if const:
                if not constraint:
                    constraint = const
                else:
                    raise NotImplementedError(
                        "Two constrained variables in SCALE are not allowed")
        elif ':' in second_var:
            from analysis.fit.result import FitResult
            fit_name, var_name = second_var.split(':')
            result = FitResult.from_yaml_file(fit_name)
            try:
                value = result.get_fit_parameter(var_name)[0]
            except KeyError:
                value = result.get_const_parameter(var_name)
            second_var = ROOT.RooFit.RooConst(value)
        else:
            second_var = ROOT.RooFit.RooConst(float(second_var))
    except KeyError as error:
        raise ValueError("Missing parameter definition -> {}".format(error))
    parameter = ROOT.RooProduct(name, title,
                                ROOT.RooArgList(ref_var, second_var))
    return parameter, constraint
Example #4
0
def action_RATIO(name, title, action_params, external_vars):
    """Configure a ratio between two variables.

    The first is the numerator, the second the denomiator. At least needs to be a shared variable,
    and currently two constrained variables are not possible.

    """
    from analysis.utils.pdf import load_pdf_by_name
    Ratio = load_pdf_by_name('RooRatio')

    if len(action_params) != 2:
        raise ValueError(
            "Wrong number of arguments for RATIO -> {}".format(action_params))
    if not any(v.startswith('@') for v in action_params):
        raise ValueError(
            "At least one parameter of a RATIO must be a reference")
    constraint = None
    processed_vars = []
    for variable in action_params:
        if variable.startswith('@'):
            processed_variable, const = external_vars[variable[1:]]
            if const:
                if not constraint:
                    constraint = const
                else:
                    raise NotImplementedError(
                        "Two constrained variables in SCALE are not allowed")
        elif ':' in variable:
            from analysis.fit.result import FitResult
            fit_name, var_name = variable.split(':')
            result = FitResult.from_yaml_file(fit_name)
            try:
                value = result.get_fit_parameter(var_name)[0]
            except KeyError:
                value = result.get_const_parameter(var_name)
            processed_variable = ROOT.RooFit.RooConst(value)
        else:
            processed_variable = ROOT.RooFit.RooConst(float(variable))
        processed_vars.append(processed_variable)
    parameter = Ratio(name, title, *processed_vars)
    return parameter, constraint
Example #5
0
def action_CONST(name, title, action_params, _):
    """Load a constant variable.

    Its argument indicates at which value to fix it.

    """
    # Take numerical value or load from file
    if ':' in action_params[0]:  # We want to load a fit result
        from analysis.fit.result import FitResult
        fit_name, var_name = action_params[0].split(':')
        result = FitResult.from_yaml_file(fit_name)
        try:
            value, _, _, _ = result.get_fit_parameter(var_name)[0]
        except KeyError:
            value = result.get_const_parameter(var_name)
    else:
        try:
            value = float(action_params[0])
        except ValueError:
            print("error, action params[0]", action_params[0])
    parameter = ROOT.RooRealVar(name, title, value)
    parameter.setConstant(True)
    return parameter, None
Example #6
0
def action_GAUSS(name, title, action_params, _):
    """Load a variable with a Gaussian constraint.

    The arguments of that Gaussian, ie, its mean and sigma, are to be given
    as action parameters.

    """
    value_error = None
    if ':' in action_params[0]:  # We want to load a fit result
        from analysis.fit.result import FitResult
        fit_name, var_name = action_params[0].split(':')
        result = FitResult.from_yaml_file(fit_name)
        try:
            value, value_error, _, _ = result.get_fit_parameter(var_name)
        except KeyError:
            value = result.get_const_parameter(var_name)
    else:
        try:
            value = float(action_params[0])
        except ValueError:
            print("error, action params[0]", action_params[0])
    parameter = ROOT.RooRealVar(name, title, value)
    try:
        if len(action_params) == 1 and value_error is None:
            raise ValueError
        elif len(action_params) == 2:
            value_error = float(action_params[1])
        else:
            raise ValueError
    except ValueError:
        raise ValueError("Wrongly specified Gaussian constraint -> {}".format(
            action_params))
    constraint = ROOT.RooGaussian(name + 'Constraint', name + 'Constraint',
                                  parameter, ROOT.RooFit.RooConst(value),
                                  ROOT.RooFit.RooConst(value_error))
    parameter.setConstant(False)
    return parameter, constraint
Example #7
0
    def __init__(self, model, acceptance, config):
        """Configure randomizer.

        To specify where the parameters come from, `config` needs a `params` key which contains
        a list of results and parameter name correspondences to be used to translate from the
        fit result to `model`.

            {'params': [{'result': result_name,
                       'param_names': {'fit_result_name': 'model_parameter_name',
                                       ...}},
                       ...]}

        Arguments:
            model (`analysis.physics.PhysicsFactory`): Factory used for generation and
                fitting.
            config (dict): Configuration.

        Raise:
            KeyError: If some configuration parameter is missing.
            RuntimeError: If the parameter names are badly specified.
            ValueError: If no yield is specified, either through the PDF model or the
                configuration.

        """

        def make_block(*matrices):
            """Make bloc-diagonal matrix.

            Arguments:
                matrices (list): Matrices to combine.

            Return:
                numpy.ndarray: Block-diagonal matrix.

            """
            dimensions = sum(mat.shape[0] for mat in matrices), sum(mat.shape[1] for mat in matrices)
            output_mat = np.zeros(dimensions)
            row = 0
            column = 0
            for mat in matrices:
                output_mat[row:row + mat.shape[0], column:column + mat.shape[1]] = mat
                row = row + mat.shape[0]
                column = column + mat.shape[1]
            return output_mat

        super(FixedParamsRandomizer, self).__init__(model, config=config, acceptance=acceptance)
        cov_matrices = []
        central_values = []
        param_translation = OrderedDict()
        # Load fit results and their covariance matrices
        rand_config = config['params']
        if not isinstance(rand_config, (list, tuple)):
            rand_config = [rand_config]
        for result_config in rand_config:
            fit_result = FitResult.from_yaml_file(result_config['result'])
            param_translation.update(result_config['param_names'])
            cov_matrices.append(fit_result.get_covariance_matrix(param_translation.keys()))
            central_values.extend([float(fit_result.get_fit_parameter(param)[0])
                                   for param in param_translation.keys()])
        # Check that there is a correspondence between the fit result and parameters in the generation PDF
        self._cov_matrix = make_block(*cov_matrices)
        self._central_values = np.array(central_values)
        self._pdf_index = OrderedDict()
        for fit_param in param_translation.values():
            found = False
            for label, pdf_list in self._gen_pdfs.items():
                for pdf_num, pdf in enumerate(pdf_list):
                    if fit_param in [var.GetName() for var in iterate_roocollection(pdf.getVariables())]:
                        self._pdf_index[fit_param] = (label, pdf_num)
                        found = True
                        break
                if found:
                    break
            if not found:
                raise RuntimeError("Cannot find parameter {} in the physics model".format(fit_param))
Example #8
0
def run(config_files, link_from, verbose):
    """Run the script.

    Run a generate/fit sequence as many times as requested.

    Arguments:
        config_files (list[str]): Path to the configuration files.
        link_from (str): Path to link the results from.
        verbose (bool): Give verbose output?

    Raise:
        OSError: If the configuration file or some other input does not exist.
        AttributeError: If the input data are incompatible with a previous fit.
        KeyError: If some configuration data are missing.
        ValueError: If there is any problem in configuring the PDF factories.
        RuntimeError: If there is a problem during the fitting.

    """
    try:
        config = _config.load_config(
            *config_files, validate=['syst/ntoys', 'name', 'randomizer'])
    except OSError:
        raise OSError(
            "Cannot load configuration files: {}".format(config_files))
    except _config.ConfigError as error:
        if 'syst/ntoys' in error.missing_keys:
            logger.error("Number of toys not specified")
        if 'name' in error.missing_keys:
            logger.error("No name was specified in the config file!")
        if 'randomizer' in error.missing_keys:
            logger.error(
                "No randomizer configuration specified in config file!")
        raise KeyError("ConfigError raised -> {}".format(error.missing_keys))
    except KeyError as error:
        logger.error("YAML parsing error -> %s", error)
        raise
    model_name = config['syst'].get('model',
                                    'model')  # TODO: 'model' returns name?
    try:
        model_config = config[model_name]
    except KeyError as error:
        logger.error("Missing model configuration -> %s", str(error))
        raise KeyError("Missing model configuration")
    # Load fit model
    try:
        fit_model = configure_model(copy.deepcopy(model_config))
        randomizer_model = configure_model(copy.deepcopy(model_config))
    except KeyError:
        logger.exception('Error loading model')
        raise ValueError('Error loading model')
    # Some info
    ntoys = config['syst'].get('ntoys-per-job', config['syst']['ntoys'])
    logger.info("Doing %s generate/fit sequences", ntoys)
    logger.info("Systematics job name: %s", config['name'])
    if link_from:
        config['link-from'] = link_from
    if 'link-from' in config:
        logger.info("Linking toy data from %s", config['link-from'])
    else:
        logger.debug("No linking specified")
    # Now load the acceptance
    try:
        acceptance = get_acceptance(config['acceptance']) \
            if 'acceptance' in config \
            else None
    except _config.ConfigError as error:
        raise KeyError("Error loading acceptance -> {}".format(error))
    # Fit strategy
    fit_strategy = config['syst'].get('strategy', 'simple')
    # Load randomizer configuration
    randomizer = get_randomizer(config['randomizer'])(
        model=randomizer_model,
        config=config['randomizer'],
        acceptance=acceptance)
    # Set seed
    job_id = get_job_id()
    # Start looping
    fit_results = {}
    logger.info("Starting sampling-fit loop (print frequency is 20)")
    initial_mem = memory_usage()
    initial_time = default_timer()
    do_extended = config['syst'].get('extended', False)
    do_minos = config['syst'].get('minos', False)
    for fit_num in range(ntoys):
        # Logging
        if (fit_num + 1) % 20 == 0:
            logger.info("  Fitting event %s/%s", fit_num + 1, ntoys)
        # Generate a dataset
        seed = get_urandom_int(4)
        np.random.seed(seed=seed)
        ROOT.RooRandom.randomGenerator().SetSeed(seed)
        try:
            # Get a randomized dataset and fit it with the nominal fit
            dataset = randomizer.get_dataset(randomize=True)
            gen_values = randomizer.get_current_values()
            fit_result_nominal = fit(fit_model,
                                     model_name,
                                     fit_strategy,
                                     dataset,
                                     verbose,
                                     Extended=do_extended,
                                     Minos=do_minos)
            # Fit the randomized dataset with the randomized values as nominal
            fit_result_rand = fit(randomizer_model,
                                  model_name,
                                  fit_strategy,
                                  dataset,
                                  verbose,
                                  Extended=do_extended,
                                  Minos=do_minos)
            randomizer.reset_values(
            )  # Needed to avoid generating unphysical values
        except ValueError:
            raise RuntimeError()
        except Exception:
            # logger.exception()
            raise RuntimeError()  # TODO: provide more information?
        result = {}
        result['fitnum'] = fit_num
        result['seed'] = seed
        # Save the results of the randomized fit
        result_roofit_rand = FitResult.from_roofit(fit_result_rand)
        result['param_names'] = result_roofit_rand.get_fit_parameters().keys()
        result['rand'] = result_roofit_rand.to_plain_dict()
        result['rand_cov'] = result_roofit_rand.get_covariance_matrix()
        _root.destruct_object(fit_result_rand)
        # Save the results of the nominal fit
        result_roofit_nominal = FitResult.from_roofit(fit_result_nominal)
        result['nominal'] = result_roofit_nominal.to_plain_dict()
        result['nominal_cov'] = result_roofit_nominal.get_covariance_matrix()
        result['gen'] = gen_values
        _root.destruct_object(result_roofit_nominal)
        _root.destruct_object(dataset)
        fit_results[fit_num] = result
        logger.debug("Cleaning up")
    logger.info("Fitting loop over")
    logger.info("--> Memory leakage: %.2f MB/sample-fit",
                (memory_usage() - initial_mem) / ntoys)
    logger.info("--> Spent %.0f ms/sample-fit",
                (default_timer() - initial_time) * 1000.0 / ntoys)
    logger.info("Saving to disk")
    data_res = []
    cov_matrices = {}
    # Get covariance matrices
    for fit_num, fit_res_i in fit_results.items():
        fit_res = {
            'fitnum': fit_res_i['fitnum'],
            'seed': fit_res_i['seed'],
            'model_name': model_name,
            'fit_strategy': fit_strategy
        }
        param_names = fit_res_i['param_names']
        cov_folder_rand = os.path.join(str(job_id), str(fit_res['fitnum']),
                                       'rand')
        cov_matrices[cov_folder_rand] = pd.DataFrame(fit_res_i['rand_cov'],
                                                     index=param_names,
                                                     columns=param_names)
        cov_folder_nominal = os.path.join(str(job_id), str(fit_res['fitnum']),
                                          'nominal')
        cov_matrices[cov_folder_nominal] = pd.DataFrame(
            fit_res_i['nominal_cov'], index=param_names, columns=param_names)
        for res_name, res_value in fit_res_i['rand'].items():
            fit_res['{}_rand'.format(res_name)] = res_value
        for res_name, res_value in fit_res_i['nominal'].items():
            fit_res['{}_nominal'.format(res_name)] = res_value
        for res_name, res_value in fit_res_i['gen'].items():
            fit_res['{}_gen'.format(res_name)] = res_value
        data_res.append(fit_res)
    data_frame = pd.DataFrame(data_res)
    fit_result_frame = pd.concat([
        data_frame,
        pd.concat([pd.DataFrame({'jobid': [job_id]})] *
                  data_frame.shape[0]).reset_index(drop=True)
    ],
                                 axis=1)
    try:
        # pylint: disable=E1101
        with _paths.work_on_file(config['name'],
                                 path_func=_paths.get_toy_fit_path,
                                 link_from=config.get('link-from',
                                                      None)) as toy_fit_file:
            with modify_hdf(toy_fit_file) as hdf_file:
                # First fit results
                hdf_file.append('fit_results', fit_result_frame)
                # Save covarinance matrix under 'covariance/jobid/fitnum
                for cov_folder, cov_matrix in cov_matrices.items():
                    cov_path = os.path.join('covariance', cov_folder)
                    hdf_file.append(cov_path, cov_matrix)
                # Generator info
                hdf_file.append(
                    'input_values',
                    pd.DataFrame.from_dict(randomizer.get_input_values(),
                                           orient='index'))

            logger.info("Written output to %s", toy_fit_file)
            if 'link-from' in config:
                logger.info("Linked to %s", config['link-from'])
    except OSError as excp:
        logger.error(str(excp))
        raise
    except ValueError as error:
        logger.exception("Exception on dataset saving")
        raise RuntimeError(str(error))
Example #9
0
def test_fitresult_yaml_conversion(fit_result):
    """Test YAML conversion."""
    res = FitResult.from_roofit(fit_result)
    res_conv = FitResult.from_yaml(res.to_yaml())
    assert (
        res_conv.get_covariance_matrix() == res.get_covariance_matrix()).all()
Example #10
0
def test_fitresult_convergence(fit_result):
    """Test fit result convergence."""
    assert FitResult.from_roofit(fit_result).has_converged()
Example #11
0
def run(config_files, link_from, verbose):
    """Run the script.

    Run a sample/fit sequence as many times as requested.

    Arguments:
        config_files (list[str]): Path to the configuration files.
        link_from (str): Path to link the results from.
        verbose (bool): Give verbose output?

    Raise:
        OSError: If there either the configuration file does not exist some
            of the input toys cannot be found.
        AttributeError: If the input data are incompatible with a previous fit.
        KeyError: If some configuration data are missing.
        ValueError: If there is any problem in configuring the PDF factories.
        RuntimeError: If there is a problem during the fitting.

    """
    try:
        config = _config.load_config(*config_files,
                                     validate=['fit/nfits', 'name', 'data'])
    except OSError:
        raise OSError(
            "Cannot load configuration files: {}".format(config_files))
    except ConfigError as error:
        if 'fit/nfits' in error.missing_keys:
            logger.error("Number of fits not specified")
        if 'name' in error.missing_keys:
            logger.error("No name was specified in the config file!")
        if 'data' in error.missing_keys:
            logger.error("No input data specified in the config file!")
        raise KeyError("ConfigError raised -> {}".format(error.missing_keys))
    except KeyError as error:
        logger.error("YAML parsing error -> %s", error)
    try:
        models = {
            model_name: config[model_name]
            for model_name in config['fit'].get('models', ['model'])
        }
    except KeyError as error:
        logger.error("Missing model configuration -> %s", str(error))
        raise KeyError("Missing model configuration")
    if not models:
        logger.error(
            "Empty list specified in the config file under 'fit/models'!")
        raise KeyError()
    fit_strategies = config['fit'].get('strategies', ['simple'])
    if not fit_strategies:
        logger.error("Empty fit strategies were specified in the config file!")
        raise KeyError()
    # Some info
    nfits = config['fit'].get('nfits-per-job', config['fit']['nfits'])
    logger.info("Doing %s sample/fit sequences", nfits)
    logger.info("Fit job name: %s", config['name'])
    if link_from:
        config['link-from'] = link_from
    if 'link-from' in config:
        logger.info("Linking toy data from %s", config['link-from'])
    else:
        logger.debug("No linking specified")
    # Analyze data requirements
    logger.info("Loading input data")
    data = {}
    gen_values = {}
    if len(set('category' in data_source
               for data_source in config['data'])) > 1:
        raise KeyError("Categories in 'data' not consistently specified.")
    for data_id, data_source in config['data'].items():
        try:
            source_toy = data_source['source']
        except KeyError:
            logger.error("Data source not specified")
            raise
        data[data_id] = (get_data({
            'source': source_toy,
            'source-type': 'toy',
            'tree': 'data',
            'output-format': 'pandas',
            'selection': data_source.get('selection')
        }), data_source['nevents'], data_source.get('poisson'),
                         data_source.get('category'))
        # Generator values
        toy_info = get_data({
            'source': source_toy,
            'source-type': 'toy',
            'tree': 'toy_info',
            'output-format': 'pandas'
        })
        gen_values[data_id] = {}
        for var_name in toy_info.columns:
            if var_name in ('seed', 'jobid', 'nevents'):
                continue
            gen_values[data_id][var_name] = toy_info[var_name].iloc[0]
    try:
        fit_models = {}
        for model_name in models:
            if model_name not in config:
                raise KeyError(
                    "Missing model definition -> {}".format(model_name))
            fit_models[model_name] = configure_model(config[model_name])
            if any(yield_.isConstant()
                   for yield_ in fit_models[model_name].get_yield_vars()
                   if yield_):
                logger.warning(
                    "Model %s has constant yields. "
                    "Be careful when configuring the input data, you may need to disable poisson sampling",
                    model_name)
    except KeyError:
        logger.exception("Error loading model")
        raise ValueError("Error loading model")
    if len(set(model.is_extended() for model in fit_models.values())) == 2:
        logger.error("Mix of extended and non-extended models!")
        raise ValueError("Error loading fit models")
    # Let's check these generator values against the output file
    try:
        gen_values_frame = {}
        # pylint: disable=E1101
        with _paths.work_on_file(config['name'], _paths.get_toy_fit_path,
                                 config.get('link-from')) as toy_fit_file:
            with modify_hdf(toy_fit_file) as hdf_file:
                logger.debug("Checking generator values")
                test_gen = [('gen_{}'.format(data_source)) in hdf_file
                            for data_source in gen_values]
                if all(test_gen
                       ):  # The data were written already, crosscheck values
                    for source_id, gen_value in gen_values.items():
                        if not all(
                                hdf_file['gen_{}'.format(data_source)]
                            [var_name].iloc[0] == var_value
                                for var_name, var_value in gen_value.items()):
                            raise AttributeError(
                                "Generated and stored values don't match for source '{}'"
                                .format(source_id))
                elif not any(test_gen):  # No data were there, just overwrite
                    for source_id, gen_values in gen_values.items():
                        gen_data = {
                            'id':
                            source_id,
                            'source':
                            _paths.get_toy_path(
                                config['data'][source_id]['source']),
                            'nevents':
                            config['data'][source_id]['nevents']
                        }
                        gen_data.update(gen_values)
                        gen_values_frame['gen_{}'.format(
                            source_id)] = pd.DataFrame([gen_data])
                else:
                    raise AttributeError("Inconsistent number of data sources")
    except OSError as excp:
        logger.error(str(excp))
        raise
    # Now load the acceptance
    try:
        acceptance = get_acceptance(config['acceptance']) \
            if 'acceptance' in config \
            else None
    except ConfigError as error:
        raise KeyError("Error loading acceptance -> {}".format(error))
    # Prepare output
    gen_events = defaultdict(list)
    # Set seed
    job_id = get_job_id()
    if job_id:
        seed = int(job_id.split('.')[0])
    else:
        import random
        job_id = 'local'
        seed = random.randint(0, 100000)
    np.random.seed(seed=seed)
    ROOT.RooRandom.randomGenerator().SetSeed(seed)
    # Start looping
    fit_results = defaultdict(list)
    logger.info("Starting sampling-fit loop (print frequency is 20)")
    initial_mem = memory_usage()
    initial_time = default_timer()
    for fit_num in range(nfits):
        # Logging
        if (fit_num + 1) % 20 == 0:
            logger.info("  Fitting event %s/%s", fit_num + 1, nfits)
        # Get a compound dataset
        seed = get_urandom_int(4)
        np.random.seed(seed=seed)
        ROOT.RooRandom.randomGenerator().SetSeed(seed)
        try:
            logger.debug("Sampling input data")
            datasets, sample_sizes = get_datasets(data, acceptance, fit_models)
            for sample_name, sample_size in sample_sizes.items():
                gen_events['N^{{{}}}_{{gen}}'.format(sample_name)].append(
                    sample_size)
            logger.debug("Sampling finalized")
        except KeyError:
            logger.exception("Bad data configuration")
            raise
        logger.debug("Fitting")
        for model_name in models:
            dataset = datasets.pop(model_name)
            fit_model = fit_models[model_name]
            # Now fit
            for fit_strategy in fit_strategies:
                toy_key = (model_name, fit_strategy)
                try:
                    fit_result = fit(fit_model,
                                     model_name,
                                     fit_strategy,
                                     dataset,
                                     verbose,
                                     Extended=config['fit'].get(
                                         'extended', False),
                                     Minos=config['fit'].get('minos', False))
                except ValueError:
                    raise RuntimeError()
                # Now results are in fit_parameters
                result_roofit = FitResult.from_roofit(fit_result)
                result = result_roofit.to_plain_dict()
                result['cov_matrix'] = result_roofit.get_covariance_matrix()
                result['param_names'] = result_roofit.get_fit_parameters(
                ).keys()
                result['fitnum'] = fit_num
                result['seed'] = seed
                fit_results[toy_key].append(result)
                _root.destruct_object(fit_result)
            _root.destruct_object(dataset)
        logger.debug("Cleaning up")
    logger.info("Fitting loop over")
    logger.info("--> Memory leakage: %.2f MB/sample-fit",
                (memory_usage() - initial_mem) / nfits)
    logger.info("--> Spent %.0f ms/sample-fit",
                (default_timer() - initial_time) * 1000.0 / nfits)
    logger.info("Saving to disk")
    data_res = []
    cov_matrices = {}
    # Get gen values for this model
    for (model_name, fit_strategy), fits in fit_results.items():
        for fit_res in fits:
            fit_res = fit_res.copy()
            fit_res['model_name'] = model_name
            fit_res['fit_strategy'] = fit_strategy

            cov_folder = os.path.join(str(job_id), str(fit_res['fitnum']))
            param_names = fit_res.pop('param_names')
            cov_matrices[cov_folder] = pd.DataFrame(fit_res.pop('cov_matrix'),
                                                    index=param_names,
                                                    columns=param_names)
            data_res.append(fit_res)
    data_frame = pd.DataFrame(data_res)
    fit_result_frame = pd.concat([
        pd.DataFrame(gen_events), data_frame,
        pd.concat([pd.DataFrame({'jobid': [job_id]})] *
                  data_frame.shape[0]).reset_index(drop=True)
    ],
                                 axis=1)
    try:
        # pylint: disable=E1101
        with _paths.work_on_file(
                config['name'],
                path_func=_paths.get_toy_fit_path,
                link_from=config.get('link-from')) as toy_fit_file:
            with modify_hdf(toy_fit_file) as hdf_file:
                # First fit results
                hdf_file.append('fit_results', fit_result_frame)
                # Save covarinance matrix under 'covariance/jobid/fitnum
                for cov_folder, cov_matrix in cov_matrices.items():
                    cov_path = os.path.join('covariance', cov_folder)
                    hdf_file.append(cov_path, cov_matrix)
                # Generator info
                for key_name, gen_frame in gen_values_frame.items():
                    hdf_file.append(key_name, gen_frame)

            logger.info("Written output to %s", toy_fit_file)
            if 'link-from' in config:
                logger.info("Linked to %s", config['link-from'])
    except OSError as excp:
        logger.error(str(excp))
        raise
    except ValueError as error:
        logger.exception("Exception on dataset saving")
        raise RuntimeError(str(error))