File indexing completed on 2026-08-12 08:24:57
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 resources: Resource requirements (free-form dict, e.g., {"memory": "4GB"}).
0102 outputs: Output artifacts this job produces.
0103
0104 Example:
0105 >>> job = JobDefinition(
0106 ... name="dtlz2_evaluate",
0107 ... command="python scripts/dtlz2_problem.py",
0108 ... payload={
0109 ... "design_params_file": "{input_design_params}",
0110 ... "output_file": "{output_dir}/objectives_{job_id}.json"
0111 ... },
0112 ... outputs=[ArtifactSpec(path="objectives_*.json", format="json")]
0113 ... )
0114
0115 Notes:
0116 - Payload supports template substitution: {job_id}, {output_dir}, {stage_outputs[stage_name]}
0117 - Resources dict is executor-dependent (e.g., JobLibRunner ignores, SlurmRunner uses)
0118 """
0119 name: str = Field(..., description="Job name")
0120 command: str = Field(..., description="Executable command")
0121 payload: Dict[str, Any] = Field(default_factory=dict, description="Command arguments")
0122 resources: Dict[str, Any] = Field(default_factory=dict, description="Resource requirements")
0123 outputs: List[ArtifactSpec] = Field(default_factory=list, description="Output artifacts")
0124
0125
0126 class JobFactory(BaseModel):
0127 """Factory for generating multiple jobs from a template.
0128
0129 Enables fan-out: creating N parallel jobs from one job definition.
0130 Useful for evaluating multiple design points in parallel.
0131
0132 Attributes:
0133 type: Factory type ("range", "enumerate", "Cartesian", etc).
0134 params: Factory-specific parameters (e.g., {"n": 4} for range).
0135
0136 Example:
0137 >>> # Create 4 parallel design point evaluations
0138 >>> factory = JobFactory(type="range", params={"n": 4})
0139
0140 Notes:
0141 - "range" type: creates N copies with job_id = 0..N-1
0142 - "enumerate" type: creates one job per item in a list
0143 - "Cartesian" type: creates N_A * N_B jobs from two parameter sets
0144 """
0145 type: str = Field(default="range", description="Factory type (range, enumerate, Cartesian, etc)")
0146 params: Dict[str, Any] = Field(default_factory=dict, description="Factory parameters")
0147
0148
0149 class StageDefinition(BaseModel):
0150 """Stage/layer definition with jobs and scheduler.
0151
0152 A stage is a logical step group where multiple jobs run in parallel (fan-out),
0153 then their outputs feed into downstream stages (fan-in).
0154
0155 Attributes:
0156 name: Stage name (e.g., "evaluate", "aggregate").
0157 jobs: Job definitions to execute (usually one template, expanded via job_factory).
0158 job_factory: Optional factory for expanding jobs (e.g., N parallel evals).
0159 scheduler: Stage-level scheduler (optional, inherits global if not set).
0160 parallelism: Parallelism policy for this stage.
0161 outputs: Output artifact specs produced by this stage.
0162
0163 Example:
0164 >>> stage = StageDefinition(
0165 ... name="evaluate",
0166 ... jobs=[
0167 ... JobDefinition(
0168 ... name="dtlz2_evaluate",
0169 ... command="python scripts/dtlz2_problem.py",
0170 ... payload={...},
0171 ... outputs=[ArtifactSpec(path="objectives_*.json", format="json")]
0172 ... )
0173 ... ],
0174 ... job_factory=JobFactory(type="range", params={"n": 4}),
0175 ... parallelism=ParallelismPolicy(max_concurrent=4, retry_max=2),
0176 ... outputs=[ArtifactSpec(path="objectives_*.json", format="json")]
0177 ... )
0178
0179 Notes:
0180 - job_factory expands the first job in jobs list to N parallel jobs
0181 - scheduler overrides global scheduler (from WorkflowsConfiguration)
0182 - outputs are collected after all jobs complete
0183 """
0184 name: str = Field(..., description="Stage name")
0185 jobs: List[JobDefinition] = Field(default_factory=list, description="Job definitions")
0186 job_factory: Optional[JobFactory] = Field(default=None, description="Job expansion factory")
0187 scheduler: Optional[SchedulerConfiguration] = Field(default=None, description="Stage-level scheduler override")
0188 parallelism: ParallelismPolicy = Field(default_factory=ParallelismPolicy, description="Parallelism policy")
0189 outputs: List[ArtifactSpec] = Field(default_factory=list, description="Output artifacts")
0190
0191
0192 class BranchDefinition(BaseModel):
0193 """Branch definition (optional, for organizing stages in a DAG).
0194
0195 A branch is an optional subgraph inside a workflow, useful when you want
0196 multiple independent pipelines under one workflow (e.g., "physics sim" branch
0197 + "surrogate" branch). Stages within a branch are executed in topological order.
0198
0199 Attributes:
0200 name: Branch name (e.g., "main", "physics_sim", "surrogate").
0201 stages: List of stages in DAG order (assumes simple sequential order; extend with explicit DAG if needed).
0202 scheduler: Optional branch-level scheduler default (used if stage not set).
0203
0204 Example:
0205 >>> branch = BranchDefinition(
0206 ... name="main",
0207 ... stages=[
0208 ... StageDefinition(name="evaluate", ...),
0209 ... StageDefinition(name="aggregate", ...)
0210 ... ]
0211 ... )
0212
0213 Notes:
0214 - Multiple branches in one workflow execute independently
0215 - For complex DAGs, extend this model with explicit edge definitions
0216 """
0217 name: str = Field(..., description="Branch name")
0218 stages: List[StageDefinition] = Field(default_factory=list, description="Stages in execution order")
0219 scheduler: Optional[SchedulerConfiguration] = Field(
0220 default=None,
0221 description="Branch-level scheduler default (overrides workflow, used if stage unset)",
0222 )
0223
0224
0225 class WorkflowDefinition(BaseModel):
0226 """Workflow definition with branches, objectives, and scheduler defaults.
0227
0228 A workflow is an end-to-end evaluation unit (e.g., one design point evaluation).
0229 It consists of optional branches, each with multiple stages, and defines the
0230 objectives to compute from the outputs.
0231
0232 Attributes:
0233 name: Workflow name (e.g., "dtlz2_eval").
0234 description: Optional description.
0235 branches: Workflow branches (optional, defaults to single implicit branch if missing).
0236 objectives: Objectives to compute (reuses ObjectiveDefinition).
0237 combined_objectives: Optional combined plans emitting multiple metrics in one run.
0238 scheduler: Workflow-level scheduler default (used if branch/stage unset).
0239
0240 Notes:
0241 - If branches is empty, executor creates single implicit branch
0242 - Objectives are unified model (ObjectiveDefinition) for consistency
0243 """
0244 name: str = Field(..., description="Workflow name")
0245 description: Optional[str] = Field(default=None, description="Workflow description")
0246 branches: List[BranchDefinition] = Field(default_factory=list, description="Workflow branches (optional)")
0247 objectives: List[ObjectiveDefinition] = Field(
0248 default_factory=list,
0249 description="Objectives to compute (reuses ObjectiveDefinition)"
0250 )
0251 combined_objectives: List[CombinedObjectivePlan] = Field(
0252 default_factory=list,
0253 description="Combined objective plans emitting multiple metrics in one run",
0254 )
0255 scheduler: Optional[SchedulerConfiguration] = Field(
0256 default=None,
0257 description="Workflow-level scheduler default (overrides global, used if branch/stage unset)",
0258 )
0259 stack_type: Optional[str] = Field(default=None,description="Experimental stack type for workflow-level geometry prep")
0260
0261 def get_implicit_branch(self) -> BranchDefinition:
0262 """Get or create single implicit branch if branches list is empty.
0263
0264 Returns:
0265 Single implicit branch if branches is empty, else raises error.
0266
0267 Raises:
0268 ValueError: If branches list is not empty.
0269 """
0270 if self.branches:
0271 raise ValueError("Branches already defined; cannot use implicit branch")
0272 return BranchDefinition(name="implicit")
0273
0274
0275 class WorkflowsConfiguration(BaseModel):
0276 """Top-level workflows configuration.
0277
0278 Container for multiple independent workflows (e.g., one per objective in a
0279 holistic optimization). Each workflow can have its own stages, scheduler, and
0280 objective specs.
0281
0282 Attributes:
0283 workflows: List of independent workflows.
0284 global_scheduler: Default scheduler for all stages (can be overridden per-stage).
0285
0286 Example:
0287 >>> config = WorkflowsConfiguration(
0288 ... workflows=[
0289 ... WorkflowDefinition(name="dtlz2_eval", ...),
0290 ... WorkflowDefinition(name="physics_sim", ...)
0291 ... ],
0292 ... global_scheduler=SchedulerConfiguration(
0293 ... runner_type="JobLibRunner",
0294 ... joblib=JobLibRunnerConfig(n_jobs=-1)
0295 ... )
0296 ... )
0297
0298 Notes:
0299 - global_scheduler is inherited by all stages unless overridden
0300 - workflows list must be non-empty
0301 - Useful for Option B: multiple independent workflows per objective
0302 """
0303 workflows: List[WorkflowDefinition] = Field(..., min_items=1, description="List of workflows")
0304 global_scheduler: Optional[SchedulerConfiguration] = Field(
0305 default=None,
0306 description="Default scheduler for all stages (can be overridden per-stage)"
0307 )
0308
0309 @field_validator('workflows')
0310 @classmethod
0311 def validate_unique_workflow_names(cls, workflows: List[WorkflowDefinition]) -> List[WorkflowDefinition]:
0312 """Ensure all workflow names are unique.
0313
0314 Args:
0315 workflows: List of workflow definitions.
0316
0317 Returns:
0318 Same list if valid.
0319
0320 Raises:
0321 ValueError: If duplicate workflow names found.
0322 """
0323 names = [w.name for w in workflows]
0324 if len(set(names)) != len(names):
0325 raise ValueError("Workflow names must be unique")
0326 return workflows