Skip to content

Commit a546b5e

Browse files
saitcakmakfacebook-github-bot
authored andcommitted
Add a helper for evaluating feasibility of a set of points (#2565)
Summary: Adds a helper for evaluating the feasibility of intra-point parameter constraints on a given tensor. Differential Revision: D63909338
1 parent df93789 commit a546b5e

File tree

2 files changed

+150
-1
lines changed

2 files changed

+150
-1
lines changed

botorch/optim/parameter_constraints.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from __future__ import annotations
1212

1313
from collections.abc import Callable
14-
1514
from functools import partial
1615
from typing import Union
1716

@@ -26,6 +25,7 @@
2625
str, Union[str, Callable[[np.ndarray], float], Callable[[np.ndarray], np.ndarray]]
2726
]
2827
NLC_TOL = -1e-6
28+
INTRA_POINT_CONST_ERR: str = "Only intra-point constraints are supported."
2929

3030

3131
def make_scipy_bounds(
@@ -601,3 +601,58 @@ def make_scipy_nonlinear_inequality_constraints(
601601
shapeX=shapeX,
602602
)
603603
return scipy_nonlinear_inequality_constraints
604+
605+
606+
def evaluate_feasibility(
607+
X: Tensor,
608+
inequality_constraints: list[tuple[Tensor, Tensor, float]] | None = None,
609+
equality_constraints: list[tuple[Tensor, Tensor, float]] | None = None,
610+
nonlinear_inequality_constraints: list[tuple[Callable, bool]] | None = None,
611+
) -> Tensor:
612+
r"""Evaluate feasibility of a set of points. Only supports intra-point constraints.
613+
614+
Args:
615+
X: A tensor of points of shape `batch_shape x d`.
616+
inequality_constraints: A list of tuples (indices, coefficients, rhs),
617+
with each tuple encoding an inequality constraint of the form
618+
`\sum_i (X[indices[i]] * coefficients[i]) >= rhs`. `indices` and
619+
`coefficients` should be torch tensors. See the docstring of
620+
`make_scipy_linear_constraints` for an example.
621+
equality_constraints: A list of tuples (indices, coefficients, rhs),
622+
with each tuple encoding an equality constraint of the form
623+
`\sum_i (X[indices[i]] * coefficients[i]) = rhs`. See the docstring of
624+
`make_scipy_linear_constraints` for an example.
625+
nonlinear_inequality_constraints: A list of tuples representing the nonlinear
626+
inequality constraints. The first element in the tuple is a callable
627+
representing a constraint of the form `callable(x) >= 0`. The `callable()`
628+
takes in an one-dimensional tensor of shape `d` and returns a scalar. The
629+
second element is a boolean, indicating if it is an intra-point or
630+
inter-point constraint (`True` for intra-point. `False` for
631+
inter-point). Only `True` is supported here. For more information on
632+
intra-point vs inter-point constraints, see the docstring of the
633+
`inequality_constraints` argument to `optimize_acqf()`.
634+
635+
Returns:
636+
A boolean tensor of shape `batch` denoting whether each point is feasible.
637+
"""
638+
is_feasible = torch.ones(X.shape[:-1], device=X.device, dtype=torch.bool)
639+
if inequality_constraints is not None:
640+
for idx, coef, rhs in inequality_constraints:
641+
if idx.ndim != 1:
642+
raise UnsupportedError(INTRA_POINT_CONST_ERR)
643+
is_feasible &= (X[..., idx] * coef).sum(dim=-1) >= rhs
644+
if equality_constraints is not None:
645+
for idx, coef, rhs in equality_constraints:
646+
if idx.ndim != 1:
647+
raise UnsupportedError(INTRA_POINT_CONST_ERR)
648+
is_feasible &= (X[..., idx] * coef).sum(dim=-1) == rhs
649+
if nonlinear_inequality_constraints is not None:
650+
for const, intra in nonlinear_inequality_constraints:
651+
if not intra:
652+
raise UnsupportedError(INTRA_POINT_CONST_ERR)
653+
is_feasible &= torch.tensor(
654+
[const(x) >= NLC_TOL for x in X.view(-1, X.shape[-1])],
655+
device=X.device,
656+
dtype=torch.bool,
657+
).view_as(is_feasible)
658+
return is_feasible

test/optim/test_parameter_constraints.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
_make_linear_constraints,
1818
_make_nonlinear_constraints,
1919
eval_lin_constraint,
20+
evaluate_feasibility,
21+
INTRA_POINT_CONST_ERR,
2022
lin_constraint_jac,
2123
make_scipy_bounds,
2224
make_scipy_linear_constraints,
@@ -528,6 +530,98 @@ def test_generate_unfixed_lin_constraints(self):
528530
eq=eq,
529531
)
530532

