File indexing completed on 2026-08-12 08:24:56
0001 """Base optimizer abstractions for the AID2E framework.
0002
0003 This module defines reusable dataclasses and abstract interfaces that optimizers
0004 use to consume design parameter definitions coming from the design configuration
0005 layer. Search spaces are built from typed design parameters, and trials track
0006 optimizer evaluations in a consistent structure.
0007
0008 Key concepts:
0009 - ``SearchSpace``: Typed parameter domain derived from a DesignConfig.
0010 - ``Trial``: One evaluated point in the search space, with metrics attached.
0011 - ``BaseOptimizer``: Abstract base that all optimizer backends must implement.
0012 - ``compute_pareto_front``: Backend-agnostic utility for Pareto front extraction.
0013
0014 Entrypoints:
0015 Most users interact with a concrete backend (e.g. ``AxOptimizer``,
0016 ``PyMOOOptimizer``) rather than these base classes directly.
0017 """
0018
0019 from abc import ABC, abstractmethod
0020 from dataclasses import dataclass, field
0021 from typing import Any, Dict, List, Optional, Tuple, Union
0022 import json
0023 import logging
0024 from pathlib import Path
0025
0026 logger = logging.getLogger(__name__)
0027
0028 TRIAL_STATUS_PENDING = "pending"
0029 TRIAL_STATUS_SUGGESTED = "suggested"
0030 TRIAL_STATUS_RUNNING = "running"
0031 TRIAL_STATUS_COMPLETED = "completed"
0032 TRIAL_STATUS_FAILED = "failed"
0033 TRIAL_STATUS_ABORTED = "aborted"
0034 TRIAL_STATUS_CANCELLED = "cancelled"
0035
0036 VALID_TRIAL_STATUSES = {
0037 TRIAL_STATUS_PENDING,
0038 TRIAL_STATUS_SUGGESTED,
0039 TRIAL_STATUS_RUNNING,
0040 TRIAL_STATUS_COMPLETED,
0041 TRIAL_STATUS_FAILED,
0042 TRIAL_STATUS_ABORTED,
0043 TRIAL_STATUS_CANCELLED,
0044 }
0045
0046 DISPLAY_STATUS_MAP = {
0047 TRIAL_STATUS_PENDING: "Pending",
0048 TRIAL_STATUS_SUGGESTED: "Suggested",
0049 TRIAL_STATUS_RUNNING: "Running",
0050 TRIAL_STATUS_COMPLETED: "Finished",
0051 TRIAL_STATUS_FAILED: "Failed",
0052 TRIAL_STATUS_ABORTED: "Aborted",
0053 TRIAL_STATUS_CANCELLED: "Cancelled",
0054 }
0055
0056 from aid2e.utilities.configurations.base_models import BaseParameter, parse_parameter
0057 from aid2e.utilities.configurations.design_config import (
0058 DesignConfig,
0059 ParameterConstraint,
0060 )
0061
0062
0063 @dataclass
0064 class SearchSpace:
0065 """Represent an optimization search space built from design parameters.
0066
0067 Attributes:
0068 parameters: Mapping of parameter names to typed design parameters.
0069 constraints: Optional list of parameter constraints to enforce.
0070 name: Optional identifier for the search space.
0071 source_config: Optional originating DesignConfig for traceability.
0072
0073 Examples:
0074 >>> from aid2e.utilities.configurations.base_models import RangeParameter
0075 >>> params = {
0076 ... "x": RangeParameter(name="x", value=0.5, bounds=(0.0, 1.0)),
0077 ... }
0078 >>> space = SearchSpace(parameters=params)
0079 >>> space.validate({"x": 0.4})
0080 (True, [])
0081 """
0082
0083 parameters: Dict[str, BaseParameter]
0084 constraints: List[ParameterConstraint] = field(default_factory=list)
0085 name: Optional[str] = None
0086 source_config: Optional[DesignConfig] = None
0087
0088 def __post_init__(self) -> None:
0089 """Normalize parameter and constraint inputs after initialization."""
0090 parsed_parameters: Dict[str, BaseParameter] = {}
0091 for param_name, param in self.parameters.items():
0092 if isinstance(param, BaseParameter):
0093 parsed = param
0094 elif isinstance(param, dict):
0095 param_data = dict(param)
0096 if "values" in param_data:
0097 raise ValueError(
0098 f"Parameter '{param_name}' uses retired key 'values'. "
0099 "Use 'choices'."
0100 )
0101 if "bounds" in param_data and "value" not in param_data:
0102 raise ValueError(
0103 f"Range parameter '{param_name}' must define an explicit "
0104 "'value' alongside 'bounds'."
0105 )
0106 if "choices" in param_data and "value" not in param_data:
0107 raise ValueError(
0108 f"Choice parameter '{param_name}' must define an explicit "
0109 "'value' alongside 'choices'."
0110 )
0111 parsed = parse_parameter(param_name, param_data)
0112 else:
0113 raise TypeError(
0114 "Parameters must be BaseParameter instances or dictionaries"
0115 )
0116
0117 if parsed.name != param_name:
0118 parsed = parsed.model_copy(update={"name": param_name})
0119 parsed_parameters[param_name] = parsed
0120
0121 self.parameters = parsed_parameters
0122
0123 constraints_input = self.constraints or []
0124 self.constraints = [
0125 c if isinstance(c, ParameterConstraint) else ParameterConstraint(**c)
0126 for c in constraints_input
0127 ]
0128
0129 @classmethod
0130 def from_design_config(cls, design_config: DesignConfig) -> "SearchSpace":
0131 """Build a search space from a DesignConfig instance.
0132
0133 Args:
0134 design_config: Fully validated design configuration containing
0135 parameters and optional parameter constraints.
0136
0137 Returns:
0138 SearchSpace populated with flattened parameters and constraints.
0139 """
0140
0141 return cls(
0142 parameters=design_config.get_flat_parameters(),
0143 constraints=design_config.parameter_constraints or [],
0144 name=getattr(design_config, "name", None),
0145 source_config=design_config,
0146 )
0147
0148 def validate(self, param_values: Dict[str, Any]) -> Tuple[bool, List[str]]:
0149 """Check if parameter values satisfy all constraints (for non-Ax optimizers).
0150
0151 This method is for optimizers that DON'T have native constraint support
0152 (e.g., random search, simple evolutionary algorithms). For optimizers
0153 with native constraint support (e.g., Ax), pass self.constraints directly
0154 to the optimizer instead of calling this method.
0155
0156 Args:
0157 param_values: Mapping of qualified parameter names to concrete values.
0158
0159 Returns:
0160 Tuple of ``(all_valid, failed_constraints)`` where ``all_valid`` is
0161 ``True`` when every constraint passes and ``failed_constraints``
0162 lists the names (or error strings) of failing constraints.
0163
0164 Example:
0165 >>> # For optimizers WITHOUT constraint support
0166 >>> is_valid, failures = search_space.validate(candidate)
0167 >>> if not is_valid:
0168 ... print(f"Constraint violations: {failures}")
0169
0170 >>> # For Ax (HAS constraint support) - DON'T use this method
0171 >>> # Instead, pass search_space.constraints to Ax optimizer
0172
0173 Notes:
0174 - Only use this for runtime checking with constraint-agnostic optimizers
0175 - Ax and similar optimizers handle constraints internally via self.constraints
0176 - Syntax validation already done at DesignConfig load time
0177 """
0178
0179 if not self.constraints:
0180 return True, []
0181
0182 failed: List[str] = []
0183 for constraint in self.constraints:
0184 try:
0185 if not constraint.evaluate(param_values):
0186 failed.append(constraint.name)
0187 except Exception as exc:
0188 failed.append(f"{constraint.name} (error: {exc})")
0189
0190 return len(failed) == 0, failed
0191
0192
0193 @dataclass
0194 class Trial:
0195 """Capture the parameters and results of a single optimization trial.
0196
0197 Attributes:
0198 index: Unique trial identifier within the optimizer.
0199 parameters: Parameter values evaluated during the trial.
0200 metrics: Objective values produced by evaluation (if available).
0201 metadata: Optional auxiliary metadata about the trial.
0202 status: Lifecycle status such as ``pending``, ``completed``, or ``failed``.
0203
0204 Examples:
0205 >>> trial = Trial(
0206 ... index=0,
0207 ... parameters={"x": 0.5},
0208 ... metrics={"loss": 0.1},
0209 ... status="completed",
0210 ... )
0211 >>> trial.metadata
0212 {}
0213 """
0214
0215 index: int
0216 parameters: Dict[str, Any]
0217 metrics: Optional[Dict[str, float]] = None
0218 metadata: Dict[str, Any] = None
0219 status: str = "pending"
0220
0221 def __post_init__(self) -> None:
0222 """Normalize metadata and status values after initialization."""
0223 if self.metadata is None:
0224 self.metadata = {}
0225 normalized = str(self.status).strip().lower()
0226 self.status = normalized if normalized else TRIAL_STATUS_PENDING
0227 if self.status not in VALID_TRIAL_STATUSES:
0228 logger.warning(
0229 "Unknown trial status '%s'; keeping value as-is.",
0230 self.status,
0231 )
0232
0233 def save_to_json(self, output_path: Union[str, Path]) -> Path:
0234 """Write the trial's design parameters to disk as pretty-printed JSON.
0235
0236 This helper is intentionally design-point focused: it serializes only
0237 ``parameters`` so command-line evaluators can consume the resulting file
0238 as an input payload without needing optimizer metadata.
0239
0240 Args:
0241 output_path: Target JSON path.
0242
0243 Returns:
0244 Resolved path of the written file.
0245 """
0246 path = Path(output_path)
0247 path.parent.mkdir(parents=True, exist_ok=True)
0248 with path.open("w", encoding="utf-8") as handle:
0249 json.dump(dict(self.parameters or {}), handle, indent=2, sort_keys=True)
0250 return path
0251
0252
0253 def compute_pareto_front(
0254 trials: List["Trial"],
0255 objective_names: List[str],
0256 objective_directions: Optional[Dict[str, Any]] = None,
0257 ) -> List["Trial"]:
0258 """Extract non-dominated (Pareto-optimal) trials from a collection of completed trials.
0259
0260 Objectives are compared using ``objective_directions``. Minimization is the
0261 default; maximization objectives treat larger values as better. For
0262 single-objective problems the function returns the best trial under that
0263 objective's direction.
0264
0265 Args:
0266 trials: Iterable of Trial objects. Only trials whose ``status`` is
0267 ``"completed"`` and whose ``metrics`` dict is non-empty are
0268 considered.
0269 objective_names: Ordered list of objective metric keys that must be
0270 present in each trial's ``metrics`` dict.
0271 objective_directions: Optional mapping from objective name to
0272 ``"minimize"`` or ``"maximize"``. Missing objectives default to
0273 minimization.
0274
0275 Returns:
0276 List of non-dominated Trial objects ordered by their original
0277 position in ``trials``. Returns an empty list when no completed
0278 trials are available.
0279
0280 Examples:
0281 >>> from aid2e.optimizers.base import Trial, compute_pareto_front
0282 >>> t1 = Trial(index=0, parameters={}, metrics={"f1": 1.0, "f2": 3.0}, status="completed")
0283 >>> t2 = Trial(index=1, parameters={}, metrics={"f1": 2.0, "f2": 1.0}, status="completed")
0284 >>> t3 = Trial(index=2, parameters={}, metrics={"f1": 0.5, "f2": 2.0}, status="completed")
0285 >>> front = compute_pareto_front([t1, t2, t3], ["f1", "f2"])
0286 >>> {t.index for t in front}
0287 {0, 1, 2}
0288
0289 Notes:
0290 Uses NumPy for vectorised comparisons when available; falls back to
0291 a pure-Python O(n²) loop otherwise. For production workloads with
0292 thousands of trials, the NumPy path is strongly recommended.
0293 """
0294 completed = [
0295 t for t in trials
0296 if t is not None and t.status == "completed" and t.metrics
0297 ]
0298 if not completed:
0299 return []
0300 if len(completed) == 1:
0301 return completed
0302
0303 directions = objective_directions or {}
0304 objective_signs = []
0305 for obj in objective_names:
0306 direction = getattr(directions.get(obj), "value", directions.get(obj, "minimize"))
0307 objective_signs.append(-1.0 if str(direction).lower() == "maximize" else 1.0)
0308
0309 def score(trial: "Trial", obj: str, sign: float) -> float:
0310 value = trial.metrics.get(obj)
0311 return float("inf") if value is None else float(value) * sign
0312
0313 try:
0314 import numpy as np
0315
0316 n = len(completed)
0317 F = np.array(
0318 [
0319 [score(t, obj, sign) for obj, sign in zip(objective_names, objective_signs)]
0320 for t in completed
0321 ],
0322 dtype=float,
0323 )
0324 is_dominated = np.zeros(n, dtype=bool)
0325 for i in range(n):
0326 if is_dominated[i]:
0327 continue
0328
0329
0330 dom_mask = np.all(F <= F[i], axis=1) & np.any(F < F[i], axis=1)
0331 dom_mask[i] = False
0332 if np.any(dom_mask):
0333 is_dominated[i] = True
0334
0335 return [t for t, dom in zip(completed, is_dominated) if not dom]
0336
0337 except ImportError:
0338 logger.warning("NumPy not available; falling back to pure-Python Pareto computation.")
0339 n = len(completed)
0340 is_dominated = [False] * n
0341 for i in range(n):
0342 if is_dominated[i]:
0343 continue
0344 for j in range(n):
0345 if i == j or is_dominated[j]:
0346 continue
0347
0348 all_leq = all(
0349 score(completed[j], obj, sign) <= score(completed[i], obj, sign)
0350 for obj, sign in zip(objective_names, objective_signs)
0351 )
0352 any_lt = any(
0353 score(completed[j], obj, sign) < score(completed[i], obj, sign)
0354 for obj, sign in zip(objective_names, objective_signs)
0355 )
0356 if all_leq and any_lt:
0357 is_dominated[i] = True
0358 break
0359
0360 return [t for t, dom in zip(completed, is_dominated) if not dom]
0361
0362
0363 class BaseOptimizer(ABC):
0364 """Abstract base class for all AID2E optimizers.
0365
0366 Subclasses must implement the abstract methods to suggest candidates, ingest
0367 evaluation results, and surface optimizer state. The interface is intentionally
0368 minimal to support a range of backends (Ax, genetic algorithms, grid search)
0369 while keeping a consistent contract for the rest of the framework.
0370
0371 Concrete default implementations are provided for ``get_pareto_front`` and
0372 ``get_best_trial`` using :func:`compute_pareto_front`. Individual backends
0373 may override these when native support (e.g. PyMOO's built-in Pareto tools)
0374 is preferable.
0375 """
0376
0377 def __init__(
0378 self,
0379 search_space: Union[SearchSpace, DesignConfig],
0380 objective_names: List[str],
0381 seed: Optional[int] = None,
0382 objective_directions: Optional[Dict[str, Any]] = None,
0383 ) -> None:
0384 """Initialize the optimizer with a search space and objective specification.
0385
0386 Args:
0387 search_space: Typed search space or DesignConfig to optimize over.
0388 objective_names: Ordered list of objective metric names. Each name
0389 must match the keys returned in the ``metrics`` dict when
0390 ``update_with_results`` is called.
0391 seed: Optional integer seed for reproducibility.
0392 objective_directions: Optional mapping from objective name to
0393 ``"minimize"`` or ``"maximize"``. Missing objectives default to
0394 minimization.
0395
0396 Raises:
0397 ValueError: If the search space is empty or no objective names are
0398 provided.
0399 """
0400
0401 resolved_space = (
0402 SearchSpace.from_design_config(search_space)
0403 if isinstance(search_space, DesignConfig)
0404 else search_space
0405 )
0406
0407 if not resolved_space.parameters:
0408 raise ValueError("Search space cannot be empty")
0409 if not objective_names:
0410 raise ValueError("objective_names must contain at least one name")
0411
0412 self.search_space = resolved_space
0413 self.objective_names: List[str] = list(objective_names)
0414 self.objective_directions: Dict[str, Any] = dict(objective_directions or {})
0415 self.seed = seed
0416
0417
0418
0419
0420
0421 self._trials: List[Optional[Trial]] = []
0422 self._trial_counter: int = 0
0423
0424 @property
0425 def n_objectives(self) -> int:
0426 """Return the number of optimisation objectives.
0427
0428 Returns:
0429 Integer count derived from ``objective_names``.
0430 """
0431 return len(self.objective_names)
0432
0433 @abstractmethod
0434 def suggest_candidates(self, n_candidates: int = 1) -> List[Dict[str, Any]]:
0435 """Suggest next parameter configurations to evaluate.
0436
0437 Args:
0438 n_candidates: Number of candidates to suggest.
0439
0440 Returns:
0441 List of parameter dictionaries, where each dictionary maps
0442 parameter names to their suggested values.
0443
0444 Raises:
0445 RuntimeError: If optimizer is not properly initialized.
0446
0447 Examples:
0448 >>> candidates = optimizer.suggest_candidates(n_candidates=5)
0449 >>> candidates[0]
0450 {'x': 0.5, 'y': 0.3, 'z': 2.1}
0451
0452 Notes:
0453 The implementation should use the configured strategy
0454 (e.g., Sobol sampling, Bayesian optimization, genetic algorithms).
0455 """
0456 pass
0457
0458 @abstractmethod
0459 def update_with_results(
0460 self,
0461 trial_index: int,
0462 parameters: Dict[str, Any],
0463 metrics: Dict[str, float]
0464 ) -> None:
0465 """Update optimizer with evaluation results from a trial.
0466
0467 Args:
0468 trial_index: Unique identifier for the trial.
0469 parameters: Parameter values that were evaluated.
0470 metrics: Objective values obtained from evaluation.
0471 Keys are metric names, values are metric values.
0472
0473 Raises:
0474 ValueError: If metrics don't match expected objectives.
0475
0476 Examples:
0477 >>> optimizer.update_with_results(
0478 ... trial_index=0,
0479 ... parameters={'x': 0.5, 'y': 0.3},
0480 ... metrics={'loss': 0.1, 'accuracy': 0.9}
0481 ... )
0482
0483 Notes:
0484 After updating, the optimizer can use this information to
0485 suggest better candidates in subsequent calls to suggest_candidates().
0486 """
0487 pass
0488
0489 def get_trials(self) -> List[Trial]:
0490 """Return all recorded trials (pending, completed, and failed).
0491
0492 The base implementation reads directly from ``self._trials``, which is
0493 owned by ``BaseOptimizer`` and kept up to date by every backend.
0494 Backends that maintain additional internal state may override this to
0495 include synthetic or reconstructed trials, but doing so is uncommon.
0496
0497 Returns:
0498 List of non-``None`` Trial objects in creation order.
0499
0500 Examples:
0501 >>> done = [t for t in optimizer.get_trials() if t.status == "completed"]
0502 """
0503 return [t for t in self._trials if t is not None]
0504
0505 def set_trial_status(
0506 self,
0507 trial_index: int,
0508 status: str,
0509 *,
0510 parameters: Optional[Dict[str, Any]] = None,
0511 metrics: Optional[Dict[str, float]] = None,
0512 metadata: Optional[Dict[str, Any]] = None,
0513 ) -> Trial:
0514 """Create or update a trial entry with a new lifecycle status.
0515
0516 Args:
0517 trial_index: Unique trial identifier.
0518 status: Trial lifecycle status (for example ``running``,
0519 ``completed``, ``aborted``).
0520 parameters: Optional parameter dictionary to store on the trial.
0521 metrics: Optional objective dictionary to store on the trial.
0522 metadata: Optional metadata to merge into existing metadata.
0523
0524 Returns:
0525 The updated Trial object.
0526
0527 Raises:
0528 ValueError: If ``trial_index`` is negative.
0529 """
0530 if trial_index < 0:
0531 raise ValueError("trial_index must be >= 0")
0532
0533 normalized_status = str(status).strip().lower()
0534 while len(self._trials) <= trial_index:
0535 self._trials.append(None)
0536
0537 existing = self._trials[trial_index]
0538 existing_parameters = existing.parameters if existing else {}
0539 existing_metrics = existing.metrics if existing else None
0540 existing_metadata = dict(existing.metadata) if existing and existing.metadata else {}
0541
0542 trial = Trial(
0543 index=trial_index,
0544 parameters=parameters if parameters is not None else existing_parameters,
0545 metrics=metrics if metrics is not None else existing_metrics,
0546 status=normalized_status,
0547 metadata={**existing_metadata, **(metadata or {})},
0548 )
0549 self._trials[trial_index] = trial
0550 self._trial_counter = max(self._trial_counter, trial_index + 1)
0551 return trial
0552
0553 def get_optimization_results(self) -> Dict[str, Any]:
0554 """Return a normalized optimization-results payload.
0555
0556 Returns:
0557 Dictionary containing objective names and trial records with
0558 parameters, metrics, and both raw and display status labels.
0559 """
0560 trials_payload: List[Dict[str, Any]] = []
0561 for trial in self.get_trials():
0562 trials_payload.append(
0563 {
0564 "trial_index": trial.index,
0565 "status": trial.status,
0566 "display_status": DISPLAY_STATUS_MAP.get(
0567 trial.status,
0568 trial.status.title(),
0569 ),
0570 "design_parameters": dict(trial.parameters or {}),
0571 "objectives": dict(trial.metrics or {}),
0572 "metadata": dict(trial.metadata or {}),
0573 }
0574 )
0575
0576 return {
0577 "objective_names": list(self.objective_names),
0578 "n_objectives": self.n_objectives,
0579 "n_trials": len(trials_payload),
0580 "trials": trials_payload,
0581 }
0582
0583 def save_optimization_results(self, output_path: Union[str, Path]) -> Path:
0584 """Write optimization results to disk as pretty-printed JSON.
0585
0586 Args:
0587 output_path: Target JSON path.
0588
0589 Returns:
0590 Resolved path of the written file.
0591 """
0592 path = Path(output_path)
0593 path.parent.mkdir(parents=True, exist_ok=True)
0594 payload = self.get_optimization_results()
0595 with path.open("w", encoding="utf-8") as handle:
0596 json.dump(payload, handle, indent=2, sort_keys=True)
0597 return path
0598
0599 def seed_from_trials(
0600 self,
0601 trials: List[Trial],
0602 *,
0603 only_completed: bool = True,
0604 ) -> int:
0605 """Inject external trials into the optimizer history without advancing the algorithm.
0606
0607 This is the *backend-switch primitive*. It lets you:
0608
0609 - Seed a new optimizer with results from a previous one (e.g. random
0610 init → MOEA → BO transition).
0611 - Inject prior knowledge before the first ``suggest_candidates`` call.
0612 - Resume an optimisation from a checkpoint produced by a *different*
0613 backend.
0614
0615 Trials are appended to the internal ``_trials`` list with freshly
0616 assigned sequential indices (starting from the current
0617 ``_trial_counter``). The original ``trial.index`` values from the
0618 source optimizer are preserved in each trial's ``metadata`` under the
0619 key ``"source_index"``.
0620
0621 Args:
0622 trials: Iterable of Trial objects to inject. The list may contain
0623 ``None`` placeholders (they are silently skipped).
0624 only_completed: When ``True`` (default), only trials whose
0625 ``status`` is ``"completed"`` are imported. Set to ``False``
0626 to also import ``"pending"`` or ``"failed"`` trials.
0627
0628 Returns:
0629 Number of trials actually injected.
0630
0631 Examples:
0632 >>> # Transfer best results from a random-search warmup:
0633 >>> warmup_trials = random_opt.get_trials()
0634 >>> pymoo_opt.seed_from_trials(warmup_trials)
0635 100
0636 >>> # Now start MOEA generation — the history already has 100 points
0637 >>> candidates = pymoo_opt.suggest_candidates()
0638
0639 Notes:
0640 - This method does NOT advance PyMOO's (or any other backend's)
0641 internal population. The injected trials are purely visible in
0642 the history for Pareto-front and best-trial queries.
0643 - Backends that want to warm-start their internal state from these
0644 trials should override this method.
0645 """
0646 accepted = 0
0647 for trial in trials:
0648 if trial is None:
0649 continue
0650 if only_completed and trial.status != "completed":
0651 continue
0652 new_idx = self._trial_counter
0653 seeded_trial = Trial(
0654 index=new_idx,
0655 parameters=trial.parameters,
0656 metrics=trial.metrics,
0657 status=trial.status,
0658 metadata={**(trial.metadata or {}), "source_index": trial.index},
0659 )
0660 while len(self._trials) <= new_idx:
0661 self._trials.append(None)
0662 self._trials[new_idx] = seeded_trial
0663 self._trial_counter += 1
0664 accepted += 1
0665
0666 if accepted:
0667 logger.debug(
0668 "seed_from_trials: injected %d trial(s) (total history: %d).",
0669 accepted,
0670 self._trial_counter,
0671 )
0672 return accepted
0673
0674 def get_pareto_front(self) -> List[Trial]:
0675 """Retrieve the current Pareto front of non-dominated solutions.
0676
0677 The default implementation delegates to :func:`compute_pareto_front`,
0678 which operates on the trials returned by :meth:`get_trials`. Backends
0679 with native Pareto support (e.g. PyMOO) may override this method to
0680 expose more detailed Pareto metadata.
0681
0682 Returns:
0683 List of Trial objects representing Pareto-optimal solutions.
0684 For single-objective optimisation, returns the single trial with
0685 the best metric value under its configured direction. Returns an
0686 empty list when no completed trials are available.
0687
0688 Examples:
0689 >>> pareto_front = optimizer.get_pareto_front()
0690 >>> for trial in pareto_front:
0691 ... print(f"Params: {trial.parameters}, Metrics: {trial.metrics}")
0692
0693 Notes:
0694 Objective directions are read from ``self.objective_directions``.
0695 """
0696 return compute_pareto_front(
0697 self.get_trials(),
0698 self.objective_names,
0699 self.objective_directions,
0700 )
0701
0702 def get_best_trial(self) -> Optional[Trial]:
0703 """Get the best trial found so far.
0704
0705 For single-objective optimisation, returns the completed trial with the
0706 best metric value under its configured direction. For multi-objective,
0707 returns the first trial from the Pareto front (arbitrary representative;
0708 use :meth:`get_pareto_front` for the full front).
0709
0710 Returns:
0711 Best Trial, or ``None`` if no completed trials exist.
0712
0713 Examples:
0714 >>> best = optimizer.get_best_trial()
0715 >>> if best:
0716 ... print(f"Best parameters: {best.parameters}")
0717 ... print(f"Best metrics: {best.metrics}")
0718 """
0719 front = self.get_pareto_front()
0720 if not front:
0721 return None
0722 if self.n_objectives == 1:
0723 obj = self.objective_names[0]
0724 return min(front, key=lambda t: t.metrics[obj])
0725
0726 return front[0]
0727
0728 @abstractmethod
0729 def serialize_state(self) -> Dict[str, Any]:
0730 """Serialize optimizer state for distributed execution or checkpointing.
0731
0732 Returns:
0733 Dictionary containing all necessary state to reconstruct
0734 the optimizer. Should be JSON-serializable.
0735
0736 Examples:
0737 >>> state = optimizer.serialize_state()
0738 >>> import json
0739 >>> with open('optimizer_state.json', 'w') as f:
0740 ... json.dump(state, f)
0741
0742 Notes:
0743 This is crucial for distributed optimization where optimizer
0744 state needs to be shared across workers or checkpointed.
0745 """
0746 pass
0747
0748 @abstractmethod
0749 def load_state(self, state: Dict[str, Any]) -> None:
0750 """Load optimizer state from serialized form.
0751
0752 Args:
0753 state: Dictionary containing serialized optimizer state,
0754 as returned by serialize_state().
0755
0756 Raises:
0757 ValueError: If state is invalid or incompatible.
0758
0759 Examples:
0760 >>> import json
0761 >>> with open('optimizer_state.json', 'r') as f:
0762 ... state = json.load(f)
0763 >>> optimizer.load_state(state)
0764
0765 Notes:
0766 After loading state, the optimizer should be able to continue
0767 optimization as if it never stopped.
0768 """
0769 pass
0770
0771 def __repr__(self) -> str:
0772 """Return string representation of the optimizer.
0773
0774 Returns:
0775 String describing the optimizer configuration.
0776 """
0777 return (
0778 f"{self.__class__.__name__}("
0779 f"n_params={len(self.search_space.parameters)}, "
0780 f"n_objectives={self.n_objectives}, "
0781 f"seed={self.seed}"
0782 f")"
0783 )