Skip to content

Commit f7fb066

Browse files
authored
Use BetaPrior in MultiTask Botorch Preset (#834)
This PR changes the `BOTORCH` preset to use the `BetaPrior(2.5, 1.5)` in the multi task case. This change was introduced in Botorch v.0.18.0 (see https://github.com/meta-pytorch/botorch/releases/tag/v0.18.0). Consequently, our `BOTORCH` preset no longer reflected the actual botorch defaults and failed. This PR thus updates the `BotorchKernelFactory` to not simply use `HVARFNER` but to use the new prior in the multi task case instead. Also, this changes the way that we use `botorch` in our tests. We now enforce `botorch>=0.18.0` whenever we test for python versions >3.10. We cannot enforce this for Python 3.10 due to the corresponding `botorch` version not being available for Python 3.10, hence preset equivalence test is skipped for this.
2 parents 5b028ec + 5c620a0 commit f7fb066

4 files changed

Lines changed: 141 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
### Changed
9+
- `BOTORCH` GP preset now includes `BetaPrior(2.5, 1.5)` for the task covariance
10+
kernel in multi-task scenarios, matching BoTorch's `MultiTaskGP` defaults introduced
11+
in version `0.18.0`
12+
- The `BOTORCH` GP preset now requires BoTorch `>= 0.18.0` and raises an
13+
`IncompatibilityError` if an older version is installed
14+
715
## [0.15.0] - 2026-06-11
816
### Breaking Changes
917
- `GaussianProcessSurrogate` no longer automatically adds a task kernel in multi-task

baybe/surrogates/gaussian_process/presets/botorch.py

Lines changed: 115 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
"""BoTorch preset for Gaussian process surrogates.
1+
"""BoTorch preset for Gaussian process surrogates."""
22

3-
Currently mimics the Hvarfner preset :cite:p:`Hvarfner2024`.
4-
"""
3+
from __future__ import annotations
54

6-
from baybe.surrogates.gaussian_process.presets.hvarfner import (
7-
FIT_CRITERION_FACTORY,
8-
KERNEL_FACTORY,
9-
LIKELIHOOD_FACTORY,
10-
MEAN_FACTORY,
5+
import gc
6+
from itertools import chain
7+
from typing import TYPE_CHECKING, ClassVar
8+
9+
import pandas as pd
10+
from attrs import define
11+
from typing_extensions import override
12+
13+
from baybe.kernels.base import Kernel
14+
from baybe.objectives.base import Objective
15+
from baybe.parameters.enum import _ParameterKind
16+
from baybe.searchspace.core import SearchSpace
17+
from baybe.surrogates.gaussian_process.components.fit_criterion import (
18+
FitCriterion,
19+
PlainFitCriterionFactory,
1120
)
12-
from baybe.surrogates.gaussian_process.presets.hvarfner import (
13-
HvarfnerKernelFactory as BotorchKernelFactory,
21+
from baybe.surrogates.gaussian_process.components.kernel import (
22+
ICMKernelFactory,
23+
_PureKernelFactory,
1424
)
1525
from baybe.surrogates.gaussian_process.presets.hvarfner import (
1626
HvarfnerLikelihoodFactory as BotorchLikelihoodFactory,
@@ -19,6 +29,101 @@
1929
HvarfnerMeanFactory as BotorchMeanFactory,
2030
)
2131

32+
if TYPE_CHECKING:
33+
from gpytorch.kernels import Kernel as GPyTorchKernel
34+
35+
# The minimum BoTorch version required for the preset
36+
_MIN_BOTORCH_VERSION = "0.18.0"
37+
38+
39+
@define
40+
class BotorchKernelFactory(_PureKernelFactory):
41+
"""A factory providing kernels matching BoTorch's :class:`~botorch.models.MultiTaskGP` defaults.""" # noqa: E501
42+
43+
_uses_parameter_names: ClassVar[bool] = True
44+
# See base class.
45+
46+
_supported_parameter_kinds: ClassVar[_ParameterKind] = (
47+
_ParameterKind.REGULAR | _ParameterKind.TASK
48+
)
49+
# See base class.
50+
51+
@override
52+
def _make(
53+
self, searchspace: SearchSpace, objective: Objective, measurements: pd.DataFrame
54+
) -> Kernel | GPyTorchKernel:
55+
self._validate_botorch_version()
56+
57+
from botorch.models.kernels.positive_index import PositiveIndexKernel
58+
from botorch.models.utils.gpytorch_modules import (
59+
get_covar_module_with_dim_scaled_prior,
60+
)
61+
from botorch.models.utils.priors import BetaPrior
62+
63+
parameter_names = self.get_parameter_names(searchspace)
64+
65+
# For regular parameters, resolve parameter names to active dimension indices
66+
active_dims = list(
67+
chain.from_iterable(
68+
searchspace.get_comp_rep_parameter_indices(name)
69+
for name in parameter_names
70+
if searchspace.get_parameters_by_name([name])[0]._kind
71+
is _ParameterKind.REGULAR
72+
)
73+
)
74+
ard_num_dims = len(active_dims)
75+
76+
# Create the base kernel for the regular parameters
77+
base_kernel = get_covar_module_with_dim_scaled_prior(
78+
ard_num_dims=ard_num_dims, active_dims=active_dims
79+
)
80+
81+
# Single-task case
82+
if (task_idx := searchspace.task_idx) is None:
83+
return base_kernel
84+
85+
task_prior = BetaPrior(concentration1=2.5, concentration0=1.5)
86+
index_kernel = PositiveIndexKernel(
87+
num_tasks=searchspace.n_tasks,
88+
rank=searchspace.n_tasks,
89+
task_prior=task_prior,
90+
active_dims=[task_idx],
91+
)
92+
return ICMKernelFactory(base_kernel, index_kernel)(
93+
searchspace, objective, measurements
94+
)
95+
96+
def _validate_botorch_version(self) -> None:
97+
"""Verify that the installed BoTorch version meets the minimum requirement.
98+
99+
Raises:
100+
IncompatibilityError: If the installed BoTorch version is too old.
101+
"""
102+
from importlib.metadata import version
103+
104+
from packaging.version import Version
105+
106+
from baybe.exceptions import IncompatibilityError
107+
108+
installed = version("botorch")
109+
if Version(installed) < Version(_MIN_BOTORCH_VERSION):
110+
raise IncompatibilityError(
111+
f"The '{self.__class__.__name__}' requires botorch>="
112+
f"{_MIN_BOTORCH_VERSION}, but version {installed} is installed. "
113+
f"Please upgrade: pip install 'botorch>="
114+
f"{_MIN_BOTORCH_VERSION}'."
115+
)
116+
117+
118+
# Collect leftover original slotted classes processed by `attrs.define`
119+
gc.collect()
120+
121+
# Aliases for generic preset imports
122+
KERNEL_FACTORY = BotorchKernelFactory()
123+
MEAN_FACTORY = BotorchMeanFactory()
124+
LIKELIHOOD_FACTORY = BotorchLikelihoodFactory()
125+
FIT_CRITERION_FACTORY = PlainFitCriterionFactory(FitCriterion.MARGINAL_LOG_LIKELIHOOD)
126+
22127
__all__ = [
23128
"BotorchKernelFactory",
24129
"BotorchLikelihoodFactory",

tests/test_gp.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for the Gaussian Process surrogate."""
22

3+
import sys
4+
35
import pandas as pd
46
import pytest
57
import torch
@@ -195,13 +197,16 @@ def test_invalid_components():
195197
GaussianProcessSurrogate(fit_criterion_or_factory=MaternKernel())
196198

197199

198-
# NOTE: BOTORCH and HVARFNER presets coincide at the moment but BOTORCH settings can
199-
# change in the future. If that happens, the test below will start to fail and the
200-
# HVARFNER parametrization needs to be dropped.
201-
@pytest.mark.parametrize("preset", ["BOTORCH", "HVARFNER"], ids=["botorch", "hvarfner"])
200+
# NOTE: The BOTORCH preset tracks BoTorch's GP defaults while the HVARFNER preset
201+
# implements BoTorch's static Hvarfner et al. (2024) parametrization. Therefore, the
202+
# presets diverge as BoTorch evolves (e.g., BetaPrior added in 0.18.0).
203+
@pytest.mark.skipif(
204+
sys.version_info < (3, 11),
205+
reason="BoTorch >=0.18.0 requires Python >=3.11.",
206+
)
202207
@pytest.mark.parametrize("multitask", [False, True], ids=["single-task", "multi-task"])
203-
def test_botorch_preset(multitask: bool, preset: str):
204-
"""The BoTorch/Hvarfner presets exactly mimic BoTorch's behavior."""
208+
def test_botorch_preset(multitask: bool):
209+
"""The BoTorch preset exactly mimics BoTorch's MultiTaskGP/SingleTaskGP behavior."""
205210
if multitask:
206211
sp = searchspace_mt
207212
data = measurements_mt
@@ -210,7 +215,7 @@ def test_botorch_preset(multitask: bool, preset: str):
210215
data = measurements
211216

212217
active_settings.random_seed = 1337
213-
gp = GaussianProcessSurrogate.from_preset(preset)
218+
gp = GaussianProcessSurrogate.from_preset("BOTORCH")
214219
gp.fit(sp, objective, data)
215220
posterior1 = gp.posterior_stats(data)
216221

tox.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ isolated_build = True
66
[testenv:fulltest,fulltest-py{310,311,312,313,314}]
77
description = Run PyTest with all extra functionality
88
extras = extras,examples,lint,test
9+
deps =
10+
py311,py312,py313,py314: botorch>=0.18.0,<1
911
passenv =
1012
CI
1113
BAYBE_USE_SINGLE_PRECISION_NUMPY
@@ -21,6 +23,8 @@ commands =
2123
[testenv:gputest,gputest-py{310,311,312,313,314}]
2224
description = Runs GPU tests
2325
extras = test
26+
deps =
27+
py311,py312,py313,py314: botorch>=0.18.0,<1
2428
passenv =
2529
CI
2630
BAYBE_USE_SINGLE_PRECISION_NUMPY
@@ -35,6 +39,8 @@ commands =
3539
[testenv:coretest,coretest-py{310,311,312,313,314}]
3640
description = Run PyTest with core functionality
3741
extras = test
42+
deps =
43+
py311,py312,py313,py314: botorch>=0.18.0,<1
3844
passenv =
3945
CI
4046
BAYBE_USE_SINGLE_PRECISION_NUMPY

0 commit comments

Comments
 (0)