Skip to content

Commit 1518b30

Browse files
AVHoppfacebook-github-bot
authored andcommitted
Update documentation and error handling for inter-point constraints (#3003)
Summary: ## Motivation This PR improves the documentation and error handling when attempting to use inter-point constraints within `optimize_acqf_mixed` which is not supported. It implements the solution to #2996 discussed in this issue. ### Have you read the [Contributing Guidelines on pull requests](https://github.com/pytorch/botorch/blob/main/CONTRIBUTING.md#pull-requests)? Yes. Pull Request resolved: #3003 Test Plan: I verified my code by ensuring that the example posted in #2996 raises an error message. I did not add any tests, but would be happy to turn that example into a dedicated test if being pointed to the correct place for doing so. EDIT: A minimal test has been added in 92140e9 ## Related PRs None. Reviewed By: saitcakmak Differential Revision: D81792795 Pulled By: Balandat fbshipit-source-id: d54a7d225170f17049ea588d699e4345222afb4c
1 parent 8085458 commit 1518b30

File tree

2 files changed

+113
-23
lines changed

2 files changed

+113
-23
lines changed

botorch/optim/optimize.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,9 @@ def optimize_acqf_mixed(
10551055
For q > 1 this function always performs sequential greedy optimization (with
10561056
proper conditioning on generated candidates).
10571057
1058+
NOTE: This method does not support the kind of "inter-point constraints" that
1059+
are supported by `optimize_acqf()`.
1060+
10581061
Args:
10591062
acq_function: An AcquisitionFunction
10601063
bounds: A `2 x d` tensor of lower and upper bounds for each column of `X`
@@ -1076,19 +1079,12 @@ def optimize_acqf_mixed(
10761079
`\sum_i (X[indices[i]] * coefficients[i]) = rhs`
10771080
nonlinear_inequality_constraints: A list of tuples representing the nonlinear
10781081
inequality constraints. The first element in the tuple is a callable
1079-
representing a constraint of the form `callable(x) >= 0`. In case of an
1080-
intra-point constraint, `callable()`takes in an one-dimensional tensor of
1081-
shape `d` and returns a scalar. In case of an inter-point constraint,
1082-
`callable()` takes a two dimensional tensor of shape `q x d` and again
1083-
returns a scalar. The second element is a boolean, indicating if it is an
1082+
representing a constraint of the form `callable(x) >= 0`. The `callable()`
1083+
takes in an one-dimensional tensor of shape `d` and returns a scalar.
1084+
The second element is a boolean, indicating if it is an
10841085
intra-point or inter-point constraint (`True` for intra-point. `False` for
1085-
inter-point). For more information on intra-point vs inter-point
1086-
constraints, see the docstring of the `inequality_constraints` argument to
1087-
`optimize_acqf()`. The constraints will later be passed to the scipy
1088-
solver. You need to pass in `batch_initial_conditions` in this case.
1089-
Using non-linear inequality constraints also requires that `batch_limit`
1090-
is set to 1, which will be done automatically if not specified in
1091-
`options`.
1086+
inter-point). Since inter-point constraints are not supported by this
1087+
method, this has to be `True` and raises an error if being `False`.
10921088
post_processing_func: A function that post-processes an optimization
10931089
result appropriately (i.e., according to `round-trip`
10941090
transformations).
@@ -1123,6 +1119,27 @@ def optimize_acqf_mixed(
11231119
- a tensor of associated acquisition values of dim `num_restarts`
11241120
if `return_best_only=False` else a scalar acquisition value.
11251121
"""
1122+
const_err_message = (
1123+
"Inter-point constraints are not supported for sequential optimization. "
1124+
"But the {}th {} constraint is defined as inter-point."
1125+
)
1126+
# Check for existence of inter-point constraints
1127+
# Code adapted from _validate_sequential_inputs
1128+
if inequality_constraints is not None:
1129+
for i, (indices, _, _) in enumerate(inequality_constraints):
1130+
if indices.ndim > 1:
1131+
raise UnsupportedError(const_err_message.format(i, "linear inequality"))
1132+
if equality_constraints is not None:
1133+
for i, (indices, _, _) in enumerate(equality_constraints):
1134+
if indices.ndim > 1:
1135+
raise UnsupportedError(const_err_message.format(i, "linear equality"))
1136+
if nonlinear_inequality_constraints is not None:
1137+
for i, (_, intra_point) in enumerate(nonlinear_inequality_constraints):
1138+
if not intra_point:
1139+
raise UnsupportedError(
1140+
const_err_message.format(i, "non-linear inequality")
1141+
)
1142+
11261143
if not return_best_only and q > 1:
11271144
raise NotImplementedError("`return_best_only=False` is only supported for q=1.")
11281145

@@ -1283,8 +1300,7 @@ def optimize_acqf_discrete(
12831300
"""
12841301
if isinstance(acq_function, OneShotAcquisitionFunction):
12851302
raise UnsupportedError(
1286-
"Discrete optimization is not supported for"
1287-
"one-shot acquisition functions."
1303+
"Discrete optimization is not supported for one-shot acquisition functions."
12881304
)
12891305
if X_avoid is not None and unique:
12901306
choices = _filter_invalid(X=choices, X_avoid=X_avoid)

test/optim/test_optimize.py

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,15 +1649,18 @@ def test_optimize_acqf_list(self, mock_optimize_acqf, mock_optimize_acqf_mixed):
16491649
mock_optimize_acqf_mixed.side_effect = side_effect
16501650
orig_candidates = candidate_rvs[0].clone()
16511651
# Wrap the set_X_pending method for checking that call arguments
1652-
with mock.patch.object(
1653-
MockAcquisitionFunction,
1654-
"set_X_pending",
1655-
wraps=mock_acq_function_1.set_X_pending,
1656-
) as mock_set_X_pending_1, mock.patch.object(
1657-
MockAcquisitionFunction,
1658-
"set_X_pending",
1659-
wraps=mock_acq_function_2.set_X_pending,
1660-
) as mock_set_X_pending_2:
1652+
with (
1653+
mock.patch.object(
1654+
MockAcquisitionFunction,
1655+
"set_X_pending",
1656+
wraps=mock_acq_function_1.set_X_pending,
1657+
) as mock_set_X_pending_1,
1658+
mock.patch.object(
1659+
MockAcquisitionFunction,
1660+
"set_X_pending",
1661+
wraps=mock_acq_function_2.set_X_pending,
1662+
) as mock_set_X_pending_2,
1663+
):
16611664
candidates, _ = optimize_acqf_list(
16621665
acq_function_list=mock_acq_function_list[:num_acqf],
16631666
bounds=bounds,
@@ -1934,6 +1937,77 @@ def test_optimize_acqf_mixed_empty_ff(self):
19341937
raw_samples=10,
19351938
)
19361939

1940+
def test_optimize_acqf_mixed_inter_point_inequality_constraints(self):
1941+
mock_acq_function = MockAcquisitionFunction()
1942+
with self.assertRaisesRegex(
1943+
UnsupportedError,
1944+
expected_regex="Inter-point constraints are not supported for sequential "
1945+
"optimization. But the 0th linear inequality constraint is defined "
1946+
"as inter-point.",
1947+
):
1948+
optimize_acqf_mixed(
1949+
acq_function=mock_acq_function,
1950+
q=1,
1951+
fixed_features_list=[{0: 0.0}],
1952+
bounds=torch.stack([torch.zeros(3), 4 * torch.ones(3)]),
1953+
num_restarts=2,
1954+
raw_samples=10,
1955+
inequality_constraints=[
1956+
( # Inter-point constraint: X[0, 0] - X[1, 0] >= 0
1957+
torch.tensor([[0, 0], [1, 0]], dtype=torch.long),
1958+
torch.tensor([1.0, -1.0]),
1959+
0.0,
1960+
)
1961+
],
1962+
)
1963+
1964+
def test_optimize_acqf_mixed_inter_point_equality_constraints(self):
1965+
mock_acq_function = MockAcquisitionFunction()
1966+
with self.assertRaisesRegex(
1967+
UnsupportedError,
1968+
expected_regex="Inter-point constraints are not supported for sequential "
1969+
"optimization. But the 0th linear equality constraint is defined "
1970+
"as inter-point.",
1971+
):
1972+
optimize_acqf_mixed(
1973+
acq_function=mock_acq_function,
1974+
q=1,
1975+
fixed_features_list=[{0: 0.0}],
1976+
bounds=torch.stack([torch.zeros(3), 4 * torch.ones(3)]),
1977+
num_restarts=2,
1978+
raw_samples=10,
1979+
equality_constraints=[
1980+
( # Inter-point constraint: X[0, 0] - X[1, 0] == 0
1981+
torch.tensor([[0, 0], [1, 0]], dtype=torch.long),
1982+
torch.tensor([1.0, -1.0]),
1983+
0.0,
1984+
)
1985+
],
1986+
)
1987+
1988+
def test_optimize_acqf_mixed_inter_point_nonlinear_constraints(self):
1989+
mock_acq_function = MockAcquisitionFunction()
1990+
with self.assertRaisesRegex(
1991+
UnsupportedError,
1992+
expected_regex="Inter-point constraints are not supported for sequential "
1993+
"optimization. But the 0th non-linear inequality constraint is defined "
1994+
"as inter-point.",
1995+
):
1996+
optimize_acqf_mixed(
1997+
acq_function=mock_acq_function,
1998+
q=1,
1999+
fixed_features_list=[{0: 0.0}],
2000+
bounds=torch.stack([torch.zeros(3), 4 * torch.ones(3)]),
2001+
num_restarts=2,
2002+
raw_samples=10,
2003+
nonlinear_inequality_constraints=[
2004+
( # Inter-point constraint: sum of all points >= 0
2005+
lambda X: X.sum(dim=(-1, -2)),
2006+
False, # False indicates inter-point constraint
2007+
)
2008+
],
2009+
)
2010+
19372011
def test_optimize_acqf_mixed_return_best_only_q2(self):
19382012
mock_acq_function = MockAcquisitionFunction()
19392013
with self.assertRaisesRegex(

0 commit comments

Comments
 (0)