File indexing completed on 2026-08-12 08:24:55
0001 """DTLZ2 Optimization with Ax Bayesian Optimizer using PanDAiDDS Runner.
0002
0003 This example demonstrates the DAG Executor with Ax optimizer and PanDAiDDS scheduler
0004 running the DTLZ2 problem in two different workflow configurations.
0005
0006 Key Differences from Default Showcase:
0007 - Uses PanDAiDDS scheduler for distributed grid job execution
0008 - Configures PanDA cloud, queue, and resource requirements
0009 - Job names auto-generated as 'user.<username>' (username from system or PANDA_USERNAME env)
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 - PanDAiDDS cloud: US
0017 - PanDAiDDS queue: BNL_PanDA_1
0018
0019 Environment Variables:
0020 - PANDA_USERNAME: Override system username for PanDA job names (optional)
0021
0022 Project: AID2E v0.0.0 - AI assisted Detector Design for EIC
0023 """
0024
0025 import numpy as np
0026 import json
0027 import logging
0028 import time as _time
0029 from pathlib import Path
0030 from typing import Dict, Any, List, Optional
0031
0032 from aid2e.utilities.workflows import (
0033 DAGExecutor,
0034 WorkflowDefinition,
0035 BranchDefinition,
0036 StageDefinition,
0037 JobDefinition,
0038 JobContext,
0039 )
0040
0041
0042 from examples.evaluators.dtlz2 import (
0043 dtlz2_both_objectives,
0044 dtlz2_f1_only,
0045 dtlz2_f2_only,
0046 evaluate_both_objectives_wrapper,
0047 evaluate_f1_wrapper,
0048 evaluate_f2_wrapper,
0049 )
0050 from aid2e.utilities.configurations.objectives import (
0051 ObjectiveDefinition,
0052 ObjectiveDirection,
0053 )
0054 from aid2e.optimizers.base import SearchSpace
0055 from aid2e.optimizers.ax import AxOptimizer, AxOptimizerConfig
0056 from aid2e.schedulers.PanDAiDDS.config import PanDAiDDSRunnerConfig
0057 from aid2e.schedulers.PanDAiDDS.runner import PanDAiDDSScheduler
0058
0059
0060
0061
0062
0063
0064
0065
0066
0067
0068
0069
0070
0071 def create_single_branch_workflow() -> WorkflowDefinition:
0072 """Create workflow with single branch computing both objectives."""
0073 compute_job = JobDefinition(
0074 name="compute_objectives",
0075 command="python",
0076 payload={
0077 "evaluator_type": "python",
0078 "python_callable": evaluate_both_objectives_wrapper,
0079 "op_args": (),
0080 "op_kwargs": {},
0081 },
0082 )
0083
0084 eval_stage = StageDefinition(name="evaluate", jobs=[compute_job])
0085 main_branch = BranchDefinition(name="main", stages=[eval_stage])
0086
0087 workflow = WorkflowDefinition(
0088 name="dtlz2_ax_panda_single_branch",
0089 description="DTLZ2 with Ax optimizer and PanDAiDDS scheduler - single branch",
0090 branches=[main_branch],
0091 objectives=[
0092 ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0093 ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0094 ],
0095 )
0096
0097 return workflow
0098
0099
0100 def create_separate_branches_workflow() -> WorkflowDefinition:
0101 """Create workflow with separate branches for each objective."""
0102
0103 f1_job = JobDefinition(
0104 name="compute_f1",
0105 command="python",
0106 payload={
0107 "evaluator_type": "python",
0108 "python_callable": evaluate_f1_wrapper,
0109 "op_args": (),
0110 "op_kwargs": {},
0111 },
0112 )
0113 f1_stage = StageDefinition(name="evaluate_f1", jobs=[f1_job])
0114 f1_branch = BranchDefinition(name="f1_branch", stages=[f1_stage])
0115
0116
0117 f2_job = JobDefinition(
0118 name="compute_f2",
0119 command="python",
0120 payload={
0121 "evaluator_type": "python",
0122 "python_callable": evaluate_f2_wrapper,
0123 "op_args": (),
0124 "op_kwargs": {},
0125 },
0126 )
0127 f2_stage = StageDefinition(name="evaluate_f2", jobs=[f2_job])
0128 f2_branch = BranchDefinition(name="f2_branch", stages=[f2_stage])
0129
0130 workflow = WorkflowDefinition(
0131 name="dtlz2_ax_panda_separate_branches",
0132 description="DTLZ2 with Ax optimizer and PanDAiDDS scheduler - separate branches",
0133 branches=[f1_branch, f2_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 run_pool_optimization(
0144 max_parallel: int = 5,
0145 total_evaluations: int = 40,
0146 n_initial_samples: int = 10,
0147 poll_interval: float = 5.0,
0148 ) -> AxOptimizer:
0149 """Run a pool-based asynchronous optimization loop on PanDA.
0150
0151 Keeps up to ``max_parallel`` design points running concurrently. When a job
0152 finishes, results are fed to Ax and a new candidate is submitted until the
0153 total evaluation budget is exhausted.
0154 """
0155
0156
0157 workflow = create_single_branch_workflow()
0158 panda_config = PanDAiDDSRunnerConfig(
0159 cloud="US",
0160 queue="BNL_PanDA_1",
0161 max_walltime=3600,
0162 core_count=1,
0163 total_memory=4000,
0164 enable_separate_log=True,
0165 init_env="source setup_aid2e.sh && bash install_aid2e_dependencies.sh; ",
0166 job_dir=str(Path.cwd() / "panda_jobs" / "pool"),
0167 )
0168
0169 scheduler = PanDAiDDSScheduler(config=panda_config)
0170
0171
0172 search_space = SearchSpace(
0173 parameters={
0174 "x1": {"type": "range", "bounds": [0.0, 1.0]},
0175 "x2": {"type": "range", "bounds": [0.0, 1.0]},
0176 "x3": {"type": "range", "bounds": [0.0, 1.0]},
0177 }
0178 )
0179
0180 ax_config = AxOptimizerConfig(
0181 initialization_strategy="sobol",
0182 n_initial_samples=n_initial_samples,
0183 batch_size=1,
0184 generator="BOTORCH_MODULAR",
0185 seed=42,
0186 )
0187
0188 optimizer = AxOptimizer(
0189 search_space=search_space,
0190 config=ax_config,
0191 objective_names=["f1", "f2"],
0192 seed=42,
0193 )
0194
0195 logger = logging.getLogger("dtlz2_panda_pool")
0196 logger.info(
0197 "Starting pool optimization: max_parallel=%d, total_evaluations=%d, n_initial_samples=%d",
0198 max_parallel,
0199 total_evaluations,
0200 n_initial_samples,
0201 )
0202
0203
0204 running: Dict[str, Dict[str, Any]] = {}
0205 finished = 0
0206 trial_index = 0
0207
0208 def _submit_one() -> Optional[str]:
0209 nonlocal trial_index
0210 if finished + len(running) >= total_evaluations:
0211 return None
0212
0213
0214 if trial_index < n_initial_samples:
0215 candidates = optimizer.suggest_candidates(n_candidates=1)
0216 else:
0217 candidates = optimizer.suggest_candidates(n_candidates=1)
0218
0219 dp = candidates[0]
0220 job_name = f"dp_{trial_index}"
0221 job_id = f"pool_{job_name}_{trial_index}"
0222
0223 job_def = {
0224 "name": job_name,
0225 "function": dtlz2_both_objectives,
0226 "params": {"x": [dp["x1"], dp["x2"], dp["x3"]]},
0227 }
0228
0229 logger.info("Submitting job %s for trial %d", job_id, trial_index)
0230 job_def["job_id"] = job_id
0231 scheduler.submit_job("pool_stage", job_def, working_dir=None)
0232
0233 running[job_id] = {"trial_index": trial_index, "design_point": dp}
0234 trial_index += 1
0235 return job_id
0236
0237
0238 while len(running) < max_parallel and (finished + len(running)) < total_evaluations:
0239 _submit_one()
0240
0241
0242 logger.info("Pool initialised with %d jobs", len(running))
0243
0244 while finished < total_evaluations:
0245 _time.sleep(poll_interval)
0246
0247 for job_id in list(running.keys()):
0248 job = {"job_id": job_id, "stage_name": "pool_stage"}
0249 try:
0250 scheduler.check_single_job_status(job, job_context=None)
0251 except Exception:
0252
0253 continue
0254
0255
0256 stage_funcs = scheduler.running_funcs.get("pool_stage", {})
0257 if job_id in stage_funcs:
0258 continue
0259
0260 info = running.pop(job_id)
0261 t_idx = info["trial_index"]
0262 dp = info["design_point"]
0263
0264
0265
0266 objectives = dtlz2_both_objectives([dp["x1"], dp["x2"], dp["x3"]])
0267 optimizer.update_with_results(t_idx, dp, objectives)
0268
0269 logger.info(
0270 "Completed trial %d: x=(%.4f, %.4f, %.4f), f1=%.6f, f2=%.6f",
0271 t_idx,
0272 dp["x1"],
0273 dp["x2"],
0274 dp["x3"],
0275 objectives["f1"],
0276 objectives["f2"],
0277 )
0278
0279 finished += 1
0280
0281
0282 if len(running) < max_parallel and (finished + len(running)) < total_evaluations:
0283 _submit_one()
0284
0285 logger.info("Pool optimisation finished: %d evaluations", finished)
0286 return optimizer
0287
0288
0289 def run_case_2_separate_branches():
0290 """Run Case 2: Separate branches with Ax optimizer and PanDAiDDS scheduler."""
0291 print("\n" + "="*80)
0292 print("CASE 2: Separate Branches with Ax Bayesian Optimizer + PanDAiDDS Scheduler")
0293 print("="*80)
0294
0295
0296 workflow = create_separate_branches_workflow()
0297 print(f"\n✓ Workflow: {workflow.name}")
0298 print(f" Description: {workflow.description}")
0299 print(f" Branches: {len(workflow.branches)} ({[b.name for b in workflow.branches]})")
0300 print(f" Objectives: {[obj.name for obj in workflow.objectives]}")
0301
0302
0303
0304
0305
0306 panda_config = PanDAiDDSRunnerConfig(
0307
0308 cloud="US",
0309 queue="BNL_PanDA_1",
0310 max_walltime=3600,
0311 core_count=1,
0312 total_memory=2000,
0313 enable_separate_log=True,
0314 job_dir=str(Path.cwd() / "panda_jobs" / "case2"),
0315 post_script="rm -fr .src .venv .local src examples ",
0316 )
0317
0318 print(f"\n✓ Scheduler: PanDAiDDS")
0319 print(f" Workers: {panda_config.core_count} ")
0320 print(f" Backend: {panda_config.queue}")
0321 print(f" Timeout: {panda_config.max_walltime}s per job")
0322
0323
0324 executor = DAGExecutor(
0325 workflow=workflow,
0326 base_output_dir="/tmp/dtlz2_ax_panda_optimization/case2",
0327 log_level="WARNING",
0328 scheduler_config={
0329 "runner_type": "PanDAiDDSRunner",
0330 "config": panda_config,
0331 },
0332 )
0333
0334
0335 search_space = SearchSpace(
0336 parameters={
0337 "x1": {"type": "range", "bounds": [0.0, 1.0]},
0338 "x2": {"type": "range", "bounds": [0.0, 1.0]},
0339 "x3": {"type": "range", "bounds": [0.0, 1.0]},
0340 }
0341 )
0342
0343
0344 ax_config = AxOptimizerConfig(
0345 initialization_strategy="sobol",
0346 n_initial_samples=10,
0347 batch_size=3,
0348 generator="BOTORCH_MODULAR",
0349 seed=42,
0350 )
0351
0352
0353 optimizer = AxOptimizer(
0354 search_space=search_space,
0355 config=ax_config,
0356 objective_names=["f1", "f2"],
0357 seed=42,
0358 )
0359
0360 print(f"\n✓ Optimizer: Ax Bayesian Optimizer")
0361 print(f" Initialization: {ax_config.initialization_strategy} ({ax_config.n_initial_samples} points)")
0362 print(f" Generator: {ax_config.generator}")
0363 print(f" Batch Size: {ax_config.batch_size}")
0364 print(f" Total Iterations: 10 Bayesian iterations")
0365
0366
0367 print(f"\n{'Iter':<6} {'Batch':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12} {'Phase':<15}")
0368 print("-" * 95)
0369
0370 trial_index = 0
0371
0372
0373 n_sobol_batches = int(np.ceil(ax_config.n_initial_samples / ax_config.batch_size))
0374 for batch in range(n_sobol_batches):
0375 batch_size = min(ax_config.batch_size, ax_config.n_initial_samples - batch * ax_config.batch_size)
0376 candidates = optimizer.suggest_candidates(n_candidates=batch_size)
0377
0378 for i, design_point in enumerate(candidates):
0379 objectives = executor.execute(design_point)
0380 optimizer.update_with_results(trial_index, design_point, objectives)
0381
0382 print(f"{trial_index+1:<6} {batch+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0383 f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0384 f"{objectives.get('f2', 0):<12.6f} {'Sobol Init':<15}")
0385
0386 trial_index += 1
0387
0388
0389 n_bayesian_iterations = 10
0390 for iteration in range(n_bayesian_iterations):
0391 candidates = optimizer.suggest_candidates(n_candidates=ax_config.batch_size)
0392
0393 for i, design_point in enumerate(candidates):
0394 objectives = executor.execute(design_point)
0395 optimizer.update_with_results(trial_index, design_point, objectives)
0396
0397 print(f"{trial_index+1:<6} {iteration+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0398 f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0399 f"{objectives.get('f2', 0):<12.6f} {'Bayesian':<15}")
0400
0401 trial_index += 1
0402
0403
0404 pareto_front = optimizer.get_pareto_front()
0405
0406 print(f"\n✓ Optimization Complete!")
0407 print(f" Total evaluations: {trial_index}")
0408 print(f" Sobol initialization: {ax_config.n_initial_samples}")
0409 print(f" Bayesian iterations: {n_bayesian_iterations}")
0410 print(f" Pareto front points: {len(pareto_front)}")
0411
0412 print(f"\nPareto Front (non-dominated points):")
0413 print(f"{'Trial':<8} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0414 print("-" * 72)
0415 for trial in pareto_front[:10]:
0416 dp = trial.parameters
0417 obj = trial.metrics if trial.metrics else {}
0418 print(f"{trial.index:<8} {dp.get('x1', 0):<10.4f} {dp.get('x2', 0):<10.4f} {dp.get('x3', 0):<10.4f} "
0419 f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0420
0421 return optimizer, executor
0422
0423
0424 if __name__ == "__main__":
0425 logging.basicConfig(
0426 level=logging.INFO,
0427 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
0428 datefmt="%Y-%m-%d %H:%M:%S",
0429 )
0430
0431 print("\n" + "=" * 80)
0432 print("DTLZ2 Pool-based Optimization with Ax + PanDAiDDS Scheduler")
0433 print("=" * 80)
0434 print("\nConfiguration:")
0435 print(" • Total evaluations: 40")
0436 print(" • Initial Sobol points: 10")
0437 print(" • Pool size (max_parallel): 5")
0438
0439 try:
0440 optimizer = run_pool_optimization(
0441 max_parallel=5,
0442 total_evaluations=40,
0443 n_initial_samples=10,
0444 poll_interval=5.0,
0445 )
0446
0447 pareto_front = optimizer.get_pareto_front()
0448 print("\nPareto front size:", len(pareto_front))
0449
0450 except ImportError as e:
0451 print("\n" + "=" * 80)
0452 print("⚠️ ERROR: Missing Dependencies")
0453 print("=" * 80)
0454 print(f"\n{e}")
0455 print("\nTo run this showcase, install required packages:")
0456 print(" pip install ax-platform panda")
0457 print("\nAlternatively, use the simple random optimizer showcase:")
0458 print(" python examples/dtlz2_optimizer_showcase.py")