class BO(object):
    """
    Runner of the multi-attribute Bayesian optimization loop. This class wraps the optimization loop around the different handlers.
    :param model: GPyOpt model class.
    :param space: GPyOpt space class.
    :param objective: GPyOpt objective class.
    :param acquisition: GPyOpt acquisition class.
    :param evaluator: GPyOpt evaluator class.
    :param X_init: 2d numpy array containing the initial inputs (one per row) of the model.
    :param Y_init: 2d numpy array containing the initial outputs (one per row) of the model.
    :param cost: GPyOpt cost class (default, none).
    :param normalize_Y: whether to normalize the outputs before performing any optimization (default, True).
    :param model_update_interval: interval of collected observations after which the model is updated (default, 1).
    :param de_duplication: GPyOpt DuplicateManager class. Avoids re-evaluating the objective at previous, pending or infeasible locations (default, False).
    """
    def __init__(self,
                 model,
                 model_c,
                 space,
                 objective,
                 constraint,
                 acquisition,
                 evaluator,
                 X_init,
                 ref_point=None,
                 expensive=False,
                 Y_init=None,
                 C_init=None,
                 cost=None,
                 normalize_Y=False,
                 model_update_interval=1,
                 deterministic=True,
                 true_preference=0.5):
        self.true_preference = true_preference
        self.model_c = model_c
        self.model = model
        self.space = space
        self.objective = objective
        self.constraint = constraint
        self.acquisition = acquisition
        self.utility = acquisition.utility
        self.evaluator = evaluator
        self.normalize_Y = normalize_Y
        self.model_update_interval = model_update_interval
        self.X = X_init
        self.Y = Y_init
        self.C = C_init
        self.ref_point = ref_point
        self.deterministic = deterministic
        self.cost = CostModel(cost)
        self.model_parameters_iterations = None
        self.expensive = expensive

        try:
            if acquisition.name == "Constrained_Thompson_Sampling":
                self.sample_from_acq = True
                self.tag_last_evaluation = False
            else:
                self.sample_from_acq = False
                self.tag_last_evaluation = True
        except:
            print("name of acquisition function wasnt provided")
            self.sample_from_acq = False
            self.tag_last_evaluation = False

    def suggest_next_locations(self,
                               context=None,
                               pending_X=None,
                               ignored_X=None):
        """
        Run a single optimization step and return the next locations to evaluate the objective.
        Number of suggested locations equals to batch_size.

        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param pending_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet) (default, None).
        :param ignored_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again (default, None).
        """
        self.model_parameters_iterations = None
        self.num_acquisitions = 0
        self.context = context
        self._update_model(self.normalization_type)

        suggested_locations = self._compute_next_evaluations(
            pending_zipped_X=pending_X, ignored_zipped_X=ignored_X)
        return suggested_locations

    # def _value_so_far(self):
    #     """
    #     Computes E_n[U(f(x_max))|f], where U is the utility function, f is the true underlying ojective function and x_max = argmax E_n[U(f(x))|U]. See
    #     function _marginal_max_value_so_far below.
    #     """
    #
    #     output = 0
    #     support = self.utility.parameter_dist.support
    #     utility_dist = self.utility.parameter_dist.prob_dist
    #
    #     a = np.reshape(self.objective.evaluate(self._marginal_max_value_so_far())[0],(self.objective.output_dim,))
    #
    #     output += self.utility.eval_func(support[i],a)*utility_dist[i]
    #     #print(output)
    #     return output
    #
    #
    # def _marginal_max_value_so_far(self):
    #     """
    #     Computes argmax E_n[U(f(x))|U] (The abuse of notation can be misleading; note that the expectation is with
    #     respect to the posterior distribution on f after n evaluations)
    #     """
    #
    #     def val_func(X):
    #         X = np.atleast_2d(X)
    #         muX = self.model.posterior_mean(X)
    #         return muX
    #
    #     def val_func_with_gradient(X):
    #         X = np.atleast_2d(X)
    #         muX = self.model.posterior_mean(X)
    #         dmu_dX = self.model.posterior_mean_gradient(X)
    #         valX = np.reshape( muX, (X.shape[0],1))
    #         dval_dX =  dmu_dX
    #         return -valX, -dval_dX
    #
    #
    #     argmax = self.acquisition.optimizer.optimize_inner_func(f=val_func, f_df=val_func_with_gradient)[0]
    #     return argmax
    #
    #
    def run_optimization(self,
                         max_iter=1,
                         max_time=np.inf,
                         rep=None,
                         last_step_evaluator=None,
                         eps=1e-8,
                         context=None,
                         verbosity=False,
                         path=None,
                         evaluations_file=None):
        """
        Runs Bayesian Optimization for a number 'max_iter' of iterations (after the initial exploration data)

        :param max_iter: exploration horizon, or number of acquisitions. If nothing is provided optimizes the current acquisition.
        :param max_time: maximum exploration horizon in seconds.
        :param eps: minimum distance between two consecutive x's to keep running the model.
        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param verbosity: flag to print the optimization results after each iteration (default, False).
        :param evaluations_file: filename of the file where the evaluated points and corresponding evaluations are saved (default, None).
        """
        self.last_step_evaluator = last_step_evaluator
        if self.objective is None:
            raise InvalidConfigError(
                "Cannot run the optimization loop without the objective function"
            )

        # --- Save the options to print and save the results
        self.verbosity = verbosity
        self.evaluations_file = evaluations_file
        self.context = context
        self.path = path
        self.rep = rep
        # --- Setting up stop conditions
        self.eps = eps
        if (max_iter is None) and (max_time is None):
            self.max_iter = 0
            self.max_time = np.inf
        elif (max_iter is None) and (max_time is not None):
            self.max_iter = np.inf
            self.max_time = max_time
        elif (max_iter is not None) and (max_time is None):
            self.max_iter = max_iter
            self.max_time = np.inf
        else:
            self.max_iter = max_iter
            self.max_time = max_time

        # --- Initial function evaluation and model fitting
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)

            if self.constraint is not None:
                self.C, cost_values = self.constraint.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)

        #self.model.updateModel(self.X,self.Y)

        # --- Initialize iterations and running time
        self.time_zero = time.time()
        self.cum_time = 0
        self.num_acquisitions = 0
        self.suggested_sample = self.X
        self.Y_new = self.Y
        self.Opportunity_Cost = {"Hypervolume": np.array([])}
        value_so_far = []

        # --- Initialize time cost of the evaluations
        print("MAIN LOOP STARTS")
        self.true_best_stats = {
            "true_best": [],
            "mean_gp": [],
            "std gp": [],
            "pf": [],
            "mu_pf": [],
            "var_pf": [],
            "residual_noise": []
        }
        self._update_model()
        while (self.max_iter > self.num_acquisitions):

            self.optimize_final_evaluation()
            print("maKG optimizer")
            start = time.time()

            self.suggested_sample = self._compute_next_evaluations()
            print("self.suggested_sample", self.suggested_sample)

            if verbosity:
                if self.constraint is not None:
                    self.verbosity_plot_2D_constrained()
                else:
                    self.verbosity_plot_2D_unconstrained()

            # self.suggested_sample = np.array([[0.1,0.0]])
            finish = time.time()
            print("time optimisation point X", finish - start)

            self.X = np.vstack((self.X, self.suggested_sample))
            # --- Evaluate *f* in X, augment Y and update cost function (if needed)
            self.evaluate_objective()

            self._update_model()
            # --- Update current evaluation time and function evaluations
            self.cum_time = time.time() - self.time_zero
            self.num_acquisitions += 1
            print("optimize_final_evaluation")
            print("num acquired samples Main Alg: ", self.num_acquisitions)
            print("self.X, self.Y, self.C , self.Opportunity_Cost", self.X,
                  self.Y, self.C, self.Opportunity_Cost)

        return self.X, self.Y, self.C, self.Opportunity_Cost
        # --- Print the desired result in files
        #if self.evaluations_file is not None:
        #self.save_evaluations(self.evaluations_file)

        #file = open('test_file.txt','w')
        #np.savetxt('test_file.txt',value_so_far)

    def verbosity_plot_1D(self):
        ####plots
        print("generating plots")
        design_plot = np.linspace(0, 5, 100)[:, None]

        # precision = []
        # for i in range(20):
        #     kg_f = -self.acquisition._compute_acq(design_plot)
        #     precision.append(np.array(kg_f).reshape(-1))

        # print("mean precision", np.mean(precision, axis=0), "std precision",  np.std(precision, axis=0), "max precision", np.max(precision, axis=0), "min precision",np.min(precision, axis=0))
        ac_f = self.expected_improvement(design_plot)

        Y, _ = self.objective.evaluate(design_plot)
        C, _ = self.constraint.evaluate(design_plot)
        pf = self.probability_feasibility_multi_gp(design_plot,
                                                   self.model_c).reshape(
                                                       -1, 1)
        mu_f = self.model.predict(design_plot)[0]

        bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
        func_val = Y * bool_C.reshape(-1, 1)

        kg_f = -self.acquisition._compute_acq(design_plot)
        design_plot = design_plot.reshape(-1)
        fig, axs = plt.subplots(3, 2)
        axs[0, 0].set_title('True Function')
        axs[0, 0].plot(design_plot, np.array(func_val).reshape(-1))
        axs[0, 0].scatter(self.X, self.Y, color="red", label="sampled")
        # suggested_sample_value , _= self.objective.evaluate(self.suggested_sample)
        # axs[0, 0].scatter(self.suggested_sample, suggested_sample_value, marker="x", color="red",
        #                   label="suggested")

        axs[0, 0].legend()

        axs[0, 1].set_title('approximation Acqu Function')
        axs[0, 1].plot(design_plot, np.array(ac_f).reshape(-1))
        axs[0, 1].legend()

        axs[1, 0].set_title("mu and pf separetely ")
        axs[1, 0].plot(design_plot, np.array(mu_f).reshape(-1), label="mu")
        axs[1, 0].plot(design_plot, np.array(pf).reshape(-1), label="pf")
        axs[1, 0].legend()

        axs[1, 1].set_title("mu pf")
        axs[1, 1].plot(design_plot,
                       np.array(mu_f).reshape(-1) * np.array(pf).reshape(-1))
        axs[1, 1].legend()

        axs[2, 1].set_title('approximation kg Function')
        axs[2, 1].plot(design_plot, np.array(kg_f).reshape(-1))
        axs[2, 1].legend()
        plt.show()

    def verbosity_plot_2D_unconstrained(self):
        ####plots
        print("generating plots")
        design_plot = initial_design('random', self.space, 10000)

        # precision = []
        # for i in range(20):
        # kg_f = -self.acquisition._compute_acq(design_plot)
        #     precision.append(np.array(kg_f).reshape(-1))

        # print("mean precision", np.mean(precision, axis=0), "std precision",  np.std(precision, axis=0), "max precision", np.max(precision, axis=0), "min precision",np.min(precision, axis=0))

        func_val, _ = self.objective.evaluate(design_plot)
        func_val = np.concatenate(func_val, axis=1)

        mu_f = self.model.posterior_mean(design_plot)
        var_f = self.model.posterior_variance(design_plot, noise=False)
        mu_predicted_best = self.model.posterior_mean(self.suggested_sample)

        HVI = self.acquisition._compute_acq(design_plot)
        fig, axs = plt.subplots(2, 2)
        axs[0, 0].set_title('True PF Function')
        axs[0, 0].scatter(func_val[:, 0], func_val[:, 1])

        print("self.suggested_sample", self.suggested_sample)
        axs[0, 1].set_title("GP(X)")
        axs[0,
            1].scatter(
                design_plot[:, 0],
                design_plot[:, 1],
                c=np.array(mu_f).reshape(-1))  #,c= np.array(HVI).reshape(-1))
        axs[0, 1].scatter(self.suggested_sample[:, 0],
                          self.suggested_sample[:, 1],
                          color="magenta")
        axs[0, 1].legend()

        print("self.suggested_sample", self.suggested_sample)
        axs[1, 0].set_title("Var[GP(X)]")
        axs[1, 0].scatter(
            design_plot[:, 0],
            design_plot[:, 1],
            c=np.array(var_f).reshape(-1))  #,c= np.array(HVI).reshape(-1))
        axs[1, 0].scatter(self.suggested_sample[:, 0],
                          self.suggested_sample[:, 1],
                          color="magenta")
        axs[1, 0].legend()

        axs[1, 1].set_title("acq(X)")
        axs[1,
            1].scatter(
                design_plot[:, 0],
                design_plot[:, 1],
                c=np.array(HVI).reshape(-1))  #,c= np.array(HVI).reshape(-1))
        axs[1, 1].scatter(self.suggested_sample[:, 0],
                          self.suggested_sample[:, 1],
                          color="magenta")
        axs[1, 1].legend()

        # axs[1, 1].set_title("mu pf")
        # axs[1, 1].scatter(design_plot[:,0],design_plot[:,1],c= np.array(mu_f).reshape(-1) * np.array(pf).reshape(-1))
        # axs[1, 1].legend()
        #
        # axs[1, 0].set_title('Opportunity Cost')
        # axs[1, 0].plot(range(len(self.Opportunity_Cost)), self.Opportunity_Cost)
        # axs[1, 0].set_yscale("log")
        # axs[1, 0].legend()

        # axs[1, 1].set_title('True PF Function with sampled points')
        # axs[1, 1].scatter(func_val[:, 0], func_val[:, 1])
        # axs[1, 1].scatter(self.Y[0],self.Y[1], color="red", label="sampled")

        # import os
        # folder = "IMAGES"
        # subfolder = "new_branin"
        # cwd = os.getcwd()
        # print("cwd", cwd)
        # time_taken = time.time()
        # path = cwd + "/" + folder + "/" + subfolder + '/im_' +str(time_taken) +str(self.X.shape[0]) + '.pdf'
        # if os.path.isdir(cwd + "/" + folder + "/" + subfolder) == False:
        #     os.makedirs(cwd + "/" + folder + "/" + subfolder)
        # plt.savefig(path)
        plt.show()

    def verbosity_plot_2D_constrained(self):
        ####plots
        print("generating plots")
        design_plot = initial_design('random', self.space, 1000)

        # precision = []
        # for i in range(20):
        # kg_f = -self.acquisition._compute_acq(design_plot)
        #     precision.append(np.array(kg_f).reshape(-1))

        # print("mean precision", np.mean(precision, axis=0), "std precision",  np.std(precision, axis=0), "max precision", np.max(precision, axis=0), "min precision",np.min(precision, axis=0))

        # self.acquisition._gradient_sanity_check_2D(f=self.acquisition._compute_acq, grad_f = self.acquisition.acquisition_Gradients, x_value = self.suggested_sample, delta=1e-4)

        Y, _ = self.objective.evaluate(design_plot)
        Y = np.concatenate(Y, axis=1)
        C, _ = self.constraint.evaluate(design_plot)
        pf = self.probability_feasibility_multi_gp(design_plot,
                                                   self.model_c).reshape(
                                                       -1, 1)
        mu_f = self.model.posterior_mean(design_plot)
        bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
        bool_C = np.array(bool_C, dtype=bool)

        func_val = Y[bool_C]
        mu_predicted_best = self.model.posterior_mean(self.suggested_sample)
        # mu_predicted_final_best = self.model.posterior_mean(self.suggested_final_evaluation)
        feasable_mu_index = np.array(pf > 0.51, dtype=bool).reshape(-1)

        # HVI = self.acquisition._compute_acq(design_plot[feasable_mu_index ])
        # kg_f = -self.acquisition._compute_acq(design_plot)
        # HVI_optimiser = self.acquisition._compute_acq(self.suggested_sample)
        # print("optimiser best", HVI_optimiser, "discretisation best", np.max(np.array(HVI).reshape(-1)))
        # print("x", design_plot[np.argmax(np.array(HVI).reshape(-1))])
        # x_suggested_discretisation = design_plot[np.argmax(np.array(HVI).reshape(-1))]
        # print("ac with grad info optimised", self.acquisition._compute_acq_withGradients(self.suggested_sample), "ac info optimised",self.acquisition._compute_acq(self.suggested_sample))
        # print("best discretisation ac", self.acquisition._compute_acq(x_suggested_discretisation ))
        # print("best discretisation ac with gradients", self.acquisition._compute_acq_withGradients(x_suggested_discretisation))
        # print("mu predicted best opt", mu_predicted_best[0], mu_predicted_best[1])
        # print("mu predicted best discretisation", mu_f[0][np.argmax(np.array(HVI).reshape(-1))], mu_f[1][np.argmax(np.array(HVI).reshape(-1))])

        fig, axs = plt.subplots(2, 2)
        axs[0, 0].set_title('True PF Function')
        axs[0, 0].scatter(func_val[:, 0], func_val[:, 1])

        axs[0, 1].set_title("HVI")
        axs[0, 1].scatter(mu_f[0], mu_f[1],
                          color="green")  # , c=np.array(HVI).reshape(-1))
        axs[0, 1].scatter(mu_f[0][feasable_mu_index],
                          mu_f[1][feasable_mu_index],
                          color="blue")  #, c=np.array(HVI).reshape(-1))
        axs[0, 1].scatter(mu_predicted_best[0],
                          mu_predicted_best[1],
                          color="red",
                          label="optimiser best")
        # axs[0, 1].scatter(mu_predicted_final_best[0], mu_predicted_final_best[1], color="red", label="optimiser final best")
        #axs[0, 1].scatter(mu_f[0][np.argmax(np.array(HVI).reshape(-1))], mu_f[1][np.argmax(np.array(HVI).reshape(-1))], color="red",label="discretisation best")
        axs[0, 1].legend()

        axs[1, 0].set_title('Opportunity Cost')
        axs[1, 0].plot(range(len(self.Opportunity_Cost["Hypervolume"])),
                       self.Opportunity_Cost["Hypervolume"])
        axs[1, 0].set_yscale("log")
        axs[1, 0].legend()

        Y_reccomended, _ = self.objective.evaluate(self.suggested_sample)
        # Y_reccomended = np.concatenate(Y_reccomended, axis=1)

        axs[1, 1].set_title('True PF Function with sampled points')
        axs[1, 1].scatter(func_val[:, 0], func_val[:, 1])
        axs[1, 1].scatter(Y_reccomended[0],
                          Y_reccomended[1],
                          color="red",
                          label="sampled")
        axs[1, 1].scatter(self.Y[0], self.Y[1], color="green")

        # import os
        # folder = "IMAGES"
        # subfolder = "new_branin"
        # cwd = os.getcwd()
        # print("cwd", cwd)
        # time_taken = time.time()
        # path = cwd + "/" + folder + "/" + subfolder + '/im_' +str(time_taken) +str(self.X.shape[0]) + '.pdf'
        # if os.path.isdir(cwd + "/" + folder + "/" + subfolder) == False:
        #     os.makedirs(cwd + "/" + folder + "/" + subfolder)
        # plt.savefig(path)
        plt.show()

    def optimize_final_evaluation(self):

        if self.last_step_evaluator is None:
            if self.constraint is None:
                sampled_Y = self.model.get_Y_values()
                sampled_Y = np.concatenate(sampled_Y, axis=1).tolist()

                sampled_hv = hypervolume(sampled_Y)
                sampled_HV = sampled_hv.compute(ref_point=self.ref_point)
                #self.Opportunity_Cost.append(sampled_HV)
                feasable_Y = sampled_Y
                self.store_results(feasable_Y)
            else:
                sampled_Y = self.model.get_Y_values()
                sampled_Y = np.concatenate(sampled_Y, axis=1)

                C_true, C_cost_new = self.constraint.evaluate(self.X,
                                                              true_val=True)
                feasable_samples = np.product(
                    np.concatenate(C_true, axis=1) < 0, axis=1)
                feasable_samples = np.array(feasable_samples, dtype=bool)
                feasable_Y = sampled_Y[feasable_samples]

                self.store_results(feasable_Y)
        else:

            if self.constraint is not None:
                suggested_sample = self._compute_final_evaluations()
                self.suggested_final_evaluation = suggested_sample

                Y_new, cost_new = self.objective.evaluate(suggested_sample)
                Y_new = np.concatenate(Y_new, axis=1)
                C_new, C_cost_new = self.constraint.evaluate(suggested_sample)
                C_new = np.concatenate(C_new, axis=1)

                sampled_Y = self.model.get_Y_values()
                sampled_Y = np.concatenate(sampled_Y, axis=1)
                sampled_Y = np.vstack((sampled_Y, Y_new))

                C_true, C_cost_new = self.constraint.evaluate(self.X,
                                                              true_val=True)
                C_true = np.concatenate(C_true, axis=1)
                C_true = np.vstack((C_true, C_new))

                feasable_samples = np.product(C_true < 0, axis=1)
                feasable_samples = np.array(feasable_samples, dtype=bool)
                feasable_Y = sampled_Y[feasable_samples]
                self.store_results(feasable_Y)
            else:
                suggested_sample = self._compute_final_evaluations()
                # self.suggested_final_evaluation = suggested_sample
                #
                # Y_new, cost_new = self.objective.evaluate(suggested_sample)
                # Y_new = np.concatenate(Y_new, axis=1)
                #
                # X_train = self.model.get_X_values()
                # sampled_Y , cost_new = self.objective.evaluate(X_train)
                # sampled_Y = np.concatenate(sampled_Y, axis=1)
                # sampled_Y = np.vstack((sampled_Y, Y_new))
                #
                # feasable_Y = sampled_Y
                # self.store_results(feasable_Y)

        # design_plot = initial_design('random', self.space, 1000)
        # ac_f = self.expected_improvement(design_plot)
        # fig, axs = plt.subplots(2, 2)
        # axs[0, 0].set_title('True Function')
        # axs[0, 0].scatter(design_plot[:, 0], design_plot[:, 1], c=np.array(ac_f).reshape(-1))
        #
        # if self.tag_last_evaluation:
        #     start = time.time()
        #     self.acquisition.optimizer.context_manager = ContextManager(self.space, self.context)
        #     if self.constraint is None:
        #         out = self.acquisition.optimizer.optimize_inner_func(f=self.expected_improvement_unconstrained, duplicate_manager=None,  num_samples=100)
        #     else:
        #         out = self.acquisition.optimizer.optimize_inner_func(f=self.expected_improvement_constrained,
        #                                                              duplicate_manager=None, num_samples=100)
        #     print("out",out)
        #     suggested_sample =  self.space.zip_inputs(out[0])
        #     stop = time.time()
        #     # axs[0, 0].scatter(suggested_sample[:, 0], suggested_sample[:, 1], color="red")
        #     # plt.show()
        #     print("time EI", stop - start)
        #     # print("self.suggested_sample",suggested_sample)
        #     # --- Evaluate *f* in X, augment Y and update cost function (if needed)
        #
        #
        #     if self.deterministic:
        #         if self.constraint is None:
        #             func_val , _ = self.objective.evaluate(suggested_sample, true_val=True)
        #             # print("Y",Y)
        #             Y_true, cost_new = self.objective.evaluate(self.X, true_val=True)
        #
        #             feasable_Y_data = np.array(Y_true).reshape(-1)
        #             Y_aux = np.concatenate((func_val.reshape(-1), np.array(feasable_Y_data).reshape(-1)))
        #         else:
        #             Y, _ = self.objective.evaluate(suggested_sample, true_val=True)
        #             # print("Y",Y)
        #
        #             C, _ = self.constraint.evaluate(suggested_sample, true_val=True)
        #             bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
        #             func_val = Y * bool_C.reshape(-1, 1)
        #
        #             Y_true, cost_new = self.objective.evaluate(self.X ,true_val=True)
        #             C_true, C_cost_new = self.constraint.evaluate(self.X ,true_val=True)
        #
        #             feasable_Y_data = np.array(Y_true).reshape(-1) * np.product(np.concatenate(C_true, axis=1) < 0, axis=1)
        #
        #             Y_aux = np.concatenate((func_val.reshape(-1), np.array(feasable_Y_data).reshape(-1)))
        #
        #         if self.expensive:
        #             self.Opportunity_Cost.append(np.array(np.abs(np.max(Y_aux))).reshape(-1))
        #         else:
        #             self.true_best_value()
        #             optimum = np.max(np.abs(self.true_best_stats["true_best"]))
        #             print("optimum", optimum)
        #             self.Opportunity_Cost.append(optimum - np.array(np.abs(np.max(Y_aux))).reshape(-1))
        #
        #     else:
        #         # print("self.X,suggested_sample",self.X,suggested_sample)
        #
        #         samples = np.concatenate((self.X,suggested_sample))
        #         # print("samples", samples)
        #         Y= self.model.posterior_mean(samples)
        #         # print("Y",Y)
        #         pf = self.probability_feasibility_multi_gp(samples, model=self.model_c)
        #
        #         # print("pf", pf)
        #         func_val = np.array(Y).reshape(-1) * np.array(pf).reshape(-1)
        #
        #         # print("Y", Y, "pf", pf, "func_val", func_val)
        #         suggested_final_sample = samples[np.argmax(func_val)]
        #         suggested_final_sample = np.array(suggested_final_sample).reshape(-1)
        #         suggested_final_sample = np.array(suggested_final_sample).reshape(1,-1)
        #         # print("suggested_final_sample", suggested_final_sample, "val",np.max(func_val) )
        #         Y_true, _ = self.objective.evaluate(suggested_final_sample, true_val=True)
        #         # print("Y_true", Y_true)
        #         C_true, _ = self.constraint.evaluate(suggested_final_sample, true_val=True)
        #         # print("C_true", C_true)
        #         bool_C_true = np.product(np.concatenate(C_true, axis=1) < 0, axis=1)
        #         func_val_true = Y_true * bool_C_true.reshape(-1, 1)
        #         # print("func_val_true",func_val_true)
        #
        #         if self.expensive:
        #             self.Opportunity_Cost.append(np.array(np.abs(np.max(func_val_true))).reshape(-1))
        #         else:
        #             self.true_best_value()
        #             optimum = np.max(np.abs(self.true_best_stats["true_best"]))
        #             # print("optimum", optimum)
        #             self.Opportunity_Cost.append(optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))
        #             # print("OC_i", optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))
        #
        # else:
        #     samples = self.X
        #     # print("samples", samples)
        #     Y = self.model.posterior_mean(samples)
        #     # print("Y",Y)
        #     pf = self.probability_feasibility_multi_gp(samples, model=self.model_c)
        #
        #     # print("pf", pf)
        #     func_val = np.array(Y).reshape(-1) * np.array(pf).reshape(-1)
        #
        #     # print("Y", Y, "pf", pf, "func_val", func_val)
        #     suggested_final_sample = samples[np.argmax(func_val)]
        #     suggested_final_sample = np.array(suggested_final_sample).reshape(-1)
        #     suggested_final_sample = np.array(suggested_final_sample).reshape(1, -1)
        #     # print("suggested_final_sample", suggested_final_sample, "val",np.max(func_val) )
        #     Y_true, _ = self.objective.evaluate(suggested_final_sample, true_val=True)
        #     # print("Y_true", Y_true)
        #     C_true, _ = self.constraint.evaluate(suggested_final_sample, true_val=True)
        #     # print("C_true", C_true)
        #     bool_C_true = np.product(np.concatenate(C_true, axis=1) < 0, axis=1)
        #     func_val_true = Y_true * bool_C_true.reshape(-1, 1)
        #
        #     if self.expensive:
        #         self.Opportunity_Cost.append(np.array(np.abs(np.max(func_val_true))).reshape(-1))
        #     else:
        #         self.true_best_value()
        #         optimum = np.max(np.abs(self.true_best_stats["true_best"]))
        #         # print("optimum", optimum)
        #         self.Opportunity_Cost.append(optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))
        #         # print("OC_i", optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))
        #

    def store_results(self, feasable_Y):
        if len(feasable_Y) >= 1:
            sampled_hv = hypervolume(-feasable_Y)
            sampled_HV = 0  #sampled_hv.compute(ref_point=self.ref_point)
        else:
            sampled_HV = 0

        self.Opportunity_Cost["Hypervolume"] = np.concatenate(
            (self.Opportunity_Cost["Hypervolume"],
             np.array(sampled_HV).reshape(-1)))

        data = self.Opportunity_Cost
        if self.path is not None:
            gen_file = pd.DataFrame.from_dict(data)
            results_folder = "HVI"

            path = self.path + "/" + results_folder + '/it_' + str(
                self.rep) + '.csv'
            if os.path.isdir(self.path + "/" + results_folder) == False:
                os.makedirs(self.path + "/" + results_folder)

            gen_file.to_csv(path_or_buf=path)

    def expected_improvement_unconstrained(self, X, offset=1e-4):
        '''
        Computes the EI at points X based on existing samples X_sample
        and Y_sample using a Gaussian process surrogate model.

        Args:
            X: Points at which EI shall be computed (m x d).
            X_sample: Sample locations (n x d).
            Y_sample: Sample values (n x 1).
            gpr: A GaussianProcessRegressor fitted to samples.
            xi: Exploitation-exploration trade-off parameter.

        Returns:
            Expected improvements at points X.
        '''

        if self.deterministic:
            #print("DETERMINISTIC LAST STEP")
            mu = self.model.posterior_mean(X)
            sigma = self.model.posterior_variance(X, noise=False)

            sigma = np.sqrt(sigma).reshape(-1, 1)
            mu = mu.reshape(-1, 1)
            # Needed for noise-based model,
            # otherwise use np.max(Y_sample).
            # See also section 2.4 in [...]
            func_val = self.Y  #* bool_C.reshape(-1, 1)
            mu_sample_opt = np.max(func_val) - offset

            with np.errstate(divide='warn'):
                imp = mu - mu_sample_opt
                Z = imp / sigma
                ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z)
                ei[sigma == 0.0] = 0.0
            return -np.array(ei).reshape(-1)
        else:
            print("NOISY LAST STEP")
            mu = self.model.posterior_mean(X)
            mu = mu.reshape(-1, 1)
            return -np.array(mu).reshape(-1)

    def expected_improvement_constrained(self, X, offset=1e-4):
        '''
        Computes the EI at points X based on existing samples X_sample
        and Y_sample using a Gaussian process surrogate model.

        Args:
            X: Points at which EI shall be computed (m x d).
            X_sample: Sample locations (n x d).
            Y_sample: Sample values (n x 1).
            gpr: A GaussianProcessRegressor fitted to samples.
            xi: Exploitation-exploration trade-off parameter.

        Returns:
            Expected improvements at points X.
        '''

        if self.deterministic:
            #print("DETERMINISTIC LAST STEP")
            mu = self.model.posterior_mean(X)
            sigma = self.model.posterior_variance(X, noise=False)

            sigma = np.sqrt(sigma).reshape(-1, 1)
            mu = mu.reshape(-1, 1)
            # Needed for noise-based model,
            # otherwise use np.max(Y_sample).
            # See also section 2.4 in [...]
            bool_C = np.product(np.concatenate(self.C, axis=1) < 0, axis=1)
            func_val = self.Y * bool_C.reshape(-1, 1)
            mu_sample_opt = np.max(func_val) - offset

            with np.errstate(divide='warn'):
                imp = mu - mu_sample_opt
                Z = imp / sigma
                ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z)
                ei[sigma == 0.0] = 0.0
            pf = self.probability_feasibility_multi_gp(X,
                                                       self.model_c).reshape(
                                                           -1, 1)

            return -(ei * pf)
        else:
            print("NOISY LAST STEP")
            mu = self.model.posterior_mean(X)
            mu = mu.reshape(-1, 1)
            pf = self.probability_feasibility_multi_gp(X,
                                                       self.model_c).reshape(
                                                           -1, 1)
            return -(mu * pf)

    def probability_feasibility_multi_gp(self,
                                         x,
                                         model,
                                         mean=None,
                                         cov=None,
                                         grad=False,
                                         l=0):
        # print("model",model.output)
        x = np.atleast_2d(x)

        Fz = []
        for m in range(model.output_dim):
            Fz.append(self.probability_feasibility(x, model.output[m], grad,
                                                   l))
        Fz = np.product(Fz, axis=0)
        return Fz

    def probability_feasibility(self,
                                x,
                                model,
                                mean=None,
                                cov=None,
                                grad=False,
                                l=0):

        model = model.model
        # kern = model.kern
        # X = model.X
        mean = model.posterior_mean(x)
        var = model.posterior_variance(x, noise=False)
        # print("mean",mean,"var",var)
        std = np.sqrt(var).reshape(-1, 1)
        # print("std",std)
        aux_var = np.reciprocal(var)
        mean = mean.reshape(-1, 1)

        norm_dist = norm(mean, std)
        fz = norm_dist.pdf(l)
        Fz = norm_dist.cdf(l)

        if grad == True:
            grad_mean, grad_var = model.predictive_gradients(x)
            grad_std = (1 / 2.0) * grad_var

            # cov = kern.K(X, X) + np.eye(X.shape[0]) * 1e-3
            # L = scipy.linalg.cholesky(cov, lower=True)
            # u = scipy.linalg.solve(L, np.eye(X.shape[0]))
            # Ainv = scipy.linalg.solve(L.T, u)

            dims = range(x.shape[1])
            grad_Fz = []

            for d in dims:
                # K1 = np.diag(np.dot(np.dot(kern.dK_dX(x, X, d), Ainv), kern.dK_dX2(X, x, d)))
                # K2 = np.diag(kern.dK2_dXdX2(x, x, d, d))
                # var_grad = K2 - K1
                # var_grad = var_grad.reshape(-1, 1)
                grd_mean_d = grad_mean[:, d].reshape(-1, 1)
                grd_std_d = grad_std[:, d].reshape(-1, 1)
                # print("grd_mean ",grd_mean_d, "var_grad ",grd_std_d )
                # print("std",std)
                # print("aux_var", aux_var)
                # print("fz",fz)
                grad_Fz.append(fz * aux_var *
                               (mean * grd_std_d - grd_mean_d * std))
            grad_Fz = np.stack(grad_Fz, axis=1)
            return Fz.reshape(-1, 1), grad_Fz[:, :, 0]
        else:
            return Fz.reshape(-1, 1)

    def evaluate_objective(self):
        """
        Evaluates the objective
        """
        print(1)
        print(self.suggested_sample)
        self.Y_new, cost_new = self.objective.evaluate(self.suggested_sample)
        if self.constraint is not None:
            self.C_new, C_cost_new = self.constraint.evaluate(
                self.suggested_sample)

            for k in range(self.constraint.output_dim):

                self.C[k] = np.vstack((self.C[k], self.C_new[k]))

        self.cost.update_cost_model(self.suggested_sample, cost_new)
        for j in range(self.objective.output_dim):
            print(self.Y_new[j])
            self.Y[j] = np.vstack((self.Y[j], self.Y_new[j]))

    def compute_current_best(self):
        current_acqX = self.acquisition.current_compute_acq()
        return current_acqX

    def _distance_last_evaluations(self):
        """
        Computes the distance between the last two evaluations.
        """
        return np.sqrt(
            sum((self.X[self.X.shape[0] - 1, :] -
                 self.X[self.X.shape[0] - 2, :])**2))

    def _compute_next_evaluations(self,
                                  pending_zipped_X=None,
                                  ignored_zipped_X=None,
                                  re_use=False):
        """
        Computes the location of the new evaluation (optimizes the acquisition in the standard case).
        :param pending_zipped_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet).
        :param ignored_zipped_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again.
        :return:
        """
        ## --- Update the context if any

        self.acquisition.optimizer.context_manager = ContextManager(
            self.space,
            self.context,
        )
        print("compute next evaluation")
        if self.sample_from_acq:
            print("suggest next location given THOMPSON SAMPLING")
            candidate_points = initial_design('latin', self.space, 2000)
            aux_var = self.acquisition._compute_acq(candidate_points)

        else:
            if self.constraint is not None:

                aux_var = self.evaluator.compute_batch(duplicate_manager=None,
                                                       re_use=re_use,
                                                       constrained=True)
            else:

                aux_var = self.evaluator.compute_batch(duplicate_manager=None,
                                                       re_use=re_use,
                                                       constrained=False)

        return self.space.zip_inputs(aux_var[0])

    def _compute_final_evaluations(self,
                                   pending_zipped_X=None,
                                   ignored_zipped_X=None,
                                   re_use=False):
        """
        Computes the location of the new evaluation (optimizes the acquisition in the standard case).
        :param pending_zipped_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet).
        :param ignored_zipped_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again.
        :return:
        """
        ## --- Update the context if any

        self.acquisition.optimizer.context_manager = ContextManager(
            self.space,
            self.context,
        )
        print("compute next evaluation")
        if self.sample_from_acq:
            print("suggest next location given THOMPSON SAMPLING")
            candidate_points = initial_design('latin', self.space, 2000)
            aux_var = self.acquisition._compute_acq(candidate_points)
        else:
            if self.constraint is not None:
                aux_var = self.last_step_evaluator.compute_batch(
                    duplicate_manager=None, re_use=re_use, constrained=True)
            else:
                aux_var = self.last_step_evaluator.compute_batch(
                    duplicate_manager=None, re_use=re_use, constrained=False)

        return self.space.zip_inputs(aux_var[0])
        #return initial_design('random', self.space, 1)

    def _update_model(self):
        """
        Updates the model (when more than one observation is available) and saves the parameters (if available).
        """
        if (self.num_acquisitions % self.model_update_interval) == 0:

            ### --- input that goes into the model (is unziped in case there are categorical variables)
            X_inmodel = self.space.unzip_inputs(self.X)
            Y_inmodel = list(self.Y)
            Y_inmodel = -np.concatenate(Y_inmodel, axis=1)

            scalarisation_rdn = self.utility.sample_parameter(
                1)  #np.array([[0.9055273, 0.0944727]])# np.array([[0.5,0.5]])

            Utility = self.utility.func(parameter=scalarisation_rdn,
                                        y=Y_inmodel)
            Utility = [Utility]
            if self.constraint is not None:
                C_inmodel = list(self.C)
                self.model_c.updateModel(X_inmodel, C_inmodel)

            self.model.updateModel(X_inmodel, Utility)

        ### --- Save parameters of the model
        #self._save_model_parameter_values()

    def get_evaluations(self):
        return self.X.copy(), self.Y.copy()

    def true_best_value(self):
        from scipy.optimize import minimize

        X = initial_design('random', self.space, 1000)

        fval = self.func_val(X)

        anchor_point = np.array(X[np.argmin(fval)]).reshape(-1)
        anchor_point = anchor_point.reshape(1, -1)
        print("anchor_point", anchor_point)
        best_design = minimize(self.func_val,
                               anchor_point,
                               method='Nelder-Mead',
                               tol=1e-8).x

        self.true_best_stats["true_best"].append(self.func_val(best_design))
        self.true_best_stats["mean_gp"].append(
            self.model.posterior_mean(best_design))
        self.true_best_stats["std gp"].append(
            self.model.posterior_variance(best_design, noise=False))
        self.true_best_stats["pf"].append(
            self.probability_feasibility_multi_gp(best_design,
                                                  self.model_c).reshape(-1, 1))
        mean = self.model_c.posterior_mean(best_design)
        var = self.model_c.posterior_variance(best_design, noise=False)
        residual_noise = self.model_c.posterior_variance(self.X[1],
                                                         noise=False)
        self.true_best_stats["mu_pf"].append(mean)
        self.true_best_stats["var_pf"].append(var)
        self.true_best_stats["residual_noise"].append(residual_noise)

        if False:
            fig, axs = plt.subplots(3, 2)
            N = len(np.array(self.true_best_stats["std gp"]).reshape(-1))
            GAP = np.array(
                np.abs(
                    np.abs(self.true_best_stats["true_best"]).reshape(-1) -
                    np.abs(self.true_best_stats["mean_gp"]).reshape(-1))
            ).reshape(-1)
            print("GAP len", len(GAP))
            print("N", N)
            axs[0, 0].set_title('GAP')
            axs[0, 0].plot(range(N), GAP)
            axs[0, 0].set_yscale("log")

            axs[0, 1].set_title('VAR')
            axs[0,
                1].plot(range(N),
                        np.array(self.true_best_stats["std gp"]).reshape(-1))
            axs[0, 1].set_yscale("log")

            axs[1, 0].set_title("PF")
            axs[1, 0].plot(range(N),
                           np.array(self.true_best_stats["pf"]).reshape(-1))

            axs[1, 1].set_title("mu_PF")
            axs[1, 1].plot(
                range(N),
                np.abs(np.array(self.true_best_stats["mu_pf"]).reshape(-1)))
            axs[1, 1].set_yscale("log")

            axs[2, 1].set_title("std_PF")
            axs[2, 1].plot(
                range(N),
                np.sqrt(np.array(self.true_best_stats["var_pf"]).reshape(-1)))
            axs[2, 1].set_yscale("log")

            axs[2, 0].set_title("Irreducible noise")
            axs[2, 0].plot(
                range(N),
                np.sqrt(
                    np.array(
                        self.true_best_stats["residual_noise"]).reshape(-1)))
            axs[2, 0].set_yscale("log")

            plt.show()

    def func_val(self, x):
        if len(x.shape) == 1:
            x = x.reshape(1, -1)
        Y, _ = self.objective.evaluate(x, true_val=True)
        C, _ = self.constraint.evaluate(x, true_val=True)
        Y = np.array(Y).reshape(-1)
        out = Y.reshape(-1) * np.product(np.concatenate(C, axis=1) < 0,
                                         axis=1).reshape(-1)
        out = np.array(out).reshape(-1)
        return -out
