Skip to content

Commit b16b28f

Browse files
ItsMrLinmeta-codesync[bot]
authored andcommitted
Fix tensor-valued fixed_features shape mismatch in infeasible projection (meta-pytorch#3259)
Summary: Pull Request resolved: meta-pytorch#3259 In `_optimize_acqf_batch`, when projecting infeasible candidates back to the feasible set via `project_to_feasible_space_via_slsqp`, the code passed `opt_inputs.fixed_features` directly. However, when called from `continuous_step` in `optimize_acqf_mixed_alternating`, `fixed_features` contains tensor-valued entries of shape `(b,)` — one value per candidate for each discrete/categorical dimension. Since only the infeasible subset `batch_candidates[infeasible]` is passed as `X`, there's a shape mismatch when `fix_features` tries to assign the full tensor to the reduced batch. For example, with `b=4` candidates and only 1 infeasible, the tensor-valued fixed features have shape `(4,)` but the target has shape `(1,)`, causing `RuntimeError: The expanded size of the tensor (1) must match the existing size (4)`. The fix filters tensor-valued fixed feature entries by the `infeasible` boolean mask before passing to `project_to_feasible_space_via_slsqp`, so shapes match. Reviewed By: bletham Differential Revision: D99209740 fbshipit-source-id: faffb9833c160ae629843c859bebde1df9a044b1
1 parent 7b2c668 commit b16b28f

2 files changed

Lines changed: 119 additions & 1 deletion

File tree

botorch/optim/optimize.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,12 +552,22 @@ def _optimize_batch_candidates() -> tuple[Tensor, Tensor, list[Warning]]:
552552
)
553553
infeasible = ~is_feasible
554554
if nonlinear_inequality_constraints is None and infeasible.any():
555+
# Filter tensor-valued fixed features to match the infeasible subset.
556+
# This is needed because fixed_features may contain per-element tensor
557+
# values (e.g., from continuous_step in mixed optimization), and we
558+
# must subset them to match batch_candidates[infeasible].
559+
fixed_features = opt_inputs.fixed_features
560+
if fixed_features is not None:
561+
fixed_features = {
562+
k: v[infeasible] if torch.is_tensor(v) and v.ndim > 0 else v
563+
for k, v in fixed_features.items()
564+
}
555565
projected_candidates = project_to_feasible_space_via_slsqp(
556566
X=batch_candidates[infeasible],
557567
bounds=opt_inputs.bounds,
558568
equality_constraints=equality_constraints,
559569
inequality_constraints=inequality_constraints,
560-
fixed_features=opt_inputs.fixed_features,
570+
fixed_features=fixed_features,
561571
)
562572
if opt_inputs.post_processing_func is not None:
563573
projected_candidates = opt_inputs.post_processing_func(projected_candidates)

test/optim/test_optimize.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
_filter_invalid,
4040
_gen_batch_initial_conditions_local_search,
4141
_generate_neighbors,
42+
_optimize_acqf_batch,
4243
gen_batch_initial_conditions,
4344
optimize_acqf,
4445
optimize_acqf_cyclic,
@@ -1885,6 +1886,113 @@ def check_mixed_constraints(candidates):
18851886
)
18861887

18871888

