Parameter validation for kernel factories#776
Conversation
The new parameter kind validation step guards us
There was a problem hiding this comment.
Pull request overview
This PR introduces a parameter-kind validation mechanism for Gaussian process kernel factories and updates kernel translation to infer dimensions from a SearchSpace, enabling factories to explicitly declare and enforce which parameter roles (e.g., task vs. regular) they support.
Changes:
- Add
ParameterKind(flag enum) +Parameter.kindand enforce supported parameter kinds inKernelFactory. - Introduce parameter sub-selection via
parameter_selector/parameter_names, and refactorKernel.to_gpytorchto take aSearchSpacefor automaticactive_dims/ard_num_dims. - Add a deprecation guard that raises a
DeprecationErrorwhen using a custom kernel in multi-task GP contexts unless suppressed via env var.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
baybe/kernels/base.py |
Refactors to_gpytorch to use SearchSpace-derived dimensions; adds parameter_names to basic kernels. |
baybe/parameters/enum.py |
Introduces ParameterKind flag enum. |
baybe/parameters/base.py |
Adds Parameter.kind property derived from ParameterKind. |
baybe/parameters/__init__.py |
Exposes ParameterKind in public parameters API. |
baybe/parameters/selector.py |
Adds parameter selector protocol + concrete selectors (e.g., TypeSelector). |
baybe/surrogates/gaussian_process/components/generic.py |
Renames factory protocol type to GPComponentFactoryProtocol and updates conversion helper typing. |
baybe/surrogates/gaussian_process/components/kernel.py |
Adds kernel factory base class with parameter-kind validation and introduces ICMKernelFactory. |
baybe/surrogates/gaussian_process/components/mean.py |
Switches to protocol-based mean factory typing. |
baybe/surrogates/gaussian_process/components/likelihood.py |
Switches to protocol-based likelihood factory typing. |
baybe/surrogates/gaussian_process/components/__init__.py |
Exposes new *Protocol factory types. |
baybe/surrogates/gaussian_process/presets/baybe.py |
Replaces alias with explicit default kernel/task-kernel factories (incl. multitask handling). |
baybe/surrogates/gaussian_process/presets/edbo.py |
Updates EDBO kernel factory to use parameter_names selection; adjusts likelihood factory typing. |
baybe/surrogates/gaussian_process/presets/edbo_smoothed.py |
Same as above for smoothed EDBO. |
baybe/surrogates/gaussian_process/core.py |
Updates GP surrogate to use protocol factories, SearchSpace-based kernel conversion, and adds multitask custom-kernel deprecation guard. |
baybe/settings.py |
Whitelists and validates BAYBE_DISABLE_CUSTOM_KERNEL_WARNING. |
tests/test_kernels.py |
Updates kernel assembly test to build a SearchSpace and validate inferred dims / mapping. |
tests/hypothesis_strategies/kernels.py |
Extends kernel strategies to optionally generate parameter_names. |
tests/test_deprecations.py |
Adds deprecation test for multitask custom-kernel behavior and env-var suppression. |
CHANGELOG.md |
Documents new features, breaking changes, and the new deprecation. |
Comments suppressed due to low confidence (2)
baybe/surrogates/gaussian_process/presets/edbo.py:76
effective_dimsis nowtrain_x.shape[-1], which will include task columns in multi-task settings even though this factory can be used withparameter_selectorto excludeTaskParameters. Sinceeffective_dimscontrols prior/initialization regime selection, it should reflect the dimensionality of the kernel’s active (selected) inputs (e.g., based onself.get_parameter_names(searchspace)/BasicKernel._get_dimensions(searchspace)), otherwise priors will shift when adding a task parameter.
@override
def _make(
self, searchspace: SearchSpace, train_x: Tensor, train_y: Tensor
) -> Kernel:
effective_dims = train_x.shape[-1]
switching_condition = _contains_encoding(
searchspace.discrete, _EDBO_ENCODINGS
) and (effective_dims >= 50)
# low D priors
if effective_dims < 5:
lengthscale_prior = GammaPrior(1.2, 1.1)
baybe/surrogates/gaussian_process/presets/edbo_smoothed.py:60
- Same as in
EDBOKernelFactory:effective_dims=train_x.shape[-1]will count task dimensions even when aparameter_selectorexcludes them. Since the interpolated priors depend oneffective_dims, compute it from the selected/active dimensions instead of the rawtrain_xwidth to keep behavior stable in multi-task setups.
@override
def _make(
self, searchspace: SearchSpace, train_x: Tensor, train_y: Tensor
) -> Kernel:
effective_dims = train_x.shape[-1]
# Interpolate prior moments linearly between low D and high D regime.
# The high D regime itself is the average of the EDBO OHE and Mordred regime.
# Values outside the dimension limits will get the border value assigned.
lengthscale_prior = GammaPrior(
np.interp(effective_dims, _DIM_LIMITS, [1.2, 2.5]),
np.interp(effective_dims, _DIM_LIMITS, [1.1, 0.55]),
)
lengthscale_initial_value = np.interp(effective_dims, _DIM_LIMITS, [0.2, 6.0])
outputscale_prior = GammaPrior(
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Convert specified inner kernels to gpytorch, if provided | ||
| kernel_dict = { | ||
| key: value.to_gpytorch(**kw) | ||
| key: value.to_gpytorch(searchspace, **kw) |
There was a problem hiding this comment.
Kernel.to_gpytorch recurses into inner kernels via value.to_gpytorch(searchspace, **kw), but the new to_gpytorch signature only accepts (searchspace) (no **kw). This will raise a TypeError for composite kernels at runtime. Call inner kernels with just the searchspace (or update the signature consistently) and let each kernel compute its own dimensions.
| key: value.to_gpytorch(searchspace, **kw) | |
| key: value.to_gpytorch(searchspace) |
| # Exception: initial values are not used during construction but are set | ||
| # on the created object (see code at the end of the method). | ||
| missing = set(unmatched) - set(kernel_attrs) | ||
| missing = set(unmatched) - set(kernel_attrs) - self._whitelisted_attributes |
There was a problem hiding this comment.
The sanity-check for unmatched attributes computes missing from unmatched (the last loop iteration) instead of the accumulated unmatched_attrs. This can silently miss unmatched BayBE attributes depending on base-class iteration order. Use the collected unmatched_attrs when computing missing.
| missing = set(unmatched) - set(kernel_attrs) - self._whitelisted_attributes | |
| missing = set(unmatched_attrs) - set(kernel_attrs) - self._whitelisted_attributes |
| gpytorch_kernel = kernel_cls(**kernel_attrs, ard_num_dims=ard_num_dims, **kw) | ||
|
|
There was a problem hiding this comment.
to_gpytorch always passes ard_num_dims=ard_num_dims into the GPyTorch constructor, even when ard_num_dims is None. The comment above explicitly notes that passing None can break kernels that use kwargs.get('ard_num_dims', ...) semantics, and it also defeats the earlier filtering of default kwargs. Only pass ard_num_dims when it is not None (consistent with how active_dims is handled).
| gpytorch_kernel = kernel_cls(**kernel_attrs, ard_num_dims=ard_num_dims, **kw) | |
| # Only pass `ard_num_dims` if it is not None, to avoid overriding | |
| # kernels that rely on `kwargs.get("ard_num_dims", ...)` semantics. | |
| kernel_kwargs: dict[str, Any] = {**kernel_attrs, **kw} | |
| if ard_num_dims is not None: | |
| kernel_kwargs["ard_num_dims"] = ard_num_dims | |
| gpytorch_kernel = kernel_cls(**kernel_kwargs) |
| base_kernel_factory: KernelFactoryProtocol = field(alias="base_kernel_or_factory") | ||
| """The factory for the base kernel operating on numerical input features.""" | ||
|
|
||
| task_kernel_factory: KernelFactoryProtocol = field(alias="task_kernel_or_factory") | ||
| """The factory for the task kernel operating on the task indices.""" | ||
|
|
||
| @base_kernel_factory.default | ||
| def _default_base_kernel_factory(self) -> KernelFactoryProtocol: | ||
| from baybe.surrogates.gaussian_process.presets.baybe import ( | ||
| BayBENumericalKernelFactory, | ||
| ) | ||
|
|
||
| return BayBENumericalKernelFactory(TypeSelector((TaskParameter,), exclude=True)) | ||
|
|
||
| @task_kernel_factory.default | ||
| def _default_task_kernel_factory(self) -> KernelFactoryProtocol: | ||
| from baybe.surrogates.gaussian_process.presets.baybe import ( | ||
| BayBETaskKernelFactory, | ||
| ) | ||
|
|
||
| return BayBETaskKernelFactory(TypeSelector((TaskParameter,))) | ||
|
|
||
| @override | ||
| def _make( | ||
| self, searchspace: SearchSpace, train_x: Tensor, train_y: Tensor | ||
| ) -> Kernel: | ||
| base_kernel = self.base_kernel_factory(searchspace, train_x, train_y) | ||
| task_kernel = self.task_kernel_factory(searchspace, train_x, train_y) | ||
| return ProductKernel([base_kernel, task_kernel]) |
There was a problem hiding this comment.
ICMKernelFactory fields are aliased as base_kernel_or_factory / task_kernel_or_factory, but they have no converter/validator like GaussianProcessSurrogate.kernel_factory. Passing a Kernel instance (or any non-callable component) will fail at runtime when invoked. Either (a) rename aliases to *_factory and validate is_callable(), or (b) add a to_component_factory converter here as well.
Related: _make builds a BayBE ProductKernel, so these sub-factories should be constrained to return BayBE Kernel objects (not raw GPyTorch kernels), otherwise ProductKernel([base_kernel, task_kernel]) will break.
| from baybe.parameters.base import Parameter | ||
|
|
||
|
|
||
| class ParameterKind(Flag): |
There was a problem hiding this comment.
or ParameterRole?
DevPR, parent is #745
Adds a validation mechanism to ensure kernel factories only produce kernels for search spaces they are intended for.
This is achieved via a new
ParameterKindflag enum that factories can use to signal which parameter types they support.