Add Per-Parameter Convenience Kernel Override - #904
Conversation
…a TaskParameter - Change the default transfer learning kernel from `IndexKernel` to `PositiveIndexKernel`, enforcing positive task correlations; `PositiveIndexKernel` disables BoTorch's target-task normalization (`unit_scale_for_target=False`) - Add `TransferLearningMode` enum (`INDEX_KERNEL`, `POSITIVE_INDEX_KERNEL`) in `baybe/parameters/enum.py` - Add optional `TaskParameter.override_transfer_learning_mode` field (default `None`) to override the task kernel used for transfer learning - Add `GaussianProcessSurrogate._resolve_kernel`: when an override is set, strip the task parameter, run the kernel factory on the reduced search space, and attach the requested task kernel - Add `SearchSpace._without_task_parameter()` helper to build a task-free search space - Raise the new `IncompatibleKernelError` when an override clashes with a task-aware kernel factory - Extend the `task_parameters` and kernel hypothesis strategies to cover the new field and `PositiveIndexKernel`; add tests for factory dispatch and `_resolve_kernel` - Use the default transfer-learning kernel in the transfer-learning benchmarks - Filter the "Negative variance values detected" `NumericalWarning` in `pytest.ini`, add a `gpytorch` intersphinx mapping for the docs, and update the CHANGELOG Co-authored-by: Martin Fitzner <martin.fitzner@merckgroup.com>
- build the base kernel from what the user gave us and attach the requested task kernel manually - Strip the task out of user-provided (scaled) kernels - Raise when the override can't be implementded (raw gpytorch kernels) - Add a dedicated error for blocked reduced-search-space access so we catch the right thing instead of an general AttributeError - Update the tests for the new behavior
- Undo unintended reformat - Remove test for defautls - Export TransferLearningMode at top level namespace - Improve docstring - Rename IncompatibleKErnelError to IncompatibleOverrideError - Make IndexKernel case explicit
- The override used to build the base kernel on a reduced search space, where the default factory's numerical kernel cannot resolve its active dimensions, so it raised on non-substance spaces. - Route the default factory through the ICM machinery on the full search space instead, pairing the task-excluded base kernel with the requested index kernel. - Leave the reduced-search-space path for other factories and the strip path for fixed kernels untouched, so all other behavior stays the same. - Move the default-factory override cases from the raising test to the success test.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a per-parameter kernel_override convenience interface to compose Gaussian process kernels at the parameter level, while keeping transfer-learning overrides compatible and warning when non-GP surrogates ignore these overrides.
Changes:
- Introduces
kernel_overrideon regular parameters (explicitly disallowed onTaskParameter) with validation and equivalence handling. - Refactors GP kernel resolution to apply (parameter + transfer-learning) overrides via a dedicated
_override/subpackage and residual-kernel composition. - Adds comprehensive tests, docs, and a changelog entry; warns when non-GP surrogates ignore configured overrides.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/validation/test_parameter_validation.py | Adds constructor-time validation coverage for invalid overrides and TaskParameter rejection. |
| tests/test_surrogate.py | Verifies non-GP surrogates emit UnusedObjectWarning when overrides are present. |
| tests/test_parameter_kernel_overrides.py | New functional test suite covering override binding/composition and incompatibility cases. |
| tests/test_iterations.py | Adds an end-to-end iteration test exercising parameter kernel overrides. |
| tests/hypothesis_strategies/parameters.py | Extends parameter strategies to optionally generate valid kernel overrides. |
| docs/components/surrogates.md | Links surrogate kernel documentation to the new parameter override section. |
| docs/components/parameters.md | Documents parameter-specific kernel overrides, semantics, and limitations. |
| CHANGELOG.md | Records the new parameter-specific kernel override feature. |
| baybe/surrogates/gaussian_process/core.py | Implements the new override-aware kernel resolution flow and residual-kernel logic. |
| baybe/surrogates/gaussian_process/_override/init.py | Exposes the private override-resolution helpers for the GP surrogate. |
| baybe/surrogates/gaussian_process/_override/core.py | Adds shared helpers for reducing kernel specs and raising incompatibility errors. |
| baybe/surrogates/gaussian_process/_override/parameter.py | Extracts and binds per-parameter overrides (BayBE and raw GPyTorch kernels). |
| baybe/surrogates/gaussian_process/_override/tl.py | Extracts and builds the transfer-learning override kernel factor. |
| baybe/surrogates/base.py | Adds supports_kernel_overrides and emits warnings when unsupported surrogates ignore overrides. |
| baybe/parameters/categorical.py | Prevents TaskParameter from exposing/accepting kernel_override. |
| baybe/parameters/base.py | Adds kernel_override field, validation/scoping converter, and updates equivalence logic. |
| baybe/kernels/composite.py | Implements _with_parameter scoping for composite kernels to support owner rebinding. |
| baybe/kernels/base.py | Adds a _with_parameter API on kernels and implements it for basic kernels. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
AVHopp
left a comment
There was a problem hiding this comment.
First round of reviews - already looks quite nice :)
| ) | ||
|
|
||
| def _with_parameter(self, name: str, /) -> Kernel: | ||
| """Return a copy of the kernel scoped to a single parameter. |
There was a problem hiding this comment.
| """Return a copy of the kernel scoped to a single parameter. | |
| """Return a copy of the kernel that acts only on the given parameter. |
There was a problem hiding this comment.
To mimic the style of the _without_parameter
| return value._with_parameter(instance.name) | ||
|
|
||
| # GPyTorch kernels: no explicit active dimensions allowed anywhere in the tree. | ||
| if sys.modules.get("gpytorch") is not None: |
There was a problem hiding this comment.
Is there actually a situation where somebody would ever use this without having gpytorch installed? Or is this necessary for other reasons?
| return attrs.evolve(self, name=other.name) == other | ||
| # The override is owner-scoped, so rebind it to the other parameter's name. | ||
| kernel_override = self.kernel_override | ||
| if isinstance(kernel_override, Kernel): |
There was a problem hiding this comment.
What happens in the case of a GPyTorch Kernel?
There was a problem hiding this comment.
Also, Claude claims that two "seperately instantiated, but structurally identical GPyTorch overrides always compare unequal", so please double-check
| Returns: | ||
| The reduced kernel, or ``None`` if nothing remains. | ||
| """ | ||
| if not isinstance(component, Kernel): |
There was a problem hiding this comment.
If this is the first thing that you check here, why do you then allow general object in the type hint?
| """ | ||
| if not isinstance(component, Kernel): | ||
| raise_incompatible_override(excluded_names, factory) | ||
| spec: Kernel | None = component |
There was a problem hiding this comment.
Can this happen, given the check and the type hint?
| yield kernel | ||
| elif isinstance(kernel, ScaleKernel): | ||
| yield from _iter_basic_kernels(kernel.base_kernel) | ||
| elif isinstance(kernel, (AdditiveKernel, ProductKernel)): |
There was a problem hiding this comment.
Shouldn't we have an "else" for potential additional subclasses/for raising errors right now? Otherwise, high danger of silently ignoring stuff we might add in the future.
| # https://github.com/python-attrs/attrs/issues/164 | ||
|
|
||
|
|
||
| def _iter_basic_kernels(kernel: Kernel): |
There was a problem hiding this comment.
No return type annotation?
| The GPyTorch kernel bound to the parameter's dimensions. | ||
| """ | ||
| override = parameter.kernel_override | ||
| assert override is not None |
There was a problem hiding this comment.
Why can we make this assert here? Isn't this a very generic helper?
| """A strategy that generates parameter categories.""" | ||
|
|
||
|
|
||
| def _remove_kernel_parameter_names(kernel): |
There was a problem hiding this comment.
Doesn't this exactly replicate the helper you defined in the main part of the PR? Why don't we re-use it?
| """The individual kernels to be multiplied.""" | ||
|
|
||
| @override | ||
| def _with_parameter(self, name: str, /) -> Kernel: |
There was a problem hiding this comment.
I was wondering, why we do not support _without_parameter for the ProductKernel as well? Shouldn't it be possible to combine a ProductKernel with an override by just removing the parameter from every factor in the product?
| has_tl_override = context.tl_override is not None | ||
| if ( | ||
| context.is_multitask | ||
| and self._custom_kernel |
There was a problem hiding this comment.
Could you clarify whether this error can ever be triggered when combining the default kernel factory with a parameter override? My intuition was that once you attach an override, the effective kernel is no longer the plain default, but a custom one. But as far as I can tel the self._custom_kernelwill never be affected by an override. Is this the intended behaviour?
| kernel, _ = _resolve( | ||
| [ | ||
| NumericalContinuousParameter("x1", (0, 1)), | ||
| NumericalContinuousParameter("x2", (0, 1), kernel_override=RBFKernel()), |
There was a problem hiding this comment.
Can we add a similar test with on override on the 2nd of three parameters for the non-default branch in _resolve_residual_kernel like a MaternKernel() to check that the indices are resolved ocrrectly?
|
|
||
|
|
||
| def test_default_factory_selector_is_preserved(): | ||
| """Partitioning preserves an explicit default-factory parameter selector.""" |
There was a problem hiding this comment.
Can we add other success cases besides the default BayBEKernelFactory like a (scaled) reducible kernel + a parameter override:
MaternKernel()MaternKernel(("x1","x2"))(named branch)ScaleKernel(MaternKernel())(unnamed branch)
| default=None, | ||
| validator=optional(is_callable()), | ||
| ) | ||
| """The factory used to create the kernel for the Gaussian process. |
There was a problem hiding this comment.
Could you document the override behaviour here?
There was a problem hiding this comment.
what exactly? the control flow (similar tot he picture) in words?

This adds the per-parameter convenience override for the kernel expressed in parameter objects
Builds on PR #868
Adds an optional, kw-only
kernel_overrideto regular parameters,TaskParameterintentionally does not expose itStreamlines override inside the GP. New flow in
_resolve_kernel:_override/withparameter.py,tl.py,core.py)Limitations:
UnusedObjectWarningparam_names=Noneoractive_dimsard_num_dimsmust be None or match the parameter's comp rep widthKernel factories are not supported
The surrogate kernel/factory must allow the overridden dimensions to be excluded; incompatible custom kernels/factories raise
IncompatibleOverrideErrorNotes:
TaskParameterdoes not allow kernel overrides like other kernelsSymmetry-indicated symmetrization of base kernels