Example #1
0
 def _update_pareto_Y(self) -> bool:
     r"""Update the non-dominated front."""
     # is_non_dominated assumes maximization
     non_dominated_mask = is_non_dominated(-self.Y)
     pf = self.Y[non_dominated_mask]
     # sort by first objective
     new_pareto_Y = pf[torch.argsort(pf[:, 0])]
     if not hasattr(self, "_pareto_Y") or not torch.equal(
         new_pareto_Y, self._pareto_Y
     ):
         self.register_buffer("_pareto_Y", new_pareto_Y)
         return True
     return False
    def _update_pareto_Y(self) -> bool:
        r"""Update the non-dominated front."""
        # is_non_dominated assumes maximization
        pareto_mask = is_non_dominated(-self.Y)

        if len(self.batch_shape) > 0:
            # Note: in the batch case, the Pareto frontier is padded by repeating
            # a Pareto point. This ensures that the padded box-decomposition has
            # the same number of points, which enables fast batch operations.
            max_n_pareto = pareto_mask.sum(dim=-1).max().item()
            pareto_Y = torch.empty(
                *self.batch_shape,
                max_n_pareto,
                self.Y.shape[-1],
                dtype=self.Y.dtype,
                device=self.Y.device,
            )
            for i in range(self.Y.shape[0]):
                pareto_i = self.Y[i, pareto_mask[i]]
                n_pareto = pareto_i.shape[0]
                pareto_Y[i, :n_pareto] = pareto_i
                # pad pareto_Y, so that all batches have the same size Pareto set
                pareto_Y[i, n_pareto:] = pareto_i[-1]
            # sort by first objective
            new_pareto_Y = pareto_Y.gather(
                index=torch.argsort(pareto_Y[..., :1], dim=-2).expand(pareto_Y.shape),
                dim=-2,
            )
        else:
            pareto_Y = self.Y[pareto_mask]
            # sort by first objective
            new_pareto_Y = pareto_Y[torch.argsort(pareto_Y[:, 0])]

        if not hasattr(self, "_pareto_Y") or not torch.equal(
            new_pareto_Y, self._pareto_Y
        ):
            self.register_buffer("_pareto_Y", new_pareto_Y)
            return True
        return False
Example #3
0
    def update(self, Y: Tensor) -> None:
        r"""Update non-dominated front and decomposition.

        Args:
            Y: A `(batch_shape) x n x m`-dim tensor of new, incremental outcomes.
        """
        if self._update_neg_Y(Y=Y):
            self.reset()
        else:
            if self.num_outcomes == 2 or self._neg_pareto_Y.shape[-2] == 0:
                # If there are two objective, recompute the box decomposition
                # because the partitions can be computed analytically.
                # If the current pareto set has no points, recompute the box
                # decomposition.
                self.reset()
            else:
                # only include points that are better than the reference point
                better_than_ref = (Y > self.ref_point).all(dim=-1)
                Y = Y[better_than_ref]
                Y_all = torch.cat([self._neg_pareto_Y, -Y], dim=-2)
                pareto_mask = is_non_dominated(-Y_all)
                # determine the number of points in Y that are Pareto optimal
                num_new_pareto = pareto_mask[-Y.shape[-2] :].sum()
                self._neg_pareto_Y = Y_all[pareto_mask]
                if num_new_pareto > 0:
                    # update local upper bounds for the minimization problem
                    self._U, self._Z = update_local_upper_bounds_incremental(
                        # this assumes minimization
                        new_pareto_Y=self._neg_pareto_Y[-num_new_pareto:],
                        U=self._U,
                        Z=self._Z,
                    )
                    # use the negative local upper bounds as the new pareto
                    # frontier for the minimization problem and perform
                    # box decomposition on dominated space.
                    self._get_partitioning()
def evaluate(mth, run_i, seed):
    print(mth, run_i, seed, '===== start =====', flush=True)

    def objective_function(x: torch.Tensor):
        # Caution: unnormalize and maximize
        x = unnormalize(x, bounds=problem_bounds)
        x = x.cpu().numpy().astype(np.float64)  # caution
        res = problem.evaluate(x)
        res['objs'] = [-y for y in res['objs']]
        return res  # Caution: negative values imply feasibility in botorch

    hv_diffs = []
    time_list = []
    global_start_time = time.time()

    # random seed
    np.random.seed(seed)
    torch.manual_seed(seed)

    # call helper functions to generate initial training data and initialize model
    train_x, train_obj, train_con = generate_initial_data(
        initial_runs, objective_function, time_list, global_start_time)
    # fix bug: find feasible
    real_initial_runs = initial_runs
    while real_initial_runs < max_runs:
        # compute feasible observations
        is_feas = (train_con <= 0).all(dim=-1)
        # compute points that are better than the known reference point
        better_than_ref = (train_obj > problem.ref_point).all(dim=-1)
        if (is_feas & better_than_ref).any():
            break
        train_x, train_obj, train_con = expand_initial_data(
            train_x, train_obj, train_con, objective_function, time_list,
            global_start_time)
        real_initial_runs += 1
        print('=== Expand initial data to find feasible. Iter =',
              real_initial_runs)
    mll, model = initialize_model(train_x, train_obj, train_con)

    # for plot
    X_init = train_x.cpu().numpy().astype(np.float64)
    Y_init = -1 * train_obj.cpu().numpy().astype(np.float64)
    # calculate hypervolume of init data
    for i in range(real_initial_runs):
        train_obj_i = train_obj[:i + 1]
        train_con_i = train_con[:i + 1]
        # compute pareto front
        is_feas_i = (train_con_i <= 0).all(dim=-1)
        feas_train_obj_i = train_obj_i[is_feas_i]
        pareto_mask = is_non_dominated(feas_train_obj_i)
        pareto_y = feas_train_obj_i[pareto_mask]
        # compute hypervolume
        volume = hv.compute(pareto_y)
        hv_diff = problem.max_hv - volume
        hv_diffs.append(hv_diff)

    # run (max_runs - real_initial_runs) rounds of BayesOpt after the initial random batch
    for iteration in range(real_initial_runs + 1, max_runs + 1):
        t0 = time.time()
        try:
            # fit the models
            fit_gpytorch_model(mll)

            # define the qEHVI acquisition modules using a QMC sampler
            sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES)
            # compute feasible observations
            is_feas = (train_con <= 0).all(dim=-1)
            # compute points that are better than the known reference point
            better_than_ref = (train_obj > problem.ref_point).all(dim=-1)
            # partition non-dominated space into disjoint rectangles
            partitioning = NondominatedPartitioning(
                num_outcomes=problem.num_objs,
                # use observations that are better than the specified reference point and feasible
                Y=train_obj[better_than_ref & is_feas],
            )
            qEHVI = qExpectedHypervolumeImprovement(
                model=model,
                ref_point=problem.ref_point.tolist(
                ),  # use known reference point
                partitioning=partitioning,
                sampler=sampler,
                # define an objective that specifies which outcomes are the objectives
                objective=IdentityMCMultiOutputObjective(
                    outcomes=list(range(problem.num_objs))),
                # specify that the constraint is on the last outcome
                constraints=constraint_callable_list(
                    problem.num_constraints, num_objs=problem.num_objs),
            )
            # optimize and get new observation
            new_x, new_obj, new_con = optimize_acqf_and_get_observation(
                qEHVI, objective_function, time_list, global_start_time)
        except Exception as e:  # handle numeric problem
            step = 2
            print(
                '===== Exception in optimization loop, restart with 1/%d of training data: %s'
                % (step, str(e)))
            if refit == 1:
                mll, model = initialize_model(train_x[::step],
                                              train_obj[::step],
                                              train_con[::step])
            else:
                mll, model = initialize_model(
                    train_x[::step],
                    train_obj[::step],
                    train_con[::step],
                    model.state_dict(),
                )
            # fit the models
            fit_gpytorch_model(mll)

            # define the qEHVI acquisition modules using a QMC sampler
            sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES)
            # compute feasible observations
            is_feas = (train_con[::step] <= 0).all(dim=-1)
            # compute points that are better than the known reference point
            better_than_ref = (train_obj[::step] > problem.ref_point).all(
                dim=-1)
            # partition non-dominated space into disjoint rectangles
            partitioning = NondominatedPartitioning(
                num_outcomes=problem.num_objs,
                # use observations that are better than the specified reference point and feasible
                Y=train_obj[::step][better_than_ref & is_feas],
            )
            qEHVI = qExpectedHypervolumeImprovement(
                model=model,
                ref_point=problem.ref_point.tolist(
                ),  # use known reference point
                partitioning=partitioning,
                sampler=sampler,
                # define an objective that specifies which outcomes are the objectives
                objective=IdentityMCMultiOutputObjective(
                    outcomes=list(range(problem.num_objs))),
                # specify that the constraint is on the last outcome
                constraints=constraint_callable_list(
                    problem.num_constraints, num_objs=problem.num_objs),
            )
            # optimize and get new observation
            new_x, new_obj, new_con = optimize_acqf_and_get_observation(
                qEHVI, objective_function, time_list, global_start_time)
            assert len(time_list) == iteration

        # update training points
        train_x = torch.cat([train_x, new_x])
        train_obj = torch.cat([train_obj, new_obj])
        train_con = torch.cat([train_con, new_con])

        # update progress
        # compute pareto front
        is_feas = (train_con <= 0).all(dim=-1)
        feas_train_obj = train_obj[is_feas]
        pareto_mask = is_non_dominated(feas_train_obj)
        pareto_y = feas_train_obj[pareto_mask]
        # compute hypervolume
        volume = hv.compute(pareto_y)
        hv_diff = problem.max_hv - volume
        hv_diffs.append(hv_diff)

        # reinitialize the models so they are ready for fitting on next iteration
        # use the current state dict to speed up fitting
        # Note: they find improved performance from not warm starting the model hyperparameters
        # using the hyperparameters from the previous iteration
        if refit == 1:
            mll, model = initialize_model(train_x, train_obj, train_con)
        else:
            mll, model = initialize_model(
                train_x,
                train_obj,
                train_con,
                model.state_dict(),
            )

        t1 = time.time()
        print(
            "Iter %d: x=%s, perf=%s, con=%s, hv_diff=%f, time=%.2f, global_time=%.2f"
            % (iteration, unnormalize(new_x, bounds=problem_bounds), -new_obj,
               new_con, hv_diff, t1 - t0, time_list[-1]),
            flush=True)

    # compute pareto front
    is_feas = (train_con <= 0).all(dim=-1)
    feas_train_obj = train_obj[is_feas]
    pareto_mask = is_non_dominated(feas_train_obj)
    pareto_y = feas_train_obj[pareto_mask]
    pf = -1 * pareto_y.cpu().numpy().astype(np.float64)
    # Save result
    X = unnormalize(train_x, bounds=problem_bounds).cpu().numpy().astype(
        np.float64)  # caution
    train_obj[~is_feas] = -INFEASIBLE_OBJ_VALUE  # set infeasible
    Y = -1 * train_obj.cpu().numpy().astype(np.float64)

    # plot for debugging
    if plot_mode == 1:
        plot_pf(problem, problem_str, mth, pf, Y_init)

    return hv_diffs, pf, X, Y, time_list
