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