File indexing completed on 2026-08-12 08:24:56
0001 """Workflow configuration models for multi-stage, multi-objective orchestration.
0002
0003 Defines DAG-based workflows with branches, stages, jobs, and objective computation specs.
0004 Reuses ObjectiveDefinition from objectives.py for unified objective specification.
0005
0006 Key concepts:
0007 Workflow: An end-to-end evaluation unit (one design point evaluation).
0008 Branch: Optional subgraph inside a workflow (useful for multiple independent pipelines).
0009 Stage/Layer: Logical step group where multiple jobs run in parallel (fan-out).
0010 Job/Task: Smallest schedulable unit (one simulation, one training run, etc).
0011 Scheduler: Runtime executor for jobs (submit, monitor, collect status/artifacts).
0012
0013 Project: AID2E v0.0.0 - AI assisted Detector Design for EIC
0014 Homepage: https://aid2e.github.io/aid2e-framework
0015 Repository: https://github.com/aid2e/AID2E-framework.git
0016 """
0017
0018 from typing import List, Optional, Dict, Any, Union
0019 from pydantic import BaseModel, Field, field_validator
0020 from aid2e.utilities.configurations.objectives import ObjectiveDefinition, ObjectivePlanSpec, ObjectiveDirection
0021 from aid2e.utilities.configurations.scheduler_config import SchedulerConfiguration
0022
0023
0024 class CombinedObjectiveMetric(BaseModel):
0025 """Metric emitted by a combined objective plan.
0026
0027 Attributes:
0028 name: Objective name (e.g., "f1").
0029 direction: Optimization direction for this metric.
0030 metric_key: Key in the plan output to extract this metric.
0031 """
0032
0033 name: str = Field(..., description="Objective metric name")
0034 direction: ObjectiveDirection = Field(..., description="Direction for this metric")
0035 metric_key: str = Field(..., description="Key in plan output for this metric")
0036
0037
0038 class CombinedObjectivePlan(BaseModel):
0039 """Combined objective execution producing multiple metrics in one plan.
0040
0041 Attributes:
0042 name: Identifier for the combined objective bundle.
0043 objective_plan: Plan executed once to emit multiple metrics.
0044 metrics: Metrics extracted from the plan output with their directions.
0045 scheduler: Optional scheduler default for this combined plan.
0046 """
0047
0048 name: str = Field(..., description="Combined objective bundle name")
0049 objective_plan: ObjectivePlanSpec = Field(..., description="Plan producing multiple metrics")
0050 metrics: list[CombinedObjectiveMetric] = Field(
0051 ..., min_items=1, description="Metrics emitted by this plan"
0052 )
0053 scheduler: Optional[SchedulerConfiguration] = Field(
0054 default=None,
0055 description="Scheduler default for this combined plan",
0056 )
0057
0058
0059 class ParallelismPolicy(BaseModel):
0060 """Policy for parallel job execution within a stage.
0061
0062 Attributes:
0063 max_concurrent: Maximum jobs to run concurrently in a stage.
0064 retry_max: Maximum retries on job failure.
0065 timeout_sec: Timeout per job in seconds.
0066
0067 Example:
0068 >>> policy = ParallelismPolicy(max_concurrent=4, retry_max=2, timeout_sec=300)
0069 """
0070 max_concurrent: int = Field(default=4, ge=1, description="Max concurrent jobs in stage")
0071 retry_max: int = Field(default=2, ge=0, description="Max retries per failed job")
0072 timeout_sec: int = Field(default=300, ge=1, description="Timeout per job (seconds)")
0073
0074
0075 class ArtifactSpec(BaseModel):
0076 """Output artifact specification for a stage.
0077
0078 Defines expected output files that stages/jobs produce.
0079
0080 Attributes:
0081 path: File path pattern (e.g., "objectives_*.json").
0082 format: File format ("json", "yaml", "csv", or "root").
0083
0084 Example:
0085 >>> artifact = ArtifactSpec(path="objectives_*.json", format="json")
0086 """
0087 path: str = Field(..., description="File path pattern (e.g., 'output_*.json')")
0088 format: str = Field(default="json", pattern="^(json|yaml|csv|root)$", description="File format")
0089
0090
0091 class JobDefinition(BaseModel):
0092 """Single job/task definition within a stage.
0093
0094 A job is the smallest schedulable unit (e.g., one simulation, training run, etc).
0095 Jobs can be expanded from a template via job_factory.
0096
0097 Attributes:
0098 name: Job name (e.g., "simulate").
0099 command: Executable command (e.g., "python scripts/dtlz2_problem.py").
0100 payload: Command arguments/payload (free-form dict, supports template substitution).
0101 rule: Optional template for constructing final command from payload.
0102 Uses format: "{command} {payload[key1]} {payload[key2]}" etc.
0103 If not specified, defaults to "{command}" (just execute command).
0104 resources: Resource requirements (free-form dict, e.g., {"memory": "4GB"}).
0105 outputs: Output artifacts this job produces.
0106
0107 Example:
0108 >>> job = JobDefinition(
0109 ... name="dtlz2_evaluate",
0110 ... command="python scripts/dtlz2_problem.py",
0111 ... rule="{{command}} {{payload[design_params_file]}} {{payload[output_dir]}} {payload[job_id]}",
0112 ... payload={
0113 ... "design_params_file": "{{input_design_params}}",
0114 ... "output_dir": "{{output_dir}}",
0115 ... "job_id": "{{job_id}}"
0116 ... },
0117 ... outputs=[ArtifactSpec(path="objectives_*.json", format="json")]
0118 ... )
0119
0120 Notes:
0121 - Payload supports template substitution: {{job_id}}, {{output_dir}}, {{stage_outputs[stage_name]}}
0122 - Rule template follows experimental_stack.py StackLayer pattern
0123 - Resources dict is executor-dependent (e.g., JobLibRunner ignores, SlurmRunner uses)
0124 """
0125 name: str = Field(..., description="Job name")
0126 command: str = Field(..., description="Executable command")
0127 payload: Dict[str, Any] = Field(default_factory=dict, description="Command arguments and metadata")
0128 rule: Optional[str] = Field(
0129 default=None,
0130 description="Template rule for command construction (e.g., '{{command}} {{payload[input]}} {{payload[output]}}')"
0131 )
0132 resources: Dict[str, Any] = Field(default_factory=dict, description="Resource requirements")
0133 outputs: List[ArtifactSpec] = Field(default_factory=list, description="Output artifacts")
0134
0135
0136 class JobFactory(BaseModel):
0137 """Factory for generating multiple jobs from a template.
0138
0139 Enables fan-out: creating N parallel jobs from one job definition.
0140 Useful for evaluating multiple design points in parallel.
0141
0142 Attributes:
0143 type: Factory type ("range", "enumerate", "Cartesian", etc).
0144 params: Factory-specific parameters (e.g., {"n": 4} for range).
0145
0146 Example:
0147 >>> # Create 4 parallel design point evaluations
0148 >>> factory = JobFactory(type="range", params={"n": 4})
0149
0150 Notes:
0151 - "range" type: creates N copies with job_id = 0..N-1
0152 - "enumerate" type: creates one job per item in a list
0153 - "Cartesian" type: creates N_A * N_B jobs from two parameter sets
0154 """
0155 type: str = Field(default="range", description="Factory type (range, enumerate, Cartesian, etc)")
0156 params: Dict[str, Any] = Field(default_factory=dict, description="Factory parameters")
0157
0158
0159 class StageDefinition(BaseModel):
0160 """Stage/layer definition with jobs and scheduler.
0161
0162 A stage is a logical step group where multiple jobs run in parallel (fan-out),
0163 then their outputs feed into downstream stages (fan-in).
0164
0165 Attributes:
0166 name: Stage name (e.g., "evaluate", "aggregate").
0167 jobs: Job definitions to execute (usually one template, expanded via job_factory).
0168 job_factory: Optional factory for expanding jobs (e.g., N parallel evals).
0169 scheduler: Stage-level scheduler (optional, inherits global if not set).
0170 parallelism: Parallelism policy for this stage.
0171 outputs: Output artifact specs produced by this stage.
0172
0173 Example:
0174 >>> stage = StageDefinition(
0175 ... name="evaluate",
0176 ... jobs=[
0177 ... JobDefinition(
0178 ... name="dtlz2_evaluate",
0179 ... command="python scripts/dtlz2_problem.py",
0180 ... payload={...},
0181 ... outputs=[ArtifactSpec(path="objectives_*.json", format="json")]
0182 ... )
0183 ... ],
0184 ... job_factory=JobFactory(type="range", params={"n": 4}),
0185 ... parallelism=ParallelismPolicy(max_concurrent=4, retry_max=2),
0186 ... outputs=[ArtifactSpec(path="objectives_*.json", format="json")]
0187 ... )
0188
0189 Notes:
0190 - job_factory expands the first job in jobs list to N parallel jobs
0191 - scheduler overrides global scheduler (from WorkflowsConfiguration)
0192 - outputs are collected after all jobs complete
0193 """
0194 name: str = Field(..., description="Stage name")
0195 jobs: List[JobDefinition] = Field(default_factory=list, description="Job definitions")
0196 job_factory: Optional[JobFactory] = Field(default=None, description="Job expansion factory")
0197 scheduler: Optional[SchedulerConfiguration] = Field(default=None, description="Stage-level scheduler override")
0198 parallelism: ParallelismPolicy = Field(default_factory=ParallelismPolicy, description="Parallelism policy")
0199 outputs: List[ArtifactSpec] = Field(default_factory=list, description="Output artifacts")
0200
0201
0202 class BranchDefinition(BaseModel):
0203 """Branch definition (optional, for organizing stages in a DAG).
0204
0205 A branch is an optional subgraph inside a workflow, useful when you want
0206 multiple independent pipelines under one workflow (e.g., "physics sim" branch
0207 + "surrogate" branch). Stages within a branch are executed in topological order.
0208
0209 Attributes:
0210 name: Branch name (e.g., "main", "physics_sim", "surrogate").
0211 stages: List of stages in DAG order (assumes simple sequential order; extend with explicit DAG if needed).
0212 scheduler: Optional branch-level scheduler default (used if stage not set).
0213
0214 Example:
0215 >>> branch = BranchDefinition(
0216 ... name="main",
0217 ... stages=[
0218 ... StageDefinition(name="evaluate", ...),
0219 ... StageDefinition(name="aggregate", ...)
0220 ... ]
0221 ... )
0222
0223 Notes:
0224 - Multiple branches in one workflow execute independently
0225 - For complex DAGs, extend this model with explicit edge definitions
0226 """
0227 name: str = Field(..., description="Branch name")
0228 stages: List[StageDefinition] = Field(default_factory=list, description="Stages in execution order")
0229 scheduler: Optional[SchedulerConfiguration] = Field(
0230 default=None,
0231 description="Branch-level scheduler default (overrides workflow, used if stage unset)",
0232 )
0233
0234
0235 class WorkflowDefinition(BaseModel):
0236 """Workflow definition with branches, objectives, and scheduler defaults.
0237
0238 A workflow is an end-to-end evaluation unit (e.g., one design point evaluation).
0239 It consists of optional branches, each with multiple stages, and defines the
0240 objectives to compute from the outputs.
0241
0242 Attributes:
0243 name: Workflow name (e.g., "dtlz2_eval").
0244 description: Optional description.
0245 branches: Workflow branches (optional, defaults to single implicit branch if missing).
0246 objectives: Objectives to compute (reuses ObjectiveDefinition).
0247 combined_objectives: Optional combined plans emitting multiple metrics in one run.
0248 scheduler: Workflow-level scheduler default (used if branch/stage unset).
0249
0250 Notes:
0251 - If branches is empty, executor creates single implicit branch
0252 - Objectives are unified model (ObjectiveDefinition) for consistency
0253 """
0254 name: str = Field(..., description="Workflow name")
0255 description: Optional[str] = Field(default=None, description="Workflow description")
0256 stack_type: Optional[str] = Field(default=None,description="Experimental stack type for workflow-level geometry prep")
0257 branches: List[BranchDefinition] = Field(default_factory=list, description="Workflow branches (optional)")
0258 objectives: List[ObjectiveDefinition] = Field(
0259 default_factory=list,
0260 description="Objectives to compute (reuses ObjectiveDefinition)"
0261 )
0262 combined_objectives: List[CombinedObjectivePlan] = Field(
0263 default_factory=list,
0264 description="Combined objective plans emitting multiple metrics in one run",
0265 )
0266 scheduler: Optional[SchedulerConfiguration] = Field(
0267 default=None,
0268 description="Workflow-level scheduler default (overrides global, used if branch/stage unset)",
0269 )
0270 stack_type: Optional[str] = Field(default=None,description="Experimental stack type for workflow-level geometry prep")
0271
0272 def get_implicit_branch(self) -> BranchDefinition:
0273 """Get or create single implicit branch if branches list is empty.
0274
0275 Returns:
0276 Single implicit branch if branches is empty, else raises error.
0277
0278 Raises:
0279 ValueError: If branches list is not empty.
0280 """
0281 if self.branches:
0282 raise ValueError("Branches already defined; cannot use implicit branch")
0283 return BranchDefinition(name="implicit")
0284
0285
0286 class WorkflowsConfiguration(BaseModel):
0287 """Top-level workflows configuration.
0288
0289 Container for multiple independent workflows (e.g., one per objective in a
0290 holistic optimization). Each workflow can have its own stages, scheduler, and
0291 objective specs.
0292
0293 Attributes:
0294 workflows: List of independent workflows.
0295 global_scheduler: Default scheduler for all stages (can be overridden per-stage).
0296
0297 Example:
0298 >>> config = WorkflowsConfiguration(
0299 ... workflows=[
0300 ... WorkflowDefinition(name="dtlz2_eval", ...),
0301 ... WorkflowDefinition(name="physics_sim", ...)
0302 ... ],
0303 ... global_scheduler=SchedulerConfiguration(
0304 ... runner_type="JobLibRunner",
0305 ... joblib=JobLibRunnerConfig(n_jobs=-1)
0306 ... )
0307 ... )
0308
0309 Notes:
0310 - global_scheduler is inherited by all stages unless overridden
0311 - workflows list must be non-empty
0312 - Useful for Option B: multiple independent workflows per objective
0313 """
0314 workflows: List[WorkflowDefinition] = Field(..., min_items=1, description="List of workflows")
0315 global_scheduler: Optional[SchedulerConfiguration] = Field(
0316 default=None,
0317 description="Default scheduler for all stages (can be overridden per-stage)"
0318 )
0319
0320 @field_validator('workflows')
0321 @classmethod
0322 def validate_unique_workflow_names(cls, workflows: List[WorkflowDefinition]) -> List[WorkflowDefinition]:
0323 """Ensure all workflow names are unique.
0324
0325 Args:
0326 workflows: List of workflow definitions.
0327
0328 Returns:
0329 Same list if valid.
0330
0331 Raises:
0332 ValueError: If duplicate workflow names found.
0333 """
0334 names = [w.name for w in workflows]
0335 if len(set(names)) != len(names):
0336 raise ValueError("Workflow names must be unique")
0337 return workflows