Example #5
0
    def test_is_non_dominated(self) -> None:
        tkwargs = {"device": self.device}
        Y = torch.tensor(
            [
                [1.0, 5.0],
                [10.0, 3.0],
                [4.0, 5.0],
                [4.0, 5.0],
                [5.0, 5.0],
                [8.5, 3.5],
                [8.5, 3.5],
                [8.5, 3.0],
                [9.0, 1.0],
            ]
        )
        expected_nondom_Y = torch.tensor([[10.0, 3.0], [5.0, 5.0], [8.5, 3.5]])
        Yb = Y.clone()
        Yb[1] = 0
        expected_nondom_Yb = torch.tensor([[5.0, 5.0], [8.5, 3.5], [9.0, 1.0]])
        Y3 = torch.tensor(
            [
                [4.0, 2.0, 3.0],
                [2.0, 4.0, 1.0],
                [3.0, 5.0, 1.0],
                [2.0, 4.0, 2.0],
                [2.0, 4.0, 2.0],
                [1.0, 3.0, 4.0],
                [1.0, 2.0, 4.0],
                [1.0, 2.0, 6.0],
            ]
        )
        Y3b = Y3.clone()
        Y3b[0] = 0
        expected_nondom_Y3 = torch.tensor(
            [
                [4.0, 2.0, 3.0],
                [3.0, 5.0, 1.0],
                [2.0, 4.0, 2.0],
                [1.0, 3.0, 4.0],
                [1.0, 2.0, 6.0],
            ]
        )
        expected_nondom_Y3b = expected_nondom_Y3[1:]
        for dtype in (torch.float, torch.double):
            tkwargs["dtype"] = dtype
            Y = Y.to(**tkwargs)
            expected_nondom_Y = expected_nondom_Y.to(**tkwargs)
            Yb = Yb.to(**tkwargs)
            expected_nondom_Yb = expected_nondom_Yb.to(**tkwargs)
            Y3 = Y3.to(**tkwargs)
            expected_nondom_Y3 = expected_nondom_Y3.to(**tkwargs)
            Y3b = Y3b.to(**tkwargs)
            expected_nondom_Y3b = expected_nondom_Y3b.to(**tkwargs)

            # test 2d
            nondom_Y = Y[is_non_dominated(Y)]
            self.assertTrue(torch.equal(expected_nondom_Y, nondom_Y))
            # test deduplicate=False
            expected_nondom_Y_no_dedup = torch.cat(
                [expected_nondom_Y, expected_nondom_Y[-1:]], dim=0
            )
            nondom_Y = Y[is_non_dominated(Y, deduplicate=False)]
            self.assertTrue(torch.equal(expected_nondom_Y_no_dedup, nondom_Y))

            # test batch
            batch_Y = torch.stack([Y, Yb], dim=0)
            nondom_mask = is_non_dominated(batch_Y)
            self.assertTrue(torch.equal(batch_Y[0][nondom_mask[0]], expected_nondom_Y))
            self.assertTrue(torch.equal(batch_Y[1][nondom_mask[1]], expected_nondom_Yb))
            # test deduplicate=False
            expected_nondom_Yb_no_dedup = torch.cat(
                [expected_nondom_Yb[:-1], expected_nondom_Yb[-2:]], dim=0
            )
            nondom_mask = is_non_dominated(batch_Y, deduplicate=False)
            self.assertTrue(
                torch.equal(batch_Y[0][nondom_mask[0]], expected_nondom_Y_no_dedup)
            )
            self.assertTrue(
                torch.equal(batch_Y[1][nondom_mask[1]], expected_nondom_Yb_no_dedup)
            )

            # test 3d
            nondom_Y3 = Y3[is_non_dominated(Y3)]
            self.assertTrue(torch.equal(expected_nondom_Y3, nondom_Y3))
            # test deduplicate=False
            expected_nondom_Y3_no_dedup = torch.cat(
                [expected_nondom_Y3[:3], expected_nondom_Y3[2:]], dim=0
            )
            nondom_Y3 = Y3[is_non_dominated(Y3, deduplicate=False)]
            self.assertTrue(torch.equal(expected_nondom_Y3_no_dedup, nondom_Y3))
            # test batch
            batch_Y3 = torch.stack([Y3, Y3b], dim=0)
            nondom_mask3 = is_non_dominated(batch_Y3)
            self.assertTrue(
                torch.equal(batch_Y3[0][nondom_mask3[0]], expected_nondom_Y3)
            )
            self.assertTrue(
                torch.equal(batch_Y3[1][nondom_mask3[1]], expected_nondom_Y3b)
            )
            # test deduplicate=False
            nondom_mask3 = is_non_dominated(batch_Y3, deduplicate=False)
            self.assertTrue(
                torch.equal(batch_Y3[0][nondom_mask3[0]], expected_nondom_Y3_no_dedup)
            )
            expected_nondom_Y3b_no_dedup = torch.cat(
                [expected_nondom_Y3b[:2], expected_nondom_Y3b[1:]], dim=0
            )
            self.assertTrue(
                torch.equal(batch_Y3[1][nondom_mask3[1]], expected_nondom_Y3b_no_dedup)
            )

            # test empty pareto
            mask = is_non_dominated(Y3[:0])
            expected_mask = torch.zeros(0, dtype=torch.bool, device=Y3.device)
            self.assertTrue(torch.equal(expected_mask, mask))
            mask = is_non_dominated(batch_Y3[:, :0])
            expected_mask = torch.zeros(
                *batch_Y3.shape[:-2], 0, dtype=torch.bool, device=Y3.device
            )
            self.assertTrue(torch.equal(expected_mask, mask))
 def _get_pareto_points(self):
     # taken from https://botorch.org/tutorials/constrained_multi_objective_bo
     y_tensor = torch.stack(self._torch_model.train_targets, dim=1)
     pareto_mask = is_non_dominated(y_tensor)
     pareto_y = y_tensor[pareto_mask]
     return pareto_y
