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
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
0061 compute_job = JobDefinition(
0062 name="compute_objectives",
0063 command="python",
0064 payload={
0065 "evaluator_type": "python",
0066 "python_callable": evaluate_both_objectives_wrapper,
0067 "op_args": (),
0068 "op_kwargs": {},
0069 },
0070 )
0071
0072
0073 eval_stage = StageDefinition(
0074 name="evaluate",
0075 jobs=[compute_job],
0076 )
0077
0078
0079 main_branch = BranchDefinition(
0080 name="main",
0081 stages=[eval_stage],
0082 )
0083
0084
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
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
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
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
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
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
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
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
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
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
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
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
0276 executor = DAGExecutor(
0277 workflow=workflow,
0278 base_output_dir="/tmp/dtlz2_optimization/case1",
0279 log_level="WARNING",
0280 )
0281
0282
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
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
0299 design_point = optimizer.suggest()
0300
0301
0302 objectives = executor.execute(design_point)
0303
0304
0305 optimizer.tell(design_point, objectives)
0306
0307
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
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
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
0345 executor = DAGExecutor(
0346 workflow=workflow,
0347 base_output_dir="/tmp/dtlz2_optimization/case2",
0348 log_level="WARNING",
0349 )
0350
0351
0352 optimizer = SimpleRandomOptimizer(
0353 bounds={"x1": (0, 1), "x2": (0, 1), "x3": (0, 1)},
0354 n_iterations=15,
0355 seed=42,
0356 )
0357
0358 print(f"\n✓ Optimizer: SimpleRandomOptimizer")
0359 print(f" Iterations: {optimizer.n_iterations}")
0360 print(f" Bounds: {optimizer.bounds}")
0361
0362
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
0368 design_point = optimizer.suggest()
0369
0370
0371 objectives = executor.execute(design_point)
0372
0373
0374 optimizer.tell(design_point, objectives)
0375
0376
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
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
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
0417 optimizer1, executor1 = run_case_1_single_branch()
0418 optimizer2, executor2 = run_case_2_separate_branches()
0419
0420
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")