Skip to content
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Changed
- `BOTORCH` GP preset now includes `BetaPrior(2.5, 1.5)` for the task covariance
kernel in multi-task scenarios, matching BoTorch's `MultiTaskGP` defaults introduced
in version `0.18.0`
- The `BOTORCH` GP preset now requires BoTorch `>= 0.18.0` and raises an
`IncompatibilityError` if an older version is installed

## [0.15.0] - 2026-06-11
### Breaking Changes
- `GaussianProcessSurrogate` no longer automatically adds a task kernel in multi-task
Expand Down
125 changes: 115 additions & 10 deletions baybe/surrogates/gaussian_process/presets/botorch.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
"""BoTorch preset for Gaussian process surrogates.
"""BoTorch preset for Gaussian process surrogates."""

Currently mimics the Hvarfner preset :cite:p:`Hvarfner2024`.
"""
from __future__ import annotations

from baybe.surrogates.gaussian_process.presets.hvarfner import (
FIT_CRITERION_FACTORY,
KERNEL_FACTORY,
LIKELIHOOD_FACTORY,
MEAN_FACTORY,
import gc
from itertools import chain
from typing import TYPE_CHECKING, ClassVar

import pandas as pd
from attrs import define
from typing_extensions import override

from baybe.kernels.base import Kernel
from baybe.objectives.base import Objective
from baybe.parameters.enum import _ParameterKind
from baybe.searchspace.core import SearchSpace
from baybe.surrogates.gaussian_process.components.fit_criterion import (
FitCriterion,
PlainFitCriterionFactory,
)
from baybe.surrogates.gaussian_process.presets.hvarfner import (
HvarfnerKernelFactory as BotorchKernelFactory,
from baybe.surrogates.gaussian_process.components.kernel import (
ICMKernelFactory,
_PureKernelFactory,
)
from baybe.surrogates.gaussian_process.presets.hvarfner import (
HvarfnerLikelihoodFactory as BotorchLikelihoodFactory,
Expand All @@ -19,6 +29,101 @@
HvarfnerMeanFactory as BotorchMeanFactory,
)

if TYPE_CHECKING:
from gpytorch.kernels import Kernel as GPyTorchKernel

# The minimum BoTorch version required for the preset
_MIN_BOTORCH_VERSION = "0.18.0"


@define
class BotorchKernelFactory(_PureKernelFactory):
"""A factory providing kernels matching BoTorch's :class:`~botorch.models.MultiTaskGP` defaults.""" # noqa: E501

_uses_parameter_names: ClassVar[bool] = True
# See base class.

_supported_parameter_kinds: ClassVar[_ParameterKind] = (
_ParameterKind.REGULAR | _ParameterKind.TASK
)
# See base class.

@override
def _make(
self, searchspace: SearchSpace, objective: Objective, measurements: pd.DataFrame
) -> Kernel | GPyTorchKernel:
self._validate_botorch_version()

from botorch.models.kernels.positive_index import PositiveIndexKernel
Comment thread
AdrianSosic marked this conversation as resolved.
from botorch.models.utils.gpytorch_modules import (
get_covar_module_with_dim_scaled_prior,
)
from botorch.models.utils.priors import BetaPrior

parameter_names = self.get_parameter_names(searchspace)

# For regular parameters, resolve parameter names to active dimension indices
active_dims = list(
chain.from_iterable(
searchspace.get_comp_rep_parameter_indices(name)
for name in parameter_names
if searchspace.get_parameters_by_name([name])[0]._kind
is _ParameterKind.REGULAR
)
)
ard_num_dims = len(active_dims)

# Create the base kernel for the regular parameters
base_kernel = get_covar_module_with_dim_scaled_prior(
ard_num_dims=ard_num_dims, active_dims=active_dims
)

# Single-task case
if (task_idx := searchspace.task_idx) is None:
return base_kernel

task_prior = BetaPrior(concentration1=2.5, concentration0=1.5)
index_kernel = PositiveIndexKernel(
num_tasks=searchspace.n_tasks,
rank=searchspace.n_tasks,
task_prior=task_prior,
active_dims=[task_idx],
)
return ICMKernelFactory(base_kernel, index_kernel)(
searchspace, objective, measurements
)

def _validate_botorch_version(self) -> None:
"""Verify that the installed BoTorch version meets the minimum requirement.

Raises:
IncompatibilityError: If the installed BoTorch version is too old.
"""
from importlib.metadata import version

from packaging.version import Version

from baybe.exceptions import IncompatibilityError

installed = version("botorch")
if Version(installed) < Version(_MIN_BOTORCH_VERSION):
raise IncompatibilityError(
f"The '{self.__class__.__name__}' requires botorch>="
f"{_MIN_BOTORCH_VERSION}, but version {installed} is installed. "
f"Please upgrade: pip install 'botorch>="
f"{_MIN_BOTORCH_VERSION}'."
)


# Collect leftover original slotted classes processed by `attrs.define`
gc.collect()

# Aliases for generic preset imports
KERNEL_FACTORY = BotorchKernelFactory()
MEAN_FACTORY = BotorchMeanFactory()
LIKELIHOOD_FACTORY = BotorchLikelihoodFactory()
FIT_CRITERION_FACTORY = PlainFitCriterionFactory(FitCriterion.MARGINAL_LOG_LIKELIHOOD)

__all__ = [
"BotorchKernelFactory",
"BotorchLikelihoodFactory",
Expand Down
19 changes: 12 additions & 7 deletions tests/test_gp.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for the Gaussian Process surrogate."""

import sys

import pandas as pd
import pytest
import torch
Expand Down Expand Up @@ -195,13 +197,16 @@ def test_invalid_components():
GaussianProcessSurrogate(fit_criterion_or_factory=MaternKernel())


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

active_settings.random_seed = 1337
gp = GaussianProcessSurrogate.from_preset(preset)
gp = GaussianProcessSurrogate.from_preset("BOTORCH")
gp.fit(sp, objective, data)
posterior1 = gp.posterior_stats(data)

Expand Down
6 changes: 6 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ isolated_build = True
[testenv:fulltest,fulltest-py{310,311,312,313,314}]
description = Run PyTest with all extra functionality
extras = extras,examples,lint,test
deps =
py311,py312,py313,py314: botorch>=0.18.0,<1
passenv =
CI
BAYBE_USE_SINGLE_PRECISION_NUMPY
Expand All @@ -21,6 +23,8 @@ commands =
[testenv:gputest,gputest-py{310,311,312,313,314}]
description = Runs GPU tests
extras = test
deps =
py311,py312,py313,py314: botorch>=0.18.0,<1
passenv =
CI
BAYBE_USE_SINGLE_PRECISION_NUMPY
Expand All @@ -35,6 +39,8 @@ commands =
[testenv:coretest,coretest-py{310,311,312,313,314}]
description = Run PyTest with core functionality
extras = test
deps =
py311,py312,py313,py314: botorch>=0.18.0,<1
passenv =
CI
BAYBE_USE_SINGLE_PRECISION_NUMPY
Expand Down