def evaluate(mth, run_i, seed):
    print(mth, run_i, seed, '===== start =====', flush=True)

    def objective_function(x: torch.Tensor):
        # Caution: unnormalize and maximize
        x = unnormalize(x, bounds=problem_bounds)
        x = x.cpu().numpy().astype(np.float64)  # caution
        res = problem.evaluate(x)
        objs = [-y for y in res['objs']]
        return objs

    hv_diffs = []
    time_list = []
    global_start_time = time.time()

    # random seed
    np.random.seed(seed)
    torch.manual_seed(seed)

    # call helper functions to generate initial training data and initialize model
    train_x, train_obj = generate_initial_data(initial_runs,
                                               objective_function, time_list,
                                               global_start_time)
    mll, model = initialize_model(train_x, train_obj)

    # for plot
    X_init = train_x.cpu().numpy().astype(np.float64)
    Y_init = -1 * train_obj.cpu().numpy().astype(np.float64)
    # calculate hypervolume of init data
    for i in range(initial_runs):
        train_obj_i = train_obj[:i + 1]
        # compute pareto front
        pareto_mask = is_non_dominated(train_obj_i)
        pareto_y = train_obj_i[pareto_mask]
        # compute hypervolume
        volume = hv.compute(pareto_y)
        hv_diff = problem.max_hv - volume
        hv_diffs.append(hv_diff)

    # run (max_runs - initial_runs) rounds of BayesOpt after the initial random batch
    for iteration in range(initial_runs + 1, max_runs + 1):
        t0 = time.time()
        try:
            # fit the models
            fit_gpytorch_model(mll)

            # define the qEHVI acquisition modules using a QMC sampler
            sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES)
            # partition non-dominated space into disjoint rectangles
            partitioning = NondominatedPartitioning(
                num_outcomes=problem.num_objs, Y=train_obj)
            qEHVI = qExpectedHypervolumeImprovement(
                model=model,
                ref_point=problem.ref_point.tolist(
                ),  # use known reference point
                partitioning=partitioning,
                sampler=sampler,
            )
            # optimize and get new observation
            new_x, new_obj = optimize_acqf_and_get_observation(
                qEHVI, objective_function, time_list, global_start_time)
        except Exception as e:
            step = 2
            print(
                '===== Exception in optimization loop, restart with 1/%d of training data: %s'
                % (step, str(e)))
            if refit == 1:
                mll, model = initialize_model(train_x[::step],
                                              train_obj[::step])
            else:
                mll, model = initialize_model(
                    train_x[::step],
                    train_obj[::step],
                    model.state_dict(),
                )
            # fit the models
            fit_gpytorch_model(mll)

            # define the qEHVI acquisition modules using a QMC sampler
            sampler = SobolQMCNormalSampler(num_samples=MC_SAMPLES)
            # partition non-dominated space into disjoint rectangles
            partitioning = NondominatedPartitioning(
                num_outcomes=problem.num_objs, Y=train_obj[::step])
            qEHVI = qExpectedHypervolumeImprovement(
                model=model,
                ref_point=problem.ref_point.tolist(
                ),  # use known reference point
                partitioning=partitioning,
                sampler=sampler,
            )
            # optimize and get new observation
            new_x, new_obj = optimize_acqf_and_get_observation(
                qEHVI, objective_function, time_list, global_start_time)
            assert len(time_list) == iteration

        # update training points
        train_x = torch.cat([train_x, new_x])
        train_obj = torch.cat([train_obj, new_obj])

        # update progress
        # compute pareto front
        pareto_mask = is_non_dominated(train_obj)
        pareto_y = train_obj[pareto_mask]
        # compute hypervolume
        volume = hv.compute(pareto_y)
        hv_diff = problem.max_hv - volume
        hv_diffs.append(hv_diff)

        # reinitialize the models so they are ready for fitting on next iteration
        # use the current state dict to speed up fitting
        # Note: they find improved performance from not warm starting the model hyperparameters
        # using the hyperparameters from the previous iteration
        if refit == 1:
            mll, model = initialize_model(train_x, train_obj)
        else:
            mll, model = initialize_model(
                train_x,
                train_obj,
                model.state_dict(),
            )

        t1 = time.time()
        print(
            "Iter %d: x=%s, perf=%s, hv_diff=%f, time=%.2f, global_time=%.2f" %
            (iteration, unnormalize(new_x, bounds=problem_bounds), -new_obj,
             hv_diff, t1 - t0, time_list[-1]),
            flush=True)

    # Save result
    X = unnormalize(train_x, bounds=problem_bounds).cpu().numpy().astype(
        np.float64)  # caution
    Y = -1 * train_obj.cpu().numpy().astype(np.float64)
    # compute pareto front
    pareto_mask = is_non_dominated(train_obj)
    pareto_y = train_obj[pareto_mask]
    pf = -1 * pareto_y.cpu().numpy().astype(np.float64)

    # plot for debugging
    if plot_mode == 1:
        plot_pf(problem, problem_str, mth, pf, Y_init)

    return hv_diffs, pf, X, Y, time_list
Example #8
0
def prune_inferior_points_multi_objective(
    model: Model,
    X: Tensor,
    ref_point: Tensor,
    objective: Optional[MCMultiOutputObjective] = None,
    constraints: Optional[List[Callable[[Tensor], Tensor]]] = None,
    num_samples: int = 2048,
    max_frac: float = 1.0,
    marginalize_dim: Optional[int] = None,
) -> Tensor:
    r"""Prune points from an input tensor that are unlikely to be pareto optimal.

    Given a model, an objective, and an input tensor `X`, this function returns
    the subset of points in `X` that have some probability of being pareto
    optimal, better than the reference point, and feasible. This function uses
    sampling to estimate the probabilities, the higher the number of points `n`
    in `X` the higher the number of samples `num_samples` should be to obtain
    accurate estimates.

    Args:
        model: A fitted model. Batched models are currently not supported.
        X: An input tensor of shape `n x d`. Batched inputs are currently not
            supported.
        ref_point: The reference point.
        objective: The objective under which to evaluate the posterior.
        constraints: A list of callables, each mapping a Tensor of dimension
            `sample_shape x batch-shape x q x m` to a Tensor of dimension
            `sample_shape x batch-shape x q`, where negative values imply
            feasibility.
        num_samples: The number of samples used to compute empirical
            probabilities of being the best point.
        max_frac: The maximum fraction of points to retain. Must satisfy
            `0 < max_frac <= 1`. Ensures that the number of elements in the
            returned tensor does not exceed `ceil(max_frac * n)`.
        marginalize_dim: A batch dimension that should be marginalized.
            For example, this is useful when using a batched fully Bayesian
            model.

    Returns:
        A `n' x d` with subset of points in `X`, where

            n' = min(N_nz, ceil(max_frac * n))

        with `N_nz` the number of points in `X` that have non-zero (empirical,
        under `num_samples` samples) probability of being pareto optimal.
    """
    if X.ndim > 2:
        # TODO: support batched inputs (req. dealing with ragged tensors)
        raise UnsupportedError(
            "Batched inputs `X` are currently unsupported by "
            "prune_inferior_points_multi_objective")
    max_points = math.ceil(max_frac * X.size(-2))
    if max_points < 1 or max_points > X.size(-2):
        raise ValueError(f"max_frac must take values in (0, 1], is {max_frac}")
    with torch.no_grad():
        posterior = model.posterior(X=X)
    if posterior.event_shape.numel() > SobolEngine.MAXDIM:
        if settings.debug.on():
            warnings.warn(
                f"Sample dimension q*m={posterior.event_shape.numel()} exceeding Sobol "
                f"max dimension ({SobolEngine.MAXDIM}). Using iid samples instead.",
                SamplingWarning,
            )
        sampler = IIDNormalSampler(num_samples=num_samples)
    else:
        sampler = SobolQMCNormalSampler(num_samples=num_samples)
    samples = sampler(posterior)
    if objective is None:
        objective = IdentityMCMultiOutputObjective()
    obj_vals = objective(samples, X=X)
    if obj_vals.ndim > 3:
        if obj_vals.ndim == 4 and marginalize_dim is not None:
            obj_vals = obj_vals.mean(dim=marginalize_dim)
        else:
            # TODO: support batched inputs (req. dealing with ragged tensors)
            raise UnsupportedError(
                "Models with multiple batch dims are currently unsupported by"
                " prune_inferior_points_multi_objective.")
    if constraints is not None:
        infeas = torch.stack([c(samples) > 0 for c in constraints],
                             dim=0).any(dim=0)
        if infeas.ndim == 3 and marginalize_dim is not None:
            # make sure marginalize_dim is not negative
            if marginalize_dim < 0:
                # add 1 to the normalize marginalize_dim since we have already
                # removed the output dim
                marginalize_dim = (
                    1 + normalize_indices([marginalize_dim], d=infeas.ndim)[0])

            infeas = infeas.float().mean(dim=marginalize_dim).round().bool()
        # set infeasible points to be the ref point
        obj_vals[infeas] = ref_point
    pareto_mask = is_non_dominated(
        obj_vals, deduplicate=False) & (obj_vals > ref_point).all(dim=-1)
    probs = pareto_mask.to(dtype=X.dtype).mean(dim=0)
    idcs = probs.nonzero().view(-1)
    if idcs.shape[0] > max_points:
        counts, order_idcs = torch.sort(probs, descending=True)
        idcs = order_idcs[:max_points]

    return X[idcs]
Example #9
0
    def get_mvar_set_cpu(self, Y: Tensor) -> Tensor:
        r"""Find MVaR set based on the definition in [Prekopa2012MVaR]_.

        NOTE: This is much faster on CPU for large `n_w` than the alternative but it
        is significantly slower on GPU. Based on empirical evidence, this is recommended
        when running on CPU with `n_w > 64`.

        This first calculates the CDF for each point on the extended domain of the
        random variable (the grid defined by the given samples), then takes the
        values with CDF equal to (rounded if necessary) `alpha`. The non-dominated
        subset of these form the MVaR set.

        Args:
            Y: A `batch x n_w x m`-dim tensor of outcomes. This is currently
                restricted to `m = 2` objectives.
                TODO: Support `m > 2` objectives.

        Returns:
            A `batch` length list of `k x m`-dim tensor of MVaR values, where `k`
            depends on the corresponding batch inputs. Note that MVaR values in general
            are not in-sample points.
        """
        if Y.dim() == 3:
            return [self.get_mvar_set_cpu(y_) for y_ in Y]
        m = Y.shape[-1]
        if m != 2:  # pragma: no cover
            raise ValueError(
                "`get_mvar_set_cpu` only supports `m=2` outcomes!")
        # Generate sets of all unique values in each output dimension.
        # Note that points in MVaR are bounded from above by the
        # independent VaR of each objective. Hence, we only need to
        # consider the unique outcomes that are less than or equal to
        # the VaR of the independent objectives
        var_alpha_idx = ceil(self.alpha * self.n_w) - 1
        Y_sorted = Y.topk(Y.shape[0] - var_alpha_idx, dim=0,
                          largest=False).values
        unique_outcomes_list = [
            Y_sorted[:, i].unique().tolist()[::-1] for i in range(m)
        ]
        # Convert this into a list of m dictionaries mapping values to indices.
        unique_outcomes = [
            dict(zip(outcomes, range(len(outcomes))))
            for outcomes in unique_outcomes_list
        ]
        # Initialize a tensor counting the number of points in Y that a given grid point
        # is dominated by. This will essentially be a non-normalized CDF.
        counter_tensor = torch.zeros(
            [len(outcomes) for outcomes in unique_outcomes],
            dtype=torch.long,
            device=Y.device,
        )
        # populate the tensor, counting the dominated points.
        # we only need to consider points in Y where at least one
        # objective is less than the max objective value in
        # unique_outcomes_list
        max_vals = torch.tensor([o[0] for o in unique_outcomes_list],
                                dtype=Y.dtype,
                                device=Y.device)
        mask = (Y < max_vals).any(dim=-1)
        counter_tensor += self.n_w - mask.sum()
        Y_pruned = Y[mask]
        for y_ in Y_pruned:
            starting_idcs = [
                unique_outcomes[i].get(y_[i].item(), 0) for i in range(m)
            ]
            counter_tensor[starting_idcs[0]:, starting_idcs[1]:] += 1

        # Get the count alpha-level points should have.
        alpha_count = ceil(self.alpha * self.n_w)
        # Get the alpha level indices.
        alpha_level_indices = (counter_tensor == alpha_count).nonzero(
            as_tuple=False)
        # If there are no exact alpha level points, get the smallest alpha' > alpha
        # and find the corresponding alpha level indices.
        if alpha_level_indices.numel() == 0:
            min_greater_than_alpha = counter_tensor[
                counter_tensor > alpha_count].min()
            alpha_level_indices = (
                counter_tensor == min_greater_than_alpha).nonzero(
                    as_tuple=False)
        unique_outcomes = [
            torch.as_tensor(list(outcomes.keys()),
                            device=Y.device,
                            dtype=Y.dtype) for outcomes in unique_outcomes
        ]
        alpha_level_points = torch.stack(
            [
                unique_outcomes[i][alpha_level_indices[:, i]]
                for i in range(len(unique_outcomes))
            ],
            dim=-1,
        )
        # MVaR is simply the non-dominated subset of alpha level points.
        if self.filter_dominated:
            mask = is_non_dominated(alpha_level_points)
            mvar = alpha_level_points[mask]
        else:
            mvar = alpha_level_points
        return mvar
