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 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 # DTLZ2 Problem Implementation
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 # Python Evaluator Wrappers
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 # Workflow Definitions
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     # Branch 1: f1
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     # Branch 2: f2
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 # Optimization Functions
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     # Create workflow
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     # Configure JobLib scheduler
0204     joblib_config = JobLibRunnerConfig(
0205         n_jobs=-1,  # Use all available CPUs
0206         backend="loky",  # Default backend (safe multiprocessing)
0207         timeout=300,  # 5 minute timeout per job
0208         verbose=0,  # Silent (increase for debugging)
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     # Create executor with JobLib scheduler
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     # Create search space
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     # Create Ax optimizer configuration
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     # Create Ax optimizer
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     # Optimization loop
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     # Phase 1: Sobol initialization (10 points in batches of 3)
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         # Get batch size (might be less than 3 for last batch)
0269         batch_size = min(ax_config.batch_size, ax_config.n_initial_samples - batch * ax_config.batch_size)
0270         
0271         # Suggest candidates
0272         candidates = optimizer.suggest_candidates(n_candidates=batch_size)
0273         
0274         # Evaluate each candidate (JobLib handles parallelism internally)
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     # Phase 2: Bayesian optimization (10 iterations x 3 points = 30 points)
0286     n_bayesian_iterations = 10
0287     for iteration in range(n_bayesian_iterations):
0288         # Suggest batch of candidates
0289         candidates = optimizer.suggest_candidates(n_candidates=ax_config.batch_size)
0290         
0291         # Evaluate each candidate
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     # Get Pareto front
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]:  # Show top 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     # Create workflow
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     # Configure JobLib scheduler
0337     joblib_config = JobLibRunnerConfig(
0338         n_jobs=-1,  # Use all available CPUs
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     # Create executor with JobLib scheduler
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     # Create search space
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     # Create Ax optimizer configuration (same as Case 1)
0370     ax_config = AxOptimizerConfig(
0371         initialization_strategy="sobol",
0372         n_initial_samples=10,
0373         batch_size=3,
0374         generator="BOTORCH_MODULAR",
0375         seed=42,  # Same seed for reproducibility
0376     )
0377     
0378     # Create Ax optimizer
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     # Optimization loop
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     # Phase 1: Sobol initialization
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     # Phase 2: Bayesian optimization
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     # Get Pareto front
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]:  # Show top 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 # Main Entry Point
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         # Run both cases
0477         optimizer1, executor1 = run_case_1_single_branch()
0478         optimizer2, executor2 = run_case_2_separate_branches()
0479         
0480         # Summary
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")