Back to home page

EIC code displayed by LXR

 
 

    


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 # Import evaluator functions from examples.evaluators.dtlz2
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 # DTLZ2 Problem Implementation
0062 # =============================================================================
0063 
0064 
0065 
0066 
0067 # =============================================================================
0068 # Workflow Definitions
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     # Branch 1: f1
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     # Branch 2: f2
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     # Scheduler configuration (single-branch workflow)
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     # Search space and optimizer
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     # State
0204     running: Dict[str, Dict[str, Any]] = {}  # job_id -> {trial_index, design_point}
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         # Use Sobol first, then BO
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     # Fill initial pool
0238     while len(running) < max_parallel and (finished + len(running)) < total_evaluations:
0239         _submit_one()
0240 
0241     # Main loop
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                 # still running or transient issue
0253                 continue
0254 
0255             # If job removed from running_funcs, it's done
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             # Retrieve results from scheduler.jobs mapping is not fully wired;
0265             # fall back to local evaluation of objectives using dtlz2_both_objectives.
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             # Refill pool
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     # Create workflow
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     # Configure PanDAiDDS scheduler
0303     # Note: 'name' will be auto-generated as 'user.<username>.aid2e_job'
0304     # You can override the username via PANDA_USERNAME environment variable
0305     # Or provide a custom name (must start with 'user.')
0306     panda_config = PanDAiDDSRunnerConfig(
0307         # name="user.wguan.dtlz2_ax_panda_case2",  # Or omit to auto-generate
0308         cloud="US",  # PanDA cloud
0309         queue="BNL_PanDA_1",  # PanDA queue
0310         max_walltime=3600,  # 1 hour
0311         core_count=1,  # CPU cores per job
0312         total_memory=2000,  # MB per job
0313         enable_separate_log=True,
0314         job_dir=str(Path.cwd() / "panda_jobs" / "case2"),
0315         post_script="rm -fr .src .venv .local src examples ",  # Clean up source files after job completion to save space (optional, use with caution)
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     # Create executor with PanDAiDDS scheduler
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     # Create search space
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     # Create Ax optimizer configuration (same as Case 1)
0344     ax_config = AxOptimizerConfig(
0345         initialization_strategy="sobol",
0346         n_initial_samples=10,
0347         batch_size=3,
0348         generator="BOTORCH_MODULAR",
0349         seed=42,  # Same seed for reproducibility
0350     )
0351     
0352     # Create Ax optimizer
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     # Optimization loop
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     # Phase 1: Sobol initialization
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     # Phase 2: Bayesian optimization
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     # Get Pareto front
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]:  # Show top 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")