Example #10
0
    def test_is_non_dominated(self) -> None:
        tkwargs = {"device": self.device}
        Y = torch.tensor([
            [1.0, 5.0],
            [10.0, 3.0],
            [4.0, 5.0],
            [4.0, 5.0],
            [5.0, 5.0],
            [8.5, 3.5],
            [8.5, 3.0],
            [9.0, 1.0],
        ])
        expected_nondom_Y = torch.tensor([[10.0, 3.0], [5.0, 5.0], [8.5, 3.5]])
        Yb = Y.clone()
        Yb[1] = 0
        expected_nondom_Yb = torch.tensor([[5.0, 5.0], [8.5, 3.5], [9.0, 1.0]])
        Y3 = torch.tensor([
            [4.0, 2.0, 3.0],
            [2.0, 4.0, 1.0],
            [3.0, 5.0, 1.0],
            [2.0, 4.0, 2.0],
            [1.0, 3.0, 4.0],
            [1.0, 2.0, 4.0],
            [1.0, 2.0, 6.0],
        ])
        Y3b = Y3.clone()
        Y3b[0] = 0
        expected_nondom_Y3 = torch.tensor([
            [4.0, 2.0, 3.0],
            [3.0, 5.0, 1.0],
            [2.0, 4.0, 2.0],
            [1.0, 3.0, 4.0],
            [1.0, 2.0, 6.0],
        ])
        expected_nondom_Y3b = expected_nondom_Y3[1:]
        for dtype in (torch.float, torch.double):
            tkwargs["dtype"] = dtype
            Y = Y.to(**tkwargs)
            expected_nondom_Y = expected_nondom_Y.to(**tkwargs)
            Yb = Yb.to(**tkwargs)
            expected_nondom_Yb = expected_nondom_Yb.to(**tkwargs)
            Y3 = Y3.to(**tkwargs)
            expected_nondom_Y3 = expected_nondom_Y3.to(**tkwargs)
            Y3b = Y3b.to(**tkwargs)
            expected_nondom_Y3b = expected_nondom_Y3b.to(**tkwargs)

            # test 2d
            nondom_Y = Y[is_non_dominated(Y)]
            self.assertTrue(torch.equal(expected_nondom_Y, nondom_Y))
            # test batch
            batch_Y = torch.stack([Y, Yb], dim=0)
            nondom_mask = is_non_dominated(batch_Y)
            self.assertTrue(
                torch.equal(batch_Y[0][nondom_mask[0]], expected_nondom_Y))
            self.assertTrue(
                torch.equal(batch_Y[1][nondom_mask[1]], expected_nondom_Yb))

            # test 3d
            nondom_Y3 = Y3[is_non_dominated(Y3)]
            self.assertTrue(torch.equal(expected_nondom_Y3, nondom_Y3))
            # test batch
            batch_Y3 = torch.stack([Y3, Y3b], dim=0)
            nondom_mask3 = is_non_dominated(batch_Y3)
            self.assertTrue(
                torch.equal(batch_Y3[0][nondom_mask3[0]], expected_nondom_Y3))
            self.assertTrue(
                torch.equal(batch_Y3[1][nondom_mask3[1]], expected_nondom_Y3b))
