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 from pathlib import Path
0029 from typing import Dict, Any, List
0030 
0031 from aid2e.utilities.workflows import (
0032     DAGExecutor,
0033     WorkflowDefinition,
0034     BranchDefinition,
0035     StageDefinition,
0036     JobDefinition,
0037     JobContext,
0038 )
0039 
0040 # Import evaluator functions from examples.evaluators.dtlz2
0041 from examples.evaluators.dtlz2 import (
0042     dtlz2_both_objectives,
0043     dtlz2_f1_only,
0044     dtlz2_f2_only,
0045     evaluate_both_objectives_wrapper,
0046     evaluate_f1_wrapper,
0047     evaluate_f2_wrapper,
0048 )
0049 from aid2e.utilities.configurations.objectives import (
0050     ObjectiveDefinition,
0051     ObjectiveDirection,
0052 )
0053 from aid2e.optimizers.base import SearchSpace
0054 from aid2e.optimizers.ax import AxOptimizer, AxOptimizerConfig
0055 from aid2e.schedulers.PanDAiDDS.config import PanDAiDDSRunnerConfig
0056 
0057 
0058 # =============================================================================
0059 # DTLZ2 Problem Implementation
0060 # =============================================================================
0061 
0062 
0063 
0064 
0065 # =============================================================================
0066 # Workflow Definitions
0067 # =============================================================================
0068 
0069 def create_single_branch_workflow() -> WorkflowDefinition:
0070     """Create workflow with single branch computing both objectives."""
0071     compute_job = JobDefinition(
0072         name="compute_objectives",
0073         command="python",
0074         payload={
0075             "evaluator_type": "python",
0076             "python_callable": evaluate_both_objectives_wrapper,
0077             "op_args": (),
0078             "op_kwargs": {},
0079         },
0080     )
0081     
0082     eval_stage = StageDefinition(name="evaluate", jobs=[compute_job])
0083     main_branch = BranchDefinition(name="main", stages=[eval_stage])
0084     
0085     workflow = WorkflowDefinition(
0086         name="dtlz2_ax_panda_single_branch",
0087         description="DTLZ2 with Ax optimizer and PanDAiDDS scheduler - single branch",
0088         branches=[main_branch],
0089         objectives=[
0090             ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0091             ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0092         ],
0093     )
0094     
0095     return workflow
0096 
0097 
0098 def create_separate_branches_workflow() -> WorkflowDefinition:
0099     """Create workflow with separate branches for each objective."""
0100     # Branch 1: f1
0101     f1_job = JobDefinition(
0102         name="compute_f1",
0103         command="python",
0104         payload={
0105             "evaluator_type": "python",
0106             "python_callable": evaluate_f1_wrapper,
0107             "op_args": (),
0108             "op_kwargs": {},
0109         },
0110     )
0111     f1_stage = StageDefinition(name="evaluate_f1", jobs=[f1_job])
0112     f1_branch = BranchDefinition(name="f1_branch", stages=[f1_stage])
0113     
0114     # Branch 2: f2
0115     f2_job = JobDefinition(
0116         name="compute_f2",
0117         command="python",
0118         payload={
0119             "evaluator_type": "python",
0120             "python_callable": evaluate_f2_wrapper,
0121             "op_args": (),
0122             "op_kwargs": {},
0123         },
0124     )
0125     f2_stage = StageDefinition(name="evaluate_f2", jobs=[f2_job])
0126     f2_branch = BranchDefinition(name="f2_branch", stages=[f2_stage])
0127     
0128     workflow = WorkflowDefinition(
0129         name="dtlz2_ax_panda_separate_branches",
0130         description="DTLZ2 with Ax optimizer and PanDAiDDS scheduler - separate branches",
0131         branches=[f1_branch, f2_branch],
0132         objectives=[
0133             ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0134             ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0135         ],
0136     )
0137     
0138     return workflow
0139 
0140 
0141 # =============================================================================
0142 # Optimization Functions
0143 # =============================================================================
0144 
0145 def run_case_1_single_branch():
0146     """Run Case 1: Single branch with Ax optimizer and PanDAiDDS scheduler."""
0147     print("\n" + "="*80)
0148     print("CASE 1: Single Branch with Ax Bayesian Optimizer + PanDAiDDS Scheduler")
0149     print("="*80)
0150     
0151     # Create workflow
0152     workflow = create_single_branch_workflow()
0153     print(f"\n✓ Workflow: {workflow.name}")
0154     print(f"  Description: {workflow.description}")
0155     print(f"  Branches: {len(workflow.branches)}")
0156     print(f"  Objectives: {[obj.name for obj in workflow.objectives]}")
0157     
0158     # Configure PanDAiDDS scheduler
0159     # Note: 'name' will be auto-generated as 'user.<username>.aid2e_job'
0160     # You can override the username via PANDA_USERNAME environment variable
0161     # Or provide a custom name (must start with 'user.')
0162     panda_config = PanDAiDDSRunnerConfig(
0163         # name="user.wguan.dtlz2_ax_panda_case1",  # Or omit to auto-generate
0164         cloud="US",  # PanDA cloud
0165         queue="BNL_PanDA_1",  # PanDA queue # BNL_OSG_PanDA_1, BNL_PanDA_1
0166         max_walltime=3600,  # 1 hour
0167         core_count=1,  # CPU cores per job
0168         total_memory=4000,  # MB per job
0169         enable_separate_log=True,
0170         init_env="source setup_aid2e.sh && bash install_aid2e_dependencies.sh; ",  # Ensure environment is set up on remote workers
0171         job_dir=str(Path.cwd() / "panda_jobs" / "case1"),
0172         post_script="rm -fr .src .venv .local src examples ",  # Clean up source files after job completion to save space (optional, use with caution)
0173     )
0174     
0175     print(f"\n✓ Scheduler: PanDAiDDS")
0176     print(f"  Workers: {panda_config.core_count} ")
0177     print(f"  Backend: {panda_config.queue}")
0178     print(f"  Timeout: {panda_config.max_walltime}s per job")
0179     
0180     # Create executor with PanDAiDDS scheduler
0181     executor = DAGExecutor(
0182         workflow=workflow,
0183         base_output_dir="/tmp/dtlz2_ax_panda_optimization/case1",
0184         log_level="WARNING",
0185         scheduler_config={
0186             "runner_type": "PanDAiDDSRunner",
0187             "config": panda_config,
0188         },
0189     )
0190     
0191     # Create search space
0192     search_space = SearchSpace(
0193         parameters={
0194             "x1": {"type": "range", "bounds": [0.0, 1.0]},
0195             "x2": {"type": "range", "bounds": [0.0, 1.0]},
0196             "x3": {"type": "range", "bounds": [0.0, 1.0]},
0197         }
0198     )
0199     
0200     # Create Ax optimizer configuration
0201     ax_config = AxOptimizerConfig(
0202         initialization_strategy="sobol",
0203         n_initial_samples=10,
0204         batch_size=3,
0205         generator="BOTORCH_MODULAR",
0206         seed=42,
0207     )
0208     
0209     # Create Ax optimizer
0210     optimizer = AxOptimizer(
0211         search_space=search_space,
0212         config=ax_config,
0213         objective_names=["f1", "f2"],
0214         seed=42,
0215     )
0216     
0217     print(f"\n✓ Optimizer: Ax Bayesian Optimizer")
0218     print(f"  Initialization: {ax_config.initialization_strategy} ({ax_config.n_initial_samples} points)")
0219     print(f"  Generator: {ax_config.generator}")
0220     print(f"  Batch Size: {ax_config.batch_size}")
0221     print(f"  Total Iterations: 10 Bayesian iterations")
0222     
0223     # Optimization loop
0224     print(f"\n{'Iter':<6} {'Batch':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12} {'Phase':<15}")
0225     print("-" * 95)
0226     
0227     trial_index = 0
0228     
0229     # Phase 1: Sobol initialization (10 points in batches of 3)
0230     n_sobol_batches = int(np.ceil(ax_config.n_initial_samples / ax_config.batch_size))
0231     for batch in range(n_sobol_batches):
0232         # Get batch size (might be less than 3 for last batch)
0233         batch_size = min(ax_config.batch_size, ax_config.n_initial_samples - batch * ax_config.batch_size)
0234         
0235         # Suggest candidates
0236         candidates = optimizer.suggest_candidates(n_candidates=batch_size)
0237         
0238         # Evaluate each candidate (PanDAiDDS handles parallelism internally)
0239         for i, design_point in enumerate(candidates):
0240             objectives = executor.execute(design_point)
0241             optimizer.update_with_results(trial_index, design_point, objectives)
0242             
0243             print(f"{trial_index+1:<6} {batch+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0244                   f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0245                   f"{objectives.get('f2', 0):<12.6f} {'Sobol Init':<15}")
0246             
0247             trial_index += 1
0248     
0249     # Phase 2: Bayesian optimization (10 iterations x 3 points = 30 points)
0250     n_bayesian_iterations = 10
0251     for iteration in range(n_bayesian_iterations):
0252         # Suggest batch of candidates
0253         candidates = optimizer.suggest_candidates(n_candidates=ax_config.batch_size)
0254         
0255         # Evaluate each candidate
0256         for i, design_point in enumerate(candidates):
0257             objectives = executor.execute(design_point)
0258             optimizer.update_with_results(trial_index, design_point, objectives)
0259             
0260             print(f"{trial_index+1:<6} {iteration+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0261                   f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0262                   f"{objectives.get('f2', 0):<12.6f} {'Bayesian':<15}")
0263             
0264             trial_index += 1
0265     
0266     # Get Pareto front
0267     pareto_front = optimizer.get_pareto_front()
0268     
0269     print(f"\n✓ Optimization Complete!")
0270     print(f"  Total evaluations: {trial_index}")
0271     print(f"  Sobol initialization: {ax_config.n_initial_samples}")
0272     print(f"  Bayesian iterations: {n_bayesian_iterations}")
0273     print(f"  Pareto front points: {len(pareto_front)}")
0274     
0275     print(f"\nPareto Front (non-dominated points):")
0276     print(f"{'Trial':<8} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0277     print("-" * 72)
0278     for trial in pareto_front[:10]:  # Show top 10
0279         dp = trial.parameters
0280         obj = trial.metrics if trial.metrics else {}
0281         print(f"{trial.index:<8} {dp.get('x1', 0):<10.4f} {dp.get('x2', 0):<10.4f} {dp.get('x3', 0):<10.4f} "
0282               f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0283     
0284     return optimizer, executor
0285 
0286 
0287 def run_case_2_separate_branches():
0288     """Run Case 2: Separate branches with Ax optimizer and PanDAiDDS scheduler."""
0289     print("\n" + "="*80)
0290     print("CASE 2: Separate Branches with Ax Bayesian Optimizer + PanDAiDDS Scheduler")
0291     print("="*80)
0292     
0293     # Create workflow
0294     workflow = create_separate_branches_workflow()
0295     print(f"\n✓ Workflow: {workflow.name}")
0296     print(f"  Description: {workflow.description}")
0297     print(f"  Branches: {len(workflow.branches)} ({[b.name for b in workflow.branches]})")
0298     print(f"  Objectives: {[obj.name for obj in workflow.objectives]}")
0299     
0300     # Configure PanDAiDDS scheduler
0301     # Note: 'name' will be auto-generated as 'user.<username>.aid2e_job'
0302     # You can override the username via PANDA_USERNAME environment variable
0303     # Or provide a custom name (must start with 'user.')
0304     panda_config = PanDAiDDSRunnerConfig(
0305         # name="user.wguan.dtlz2_ax_panda_case2",  # Or omit to auto-generate
0306         cloud="US",  # PanDA cloud
0307         queue="BNL_PanDA_1",  # PanDA queue
0308         max_walltime=3600,  # 1 hour
0309         core_count=1,  # CPU cores per job
0310         total_memory=2000,  # MB per job
0311         enable_separate_log=True,
0312         job_dir=str(Path.cwd() / "panda_jobs" / "case2"),
0313         post_script="rm -fr .src .venv .local src examples ",  # Clean up source files after job completion to save space (optional, use with caution)
0314     )
0315     
0316     print(f"\n✓ Scheduler: PanDAiDDS")
0317     print(f"  Workers: {panda_config.core_count} ")
0318     print(f"  Backend: {panda_config.queue}")
0319     print(f"  Timeout: {panda_config.max_walltime}s per job")
0320     
0321     # Create executor with PanDAiDDS scheduler
0322     executor = DAGExecutor(
0323         workflow=workflow,
0324         base_output_dir="/tmp/dtlz2_ax_panda_optimization/case2",
0325         log_level="WARNING",
0326         scheduler_config={
0327             "runner_type": "PanDAiDDSRunner",
0328             "config": panda_config,
0329         },
0330     )
0331     
0332     # Create search space
0333     search_space = SearchSpace(
0334         parameters={
0335             "x1": {"type": "range", "bounds": [0.0, 1.0]},
0336             "x2": {"type": "range", "bounds": [0.0, 1.0]},
0337             "x3": {"type": "range", "bounds": [0.0, 1.0]},
0338         }
0339     )
0340     
0341     # Create Ax optimizer configuration (same as Case 1)
0342     ax_config = AxOptimizerConfig(
0343         initialization_strategy="sobol",
0344         n_initial_samples=10,
0345         batch_size=3,
0346         generator="BOTORCH_MODULAR",
0347         seed=42,  # Same seed for reproducibility
0348     )
0349     
0350     # Create Ax optimizer
0351     optimizer = AxOptimizer(
0352         search_space=search_space,
0353         config=ax_config,
0354         objective_names=["f1", "f2"],
0355         seed=42,
0356     )
0357     
0358     print(f"\n✓ Optimizer: Ax Bayesian Optimizer")
0359     print(f"  Initialization: {ax_config.initialization_strategy} ({ax_config.n_initial_samples} points)")
0360     print(f"  Generator: {ax_config.generator}")
0361     print(f"  Batch Size: {ax_config.batch_size}")
0362     print(f"  Total Iterations: 10 Bayesian iterations")
0363     
0364     # Optimization loop
0365     print(f"\n{'Iter':<6} {'Batch':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12} {'Phase':<15}")
0366     print("-" * 95)
0367     
0368     trial_index = 0
0369     
0370     # Phase 1: Sobol initialization
0371     n_sobol_batches = int(np.ceil(ax_config.n_initial_samples / ax_config.batch_size))
0372     for batch in range(n_sobol_batches):
0373         batch_size = min(ax_config.batch_size, ax_config.n_initial_samples - batch * ax_config.batch_size)
0374         candidates = optimizer.suggest_candidates(n_candidates=batch_size)
0375         
0376         for i, design_point in enumerate(candidates):
0377             objectives = executor.execute(design_point)
0378             optimizer.update_with_results(trial_index, design_point, objectives)
0379             
0380             print(f"{trial_index+1:<6} {batch+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0381                   f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0382                   f"{objectives.get('f2', 0):<12.6f} {'Sobol Init':<15}")
0383             
0384             trial_index += 1
0385     
0386     # Phase 2: Bayesian optimization
0387     n_bayesian_iterations = 10
0388     for iteration in range(n_bayesian_iterations):
0389         candidates = optimizer.suggest_candidates(n_candidates=ax_config.batch_size)
0390         
0391         for i, design_point in enumerate(candidates):
0392             objectives = executor.execute(design_point)
0393             optimizer.update_with_results(trial_index, design_point, objectives)
0394             
0395             print(f"{trial_index+1:<6} {iteration+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0396                   f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0397                   f"{objectives.get('f2', 0):<12.6f} {'Bayesian':<15}")
0398             
0399             trial_index += 1
0400     
0401     # Get Pareto front
0402     pareto_front = optimizer.get_pareto_front()
0403     
0404     print(f"\n✓ Optimization Complete!")
0405     print(f"  Total evaluations: {trial_index}")
0406     print(f"  Sobol initialization: {ax_config.n_initial_samples}")
0407     print(f"  Bayesian iterations: {n_bayesian_iterations}")
0408     print(f"  Pareto front points: {len(pareto_front)}")
0409     
0410     print(f"\nPareto Front (non-dominated points):")
0411     print(f"{'Trial':<8} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0412     print("-" * 72)
0413     for trial in pareto_front[:10]:  # Show top 10
0414         dp = trial.parameters
0415         obj = trial.metrics if trial.metrics else {}
0416         print(f"{trial.index:<8} {dp.get('x1', 0):<10.4f} {dp.get('x2', 0):<10.4f} {dp.get('x3', 0):<10.4f} "
0417               f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0418     
0419     return optimizer, executor
0420 
0421 
0422 # =============================================================================
0423 # Main Entry Point
0424 # =============================================================================
0425 
0426 if __name__ == "__main__":
0427     # Configure logging
0428     logging.basicConfig(
0429         level=logging.DEBUG,
0430         format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
0431         datefmt='%Y-%m-%d %H:%M:%S'
0432     )
0433 
0434     # All main workflow logic is guarded here
0435     print("\n" + "="*80)
0436     print("DTLZ2 Multi-Objective Bayesian Optimization with Ax + PanDAiDDS Scheduler")
0437     print("="*80)
0438     print("\nConfiguration:")
0439     print("  • 10 Sobol initialization points")
0440     print("  • 10 Bayesian optimization iterations")
0441     print("  • Batch size of 3 (parallel evaluations)")
0442     print("  • Total evaluations: 10 + (10 × 3) = 40 points")
0443     print("\nScheduler:")
0444     print("  • PanDAiDDS for local parallel execution")
0445     print("  • Backend: loky (safe multiprocessing)")
0446     print("  • Workers: all available CPUs")
0447     print("\nWorkflow Configurations:")
0448     print("  1. Single branch - both objectives in one Python function")
0449     print("  2. Separate branches - each objective in different branch")
0450     print("\nDTLZ2 Problem:")
0451     print("  Variables: x1, x2, x3 in [0, 1]")
0452     print("  Objectives: f1, f2 (minimize both)")
0453     print("  Optimal Pareto front: x1 in [0, 1], x2 = x3 = 0.5")
0454 
0455     try:
0456         # Run both cases
0457         optimizer1, executor1 = run_case_1_single_branch()
0458         optimizer2, executor2 = run_case_2_separate_branches()
0459 
0460         # Summary
0461         print("\n" + "="*80)
0462         print("COMPARISON SUMMARY")
0463         print("="*80)
0464 
0465         print("\nCase 1 (Single Branch):")
0466         print(f"  Workflow: {executor1.workflow.name}")
0467         print(f"  Branches: {len(executor1.workflow.branches)}")
0468         print(f"  Total evaluations: {len(optimizer1.get_trials())}")
0469         print(f"  Pareto front size: {len(optimizer1.get_pareto_front())}")
0470         print(f"  Output directory: {executor1.output_dir}")
0471 
0472         print("\nCase 2 (Separate Branches):")
0473         print(f"  Workflow: {executor2.workflow.name}")
0474         print(f"  Branches: {len(executor2.workflow.branches)}")
0475         print(f"  Total evaluations: {len(optimizer2.get_trials())}")
0476         print(f"  Pareto front size: {len(optimizer2.get_pareto_front())}")
0477         print(f"  Output directory: {executor2.output_dir}")
0478 
0479         print("\n" + "="*80)
0480         print("✅ Ax + PanDAiDDS Optimization Showcase Complete!")
0481         print("="*80)
0482         print("\nKey Results:")
0483         print("  • Both workflow configurations use identical Ax optimizer")
0484         print("  • Bayesian optimization with SAASBO surrogate model")
0485         print("  • qNEHVI acquisition for multi-objective optimization")
0486         print("  • Batch optimization with 3 parallel evaluations per iteration")
0487         print("  • Sobol initialization ensures good space exploration")
0488         print("  • PanDAiDDS scheduler provides local multi-core parallelism")
0489 
0490     except ImportError as e:
0491         print("\n" + "="*80)
0492         print("⚠️  ERROR: Missing Dependencies")
0493         print("="*80)
0494         print(f"\n{e}")
0495         print("\nTo run this showcase, install required packages:")
0496         print("  pip install ax-platform panda")
0497         print("\nAlternatively, use the simple random optimizer showcase:")
0498         print("  python examples/dtlz2_optimizer_showcase.py")