Warning, /AID2E-framework/docs/user-guide/workflows.md is written in an unsupported language. File is not indexed.
0001 # Workflows and Objectives
0002
0003 This guide documents AID2E's workflow execution and objective specification system.
0004
0005 ## Execution Model
0006
0007 A workflow evaluates one design point. It contains branches, each branch contains stages, and each stage contains zero or more jobs. The current executor runs branches in configuration order. Within each branch, stages are treated as a sequential chain: every stage depends on the previous stage. Jobs expanded within one stage are submitted together and may run in parallel through the effective scheduler.
0008
0009 A downstream stage begins only after all jobs in the preceding stage complete successfully. A failed scheduler stage stops the workflow and prevents later stages in that branch from running.
0010
0011 The current workflow model does not expose explicit dependencies between workflow stages. Objective step plans have their own `depends_on` fields and are topologically sorted separately. Although an empty branch list is accepted by the model, the current executor does not create executable stages for an implicit branch; executable workflows should declare their branches and stages.
0012
0013 For each stage, the executor:
0014
0015 1. Expands the configured jobs.
0016 2. Resolves the stage scheduler.
0017 3. Executes all jobs directly or through that scheduler.
0018 4. Collects scheduler outputs and artifacts into the workflow XCom mapping.
0019 5. Executes the stage objective plan, when configured.
0020
0021 After all branches complete, the executor collects the declared objectives and returns them to the optimization loop.
0022
0023 ## Jobs and Job Factories
0024
0025 ### Job Execution Types
0026
0027 The job `payload.evaluator_type` selects the execution engine. When it is omitted, the job uses command execution.
0028
0029 - **Command/Bash** executes `job.command`; direct execution uses `payload.env` for additional environment variables.
0030 - **Python** uses `evaluator_type: "python"` with `python_callable`. Direct execution expects a callable object and passes `JobContext` first, followed by `op_args` and `op_kwargs`. Scheduler-backed Python jobs forward `op_kwargs`; the workflow adapter currently rejects this evaluator type for Slurm.
0031 - **Container** uses `evaluator_type: "container"` with image, command, environment, volume, and resource settings for direct execution.
0032 - **Stack** uses `evaluator_type: "stack"` with a registered `stack_type` and one or more ordered layers.
0033
0034 ### Job Factories
0035
0036 A stage can execute its declared jobs directly or expand the first job as a template through `job_factory`. The current executor implements two factory types: `range` and `payloads`. Other names described by the model are not implemented and raise `ValueError`.
0037
0038 The `range` factory creates `n` deep copies of the first job. Each copy is named `<template_name>_<index>` and receives `job_index` in its payload:
0039
0040 ```yaml
0041 job_factory:
0042 type: "range"
0043 params:
0044 n: 4
0045 ```
0046
0047 The `payloads` factory calls a Python function referenced as `module.path:function_name`. The function receives `stage_id`, `problem_config`, and `workflow_context` and must return a list of mappings. One deep-copied job is created for each mapping, and that mapping is merged into the template payload:
0048
0049 ```yaml
0050 job_factory:
0051 type: "payloads"
0052 params:
0053 from: "workflow_utils:simulation_payloads"
0054 ```
0055
0056 Because expansion deep-copies the template job, stack layer definitions are preserved for every generated job. Additional jobs declared after the first template are not expanded when a factory is present.
0057
0058 ### Callable Entrypoints
0059
0060 Payload factories and inline objective functions use `module.path:function_name`. The framework imports the module and resolves the named function. Factories receive the stage and workflow context; objective functions receive the design point and objective inputs and outputs. Python job callable support depends on the execution path; direct and JobLib jobs require a callable object.
0061
0062 ## Scheduler Cascade
0063
0064 ### Overview
0065
0066 Stages inherit scheduler settings in this order:
0067
0068 ```
0069 Cascade Precedence (highest to lowest):
0070 1. Stage-level scheduler (override)
0071 2. Branch-level scheduler (branch default)
0072 3. Workflow-level scheduler (workflow default)
0073 4. Global scheduler (global default)
0074 ```
0075
0076 The top-level `scheduler` is used as the global default when present; otherwise, `workflows.global_scheduler` is used. An objective-level scheduler is reserved in the schema but raises an error in the current runtime. Scheduled objective work must be represented as a workflow stage.
0077
0078 ### Example
0079
0080 Illustrative scheduler overrides (add an optimizer and evaluator for a complete run):
0081
0082 ```yaml
0083 problem:
0084 name: "DTLZ2"
0085 problem_type: "DTLZ2"
0086 design_parameters_file: "design.params"
0087 objectives:
0088 - name: "f1"
0089 direction: "minimize"
0090
0091 # Global/workflow-level scheduler
0092 scheduler:
0093 runner_type: "JobLibRunner"
0094 parameters:
0095 n_jobs: 8
0096 backend: "loky"
0097
0098 workflows:
0099 workflows:
0100 - name: "dtlz2_eval"
0101
0102 # Workflow-level scheduler (overrides global)
0103 scheduler:
0104 runner_type: "JobLibRunner"
0105 parameters:
0106 n_jobs: 4
0107 backend: "threading"
0108
0109 branches:
0110 - name: "main"
0111
0112 # Branch-level scheduler (overrides workflow)
0113 scheduler:
0114 runner_type: "JobLibRunner"
0115 parameters:
0116 n_jobs: 2
0117 backend: "loky"
0118
0119 stages:
0120 - name: "evaluate"
0121
0122 # Stage-level scheduler (overrides branch)
0123 scheduler:
0124 runner_type: "SlurmRunner"
0125 parameters:
0126 partition: "gpu"
0127 nodes: 1
0128
0129 jobs:
0130 - name: "compute_objective"
0131 command: "python eval.py"
0132
0133 ```
0134
0135 ### Precedence Resolution
0136
0137 For `evaluate`, the stage-level `SlurmRunner` overrides the branch default. A stage without its own scheduler uses the branch's `JobLibRunner` with `n_jobs=2`.
0138
0139 ## Stack Jobs and EpicStack
0140
0141 The stack interface allows a workflow job to execute one or more ordered layers of a registered experimental software stack. The ePIC implementation is registered as `epic`.
0142
0143 A stack workflow declares `stack_type`, while each stack job declares `evaluator_type: "stack"` and one or more `layers`. A job-level `stack_type` can override the workflow value. Missing or unknown stack types and missing layer configurations raise errors.
0144
0145 This layer fragment requires the ePIC environment and design configuration:
0146
0147 ```yaml
0148 workflows:
0149 workflows:
0150 - name: "epic_workflow"
0151 stack_type: "epic"
0152 branches:
0153 - name: "main"
0154 stages:
0155 - name: "sim_rec"
0156 jobs:
0157 - name: "epic_sim_rec"
0158 payload:
0159 evaluator_type: "stack"
0160 layers:
0161 - layer: "sim"
0162 inputs: []
0163 outputs: ["sim.root"]
0164 arguments: ["-N 100"]
0165 - layer: "rec"
0166 inputs: ["sim.root"]
0167 outputs: ["reco.root"]
0168 ```
0169
0170 Layers execute in their listed order within one job. Inputs, outputs, arguments, commands, and rules support templates resolved from the job context.
0171
0172 ### EpicStack
0173
0174 ```python
0175 from aid2e.utilities.epic_utils import EpicStack
0176 ```
0177
0178 `EpicStack` provides four layers:
0179
0180 - `geo` runs `checkOverlaps`. It requires one geometry input and one log output, and exits with code 9 unless the log reports zero illegal overlaps or extrusions.
0181 - `sim` runs `npsim` and formats generator, HepMC, macro, and output arguments.
0182 - `rec` runs `eicrecon` and formats reconstruction inputs and podio output arguments.
0183 - `ana` runs a user-provided analysis command and rule.
0184
0185 Geometry is prepared once per trial before branches execute. In `no_build` mode, EpicStack copies `<epic_install>/share/epic` into the workflow output under `geometry/epic`, applies the design-point XML modifications, and uses that copy as `DETECTOR_PATH`. In `build` mode, it copies the configured geometry source, applies the modifications, and builds and installs that workflow copy.
0186
0187 Generated ePIC driver scripts use `set -euo pipefail`, initialize the selected geometry, and run through `EIC_SINGULARITY_IMAGE` or `EIC_SHELL`. The workflow therefore requires a valid ePIC environment configuration and an ePIC design configuration. See the [dRICH example](https://github.com/aid2e/AID2E-framework/tree/main/examples/epic/drich) for a complete `geo`, `sim`/`rec`, and `ana` workflow, and the [BIC example](https://github.com/aid2e/AID2E-framework/tree/main/examples/epic/bic) for another ePIC detector workflow.
0188
0189 The current full-config loader selects stack-specific workflow models when a workflow declares `stack_type`. A `WorkflowsConfiguration` should currently use one registered stack type; mixed stacks in one workflow collection are not yet supported reliably.
0190
0191 ## Objective Plans
0192
0193 ### Objective Collection
0194
0195 Objectives can be returned in three ways:
0196
0197 1. A job returns declared objective names directly in a mapping, an `objectives` mapping, or individual output keys.
0198 2. A stage-level objective plan runs after that stage's jobs and stores its extracted metrics in XCom.
0199 3. An objective-level or combined objective plan runs during final objective collection.
0200
0201 Objective plans execute locally inside the trial's `DAGExecutor`, even when workflow jobs use a remote scheduler. Their `StepStage` objects are separate from workflow `StageDefinition` objects and do not use the scheduler cascade.
0202
0203 - Inline steps receive `design_point`, `inputs`, `outputs`, `extra_args`, `xcom`, `work_dir`, `output_dir`, `problem_config`, and `workflow_context`. Trial metadata is available through `workflow_context`.
0204 - Script steps receive design-point and step-input JSON files, run the configured executable, and read its JSON output. Parameter and output paths are passed through command arguments and environment variables.
0205 - Dependencies determine execution order. Their results are passed to downstream steps through the input mapping. `produces_from_stage` selects the plan result.
0206
0207 A stage-level plan must return all objectives declared for the workflow. A single-objective plan extracts its one `metrics_keys` entry, or defaults to the objective name. Optional uncertainty values use `<objective_name>_err`.
0208
0209 In a full configuration, declare objectives under `problem.objectives` only; duplicated `workflows[].objectives` declarations are rejected.
0210
0211 See [workflow notes](notes/workflows.md) for objective model classes, stack registration, and geometry preparation internals.
0212
0213 Objective-level and combined-plan `scheduler` fields are reserved but unsupported by the executor. Use a workflow stage for scheduled objective work.
0214
0215 ### Single-Step and Multi-Step Plans
0216
0217 All objective plans use `steps`, with one stage or several dependent stages.
0218
0219 Example (single script stage):
0220
0221 ```python
0222 from aid2e.utilities.configurations.objectives import (
0223 ObjectiveDefinition, ObjectiveDirection, ObjectivePlanSpec,
0224 ScriptObjective, StepPlanSpec, StepStage,
0225 )
0226
0227 ObjectiveDefinition(
0228 name="f1",
0229 direction=ObjectiveDirection.MINIMIZE,
0230 objective_plan=ObjectivePlanSpec(
0231 steps=StepPlanSpec(
0232 stages=[
0233 StepStage(
0234 name="f1_stage_0",
0235 script=ScriptObjective(
0236 path="scripts/dtlz2.py",
0237 output_file="f1.json",
0238 ),
0239 produces_objective=True,
0240 )
0241 ]
0242 )
0243 )
0244 )
0245 ```
0246
0247 ## Combined Objectives
0248
0249 ### Motivation
0250
0251 Sometimes a single computation produces **multiple objective metrics**. For example:
0252 - A DTLZ2 evaluation script outputs both `f1` and `f2`
0253 - A surrogate model prediction outputs multiple target values
0254 - A simulation produces both efficiency and quality scores
0255
0256 Instead of running the same plan twice, **combined objectives** allow one execution to produce multiple metrics.
0257
0258 ### Usage in Workflows
0259
0260 Add `combined_objectives` to a workflow. This illustrative fragment requires matching declarations in `problem.objectives` and a script that returns all three metrics:
0261
0262 ```yaml
0263 workflows:
0264 workflows:
0265 - name: "dtlz2_multi"
0266
0267 # Combined objectives: one plan produces multiple metrics
0268 combined_objectives:
0269 - name: "dtlz2_pareto"
0270
0271 objective_plan:
0272 steps:
0273 stages:
0274 - name: "evaluate_objectives"
0275 script:
0276 path: "scripts/evaluate_metrics.py"
0277 output_file: "objectives.json"
0278 produces_objective: true
0279
0280 metrics:
0281 - name: "f1"
0282 direction: "minimize"
0283 metric_key: "f1" # Extract {"f1": ...} from output
0284
0285 - name: "f2"
0286 direction: "minimize"
0287 metric_key: "f2" # Extract {"f2": ...} from output
0288
0289 - name: "efficiency"
0290 direction: "maximize"
0291 metric_key: "efficiency" # Extract {"efficiency": ...}
0292 ```
0293
0294 ### Output Format
0295
0296 The objective plan script should output a JSON file with the metric values:
0297
0298 ```json
0299 {
0300 "f1": 0.45,
0301 "f2": 0.67,
0302 "efficiency": 0.92
0303 }
0304 ```
0305
0306 Each key in this object becomes extractable via `metric_key` in `CombinedObjectiveMetric`.
0307
0308 In full configurations, combined metrics should match `problem.objectives`, which supplies the optimizer's objective names and directions. `metric_key` controls how output keys map to those names.
0309
0310 ## Step Plans
0311
0312 ### Structure
0313
0314 A step plan contains one or more stages, each with a distinct computation:
0315
0316 ```python
0317 from aid2e.utilities.configurations.objectives import (
0318 ObjectiveDefinition, ObjectiveDirection, ObjectivePlanSpec,
0319 ScriptObjective, StepPlanSpec, StepStage,
0320 )
0321
0322 ObjectiveDefinition(
0323 name="complex_eval",
0324 direction=ObjectiveDirection.MINIMIZE,
0325 objective_plan=ObjectivePlanSpec(
0326 steps=StepPlanSpec(
0327 stages=[
0328 StepStage(
0329 name="preprocess",
0330 script=ScriptObjective(path="preprocess.py", output_file="prep.json"),
0331 inputs={"design_file": "design_params.json"},
0332 outputs={"prepared_file": "preprocessed.json"},
0333 produces_objective=False,
0334 ),
0335 StepStage(
0336 name="evaluate",
0337 script=ScriptObjective(path="evaluate.py", output_file="eval.json"),
0338 depends_on=["preprocess"],
0339 inputs={"prepared_file": "preprocessed.json"},
0340 outputs={"metrics_file": "objectives.json"},
0341 produces_objective=True, # This stage produces the final objective
0342 ),
0343 ],
0344 produces_from_stage="evaluate", # Which stage's output is the objective value
0345 )
0346 )
0347 )
0348 ```
0349
0350 ### StepStage Fields
0351
0352 - `name` (str): Stage name (must be unique within a plan)
0353 - `script` or `inline` (exactly one): The execution mode
0354 - `script`: Path to executable script
0355 - `inline`: Python function entrypoint
0356 - `inputs` (Dict[str, Any]): Named input bindings
0357 - `outputs` (Dict[str, Any]): Named output bindings
0358 - `extra_args` (Dict[str, Any]): Additional arguments to pass to the stage
0359 - `produces_objective` (bool): Whether this stage produces the objective value
0360 - `depends_on` (List[str]): Names of preceding stages this depends on
0361
0362 ### Validation
0363
0364 The model automatically validates:
0365 - **Mutual exclusivity**: Each stage has exactly one of `script` or `inline`
0366 - **Unique names**: All stage names are unique within a plan
0367 - **Single producer**: Select `produces_from_stage`, or mark exactly one stage with `produces_objective=True`
0368 - **Dependency satisfaction**: All dependencies reference existing stages
0369
0370 At execution, steps are topologically sorted and cycles are rejected. Results from dependencies are supplied to downstream steps through their input mapping.
0371
0372 ## Outputs and Failures
0373
0374 For `aid2e optimize`, the configured output and work locations contain one directory per run:
0375
0376 ```text
0377 <output_location>/<run-id>/
0378 |-- optimization_results.json
0379 |-- pareto_front.json
0380 `-- trials/
0381 `-- trial_<index>/
0382 |-- log/
0383 |-- <stage>/<job>/
0384 `-- geometry/epic/
0385
0386 <work_location>/<run-id>/
0387 `-- trials/
0388 `-- trial_<index>/
0389 |-- _scheduler/<stage>/
0390 |-- _objectives/<plan>/<step>/
0391 `-- <stage>/<job>/
0392 ```
0393
0394 Each job receives a work directory at `<work_dir>/<stage>/<job>` and an output directory at `<output_dir>/<stage>/<job>`. The workflow also creates a shared `log` directory. Direct job return values are pushed to XCom. For scheduler jobs, collected outputs, standard output, standard error, and artifacts are stored in the workflow XCom mapping for downstream stages and objective collection.
0395
0396 For command jobs, `job.outputs` describes scheduler-collected artifacts, with paths resolved against the runtime context. Collection depends on the runner; JobLib reads existing declared files as text. `StageDefinition.outputs` is not separately collected by the executor. Stack-layer `outputs` construct the layer commands; the current scheduler adapter does not automatically inventory them.
0397
0398 Workflow checkpoints record stage and job completion or failure. See [run records](cli.md#run-records) for saved optimization results.
0399
0400 The executor requires every declared objective. Missing metrics, invalid JSON, non-numeric values, nonzero job return codes, and failed scheduler stages are workflow errors. Direct objective collection currently visits XCom values in insertion order, so repeated objective keys can overwrite earlier values rather than raising an error; workflows with multiple producers should use an explicit aggregation plan.
0401
0402 Workflow errors propagate to the optimization loop. The problem's `evaluation_config.trial_failure_policy` then determines whether the optimizer marks the trial as failed or completes it with configured `penalty_objectives`. `max_failed_trials` limits tolerated failed evaluations. The default policy is `fail`, the default `max_failed_trials` is `0`, and the `penalty` policy requires a configured value for every declared objective. Scheduler retry and timeout limitations are documented in the [scheduler guide](schedulers.md#parallelism-and-failure-behavior).