533+
def test_evaluate_feasibility_intra_point_checks(self) -> None:
534+
# Check that `evaluate_feasibility` raises an error if inter-point
535+
# constraints are used.
536+
X = torch.ones(3, 2, device=self.device)
537+
inter_cons = (
538+
torch.tensor([[0, 0], [1, 0]], device=self.device),
539+
torch.tensor([1.0, -1.0], device=self.device),
540+
0,
541+
)
542+
for const_arg in (
543+
{"inequality_constraints": [inter_cons]},
544+
{"equality_constraints": [inter_cons]},
545+
{"nonlinear_inequality_constraints": [(None, False)]},
546+
):
547+
with self.assertRaisesRegex(UnsupportedError, INTRA_POINT_CONST_ERR):
548+
evaluate_feasibility(X=X, **const_arg)
549+
550+
def test_evaluate_feasibility(self) -> None:
551+
# Check that the feasibility is evaluated correctly.
552+
X = torch.tensor(
553+
[
554+
[[1.0, 1.0, 1.0]],
555+
[[1.0, 1.0, 3.0]],
556+
[[2.0, 2.0, 1.0]],
557+
[[2.0, 2.0, 5.0]],
558+
[[3.0, 3.0, 3.0]],
559+
],
560+
device=self.device,
561+
)
562+
# X[..., 2] * 4 >= 5.
563+
inequality_constraints = [
564+
(
565+
torch.tensor([2], device=self.device),
566+
torch.tensor([4], device=self.device),
567+
5.0,
568+
)
569+
]
570+
# X[..., 0] + X[..., 1] == 4.
571+
equality_constraints = [
572+
(
573+
torch.tensor([0, 1], device=self.device),
574+
torch.ones(2, device=self.device),
575+
4.0,
576+
)
577+
]
578+
579+
# sum(X, dim=-1) < 4.
580+
def nlc1(x):
581+
return 4 - x.sum(dim=-1)
582+
583+
# Only inequality.
584+
self.assertAllClose(
585+
evaluate_feasibility(
586+
X=X,
587+
inequality_constraints=inequality_constraints,
588+
),
589+
torch.tensor(
590+
[[False], [True], [False], [True], [True]], device=self.device
591+
),
592+
)
593+
# Only equality.
594+
self.assertAllClose(
595+
evaluate_feasibility(
596+
X=X,
597+
equality_constraints=equality_constraints,
598+
),
599+
torch.tensor(
600+
[[False], [False], [True], [True], [False]], device=self.device
601+
),
602+
)
603+
# Both inequality and equality.
604+
self.assertAllClose(
605+
evaluate_feasibility(
606+
X=X,
607+
inequality_constraints=inequality_constraints,
608+
equality_constraints=equality_constraints,
609+
),
610+
torch.tensor(
611+
[[False], [False], [False], [True], [False]], device=self.device
612+
),
613+
)
614+
# Nonlinear inequality.
615+
self.assertAllClose(
616+
evaluate_feasibility(
617+
X=X,
618+
nonlinear_inequality_constraints=[(nlc1, True)],
619+
),
620+
torch.tensor(
621+
[[True], [False], [False], [False], [False]], device=self.device
622+
),
623+
)
624+
531625

532626
class TestMakeScipyBounds(BotorchTestCase):
533627
def test_make_scipy_bounds(self):

0 commit comments

Comments
 (0)