Example #11
0
    def test_infer_objective_thresholds(self, _, cuda=False):
        # lightweight test
        exp = get_branin_experiment_with_multi_objective(
            has_optimization_config=True,
            with_batch=True,
            with_status_quo=True,
        )
        for trial in exp.trials.values():
            trial.mark_running(no_runner_required=True).mark_completed()
        exp.attach_data(
            get_branin_data_multi_objective(trial_indices=exp.trials.keys())
        )
        data = exp.fetch_data()
        modelbridge = TorchModelBridge(
            search_space=exp.search_space,
            model=MultiObjectiveBotorchModel(),
            optimization_config=exp.optimization_config,
            transforms=Cont_X_trans + Y_trans,
            torch_device=torch.device("cuda" if cuda else "cpu"),
            experiment=exp,
            data=data,
        )
        fixed_features = ObservationFeatures(parameters={"x1": 0.0})
        search_space = exp.search_space.clone()
        param_constraints = [
            ParameterConstraint(constraint_dict={"x1": 1.0}, bound=10.0)
        ]
        search_space.add_parameter_constraints(param_constraints)
        oc = exp.optimization_config.clone()
        oc.objective._objectives[0].minimize = True
        expected_base_gen_args = modelbridge._get_transformed_gen_args(
            search_space=search_space.clone(),
            optimization_config=oc,
            fixed_features=fixed_features,
        )
        with ExitStack() as es:
            mock_model_infer_obj_t = es.enter_context(
                patch(
                    "ax.modelbridge.torch.infer_objective_thresholds",
                    wraps=infer_objective_thresholds,
                )
            )
            mock_get_transformed_gen_args = es.enter_context(
                patch.object(
                    modelbridge,
                    "_get_transformed_gen_args",
                    wraps=modelbridge._get_transformed_gen_args,
                )
            )
            mock_get_transformed_model_gen_args = es.enter_context(
                patch.object(
                    modelbridge,
                    "_get_transformed_model_gen_args",
                    wraps=modelbridge._get_transformed_model_gen_args,
                )
            )
            mock_untransform_objective_thresholds = es.enter_context(
                patch.object(
                    modelbridge,
                    "_untransform_objective_thresholds",
                    wraps=modelbridge._untransform_objective_thresholds,
                )
            )
            obj_thresholds = modelbridge.infer_objective_thresholds(
                search_space=search_space,
                optimization_config=oc,
                fixed_features=fixed_features,
            )
            expected_obj_weights = torch.tensor([-1.0, 1.0])
            ckwargs = mock_model_infer_obj_t.call_args[1]
            self.assertTrue(
                torch.equal(ckwargs["objective_weights"], expected_obj_weights)
            )
            # check that transforms have been applied (at least UnitX)
            self.assertEqual(ckwargs["bounds"], [(0.0, 1.0), (0.0, 1.0)])
            lc = ckwargs["linear_constraints"]
            self.assertTrue(torch.equal(lc[0], torch.tensor([[15.0, 0.0]])))
            self.assertTrue(torch.equal(lc[1], torch.tensor([[15.0]])))
            self.assertEqual(ckwargs["fixed_features"], {0: 1.0 / 3.0})
            mock_get_transformed_gen_args.assert_called_once()
            mock_get_transformed_model_gen_args.assert_called_once_with(
                search_space=expected_base_gen_args.search_space,
                fixed_features=expected_base_gen_args.fixed_features,
                pending_observations=expected_base_gen_args.pending_observations,
                optimization_config=expected_base_gen_args.optimization_config,
            )
            mock_untransform_objective_thresholds.assert_called_once()
            ckwargs = mock_untransform_objective_thresholds.call_args[1]

            self.assertTrue(
                torch.equal(ckwargs["objective_weights"], expected_obj_weights)
            )
            self.assertEqual(ckwargs["bounds"], [(0.0, 1.0), (0.0, 1.0)])
            self.assertEqual(ckwargs["fixed_features"], {0: 1.0 / 3.0})
        self.assertEqual(obj_thresholds[0].metric.name, "branin_a")
        self.assertEqual(obj_thresholds[1].metric.name, "branin_b")
        self.assertEqual(obj_thresholds[0].op, ComparisonOp.LEQ)
        self.assertEqual(obj_thresholds[1].op, ComparisonOp.GEQ)
        self.assertFalse(obj_thresholds[0].relative)
        self.assertFalse(obj_thresholds[1].relative)
        df = exp_to_df(exp)
        Y = np.stack([df.branin_a.values, df.branin_b.values]).T
        Y = torch.from_numpy(Y)
        Y[:, 0] *= -1
        pareto_Y = Y[is_non_dominated(Y)]
        nadir = pareto_Y.min(dim=0).values
        self.assertTrue(
            np.all(
                np.array([-obj_thresholds[0].bound, obj_thresholds[1].bound])
                < nadir.numpy()
            )
        )
        # test using MTGP
        sobol_generator = get_sobol(
            search_space=exp.search_space,
            seed=TEST_SOBOL_SEED,
            # set initial position equal to the number of sobol arms generated
            # so far. This means that new sobol arms will complement the previous
            # arms in a space-filling fashion
            init_position=len(exp.arms_by_name) - 1,
        )
        sobol_run = sobol_generator.gen(n=2)
        trial = exp.new_batch_trial(optimize_for_power=True)
        trial.add_generator_run(sobol_run)
        trial.mark_running(no_runner_required=True).mark_completed()
        data = exp.fetch_data()
        torch.manual_seed(0)  # make model fitting deterministic
        modelbridge = TorchModelBridge(
            search_space=exp.search_space,
            model=MultiObjectiveBotorchModel(),
            optimization_config=exp.optimization_config,
            transforms=ST_MTGP_trans,
            experiment=exp,
            data=data,
        )
        fixed_features = ObservationFeatures(parameters={}, trial_index=1)
        expected_base_gen_args = modelbridge._get_transformed_gen_args(
            search_space=search_space.clone(),
            optimization_config=exp.optimization_config,
            fixed_features=fixed_features,
        )
        with ExitStack() as es:
            mock_model_infer_obj_t = es.enter_context(
                patch(
                    "ax.modelbridge.torch.infer_objective_thresholds",
                    wraps=infer_objective_thresholds,
                )
            )
            mock_untransform_objective_thresholds = es.enter_context(
                patch.object(
                    modelbridge,
                    "_untransform_objective_thresholds",
                    wraps=modelbridge._untransform_objective_thresholds,
                )
            )
            obj_thresholds = modelbridge.infer_objective_thresholds(
                search_space=search_space,
                optimization_config=exp.optimization_config,
                fixed_features=fixed_features,
            )
            ckwargs = mock_model_infer_obj_t.call_args[1]
            self.assertEqual(ckwargs["fixed_features"], {2: 1.0})
            mock_untransform_objective_thresholds.assert_called_once()
            ckwargs = mock_untransform_objective_thresholds.call_args[1]
            self.assertEqual(ckwargs["fixed_features"], {2: 1.0})
        self.assertEqual(obj_thresholds[0].metric.name, "branin_a")
        self.assertEqual(obj_thresholds[1].metric.name, "branin_b")
        self.assertEqual(obj_thresholds[0].op, ComparisonOp.GEQ)
        self.assertEqual(obj_thresholds[1].op, ComparisonOp.GEQ)
        self.assertFalse(obj_thresholds[0].relative)
        self.assertFalse(obj_thresholds[1].relative)
        df = exp_to_df(exp)
        trial_mask = df.trial_index == 1
        Y = np.stack([df.branin_a.values[trial_mask], df.branin_b.values[trial_mask]]).T
        Y = torch.from_numpy(Y)
        pareto_Y = Y[is_non_dominated(Y)]
        nadir = pareto_Y.min(dim=0).values
        self.assertTrue(
            np.all(
                np.array([obj_thresholds[0].bound, obj_thresholds[1].bound])
                < nadir.numpy()
            )
        )
    def test_infer_objective_thresholds(self, _, cuda=False):
        # lightweight test
        exp = get_branin_experiment_with_multi_objective(
            has_optimization_config=True,
            with_batch=True,
            with_status_quo=True,
        )
        for trial in exp.trials.values():
            trial.mark_running(no_runner_required=True).mark_completed()
        exp.attach_data(
            get_branin_data_multi_objective(trial_indices=exp.trials.keys()))
        data = exp.fetch_data()
        modelbridge = MultiObjectiveTorchModelBridge(
            search_space=exp.search_space,
            model=MultiObjectiveBotorchModel(),
            optimization_config=exp.optimization_config,
            transforms=Cont_X_trans + Y_trans,
            torch_device=torch.device("cuda" if cuda else "cpu"),
            experiment=exp,
            data=data,
        )
        fixed_features = ObservationFeatures(parameters={"x1": 0.0})
        search_space = exp.search_space.clone()
        param_constraints = [
            ParameterConstraint(constraint_dict={"x1": 1.0}, bound=10.0)
        ]
        outcome_constraints = [
            OutcomeConstraint(
                metric=exp.metrics["branin_a"],
                op=ComparisonOp.GEQ,
                bound=-40.0,
                relative=False,
            )
        ]
        search_space.add_parameter_constraints(param_constraints)
        exp.optimization_config.outcome_constraints = outcome_constraints
        oc = exp.optimization_config.clone()
        oc.objective._objectives[0].minimize = True
        expected_base_gen_args = modelbridge._get_transformed_gen_args(
            search_space=search_space.clone(),
            optimization_config=oc,
            fixed_features=fixed_features,
        )
        with ExitStack() as es:
            mock_model_infer_obj_t = es.enter_context(
                patch(
                    "ax.modelbridge.multi_objective_torch.infer_objective_thresholds",
                    wraps=infer_objective_thresholds,
                ))
            mock_get_transformed_gen_args = es.enter_context(
                patch.object(
                    modelbridge,
                    "_get_transformed_gen_args",
                    wraps=modelbridge._get_transformed_gen_args,
                ))
            mock_get_transformed_model_gen_args = es.enter_context(
                patch.object(
                    modelbridge,
                    "_get_transformed_model_gen_args",
                    wraps=modelbridge._get_transformed_model_gen_args,
                ))
            mock_untransform_objective_thresholds = es.enter_context(
                patch.object(
                    modelbridge,
                    "untransform_objective_thresholds",
                    wraps=modelbridge.untransform_objective_thresholds,
                ))
            obj_thresholds = modelbridge.infer_objective_thresholds(
                search_space=search_space,
                optimization_config=oc,
                fixed_features=fixed_features,
            )
            expected_obj_weights = torch.tensor([-1.0, 1.0])
            ckwargs = mock_model_infer_obj_t.call_args[1]
            self.assertTrue(
                torch.equal(ckwargs["objective_weights"],
                            expected_obj_weights))
            # check that transforms have been applied (at least UnitX)
            self.assertEqual(ckwargs["bounds"], [(0.0, 1.0), (0.0, 1.0)])
            oc = ckwargs["outcome_constraints"]
            self.assertTrue(torch.equal(oc[0], torch.tensor([[-1.0, 0.0]])))
            self.assertTrue(torch.equal(oc[1], torch.tensor([[45.0]])))
            lc = ckwargs["linear_constraints"]
            self.assertTrue(torch.equal(lc[0], torch.tensor([[15.0, 0.0]])))
            self.assertTrue(torch.equal(lc[1], torch.tensor([[15.0]])))
            self.assertEqual(ckwargs["fixed_features"], {0: 1.0 / 3.0})
            mock_get_transformed_gen_args.assert_called_once()
            mock_get_transformed_model_gen_args.assert_called_once_with(
                search_space=expected_base_gen_args.search_space,
                fixed_features=expected_base_gen_args.fixed_features,
                pending_observations=expected_base_gen_args.
                pending_observations,
                optimization_config=expected_base_gen_args.optimization_config,
            )
            mock_untransform_objective_thresholds.assert_called_once()
            ckwargs = mock_untransform_objective_thresholds.call_args[1]

            self.assertTrue(
                torch.equal(ckwargs["objective_weights"],
                            expected_obj_weights))
            self.assertEqual(ckwargs["bounds"], [(0.0, 1.0), (0.0, 1.0)])
            self.assertEqual(ckwargs["fixed_features"], {0: 1.0 / 3.0})
        self.assertEqual(obj_thresholds[0].metric.name, "branin_a")
        self.assertEqual(obj_thresholds[1].metric.name, "branin_b")
        self.assertEqual(obj_thresholds[0].op, ComparisonOp.LEQ)
        self.assertEqual(obj_thresholds[1].op, ComparisonOp.GEQ)
        self.assertFalse(obj_thresholds[0].relative)
        self.assertFalse(obj_thresholds[1].relative)
        df = exp_to_df(exp)
        Y = np.stack([df.branin_a.values, df.branin_b.values]).T
        Y = torch.from_numpy(Y)
        Y[:, 0] *= -1
        pareto_Y = Y[is_non_dominated(Y)]
        nadir = pareto_Y.min(dim=0).values
        self.assertTrue(
            np.all(
                np.array([-obj_thresholds[0].bound, obj_thresholds[1].bound]) <
                nadir.numpy()))
        # test using MTGP
        sobol_generator = get_sobol(search_space=exp.search_space)
        sobol_run = sobol_generator.gen(n=5)
        trial = exp.new_batch_trial(optimize_for_power=True)
        trial.add_generator_run(sobol_run)
        trial.mark_running(no_runner_required=True).mark_completed()
        data = exp.fetch_data()
        modelbridge = MultiObjectiveTorchModelBridge(
            search_space=exp.search_space,
            model=MultiObjectiveBotorchModel(),
            optimization_config=exp.optimization_config,
            transforms=ST_MTGP_trans,
            experiment=exp,
            data=data,
        )
        fixed_features = ObservationFeatures(parameters={}, trial_index=1)
        expected_base_gen_args = modelbridge._get_transformed_gen_args(
            search_space=search_space.clone(),
            optimization_config=exp.optimization_config,
            fixed_features=fixed_features,
        )
        with self.assertRaises(ValueError):
            # Check that a ValueError is raised when MTGP is being used
            # and trial_index is not specified as a fixed features.
            # Note: this error is raised by StratifiedStandardizeY
            modelbridge.infer_objective_thresholds(
                search_space=search_space,
                optimization_config=exp.optimization_config,
            )
        with ExitStack() as es:
            mock_model_infer_obj_t = es.enter_context(
                patch(
                    "ax.modelbridge.multi_objective_torch.infer_objective_thresholds",
                    wraps=infer_objective_thresholds,
                ))
            mock_untransform_objective_thresholds = es.enter_context(
                patch.object(
                    modelbridge,
                    "untransform_objective_thresholds",
                    wraps=modelbridge.untransform_objective_thresholds,
                ))
            obj_thresholds = modelbridge.infer_objective_thresholds(
                search_space=search_space,
                optimization_config=exp.optimization_config,
                fixed_features=fixed_features,
            )
            ckwargs = mock_model_infer_obj_t.call_args[1]
            self.assertEqual(ckwargs["fixed_features"], {2: 1.0})
            mock_untransform_objective_thresholds.assert_called_once()
            ckwargs = mock_untransform_objective_thresholds.call_args[1]
            self.assertEqual(ckwargs["fixed_features"], {2: 1.0})
        self.assertEqual(obj_thresholds[0].metric.name, "branin_a")
        self.assertEqual(obj_thresholds[1].metric.name, "branin_b")
        self.assertEqual(obj_thresholds[0].op, ComparisonOp.GEQ)
        self.assertEqual(obj_thresholds[1].op, ComparisonOp.GEQ)
        self.assertFalse(obj_thresholds[0].relative)
        self.assertFalse(obj_thresholds[1].relative)
        df = exp_to_df(exp)
        trial_mask = df.trial_index == 1
        Y = np.stack(
            [df.branin_a.values[trial_mask], df.branin_b.values[trial_mask]]).T
        Y = torch.from_numpy(Y)
        pareto_Y = Y[is_non_dominated(Y)]
        nadir = pareto_Y.min(dim=0).values
        self.assertTrue(
            np.all(
                np.array([obj_thresholds[0].bound, obj_thresholds[1].bound]) <
                nadir.numpy()))
    def test_mvar(self):
        with self.assertRaises(ValueError):
            MVaR(n_w=5, alpha=3.0)

        def set_equals(t1: Tensor, t2: Tensor) -> bool:
            r"""Check if two `k x m`-dim tensors are equivalent after possibly
            reordering the `k` dimension. Ignores duplicate entries.
            """
            t1 = t1.unique(dim=0)
            t2 = t2.unique(dim=0)
            if t1.shape != t2.shape:
                return False
            equals_sum = (t1.unsqueeze(-2) == t2).all(dim=-1).sum(dim=-1)
            return torch.equal(equals_sum, torch.ones_like(equals_sum))

        for dtype in (torch.float, torch.double):
            tkwargs = {"device": self.device, "dtype": dtype}
            mvar = MVaR(n_w=5, alpha=0.6)
            # a simple negatively correlated example
            Y = torch.stack(
                [torch.linspace(1, 5, 5), torch.linspace(5, 1, 5)],
                dim=-1,
            ).to(**tkwargs)
            expected_set = torch.stack(
                [torch.linspace(1, 3, 3), torch.linspace(3, 1, 3)],
                dim=-1,
            ).to(Y)
            # check that both versions produce the correct set
            cpu_mvar = mvar.get_mvar_set_cpu(Y)  # For 2d input, returns k x m
            gpu_mvar = mvar.get_mvar_set_gpu(Y)[0]  # returns a batch list of k x m
            self.assertTrue(set_equals(cpu_mvar, gpu_mvar))
            self.assertTrue(set_equals(cpu_mvar, expected_set))
            # check that the `filter_dominated` works correctly
            mvar = MVaR(
                n_w=5,
                alpha=0.4,
                filter_dominated=False,
            )
            # negating the input to treat large values as undesirable
            Y = -torch.tensor(
                [
                    [1, 4],
                    [2, 3],
                    [3, 2],
                    [4, 1],
                    [3.5, 3.5],
                ],
                **tkwargs
            )
            cpu_mvar = mvar.get_mvar_set_cpu(Y)
            gpu_mvar = mvar.get_mvar_set_gpu(Y)[0]
            self.assertTrue(set_equals(cpu_mvar, gpu_mvar))
            # negating here as well
            expected_w_dominated = -torch.tensor(
                [
                    [2, 4],
                    [3, 3],
                    [3.5, 3],
                    [3, 3.5],
                    [4, 2],
                ],
                **tkwargs
            )
            self.assertTrue(set_equals(cpu_mvar, expected_w_dominated))
            expected_non_dominated = expected_w_dominated[
                is_non_dominated(expected_w_dominated)
            ]
            mvar.filter_dominated = True
            cpu_mvar = mvar.get_mvar_set_cpu(Y)
            gpu_mvar = mvar.get_mvar_set_gpu(Y)[0]
            self.assertTrue(set_equals(cpu_mvar, gpu_mvar))
            self.assertTrue(set_equals(cpu_mvar, expected_non_dominated))

            # test batched w/ random input
            mvar = MVaR(
                n_w=10,
                alpha=0.5,
                filter_dominated=False,
            )
            Y = torch.rand(4, 10, 2, **tkwargs)
            cpu_mvar = mvar.get_mvar_set_cpu(Y)
            gpu_mvar = mvar.get_mvar_set_gpu(Y)
            # check that the two agree
            self.assertTrue(
                all([set_equals(cpu_mvar[i], gpu_mvar[i]) for i in range(4)])
            )
            # check that the MVaR is dominated by `alpha` fraction (maximization).
            dominated_count = (Y[0].unsqueeze(-2) >= cpu_mvar[0]).all(dim=-1).sum(dim=0)
            expected_count = (
                torch.ones(cpu_mvar[0].shape[0], device=self.device, dtype=torch.long)
                * 5
            )
            self.assertTrue(torch.equal(dominated_count, expected_count))

            # test forward pass
            # with `expectation=True`
            mvar = MVaR(
                n_w=10,
                alpha=0.5,
                expectation=True,
            )
            samples = torch.rand(2, 20, 2, **tkwargs)
            mvar_exp = mvar(samples)
            expected = [
                mvar.get_mvar_set_cpu(Y).mean(dim=0) for Y in samples.view(4, 10, 2)
            ]
            self.assertTrue(
                torch.allclose(mvar_exp, torch.stack(expected).view(2, 2, 2))
            )

            # m > 2
            samples = torch.rand(2, 20, 3, **tkwargs)
            mvar_exp = mvar(samples)
            expected = [
                mvar.get_mvar_set_gpu(Y)[0].mean(dim=0) for Y in samples.view(4, 10, 3)
            ]
            self.assertTrue(torch.equal(mvar_exp, torch.stack(expected).view(2, 2, 3)))

            # with `expectation=False`
            mvar = MVaR(
                n_w=10,
                alpha=0.5,
                expectation=False,
                pad_to_n_w=True,
            )
            samples = torch.rand(2, 20, 2, **tkwargs)
            mvar_vals = mvar(samples)
            self.assertTrue(mvar_vals.shape == samples.shape)
            expected = [mvar.get_mvar_set_cpu(Y) for Y in samples.view(4, 10, 2)]
            for i in range(4):
                batch_idx = i // 2
                q_idx_start = 10 * (i % 2)
                expected_ = expected[i]
                # check that the actual values are there
                self.assertTrue(
                    set_equals(
                        mvar_vals[
                            batch_idx, q_idx_start : q_idx_start + expected_.shape[0]
                        ],
                        expected_,
                    )
                )
                # check for correct padding
                self.assertTrue(
                    torch.equal(
                        mvar_vals[
                            batch_idx,
                            q_idx_start + expected_.shape[0] : q_idx_start + 10,
                        ],
                        mvar_vals[
                            batch_idx, q_idx_start + expected_.shape[0] - 1
                        ].expand(10 - expected_.shape[0], -1),
                    )
                )

            # Test the no-exact alpha level points case.
            # This happens when there are duplicates in the input.
            Y = torch.ones(10, 2, **tkwargs)
            cpu_mvar = mvar.get_mvar_set_cpu(Y)
            gpu_mvar = mvar.get_mvar_set_gpu(Y)[0]
            self.assertTrue(torch.equal(cpu_mvar, Y[:1]))
            self.assertTrue(torch.equal(gpu_mvar, Y[:1]))

            # Test grad warning
            with self.assertWarnsRegex(RuntimeWarning, "requires grad"):
                mvar(Y.requires_grad_())
