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
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
0060 g = np.sum((x[1:] - 0.5) ** 2)
0061
0062
0063 f1 = (1 + g) * np.cos(x[0] * np.pi / 2) * np.cos(x[1] * np.pi / 2)
0064
0065
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
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
0115 context.add_log(f"Design point: {x}")
0116 context.add_log(f"Objectives: {objectives}")
0117
0118
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
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
0149 context.xcom_push("f2", f2)
0150
0151 return f2
0152
0153
0154
0155
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
0168 compute_job = JobDefinition(
0169 name="compute_objectives",
0170 command="python",
0171 payload={
0172 "evaluator_type": "python",
0173 "python_callable": evaluate_both_objectives_wrapper,
0174 "op_args": (),
0175 "op_kwargs": {},
0176 },
0177 )
0178
0179
0180 eval_stage = StageDefinition(
0181 name="evaluate",
0182 jobs=[compute_job],
0183 )
0184
0185
0186 main_branch = BranchDefinition(
0187 name="main",
0188 stages=[eval_stage],
0189 )
0190
0191
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
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
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
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
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
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
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
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
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
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
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
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
0380 executor = DAGExecutor(
0381 workflow=workflow,
0382 base_output_dir="/tmp/dtlz2_optimization/case1",
0383 log_level="WARNING",
0384 )
0385
0386
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
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
0403 design_point = optimizer.suggest()
0404
0405
0406 objectives = executor.execute(design_point)
0407
0408
0409 optimizer.tell(design_point, objectives)
0410
0411
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
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
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
0449 executor = DAGExecutor(
0450 workflow=workflow,
0451 base_output_dir="/tmp/dtlz2_optimization/case2",
0452 log_level="WARNING",
0453 )
0454
0455
0456 optimizer = SimpleRandomOptimizer(
0457 bounds={"x1": (0, 1), "x2": (0, 1), "x3": (0, 1)},
0458 n_iterations=15,
0459 seed=42,
0460 )
0461
0462 print(f"\n✓ Optimizer: SimpleRandomOptimizer")
0463 print(f" Iterations: {optimizer.n_iterations}")
0464 print(f" Bounds: {optimizer.bounds}")
0465
0466
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
0472 design_point = optimizer.suggest()
0473
0474
0475 objectives = executor.execute(design_point)
0476
0477
0478 optimizer.tell(design_point, objectives)
0479
0480
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
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
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
0521 optimizer1, executor1 = run_case_1_single_branch()
0522 optimizer2, executor2 = run_case_2_separate_branches()
0523
0524
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")