Function to register finished runs. Every time a run has finished, this function should be called
        to register it with the loss.

        Parameters:
        -----------
        loss: float
            the loss of the parameters
        budget: float
            the budget of the parameters
        parameters: dict
            the parameters of this trial
        update_model: bool
            whether use this parameter to update BP model

        Returns
        -------
        None

def new_result(self, loss, budget, parameters, update_model=True):
        """
        Function to register finished runs. Every time a run has finished, this function should be called
        to register it with the loss.

        Parameters:
        -----------
        loss: float
            the loss of the parameters
        budget: float
            the budget of the parameters
        parameters: dict
            the parameters of this trial
        update_model: bool
            whether use this parameter to update BP model

        Returns
        -------
        None
        """
        if loss is None:
            # One could skip crashed results, but we decided
            # assign a +inf loss and count them as bad configurations
            loss = np.inf

        if budget not in self.configs.keys():
            self.configs[budget] = []
            self.losses[budget] = []

        # skip model building if we already have a bigger model
        if max(list(self.kde_models.keys()) + [-np.inf]) > budget:
            return

        # We want to get a numerical representation of the configuration in the original space
        conf = ConfigSpace.Configuration(self.configspace, parameters)
        self.configs[budget].append(conf.get_array())
        self.losses[budget].append(loss)

        # skip model building:
        # a) if not enough points are available
        if len(self.configs[budget]) <= self.min_points_in_model - 1:
            logger.debug("Only %i run(s) for budget %f available, need more than %s \
            -> can't build model!"%(len(self.configs[budget]), budget, self.min_points_in_model+1))
            return
        # b) during warnm starting when we feed previous results in and only update once
        if not update_model:
            return

        train_configs = np.array(self.configs[budget])
        train_losses = np.array(self.losses[budget])

        n_good = max(self.min_points_in_model, (self.top_n_percent * train_configs.shape[0])//100)
        n_bad = max(self.min_points_in_model, ((100-self.top_n_percent)*train_configs.shape[0])//100)

        # Refit KDE for the current budget
        idx = np.argsort(train_losses)

        train_data_good = self.impute_conditional_data(train_configs[idx[:n_good]])
        train_data_bad = self.impute_conditional_data(train_configs[idx[n_good:n_good+n_bad]])

        if train_data_good.shape[0] <= train_data_good.shape[1]:
            return
        if train_data_bad.shape[0] <= train_data_bad.shape[1]:
            return

        #more expensive crossvalidation method
        #bw_estimation = 'cv_ls'
        # quick rule of thumb
        bw_estimation = 'normal_reference'

        bad_kde = sm.nonparametric.KDEMultivariate(data=train_data_bad, var_type=self.kde_vartypes, bw=bw_estimation)
        good_kde = sm.nonparametric.KDEMultivariate(data=train_data_good, var_type=self.kde_vartypes, bw=bw_estimation)

        bad_kde.bw = np.clip(bad_kde.bw, self.min_bandwidth, None)
        good_kde.bw = np.clip(good_kde.bw, self.min_bandwidth, None)

        self.kde_models[budget] = {
            'good': good_kde,
            'bad' : bad_kde
        }

        # update probs for the categorical parameters for later sampling
        logger.debug('done building a new model for budget %f based on %i/%i split\nBest loss for this budget:%f\n'
                     %(budget, n_good, n_bad, np.min(train_losses)))