1889+
class TestTensorValuedFixedFeaturesProjection(BotorchTestCase):
1890+
"""Regression test for tensor-valued fixed_features with infeasible projection.
1891+
1892+
When _optimize_acqf_batch projects infeasible candidates via
1893+
project_to_feasible_space_via_slsqp, it must filter tensor-valued
1894+
fixed_features to match the infeasible subset. Previously, the full
1895+
tensor was passed, causing a shape mismatch RuntimeError.
1896+
"""
1897+
1898+
@mock.patch(
1899+
"botorch.optim.optimize.project_to_feasible_space_via_slsqp",
1900+
wraps=project_to_feasible_space_via_slsqp,
1901+
)
1902+
@mock.patch("botorch.generation.gen.minimize_with_timeout")
1903+
def test_projection_with_tensor_valued_fixed_features(
1904+
self,
1905+
mock_minimize: mock.Mock,
1906+
mock_project: mock.Mock,
1907+
) -> None:
1908+
num_restarts = 4
1909+
q = 1
1910+
d = 3
1911+
dtype = torch.double
1912+
1913+
mock_acq_function = MockAcquisitionFunction()
1914+
bounds = torch.zeros(2, d, dtype=dtype, device=self.device)
1915+
bounds[1] = 2.0
1916+
1917+
# Create initial conditions: 4 restarts, q=1, d=3
1918+
batch_ics = torch.ones(num_restarts, q, d, dtype=dtype, device=self.device)
1919+
1920+
# Mock optimizer to return candidates where some violate the
1921+
# constraint x[0] + x[1] >= 1.5. With max_aggregation_size=1,
1922+
# minimize is called once per restart, each getting a reduced
1923+
# 2D input (d=3 minus 1 fixed = 2 free dims).
1924+
call_count = 0
1925+
1926+
def side_effect(*args, **kwargs):
1927+
nonlocal call_count
1928+
call_count += 1
1929+
if call_count in (1, 3):
1930+
# Infeasible: x[0]=0.3, x[1]=0.3 => sum=0.6 < 1.5
1931+
x = np.array([0.3, 0.3])
1932+
else:
1933+
# Feasible: x[0]=1.0, x[1]=1.0 => sum=2.0 >= 1.5
1934+
x = np.array([1.0, 1.0])
1935+
return OptimizeResult(x=x, success=True, status=0)
1936+
1937+
mock_minimize.side_effect = side_effect
1938+
1939+
# Tensor-valued fixed features (one value per restart), as
1940+
# created by continuous_step for discrete dims.
1941+
tensor_fixed_features = {
1942+
2: torch.tensor([0.5, 0.6, 0.7, 0.8], dtype=dtype, device=self.device),
1943+
}
1944+
1945+
inequality_constraints = [
1946+
(
1947+
torch.tensor([0, 1], dtype=torch.long, device=self.device),
1948+
torch.tensor([1.0, 1.0], dtype=dtype, device=self.device),
1949+
1.5,
1950+
)
1951+
]
1952+
1953+
# Before the fix, this would raise:
1954+
# RuntimeError: The expanded size of the tensor (1) must match the
1955+
# existing size (4) at non-singleton dimension 0.
1956+
opt_inputs = OptimizeAcqfInputs(
1957+
acq_function=mock_acq_function,
1958+
bounds=bounds,
1959+
q=q,
1960+
num_restarts=num_restarts,
1961+
raw_samples=None,
1962+
options={
1963+
"batch_limit": num_restarts,
1964+
"max_optimization_problem_aggregation_size": 1,
1965+
},
1966+
inequality_constraints=inequality_constraints,
1967+
equality_constraints=None,
1968+
nonlinear_inequality_constraints=None,
1969+
fixed_features=tensor_fixed_features,
1970+
post_processing_func=None,
1971+
batch_initial_conditions=batch_ics,
1972+
return_best_only=False,
1973+
gen_candidates=gen_candidates_scipy,
1974+
sequential=False,
1975+
)
1976+
1977+
candidates, acq_values = _optimize_acqf_batch(opt_inputs=opt_inputs)
1978+
self.assertEqual(candidates.shape, (num_restarts, q, d))
1979+
1980+
# Verify projection was called with correctly subsetted fixed features.
1981+
if mock_project.called:
1982+
call_kwargs = mock_project.call_args
1983+
ff = call_kwargs.kwargs.get(
1984+
"fixed_features", call_kwargs[1].get("fixed_features")
1985+
)
1986+
if ff is not None:
1987+
X_arg = (
1988+
call_kwargs.args[0] if call_kwargs.args else call_kwargs.kwargs["X"]
1989+
)
1990+
n_infeasible = X_arg.shape[0]
1991+
for v in ff.values():
1992+
if torch.is_tensor(v) and v.ndim > 0:
1993+
self.assertEqual(v.shape[0], n_infeasible)
1994+
1995+
18881996
class TestAllOptimizers(BotorchTestCase):
18891997
@mock_optimize
18901998
def test_negative_fixed_features(self) -> None:

0 commit comments

Comments
 (0)