Example #2
0
class BO(object):
    """
    Runner of the multi-attribute Bayesian optimization loop. This class wraps the optimization loop around the different handlers.
    :param model: GPyOpt model class.
    :param space: GPyOpt space class.
    :param objective: GPyOpt objective class.
    :param acquisition: GPyOpt acquisition class.
    :param evaluator: GPyOpt evaluator class.
    :param X_init: 2d numpy array containing the initial inputs (one per row) of the model.
    :param Y_init: 2d numpy array containing the initial outputs (one per row) of the model.
    :param cost: GPyOpt cost class (default, none).
    :param normalize_Y: whether to normalize the outputs before performing any optimization (default, True).
    :param model_update_interval: interval of collected observations after which the model is updated (default, 1).
    :param de_duplication: GPyOpt DuplicateManager class. Avoids re-evaluating the objective at previous, pending or infeasible locations (default, False).
    """


    def __init__(self, model, model_c,space, objective, constraint, acquisition, evaluator, X_init ,  Y_init=None, C_init=None, cost = None, normalize_Y = False, model_update_interval = 1, true_preference = 0.5):
        self.true_preference = true_preference
        self.model_c = model_c
        self.model = model
        self.space = space
        self.objective = objective
        self.constraint = constraint
        self.acquisition = acquisition
        self.utility = acquisition.utility
        self.evaluator = evaluator
        self.normalize_Y = normalize_Y
        self.model_update_interval = model_update_interval
        self.X = X_init
        self.Y = Y_init
        self.C = C_init
        self.cost = CostModel(cost)
        self.model_parameters_iterations = None

    def suggest_next_locations(self, context = None, pending_X = None, ignored_X = None):
        """
        Run a single optimization step and return the next locations to evaluate the objective.
        Number of suggested locations equals to batch_size.

        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param pending_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet) (default, None).
        :param ignored_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again (default, None).
        """
        self.model_parameters_iterations = None
        self.num_acquisitions = 0
        self.context = context
        self._update_model(self.normalization_type)

        suggested_locations = self._compute_next_evaluations(pending_zipped_X = pending_X, ignored_zipped_X = ignored_X)

        return suggested_locations
    
    
    # def _value_so_far(self):
    #     """
    #     Computes E_n[U(f(x_max))|f], where U is the utility function, f is the true underlying ojective function and x_max = argmax E_n[U(f(x))|U]. See
    #     function _marginal_max_value_so_far below.
    #     """
    #
    #     output = 0
    #     support = self.utility.parameter_dist.support
    #     utility_dist = self.utility.parameter_dist.prob_dist
    #
    #     a = np.reshape(self.objective.evaluate(self._marginal_max_value_so_far())[0],(self.objective.output_dim,))
    #
    #     output += self.utility.eval_func(support[i],a)*utility_dist[i]
    #     #print(output)
    #     return output
    #
    #
    # def _marginal_max_value_so_far(self):
    #     """
    #     Computes argmax E_n[U(f(x))|U] (The abuse of notation can be misleading; note that the expectation is with
    #     respect to the posterior distribution on f after n evaluations)
    #     """
    #
    #     def val_func(X):
    #         X = np.atleast_2d(X)
    #         muX = self.model.posterior_mean(X)
    #         return muX
    #
    #     def val_func_with_gradient(X):
    #         X = np.atleast_2d(X)
    #         muX = self.model.posterior_mean(X)
    #         dmu_dX = self.model.posterior_mean_gradient(X)
    #         valX = np.reshape( muX, (X.shape[0],1))
    #         dval_dX =  dmu_dX
    #         return -valX, -dval_dX
    #
    #
    #     argmax = self.acquisition.optimizer.optimize_inner_func(f=val_func, f_df=val_func_with_gradient)[0]
    #     return argmax
    #
    #
    def run_optimization(self, max_iter = 1, max_time = np.inf,  eps = 1e-8, context = None, verbosity=False, evaluations_file = None):
        """
        Runs Bayesian Optimization for a number 'max_iter' of iterations (after the initial exploration data)

        :param max_iter: exploration horizon, or number of acquisitions. If nothing is provided optimizes the current acquisition.
        :param max_time: maximum exploration horizon in seconds.
        :param eps: minimum distance between two consecutive x's to keep running the model.
        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param verbosity: flag to print the optimization results after each iteration (default, False).
        :param evaluations_file: filename of the file where the evaluated points and corresponding evaluations are saved (default, None).
        """

        if self.objective is None:
            raise InvalidConfigError("Cannot run the optimization loop without the objective function")

        # --- Save the options to print and save the results
        self.verbosity = verbosity
        self.evaluations_file = evaluations_file
        self.context = context
    
                
        # --- Setting up stop conditions
        self.eps = eps
        if  (max_iter is None) and (max_time is None):
            self.max_iter = 0
            self.max_time = np.inf
        elif (max_iter is None) and (max_time is not None):
            self.max_iter = np.inf
            self.max_time = max_time
        elif (max_iter is not None) and (max_time is None):
            self.max_iter = max_iter
            self.max_time = np.inf
        else:
            self.max_iter = max_iter
            self.max_time = max_time

        # --- Initial function evaluation and model fitting
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)
            self.C, cost_values = self.constraint.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)
    
        #self.model.updateModel(self.X,self.Y)

        # --- Initialize iterations and running time
        self.time_zero = time.time()
        self.cum_time  = 0
        self.num_acquisitions = 0
        self.suggested_sample = self.X
        self.Y_new = self.Y
        self.Opportunity_Cost = []
        value_so_far = []

        # --- Initialize time cost of the evaluations
        print("MAIN LOOP STARTS")
        Opportunity_Cost = []
        while (self.max_iter > self.num_acquisitions ):


            # self._update_model()


            print("maKG optimizer")
            start = time.time()
            self.suggested_sample = initial_design('random', self.space, 1)
            finish = time.time()
            print("time optimisation point X", finish - start)

            if verbosity:
                self.verbosity_plot_2D()
            print("self.Opportunity_Cost",self.Opportunity_Cost)
            self.X = np.vstack((self.X,self.suggested_sample))
            # --- Evaluate *f* in X, augment Y and update cost function (if needed)
            self.evaluate_objective()

            # --- Update current evaluation time and function evaluations
            self.cum_time = time.time() - self.time_zero
            self.num_acquisitions += 1
            print("optimize_final_evaluation")
            self.optimize_final_evaluation()
            print("self.X, self.Y, self.C , self.Opportunity_Cost",self.X, self.Y, self.C , self.Opportunity_Cost)

        return self.X, self.Y, self.C , self.Opportunity_Cost
        # --- Print the desired result in files
        #if self.evaluations_file is not None:
            #self.save_evaluations(self.evaluations_file)

        #file = open('test_file.txt','w')
        #np.savetxt('test_file.txt',value_so_far)

    def verbosity_plot_1D(self):
        ####plots
        print("generating plots")
        design_plot = np.linspace(0,5,100)[:,None]

        # precision = []
        # for i in range(20):
        #     kg_f = -self.acquisition._compute_acq(design_plot)
        #     precision.append(np.array(kg_f).reshape(-1))

        # print("mean precision", np.mean(precision, axis=0), "std precision",  np.std(precision, axis=0), "max precision", np.max(precision, axis=0), "min precision",np.min(precision, axis=0))
        ac_f = self.expected_improvement(design_plot)

        Y, _ = self.objective.evaluate(design_plot)
        C, _ = self.constraint.evaluate(design_plot)
        pf = self.probability_feasibility_multi_gp(design_plot, self.model_c).reshape(-1, 1)
        mu_f = self.model.predict(design_plot)[0]

        bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
        func_val = Y * bool_C.reshape(-1, 1)

        kg_f = -self.acquisition._compute_acq(design_plot)
        design_plot = design_plot.reshape(-1)
        fig, axs = plt.subplots(3, 2)
        axs[0, 0].set_title('True Function')
        axs[0, 0].plot(design_plot, np.array(func_val).reshape(-1))
        axs[0, 0].scatter(self.X, self.Y, color="red", label="sampled")
        suggested_sample_value , _= self.objective.evaluate(self.suggested_sample)
        axs[0, 0].scatter(self.suggested_sample, suggested_sample_value, marker="x", color="red",
                          label="suggested")

        axs[0, 0].legend()

        axs[0, 1].set_title('approximation Acqu Function')
        axs[0, 1].plot(design_plot, np.array(ac_f).reshape(-1))
        axs[0, 1].legend()

        axs[1, 0].set_title("mu and pf separetely ")
        axs[1, 0].plot(design_plot, np.array(mu_f).reshape(-1) , label="mu")
        axs[1, 0].plot(design_plot,  np.array(pf).reshape(-1), label="pf")
        axs[1, 0].legend()

        axs[1, 1].set_title("mu pf")
        axs[1, 1].plot(design_plot, np.array(mu_f).reshape(-1) * np.array(pf).reshape(-1))
        axs[1, 1].legend()

        axs[2, 1].set_title('approximation kg Function')
        axs[2, 1].plot(design_plot, np.array(kg_f).reshape(-1))
        axs[2, 1].legend()
        plt.show()
    def verbosity_plot_2D(self):
        ####plots
        print("generating plots")
        design_plot = initial_design('random', self.space, 1000)

        # precision = []
        # for i in range(20):
        #     kg_f = -self.acquisition._compute_acq(design_plot)
        #     precision.append(np.array(kg_f).reshape(-1))

        # print("mean precision", np.mean(precision, axis=0), "std precision",  np.std(precision, axis=0), "max precision", np.max(precision, axis=0), "min precision",np.min(precision, axis=0))
        ac_f = self.expected_improvement(design_plot)

        Y, _ = self.objective.evaluate(design_plot)
        C, _ = self.constraint.evaluate(design_plot)
        pf = self.probability_feasibility_multi_gp(design_plot, self.model_c).reshape(-1, 1)
        mu_f = self.model.predict(design_plot)[0]

        bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
        func_val = Y * bool_C.reshape(-1, 1)

        # kg_f = -self.acquisition._compute_acq(design_plot)
        fig, axs = plt.subplots(2, 2)
        axs[0, 0].set_title('True Function')
        axs[0, 0].scatter(design_plot[:, 0], design_plot[:, 1], c=np.array(func_val).reshape(-1))
        axs[0, 0].scatter(self.X[:, 0], self.X[:, 1], color="red", label="sampled")
        #suggested_sample_value = self.objective.evaluate(self.suggested_sample)
        axs[0, 0].scatter(self.suggested_sample[:,0], self.suggested_sample[:,1], marker="x", color="red",
                          label="suggested")
        axs[0, 0].legend()

        axs[0, 1].set_title('approximation Acqu Function')
        axs[0, 1].scatter(design_plot[:,0],design_plot[:,1], c=np.array(ac_f).reshape(-1))
        axs[0, 1].legend()

        # axs[1, 0].set_title("KG")
        # axs[1, 0].scatter(design_plot[:,0],design_plot[:,1],c= np.array(kg_f).reshape(-1))
        # axs[1, 0].legend()

        axs[1, 1].set_title("mu pf")
        axs[1, 1].scatter(design_plot[:,0],design_plot[:,1],c= np.array(mu_f).reshape(-1) * np.array(pf).reshape(-1))
        axs[1, 1].legend()

        # axs[2, 1].set_title('approximation kg Function')
        # axs[2, 1].scatter(design_plot, np.array(kg_f).reshape(-1))
        # axs[2, 1].legend()
        # import os
        # folder = "IMAGES"
        # subfolder = "new_branin"
        # cwd = os.getcwd()
        # print("cwd", cwd)
        # time_taken = time.time()
        # path = cwd + "/" + folder + "/" + subfolder + '/im_' +str(time_taken) +str(self.X.shape[0]) + '.pdf'
        # if os.path.isdir(cwd + "/" + folder + "/" + subfolder) == False:
        #     os.makedirs(cwd + "/" + folder + "/" + subfolder)
        # plt.savefig(path)
        plt.show()
    def optimize_final_evaluation(self):



        # design_plot = initial_design('random', self.space, 1000)
        # ac_f = self.expected_improvement(design_plot)
        # fig, axs = plt.subplots(2, 2)
        # axs[0, 0].set_title('True Function')
        # axs[0, 0].scatter(design_plot[:, 0], design_plot[:, 1], c=np.array(ac_f).reshape(-1))

        # start = time.time()
        # self.acquisition.optimizer.context_manager = ContextManager(self.space, self.context)
        # out = self.acquisition.optimizer.optimize(f=self.expected_improvement, duplicate_manager=None, re_use=False, num_samples=20, verbose=False)
        # suggested_sample =  self.space.zip_inputs(out[0])
        # stop = time.time()
        # # axs[0, 0].scatter(suggested_sample[:, 0], suggested_sample[:, 1], color="red")
        # # plt.show()
        # print("time EI", stop - start)
        # # print("self.suggested_sample",suggested_sample)
        # # --- Evaluate *f* in X, augment Y and update cost function (if needed)
        # Y, _ = self.objective.evaluate(suggested_sample)
        # # print("Y",Y)
        # C, _ = self.constraint.evaluate(suggested_sample)
        #
        # bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
        # func_val = Y * bool_C.reshape(-1, 1)
        feasable_Y_data = np.array(self.Y).reshape(-1) * np.product(np.concatenate(self.C, axis=1) < 0, axis=1)
        # print("suggested_sample",suggested_sample, "feasable_Y_data",func_val)
        # print("C[-1, :]",C[-1, :])
        # feasable_point = bool_C

        Y_aux = np.array(feasable_Y_data).reshape(-1)

        self.Opportunity_Cost.append(np.max(Y_aux))

    def expected_improvement(self, X, offset=0.0):
        '''
        Computes the EI at points X based on existing samples X_sample
        and Y_sample using a Gaussian process surrogate model.

        Args:
            X: Points at which EI shall be computed (m x d).
            X_sample: Sample locations (n x d).
            Y_sample: Sample values (n x 1).
            gpr: A GaussianProcessRegressor fitted to samples.
            xi: Exploitation-exploration trade-off parameter.

        Returns:
            Expected improvements at points X.
        '''
        mu, sigma = self.model.predict(X)

        # sigma = np.sqrt(sigma).reshape(-1, 1)
        mu = mu.reshape(-1,1)
        # Needed for noise-based model,
        # otherwise use np.max(Y_sample).
        # See also section 2.4 in [...]
        bool_C = np.product(np.concatenate(self.C, axis=1) < 0, axis=1)
        func_val = self.Y * bool_C.reshape(-1, 1)
        mu_sample_opt = np.max(func_val) - offset

        # with np.errstate(divide='warn'):
        #     imp = mu - mu_sample_opt
        #     Z = imp / sigma
        #     ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z)
        #     ei[sigma == 0.0] = 0.0
        pf = self.probability_feasibility_multi_gp(X,self.model_c).reshape(-1,1)

        pf[pf<0.50] = 0

        return -(mu *pf )

    def probability_feasibility_multi_gp(self, x, model, mean=None, cov=None, grad=False, l=0):
        # print("model",model.output)
        x = np.atleast_2d(x)
        if grad == False:
            Fz = []
            for m in range(model.output_dim):
                Fz.append(self.probability_feasibility( x, model.output[m], grad, l))
            Fz = np.product(Fz,axis=0)
            return Fz
        else:
            Fz = []
            grad_Fz = []
            for m in range(model.output_dim):
                # print("model.output[m]",model.output[m])
                # print("mean[m]",mean[m])
                # print("cov[m]",cov[m])
                Fz_aux, grad_Fz_aux = self.probability_feasibility( x, model.output[m])
                Fz.append(Fz_aux)
                grad_Fz.append(grad_Fz_aux)

            # print("np.array(Fz)", np.array(Fz), "grad_Fz", np.array(grad_Fz))
            grad_Fz = self.product_gradient_rule(func = np.array(Fz), grad = np.array(grad_Fz))
            # print("output grad_Fz", grad_Fz)

            Fz = np.product(Fz, axis=0)
            return Fz, grad_Fz

    def probability_feasibility(self, x, model, mean=None, cov=None, grad=False, l=0):

        model = model.model
        # kern = model.kern
        # X = model.X
        mean, cov = model.predict(x, full_cov=True)
        var = np.diag(cov).reshape(-1, 1)
        std = np.sqrt(var).reshape(-1, 1)

        aux_var = np.reciprocal(var)
        mean = mean.reshape(-1, 1)

        norm_dist = norm(mean, std)
        fz = norm_dist.pdf(l)
        Fz = norm_dist.cdf(l)

        if grad == True:
            grad_mean , grad_var = model.predictive_gradients(x)
            grad_std = (1/2.0)*grad_var

            # cov = kern.K(X, X) + np.eye(X.shape[0]) * 1e-3
            # L = scipy.linalg.cholesky(cov, lower=True)
            # u = scipy.linalg.solve(L, np.eye(X.shape[0]))
            # Ainv = scipy.linalg.solve(L.T, u)

            dims = range(x.shape[1])
            grad_Fz = []

            for d in dims:
                # K1 = np.diag(np.dot(np.dot(kern.dK_dX(x, X, d), Ainv), kern.dK_dX2(X, x, d)))
                # K2 = np.diag(kern.dK2_dXdX2(x, x, d, d))
                # var_grad = K2 - K1
                # var_grad = var_grad.reshape(-1, 1)
                grd_mean_d = grad_mean[:, d].reshape(-1, 1)
                grd_std_d = grad_std[:, d].reshape(-1, 1)
                # print("grd_mean ",grd_mean_d, "var_grad ",grd_std_d )
                # print("std",std)
                # print("aux_var", aux_var)
                # print("fz",fz)
                grad_Fz.append(fz * aux_var * (mean * grd_std_d - grd_mean_d * std))
            grad_Fz = np.stack(grad_Fz, axis=1)
            return Fz.reshape(-1, 1), grad_Fz[:, :, 0]
        else:
            return Fz.reshape(-1, 1)


    def evaluate_objective(self):
        """
        Evaluates the objective
        """
        print(1)
        print(self.suggested_sample)
        self.Y_new, cost_new = self.objective.evaluate(self.suggested_sample)
        self.C_new, C_cost_new = self.constraint.evaluate(self.suggested_sample)
        self.cost.update_cost_model(self.suggested_sample, cost_new)   
        for j in range(self.objective.output_dim):
            print(self.Y_new[j])
            self.Y[j] = np.vstack((self.Y[j],self.Y_new[j]))

        for k in range(self.constraint.output_dim):
            print(self.C_new[k])
            self.C[k] = np.vstack((self.C[k],self.C_new[k]))

    def compute_current_best(self):
        current_acqX = self.acquisition.current_compute_acq()
        return current_acqX

    def _distance_last_evaluations(self):
        """
        Computes the distance between the last two evaluations.
        """
        return np.sqrt(sum((self.X[self.X.shape[0]-1,:]-self.X[self.X.shape[0]-2,:])**2))


    def _compute_next_evaluations(self, pending_zipped_X=None, ignored_zipped_X=None, re_use=False):

        """
        Computes the location of the new evaluation (optimizes the acquisition in the standard case).
        :param pending_zipped_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet).
        :param ignored_zipped_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again.
        :return:
        """
        ## --- Update the context if any

        self.acquisition.optimizer.context_manager = ContextManager(self.space, self.context)

        aux_var = self.evaluator.compute_batch(duplicate_manager=None, re_use=re_use)
        ### We zip the value in case there are categorical variables
        return self.space.zip_inputs(aux_var[0])
        #return initial_design('random', self.space, 1)

    def _update_model(self):
        """
        Updates the model (when more than one observation is available) and saves the parameters (if available).
        """
        if (self.num_acquisitions%self.model_update_interval)==0:

            ### --- input that goes into the model (is unziped in case there are categorical variables)
            X_inmodel = self.space.unzip_inputs(self.X)
            Y_inmodel = list(self.Y)
            C_inmodel = list(self.C)
            
            self.model.updateModel(X_inmodel, Y_inmodel)
            self.model_c.updateModel(X_inmodel, C_inmodel)
        ### --- Save parameters of the model
        #self._save_model_parameter_values()


    def get_evaluations(self):
        return self.X.copy(), self.Y.copy()