Example #14
0
def infer_objective_thresholds(
    model: Model,
    objective_weights: Tensor,  # objective_directions
    bounds: Optional[List[Tuple[float, float]]] = None,
    outcome_constraints: Optional[Tuple[Tensor, Tensor]] = None,
    linear_constraints: Optional[Tuple[Tensor, Tensor]] = None,
    fixed_features: Optional[Dict[int, float]] = None,
    subset_idcs: Optional[Tensor] = None,
    Xs: Optional[List[Tensor]] = None,
    X_observed: Optional[Tensor] = None,
) -> Tensor:
    """Infer objective thresholds.

    This method uses the model-estimated Pareto frontier over the in-sample points
    to infer absolute (not relativized) objective thresholds.

    This uses a heuristic that sets the objective threshold to be a scaled nadir
    point, where the nadir point is scaled back based on the range of each
    objective across the current in-sample Pareto frontier.

    See `botorch.utils.multi_objective.hypervolume.infer_reference_point` for
    details on the heuristic.

    Args:
        model: A fitted botorch Model.
        objective_weights: The objective is to maximize a weighted sum of
            the columns of f(x). These are the weights. These should not
            be subsetted.
        bounds: A list of (lower, upper) tuples for each column of X.
        outcome_constraints: A tuple of (A, b). For k outcome constraints
            and m outputs at f(x), A is (k x m) and b is (k x 1) such that
            A f(x) <= b. These should not be subsetted.
        linear_constraints: A tuple of (A, b). For k linear constraints on
            d-dimensional x, A is (k x d) and b is (k x 1) such that
            A x <= b.
        fixed_features: A map {feature_index: value} for features that
            should be fixed to a particular value during generation.
        subset_idcs: The indices of the outcomes that are modeled by the
            provided model. If subset_idcs not None, this method infers
            whether the model is subsetted.
        Xs: A list of m (k_i x d) feature tensors X. Number of rows k_i can
            vary from i=1,...,m.
        X_observed: A `n x d`-dim tensor of in-sample points to use for
            determining the current in-sample Pareto frontier.

    Returns:
        A `m`-dim tensor of objective thresholds, where the objective
            threshold is `nan` if the outcome is not an objective.
    """
    if X_observed is None:
        if bounds is None:
            raise ValueError("bounds is required if X_observed is None.")
        elif Xs is None:
            raise ValueError("Xs is required if X_observed is None.")
        _, X_observed = _get_X_pending_and_observed(
            Xs=Xs,
            objective_weights=objective_weights,
            outcome_constraints=outcome_constraints,
            bounds=bounds,
            linear_constraints=linear_constraints,
            fixed_features=fixed_features,
        )
    num_outcomes = objective_weights.shape[0]
    if subset_idcs is None:
        # check if only a subset of outcomes are modeled
        nonzero = objective_weights != 0
        if outcome_constraints is not None:
            A, _ = outcome_constraints
            nonzero = nonzero | torch.any(A != 0, dim=0)
        expected_subset_idcs = nonzero.nonzero().view(-1)
        if model.num_outputs > expected_subset_idcs.numel():
            # subset the model so that we only compute the posterior
            # over the relevant outcomes
            subset_model_results = subset_model(
                model=model,
                objective_weights=objective_weights,
                outcome_constraints=outcome_constraints,
            )
            model = subset_model_results.model
            objective_weights = subset_model_results.objective_weights
            outcome_constraints = subset_model_results.outcome_constraints
            subset_idcs = subset_model_results.indices
        else:
            # model is already subsetted.
            subset_idcs = expected_subset_idcs
            # subset objective weights and outcome constraints
            objective_weights = objective_weights[subset_idcs]
            if outcome_constraints is not None:
                outcome_constraints = (
                    outcome_constraints[0][:, subset_idcs],
                    outcome_constraints[1],
                )
    else:
        objective_weights = objective_weights[subset_idcs]
        if outcome_constraints is not None:
            outcome_constraints = (
                outcome_constraints[0][:, subset_idcs],
                outcome_constraints[1],
            )
    with torch.no_grad():
        pred = not_none(model).posterior(not_none(X_observed)).mean
    if outcome_constraints is not None:
        cons_tfs = get_outcome_constraint_transforms(outcome_constraints)
        # pyre-ignore [16]
        feas = torch.stack([c(pred) <= 0 for c in cons_tfs],
                           dim=-1).all(dim=-1)
        pred = pred[feas]
    if pred.shape[0] == 0:
        raise AxError("There are no feasible observed points.")
    obj_mask = objective_weights.nonzero().view(-1)
    obj_weights_subset = objective_weights[obj_mask]
    obj = pred[..., obj_mask] * obj_weights_subset
    pareto_obj = obj[is_non_dominated(obj)]
    objective_thresholds = infer_reference_point(
        pareto_Y=pareto_obj,
        scale=0.1,
    )
    # multiply by objective weights to return objective thresholds in the
    # unweighted space
    objective_thresholds = objective_thresholds * obj_weights_subset
    full_objective_thresholds = torch.full(
        (num_outcomes, ),
        float("nan"),
        dtype=objective_weights.dtype,
        device=objective_weights.device,
    )
    obj_idcs = subset_idcs[obj_mask]
    full_objective_thresholds[obj_idcs] = objective_thresholds.clone()
    return full_objective_thresholds
