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

Allow TRs to be created with empty/single region #783

Merged
merged 3 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
47 changes: 47 additions & 0 deletions tests/unit/acquisition/test_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -1274,6 +1274,41 @@ def test_trust_region_box_update_size(success: bool) -> None:
npt.assert_allclose(trb.upper, np.minimum(trb.location + trb.eps, search_space.upper))


# Check multi trust region works when no subspace is provided.
@pytest.mark.parametrize(
"rule, exp_num_subspaces",
[
(EfficientGlobalOptimization(), 1),
(EfficientGlobalOptimization(ParallelContinuousThompsonSampling(), num_query_points=2), 2),
(RandomSampling(num_query_points=2), 1),
],
)
def test_multi_trust_region_box_no_subspace(
rule: AcquisitionRule[TensorType, SearchSpace, ProbabilisticModel],
exp_num_subspaces: int,
) -> None:
search_space = Box([0.0, 0.0], [1.0, 1.0])
mtb = BatchTrustRegionBox(rule=rule)
mtb.acquire(search_space, {})

assert mtb._tags is not None
assert mtb._init_subspaces is not None
assert len(mtb._init_subspaces) == exp_num_subspaces
for i, (subspace, tag) in enumerate(zip(mtb._init_subspaces, mtb._tags)):
assert isinstance(subspace, SingleObjectiveTrustRegionBox)
assert subspace.global_search_space == search_space
assert tag == f"{i}"


# Check multi trust region works when a single subspace is provided.
def test_multi_trust_region_box_single_subspace() -> None:
search_space = Box([0.0, 0.0], [1.0, 1.0])
subspace = SingleObjectiveTrustRegionBox(search_space)
mtb = BatchTrustRegionBox(subspace) # type: ignore[var-annotated]
assert mtb._init_subspaces == (subspace,)
assert mtb._tags == ("0",)


# When state is None, acquire returns a multi search space of the correct type.
def test_multi_trust_region_box_acquire_no_state() -> None:
search_space = Box([0.0, 0.0], [1.0, 1.0])
Expand Down Expand Up @@ -1314,6 +1349,18 @@ def test_multi_trust_region_box_acquire_no_state() -> None:
assert point in subspace


def test_multi_trust_region_box_raises_on_mismatched_global_search_space() -> None:
search_space = Box([0.0, 0.0], [1.0, 1.0])
base_rule = EfficientGlobalOptimization( # type: ignore[var-annotated]
builder=ParallelContinuousThompsonSampling(), num_query_points=2
)
subspaces = [SingleObjectiveTrustRegionBox(search_space) for _ in range(2)]
mtb = BatchTrustRegionBox(subspaces, base_rule)

with pytest.raises(AssertionError, match="The global search space of the subspaces should "):
mtb.acquire(Box([0.0, 0.0], [2.0, 2.0]), {})


def test_multi_trust_region_box_raises_on_mismatched_tags() -> None:
search_space = Box([0.0, 0.0], [1.0, 1.0])
dataset = Dataset(
Expand Down
58 changes: 54 additions & 4 deletions trieste/acquisition/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -1166,19 +1166,30 @@ def __deepcopy__(self, memo: dict[int, object]) -> BatchTrustRegion.State:

def __init__(
self: "BatchTrustRegion[ProbabilisticModelType, UpdatableTrustRegionType]",
init_subspaces: Sequence[UpdatableTrustRegionType],
init_subspaces: Union[
None, UpdatableTrustRegionType, Sequence[UpdatableTrustRegionType]
] = None,
rule: AcquisitionRule[TensorType, SearchSpace, ProbabilisticModelType] | None = None,
):
"""
:param init_subspaces: The initial search spaces for each trust region.
:param init_subspaces: The initial search spaces for each trust region. If `None`, default
subspaces of type :class:`UpdatableTrustRegionType` will be created, with length
equal to the number of query points in the base `rule`.
:param rule: The acquisition rule that defines how to search for a new query point in each
subspace. Defaults to :class:`EfficientGlobalOptimization` with default arguments.
"""
if rule is None:
rule = EfficientGlobalOptimization()

self._init_subspaces = tuple(init_subspaces)
self._tags = tuple([str(index) for index in range(len(init_subspaces))])
# If init_subspaces are not provided, leave it to the subclasses to create them.
self._init_subspaces = None
self._tags = None
if init_subspaces is not None:
if not isinstance(init_subspaces, Sequence):
init_subspaces = [init_subspaces]
self._init_subspaces = tuple(init_subspaces)
self._tags = tuple([str(index) for index in range(len(init_subspaces))])

self._rule = rule

def __repr__(self) -> str:
Expand All @@ -1202,6 +1213,11 @@ def state_func(
Use the rule to acquire points from the acquisition space.
"""

# Subspaces should be set by the time we call `acquire`.
assert self._tags is not None
assert self._init_subspaces is not None

# If state is set, the tags should be the same as the tags of the acquisition space
# in the state.
if state is not None:
Expand Down Expand Up @@ -1399,6 +1415,40 @@ class BatchTrustRegionBox(BatchTrustRegion[ProbabilisticModelType, SingleObjecti
This is intended to be used for single-objective optimization with batching.
"""

def acquire(
self,
search_space: SearchSpace,
models: Mapping[Tag, ProbabilisticModelType],
datasets: Optional[Mapping[Tag, Dataset]] = None,
) -> types.State[BatchTrustRegion.State | None, TensorType]:
if self._init_subspaces is None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naive question: why is initialisation done here and not within __init__?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is because the subspaces we have (e.g. SingleObjectiveTrustRegionBox) need the global search space at initialisation time; which is not available in the rule __init__. I could have added it there as a required argument, but since that is already provided to acquire, I deferred that creation till then.

Note this does raise an important point. The subspaces take the reference to the global search space once (e.g. in first call to acquire). Theoretically the users can change the global search space to subsequent calls to acquire, but that would be ignored. I don't know how other rules deal with that or if that is even valid.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense!

Regarding the second point, if you change the global search space, then your trust region may become invalid. In that case it would make sense for the user to manually re-initialise it.

I guess we could either assert that the spaces match at every acquire call, or just ignore this corner case?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the case where users create the subspaces outside the class, the global search space passed to acquire is completely ignored -- it is only used for this default initialisation case. So the full strict check check would be to assert the search space in every call to acquire is equal to the one stored inside every subspace. I hope that check is not too restrictive. I'll go ahead and add that.

# If no initial subspaces were provided, create N default subspaces, where N is the
# number of query points in the base-rule.
# Currently the detection for N is only implemented for EGO.
# Note: the reason we don't create the default subspaces in `__init__` is because we
# don't have the global search space at that point.
if isinstance(self._rule, EfficientGlobalOptimization):
num_query_points = self._rule._num_query_points
else:
num_query_points = 1

self._init_subspaces = tuple(
[SingleObjectiveTrustRegionBox(search_space) for _ in range(num_query_points)]
)
self._tags = tuple([str(index) for index in range(len(self._init_subspaces))])

# Ensure passed in global search space is always the same as the search space passed to
# the subspaces.
for subspace in self._init_subspaces:
assert subspace.global_search_space == search_space, (
"The global search space of the subspaces should be the same as the "
"search space passed to the BatchTrustRegionBox acquisition rule. "
"If you want to change the global search space, you should recreate the rule. "
"Note: all subspaces should be initialized with the same global search space."
)

return super().acquire(search_space, models, datasets)

@inherit_check_shapes
def get_initialize_subspaces_mask(
self,
Expand Down
Loading