File indexing completed on 2026-08-12 08:24:55
0001 """Pydantic models for Ax-based optimizer configuration."""
0002
0003 from __future__ import annotations
0004
0005 from typing import Any, Literal, Optional
0006
0007 from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
0008
0009 from aid2e.utilities.configurations.optimization_registry import register
0010
0011 from ._resolver import validate_generator_name
0012
0013
0014 class AxOptimizerConfig(BaseModel):
0015 """Configure the Ax optimizer backend for AID2E.
0016
0017 Define the tuning parameters used by the Ax-based Bayesian optimization
0018 workflow, including initialization behavior, model-generation settings, and
0019 core iteration controls.
0020
0021 Attributes:
0022 initialization_strategy: Choose how the initial design points are drawn
0023 before model-based generation begins. Supported values are
0024 ``"sobol"``, ``"uniform"``, and ``"center"``.
0025 generator: Specify the Ax generator enum name used for model-based
0026 candidate generation. The default is ``"BOTORCH_MODULAR"``.
0027 generator_kwargs: Provide keyword arguments for Ax ``GeneratorSpec``
0028 setup (for example, model configuration details).
0029 generator_gen_kwargs: Provide generation-time keyword arguments passed
0030 into Ax candidate generation (for example,
0031 ``model_gen_options`` budgets).
0032 objective_thresholds: Optionally map objective metric names to
0033 threshold values for multi-objective optimization.
0034 n_initial_samples: Set the number of initialization trials.
0035 n_iterations: Set the total optimization iteration budget.
0036 batch_size: Set the number of candidates proposed per iteration.
0037 seed: Set an optional random seed for reproducibility.
0038
0039 Examples:
0040 >>> config = AxOptimizerConfig(
0041 ... initialization_strategy="sobol",
0042 ... generator="BOTORCH_MODULAR",
0043 ... generator_kwargs={"fit_out_of_design": False},
0044 ... generator_gen_kwargs={"model_gen_options": {"acqf_optimizer_kwargs": {"num_restarts": 8}}},
0045 ... n_initial_samples=12,
0046 ... n_iterations=60,
0047 ... batch_size=2,
0048 ... seed=42,
0049 ... )
0050 >>> config.generator
0051 'BOTORCH_MODULAR'
0052
0053 Notes:
0054 Legacy fields such as ``surrogate_model`` and
0055 ``acquisition_function`` are explicitly rejected.
0056 """
0057
0058 model_config = ConfigDict(extra="forbid")
0059 initialization_strategy: Literal["sobol", "uniform", "center"] = Field(
0060 default="sobol",
0061 description=(
0062 "Initialization strategy: 'sobol' for quasi-random initialization, "
0063 "'uniform' for uniform random initialization, or 'center' for one "
0064 "center point followed by additional initialization samples."
0065 ),
0066 )
0067 generator: str = Field(
0068 default="BOTORCH_MODULAR",
0069 description=(
0070 "Ax generator enum name. This backend currently supports "
0071 "'BOTORCH_MODULAR' and treats it as the default model-based backend."
0072 ),
0073 )
0074 generator_kwargs: dict[str, Any] = Field(
0075 default_factory=dict,
0076 description=(
0077 "Keyword arguments passed to Ax's GeneratorSpec for the configured "
0078 "model-based generator. YAML-friendly string values are resolved to "
0079 "supported Ax / BoTorch classes at runtime."
0080 ),
0081 )
0082 generator_gen_kwargs: dict[str, Any] = Field(
0083 default_factory=dict,
0084 description=(
0085 "Generation-time kwargs passed through Ax into candidate generation, "
0086 "such as optimizer budgets under 'model_gen_options'."
0087 ),
0088 )
0089 objective_thresholds: Optional[dict[str, float]] = Field(
0090 default=None,
0091 description=(
0092 "Optional objective thresholds for multi-objective optimization, "
0093 "keyed by metric name."
0094 ),
0095 )
0096 n_initial_samples: int = Field(
0097 default=10,
0098 ge=1,
0099 description="Number of samples in the initialization phase.",
0100 )
0101 n_iterations: int = Field(
0102 default=50,
0103 ge=1,
0104 description="Total number of optimization iterations.",
0105 )
0106 batch_size: int = Field(
0107 default=1,
0108 ge=1,
0109 description="Number of candidates to evaluate per iteration.",
0110 )
0111 seed: Optional[int] = Field(
0112 default=None,
0113 description="Random seed for reproducibility. If None, results are non-deterministic.",
0114 )
0115
0116 @model_validator(mode="before")
0117 @classmethod
0118 def reject_legacy_fields(cls, raw_value: Any) -> Any:
0119 """Fail fast on the retired Ax config surface."""
0120 if not isinstance(raw_value, dict):
0121 return raw_value
0122
0123 legacy_fields = [
0124 field_name
0125 for field_name in ("surrogate_model", "acquisition_function")
0126 if field_name in raw_value
0127 ]
0128 if legacy_fields:
0129 joined = ", ".join(legacy_fields)
0130 raise ValueError(
0131 f"AxOptimizerConfig no longer accepts legacy fields: {joined}. "
0132 "Use 'generator', 'generator_kwargs', and 'generator_gen_kwargs' "
0133 "instead."
0134 )
0135 return raw_value
0136
0137 @field_validator("generator")
0138 @classmethod
0139 def normalize_generator(cls, value: str) -> str:
0140 """Normalize the configured generator to an Ax enum-style name."""
0141 return validate_generator_name(value)
0142
0143
0144 register("ax", AxOptimizerConfig)