Example #3
0
class REMBO(BO):
    """

    Args:
        f (function): function to optimize.
        domain (list | None): the description of the inputs variables
            (See GpyOpt.core.space.Design_space class for details)
        constraints (list | None): the description of the problem constraints
            (See GpyOpt.core.space.Design_space class for details)


    Attributes:
        initial_design_numdata (int):
        initial_design_type (string):

        domain (dict | None):
        constraints (dict | None):
        space (Design_space):
        model (BOModel):
        acquisition (AcquisitionBase):
        cost (CostModel):
    """

    def __init__(self, f, domain=None, constraints=None, cost_withGradients=None, X=None,
                 Y=None, subspace_dim_size=0,
                 model_type='GP', initial_design_numdata=1, initial_design_type='random', acquisition_type='LCB',
                 normalize_Y=True, exact_feval=False, acquisition_optimizer_type='lbfgs', model_update_interval=1,
                 evaluator_type='sequential', batch_size=1, maximize=False, de_duplication=False):

        if model_type == 'input_warped_GP':
            raise NotImplementedError('input_warped_GP model is not implemented')

        if acquisition_type in ['EI_MCMC', 'MPI_MCMC', 'LCB_MCMC']:
            raise NotImplementedError('MCMC is not implemented')

        if batch_size is not 1 or evaluator_type is not 'sequential':
            raise NotImplementedError('only sequential evaluation is implemented')

        if cost_withGradients is not None:
            raise NotImplementedError('param cost is not implemented')

        if constraints is not None:
            raise NotImplementedError('param constraints is not implemented')

        # private field
        self._arguments_mng = ArgumentsManager(kwargs=dict())

        self.subspace_dim_size = subspace_dim_size
        self.cost_withGradients = cost_withGradients
        self.initial_design_numdata = initial_design_numdata
        self.initial_design_type = initial_design_type
        self.model_type = model_type
        self.acquisition_type = acquisition_type
        self.evaluator_type = evaluator_type
        self.model_update_interval = model_update_interval
        self.maximize = maximize
        self.normalize_Y = normalize_Y
        self.de_duplication = de_duplication
        self.subspace_dim_size = subspace_dim_size
        self.original_domain = domain

        # --- property injected in other methods.
        self.verbosity = False
        self.subspace = None
        self.max_time = None
        self.max_iter = None
        self.cum_time = None
        self.report_file = None
        self.evaluations_file = None
        self.models_file = None
        self.eps = None
        self.save_models_parameters = None
        # --- unnecessary property
        self.suggested_sample = None
        self.Y_new = None

        # --- BO class property in uncertain use
        self.num_cores = 1

        self.objective = SingleObjective(self._sign(f), batch_size, f.get_function_name())
        self.cost = CostModel(cost_withGradients=cost_withGradients)

        self.space = initialize_space(domain=domain, constraints=constraints)
        self.embedding_matrix = np.random.normal(size=(self.dimensionality, subspace_dim_size))

        subspace_domain = self.choose_subspace_domain(subspace_dim_size=subspace_dim_size)

        self.subspace = initialize_space(domain=subspace_domain, constraints=constraints)

        self.model = self._arguments_mng.model_creator(
            model_type=self.model_type, exact_feval=exact_feval, space=self.subspace)

        self.acquisition = self._arguments_mng.acquisition_creator(
            acquisition_type=self.acquisition_type, model=self.model, space=self.subspace,
            acquisition_optimizer=AcquisitionOptimizer(space=self.space, optimizer=acquisition_optimizer_type),
            cost_withGradients=self.cost_withGradients
        )

        self.evaluator = self._arguments_mng.evaluator_creator(
            evaluator_type=self.evaluator_type, acquisition=self.acquisition,
            batch_size=self.batch_size, model_type=self.model_type, model=self.model,
            space=self.space, acquisition_optimizer=self.acquisition_optimizer
        )

        self.X = X
        self.Y = Y
        self._set_initial_values()

        super().__init__(
            model=self.model,
            space=self.subspace,
            objective=self.objective,
            acquisition=self.acquisition,
            evaluator=self.evaluator,
            X_init=self.X,
            Y_init=self.Y,
            cost=self.cost,
            normalize_Y=self.normalize_Y,
            model_update_interval=self.model_update_interval,
            de_duplication=self.de_duplication
        )

    @property
    def dimensionality(self):
        return self.space.objective_dimensionality

    @property
    def subspace_domain(self):
        return self.subspace.config_space

    @property
    def domain(self):
        return self.space.config_space

    @property
    def constraints(self):
        return self.space.constraints

    @property
    def f(self):
        return self.objective.func

    @property
    def cost_type(self):
        return self.cost.cost_withGradients

    @property
    def exact_feval(self):
        return self.model.exact_feval

    @property
    def acquisition_optimizer_type(self):
        return self.acquisition_optimizer.optimizer_name

    @property
    def acquisition_optimizer(self):
        return self.acquisition.optimizer

    @property
    def batch_size(self):
        return self.objective.n_procs

    @property
    def objective_name(self):
        return self.objective.objective_name

    def choose_subspace_domain(self, subspace_dim_size):
        subspace_domain = list()

        for i in range(subspace_dim_size):
            subspace_domain.append({
                'name': 'x' + str(i),
                'type': 'continuous',
                'domain': (-np.sqrt(self.dimensionality), np.sqrt(self.dimensionality)),
                'dimensionality': 1
            })
        return subspace_domain

    def map_to_original_space(self, x):
        if x.shape != (1, self.subspace_dim_size):
            raise ValueError('x.shape is not correct ' + str(x.shape))

        x = np.array([np.dot(self.embedding_matrix, el) for el in x])
        return x

    def run_optimization(self, max_iter=0, max_time=np.inf, eps=1e-8, context=None,
                         verbosity=False, save_models_parameters=True, report_file=None,
                         evaluations_file=None, models_file=None):

        if self.objective is None:
            raise ValueError("Cannot run the optimization loop without the objective function")

        # --- Save the options to print and save the results
        self.verbosity = verbosity
        self.save_models_parameters = save_models_parameters
        self.report_file = report_file
        self.evaluations_file = evaluations_file
        self.models_file = models_file
        self.model_parameters_iterations = None
        self.context = context

        # --- Check if we can save the model parameters in each iteration
        if self.save_models_parameters:
            if not (isinstance(self.model, GPModel)):
                print('Models printout after each iteration is only available for GP and GP_MCMC models')
                self.save_models_parameters = False

                # --- Setting up stop conditions
            self.eps = eps
            if (max_iter is None) and (max_time is None):
                self.max_iter = 0
                self.max_time = np.inf
            elif (max_iter is None) and (max_time is not None):
                self.max_iter = np.inf
                self.max_time = max_time
            elif (max_iter is not None) and (max_time is None):
                self.max_iter = max_iter
                self.max_time = np.inf
            else:
                self.max_iter = max_iter
                self.max_time = max_time

        # --- Initialize iterations and running time
        stopwatch = StopWatch()
        self.num_acquisitions = self.initial_design_numdata
        self.suggested_sample = self.X
        self.Y_new = self.Y
        self._compute_results()

        self._run_optimization()

        self.cum_time = stopwatch.passed_time()

        # --- Stop messages and execution time
        self._compute_results()

        # --- Print the desired result in files
        if self.report_file is not None:
            self.save_report(self.report_file)
        if self.evaluations_file is not None:
            self.save_evaluations(self.evaluations_file)
        if self.models_file is not None:
            self.save_models(self.models_file)

        self._save()

    def _run_optimization(self):
        while True:
            print('.')

            # --- update model
            try:
                self.update()

            except np.linalg.LinAlgError:
                print('np.linalg.LinAlgError')
                break

            if self.num_acquisitions >= self.max_iter:
                break

            self.next_point()

            # --- Update current evaluation time and function evaluations
            self.num_acquisitions += 1

    def next_point(self):
        self.suggested_sample = self._compute_next_evaluations()

        # --- Augment X
        self.X = np.vstack((self.X, self.suggested_sample))

        # --- Evaluate *f* in X, augment Y and update cost function (if needed)
        self.evaluate_objective()

    def evaluate_objective(self):
        """
        Evaluates the objective
        """
        original_suggested_sample = self.map_to_original_space(x=self.suggested_sample)
        self.Y_new, cost_new = self.objective.evaluate(original_suggested_sample)
        self.cost.update_cost_model(self.suggested_sample, cost_new)
        self.Y = np.vstack((self.Y, self.Y_new))

    def get_best_point(self):
        self._compute_results()
        return self.x_opt, self.fx_opt

    def update(self):
        self._update_model(self.normalization_type)
        self._update_acquisition()
        self._update_evaluator()

    def _sign(self, f):
        if self.maximize:
            f_copy = f

            def f(x): return -f_copy(x)
        return f

    def _set_initial_values(self):
        if self.X is None:
            self.X = initial_design(self.initial_design_type, self.subspace, self.initial_design_numdata)
            self.Y, _ = self.objective.evaluate(self.map_to_original_space(x=self.X))
        elif self.X is not None and self.Y is None:
            self.Y, _ = self.objective.evaluate(self.map_to_original_space(x=self.X))

        # save initial values
        self.initial_X = deepcopy(self.X)
        if self.maximize:
            self.initial_Y = -deepcopy(self.Y)
        else:
            self.initial_Y = deepcopy(self.Y)

    def _update_acquisition(self):
        self.acquisition = self._arguments_mng.acquisition_creator(
            acquisition_type=self.acquisition_type, model=self.model, space=self.subspace,
            acquisition_optimizer=AcquisitionOptimizer(space=self.subspace, optimizer=self.acquisition_optimizer_type),
            cost_withGradients=self.cost_withGradients
        )

    def _update_evaluator(self):
        self.evaluator.acquisition = self.acquisition

    def _update_model(self, normalization_type='stats'):
        if self.num_acquisitions % self.model_update_interval == 0:

            self.model = self._arguments_mng.model_creator(
                model_type=self.model_type, exact_feval=self.exact_feval, space=self.subspace)

            X_inmodel, Y_inmodel = self._input_data(normalization_type=normalization_type)

            self.model.updateModel(X_inmodel, Y_inmodel, None, None)
            self.X_inmodel = X_inmodel
            self.Y_inmodel = Y_inmodel

        # Save parameters of the model
        self._save_model_parameter_values()

    def _input_data(self, normalization_type):
        # input that goes into the model (is unziped in case there are categorical variables)
        X_inmodel = self.subspace.unzip_inputs(self.X)

        # Y_inmodel is the output that goes into the model
        if self.normalize_Y:
            Y_inmodel = normalize(self.Y, normalization_type)
        else:
            Y_inmodel = self.Y

        return X_inmodel, Y_inmodel

    def _compute_next_evaluations(self, pending_zipped_X=None, ignored_zipped_X=None):
        # --- Update the context if any
        self.acquisition.optimizer.context_manager = ContextManager(self.subspace, self.context)

        # --- Activate de_duplication
        if self.de_duplication:
            duplicate_manager = DuplicateManager(
                space=self.subspace, zipped_X=self.X, pending_zipped_X=pending_zipped_X,
                ignored_zipped_X=ignored_zipped_X)
        else:
            duplicate_manager = None

        # We zip the value in case there are categorical variables
        suggested_ = self.subspace.zip_inputs(self.evaluator.compute_batch(
            duplicate_manager=duplicate_manager,
            context_manager=self.acquisition.optimizer.context_manager))

        return suggested_

    def _save(self):
        mkdir_when_not_exist(abs_path=definitions.ROOT_DIR + '/storage/' + self.objective_name)

        dir_name = definitions.ROOT_DIR + '/storage/' + self.objective_name + '/' + now_str() + ' ' + str(
            len(self.original_domain)) + 'D REMBO_' + str(self.subspace_dim_size)
        mkdir_when_not_exist(abs_path=dir_name)

        self.save_report(report_file=dir_name + '/report.txt')
        self.save_evaluations(evaluations_file=dir_name + '/evaluation.csv')
        self.save_models(models_file=dir_name + '/model.csv')

    def save_report(self, report_file=None):
        with open(report_file, 'w') as file:
            import GPyOpt
            import time

            file.write(
                '-----------------------------' + ' GPyOpt Report file ' + '-----------------------------------\n')
            file.write('GPyOpt Version ' + str(GPyOpt.__version__) + '\n')
            file.write('Date and time:               ' + time.strftime("%c") + '\n')
            if self.num_acquisitions == self.max_iter:
                file.write('Optimization completed:      ' + 'YES, ' + str(self.X.shape[0]).strip(
                    '[]') + ' samples collected.\n')
                file.write('Number initial samples:      ' + str(self.initial_design_numdata) + ' \n')
            else:
                file.write('Optimization completed:      ' + 'NO,' + str(self.X.shape[0]).strip(
                    '[]') + ' samples collected.\n')
                file.write('Number initial samples:      ' + str(self.initial_design_numdata) + ' \n')

            file.write('Tolerance:                   ' + str(self.eps) + '.\n')
            file.write('Optimization time:           ' + str(self.cum_time).strip('[]') + ' seconds.\n')

            file.write('\n')
            file.write(
                '--------------------------------' + ' Problem set up ' + '------------------------------------\n')
            file.write('Problem name:                ' + self.objective_name + '\n')
            file.write('Problem dimension:           ' + str(self.dimensionality) + '\n')
            file.write('Number continuous variables  ' + str(len(self.space.get_continuous_dims())) + '\n')
            file.write('Number discrete variables    ' + str(len(self.space.get_discrete_dims())) + '\n')
            file.write('Number bandits               ' + str(self.space.get_bandit().shape[0]) + '\n')
            file.write('Noiseless evaluations:       ' + str(self.exact_feval) + '\n')
            file.write('Cost used:                   ' + self.cost.cost_type + '\n')
            file.write('Constraints:                 ' + str(self.constraints == True) + '\n')
            file.write('Subspace Dimension:          ' + str(self.subspace_dim_size) + '\n')

            file.write('\n')
            file.write(
                '------------------------------' + ' Optimization set up ' + '---------------------------------\n')
            file.write('Normalized outputs:          ' + str(self.normalize_Y) + '\n')
            file.write('Model type:                  ' + str(self.model_type).strip('[]') + '\n')
            file.write('Model update interval:       ' + str(self.model_update_interval) + '\n')
            file.write('Acquisition type:            ' + str(self.acquisition_type).strip('[]') + '\n')
            file.write(
                'Acquisition optimizer:       ' + str(self.acquisition_optimizer.optimizer_name).strip('[]') + '\n')

            file.write('Acquisition type:            ' + str(self.acquisition_type).strip('[]') + '\n')
            if hasattr(self, 'acquisition_optimizer') and hasattr(self.acquisition_optimizer, 'optimizer_name'):
                file.write(
                    'Acquisition optimizer:       ' + str(self.acquisition_optimizer.optimizer_name).strip('[]') + '\n')
            else:
                file.write('Acquisition optimizer:       None\n')
            file.write('Evaluator type (batch size): ' + str(self.evaluator_type).strip('[]') + ' (' + str(
                self.batch_size) + ')' + '\n')
            file.write('Cores used:                  ' + str(self.num_cores) + '\n')

            file.write('\n')
            file.write(
                '---------------------------------' + ' Summary ' + '------------------------------------------\n')
            file.write('Initial X:                       ' + str(self.initial_X) + '\n')
            file.write('Initial Y:                       ' + str(self.initial_Y) + '\n')

            if self.maximize:
                file.write('Value at maximum:            ' + str(format(-min(self.Y)[0], '.20f')).strip('[]') + '\n')
                file.write('Best found maximum location: ' + str(self.X[np.argmin(self.Y), :]).strip('[]') + '\n')
            else:
                file.write('Value at minimum:            ' + str(format(min(self.Y)[0], '.20f')).strip('[]') + '\n')
                file.write('Best found minimum location: ' + str(self.X[np.argmin(self.Y), :]).strip('[]') + '\n')

            file.write(
                '----------------------------------------------------------------------------------------------\n')
            file.close()
