Add register() functions for strategies and surrogates#736
Conversation
Provide public API to register custom strategy and surrogate mappings instead of requiring users to mutate internal dictionaries directly. For BotorchSurrogate subclasses, registration automatically rebuilds the AnyBotorchSurrogate union and BotorchSurrogates Pydantic model so custom surrogates work in botorch-based strategies out of the box. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I think this looks correct to me! A few minor thoughts:
@surrogates_api.register(CustomBotorchSurrogateDataModel)
class CustomBotorchSurrogate(Surrogate):
def __init__(self, data_model, **kwargs):
...
I think this PR also breaks using |
…mic Pydantic union rebuilding Extend the register() pattern to kernels, priors, and engineered features mappers so custom types can be registered with both the mapper dictionaries and Pydantic unions. Registration dynamically patches model annotations and triggers model_rebuild() in the correct cascade order (priors → kernels → aggregation kernels → surrogates → BotorchSurrogates) so custom types pass Pydantic validation when used in surrogate model fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…features Replace the manually maintained KERNEL_MAP, PRIOR_MAP, and AGGREGATE_MAP dictionaries with @register() decorators on each map function. The dicts are now initialized empty and populated at import time via the same register() function that external users call for custom types. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@TobyBoyne: I was just pushing it further, I need to do an in-depth review here, but is this what you were looking for? |
|
Yes that syntax looks very nice, and I like the decorator being included where the kernels/priors are defined. I was also wondering why do we use, for example, |
There was a problem hiding this comment.
Do we also need _rebuild_dependent_strategy similar to rebuild_dependent_models if we want full dynamic Pydantic acceptance of new strategy data-model types?
There was a problem hiding this comment.
Yes, correct. Should be fixed now. My Agent was overlooking this.
There was a problem hiding this comment.
Yes, correct. Should be fixed now. My Agent was overlooking this. (last week)
Haha. Nice try blaming the agent. :D
|
I just had one comment on the code. |
I think this is needed for de-serialisation of objects from dicts / json. Having a pydantic class with a field |
…strategy rebuild - Extract _patch_field to shared bofire/data_models/_register_utils.py with patch_field() and append_to_union_field() helpers, reused across kernels, priors, and strategies - Make AnyContinuousKernel and AnyCategoricalKernel list-backed and dynamic; register_kernel auto-detects sub-category from ContinuousKernel / CategoricalKernel base class and updates MixedSingleTaskGPSurrogate - Replace lazy imports with data_models.register_kernel() and data_models.register_prior() in mapper register() functions - Remove meta parameter from strategy register (only ACTUAL_MAP) - Add register_strategy() to data models that rebuilds ActualStrategy union, Step, and StepwiseStrategy so custom strategies pass validation - Add introspection tests that verify _rebuild_dependent_models covers all AnyPrior/AnyPriorConstraint/AnyKernel fields in the codebase Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jduerholt
left a comment
There was a problem hiding this comment.
Some comments from my side.
There was a problem hiding this comment.
Yes, correct. Should be fixed now. My Agent was overlooking this.
ConditionalEmbeddingKernel.base_kernel and WedgeKernel.base_kernel had hardcoded inline Union types that were not patched by _rebuild_dependent_models, so newly registered kernel types were not accepted. Both parent and child need explicit patching because Pydantic gives each class its own copy of model_fields. Also enhances the introspection test to detect inline kernel Union fields on Kernel container classes, preventing this class of regression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves conflict in bofire/surrogates/engineered_features.py by combining the register() decorator pattern from feature/register with main's refactored private functions (_weighted_features, _map_weighted_feature, _map_molecular_weighted_feature) and partial() bindings for sum/mean variants. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Ok, I did more stuff here, this is now almost final @bertiqwerty can you have a look? what do you think? I will also add a tutorial etc. But would be nice to get your assesment! And of course also the assesment from the others! |
Sure. I will look into it. |
The register() infrastructure uses Union[tuple(list)] to build unions at runtime. This is valid Python but not statically analysable, so tell ty to ignore the diagnostic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents how to use the register() API for strategies, surrogates, kernels, and priors with practical code examples covering both decorator and direct-call forms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The `model_fields` attribute is a Pydantic BaseModel class attribute that ty cannot resolve on the generic `type` parameter. Add inline ignore comments on all 5 access sites. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ruff format splits long lines, moving the ignore comment away from the `.model_fields` access that triggers the error. Place the comment on the first line of each expression so ty sees it on the correct line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I faced an issue while running a SoboStrategy campaign using my custom define surrogate. Following code snippet reproduces this error. Workaround But we should fix it. Probable culprit |
|
Hmm, but is it actually a bug? The SoboStrategy only works with botorch based surrogates, so your data model should inherit from it, or? |
|
One can use non Botorch Surrogate models with |
|
Hmm, but why not just inherit in the data model from the BotorchSurrogate? There is no problem in this or? |
bofire/data_models/features/api.py
Outdated
| from typing import Union | ||
| import typing | ||
| from collections.abc import Sequence | ||
| from typing import List, Type, Union |
There was a problem hiding this comment.
use list instead of List. Don't import List.
bofire/data_models/features/api.py
Outdated
| AnyEngineeredFeature = Union[tuple(_ENGINEERED_FEATURE_TYPES)] | ||
|
|
||
|
|
||
| def register_engineered_feature(data_model_cls: Type[EngineeredFeature]) -> None: |
There was a problem hiding this comment.
Maybe don't clutter the api.py with implementations? Create a separate file and import the function here?
bofire/data_models/features/api.py
Outdated
| _ENGINEERED_FEATURE_TYPES.append(data_model_cls) | ||
| AnyEngineeredFeature = Union[tuple(_ENGINEERED_FEATURE_TYPES)] | ||
|
|
||
| # Lazy import to avoid circular dependencies |
There was a problem hiding this comment.
see above. if you had separate file for the implementation, would the import still be circular?
bofire/data_models/features/api.py
Outdated
| # Patch the Sequence[Union[...]] annotation on EngineeredFeatures.features | ||
| old = EngineeredFeatures.model_fields["features"].annotation | ||
| inner_args = typing.get_args(typing.get_args(old)[0]) | ||
| if data_model_cls not in inner_args: |
There was a problem hiding this comment.
this peace of code looks fragile. but currently i don't have a better idea. hmm... at least if this breaks at some point only the registration functinality is affected, not the existing modules.
bofire/data_models/priors/api.py
Outdated
| @@ -1,5 +1,5 @@ | |||
| from functools import partial | |||
| from typing import Union | |||
| from typing import List, Type, Union | |||
bofire/data_models/priors/api.py
Outdated
| AnyPriorConstraint = Union[tuple(_PRIOR_CONSTRAINT_TYPES)] | ||
|
|
||
|
|
||
| def _rebuild_dependent_models() -> None: |
There was a problem hiding this comment.
I prefer a separate file to keep the api.py as clean as possible.
| @@ -1,4 +1,4 @@ | |||
| from typing import Union | |||
| from typing import List, Type, Union | |||
| ActualStrategy = Union[tuple(_ACTUAL_STRATEGY_TYPES)] | ||
|
|
||
|
|
||
| def register_strategy(data_model_cls: Type[Strategy]) -> None: |
There was a problem hiding this comment.
again don't clutter the api.py
| Can be used as a decorator or as a direct function call:: | ||
|
|
||
| # Decorator form | ||
| @register(MyKernelDataModel) |
There was a problem hiding this comment.
Doesn't adding the decorator to existing functions trigger unnecessary rebuild-model calls on import time? Our import time is already pretty long. Further, I find decorators less transparent. If someone wants to register, they could simply use the direct call form. For the existing stuff, I liked the old and stupid dict. And it is somehow incosistent to the data models where we kept the lists of types and extended them. I would prefer to keep the dicts and extend those.
There was a problem hiding this comment.
@bertiqwerty: But the decorator in general would be fine? So, for registering stuff outside of the library? Or do you want to remove the decorator support in general?
There was a problem hiding this comment.
You mean as option for people to use to add with new code outside of BoFire? There is no way to remove it, is there? The register function can always be used as decorator or with a simple function call, right?
There was a problem hiding this comment.
True, so you would like to remove the decorator use from the inner workings of BoFire, correct?
There was a problem hiding this comment.
Yes. And I want the dict back.
There was a problem hiding this comment.
Ok, I will add the dict back in the internal mappings.
There was a problem hiding this comment.
Yes, correct. Should be fixed now. My Agent was overlooking this. (last week)
Haha. Nice try blaming the agent. :D
|
Thanks Johanes, it works. |
|
Hi @bertiqwerty, I implemented the changes, that you suggested, can you have a look again? Best, Johannes |
My reviews are usually YOLO. Just kidding. Thanks for the changes. I will have a look. |
|
Thanks a lot @jduerholt, |
|
Thanks @LukasHebing: I will merge it tmr, I think. |
Since _register.py modules use lazy imports inside function bodies, they can be safely imported at the top of api.py files. Also import register_strategy directly in strategies/api.py instead of re-exporting through actual_strategy_type.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace manual Sequence[Union[...]] patching with the shared append_to_union_field utility from _register_utils, consistent with the other _register.py modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Motivation
Provide public API to register custom strategy and surrogate mappings instead of requiring users to mutate internal dictionaries directly. For BotorchSurrogate subclasses, registration automatically rebuilds the AnyBotorchSurrogate union and BotorchSurrogates Pydantic model so custom surrogates work in botorch-based strategies out of the box. It addresses the discussion in this PR: #731
@TobyBoyne @bertiqwerty @LukasHebing @KislayaRavi what do you think?
This is a draft which should serve for discsussion, code build with Claude Code.
Have you read the Contributing Guidelines on pull requests?
Yes.
Have you updated
CHANGELOG.md?Not yet.
Test Plan
Unit tests.