Example #15
0
    def get_mvar_set_gpu(self, Y: Tensor) -> Tensor:
        r"""Find MVaR set based on the definition in [Prekopa2012MVaR]_.

        NOTE: This is much faster on GPU than the alternative but it scales very poorly
        on CPU as `n_w` increases. This should be preferred if a GPU is available or
        when `n_w <= 64`. In addition, this supports `m >= 2` outcomes (vs `m = 2` for
        the CPU version) and it should be used if `m > 2`.

        This first calculates the CDF for each point on the extended domain of the
        random variable (the grid defined by the given samples), then takes the
        values with CDF equal to (rounded if necessary) `alpha`. The non-dominated
        subset of these form the MVaR set.

        Args:
            Y: A `batch x n_w x m`-dim tensor of observations.

        Returns:
            A `batch` length list of `k x m`-dim tensor of MVaR values, where `k`
            depends on the corresponding batch inputs. Note that MVaR values in general
            are not in-sample points.
        """
        if Y.dim() == 2:
            Y = Y.unsqueeze(0)
        batch, m = Y.shape[0], Y.shape[-1]
        # Note that points in MVaR are bounded from above by the
        # independent VaR of each objective. Hence, we only need to
        # consider the unique outcomes that are less than or equal to
        # the VaR of the independent objectives
        var_alpha_idx = ceil(self.alpha * self.n_w) - 1
        n_points = Y.shape[-2] - var_alpha_idx
        Y_sorted = Y.topk(n_points, dim=-2, largest=False).values
        # `y_grid` is the grid formed by all inputs in each batch.
        if m == 2:
            # This is significantly faster but only works with m=2.
            y_grid = torch.stack(
                [
                    Y_sorted[..., 0].repeat_interleave(repeats=n_points,
                                                       dim=-1),
                    Y_sorted[..., 1].repeat(1, n_points),
                ],
                dim=-1,
            )
        else:
            y_grid = torch.stack(
                [
                    torch.stack(
                        torch.meshgrid([Y_sorted[b, :, i] for i in range(m)]),
                        dim=-1,
                    ).view(-1, m) for b in range(batch)
                ],
                dim=0,
            )
        # Get the non-normalized CDF.
        cdf = (Y.unsqueeze(-2) >= y_grid.unsqueeze(-3)).all(dim=-1).sum(dim=-2)
        # Get the alpha level points
        alpha_count = ceil(self.alpha * self.n_w)
        # NOTE: Need to loop here since mvar may have different shapes.
        mvar = []
        for b in range(batch):
            alpha_level_points = y_grid[b][cdf[b] == alpha_count]
            # If there are no exact alpha level points, get the smallest alpha' > alpha
            # and find the corresponding alpha level indices.
            if alpha_level_points.numel() == 0:
                min_greater_than_alpha = cdf[b][cdf[b] > alpha_count].min()
                alpha_level_points = y_grid[b][cdf[b] ==
                                               min_greater_than_alpha]
            # MVaR is the non-dominated subset of alpha level points.
            if self.filter_dominated:
                mask = is_non_dominated(alpha_level_points)
                mvar.append(alpha_level_points[mask])
            else:
                mvar.append(alpha_level_points)
        return mvar