Example #4
0
class CBO(object):
    """
    Runner of the multi-attribute Bayesian optimization loop. This class wraps the optimization loop around the different handlers.
    :param model: GPyOpt model class.
    :param space: GPyOpt space class.
    :param objective: GPyOpt objective class.
    :param acquisition: GPyOpt acquisition class.
    :param evaluator: GPyOpt evaluator class.
    :param X_init: 2d numpy array containing the initial inputs (one per row) of the model.
    :param Y_init: 2d numpy array containing the initial outputs (one per row) of the model.
    :param cost: GPyOpt cost class (default, none).
    :param normalize_Y: whether to normalize the outputs before performing any optimization (default, True).
    :param model_update_interval: interval of collected observations after which the model is updated (default, 1).
    :param de_duplication: GPyOpt DuplicateManager class. Avoids re-evaluating the objective at previous, pending or infeasible locations (default, False).
    """


    def __init__(self, model, space, objective, acquisition, evaluator, X_init, Y_init=None, cost=None, normalize_Y = False, model_update_interval = 1, expectation_utility=None):
        self.model = model
        self.space = space
        self.objective = objective
        self.acquisition = acquisition
        self.utility = acquisition.utility
        self.expectation_utility = expectation_utility
        self.evaluator = evaluator
        self.X = X_init
        self.Y = Y_init
        self.normalize_Y = normalize_Y
        self.cost = CostModel(cost)
        self.model_update_interval = model_update_interval
        self.historical_optimal_values = []
        self.historical_time = []
        self.n_attributes = self.model.output_dim
        self.n_hyps_samples = min(1, self.model.number_of_hyps_samples())
        self.n_parameter_samples = 10
        self.full_parameter_support = self.utility.parameter_dist.use_full_support
        self.evaluation_optimizer = GPyOpt.optimization.GeneralOptimizer(optimizer='lbfgs', space=space)
        #
        self.context = None
        self.current_argmax = np.atleast_2d(X_init[0,:])


    def _current_max_value(self):
        """
        Computes E_n[U(f(x_max))|f], where U is the utility function, f is the true underlying ojective function and x_max = argmax E_n[U(f(x))|U]. See
        function _marginal_max_value_so_far below.
        """
        val = 0
        if self.full_parameter_support:
            utility_param_support = self.utility.parameter_dist.support
            utility_param_dist = self.utility.parameter_dist.prob_dist
            for i in range(len(utility_param_support)):
                marginal_argmax = self._current_marginal_argmax(utility_param_support[i])
                marginal_max_val = np.reshape(self.objective.evaluate(marginal_argmax)[0],(self.n_attributes,))
                val += self.utility.eval_func(utility_param_support[i],marginal_max_val)*utility_param_dist[i]
        else:
            utility_param_samples = self.utility.parameter_dist.sample(self.n_parameter_samples)
            for i in range(len(utility_param_samples)):
                marginal_argmax = self._current_marginal_argmax(utility_param_samples[i])
                marginal_max_val = np.reshape(self.objective.evaluate(marginal_argmax)[0],(self.n_attributes,))
                val += self.utility.eval_func(utility_param_samples[i],marginal_max_val)
            val /= len(utility_param_samples)
        print('Current optimal value: {}'.format(val))
        return val


    def _current_max_value_and_var(self):
        val = 0
        var = 0
        support = self.utility.parameter_dist.support
        utility_dist = self.utility.parameter_dist.prob_dist
        for i in range(len(support)):
            marginal_argmax = self._current_marginal_argmax(support[i])
            marginal_max_val = np.reshape(self.objective.evaluate(marginal_argmax)[0],(self.n_attributes,))
            var_marginal_argmax = np.reshape(self.model.posterior_variance_noiseless(marginal_argmax),(self.n_attributes,))
            var += self.utility.eval_func(support[i],var_marginal_argmax)*utility_dist[i]
            val += self.utility.eval_func(support[i],marginal_max_val)*utility_dist[i]
        print('Current optimal value: {}'.format(val))
        return val, var


    def _current_max_value_parallel(self):
        """
        Computes E_n[U(f(x_max))|f], where U is the utility function, f is the true underlying ojective function and x_max = argmax E_n[U(f(x))|U]. See
        function _marginal_max_value_so_far below.
        """
        pool = Pool(4)
        utility_param_samples = self.utility.parameter_dist.sample(self.n_parameter_samples)
        val = sum(pool.map(self._current_marginal_max_value, utility_param_samples))/self.n_parameter_samples
        print('Current optimal value')
        print(val)
        return val


    def _current_marginal_max_value(self, parameter):
        marginal_argmax = self._current_marginal_argmax(parameter)
        marginal_max_val = np.reshape(self.objective.evaluate(marginal_argmax)[0], (self.objective.output_dim,))
        return self.utility.eval_func(parameter, marginal_max_val)



    def _current_marginal_argmax(self, parameter):
        """
        Computes argmax E_n[U(f(x))|U] (The abuse of notation can be misleading; note that the expectation is with
        respect to the posterior distribution on f after n evaluations)
        """
        if self.utility.linear:
            def val_func(X):
                X = np.atleast_2d(X)
                muX = self.model.posterior_mean(X)
                valX = np.reshape(np.matmul(parameter, muX), (X.shape[0], 1))
                return -valX

            def val_func_with_gradient(X):
                X = np.atleast_2d(X)
                muX = self.model.posterior_mean(X)
                dmu_dX = self.model.posterior_mean_gradient(X)
                valX = np.reshape(np.matmul(parameter, muX), (X.shape[0], 1))
                dval_dX = np.tensordot(parameter, dmu_dX, axes=1)
                return -valX, -dval_dX

        elif self.expectation_utility is not None:
            # Note: the value of these functions is not normalized, i.e. it is not divided by the number of Z samples and GP hyps.

            def val_func(X):
                X = np.atleast_2d(X)
                func_val = np.empty((X.shape[0], 1))
                for h in range(self.n_hyps_samples):
                    self.model.set_hyperparameters(h)
                    mean, var = self.model.predict_noiseless(X)
                    for i in range(X.shape[0]):
                        func_val[i,0] = self.expectation_utility.func(parameter, mean[:,i], var[:,i])
                return -func_val

            def val_func_with_gradient(X):
                X = np.atleast_2d(X)
                func_val = np.zeros((X.shape[0], 1))
                func_gradient = np.zeros(X.shape)
                for h in range(self.n_hyps_samples):
                    self.model.set_hyperparameters(h)
                    mean, var = self.model.predict_noiseless(X)
                    dmean_dX = self.model.posterior_mean_gradient(X)
                    dvar_dX = self.model.posterior_variance_gradient(X)
                    aux = np.concatenate((dmean_dX,dvar_dX))
                    for i in range(X.shape[0]):
                        func_val[i,0] = self.expectation_utility.func(parameter, mean[:,i], var[:,i])
                        func_gradient[i,:] = np.matmul(self.expectation_utility.gradient(parameter,mean[:,i],var[:,i]),aux[:,i])
                return -func_val, -func_gradient

        else:
            Z_samples = np.random.normal(size=(50, self.n_attributes))
            def val_func(X):
                X = np.atleast_2d(X)
                func_val = np.zeros((X.shape[0], 1))
                for h in range(self.n_hyps_samples):
                    self.model.set_hyperparameters(h)
                    mean, var = self.model.predict_noiseless(X)
                    std = np.sqrt(var)
                    for i in range(X.shape[0]):
                        for Z in Z_samples:
                            func_val[i,0] += self.utility.eval_func(parameter,mean[:,i] + np.multiply(std[:,i],Z))
                return -func_val

            def val_func_with_gradient(X):
                X = np.atleast_2d(X)
                func_val = np.zeros((X.shape[0], 1))
                func_gradient = np.zeros(X.shape)
                for h in range(self.n_hyps_samples):
                    self.model.set_hyperparameters(h)
                    mean, var = self.model.predict_noiseless(X)
                    std = np.sqrt(var)
                    dmean_dX = self.model.posterior_mean_gradient(X)
                    dstd_dX = self.model.posterior_variance_gradient(X)
                    for i in range(X.shape[0]):
                        for j in range(self.n_attributes):
                            dstd_dX[j,i,:] /= (2*std[j,i])
                        for Z in Z_samples:
                            aux1 = mean[:,i] + np.multiply(Z, std[:,i])
                            func_val[i,0] += self.utility.eval_func(parameter, aux1)
                            aux2 = dmean_dX[:,i,:] + np.multiply(dstd_dX[:,i,:].T, Z).T
                            func_gradient[i,:] += np.matmul(self.utility.eval_gradient(parameter, aux1), aux2)
                return -func_val, -func_gradient

        argmax = self.evaluation_optimizer.optimize(f=val_func, f_df=val_func_with_gradient, parallel=False)[0]
        self.current_argmax = argmax
        return argmax


    def run_optimization(self, max_iter=1, parallel=False, plot=False, results_file=None,  max_time=np.inf,  eps=1e-8, context=None, verbosity=False):
        """
        Runs Bayesian Optimization for a number 'max_iter' of iterations (after the initial exploration data)

        :param max_iter: exploration horizon, or number of acquisitions. If nothing is provided optimizes the current acquisition.
        :param max_time: maximum exploration horizon in seconds.
        :param eps: minimum distance between two consecutive x's to keep running the model.
        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param verbosity: flag to print the optimization results after each iteration (default, False).
        :param evaluations_file: filename of the file where the evaluated points and corresponding evaluations are saved (default, None).
        """

        if self.objective is None:
            raise InvalidConfigError("Cannot run the optimization loop without the objective function")

        # --- Save the options to print and save the results
        self.verbosity = verbosity
        self.results_file = results_file
        self.context = context


        # --- Setting up stop conditions
        self.eps = eps
        if (max_iter is None) and (max_time is None):
            self.max_iter = 0
            self.max_time = np.inf
        elif (max_iter is None) and (max_time is not None):
            self.max_iter = np.inf
            self.max_time = max_time
        elif (max_iter is not None) and (max_time is None):
            self.max_iter = max_iter
            self.max_time = np.inf
        else:
            self.max_iter = max_iter
            self.max_time = max_time

        # --- Initial function evaluation
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)
        # --- Initialize model
        self.model.updateModel(self.X,self.Y)

        # --- Initialize iterations and running time
        self.time_zero = time.clock()
        self.cum_time  = 0
        self.num_acquisitions = 0
        self.suggested_sample = self.X
        self.Y_new = self.Y


        # --- Initialize time cost of the evaluations
        while (self.max_time > self.cum_time) and (self.num_acquisitions < self.max_iter):

            #if not ((self.num_acquisitions < self.max_iter) and (self._distance_last_evaluations() > self.eps)):

            tmp = self.suggested_sample
            self.suggested_sample = self.compute_next_evaluations()
            if np.all(self.suggested_sample == tmp):
                self.suggested_sample = self._perturb(self.suggested_sample)
            try:
                self.acquisition.update_Z_samples()
            except:
                pass
            # --- Augment X
            self.X = np.vstack((self.X, self.suggested_sample))

            # --- Evaluate *f* in X, augment Y and update cost function (if needed)
            print('Acquisition {}'.format(self.num_acquisitions+1))
            self.evaluate_objective()
            # --- Update model
            if (self.num_acquisitions%self.model_update_interval)==0:
                self._update_model()
            self.model.get_model_parameters_names()
            self.model.get_model_parameters()
            if parallel and (not self.full_parameter_support):
                current_max_val = self._current_max_value_parallel()
            else:    
                current_max_val = self._current_max_value()
            self.historical_optimal_values.append(current_max_val)

            # --- Update current evaluation time and function evaluations
            self.cum_time = time.clock() - self.time_zero
            self.historical_time.append(self.cum_time)
            self.num_acquisitions += 1

            if verbosity:
                print("num acquisition: {}, time elapsed: {:.2f}s".format(
                    self.num_acquisitions, self.cum_time))
        if results_file is not None:
            self.save_results(results_file)
        if plot:
            self.plot_convergence(confidence_interval=True)
            #self._plot_pareto_front()

        # --- Print the desired result in files
        #if self.evaluations_file is not None:
            #self.save_evaluations(self.evaluations_file)

        #file = open('test_file.txt','w')
        #plt.plot(range(self.num_acquisitions),value_so_far)
        #plt.show()
        #np.savetxt('test_file.txt',value_so_far)


    def best_evaluated(self):
        if self.n_attributes > 1:
            raise InvalidConfigError("This option is not avialable with multiple objectives")
        else:
            scores = self.Y[0].flatten()
            x_best = self.X[np.argsort(-scores)[0],:]
            fx_best = -np.sort(-scores)
            return x_best, fx_best


    def evaluate_objective(self):
        """
        Evaluates the objective
        """
        print('Suggested point to evaluate: {}'.format(self.suggested_sample))
        self.Y_new, cost_new = self.objective.evaluate_w_noise(self.suggested_sample)
        self.cost.update_cost_model(self.suggested_sample, cost_new)
        for j in range(self.n_attributes):
            #print(self.Y_new[j])
            self.Y[j] = np.vstack((self.Y[j],self.Y_new[j]))


    def _distance_last_evaluations(self):
        """
        Computes the distance between the last two evaluations.
        """
        return np.sqrt(sum((self.X[self.X.shape[0]-1,:]-self.X[self.X.shape[0]-2,:])**2))

    def _perturb(self, x):
        perturbed_x = np.copy(x)
        while np.all(perturbed_x == x):
            perturbed_x = x + np.random.normal(size=x.shape, scale=1e-2)
            perturbed_x = self.space.round_optimum(perturbed_x)

        return perturbed_x


    def compute_next_evaluations(self, pending_zipped_X=None, ignored_zipped_X=None):
        """
        Computes the location of the new evaluation (optimizes the acquisition in the standard case).
        :param pending_zipped_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet).
        :param ignored_zipped_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again.
        :return:
        """
        # --- Initial function evaluation
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)
        # --- Initialize model
        self.model.updateModel(self.X, self.Y)

        ## --- Update the context if any
        self.acquisition.optimizer.context_manager = ContextManager(self.space, self.context)

        ### We zip the value in case there are categorical variables
        x_baseline = self.current_argmax + np.random.normal(loc=0.0, scale=0.1, size=self.current_argmax.shape)
        return self.space.zip_inputs(self.evaluator.compute_batch(duplicate_manager=None, x_baseline=self.current_argmax))
        #return self.space.zip_inputs(self.evaluator.compute_batch(duplicate_manager=None))
        #return initial_design('random', self.space, 1)

    def _update_model(self):
        """
        Updates the model (when more than one observation is available) and saves the parameters (if available).
        """

        ### --- input that goes into the model (is unziped in case there are categorical variables)
        X_inmodel = self.space.unzip_inputs(self.X)
        Y_inmodel = list(self.Y)
        #print(X_inmodel)
        #print(Y_inmodel)
        self.model.updateModel(X_inmodel, Y_inmodel)

        ### --- Save parameters of the model
        #self._save_model_parameter_values()

    def convergence_assesment(self, n_iter=10, attribute=0, context=None):
        if self.objective is None:
            raise InvalidConfigError("Cannot run the optimization loop without the objective function")
        #self.model_parameters_iterations = None
        self.context = context
        # --- Initial function evaluation
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)
            self._update_model()
        for i in range(n_iter):
            self.suggested_sample = self.compute_next_evaluations()
            filename = './experiments/1d' + str(i) + '.eps'
            model_to_plot = deepcopy(self.model)
            integrated_plot(self.acquisition.space.get_bounds(),
                                    self.X.shape[1],
                                    model_to_plot,
                                    self.X,
                                    self.Y,
                                    self.acquisition.acquisition_function,
                                    self.suggested_sample,
                                    attribute,
                                    filename)

            self.X = np.vstack((self.X,self.suggested_sample))
            self.evaluate_objective()
            self._update_model()
            #self.model.get_model_parameters_names()
            #self.model.get_model_parameters()
            #print('Acquisition value at previously evaluated points:')
            #print(self.acquisition.acquisition_function(self.X))
            #print('Posterior mean and variance')
            #print(self.model.predict(self.X))
            #print(self.Y)
            self.historical_optimal_values.append(self._current_max_value())



    def one_step_assesment(self, attribute=0, context=None):
        """
        """
        if self.objective is None:
            raise InvalidConfigError("Cannot run the optimization loop without the objective function")
        #self.model_parameters_iterations = None
        self.context = context
        # --- Initial function evaluation
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)
            self._update_model()

        self.suggested_sample = self.compute_next_evaluations()

        model_to_plot = deepcopy(self.model)

        integrated_plot(self.acquisition.space.get_bounds(),
                                self.X.shape[1],
                                model_to_plot,
                                self.X,
                                self.Y,
                                self.acquisition.acquisition_function,
                                self.suggested_sample,
                                attribute,
                                None)

        self.X = np.vstack((self.X,self.suggested_sample))
        self.evaluate_objective()
        self._update_model()
        self.historical_optimal_values.append(self._current_max_value())

    def integrated_plot(self, attribute=0, filename=None):
        """
        Plots the model and the acquisition function.
            if self.input_dim = 1: Plots data, mean and variance in one plot and the acquisition function in another plot
            if self.input_dim = 2: as before but it separates the mean and variance of the model in two different plots
        :param filename: name of the file where the plot is saved
        """
        from copy import deepcopy
        model_to_plot = deepcopy(self.model)

        integrated_plot(self.acquisition.space.get_bounds(),
                                self.X.shape[1],
                                model_to_plot,
                                self.X,
                                self.Y,
                                self.acquisition.acquisition_function,
                                self.suggest_next_locations(),
                                attribute,
                                filename)

    def plot_acquisition(self,filename=None):
        """
        Plots the acquisition function.
        """

        return plot_acquisition(self.acquisition.space.get_bounds(),
                                self.X.shape[1],
                                self.acquisition.acquisition_function,
                                filename)


    def plot_convergence(self, confidence_interval=False, filename=None):
        """
        Makes twp plots to evaluate the convergence of the model:
            plot 1: Iterations vs. distance between consecutive selected x's
            plot 2: Iterations vs. the mean of the current model in the selected sample.
        :param filename: name of the file where the plot is saved
        """
        return plot_convergence(self.historical_optimal_values, self.var_at_historical_optima, confidence_interval, filename)


    def get_evaluations(self):
        return self.X.copy(), self.Y.copy()


    def save_results(self, filename):

        results = np.zeros((len(self.historical_optimal_values),2))
        results[:, 0] = np.atleast_1d(self.historical_optimal_values)
        results[:, 1] = np.atleast_1d(self.historical_time)

        np.savetxt(filename,results)
