def test_optimize_acqf_empty_ff(self): with self.assertRaises(ValueError): mock_acq_function = MockAcquisitionFunction() optimize_acqf_mixed( acq_function=mock_acq_function, q=1, fixed_features_list=[], bounds=torch.stack([torch.zeros(3), 4 * torch.ones(3)]), num_restarts=2, raw_samples=10, )
def test_optimize_acqf_one_shot_large_q(self): with self.assertRaises(ValueError): mock_acq_function = MockOneShotAcquisitionFunction() fixed_features_list = [{i: i * 0.1} for i in range(2)] optimize_acqf_mixed( acq_function=mock_acq_function, q=2, fixed_features_list=fixed_features_list, bounds=torch.stack([torch.zeros(3), 4 * torch.ones(3)]), num_restarts=2, raw_samples=10, )
def test_optimize_acqf_mixed_q2(self, mock_optimize_acqf): num_restarts = 2 raw_samples = 10 q = 2 options = {} tkwargs = {"device": self.device} bounds = torch.stack([torch.zeros(3), 4 * torch.ones(3)]) mock_acq_functions = [ MockAcquisitionFunction(), MockOneShotEvaluateAcquisitionFunction(), ] for num_ff, dtype, mock_acq_function in itertools.product( [1, 3], (torch.float, torch.double), mock_acq_functions ): tkwargs["dtype"] = dtype mock_optimize_acqf.reset_mock() bounds = bounds.to(**tkwargs) fixed_features_list = [{i: i * 0.1} for i in range(num_ff)] candidate_rvs, exp_candidates, acq_val_rvs = [], [], [] # generate mock side effects and compute expected outputs for _ in range(q): candidate_rvs_q = [torch.rand(1, 3, **tkwargs) for _ in range(num_ff)] acq_val_rvs_q = [torch.rand(1, **tkwargs) for _ in range(num_ff)] best = torch.argmax(torch.stack(acq_val_rvs_q)) exp_candidates.append(candidate_rvs_q[best]) candidate_rvs += candidate_rvs_q acq_val_rvs += acq_val_rvs_q side_effect = list(zip(candidate_rvs, acq_val_rvs)) mock_optimize_acqf.side_effect = side_effect candidates, acq_value = optimize_acqf_mixed( acq_function=mock_acq_function, q=q, fixed_features_list=fixed_features_list, bounds=bounds, num_restarts=num_restarts, raw_samples=raw_samples, options=options, post_processing_func=rounding_func, ) expected_candidates = torch.cat(exp_candidates, dim=-2) if isinstance(mock_acq_function, MockOneShotEvaluateAcquisitionFunction): expected_acq_value = mock_acq_function.evaluate( expected_candidates, bounds=bounds ) else: expected_acq_value = mock_acq_function(expected_candidates) self.assertTrue(torch.equal(candidates, expected_candidates)) self.assertTrue(torch.equal(acq_value, expected_acq_value))
def test_optimize_acqf_mixed(self, mock_optimize_acqf): num_restarts = 2 raw_samples = 10 q = 1 options = {} tkwargs = {"device": self.device} bounds = torch.stack([torch.zeros(3), 4 * torch.ones(3)]) mock_acq_function = MockAcquisitionFunction() for num_ff, dtype in itertools.product([1, 3], (torch.float, torch.double)): tkwargs["dtype"] = dtype mock_optimize_acqf.reset_mock() bounds = bounds.to(**tkwargs) candidate_rvs = [] acq_val_rvs = [] gcs_return_vals = [(torch.rand(1, 3, **tkwargs), torch.rand(1, **tkwargs)) for _ in range(num_ff)] for rv in gcs_return_vals: candidate_rvs.append(rv[0]) acq_val_rvs.append(rv[1]) fixed_features_list = [{i: i * 0.1} for i in range(num_ff)] side_effect = list(zip(candidate_rvs, acq_val_rvs)) mock_optimize_acqf.side_effect = side_effect candidates, acq_value = optimize_acqf_mixed( acq_function=mock_acq_function, q=q, fixed_features_list=fixed_features_list, bounds=bounds, num_restarts=num_restarts, raw_samples=raw_samples, options=options, post_processing_func=rounding_func, ) # compute expected output ff_acq_values = torch.stack(acq_val_rvs) best = torch.argmax(ff_acq_values) expected_candidates = candidate_rvs[best] expected_acq_value = ff_acq_values[best] self.assertTrue(torch.equal(candidates, expected_candidates)) self.assertTrue(torch.equal(acq_value, expected_acq_value)) # check call arguments for optimize_acqf call_args_list = mock_optimize_acqf.call_args_list expected_call_args = { "acq_function": None, "bounds": bounds, "q": q, "num_restarts": num_restarts, "raw_samples": raw_samples, "options": options, "inequality_constraints": None, "equality_constraints": None, "fixed_features": None, "post_processing_func": rounding_func, "batch_initial_conditions": None, "return_best_only": True, "sequential": False, } for i in range(len(call_args_list)): expected_call_args["fixed_features"] = fixed_features_list[i] for k, v in call_args_list[i][1].items(): if torch.is_tensor(v): self.assertTrue(torch.equal(expected_call_args[k], v)) elif k == "acq_function": self.assertIsInstance(v, MockAcquisitionFunction) else: self.assertEqual(expected_call_args[k], v)
def optimize( self, n: int, search_space_digest: SearchSpaceDigest, inequality_constraints: Optional[List[Tuple[Tensor, Tensor, float]]] = None, fixed_features: Optional[Dict[int, float]] = None, rounding_func: Optional[Callable[[Tensor], Tensor]] = None, optimizer_options: Optional[Dict[str, Any]] = None, ) -> Tuple[Tensor, Tensor]: """Generate a set of candidates via multi-start optimization. Obtains candidates and their associated acquisition function values. Args: n: The number of candidates to generate. search_space_digest: A ``SearchSpaceDigest`` object containing search space properties, e.g. ``bounds`` for optimization. inequality constraints: A list of tuples (indices, coefficients, rhs), with each tuple encoding an inequality constraint of the form ``sum_i (X[indices[i]] * coefficients[i]) >= rhs``. fixed_features: A map `{feature_index: value}` for features that should be fixed to a particular value during generation. rounding_func: A function that post-processes an optimization result appropriately (i.e., according to `round-trip` transformations). optimizer_options: Options for the optimizer function, e.g. ``sequential`` or ``raw_samples``. """ # NOTE: Could make use of `optimizer_class` when its added to BoTorch # instead of calling `optimizer_acqf` or `optimize_acqf_discrete` etc. optimizer_options_with_defaults = { **get_default_optimizer_options(acqf_class=self.botorch_acqf_class), **(optimizer_options or {}), } ssd = search_space_digest tkwargs = {"dtype": self.dtype, "device": self.device} # pyre-fixme[6]: Expected `Optional[torch.dtype]` for 2nd param but got # `Union[torch.device, torch.dtype]`. bounds = torch.tensor(ssd.bounds, **tkwargs).transpose(0, 1) discrete_features = sorted(ssd.ordinal_features + ssd.categorical_features) if fixed_features is not None: for i in fixed_features: if not 0 <= i < len(ssd.feature_names): raise ValueError(f"Invalid fixed_feature index: {i}") # 1. Handle the fully continuous search space. if not discrete_features: return optimize_acqf( acq_function=self.acqf, bounds=bounds, q=n, inequality_constraints=inequality_constraints, fixed_features=fixed_features, post_processing_func=rounding_func, **optimizer_options_with_defaults, ) # 2. Handle search spaces with discrete features. discrete_choices = mk_discrete_choices(ssd=ssd, fixed_features=fixed_features) # 2a. Handle the fully discrete search space. if len(discrete_choices) == len(ssd.feature_names): # For now we just enumerate all possible discrete combinations. This is not # scalable and and only works for a reasonably small number of choices. all_choices = (discrete_choices[i] for i in range(len(discrete_choices))) # pyre-fixme[6]: Expected `Optional[torch.dtype]` for 2nd param but got # `Union[torch.device, torch.dtype]`. all_choices = torch.tensor(list(itertools.product(*all_choices)), **tkwargs) return optimize_acqf_discrete( acq_function=self.acqf, q=n, choices=all_choices, **optimizer_options_with_defaults, ) # 2b. Handle mixed search spaces that have discrete and continuous features. return optimize_acqf_mixed( acq_function=self.acqf, bounds=bounds, q=n, # For now we just enumerate all possible discrete combinations. This is not # scalable and and only works for a reasonably small number of choices. A # slowdown warning is logged in `enumerate_discrete_combinations` if needed. fixed_features_list=enumerate_discrete_combinations( discrete_choices=discrete_choices), inequality_constraints=inequality_constraints, post_processing_func=rounding_func, **optimizer_options_with_defaults, )