Warning, /AID2E-framework/docs/optimizer-architecture.md is written in an unsupported language. File is not indexed.
0001 # AID2E Optimizer Architecture
0002
0003 ## Overview
0004
0005 `aid2e.optimizers` exposes a shared optimizer interface and two concrete backends:
0006 Ax for Bayesian optimization and PyMOO for evolutionary optimization. The package
0007 also exports the shared search-space and trial data structures, plus the Pareto
0008 utility used by the base implementation.
0009
0010 The current public export surface is:
0011
0012 - `BaseOptimizer`
0013 - `SearchSpace`
0014 - `Trial`
0015 - `compute_pareto_front`
0016 - `AxOptimizer`
0017 - `AxOptimizerConfig`
0018 - `PyMOOProblem`
0019 - `PyMOOOptimizer`
0020 - `PyMOOOptimizerConfig`
0021 - `AID2EProblem`
0022
0023 The design is intentionally split between:
0024
0025 - shared bookkeeping and result utilities in `src/aid2e/optimizers/base.py`
0026 - Ax-specific configuration, symbol resolution, and candidate generation in
0027 `src/aid2e/optimizers/ax/`
0028 - PyMOO-specific ask/tell logic in `src/aid2e/optimizers/pymoo/`
0029 - canonical config and runtime builders in `src/aid2e/utilities/configurations/`
0030 and `src/aid2e/utilities/runtime_builders.py`
0031
0032 ## Shared Abstractions
0033
0034 ### `SearchSpace`
0035
0036 `SearchSpace` is the optimizer-facing representation of the design domain. It
0037 stores:
0038
0039 - `parameters`: a mapping from parameter name to `BaseParameter`
0040 - `constraints`: optional `ParameterConstraint` objects
0041 - `name`: optional search-space identifier
0042 - `source_config`: optional originating `DesignConfig`
0043
0044 `SearchSpace.from_design_config()` flattens a validated `DesignConfig` into the
0045 optimizer form by using `DesignConfig.get_flat_parameters()` and forwarding the
0046 associated parameter constraints.
0047
0048 The constructor accepts either `BaseParameter` objects or dictionaries that are
0049 parsed into parameters. The code rejects retired dictionary shapes, including the
0050 legacy `values` key, and requires a concrete `value` field alongside `bounds` or
0051 `choices`.
0052
0053 `SearchSpace.validate()` is a backend-agnostic constraint check. It evaluates
0054 `ParameterConstraint` objects directly and is intended for backends that do not
0055 enforce constraints natively.
0056
0057 ### `Trial`
0058
0059 `Trial` is the in-memory record for one evaluated point. It stores:
0060
0061 - `index`
0062 - `parameters`
0063 - `metrics`
0064 - `metadata`
0065 - `status`
0066
0067 Statuses are normalized to lower case in `__post_init__`. The base code recognizes
0068 `pending`, `suggested`, `running`, `completed`, `failed`, `aborted`, and
0069 `cancelled`. Unknown values are preserved, but a warning is logged.
0070
0071 ### `compute_pareto_front`
0072
0073 `compute_pareto_front()` computes the non-dominated subset of completed trials
0074 using minimization semantics for every objective. It only considers trials whose
0075 status is `completed` and that have metrics. The function is used by the base
0076 implementation and is not Ax- or PyMOO-specific.
0077
0078 ### `BaseOptimizer`
0079
0080 `BaseOptimizer` owns the shared trial ledger and the default result utilities. It
0081 accepts either a `SearchSpace` or a `DesignConfig`, validates that the search
0082 space and objective list are non-empty, and stores the common `seed`.
0083
0084 The backend-required contract is small. Only these methods remain abstract:
0085
0086 - `suggest_candidates`
0087 - `update_with_results`
0088 - `serialize_state`
0089 - `load_state`
0090
0091 Everything else is provided by the base class:
0092
0093 - `get_trials()`
0094 - `set_trial_status()`
0095 - `get_optimization_results()`
0096 - `save_optimization_results()`
0097 - `seed_from_trials()`
0098 - `get_pareto_front()`
0099 - `get_best_trial()`
0100
0101 `seed_from_trials()` is the backend-switch primitive. It appends external trials
0102 to the local history, assigns new sequential indices, and preserves the original
0103 source trial index in metadata under `source_index`. This updates history only;
0104 backends that need warm-start behavior must add their own override.
0105
0106 `get_pareto_front()` delegates to `compute_pareto_front()`. `get_best_trial()`
0107 uses the first Pareto member for multi-objective cases and the lowest-valued
0108 trial for single-objective cases.
0109
0110 `get_optimization_results()` and `save_optimization_results()` provide a stable
0111 JSON-friendly result export, including raw status and display status labels.
0112
0113 ## Ax Backend
0114
0115 ### Configuration
0116
0117 `AxOptimizerConfig` is the backend config model for the Ax implementation. It is
0118 registered under the `ax` name in the optimizer config registry.
0119
0120 Supported fields are:
0121
0122 - `initialization_strategy`: `sobol`, `uniform`, or `center`
0123 - `generator`: currently validated to `BOTORCH_MODULAR`
0124 - `generator_kwargs`: runtime kwargs passed to the Ax generator spec
0125 - `generator_gen_kwargs`: generation-time kwargs passed through to Ax
0126 - `objective_thresholds`: optional multi-objective thresholds by metric name
0127 - `n_initial_samples`
0128 - `n_iterations`
0129 - `batch_size`
0130 - `seed`
0131
0132 The config explicitly rejects retired fields such as `surrogate_model` and
0133 `acquisition_function`. The current surface expects the newer `generator` plus
0134 `generator_kwargs` split instead.
0135
0136 `src/aid2e/optimizers/ax/_resolver.py` keeps the config YAML-friendly by resolving
0137 string names into the concrete Ax, BoTorch, and GPyTorch classes used by the
0138 modern Modular BoTorch generator. It resolves:
0139
0140 - `acquisition_class`
0141 - `botorch_acqf_class`
0142 - `botorch_acqf_classes_with_options`
0143 - `surrogate_spec`
0144
0145 The resolver currently knows about the supported model, transform, kernel,
0146 likelihood, and MLL classes listed in that module. When `surrogate_spec` is
0147 present, it also resolves nested `metric_to_model_configs` entries in addition
0148 to the top-level `model_configs`.
0149
0150 ### Search-space and constraint mapping
0151
0152 `AxOptimizer` converts `RangeParameter` values into Ax floating-point range
0153 parameters and `ChoiceParameter` values into Ax string choice parameters.
0154
0155 Constraint handling is best effort. The backend attempts to translate
0156 `ParameterConstraint.rule` into an Ax linear inequality. Simple expressions are
0157 handled; unsupported expressions are logged and skipped rather than being forced
0158 into an incorrect shape.
0159
0160 For multi-objective problems, the optimization config uses Ax objectives plus
0161 optional objective thresholds. The objective names are treated as minimization
0162 targets in the Ax model.
0163
0164 ### Generation strategy
0165
0166 The Ax backend uses the node-based Ax generation API. The constructor requires an
0167 Ax runtime that provides `CenterGenerationNode`, `GenerationNode`,
0168 `GeneratorSpec`, and `MinTrials`.
0169
0170 The generation strategy is built as:
0171
0172 - an initialization node using `sobol`, `uniform`, or `center`
0173 - a model-based node using `Generators.BOTORCH_MODULAR`
0174
0175 `center` is handled as a center node followed by a Sobol initialization node when
0176 additional initialization samples are needed. `uniform` falls back to Sobol if
0177 the installed Ax runtime does not expose a uniform generator.
0178
0179 The backend uses `resolve_generator_kwargs()` to turn YAML-friendly values into
0180 Ax runtime objects before passing them into the generator spec.
0181
0182 ### Candidate lifecycle
0183
0184 `suggest_candidates()` makes one Ax generation call for the requested batch, then
0185 normalizes the returned generator output into single-arm runs. Each generated arm
0186 creates a new Ax trial, is marked running in Ax, and is recorded in the base
0187 history as `suggested`.
0188
0189 The method raises if Ax returns fewer generator runs than requested. The method
0190 also records strategy metadata such as the current Ax step index or node name
0191 when available.
0192
0193 `update_with_results()` validates that every configured objective is present in
0194 the metrics payload, attaches the results to the Ax experiment as deterministic
0195 data, marks the Ax trial completed, and updates the shared `Trial` history.
0196
0197 ### State model
0198
0199 `serialize_state()` stores:
0200
0201 - the search-space parameters and constraints
0202 - objective names
0203 - seed
0204 - backend config
0205 - recorded trials
0206 - the trial counter
0207
0208 `load_state()` rebuilds the search space, Ax search space, optimization config,
0209 experiment, and generation strategy from the serialized payload, then replays
0210 completed trials into the experiment.
0211
0212 The code does not serialize an Ax experiment object directly. The restoration path
0213 is therefore reconstruction-based rather than a byte-for-byte restore of Ax
0214 internals.
0215
0216 ## PyMOO Backend
0217
0218 ### Configuration
0219
0220 `PyMOOOptimizerConfig` is registered under the `pymoo` name in the optimizer
0221 registry. It currently supports:
0222
0223 - `algorithm`: optional explicit algorithm selection
0224 - `pop_size`
0225 - `n_offsprings`
0226 - `crossover_prob`
0227 - `crossover_eta`
0228 - `mutation_eta`
0229 - `n_iterations`
0230 - `n_partitions`
0231 - `seed`
0232 - `verbose`
0233
0234 If `algorithm` is omitted, `resolve_algorithm()` selects `ga` for single-objective
0235 problems and `nsga2` otherwise. Explicit algorithms must match the objective
0236 count:
0237
0238 - `ga` only for single-objective problems
0239 - `nsga2`, `nsga3`, and `moead` only for multi-objective problems
0240
0241 ### `PyMOOProblem`
0242
0243 `PyMOOProblem` is the public PyMOO `Problem` wrapper for the AID2E search space.
0244 It is structural only:
0245
0246 - `decode_x()` converts a PyMOO float vector back into an AID2E parameter dict
0247 - `_evaluate()` always raises `NotImplementedError`
0248
0249 The class is used for ask/tell workflows where evaluation happens outside the
0250 optimizer, not inside the PyMOO problem object.
0251
0252 `AID2EProblem` remains available temporarily as a deprecated compatibility
0253 alias. Ax does not expose an equivalent public `Problem` wrapper.
0254
0255 Choice parameters are encoded as continuous indices and rounded back to the
0256 nearest valid choice during decoding. Range parameters are passed through as
0257 floats. Any other parameter types are not supported by the current backend.
0258
0259 ### Candidate lifecycle
0260
0261 PyMOO follows an external-evaluation ask/tell flow:
0262
0263 - `suggest_candidates()` calls `algorithm.ask()`
0264 - `update_with_results()` buffers one evaluation result
0265 - `_flush_generation()` calls `algorithm.tell()` once the full generation has
0266 reported back
0267
0268 `suggest_candidates()` ignores `n_candidates` as a hard request. The actual batch
0269 size is determined by the PyMOO algorithm. When the hint does not match the
0270 generated batch, a debug message is logged.
0271
0272 The backend keeps an in-flight generation buffer and refuses to start a new
0273 generation until the current one has been fully updated. Trials are inserted into
0274 the shared history with status `pending` when they are suggested and are updated
0275 to `completed` when results arrive.
0276
0277 `seed_from_trials()` is overridden to guard against seeding while a generation is
0278 in flight. The method still only updates history; it does not warm-start the
0279 internal PyMOO population.
0280
0281 ### Algorithm mapping
0282
0283 The backend currently constructs one of four algorithms:
0284
0285 - `ga`
0286 - `nsga2`
0287 - `nsga3`
0288 - `moead`
0289
0290 `SBX`, polynomial mutation, and random sampling are used as the default operators.
0291 `nsga3` and `moead` build reference directions from `n_partitions` or `pop_size`
0292 as appropriate.
0293
0294 ### Constraints and state
0295
0296 Constraints are not forwarded into PyMOO at present. When the search space
0297 contains constraints, the optimizer logs a warning and continues, so constraint
0298 satisfaction is not guaranteed by this backend.
0299
0300 `serialize_state()` stores the search space, config, objective names, trials,
0301 trial counter, generation count, resolved algorithm name, and, when possible, a
0302 base64-encoded pickle of the PyMOO algorithm state.
0303
0304 `load_state()` reconstructs the search space and problem, then either restores
0305 the pickled algorithm state or rebuilds the algorithm from config and seed if the
0306 pickle is missing or cannot be loaded. Any in-flight generation state is cleared
0307 during load.
0308
0309 ## Runtime Construction Path
0310
0311 `OptimizerConfiguration` is the canonical top-level config object used by the
0312 framework. It stores the backend `name`, the optimizer `type`, and the raw
0313 `parameters` payload.
0314
0315 The runtime builder resolves backend selection through `build_optimizer_from_config()`
0316 in `src/aid2e/utilities/runtime_builders.py`:
0317
0318 - backend inference normalizes `name`, `type`, and the `algorithm` parameter and
0319 matches any of those tokens against the supported backend keywords
0320 - `ax`, `bo`, `mobo`, and `bayesian` map to the Ax backend
0321 - `pymoo`, `ga`, `nsga2`, `nsga3`, `moead`, and `evolutionary` map to the PyMOO backend
0322
0323 For Ax, the builder instantiates `AxOptimizerConfig` from the raw parameter
0324 payload and passes `problem_cfg.design_config` into `AxOptimizer`. For PyMOO, it
0325 does the same with `PyMOOOptimizerConfig`. In both cases, the builder only
0326 constructs the optimizer object; batch sizing and iteration counts are consumed
0327 by the outer orchestration or runtime loop that drives repeated
0328 `suggest_candidates()` and `update_with_results()` calls.
0329
0330 The optimizer constructors accept `DesignConfig` objects directly, so the runtime
0331 builder passes the design config rather than pre-building `SearchSpace`. The base
0332 class handles the conversion to `SearchSpace`.
0333
0334 `optimization_registry.py` registers the backend config models lazily so the
0335 canonical optimizer config can validate backend-specific parameters without
0336 forcing every backend module to be imported up front.
0337
0338 ## Code-Visible Limitations
0339
0340 - The base class assumes minimization semantics throughout.
0341 - `compute_pareto_front()` only considers completed trials with metrics.
0342 - `AxOptimizerConfig.n_iterations` and `PyMOOOptimizerConfig.n_iterations` are
0343 configuration values used by outer execution flows; the optimizer classes do
0344 not enforce a stop condition internally, and the runtime builder does not
0345 consume them itself.
0346 - Ax constraint handling is best effort and only covers the constraint shapes
0347 that the parser can convert safely.
0348 - PyMOO does not currently enforce search-space constraints.
0349 - PyMOO only supports `RangeParameter` and `ChoiceParameter`.
0350 - The Ax backend currently requires the node-based Ax runtime; older Ax APIs are
0351 not used here.
0352
0353 ## Extension Points
0354
0355 To add a new backend, the current code path requires three pieces:
0356
0357 - a new optimizer class implementing the four abstract methods on `BaseOptimizer`
0358 - a Pydantic config model registered through `optimization_registry.register()`
0359 - a runtime-builder branch that maps the canonical optimizer config to the new backend
0360
0361 Shared history, Pareto computation, trial export, and backend-switch seeding can
0362 then be reused directly from `BaseOptimizer`.