Example #16
0
def pareto_frontier_evaluator(
    model: TorchModel,
    objective_weights: Tensor,
    objective_thresholds: Optional[Tensor] = None,
    X: Optional[Tensor] = None,
    Y: Optional[Tensor] = None,
    Yvar: Optional[Tensor] = None,
    outcome_constraints: Optional[Tuple[Tensor, Tensor]] = None,
) -> Tuple[Tensor, Tensor, Tensor]:
    """Return outcomes predicted to lie on a pareto frontier.

    Given a model and a points to evaluate use the model to predict which points
    lie on the pareto frontier.

    Args:
        model: Model used to predict outcomes.
        objective_weights: A `m` tensor of values indicating the weight to put
            on different outcomes. For pareto frontiers only the sign matters.
        objective_thresholds:  A tensor containing thresholds forming a reference point
            from which to calculate pareto frontier hypervolume. Points that do not
            dominate the objective_thresholds contribute nothing to hypervolume.
        X: A `n x d` tensor of features to evaluate.
        Y: A `n x m` tensor of outcomes to use instead of predictions.
        Yvar: A `n x m x m` tensor of input covariances (NaN if unobserved).
        outcome_constraints: A tuple of (A, b). For k outcome constraints
            and m outputs at f(x), A is (k x m) and b is (k x 1) such that
            A f(x) <= b.

    Returns:
        3-element tuple containing

        - A `j x m` tensor of outcome on the pareto frontier. j is the number
            of frontier points.
        - A `j x m x m` tensor of predictive covariances.
            cov[j, m1, m2] is Cov[m1@j, m2@j].
        - A `j` tensor of the index of each frontier point in the input Y.
    """
    if X is not None:
        Y, Yvar = model.predict(X)
    elif Y is None or Yvar is None:
        raise ValueError(
            "Requires `X` to predict or both `Y` and `Yvar` to select a subset of "
            "points on the pareto frontier.")

    # Apply objective_weights to outcomes and objective_thresholds.
    # If objective_thresholds is not None use a dummy tensor of zeros.
    (
        obj,
        weighted_objective_thresholds,
    ) = get_weighted_mc_objective_and_objective_thresholds(
        objective_weights=objective_weights,
        objective_thresholds=(objective_thresholds
                              if objective_thresholds is not None else
                              torch.zeros(objective_weights.shape)),
    )
    Y_obj = obj(Y)
    indx_frontier = torch.arange(Y.shape[0], dtype=torch.long, device=Y.device)

    # Filter Y, Yvar, Y_obj to items that dominate all objective thresholds
    if objective_thresholds is not None:
        objective_thresholds_mask = (Y_obj >=
                                     weighted_objective_thresholds).all(dim=1)
        Y = Y[objective_thresholds_mask]
        Yvar = Yvar[objective_thresholds_mask]
        Y_obj = Y_obj[objective_thresholds_mask]
        indx_frontier = indx_frontier[objective_thresholds_mask]

    # Get feasible points that do not violate outcome_constraints
    if outcome_constraints is not None:
        cons_tfs = get_outcome_constraint_transforms(outcome_constraints)
        # pyre-ignore [16]
        feas = torch.stack([c(Y) <= 0 for c in cons_tfs], dim=-1).all(dim=-1)
        Y = Y[feas]
        Yvar = Yvar[feas]
        Y_obj = Y_obj[feas]
        indx_frontier = indx_frontier[feas]

    if Y.shape[0] == 0:
        # if there are no feasible points that are better than the reference point
        # return empty tensors
        return Y, Yvar, indx_frontier

    # calculate pareto front with only objective outcomes:
    frontier_mask = is_non_dominated(Y_obj)

    # Apply masks
    Y_frontier = Y[frontier_mask]
    Yvar_frontier = Yvar[frontier_mask]
    indx_frontier = indx_frontier[frontier_mask]
    return Y_frontier, Yvar_frontier, indx_frontier
Example #17
0
def sample_points_around_best(
    acq_function: AcquisitionFunction,
    n_discrete_points: int,
    sigma: float,
    bounds: Tensor,
    best_pct: float = 5.0,
    subset_sigma: float = 1e-1,
    prob_perturb: Optional[float] = None,
) -> Optional[Tensor]:
    r"""Find best points and sample nearby points.

    Args:
        acq_function: The acquisition function.
        n_discrete_points: The number of points to sample.
        sigma: The standard deviation of the additive gaussian noise for
            perturbing the best points.
        bounds: A `2 x d`-dim tensor containing the bounds.
        best_pct: The percentage of best points to perturb.
        subset_sigma: The standard deviation of the additive gaussian
            noise for perturbing a subset of dimensions of the best points.
        prob_perturb: The probability of perturbing each dimension.

    Returns:
        An optional `n_discrete_points x d`-dim tensor containing the
            sampled points. This is None if no baseline points are found.
    """
    X = get_X_baseline(acq_function=acq_function)
    if X is None:
        return
    with torch.no_grad():
        try:
            posterior = acq_function.model.posterior(X)
        except AttributeError:
            warnings.warn(
                "Failed to sample around previous best points.",
                BotorchWarning,
            )
            return
        mean = posterior.mean
        while mean.ndim > 2:
            # take average over batch dims
            mean = mean.mean(dim=0)
        try:
            f_pred = acq_function.objective(mean)
        # Some acquisition functions do not have an objective
        # and for some acquisition functions the objective is None
        except (AttributeError, TypeError):
            f_pred = mean
        if hasattr(acq_function, "maximize"):
            # make sure that the optimiztaion direction is set properly
            if not acq_function.maximize:
                f_pred = -f_pred
        try:
            # handle constraints for EHVI-based acquisition functions
            constraints = acq_function.constraints
            if constraints is not None:
                neg_violation = -torch.stack(
                    [c(mean).clamp_min(0.0) for c in constraints], dim=-1
                ).sum(dim=-1)
                feas = neg_violation == 0
                if feas.any():
                    f_pred[~feas] = float("-inf")
                else:
                    # set objective equal to negative violation
                    f_pred = neg_violation
        except AttributeError:
            pass
        if f_pred.ndim == mean.ndim and f_pred.shape[-1] > 1:
            # multi-objective
            # find pareto set
            is_pareto = is_non_dominated(f_pred)
            best_X = X[is_pareto]
        else:
            if f_pred.shape[-1] == 1:
                f_pred = f_pred.squeeze(-1)
            n_best = max(1, round(X.shape[0] * best_pct / 100))
            # the view() is to ensure that best_idcs is not a scalar tensor
            best_idcs = torch.topk(f_pred, n_best).indices.view(-1)
            best_X = X[best_idcs]
    n_trunc_normal_points = (
        n_discrete_points // 2 if best_X.shape[-1] >= 20 else n_discrete_points
    )
    perturbed_X = sample_truncated_normal_perturbations(
        X=best_X,
        n_discrete_points=n_trunc_normal_points,
        sigma=sigma,
        bounds=bounds,
    )
    if best_X.shape[-1] > 20 or prob_perturb is not None:
        perturbed_subset_dims_X = sample_perturbed_subset_dims(
            X=best_X,
            bounds=bounds,
            # ensure that we return n_discrete_points
            n_discrete_points=n_discrete_points - n_trunc_normal_points,
            sigma=sigma,
            prob_perturb=prob_perturb,
        )
        perturbed_X = torch.cat([perturbed_X, perturbed_subset_dims_X], dim=0)
        # shuffle points
        perm = torch.randperm(perturbed_X.shape[0], device=X.device)
        perturbed_X = perturbed_X[perm]
    return perturbed_X
Example #18
0
def _pad_batch_pareto_frontier(
    Y: Tensor,
    ref_point: Tensor,
    is_pareto: bool = False,
    feasibility_mask: Optional[Tensor] = None,
) -> Tensor:
    r"""Get a batch Pareto frontier by padding the pareto frontier with repeated points.

    This assumes maximization.

    Args:
        Y: A `(batch_shape) x n x m`-dim tensor of points
        ref_point: a `(batch_shape) x m`-dim tensor containing the reference point
        is_pareto: a boolean indicating whether the points in Y are already
            non-dominated.
        feasibility_mask: A `(batch_shape) x n`-dim tensor of booleans indicating
            whether each point is feasible.

    Returns:
        A `(batch_shape) x max_num_pareto x m`-dim tensor of padded Pareto
            frontiers.
    """
    tkwargs = {"dtype": Y.dtype, "device": Y.device}
    ref_point = ref_point.unsqueeze(-2)
    batch_shape = Y.shape[:-2]
    if len(batch_shape) > 1:
        raise UnsupportedError(
            "_pad_batch_pareto_frontier only supports a single "
            f"batch dimension, but got {len(batch_shape)} "
            "batch dimensions.")
    if feasibility_mask is not None:
        # set infeasible points to be the reference point (corresponding to the batch)
        Y = torch.where(feasibility_mask.unsqueeze(-1), Y, ref_point)
    if not is_pareto:
        pareto_mask = is_non_dominated(Y)
    else:
        pareto_mask = torch.ones(Y.shape[:-1],
                                 dtype=torch.bool,
                                 device=Y.device)
    better_than_ref = (Y > ref_point).all(dim=-1)
    # is_non_dominated assumes maximization
    # TODO: filter out points that are worse than the reference point first here
    pareto_mask = pareto_mask & better_than_ref
    if len(batch_shape) == 0:
        return Y[pareto_mask]
    # Note: in the batch case, the Pareto frontier is padded by repeating
    # a Pareto point. This ensures that the padded box-decomposition has
    # the same number of points, which enables fast batch operations.
    max_n_pareto = pareto_mask.sum(dim=-1).max().item()
    pareto_Y = torch.empty(*batch_shape, max_n_pareto, Y.shape[-1], **tkwargs)
    for i, pareto_i in enumerate(pareto_mask):
        pareto_i = Y[i, pareto_mask[i]]
        n_pareto = pareto_i.shape[0]
        if n_pareto > 0:
            pareto_Y[i, :n_pareto] = pareto_i
            # pad pareto_Y, so that all batches have the same size Pareto set
            pareto_Y[i, n_pareto:] = pareto_i[-1]
        else:
            # if there are no pareto points in this batch, use the reference
            # point
            pareto_Y[i, :] = ref_point[i]
    return pareto_Y
Example #19
0
    print(f"\nTrial {trial:>2} of {N_TRIALS} ", end="")
    hvs_qparego, hvs_qehvi, hvs_random = [], [], []

    # call helper functions to generate initial training data and initialize model
    train_x_qparego, train_obj_qparego = generate_initial_data(n=6)
    mll_qparego, model_qparego = initialize_model(train_x_qparego,
                                                  train_obj_qparego)

    train_x_qehvi, train_obj_qehvi = train_x_qparego, train_obj_qparego
    train_x_random, train_obj_random = train_x_qparego, train_obj_qparego
    # compute hypervolume
    mll_qehvi, model_qehvi = initialize_model(train_x_qehvi, train_obj_qehvi)

    # compute pareto front
    pareto_mask = is_non_dominated(train_obj_qparego)
    pareto_y = train_obj_qparego[pareto_mask]
    # compute hypervolume

    volume = hv.compute(pareto_y)

    hvs_qparego.append(volume)
    hvs_qehvi.append(volume)
    hvs_random.append(volume)

    # run N_BATCH rounds of BayesOpt after the initial random batch
    for iteration in range(1, N_BATCH + 1):

        t0 = time.time()

        # fit the models