Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate constraints in optimize_acqf #1231

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add unit tests
Balandat committed May 22, 2022
commit b581ec1728a7de3e33578a5b1d4967e9869a5dcd
14 changes: 11 additions & 3 deletions botorch/optim/optimize.py
Original file line number Diff line number Diff line change
@@ -751,18 +751,22 @@ def _validate_constraints(
)
elif not (bounds.ndim == 2 and bounds.shape[0] == 2):
raise ValueError(
f"bounds should be a `2 x d` tensor, current shape: {list(bounds.shape)}."
f"bounds should be a `2 x d` tensor, current shape: {tuple(bounds.shape)}."
)
d = bounds.shape[-1]
bounds_lp, A_ub, b_ub, A_eq, b_eq = None, None, None, None, None
# The first `d` variables are `x`, the last `d` are the auxiliary `s`
if bounds.numel() > 0:
# `s` is unbounded
bounds_lp = [tuple(b_i) for b_i in bounds.t()] + [(None, None)] * d
# Encode the constraint `-x <= s <= x`
A_ub = np.zeros((2 * d, 2 * d))
b_ub = np.zeros(2 * d)
A_ub[:d, :d] = -1.0
A_ub[:d, d : 2 * d] = -1.0
A_ub[d : 2 * d, :d] = -1.0
A_ub[d : 2 * d, d : 2 * d] = 1.0
# Convet and add additional inequality constraints if present
if inequality_constraints is not None:
A_ineq = np.zeros((len(inequality_constraints), 2 * d))
b_ineq = np.zeros(len(inequality_constraints))
@@ -771,13 +775,16 @@ def _validate_constraints(
b_ineq[i] = -rhs
A_ub = np.concatenate((A_ub, A_ineq))
b_ub = np.concatenate((b_ub, b_ineq))
# Convert equality constraints if present
if equality_constraints is not None:
A_eq = np.zeros((len(equality_constraints), 2 * d))
b_eq = np.zeros(len(equality_constraints))
for i, (indices, coefficients, rhs) in enumerate(equality_constraints):
A_eq[i, indices] = coefficients
b_eq[i] = rhs
# Objective is `- sum_i s_i` (note: the `s_i` are guaranteed to be positive)
c = np.concatenate((np.zeros(d), -np.ones(d)))
# Solve the problem
result = linprog(
c=c,
bounds=bounds_lp,
@@ -786,13 +793,14 @@ def _validate_constraints(
A_eq=A_eq,
b_eq=b_eq,
)
# Check what's going on if unsuccessful
if not result.success:
if result.status == 2:
raise ValueError("Feasible set non-empty. Check your constraints")
raise ValueError("Feasible set non-empty. Check your constraints.")
if result.status == 3:
raise ValueError("Feasible set unbounded.")
warnings.warn(
"Ran into issus when checking for boundedness of feasible set. "
"Ran into issues when checking for boundedness of feasible set. "
f"Optimizer message: {result.message}."
)

61 changes: 61 additions & 0 deletions test/optim/test_optimize.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
_filter_invalid,
_gen_batch_initial_conditions_local_search,
_generate_neighbors,
_validate_constraints,
optimize_acqf,
optimize_acqf_cyclic,
optimize_acqf_discrete,
@@ -72,6 +73,66 @@ def rounding_func(X: Tensor) -> Tensor:


class TestOptimizeAcqf(BotorchTestCase):
def test_validate_constraints(self):
for dtype in (torch.float, torch.double):
tkwargs = {"device": self.device, "dtype": dtype}
with self.assertRaisesRegex(
UnsupportedError, "Must provide either `bounds` or `inequality_constraints`"
):
_validate_constraints(bounds=torch.empty(0, 2, **tkwargs))
with self.assertRaisesRegex(
# TODO: Figure out why the full rendered string doesn't regex-match
ValueError,
f"bounds should be a `2 x d` tensor, current shape:", # {(3, 2)}."
):
_validate_constraints(bounds=torch.zeros(3, 2), inequality_constraints=[])
# Check standard box bounds
bounds = torch.stack((torch.zeros(2, **tkwargs), torch.ones(2, **tkwargs)))
_validate_constraints(bounds=bounds)
# Check failure on empty box
with self.assertRaisesRegex(
ValueError, "Feasible set non-empty. Check your constraints."
):
_validate_constraints(bounds=bounds.flip(0))
# Check failure on unbounded "box"
bounds[1, 1] = float("inf")
with self.assertRaisesRegex(ValueError, "Feasible set unbounded."):
_validate_constraints(bounds=bounds)
# Check that added inequality constraint resolve this
_validate_constraints(
bounds=bounds,
inequality_constraints=[
(
torch.tensor([1], device=self.device),
torch.tensor([-1.0], **tkwargs),
-2.0,
)
],
)
# Check hat added equality constraint resolves this
_validate_constraints(
bounds=bounds,
equality_constraints=[
(
torch.tensor([0, 1], device=self.device),
torch.tensor([1.0, -1.0], **tkwargs),
0.0,
)
],
)
# Check that inequality constraints alone work
zero = torch.tensor([0], device=self.device)
one = torch.tensor([1], device=self.device)
inequality_constraints = [
(zero, torch.tensor([1.0], **tkwargs), 0.0),
(zero, torch.tensor([-1.0], **tkwargs), -1.0),
(one, torch.tensor([1.0], **tkwargs), 0.0),
(one, torch.tensor([-1.0], **tkwargs), -1.0),
]
_validate_constraints(
bounds=bounds, inequality_constraints=inequality_constraints
)

@mock.patch("botorch.optim.optimize.gen_batch_initial_conditions")
@mock.patch("botorch.optim.optimize.gen_candidates_scipy")
def test_optimize_acqf_joint(