Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-11-09 09:29:52

0001 """DTLZ2 Optimization Showcase with DAG Executor.
0002 
0003 This example demonstrates two workflow configurations for the DTLZ2 problem:
0004 
0005 Case 1: Single Branch - Both objectives computed in one Python function
0006 Case 2: Separate Branches - Each objective computed in a separate branch
0007 
0008 The DTLZ2 problem is a standard multi-objective test problem with:
0009 - 3 decision variables (x1, x2, x3) in [0, 1]
0010 - 2 objectives (f1, f2) to minimize
0011 - Optimal Pareto front: x1 in [0, 1], x2 = x3 = 0.5
0012 
0013 Project: AID2E v0.0.0 - AI assisted Detector Design for EIC
0014 """
0015 
0016 import numpy as np
0017 import json
0018 import sys
0019 from pathlib import Path
0020 from typing import Dict, Any, List
0021 
0022 REPO_ROOT = Path(__file__).resolve().parents[3]
0023 if str(REPO_ROOT) not in sys.path:
0024     sys.path.insert(0, str(REPO_ROOT))
0025 
0026 from aid2e.utilities.workflows import (
0027     DAGExecutor,
0028     WorkflowDefinition,
0029     BranchDefinition,
0030     StageDefinition,
0031     JobDefinition,
0032 )
0033 from aid2e.utilities.configurations.objectives import (
0034     ObjectiveDefinition,
0035     ObjectiveDirection,
0036 )
0037 from examples.dtlz2.dtlz2_evaluator import (
0038     evaluate_both_objectives_wrapper,
0039     evaluate_f1_wrapper,
0040     evaluate_f2_wrapper,
0041 )
0042 
0043 
0044 # =============================================================================
0045 # Case 1: Single Branch Workflow
0046 # =============================================================================
0047 
0048 def create_single_branch_workflow(
0049     *, name: str = "dtlz2_single_branch",
0050     description: str = "DTLZ2 with both objectives in single branch",
0051 ) -> WorkflowDefinition:
0052     """Create workflow with single branch computing both objectives.
0053     
0054     Workflow structure:
0055         Branch: main
0056             Stage: evaluate
0057                 Job: compute_objectives (PythonEvaluator)
0058                     → Computes both f1 and f2 in one function
0059     """
0060     # Job using PythonEvaluator to compute both objectives
0061     compute_job = JobDefinition(
0062         name="compute_objectives",
0063         command="python",  # Not used for PythonEvaluator
0064         payload={
0065             "evaluator_type": "python",
0066             "python_callable": evaluate_both_objectives_wrapper,
0067             "op_args": (),
0068             "op_kwargs": {},
0069         },
0070     )
0071     
0072     # Stage containing the job
0073     eval_stage = StageDefinition(
0074         name="evaluate",
0075         jobs=[compute_job],
0076     )
0077     
0078     # Single branch
0079     main_branch = BranchDefinition(
0080         name="main",
0081         stages=[eval_stage],
0082     )
0083     
0084     # Workflow with objectives
0085     workflow = WorkflowDefinition(
0086         name=name,
0087         description=description,
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 # =============================================================================
0099 # Case 2: Separate Branches Workflow
0100 # =============================================================================
0101 
0102 def create_separate_branches_workflow(
0103     *, name: str = "dtlz2_separate_branches",
0104     description: str = "DTLZ2 with separate branches for each objective",
0105 ) -> WorkflowDefinition:
0106     """Create workflow with separate branches for each objective.
0107     
0108     Workflow structure:
0109         Branch: f1_branch
0110             Stage: evaluate_f1
0111                 Job: compute_f1 (PythonEvaluator)
0112                     → Computes only f1
0113         
0114         Branch: f2_branch
0115             Stage: evaluate_f2
0116                 Job: compute_f2 (PythonEvaluator)
0117                     → Computes only f2
0118     """
0119     # Branch 1: Compute f1
0120     f1_job = JobDefinition(
0121         name="compute_f1",
0122         command="python",
0123         payload={
0124             "evaluator_type": "python",
0125             "python_callable": evaluate_f1_wrapper,
0126             "op_args": (),
0127             "op_kwargs": {},
0128         },
0129     )
0130     
0131     f1_stage = StageDefinition(name="evaluate_f1", jobs=[f1_job])
0132     f1_branch = BranchDefinition(name="f1_branch", stages=[f1_stage])
0133     
0134     # Branch 2: Compute f2
0135     f2_job = JobDefinition(
0136         name="compute_f2",
0137         command="python",
0138         payload={
0139             "evaluator_type": "python",
0140             "python_callable": evaluate_f2_wrapper,
0141             "op_args": (),
0142             "op_kwargs": {},
0143         },
0144     )
0145     
0146     f2_stage = StageDefinition(name="evaluate_f2", jobs=[f2_job])
0147     f2_branch = BranchDefinition(name="f2_branch", stages=[f2_stage])
0148     
0149     # Workflow with both branches
0150     workflow = WorkflowDefinition(
0151         name=name,
0152         description=description,
0153         branches=[f1_branch, f2_branch],
0154         objectives=[
0155             ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0156             ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0157         ],
0158     )
0159     
0160     return workflow
0161 
0162 
0163 # =============================================================================
0164 # Simple Random Optimizer (for demonstration)
0165 # =============================================================================
0166 
0167 class SimpleRandomOptimizer:
0168     """Simple random search optimizer for demonstration.
0169     
0170     Generates random design points within bounds and tracks best results.
0171     This is a simple optimizer for demonstration - in production, use
0172     Ax optimizer or other sophisticated algorithms.
0173     """
0174     
0175     def __init__(
0176         self,
0177         bounds: Dict[str, tuple],
0178         n_iterations: int = 10,
0179         seed: int = 42,
0180     ):
0181         """Initialize optimizer.
0182         
0183         Args:
0184             bounds: Parameter bounds, e.g., {"x1": (0, 1), "x2": (0, 1)}
0185             n_iterations: Number of optimization iterations
0186             seed: Random seed for reproducibility
0187         """
0188         self.bounds = bounds
0189         self.n_iterations = n_iterations
0190         self.rng = np.random.RandomState(seed)
0191         
0192         # Track results
0193         self.design_points: List[Dict[str, float]] = []
0194         self.objectives: List[Dict[str, float]] = []
0195         self.iteration = 0
0196     
0197     def suggest(self) -> Dict[str, float]:
0198         """Suggest next design point to evaluate.
0199         
0200         Returns:
0201             Design point dictionary
0202         """
0203         design_point = {}
0204         for param, (lower, upper) in self.bounds.items():
0205             design_point[param] = self.rng.uniform(lower, upper)
0206         return design_point
0207     
0208     def tell(self, design_point: Dict[str, float], objectives: Dict[str, float]):
0209         """Provide feedback with evaluation results.
0210         
0211         Args:
0212             design_point: Evaluated design point
0213             objectives: Computed objectives
0214         """
0215         self.design_points.append(design_point)
0216         self.objectives.append(objectives)
0217         self.iteration += 1
0218     
0219     def get_best_pareto_front(self, n_points: int = 5) -> List[Dict[str, Any]]:
0220         """Get approximate Pareto front points.
0221         
0222         Args:
0223             n_points: Number of points to return
0224             
0225         Returns:
0226             List of dicts with design_point and objectives
0227         """
0228         if not self.objectives:
0229             return []
0230         
0231         # Simple Pareto dominance check
0232         pareto_indices = []
0233         for i, obj_i in enumerate(self.objectives):
0234             is_dominated = False
0235             for j, obj_j in enumerate(self.objectives):
0236                 if i == j:
0237                     continue
0238                 # Check if j dominates i (all objectives worse or equal, at least one worse)
0239                 if all(obj_j[k] <= obj_i[k] for k in obj_i) and \
0240                    any(obj_j[k] < obj_i[k] for k in obj_i):
0241                     is_dominated = True
0242                     break
0243             if not is_dominated:
0244                 pareto_indices.append(i)
0245         
0246         # Return Pareto points
0247         pareto_points = [
0248             {
0249                 "design_point": self.design_points[i],
0250                 "objectives": self.objectives[i],
0251             }
0252             for i in pareto_indices[:n_points]
0253         ]
0254         
0255         return pareto_points
0256 
0257 
0258 # =============================================================================
0259 # Main Optimization Functions
0260 # =============================================================================
0261 
0262 def run_case_1_single_branch():
0263     """Run Case 1: Single branch with both objectives."""
0264     print("\n" + "="*80)
0265     print("CASE 1: Single Branch - Both Objectives in One Python Function")
0266     print("="*80)
0267     
0268     # Create workflow
0269     workflow = create_single_branch_workflow()
0270     print(f"\n✓ Workflow: {workflow.name}")
0271     print(f"  Description: {workflow.description}")
0272     print(f"  Branches: {len(workflow.branches)}")
0273     print(f"  Objectives: {[obj.name for obj in workflow.objectives]}")
0274     
0275     # Create executor
0276     executor = DAGExecutor(
0277         workflow=workflow,
0278         base_output_dir="/tmp/dtlz2_optimization/case1",
0279         log_level="WARNING",  # Reduce verbosity
0280     )
0281     
0282     # Create optimizer
0283     optimizer = SimpleRandomOptimizer(
0284         bounds={"x1": (0, 1), "x2": (0, 1), "x3": (0, 1)},
0285         n_iterations=15,
0286         seed=42,
0287     )
0288     
0289     print(f"\n✓ Optimizer: SimpleRandomOptimizer")
0290     print(f"  Iterations: {optimizer.n_iterations}")
0291     print(f"  Bounds: {optimizer.bounds}")
0292     
0293     # Optimization loop
0294     print(f"\n{'Iter':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0295     print("-" * 72)
0296     
0297     for i in range(optimizer.n_iterations):
0298         # Get design point from optimizer
0299         design_point = optimizer.suggest()
0300         
0301         # Execute workflow to evaluate design point
0302         objectives = executor.execute(design_point)
0303         
0304         # Provide feedback to optimizer
0305         optimizer.tell(design_point, objectives)
0306         
0307         # Print results
0308         print(f"{i+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0309               f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0310               f"{objectives.get('f2', 0):<12.6f}")
0311     
0312     # Get Pareto front
0313     pareto_front = optimizer.get_best_pareto_front(n_points=5)
0314     
0315     print(f"\n✓ Optimization Complete!")
0316     print(f"  Total evaluations: {len(optimizer.objectives)}")
0317     print(f"  Pareto front points: {len(pareto_front)}")
0318     
0319     print(f"\nPareto Front (top 5 non-dominated points):")
0320     print(f"{'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0321     print("-" * 72)
0322     for point in pareto_front:
0323         dp = point['design_point']
0324         obj = point['objectives']
0325         print(f"{dp['x1']:<10.4f} {dp['x2']:<10.4f} {dp['x3']:<10.4f} "
0326               f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0327     
0328     return optimizer, executor
0329 
0330 
0331 def run_case_2_separate_branches():
0332     """Run Case 2: Separate branches for each objective."""
0333     print("\n" + "="*80)
0334     print("CASE 2: Separate Branches - Each Objective in Different Branch")
0335     print("="*80)
0336     
0337     # Create workflow
0338     workflow = create_separate_branches_workflow()
0339     print(f"\n✓ Workflow: {workflow.name}")
0340     print(f"  Description: {workflow.description}")
0341     print(f"  Branches: {len(workflow.branches)} ({[b.name for b in workflow.branches]})")
0342     print(f"  Objectives: {[obj.name for obj in workflow.objectives]}")
0343     
0344     # Create executor
0345     executor = DAGExecutor(
0346         workflow=workflow,
0347         base_output_dir="/tmp/dtlz2_optimization/case2",
0348         log_level="WARNING",  # Reduce verbosity
0349     )
0350     
0351     # Create optimizer
0352     optimizer = SimpleRandomOptimizer(
0353         bounds={"x1": (0, 1), "x2": (0, 1), "x3": (0, 1)},
0354         n_iterations=15,
0355         seed=42,  # Same seed for reproducibility
0356     )
0357     
0358     print(f"\n✓ Optimizer: SimpleRandomOptimizer")
0359     print(f"  Iterations: {optimizer.n_iterations}")
0360     print(f"  Bounds: {optimizer.bounds}")
0361     
0362     # Optimization loop
0363     print(f"\n{'Iter':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0364     print("-" * 72)
0365     
0366     for i in range(optimizer.n_iterations):
0367         # Get design point from optimizer
0368         design_point = optimizer.suggest()
0369         
0370         # Execute workflow (will run both branches)
0371         objectives = executor.execute(design_point)
0372         
0373         # Provide feedback to optimizer
0374         optimizer.tell(design_point, objectives)
0375         
0376         # Print results
0377         print(f"{i+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0378               f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0379               f"{objectives.get('f2', 0):<12.6f}")
0380     
0381     # Get Pareto front
0382     pareto_front = optimizer.get_best_pareto_front(n_points=5)
0383     
0384     print(f"\n✓ Optimization Complete!")
0385     print(f"  Total evaluations: {len(optimizer.objectives)}")
0386     print(f"  Pareto front points: {len(pareto_front)}")
0387     
0388     print(f"\nPareto Front (top 5 non-dominated points):")
0389     print(f"{'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0390     print("-" * 72)
0391     for point in pareto_front:
0392         dp = point['design_point']
0393         obj = point['objectives']
0394         print(f"{dp['x1']:<10.4f} {dp['x2']:<10.4f} {dp['x3']:<10.4f} "
0395               f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0396     
0397     return optimizer, executor
0398 
0399 
0400 # =============================================================================
0401 # Main Entry Point
0402 # =============================================================================
0403 
0404 if __name__ == "__main__":
0405     print("\n" + "="*80)
0406     print("DTLZ2 Multi-Objective Optimization with DAG Executor")
0407     print("="*80)
0408     print("\nThis showcase demonstrates two workflow configurations:")
0409     print("  1. Single branch - both objectives in one Python function")
0410     print("  2. Separate branches - each objective in different branch")
0411     print("\nDTLZ2 Problem:")
0412     print("  Variables: x1, x2, x3 in [0, 1]")
0413     print("  Objectives: f1, f2 (minimize both)")
0414     print("  Optimal Pareto front: x1 in [0, 1], x2 = x3 = 0.5")
0415     
0416     # Run both cases
0417     optimizer1, executor1 = run_case_1_single_branch()
0418     optimizer2, executor2 = run_case_2_separate_branches()
0419     
0420     # Summary
0421     print("\n" + "="*80)
0422     print("COMPARISON SUMMARY")
0423     print("="*80)
0424     
0425     print("\nCase 1 (Single Branch):")
0426     print(f"  Workflow: {executor1.workflow.name}")
0427     print(f"  Branches: {len(executor1.workflow.branches)}")
0428     print(f"  Jobs executed: {len(executor1.global_xcom)}")
0429     print(f"  Output directory: {executor1.output_dir}")
0430     
0431     print("\nCase 2 (Separate Branches):")
0432     print(f"  Workflow: {executor2.workflow.name}")
0433     print(f"  Branches: {len(executor2.workflow.branches)}")
0434     print(f"  Jobs executed: {len(executor2.global_xcom)}")
0435     print(f"  Output directory: {executor2.output_dir}")
0436     
0437     print("\n" + "="*80)
0438     print("✅ Showcase Complete!")
0439     print("="*80)
0440     print("\nKey Takeaways:")
0441     print("  • Both workflow configurations produce the same results")
0442     print("  • Single branch is simpler for tightly coupled objectives")
0443     print("  • Separate branches allow independent objective computation")
0444     print("  • DAG Executor handles both cases seamlessly")
0445     print("  • Optimizer integration is identical for both cases")