Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-08-12 08:24:55

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 from pathlib import Path
0019 from typing import Dict, Any, List
0020 
0021 from aid2e.utilities.workflows import (
0022     DAGExecutor,
0023     WorkflowDefinition,
0024     BranchDefinition,
0025     StageDefinition,
0026     JobDefinition,
0027     JobContext,
0028 )
0029 from aid2e.utilities.configurations.objectives import (
0030     ObjectiveDefinition,
0031     ObjectiveDirection,
0032 )
0033 
0034 
0035 # =============================================================================
0036 # DTLZ2 Problem Implementation
0037 # =============================================================================
0038 
0039 def dtlz2_both_objectives(x: List[float]) -> Dict[str, float]:
0040     """Compute both DTLZ2 objectives in one function.
0041     
0042     DTLZ2 is defined as:
0043         g(x) = sum((x_i - 0.5)^2 for i in 2..n)
0044         f1(x) = (1 + g(x)) * cos(x1 * pi/2) * cos(x2 * pi/2)
0045         f2(x) = (1 + g(x)) * cos(x1 * pi/2) * sin(x2 * pi/2)
0046     
0047     For n=3: x = [x1, x2, x3] in [0, 1]^3
0048     Pareto front: x1 in [0, 1], x2 = x3 = 0.5
0049     
0050     Args:
0051         x: Design point [x1, x2, x3]
0052         
0053     Returns:
0054         Dictionary with f1 and f2 values
0055     """
0056     x = np.array(x)
0057     n = len(x)
0058     
0059     # g(x) = sum of squared deviations from 0.5 for x2, x3, ...
0060     g = np.sum((x[1:] - 0.5) ** 2)
0061     
0062     # f1 = (1 + g) * cos(x1 * pi/2) * cos(x2 * pi/2)
0063     f1 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.cos(x[1] * np.pi / 2)
0064     
0065     # f2 = (1 + g) * cos(x1 * pi/2) * sin(x2 * pi/2)
0066     f2 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.sin(x[1] * np.pi / 2)
0067     
0068     return {"f1": float(f1), "f2": float(f2)}
0069 
0070 
0071 def dtlz2_f1_only(x: List[float]) -> float:
0072     """Compute only f1 objective of DTLZ2.
0073     
0074     Args:
0075         x: Design point [x1, x2, x3]
0076         
0077     Returns:
0078         f1 value
0079     """
0080     x = np.array(x)
0081     g = np.sum((x[1:] - 0.5) ** 2)
0082     f1 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.cos(x[1] * np.pi / 2)
0083     return float(f1)
0084 
0085 
0086 def dtlz2_f2_only(x: List[float]) -> float:
0087     """Compute only f2 objective of DTLZ2.
0088     
0089     Args:
0090         x: Design point [x1, x2, x3]
0091         
0092     Returns:
0093         f2 value
0094     """
0095     x = np.array(x)
0096     g = np.sum((x[1:] - 0.5) ** 2)
0097     f2 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.sin(x[1] * np.pi / 2)
0098     return float(f2)
0099 
0100 
0101 # =============================================================================
0102 # Python Evaluator Wrapper for JobContext
0103 # =============================================================================
0104 
0105 def evaluate_both_objectives_wrapper(context: JobContext) -> Dict[str, float]:
0106     """Wrapper to evaluate both objectives from JobContext.
0107     
0108     Extracts design point from context and returns objectives.
0109     """
0110     design_point = context.design_point
0111     x = [design_point['x1'], design_point['x2'], design_point['x3']]
0112     objectives = dtlz2_both_objectives(x)
0113     
0114     # Log results
0115     context.add_log(f"Design point: {x}")
0116     context.add_log(f"Objectives: {objectives}")
0117     
0118     # Store in XCom for executor to collect
0119     context.xcom_push("objectives", objectives)
0120     
0121     return objectives
0122 
0123 
0124 def evaluate_f1_wrapper(context: JobContext) -> float:
0125     """Wrapper to evaluate f1 from JobContext."""
0126     design_point = context.design_point
0127     x = [design_point['x1'], design_point['x2'], design_point['x3']]
0128     f1 = dtlz2_f1_only(x)
0129     
0130     context.add_log(f"Design point: {x}")
0131     context.add_log(f"f1 = {f1}")
0132     
0133     # Store in XCom
0134     context.xcom_push("f1", f1)
0135     
0136     return f1
0137 
0138 
0139 def evaluate_f2_wrapper(context: JobContext) -> float:
0140     """Wrapper to evaluate f2 from JobContext."""
0141     design_point = context.design_point
0142     x = [design_point['x1'], design_point['x2'], design_point['x3']]
0143     f2 = dtlz2_f2_only(x)
0144     
0145     context.add_log(f"Design point: {x}")
0146     context.add_log(f"f2 = {f2}")
0147     
0148     # Store in XCom
0149     context.xcom_push("f2", f2)
0150     
0151     return f2
0152 
0153 
0154 # =============================================================================
0155 # Case 1: Single Branch Workflow
0156 # =============================================================================
0157 
0158 def create_single_branch_workflow() -> WorkflowDefinition:
0159     """Create workflow with single branch computing both objectives.
0160     
0161     Workflow structure:
0162         Branch: main
0163             Stage: evaluate
0164                 Job: compute_objectives (PythonEvaluator)
0165                     → Computes both f1 and f2 in one function
0166     """
0167     # Job using PythonEvaluator to compute both objectives
0168     compute_job = JobDefinition(
0169         name="compute_objectives",
0170         command="python",  # Not used for PythonEvaluator
0171         payload={
0172             "evaluator_type": "python",
0173             "python_callable": evaluate_both_objectives_wrapper,
0174             "op_args": (),
0175             "op_kwargs": {},
0176         },
0177     )
0178     
0179     # Stage containing the job
0180     eval_stage = StageDefinition(
0181         name="evaluate",
0182         jobs=[compute_job],
0183     )
0184     
0185     # Single branch
0186     main_branch = BranchDefinition(
0187         name="main",
0188         stages=[eval_stage],
0189     )
0190     
0191     # Workflow with objectives
0192     workflow = WorkflowDefinition(
0193         name="dtlz2_single_branch",
0194         description="DTLZ2 with both objectives in single branch",
0195         branches=[main_branch],
0196         objectives=[
0197             ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0198             ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0199         ],
0200     )
0201     
0202     return workflow
0203 
0204 
0205 # =============================================================================
0206 # Case 2: Separate Branches Workflow
0207 # =============================================================================
0208 
0209 def create_separate_branches_workflow() -> WorkflowDefinition:
0210     """Create workflow with separate branches for each objective.
0211     
0212     Workflow structure:
0213         Branch: f1_branch
0214             Stage: evaluate_f1
0215                 Job: compute_f1 (PythonEvaluator)
0216                     → Computes only f1
0217         
0218         Branch: f2_branch
0219             Stage: evaluate_f2
0220                 Job: compute_f2 (PythonEvaluator)
0221                     → Computes only f2
0222     """
0223     # Branch 1: Compute f1
0224     f1_job = JobDefinition(
0225         name="compute_f1",
0226         command="python",
0227         payload={
0228             "evaluator_type": "python",
0229             "python_callable": evaluate_f1_wrapper,
0230             "op_args": (),
0231             "op_kwargs": {},
0232         },
0233     )
0234     
0235     f1_stage = StageDefinition(name="evaluate_f1", jobs=[f1_job])
0236     f1_branch = BranchDefinition(name="f1_branch", stages=[f1_stage])
0237     
0238     # Branch 2: Compute f2
0239     f2_job = JobDefinition(
0240         name="compute_f2",
0241         command="python",
0242         payload={
0243             "evaluator_type": "python",
0244             "python_callable": evaluate_f2_wrapper,
0245             "op_args": (),
0246             "op_kwargs": {},
0247         },
0248     )
0249     
0250     f2_stage = StageDefinition(name="evaluate_f2", jobs=[f2_job])
0251     f2_branch = BranchDefinition(name="f2_branch", stages=[f2_stage])
0252     
0253     # Workflow with both branches
0254     workflow = WorkflowDefinition(
0255         name="dtlz2_separate_branches",
0256         description="DTLZ2 with separate branches for each objective",
0257         branches=[f1_branch, f2_branch],
0258         objectives=[
0259             ObjectiveDefinition(name="f1", direction=ObjectiveDirection.MINIMIZE),
0260             ObjectiveDefinition(name="f2", direction=ObjectiveDirection.MINIMIZE),
0261         ],
0262     )
0263     
0264     return workflow
0265 
0266 
0267 # =============================================================================
0268 # Simple Random Optimizer (for demonstration)
0269 # =============================================================================
0270 
0271 class SimpleRandomOptimizer:
0272     """Simple random search optimizer for demonstration.
0273     
0274     Generates random design points within bounds and tracks best results.
0275     This is a simple optimizer for demonstration - in production, use
0276     Ax optimizer or other sophisticated algorithms.
0277     """
0278     
0279     def __init__(
0280         self,
0281         bounds: Dict[str, tuple],
0282         n_iterations: int = 10,
0283         seed: int = 42,
0284     ):
0285         """Initialize optimizer.
0286         
0287         Args:
0288             bounds: Parameter bounds, e.g., {"x1": (0, 1), "x2": (0, 1)}
0289             n_iterations: Number of optimization iterations
0290             seed: Random seed for reproducibility
0291         """
0292         self.bounds = bounds
0293         self.n_iterations = n_iterations
0294         self.rng = np.random.RandomState(seed)
0295         
0296         # Track results
0297         self.design_points: List[Dict[str, float]] = []
0298         self.objectives: List[Dict[str, float]] = []
0299         self.iteration = 0
0300     
0301     def suggest(self) -> Dict[str, float]:
0302         """Suggest next design point to evaluate.
0303         
0304         Returns:
0305             Design point dictionary
0306         """
0307         design_point = {}
0308         for param, (lower, upper) in self.bounds.items():
0309             design_point[param] = self.rng.uniform(lower, upper)
0310         return design_point
0311     
0312     def tell(self, design_point: Dict[str, float], objectives: Dict[str, float]):
0313         """Provide feedback with evaluation results.
0314         
0315         Args:
0316             design_point: Evaluated design point
0317             objectives: Computed objectives
0318         """
0319         self.design_points.append(design_point)
0320         self.objectives.append(objectives)
0321         self.iteration += 1
0322     
0323     def get_best_pareto_front(self, n_points: int = 5) -> List[Dict[str, Any]]:
0324         """Get approximate Pareto front points.
0325         
0326         Args:
0327             n_points: Number of points to return
0328             
0329         Returns:
0330             List of dicts with design_point and objectives
0331         """
0332         if not self.objectives:
0333             return []
0334         
0335         # Simple Pareto dominance check
0336         pareto_indices = []
0337         for i, obj_i in enumerate(self.objectives):
0338             is_dominated = False
0339             for j, obj_j in enumerate(self.objectives):
0340                 if i == j:
0341                     continue
0342                 # Check if j dominates i (all objectives worse or equal, at least one worse)
0343                 if all(obj_j[k] <= obj_i[k] for k in obj_i) and \
0344                    any(obj_j[k] < obj_i[k] for k in obj_i):
0345                     is_dominated = True
0346                     break
0347             if not is_dominated:
0348                 pareto_indices.append(i)
0349         
0350         # Return Pareto points
0351         pareto_points = [
0352             {
0353                 "design_point": self.design_points[i],
0354                 "objectives": self.objectives[i],
0355             }
0356             for i in pareto_indices[:n_points]
0357         ]
0358         
0359         return pareto_points
0360 
0361 
0362 # =============================================================================
0363 # Main Optimization Functions
0364 # =============================================================================
0365 
0366 def run_case_1_single_branch():
0367     """Run Case 1: Single branch with both objectives."""
0368     print("\n" + "="*80)
0369     print("CASE 1: Single Branch - Both Objectives in One Python Function")
0370     print("="*80)
0371     
0372     # Create workflow
0373     workflow = create_single_branch_workflow()
0374     print(f"\n✓ Workflow: {workflow.name}")
0375     print(f"  Description: {workflow.description}")
0376     print(f"  Branches: {len(workflow.branches)}")
0377     print(f"  Objectives: {[obj.name for obj in workflow.objectives]}")
0378     
0379     # Create executor
0380     executor = DAGExecutor(
0381         workflow=workflow,
0382         base_output_dir="/tmp/dtlz2_optimization/case1",
0383         log_level="WARNING",  # Reduce verbosity
0384     )
0385     
0386     # Create optimizer
0387     optimizer = SimpleRandomOptimizer(
0388         bounds={"x1": (0, 1), "x2": (0, 1), "x3": (0, 1)},
0389         n_iterations=15,
0390         seed=42,
0391     )
0392     
0393     print(f"\n✓ Optimizer: SimpleRandomOptimizer")
0394     print(f"  Iterations: {optimizer.n_iterations}")
0395     print(f"  Bounds: {optimizer.bounds}")
0396     
0397     # Optimization loop
0398     print(f"\n{'Iter':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0399     print("-" * 72)
0400     
0401     for i in range(optimizer.n_iterations):
0402         # Get design point from optimizer
0403         design_point = optimizer.suggest()
0404         
0405         # Execute workflow to evaluate design point
0406         objectives = executor.execute(design_point)
0407         
0408         # Provide feedback to optimizer
0409         optimizer.tell(design_point, objectives)
0410         
0411         # Print results
0412         print(f"{i+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0413               f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0414               f"{objectives.get('f2', 0):<12.6f}")
0415     
0416     # Get Pareto front
0417     pareto_front = optimizer.get_best_pareto_front(n_points=5)
0418     
0419     print(f"\n✓ Optimization Complete!")
0420     print(f"  Total evaluations: {len(optimizer.objectives)}")
0421     print(f"  Pareto front points: {len(pareto_front)}")
0422     
0423     print(f"\nPareto Front (top 5 non-dominated points):")
0424     print(f"{'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0425     print("-" * 72)
0426     for point in pareto_front:
0427         dp = point['design_point']
0428         obj = point['objectives']
0429         print(f"{dp['x1']:<10.4f} {dp['x2']:<10.4f} {dp['x3']:<10.4f} "
0430               f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0431     
0432     return optimizer, executor
0433 
0434 
0435 def run_case_2_separate_branches():
0436     """Run Case 2: Separate branches for each objective."""
0437     print("\n" + "="*80)
0438     print("CASE 2: Separate Branches - Each Objective in Different Branch")
0439     print("="*80)
0440     
0441     # Create workflow
0442     workflow = create_separate_branches_workflow()
0443     print(f"\n✓ Workflow: {workflow.name}")
0444     print(f"  Description: {workflow.description}")
0445     print(f"  Branches: {len(workflow.branches)} ({[b.name for b in workflow.branches]})")
0446     print(f"  Objectives: {[obj.name for obj in workflow.objectives]}")
0447     
0448     # Create executor
0449     executor = DAGExecutor(
0450         workflow=workflow,
0451         base_output_dir="/tmp/dtlz2_optimization/case2",
0452         log_level="WARNING",  # Reduce verbosity
0453     )
0454     
0455     # Create optimizer
0456     optimizer = SimpleRandomOptimizer(
0457         bounds={"x1": (0, 1), "x2": (0, 1), "x3": (0, 1)},
0458         n_iterations=15,
0459         seed=42,  # Same seed for reproducibility
0460     )
0461     
0462     print(f"\n✓ Optimizer: SimpleRandomOptimizer")
0463     print(f"  Iterations: {optimizer.n_iterations}")
0464     print(f"  Bounds: {optimizer.bounds}")
0465     
0466     # Optimization loop
0467     print(f"\n{'Iter':<6} {'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0468     print("-" * 72)
0469     
0470     for i in range(optimizer.n_iterations):
0471         # Get design point from optimizer
0472         design_point = optimizer.suggest()
0473         
0474         # Execute workflow (will run both branches)
0475         objectives = executor.execute(design_point)
0476         
0477         # Provide feedback to optimizer
0478         optimizer.tell(design_point, objectives)
0479         
0480         # Print results
0481         print(f"{i+1:<6} {design_point['x1']:<10.4f} {design_point['x2']:<10.4f} "
0482               f"{design_point['x3']:<10.4f} {objectives.get('f1', 0):<12.6f} "
0483               f"{objectives.get('f2', 0):<12.6f}")
0484     
0485     # Get Pareto front
0486     pareto_front = optimizer.get_best_pareto_front(n_points=5)
0487     
0488     print(f"\n✓ Optimization Complete!")
0489     print(f"  Total evaluations: {len(optimizer.objectives)}")
0490     print(f"  Pareto front points: {len(pareto_front)}")
0491     
0492     print(f"\nPareto Front (top 5 non-dominated points):")
0493     print(f"{'x1':<10} {'x2':<10} {'x3':<10} {'f1':<12} {'f2':<12}")
0494     print("-" * 72)
0495     for point in pareto_front:
0496         dp = point['design_point']
0497         obj = point['objectives']
0498         print(f"{dp['x1']:<10.4f} {dp['x2']:<10.4f} {dp['x3']:<10.4f} "
0499               f"{obj.get('f1', 0):<12.6f} {obj.get('f2', 0):<12.6f}")
0500     
0501     return optimizer, executor
0502 
0503 
0504 # =============================================================================
0505 # Main Entry Point
0506 # =============================================================================
0507 
0508 if __name__ == "__main__":
0509     print("\n" + "="*80)
0510     print("DTLZ2 Multi-Objective Optimization with DAG Executor")
0511     print("="*80)
0512     print("\nThis showcase demonstrates two workflow configurations:")
0513     print("  1. Single branch - both objectives in one Python function")
0514     print("  2. Separate branches - each objective in different branch")
0515     print("\nDTLZ2 Problem:")
0516     print("  Variables: x1, x2, x3 in [0, 1]")
0517     print("  Objectives: f1, f2 (minimize both)")
0518     print("  Optimal Pareto front: x1 in [0, 1], x2 = x3 = 0.5")
0519     
0520     # Run both cases
0521     optimizer1, executor1 = run_case_1_single_branch()
0522     optimizer2, executor2 = run_case_2_separate_branches()
0523     
0524     # Summary
0525     print("\n" + "="*80)
0526     print("COMPARISON SUMMARY")
0527     print("="*80)
0528     
0529     print("\nCase 1 (Single Branch):")
0530     print(f"  Workflow: {executor1.workflow.name}")
0531     print(f"  Branches: {len(executor1.workflow.branches)}")
0532     print(f"  Jobs executed: {len(executor1.global_xcom)}")
0533     print(f"  Output directory: {executor1.output_dir}")
0534     
0535     print("\nCase 2 (Separate Branches):")
0536     print(f"  Workflow: {executor2.workflow.name}")
0537     print(f"  Branches: {len(executor2.workflow.branches)}")
0538     print(f"  Jobs executed: {len(executor2.global_xcom)}")
0539     print(f"  Output directory: {executor2.output_dir}")
0540     
0541     print("\n" + "="*80)
0542     print("✅ Showcase Complete!")
0543     print("="*80)
0544     print("\nKey Takeaways:")
0545     print("  • Both workflow configurations produce the same results")
0546     print("  • Single branch is simpler for tightly coupled objectives")
0547     print("  • Separate branches allow independent objective computation")
0548     print("  • DAG Executor handles both cases seamlessly")
0549     print("  • Optimizer integration is identical for both cases")