File indexing completed on 2026-08-12 08:24:55
0001 """DTLZ2 Optimization with Ax Bayesian Optimizer.
0002
0003 This example demonstrates the DAG Executor with Ax optimizer running the DTLZ2 problem
0004 in two different workflow configurations:
0005
0006 Case 1: Single Branch - Both objectives computed in one Python function
0007 Case 2: Separate Branches - Each objective computed in a separate branch
0008
0009 Configuration:
0010 - 10 Sobol initialization points
0011 - 10 Bayesian optimization iterations
0012 - Batch size of 3 (3 parallel evaluations per iteration)
0013 - Total evaluations: 10 + (10 * 3) = 40 points
0014
0015 Project: AID2E v0.0.0 - AI assisted Detector Design for EIC
0016 """
0017
0018 import numpy as np
0019 import json
0020 from pathlib import Path
0021 from typing import Dict, Any, List
0022
0023 from aid2e.utilities.workflows import (
0024 DAGExecutor,
0025 WorkflowDefinition,
0026 BranchDefinition,
0027 StageDefinition,
0028 JobDefinition,
0029 JobContext,
0030 )
0031 from aid2e.utilities.configurations.objectives import (
0032 ObjectiveDefinition,
0033 ObjectiveDirection,
0034 )
0035 from aid2e.optimizers.base import SearchSpace
0036 from aid2e.optimizers.ax import AxOptimizer, AxOptimizerConfig
0037
0038
0039
0040
0041
0042
0043 def dtlz2_both_objectives(x: List[float]) -> Dict[str, float]:
0044 """Compute both DTLZ2 objectives in one function."""
0045 x = np.array(x)
0046 g = np.sum((x[1:] - 0.5) ** 2)
0047 f1 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.cos(x[1] * np.pi / 2)
0048 f2 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.sin(x[1] * np.pi / 2)
0049 return {"f1": float(f1), "f2": float(f2)}
0050
0051
0052 def dtlz2_f1_only(x: List[float]) -> float:
0053 """Compute only f1 objective of DTLZ2."""
0054 x = np.array(x)
0055 g = np.sum((x[1:] - 0.5) ** 2)
0056 f1 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.cos(x[1] * np.pi / 2)
0057 return float(f1)
0058
0059
0060 def dtlz2_f2_only(x: List[float]) -> float:
0061 """Compute only f2 objective of DTLZ2."""
0062 x = np.array(x)
0063 g = np.sum((x[1:] - 0.5) ** 2)
0064 f2 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.sin(x[1] * np.pi / 2)
0065 return float(f2)
0066
0067
0068
0069
0070
0071
0072 def evaluate_both_objectives_wrapper(context: JobContext) -> Dict[str, float]:
0073 """Wrapper to evaluate both objectives from JobContext."""
0074 design_point = context.design_point
0075 x = [design_point['x1'], design_point['x2'], design_point['x3']]
0076 objectives = dtlz2_both_objectives(x)
0077 context.add_log(f"Design point: {x}")
0078 context.add_log(f"Objectives: {objectives}")
0079 context.xcom_push("objectives", objectives)
0080 return objectives
0081
0082
0083 def evaluate_f1_wrapper(context: JobContext) -> float:
0084 """Wrapper to evaluate f1 from JobContext."""
0085 design_point = context.design_point
0086 x = [design_point['x1'], design_point['x2'], design_point['x3']]
0087 f1 = dtlz2_f1_only(x)
0088 context.add_log(f"Design point: {x}")
0089 context.add_log(f"f1 = {f1}")
0090 context.xcom_push("f1", f1)
0091 return f1
0092
0093
0094 def evaluate_f2_wrapper(context: JobContext) -> float:
0095 """Wrapper to evaluate f2 from JobContext."""
0096 design_point = context.design_point
0097 x = [design_point['x1'], design_point['x2'], design_point['x3']]
0098 f2 = dtlz2_f2_only(x)
0099 context.add_log(f"Design point: {x}")
0100 context.add_log(f"f2 = {f2}")
0101 context.xcom_push("f2", f2)
0102 return f2
0103
0104
0105
0106
0107
0108
0109 def create_single_branch_workflow() -> WorkflowDefinition:
0110 """Create workflow with single branch computing both objectives."""
0111 compute_job = JobDefinition(
0112 name="compute_objectives",
0113 command="python",
0114 payload={
0115 "evaluator_type": "python",
0116 "python_callable": evaluate_both_objectives_wrapper,
0117 "op_args": (),
0118 "op_kwargs": {},
0119 },
0120 )
0121
0122 eval_stage = StageDefinition(name="evaluate", jobs=[compute_job])
0123 main_branch = BranchDefinition(name="main", stages=[eval_stage])
0124
0125 workflow = WorkflowDefinition(
0126 name="dtlz2_ax_single_branch",
0127 description="DTLZ2 with Ax optimizer - single branch",
0128 branches=[main_branch],
0129 objectives=[
0130 ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0131 ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0132 ],
0133 )
0134
0135 return workflow
0136
0137
0138 def create_separate_branches_workflow() -> WorkflowDefinition:
0139 """Create workflow with separate branches for each objective."""
0140
0141 f1_job = JobDefinition(
0142 name="compute_f1",
0143 command="python",
0144 payload={
0145 "evaluator_type": "python",
0146 "python_callable": evaluate_f1_wrapper,
0147 "op_args": (),
0148 "op_kwargs": {},
0149 },
0150 )
0151 f1_stage = StageDefinition(name="evaluate_f1", jobs=[f1_job])
0152 f1_branch = BranchDefinition(name="f1_branch", stages=[f1_stage])
0153
0154
0155 f2_job = JobDefinition(
0156 name="compute_f2",
0157 command="python",
0158 payload={
0159 "evaluator_type": "python",
0160 "python_callable": evaluate_f2_wrapper,
0161 "op_args": (),
0162 "op_kwargs": {},
0163 },
0164 )
0165 f2_stage = StageDefinition(name="evaluate_f2", jobs=[f2_job])
0166 f2_branch = BranchDefinition(name="f2_branch", stages=[f2_stage])
0167
0168 workflow = WorkflowDefinition(
0169 name="dtlz2_ax_separate_branches",
0170 description="DTLZ2 with Ax optimizer - separate branches",
0171 branches=[f1_branch, f2_branch],
0172 objectives=[
0173 ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0174 ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0175 ],
0176 )
0177
0178 return workflow
0179
0180
0181
0182
0183
0184
0185 def run_case_1_single_branch():
0186 """Run Case 1: Single branch with Ax optimizer."""
0187 print("\n" + "="*80)
0188 print("CASE 1: Single Branch with Ax Bayesian Optimizer")
0189 print("="*80)
0190
0191
0192 workflow = create_single_branch_workflow()
0193 print(f"\n✓ Workflow: {workflow.name}")
0194 print(f" Description: {workflow.description}")
0195 print(f" Branches: {len(workflow.branches)}")
0196 print(f" Objectives: {[obj.name for obj in workflow.objectives]}")
0197
0198
0199 executor = DAGExecutor(
0200 workflow=workflow,
0201 base_output_dir="/tmp/dtlz2_ax_optimization/case1",
0202 log_level="WARNING",
0203 )
0204
0205
0206 search_space = SearchSpace(
0207 parameters={
0208 "x1": {"type": "range", "bounds": [0.0, 1.0]},
0209 "x2": {"type": "range", "bounds": [0.0, 1.0]},
0210 "x3": {"type": "range", "bounds": [0.0, 1.0]},
0211 }
0212 )
0213
0214
0215 ax_config = AxOptimizerConfig(
0216 initialization_strategy="sobol",
0217 n_initial_samples=10,
0218 batch_size=3,
0219 generator="BOTORCH_MODULAR",
0220 seed=42,
0221 )
0222
0223
0224 optimizer = AxOptimizer(
0225 search_space=search_space,
0226 config=ax_config,
0227 objective_names=["f1", "f2"],
0228 seed=42,
0229 )
0230
0231 print(f"\n✓ Optimizer: Ax Bayesian Optimizer")
0232 print(f" Initialization: {ax_config.initialization_strategy} ({ax_config.n_initial_samples} points)")
0233 print(f" Generator: {ax_config.generator}")
0234 print(f" Batch Size: {ax_config.batch_size}")
0235 print(f" Total Iterations: 10 Bayesian iterations")
0236
0237
0238 print(f"\n{'Iter':<6} {'Batch':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12} {'Phase':<15}")
0239 print("-" * 95)
0240
0241 trial_index = 0
0242
0243
0244 n_sobol_batches = int(np.ceil(ax_config.n_initial_samples / ax_config.batch_size))
0245 for batch in range(n_sobol_batches):
0246
0247 batch_size = min(ax_config.batch_size, ax_config.n_initial_samples - batch * ax_config.batch_size)
0248
0249
0250 candidates = optimizer.suggest_candidates(n_candidates=batch_size)
0251
0252
0253 for i, design_point in enumerate(candidates):
0254 objectives = executor.execute(design_point)
0255 optimizer.update_with_results(trial_index, design_point, objectives)
0256
0257 print(f"{trial_index+1:<6} {batch+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0258 f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0259 f"{objectives.get('f2', 0):<12.6f} {'Sobol Init':<15}")
0260
0261 trial_index += 1
0262
0263
0264 n_bayesian_iterations = 10
0265 for iteration in range(n_bayesian_iterations):
0266
0267 candidates = optimizer.suggest_candidates(n_candidates=ax_config.batch_size)
0268
0269
0270 for i, design_point in enumerate(candidates):
0271 objectives = executor.execute(design_point)
0272 optimizer.update_with_results(trial_index, design_point, objectives)
0273
0274 print(f"{trial_index+1:<6} {iteration+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0275 f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0276 f"{objectives.get('f2', 0):<12.6f} {'Bayesian':<15}")
0277
0278 trial_index += 1
0279
0280
0281 pareto_front = optimizer.get_pareto_front()
0282
0283 print(f"\n✓ Optimization Complete!")
0284 print(f" Total evaluations: {trial_index}")
0285 print(f" Sobol initialization: {ax_config.n_initial_samples}")
0286 print(f" Bayesian iterations: {n_bayesian_iterations}")
0287 print(f" Pareto front points: {len(pareto_front)}")
0288
0289 print(f"\nPareto Front (non-dominated points):")
0290 print(f"{'Trial':<8} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0291 print("-" * 72)
0292 for trial in pareto_front[:10]:
0293 dp = trial.parameters
0294 obj = trial.metrics if trial.metrics else {}
0295 print(f"{trial.index:<8} {dp.get('x1', 0):<10.4f} {dp.get('x2', 0):<10.4f} {dp.get('x3', 0):<10.4f} "
0296 f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0297
0298 return optimizer, executor
0299
0300
0301 def run_case_2_separate_branches():
0302 """Run Case 2: Separate branches with Ax optimizer."""
0303 print("\n" + "="*80)
0304 print("CASE 2: Separate Branches with Ax Bayesian Optimizer")
0305 print("="*80)
0306
0307
0308 workflow = create_separate_branches_workflow()
0309 print(f"\n✓ Workflow: {workflow.name}")
0310 print(f" Description: {workflow.description}")
0311 print(f" Branches: {len(workflow.branches)} ({[b.name for b in workflow.branches]})")
0312 print(f" Objectives: {[obj.name for obj in workflow.objectives]}")
0313
0314
0315 executor = DAGExecutor(
0316 workflow=workflow,
0317 base_output_dir="/tmp/dtlz2_ax_optimization/case2",
0318 log_level="WARNING",
0319 )
0320
0321
0322 search_space = SearchSpace(
0323 parameters={
0324 "x1": {"type": "range", "bounds": [0.0, 1.0]},
0325 "x2": {"type": "range", "bounds": [0.0, 1.0]},
0326 "x3": {"type": "range", "bounds": [0.0, 1.0]},
0327 }
0328 )
0329
0330
0331 ax_config = AxOptimizerConfig(
0332 initialization_strategy="sobol",
0333 n_initial_samples=10,
0334 batch_size=3,
0335 generator="BOTORCH_MODULAR",
0336 seed=42,
0337 )
0338
0339
0340 optimizer = AxOptimizer(
0341 search_space=search_space,
0342 config=ax_config,
0343 objective_names=["f1", "f2"],
0344 seed=42,
0345 )
0346
0347 print(f"\n✓ Optimizer: Ax Bayesian Optimizer")
0348 print(f" Initialization: {ax_config.initialization_strategy} ({ax_config.n_initial_samples} points)")
0349 print(f" Generator: {ax_config.generator}")
0350 print(f" Batch Size: {ax_config.batch_size}")
0351 print(f" Total Iterations: 10 Bayesian iterations")
0352
0353
0354 print(f"\n{'Iter':<6} {'Batch':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12} {'Phase':<15}")
0355 print("-" * 95)
0356
0357 trial_index = 0
0358
0359
0360 n_sobol_batches = int(np.ceil(ax_config.n_initial_samples / ax_config.batch_size))
0361 for batch in range(n_sobol_batches):
0362 batch_size = min(ax_config.batch_size, ax_config.n_initial_samples - batch * ax_config.batch_size)
0363 candidates = optimizer.suggest_candidates(n_candidates=batch_size)
0364
0365 for i, design_point in enumerate(candidates):
0366 objectives = executor.execute(design_point)
0367 optimizer.update_with_results(trial_index, design_point, objectives)
0368
0369 print(f"{trial_index+1:<6} {batch+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0370 f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0371 f"{objectives.get('f2', 0):<12.6f} {'Sobol Init':<15}")
0372
0373 trial_index += 1
0374
0375
0376 n_bayesian_iterations = 10
0377 for iteration in range(n_bayesian_iterations):
0378 candidates = optimizer.suggest_candidates(n_candidates=ax_config.batch_size)
0379
0380 for i, design_point in enumerate(candidates):
0381 objectives = executor.execute(design_point)
0382 optimizer.update_with_results(trial_index, design_point, objectives)
0383
0384 print(f"{trial_index+1:<6} {iteration+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0385 f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0386 f"{objectives.get('f2', 0):<12.6f} {'Bayesian':<15}")
0387
0388 trial_index += 1
0389
0390
0391 pareto_front = optimizer.get_pareto_front()
0392
0393 print(f"\n✓ Optimization Complete!")
0394 print(f" Total evaluations: {trial_index}")
0395 print(f" Sobol initialization: {ax_config.n_initial_samples}")
0396 print(f" Bayesian iterations: {n_bayesian_iterations}")
0397 print(f" Pareto front points: {len(pareto_front)}")
0398
0399 print(f"\nPareto Front (non-dominated points):")
0400 print(f"{'Trial':<8} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0401 print("-" * 72)
0402 for trial in pareto_front[:10]:
0403 dp = trial.parameters
0404 obj = trial.metrics if trial.metrics else {}
0405 print(f"{trial.index:<8} {dp.get('x1', 0):<10.4f} {dp.get('x2', 0):<10.4f} {dp.get('x3', 0):<10.4f} "
0406 f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0407
0408 return optimizer, executor
0409
0410
0411
0412
0413
0414
0415 if __name__ == "__main__":
0416 print("\n" + "="*80)
0417 print("DTLZ2 Multi-Objective Bayesian Optimization with Ax")
0418 print("="*80)
0419 print("\nConfiguration:")
0420 print(" • 10 Sobol initialization points")
0421 print(" • 10 Bayesian optimization iterations")
0422 print(" • Batch size of 3 (parallel evaluations)")
0423 print(" • Total evaluations: 10 + (10 × 3) = 40 points")
0424 print("\nWorkflow Configurations:")
0425 print(" 1. Single branch - both objectives in one Python function")
0426 print(" 2. Separate branches - each objective in different branch")
0427 print("\nDTLZ2 Problem:")
0428 print(" Variables: x1, x2, x3 in [0, 1]")
0429 print(" Objectives: f1, f2 (minimize both)")
0430 print(" Optimal Pareto front: x1 in [0, 1], x2 = x3 = 0.5")
0431
0432 try:
0433
0434 optimizer1, executor1 = run_case_1_single_branch()
0435 optimizer2, executor2 = run_case_2_separate_branches()
0436
0437
0438 print("\n" + "="*80)
0439 print("COMPARISON SUMMARY")
0440 print("="*80)
0441
0442 print("\nCase 1 (Single Branch):")
0443 print(f" Workflow: {executor1.workflow.name}")
0444 print(f" Branches: {len(executor1.workflow.branches)}")
0445 print(f" Total evaluations: {len(optimizer1.get_trials())}")
0446 print(f" Pareto front size: {len(optimizer1.get_pareto_front())}")
0447 print(f" Output directory: {executor1.output_dir}")
0448
0449 print("\nCase 2 (Separate Branches):")
0450 print(f" Workflow: {executor2.workflow.name}")
0451 print(f" Branches: {len(executor2.workflow.branches)}")
0452 print(f" Total evaluations: {len(optimizer2.get_trials())}")
0453 print(f" Pareto front size: {len(optimizer2.get_pareto_front())}")
0454 print(f" Output directory: {executor2.output_dir}")
0455
0456 print("\n" + "="*80)
0457 print("✅ Ax Optimization Showcase Complete!")
0458 print("="*80)
0459 print("\nKey Results:")
0460 print(" • Both workflow configurations use identical Ax optimizer")
0461 print(" • Bayesian optimization with SAASBO surrogate model")
0462 print(" • qNEHVI acquisition for multi-objective optimization")
0463 print(" • Batch optimization with 3 parallel evaluations per iteration")
0464 print(" • Sobol initialization ensures good space exploration")
0465
0466 except ImportError as e:
0467 print("\n" + "="*80)
0468 print("⚠️ ERROR: Ax Platform Not Installed")
0469 print("="*80)
0470 print(f"\n{e}")
0471 print("\nTo run this showcase, install Ax:")
0472 print(" pip install ax-platform")
0473 print("\nAlternatively, use the simple random optimizer showcase:")
0474 print(" python examples/dtlz2_optimizer_showcase.py")