Example #5
0
class BO(object):
    """
    Runner of the multi-attribute Bayesian optimization loop. This class wraps the optimization loop around the different handlers.
    :param model: GPyOpt model class.
    :param space: GPyOpt space class.
    :param objective: GPyOpt objective class.
    :param acquisition: GPyOpt acquisition class.
    :param evaluator: GPyOpt evaluator class.
    :param X_init: 2d numpy array containing the initial inputs (one per row) of the model.
    :param Y_init: 2d numpy array containing the initial outputs (one per row) of the model.
    :param cost: GPyOpt cost class (default, none).
    :param normalize_Y: whether to normalize the outputs before performing any optimization (default, True).
    :param model_update_interval: interval of collected observations after which the model is updated (default, 1).
    :param de_duplication: GPyOpt DuplicateManager class. Avoids re-evaluating the objective at previous, pending or infeasible locations (default, False).
    """
    def __init__(self,
                 model,
                 model_c,
                 space,
                 objective,
                 constraint,
                 acquisition,
                 evaluator,
                 X_init,
                 penalty_tag="fixed",
                 penalty_value=0.0,
                 expensive=False,
                 Y_init=None,
                 C_init=None,
                 cost=None,
                 normalize_Y=False,
                 model_update_interval=1,
                 true_preference=0.5):
        self.true_preference = true_preference
        self.model_c = model_c
        self.model = model
        self.space = space
        self.objective = objective
        self.constraint = constraint
        self.acquisition = acquisition
        self.utility = acquisition.utility
        self.evaluator = evaluator
        self.normalize_Y = normalize_Y
        self.model_update_interval = model_update_interval
        self.X = X_init
        self.Y = Y_init
        self.C = C_init
        self.cost = CostModel(cost)
        self.model_parameters_iterations = None
        self.expensive = expensive
        self.penalty_tag = penalty_tag
        self.penalty_value = penalty_value

    def suggest_next_locations(self,
                               context=None,
                               pending_X=None,
                               ignored_X=None):
        """
        Run a single optimization step and return the next locations to evaluate the objective.
        Number of suggested locations equals to batch_size.

        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param pending_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet) (default, None).
        :param ignored_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again (default, None).
        """
        self.model_parameters_iterations = None
        self.num_acquisitions = 0
        self.context = context
        self._update_model(self.normalization_type)

        suggested_locations = self._compute_next_evaluations(
            pending_zipped_X=pending_X, ignored_zipped_X=ignored_X)

        return suggested_locations

    # def _value_so_far(self):
    #     """
    #     Computes E_n[U(f(x_max))|f], where U is the utility function, f is the true underlying ojective function and x_max = argmax E_n[U(f(x))|U]. See
    #     function _marginal_max_value_so_far below.
    #     """
    #
    #     output = 0
    #     support = self.utility.parameter_dist.support
    #     utility_dist = self.utility.parameter_dist.prob_dist
    #
    #     a = np.reshape(self.objective.evaluate(self._marginal_max_value_so_far())[0],(self.objective.output_dim,))
    #
    #     output += self.utility.eval_func(support[i],a)*utility_dist[i]
    #     #print(output)
    #     return output
    #
    #
    # def _marginal_max_value_so_far(self):
    #     """
    #     Computes argmax E_n[U(f(x))|U] (The abuse of notation can be misleading; note that the expectation is with
    #     respect to the posterior distribution on f after n evaluations)
    #     """
    #
    #     def val_func(X):
    #         X = np.atleast_2d(X)
    #         muX = self.model.posterior_mean(X)
    #         return muX
    #
    #     def val_func_with_gradient(X):
    #         X = np.atleast_2d(X)
    #         muX = self.model.posterior_mean(X)
    #         dmu_dX = self.model.posterior_mean_gradient(X)
    #         valX = np.reshape( muX, (X.shape[0],1))
    #         dval_dX =  dmu_dX
    #         return -valX, -dval_dX
    #
    #
    #     argmax = self.acquisition.optimizer.optimize_inner_func(f=val_func, f_df=val_func_with_gradient)[0]
    #     return argmax
    #
    #
    def run_optimization(self,
                         max_iter=1,
                         max_time=np.inf,
                         eps=1e-8,
                         context=None,
                         verbosity=False,
                         evaluations_file=None):
        """
        Runs Bayesian Optimization for a number 'max_iter' of iterations (after the initial exploration data)

        :param max_iter: exploration horizon, or number of acquisitions. If nothing is provided optimizes the current acquisition.
        :param max_time: maximum exploration horizon in seconds.
        :param eps: minimum distance between two consecutive x's to keep running the model.
        :param context: fixes specified variables to a particular context (values) for the optimization run (default, None).
        :param verbosity: flag to print the optimization results after each iteration (default, False).
        :param evaluations_file: filename of the file where the evaluated points and corresponding evaluations are saved (default, None).
        """
        self.verbosity = verbosity
        if self.objective is None:
            raise InvalidConfigError(
                "Cannot run the optimization loop without the objective function"
            )

        # --- Save the options to print and save the results
        self.verbosity = verbosity
        self.evaluations_file = evaluations_file
        self.context = context

        # --- Setting up stop conditions
        self.eps = eps
        if (max_iter is None) and (max_time is None):
            self.max_iter = 0
            self.max_time = np.inf
        elif (max_iter is None) and (max_time is not None):
            self.max_iter = np.inf
            self.max_time = max_time
        elif (max_iter is not None) and (max_time is None):
            self.max_iter = max_iter
            self.max_time = np.inf
        else:
            self.max_iter = max_iter
            self.max_time = max_time

        # print("------------------------TRAINING HYPERS----------------------")
        # self._get_hyperparameters()

        # --- Initial function evaluation and model fitting
        if self.X is not None and self.Y is None:
            self.Y, cost_values = self.objective.evaluate(self.X)

            if self.constraint is not None:
                self.C, cost_values = self.constraint.evaluate(self.X)
            if self.cost.cost_type == 'evaluation_time':
                self.cost.update_cost_model(self.X, cost_values)

        #self.model.updateModel(self.X,self.Y)

        # --- Initialize iterations and running time
        self.time_zero = time.time()
        self.cum_time = 0
        self.num_acquisitions = 0
        self.suggested_sample = self.X
        self.Y_new = self.Y
        self.Opportunity_Cost = []
        value_so_far = []

        # --- Initialize time cost of the evaluations
        print("-----------------------MAIN LOOP STARTS----------------------")
        Opportunity_Cost = []
        self.true_best_stats = {
            "true_best": [],
            "mean_gp": [],
            "std gp": [],
            "pf": [],
            "mu_pf": [],
            "var_pf": [],
            "residual_noise": []
        }
        while (self.max_iter > self.num_acquisitions):

            self._update_model()

            if self.constraint is None:
                self.Opportunity_Cost_caller_unconstrained()
            else:
                self.Opportunity_Cost_caller_constrained()

            print("maKG optimizer")
            start = time.time()
            self.suggested_sample = self._compute_next_evaluations()
            finish = time.time()
            print("time optimisation point X", finish - start)

            if verbosity:
                ####plots
                design_plot = initial_design('random', self.space, 1000)
                ac_f = self.expected_improvement(design_plot)
                Y, _ = self.objective.evaluate(design_plot)
                C, _ = self.constraint.evaluate(design_plot)
                pf = self.probability_feasibility_multi_gp(
                    design_plot, self.model_c).reshape(-1, 1)
                mu_f = self.model.predict(design_plot)[0]

                bool_C = np.product(np.concatenate(C, axis=1) < 0, axis=1)
                func_val = Y * bool_C.reshape(-1, 1)

                print("self.suggested_sample", self.suggested_sample)
                fig, axs = plt.subplots(2, 2)
                axs[0, 0].set_title('True Function')
                axs[0, 0].scatter(design_plot[:, 0],
                                  design_plot[:, 1],
                                  c=np.array(func_val).reshape(-1))
                axs[0, 0].scatter(self.X[:, 0],
                                  self.X[:, 1],
                                  color="red",
                                  label="sampled")
                axs[0, 0].scatter(self.suggested_sample[:, 0],
                                  self.suggested_sample[:, 1],
                                  marker="x",
                                  color="red",
                                  label="suggested")

                axs[0, 1].set_title('approximation Acqu Function')
                axs[0, 1].scatter(design_plot[:, 0],
                                  design_plot[:, 1],
                                  c=np.array(ac_f).reshape(-1))

                axs[1, 0].set_title("convergence")
                axs[1, 0].plot(range(len(self.Opportunity_Cost)),
                               np.array(self.Opportunity_Cost).reshape(-1))
                axs[1, 0].set_yscale("log")

                axs[1, 1].set_title("mu")
                axs[1, 1].scatter(design_plot[:, 0],
                                  design_plot[:, 1],
                                  c=np.array(mu_f).reshape(-1) *
                                  np.array(pf).reshape(-1))

                plt.show()

            self.X = np.vstack((self.X, self.suggested_sample))
            # --- Evaluate *f* in X, augment Y and update cost function (if needed)
            self.evaluate_objective()

            print("X", self.X, "Y", self.Y, "C", self.C, "OC",
                  self.Opportunity_Cost)
            # --- Update current evaluation time and function evaluations
            self.cum_time = time.time() - self.time_zero
            self.num_acquisitions += 1

        return self.X, self.Y, self.C, self.Opportunity_Cost
        # --- Print the desired result in files
        #if self.evaluations_file is not None:
        #self.save_evaluations(self.evaluations_file)

        #file = open('test_file.txt','w')
        #np.savetxt('test_file.txt',value_so_far)

    def Opportunity_Cost_caller_constrained(self):

        # design_plot = initial_design('random', self.space, 1000)
        # ac_f = self.expected_improvement(design_plot)
        # fig, axs = plt.subplots(2, 2)
        # axs[0, 0].set_title('True Function')
        # axs[0, 0].scatter(design_plot[:, 0], design_plot[:, 1], c=np.array(ac_f).reshape(-1))

        # axs[0, 0].scatter(suggested_sample[:, 0], suggested_sample[:, 1], color="red")
        # plt.show()
        # print("self.suggested_sample",suggested_sample)
        # --- Evaluate *f* in X, augment Y and update cost function (if needed)

        samples = self.X
        print("samples", samples)
        Y = self.model.posterior_mean(samples)
        # print("Y",Y)
        pf = self.probability_feasibility_multi_gp(samples, model=self.model_c)
        func_val = np.array(Y).reshape(-1) * np.array(pf).reshape(-1)

        print("Y", Y, "pf", pf)
        suggested_final_sample = samples[np.argmax(func_val)]
        suggested_final_sample = np.array(suggested_final_sample).reshape(-1)
        suggested_final_sample = np.array(suggested_final_sample).reshape(
            1, -1)
        print("suggested_final_sample", suggested_final_sample, "max",
              np.max(func_val))
        Y_true, _ = self.objective.evaluate(suggested_final_sample,
                                            true_val=True)
        C_true, _ = self.constraint.evaluate(suggested_final_sample,
                                             true_val=True)

        bool_C_true = np.product(np.concatenate(C_true, axis=1) < 0, axis=1)
        func_val_true = Y_true * bool_C_true.reshape(-1, 1)
        print("suggested_final_sample", suggested_final_sample,
              "func_val_true", func_val_true)

        if self.expensive:
            self.Opportunity_Cost.append(
                np.array(np.abs(np.max(func_val_true))).reshape(-1))
        else:
            self.true_best_value()
            optimum = np.max(np.abs(self.true_best_stats["true_best"]))
            # print("optimum", optimum)
            self.Opportunity_Cost.append(
                optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))
            # print("OC_i", optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))

    def Opportunity_Cost_caller_unconstrained(self):

        # design_plot = initial_design('random', self.space, 1000)
        # ac_f = self.expected_improvement(design_plot)
        # fig, axs = plt.subplots(2, 2)
        # axs[0, 0].set_title('True Function')
        # axs[0, 0].scatter(design_plot[:, 0], design_plot[:, 1], c=np.array(ac_f).reshape(-1))

        # axs[0, 0].scatter(suggested_sample[:, 0], suggested_sample[:, 1], color="red")
        # plt.show()
        # print("self.suggested_sample",suggested_sample)
        # --- Evaluate *f* in X, augment Y and update cost function (if needed)

        samples = self.X
        print("samples", samples)
        Y = self.model.posterior_mean(samples)
        # print("Y",Y)
        #pf = self.probability_feasibility_multi_gp(samples, model=self.model_c)
        func_val = np.array(Y).reshape(-1)  #* np.array(pf).reshape(-1)

        #print("Y", Y, "pf", pf)
        suggested_final_sample = samples[np.argmax(func_val)]
        suggested_final_sample = np.array(suggested_final_sample).reshape(-1)
        suggested_final_sample = np.array(suggested_final_sample).reshape(
            1, -1)
        print("suggested_final_sample", suggested_final_sample, "max",
              np.max(func_val))
        Y_true, _ = self.objective.evaluate(suggested_final_sample,
                                            true_val=True)
        #C_true, _ = self.constraint.evaluate(suggested_final_sample, true_val=True)

        #bool_C_true = np.product(np.concatenate(C_true, axis=1) < 0, axis=1)
        func_val_true = Y_true  #* bool_C_true.reshape(-1, 1)
        print("suggested_final_sample", suggested_final_sample,
              "func_val_true", func_val_true)

        if self.expensive:
            self.Opportunity_Cost.append(
                np.array(np.abs(np.max(func_val_true))).reshape(-1))
        else:
            self.true_best_value()
            optimum = np.max(np.abs(self.true_best_stats["true_best"]))
            # print("optimum", optimum)
            self.Opportunity_Cost.append(
                optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))
            # print("OC_i", optimum - np.array(np.abs(np.max(func_val_true))).reshape(-1))

    def optimize_final_evaluation(self):

        out = self.acquisition.optimizer.optimize(f=self.current_best,
                                                  duplicate_manager=None)
        print("out", out)
        print("out", out[1])

        self.best_mu_all = -1 * np.array(out[1]).reshape(-1)

        self.acquisition.optimizer.context_manager = ContextManager(
            self.space, self.context)
        out = self.acquisition.optimizer.optimize(f=self.expected_improvement,
                                                  duplicate_manager=None)
        suggested_sample = self.space.zip_inputs(out[0])
        # suggested_sample = suggested_sample.astype("int")
        print("suggested_sample", suggested_sample)
        return suggested_sample

    def current_best(self, X):
        # X = X.astype("int")
        mu = self.model.posterior_mean(X)
        mu = mu.reshape(-1, 1)
        return -mu.reshape(-1)

    def expected_improvement(self, X, offset=0.0):
        '''
        Computes the EI at points X based on existing samples X_sample
        and Y_sample using a Gaussian process surrogate model.

        Args:
            X: Points at which EI shall be computed (m x d).
            X_sample: Sample locations (n x d).
            Y_sample: Sample values (n x 1).
            gpr: A GaussianProcessRegressor fitted to samples.
            xi: Exploitation-exploration trade-off parameter.

        Returns:
            Expected improvements at points X.
        '''
        # X = X.astype("int")
        mu = self.model.posterior_mean(X)
        sigma = self.model.posterior_variance(X, noise=False)

        sigma = np.sqrt(sigma).reshape(-1, 1)
        mu = mu.reshape(-1, 1)
        # Needed for noise-based model,
        # otherwise use np.max(Y_sample).
        # See also section 2.4 in [...]
        bool_C = np.product(np.concatenate(self.C, axis=1) < 0, axis=1)
        func_val = self.Y * bool_C.reshape(-1, 1)

        mu_sample_opt = np.max(func_val) - offset

        with np.errstate(divide='warn'):
            imp = mu - mu_sample_opt
            Z = imp / sigma
            ei = imp * norm.cdf(Z) + sigma * norm.pdf(Z)
            ei[sigma == 0.0] = 0.0
        pf = self.probability_feasibility_multi_gp(X, self.model_c).reshape(
            -1, 1)

        if self.penalty_tag == "fixed":
            self.penalty = self.penalty_value
        else:
            dif = np.array(mu).reshape(-1) - np.array(
                self.best_mu_all).reshape(-1)
            constraints = self.model_c.posterior_mean(
                X)  # MAYBE CHECK FOR SEVERAL CONSTRAINTS
            sum_constraints = np.sum(constraints, axis=0).reshape(-1)
            self.penalty = dif * sum_constraints

        return -(ei.reshape(-1) * pf.reshape(-1) +
                 (1 - pf.reshape(-1)) * self.penalty)

    def probability_feasibility_multi_gp(self,
                                         x,
                                         model,
                                         mean=None,
                                         cov=None,
                                         grad=False,
                                         l=0):
        # print("model",model.output)
        x = np.atleast_2d(x)
        if grad == False:
            Fz = []
            for m in range(model.output_dim):
                Fz.append(
                    self.probability_feasibility(x, model.output[m], grad, l))
            Fz = np.product(Fz, axis=0)
            return Fz
        else:
            Fz = []
            grad_Fz = []
            for m in range(model.output_dim):
                # print("model.output[m]",model.output[m])
                # print("mean[m]",mean[m])
                # print("cov[m]",cov[m])
                Fz_aux, grad_Fz_aux = self.probability_feasibility(
                    x, model.output[m])
                Fz.append(Fz_aux)
                grad_Fz.append(grad_Fz_aux)

            # print("np.array(Fz)", np.array(Fz), "grad_Fz", np.array(grad_Fz))
            grad_Fz = self.product_gradient_rule(func=np.array(Fz),
                                                 grad=np.array(grad_Fz))
            # print("output grad_Fz", grad_Fz)

            Fz = np.product(Fz, axis=0)
            return Fz, grad_Fz

    def probability_feasibility(self,
                                x,
                                model,
                                mean=None,
                                cov=None,
                                grad=False,
                                l=0):

        model = model.model
        # kern = model.kern
        # X = model.X
        mean = model.posterior_mean(x)
        var = model.posterior_variance(x, noise=False)
        std = np.sqrt(var).reshape(-1, 1)

        aux_var = np.reciprocal(var)
        mean = mean.reshape(-1, 1)

        norm_dist = norm(mean, std)
        fz = norm_dist.pdf(l)
        Fz = norm_dist.cdf(l)

        if grad == True:
            grad_mean, grad_var = model.predictive_gradients(x)
            grad_std = (1 / 2.0) * grad_var

            # cov = kern.K(X, X) + np.eye(X.shape[0]) * 1e-3
            # L = scipy.linalg.cholesky(cov, lower=True)
            # u = scipy.linalg.solve(L, np.eye(X.shape[0]))
            # Ainv = scipy.linalg.solve(L.T, u)

            dims = range(x.shape[1])
            grad_Fz = []

            for d in dims:
                # K1 = np.diag(np.dot(np.dot(kern.dK_dX(x, X, d), Ainv), kern.dK_dX2(X, x, d)))
                # K2 = np.diag(kern.dK2_dXdX2(x, x, d, d))
                # var_grad = K2 - K1
                # var_grad = var_grad.reshape(-1, 1)
                grd_mean_d = grad_mean[:, d].reshape(-1, 1)
                grd_std_d = grad_std[:, d].reshape(-1, 1)
                # print("grd_mean ",grd_mean_d, "var_grad ",grd_std_d )
                # print("std",std)
                # print("aux_var", aux_var)
                # print("fz",fz)
                grad_Fz.append(fz * aux_var *
                               (mean * grd_std_d - grd_mean_d * std))
            grad_Fz = np.stack(grad_Fz, axis=1)
            return Fz.reshape(-1, 1), grad_Fz[:, :, 0]
        else:
            return Fz.reshape(-1, 1)

    def evaluate_objective(self):
        """
        Evaluates the objective
        """
        print(1)
        print(self.suggested_sample)
        self.Y_new, cost_new = self.objective.evaluate(self.suggested_sample)
        self.C_new, C_cost_new = self.constraint.evaluate(
            self.suggested_sample)
        self.cost.update_cost_model(self.suggested_sample, cost_new)
        for j in range(self.objective.output_dim):
            print(self.Y_new[j])
            self.Y[j] = np.vstack((self.Y[j], self.Y_new[j]))

        for k in range(self.constraint.output_dim):
            print(self.C_new[k])
            self.C[k] = np.vstack((self.C[k], self.C_new[k]))

    def compute_current_best(self):
        current_acqX = self.acquisition.current_compute_acq()
        return current_acqX

    def _distance_last_evaluations(self):
        """
        Computes the distance between the last two evaluations.
        """
        return np.sqrt(
            sum((self.X[self.X.shape[0] - 1, :] -
                 self.X[self.X.shape[0] - 2, :])**2))

    def _compute_next_evaluations(self,
                                  pending_zipped_X=None,
                                  ignored_zipped_X=None,
                                  re_use=False):
        """
        Computes the location of the new evaluation (optimizes the acquisition in the standard case).
        :param pending_zipped_X: matrix of input configurations that are in a pending state (i.e., do not have an evaluation yet).
        :param ignored_zipped_X: matrix of input configurations that the user black-lists, i.e., those configurations will not be suggested again.
        :return:
        """
        ## --- Update the context if any

        self.acquisition.optimizer.context_manager = ContextManager(
            self.space, self.context)

        aux_var = self.evaluator.compute_batch(duplicate_manager=None,
                                               re_use=re_use)
        ### We zip the value in case there are categorical variables
        return self.space.zip_inputs(aux_var[0])
        #return initial_design('random', self.space, 1)

    def _get_hyperparameters(self):
        """
        Updates the model (when more than one observation is available) and saves the parameters (if available).
        """
        X_basis = GPyOpt.experiment_design.initial_design(
            'latin', self.space, 2000)

        X_train = X_basis
        # N=4
        # for i in range(N - 1):
        #     X_train = np.concatenate([X_train, X_basis])

        Y_train, cost_train = self.objective.evaluate(X_train)
        C_train, C_cost_train = self.constraint.evaluate(X_train)

        ### --- input that goes into the model (is unziped in case there are categorical variables)
        X_inmodel = self.space.unzip_inputs(X_train)

        Y_inmodel = list(Y_train)
        C_inmodel = list(C_train)

        print("X", X_inmodel, len(X_inmodel), "Y", Y_inmodel, len(Y_inmodel))
        self.model.trainModel(X_inmodel, Y_inmodel)
        self.model_c.trainModel(X_inmodel, C_inmodel)

    def _update_model(self):
        """
        Updates the model (when more than one observation is available) and saves the parameters (if available).
        """
        if (self.num_acquisitions % self.model_update_interval) == 0:

            ### --- input that goes into the model (is unziped in case there are categorical variables)
            X_inmodel = self.space.unzip_inputs(self.X)
            Y_inmodel = list(self.Y)
            if self.constraint is not None:
                C_inmodel = list(self.C)
                self.model_c.updateModel(X_inmodel, C_inmodel)
            print("X", X_inmodel, len(X_inmodel), "Y", Y_inmodel,
                  len(Y_inmodel))
            self.model.updateModel(X_inmodel, Y_inmodel)

        ### --- Save parameters of the model
        #self._save_model_parameter_values()

    def get_evaluations(self):
        return self.X.copy(), self.Y.copy()

    def true_best_value(self):
        from scipy.optimize import minimize

        X = initial_design('random', self.space, 1000)

        fval = self.func_val(X)

        anchor_point = np.array(X[np.argmin(fval)]).reshape(-1)
        anchor_point = anchor_point.reshape(1, -1)
        print("anchor_point", anchor_point)
        best_design = minimize(self.func_val,
                               anchor_point,
                               method='Nelder-Mead',
                               tol=1e-8).x

        self.true_best_stats["true_best"].append(self.func_val(best_design))
        self.true_best_stats["mean_gp"].append(
            self.model.posterior_mean(best_design))
        self.true_best_stats["std gp"].append(
            self.model.posterior_variance(best_design, noise=False))
        self.true_best_stats["pf"].append(
            self.probability_feasibility_multi_gp(best_design,
                                                  self.model_c).reshape(-1, 1))
        mean = self.model_c.posterior_mean(best_design)
        var = self.model_c.posterior_variance(best_design, noise=False)
        residual_noise = self.model_c.posterior_variance(self.X[1],
                                                         noise=False)
        self.true_best_stats["mu_pf"].append(mean)
        self.true_best_stats["var_pf"].append(var)
        self.true_best_stats["residual_noise"].append(residual_noise)

        if False:
            fig, axs = plt.subplots(3, 2)
            N = len(np.array(self.true_best_stats["std gp"]).reshape(-1))
            GAP = np.array(
                np.abs(
                    np.abs(self.true_best_stats["true_best"]).reshape(-1) -
                    np.abs(self.true_best_stats["mean_gp"]).reshape(-1))
            ).reshape(-1)
            print("GAP len", len(GAP))
            print("N", N)
            axs[0, 0].set_title('GAP')
            axs[0, 0].plot(range(N), GAP)
            axs[0, 0].set_yscale("log")

            axs[0, 1].set_title('VAR')
            axs[0,
                1].plot(range(N),
                        np.array(self.true_best_stats["std gp"]).reshape(-1))
            axs[0, 1].set_yscale("log")

            axs[1, 0].set_title("PF")
            axs[1, 0].plot(range(N),
                           np.array(self.true_best_stats["pf"]).reshape(-1))

            axs[1, 1].set_title("mu_PF")
            axs[1, 1].plot(
                range(N),
                np.abs(np.array(self.true_best_stats["mu_pf"]).reshape(-1)))
            axs[1, 1].set_yscale("log")

            axs[2, 1].set_title("std_PF")
            axs[2, 1].plot(
                range(N),
                np.sqrt(np.array(self.true_best_stats["var_pf"]).reshape(-1)))
            axs[2, 1].set_yscale("log")

            axs[2, 0].set_title("Irreducible noise")
            axs[2, 0].plot(
                range(N),
                np.sqrt(
                    np.array(
                        self.true_best_stats["residual_noise"]).reshape(-1)))
            axs[2, 0].set_yscale("log")

            plt.show()

    def func_val(self, x):
        if len(x.shape) == 1:
            x = x.reshape(1, -1)
        Y, _ = self.objective.evaluate(x, true_val=True)
        C, _ = self.constraint.evaluate(x, true_val=True)
        Y = np.array(Y).reshape(-1)
        out = Y.reshape(-1) * np.product(np.concatenate(C, axis=1) < 0,
                                         axis=1).reshape(-1)
        out = np.array(out).reshape(-1)
        return -out