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