File indexing completed on 2026-08-12 08:24:57
0001 """Regression tests for PyMOO default algorithm inference."""
0002
0003 from __future__ import annotations
0004
0005 import pytest
0006
0007 from aid2e.optimizers.base import SearchSpace
0008 from aid2e.optimizers.pymoo import PyMOOOptimizer, PyMOOOptimizerConfig
0009 from aid2e.utilities import build_optimizer_from_config
0010 from aid2e.utilities.configurations import OptimizerConfiguration
0011 from aid2e.utilities.configurations.problem_config import ProblemConfigLoader
0012
0013
0014 def _make_search_space() -> SearchSpace:
0015 return SearchSpace(
0016 parameters={
0017 "x": {"type": "range", "value": 0.5, "bounds": [0.0, 1.0]},
0018 "y": {"type": "range", "value": 0.5, "bounds": [0.0, 1.0]},
0019 }
0020 )
0021
0022
0023 def _complete_generation(optimizer: PyMOOOptimizer) -> None:
0024 candidates = optimizer.suggest_candidates()
0025 start_index = len(optimizer.get_trials()) - len(candidates)
0026 for offset, parameters in enumerate(candidates):
0027 trial_index = start_index + offset
0028 if optimizer.n_objectives == 1:
0029 metrics = {"loss": float(parameters["x"] + parameters["y"])}
0030 else:
0031 metrics = {
0032 "f1": float(parameters["x"]),
0033 "f2": float(parameters["y"]),
0034 }
0035 optimizer.update_with_results(
0036 trial_index=trial_index,
0037 parameters=parameters,
0038 metrics=metrics,
0039 )
0040
0041
0042 def test_config_resolve_algorithm_defaults() -> None:
0043 config = PyMOOOptimizerConfig(n_iterations=4)
0044 assert config.n_iterations == 4
0045 assert config.resolve_algorithm(1) == "ga"
0046 assert config.resolve_algorithm(2) == "nsga2"
0047
0048
0049 @pytest.mark.parametrize(
0050 ("algorithm", "n_objectives", "message"),
0051 [
0052 ("ga", 2, "single-objective"),
0053 ("nsga2", 1, "single-objective optimization"),
0054 ("nsga3", 1, "single-objective optimization"),
0055 ("moead", 1, "single-objective optimization"),
0056 ],
0057 )
0058 def test_config_rejects_incompatible_explicit_algorithms(
0059 algorithm: str,
0060 n_objectives: int,
0061 message: str,
0062 ) -> None:
0063 config = PyMOOOptimizerConfig(algorithm=algorithm)
0064 with pytest.raises(ValueError, match=message):
0065 config.resolve_algorithm(n_objectives)
0066
0067
0068 def test_single_objective_optimizer_defaults_to_ga_and_round_trips_state() -> None:
0069 optimizer = PyMOOOptimizer(
0070 search_space=_make_search_space(),
0071 config=PyMOOOptimizerConfig(pop_size=4, n_offsprings=4, seed=11),
0072 objective_names=["loss"],
0073 seed=11,
0074 )
0075
0076 assert optimizer.resolved_algorithm == "ga"
0077 assert optimizer._algorithm.__class__.__name__ == "GA"
0078
0079 _complete_generation(optimizer)
0080 _complete_generation(optimizer)
0081
0082 results = optimizer.get_optimization_results()
0083 assert results["n_trials"] == len(optimizer.get_trials()) == 8
0084
0085 state = optimizer.serialize_state()
0086 assert state["resolved_algorithm"] == "ga"
0087
0088 restored = PyMOOOptimizer(
0089 search_space=_make_search_space(),
0090 config=PyMOOOptimizerConfig(pop_size=4, n_offsprings=4, seed=99),
0091 objective_names=["loss"],
0092 seed=99,
0093 )
0094 restored.load_state(state)
0095
0096 assert restored.resolved_algorithm == "ga"
0097 assert restored.get_optimization_results()["n_trials"] == 8
0098 assert len(restored.get_trials()) == 8
0099
0100
0101 def test_multi_objective_optimizer_defaults_to_nsga2() -> None:
0102 optimizer = PyMOOOptimizer(
0103 search_space=_make_search_space(),
0104 config=PyMOOOptimizerConfig(pop_size=4, n_offsprings=4, seed=7),
0105 objective_names=["f1", "f2"],
0106 seed=7,
0107 )
0108
0109 assert optimizer.resolved_algorithm == "nsga2"
0110 assert optimizer._algorithm.__class__.__name__ == "NSGA2"
0111
0112 _complete_generation(optimizer)
0113 _complete_generation(optimizer)
0114
0115 results = optimizer.get_optimization_results()
0116 assert results["n_trials"] == len(optimizer.get_trials()) == 8
0117 assert len(optimizer.get_pareto_front()) >= 1
0118
0119
0120 def test_explicit_incompatible_algorithm_fails_on_optimizer_init() -> None:
0121 with pytest.raises(ValueError, match="single-objective optimization"):
0122 PyMOOOptimizer(
0123 search_space=_make_search_space(),
0124 config=PyMOOOptimizerConfig(algorithm="nsga2", pop_size=4, seed=3),
0125 objective_names=["loss"],
0126 seed=3,
0127 )
0128
0129
0130 def test_runtime_builder_accepts_omitted_pymoo_algorithm(tmp_path) -> None:
0131 output_dir = tmp_path / "output"
0132 work_dir = tmp_path / "work"
0133 output_dir.mkdir()
0134 work_dir.mkdir()
0135
0136 problem_cfg = ProblemConfigLoader.from_dict(
0137 {
0138 "name": "DTLZ2",
0139 "problem_type": "toy",
0140 "output_location": str(output_dir),
0141 "work_location": str(work_dir),
0142 "inline_design": {
0143 "design_space": {
0144 "design_parameters": {
0145 "group1": {
0146 "parameters": {
0147 "x": {"value": 0.5, "bounds": [0.0, 1.0]},
0148 "y": {"value": 0.5, "bounds": [0.0, 1.0]},
0149 }
0150 }
0151 }
0152 }
0153 },
0154 "objectives": [
0155 {"name": "f1", "direction": "minimize"},
0156 {"name": "f2", "direction": "minimize"},
0157 ],
0158 },
0159 )
0160 optimizer_cfg = OptimizerConfiguration(
0161 name="pymoo",
0162 type="evolutionary",
0163 parameters={
0164 "pop_size": 4,
0165 "n_offsprings": 4,
0166 "n_iterations": 2,
0167 "seed": 5,
0168 },
0169 )
0170
0171 optimizer = build_optimizer_from_config(problem_cfg, optimizer_cfg)
0172
0173 assert isinstance(optimizer, PyMOOOptimizer)
0174 assert optimizer.resolved_algorithm == "nsga2"
0175 assert optimizer.config.algorithm is None