Data Reduction Pipeline Design Guidelines#
1. Purpose#
This document defines the minimum architecture for an observatory Data Reduction Pipeline (DRP).
The goal is to provide enough structure for a DRP to be:
Observable: what ran, with which parameters, how far it progressed, and what it produced are visible during and after a run.
Reproducible: a reduction can be re-run and yield the same scientific result from the same inputs.
Selective: a change to one step re-runs that step and its dependents, not the whole reduction.
Scalable: independent work runs in parallel without changes to scientific code.
Expandable: a new instrument reuses the framework rather than rebuilding it.
This specification is intentionally minimal. It defines the architectural spine that each instrument-specific pipeline should extend.
2. Scope#
This specification applies to the software architecture of instrument data reduction pipelines, including:
Scientific reduction primitives
Calibration product generation
Quicklook and quality-assurance reductions
Full science reductions
Archival reprocessing campaigns
Pipeline execution, scheduling, and monitoring
Intermediate product caching and persistence
This specification does not define:
A specific reduction algorithm for any instrument
A specific orchestration product
A data archive or file naming standard
A complete calibration plan for any instrument
Instrument teams are expected to extend this specification with instrument-specific primitives, calibration sequences, data models, and reduction flows.
3. Architectural Principle#
A reduction shall be expressed as a directed acyclic graph (DAG) of state-aware tasks rather than as a driver script.
Each task shall own one meaningful reduction step, report its own state, log its own parameters and results, and be independently cacheable. Orchestration concerns (state tracking, caching, retries, monitoring, scheduling, parallelism) shall be provided by a general-purpose workflow orchestrator rather than built per project.
The consequence is that routine operations become inspections of state the pipeline already holds: diagnosing a failed batch, re-running a subset of steps after a parameter change, and monitoring progress require no reconstruction from log files.
4. System Context#
flowchart LR
Astronomer[Astronomer / Pipeline Engineer]
Trigger[Manual / Event / Schedule Trigger]
Orchestrator[Workflow Orchestrator]
Flows[Flow Layer: CAL / Quicklook / Science]
Tasks[Task Layer: Observable Wrappers]
Core[Core Layer: Scientific Primitives]
Cache[Result Cache / Persisted Products]
Raw[Raw Frames + Calibration Data]
Products[Reduced Products + QA]
Dashboard[Dashboard / Run History / Logs]
Astronomer --> Trigger
Trigger --> Orchestrator
Orchestrator --> Flows
Flows --> Tasks
Tasks --> Core
Raw --> Tasks
Tasks --> Cache
Cache --> Tasks
Tasks --> Products
Orchestrator --> Dashboard
Astronomer --> Dashboard
Core --> Astronomer
The specific orchestrator may vary by project. The architecture requires that task state, parameters, logs, cached products, and failure context remain available regardless of the selected orchestrator.
5. Required Architectural Components#
A conforming DRP design shall include the following minimum components.
5.1 Layer Separation#
The pipeline shall be organized into three strictly separated layers.
Layer |
Contents |
Prohibited |
|---|---|---|
Core |
Scientific primitives: pure functions over standard array and astronomy types |
Any orchestration awareness |
Task |
Thin wrappers adding logging, caching, retry policy, and a readable name |
Reduction logic |
Flow |
DAG composition and parameter plumbing |
Reduction logic, direct hardware or archive access |
Imports shall cross these boundaries in one direction only. The core layer shall not import from the task layer, the flow layer, or the orchestrator. This boundary shall be enforced by an automated test so that it is a verified property of the codebase rather than a stated intention.
A core primitive shall be usable without modification in three contexts:
Inside a task during a pipeline run
Inside a notebook during interactive analysis
Inside a unit test during verification
This requirement is what makes an unexpected result investigable: the astronomer imports the identical function the pipeline ran, calls it on the identical inputs, and iterates on parameters interactively.
5.2 Task Wrappers#
A task wrapper shall add exactly four things:
Parameter logging at entry
Summary-statistic logging at exit
Cache configuration
Retry policy
A wrapper that exceeds roughly twenty lines shall be treated as a design smell: the excess is reduction logic that belongs in the core layer.
Not every function becomes a task. A function shall be wrapped as a task only if it performs I/O, is computationally substantial, is a meaningful unit of caching, or produces output worth inspecting.
Over-tasking adds scheduler overhead and dashboard clutter without observability gain. Under-tasking collapses the DAG into opaque blocks and forfeits selective re-execution.
5.3 Flow Scoping#
Each instrument’s flow layer shall be organized as a triad.
Flow |
Purpose |
Timing constraint |
|---|---|---|
CAL |
Build and cache master calibrations |
Real time as frames arrive, or on demand |
Quicklook |
Fast, fixed-parameter reduction with QA products |
Must keep pace with observing |
Science |
Full-quality reduction: optimal extraction, sky subtraction, flux calibration |
None |
The triad shall be joined by the cache: master calibrations produced by the CAL flow shall be consumed by the Quicklook and Science flows as cache hits rather than being rebuilt. A master calibration shall be computed once per calibration epoch and served to every downstream reduction that needs it.
Within an instrument, a detector channel or similar configuration variant that produces an identical DAG shape shall be a flow parameter, not a separate flow.
5.4 Caching and Selective Re-execution#
Caching shall be treated as the mechanism by which selective re-execution exists, not as a performance optimization layered onto a working pipeline.
Every cacheable task shall derive its cache key from the content of its inputs, so that “has this step already run on these inputs with these parameters” is a decidable question.
For the multi-megabyte arrays a reduction passes between steps, the cache key should be a content fingerprint (shape, dtype, and statistical moments) combined with all non-array parameters, rather than a hash of the full buffer. A fingerprint is orders of magnitude cheaper than a full hash and is robust to bit-level noise while remaining sensitive to any change that alters the data scientifically.
Where bit-exactness is required, such as differential testing of two implementations of the same primitive, full-content hashing shall be used instead.
Content-derived keys impose one obligation on the rest of the architecture: task inputs shall never be mutated. All inter-task data types shall be immutable by convention, and primitives shall return new objects rather than modifying arguments. Where an external library mutates in place, its core wrapper shall contain the mutation behind a copy-and-return form.
Immutability also provides failure isolation: concurrent per-frame chains share no mutable state, so one frame’s failure leaves its neighbors untouched.
5.5 Canonical Data Model#
The pipeline shall define a canonical internal data model and use it for every value that crosses a task boundary.
At minimum:
Two-dimensional frames shall travel as a standard container carrying data, uncertainty, mask, and header
Structural metadata shall travel as small frozen records, not as loose dictionaries
Adaptation between canonical types and an external library’s native types shall happen inside the core primitive, in one adapter module per library
An external library’s native type shall never appear in a task signature or as a node in the DAG. This is what allows primitives sourced from different community packages to compose under one DAG with uniform observability, caching, and failure semantics.
The orchestration layer shall be primitive-source-agnostic: a task wrapper does not know or care which library the function beneath it came from, only that it satisfies the core primitive contract.
5.6 Observability#
The pipeline’s operational surface shall expose, during and after any run:
The DAG with per-task state in real time
Every task’s parameters as logged at entry
Every task’s summary statistics as logged at exit
Per-task log streams, so a failure reads as one task’s contextualized traceback rather than a needle in an interleaved global log
Timing per task and per run
Cache-hit status per task
Cached intermediate products, retrievable from past runs and exportable on request (§5.10)
Exit logs shall report scientific meaning, not merely success. A completed task shall communicate what happened (slit counts, median widths, frame medians), not only that it succeeded.
Orchestration shall live at the flow level. Tasks shall not call tasks: a group of tasks wrapped inside a parent task renders as a single opaque node and loses the inner structure. Where nesting is genuinely required, it shall be a subflow, which preserves full structure.
5.7 Failure Handling#
The pipeline shall degrade gracefully. A failure in one frame’s reduction chain shall not fail the batch.
Required failure behavior:
The affected unit fails visibly, with its input path, exception, and retry history surfaced in its own task state
The remainder of the batch completes normally and remains trustworthy
The run reports completion with a documented per-unit failure rather than an unrecoverable batch-level error
The operator can re-run only the failed unit’s chain, with cached results from successful units served immediately
Retry policy shall encode the failure taxonomy:
Task class |
Retry policy |
Rationale |
|---|---|---|
I/O tasks (frame loading, result saving) |
Two to three retries with backoff |
Failures are frequently transient at an observatory: network filesystems, disk contention |
Computation tasks |
Zero retries |
A deterministic primitive that failed once on given inputs will fail identically again; surface it immediately with full context |
5.8 Concurrency Model#
Concurrency shall be a per-flow configuration parameter, not an implementation detail embedded in scientific code. Changing the execution model shall not require changes to any primitive, wrapper, or flow.
Guidance for selecting the model:
I/O-bound stages are well served by thread-based concurrency
CPU-bound stages shall use process-based execution. Threaded execution of CPU-bound Python does not merely fail to speed up, it can regress measurably against serial execution due to interpreter-lock contention
Batch shape shall be discovered by a visible fan-out task, so that the number of units processed appears in the DAG rather than hiding inside a Python loop
Within a unit, the chain is sequential because it is a data dependency; across units, chains are concurrent
When a threaded stage underperforms, thread oversubscription in numerical libraries and thread-safety violations shall both be ruled out explicitly before attributing the result to interpreter-lock contention.
5.9 Dual-Layer Primitives#
Performance shall not be purchased with accessibility.
Every primitive shall have a canonical pure-Python implementation that defines correct behavior. A performance-critical primitive may additionally carry a compiled implementation, selected automatically when present.
Requirements for the dual-layer model:
The canonical implementation registers itself as the default for a primitive name
The compiled implementation, if importable, overrides the registry entry for that name
Call sites are unchanged in either case; selection is internal to the core layer
An environment override shall force the canonical path for debugging, verification, or environments where the compiled extension is unavailable
Both implementations shall produce identical results within documented floating-point tolerances, enforced by differential tests in continuous integration that run every dual-layer primitive through both backends on identical inputs
Opting into the compiled path shall affect performance only. Nothing about correctness or usability shall depend on it.
5.10 Product Headers and Materialization#
Every product written to disk shall record its own processing history in its FITS header.
At minimum, the header shall record:
Pipeline version and run identifier
The ordered list of reduction steps applied, with the key parameters each was called with
The input frames and calibration products consumed
A structured, machine-readable convention (one keyword per applied step) is preferred over free-form HISTORY cards, so that a directory of products can be queried for what was done to them.
This is a secondary visibility layer, and the architecture shall not depend on it. The orchestrator’s task state and the content-derived cache keys of §5.4 remain the authoritative record of what has run. A pipeline shall never parse a product header to decide whether a step is needed, which step to run next, or whether a cached result is valid. The header answers “what was done to this file” for a human or an external tool holding the file alone, without access to the run.
Intermediate products live in the result cache. FITS serialization of an intermediate product shall happen only when a download or export is requested, not as a side effect of every step. Final science products are written as a matter of course.
The consequence is that the same provenance information exists in two independent places, arrived at by different means: the orchestrator holds it as run state, and each exported file carries it inline. Where the two disagree, the run state is correct and the discrepancy is a bug in header writing.
6. Task Contract#
All tasks shall follow a common structural contract.
6.1 Minimum Entry Log Fields#
task_name
input_fingerprints
parameters
cache_key
6.2 Minimum Exit Log Fields#
task_name
final_state
duration
cache_hit
summary_statistics
6.3 Minimum Failure Fields#
task_name
input_identifier
exception_type
exception_message
traceback
retry_count
6.4 Run Identifier Requirement#
Every run shall have a unique identifier that correlates task states, logs, parameters, cached products, and produced files.
A reduction shall not produce a data product that cannot be traced back to the run, the parameters, and the input frames that generated it.
7. Required State Models#
7.1 Task Lifecycle#
A task shall follow an explicit lifecycle.
stateDiagram-v2
[*] --> Pending
Pending --> Cached
Pending --> Running
Running --> Completed
Running --> Failed
Running --> Retrying
Retrying --> Running
Retrying --> Failed
Completed --> [*]
Cached --> [*]
Failed --> [*]
Minimum task states:
State |
Meaning |
|---|---|
|
Task is scheduled but has not started. |
|
Task is actively executing. |
|
Task was satisfied from a prior result with a matching cache key. |
|
Task failed and is being retried under its retry policy. |
|
Task completed successfully. |
|
Task exhausted its retry policy without completing. |
Instrument teams may add additional states, but they shall preserve these minimum lifecycle concepts.
7.2 Run Lifecycle#
A run shall report a state distinct from the states of its tasks. A run containing a documented per-unit failure that otherwise completed its batch shall not be reported identically to a run that failed as a whole.
8. Reproducibility Requirements#
The pipeline shall make a reduction reproducible from recorded state.
At minimum:
All top-level dependencies shall be pinned and the repository shall carry a lockfile
Reduction parameters shall be configuration data, not values embedded in code
Every task’s parameters shall be recorded as executed, not as defaulted
Intermediate products shall be cached and retrievable from past runs, and exported products shall carry their processing history in their headers (§5.10)
Cache keys shall be derived from input content, so that a cache hit means the inputs are scientifically identical to a previous run, not merely that a filename matched
A provenance layer above the DAG, capable of identifying and re-running the reductions that consumed a superseded calibration product, is a recommended extension. It shall be designed as an addition to this architecture, not a replacement for the caching model.
9. Testing Requirements#
The pipeline shall be testable without instrument hardware and without a full data set.
At minimum:
A static test shall fail if the core layer imports from the task layer, the flow layer, or the orchestrator
Core primitives shall be unit-testable as plain functions
Dual-layer primitives shall carry differential tests across both backends using full-content comparison
Flows shall be runnable against a small fixture data set
Pipelines shall be validated against raw instrument output, not only against pre-assembled data. Synthetic frames that are already assembled and already oriented conceal a real reduction stage: raw frames arrive with overscan regions, multi-amplifier structure, and an orientation that downstream primitives may not expect. Raw-image assembly and orientation are first-class reduction stages, and a pipeline validated only on synthetic data should expect additional integration work when first applied to real frames.
The manual path shall remain viable. Automation adds its own complexity and failure modes, and the mitigation is that every core primitive remains runnable by hand in a notebook.
10. Minimum Viable Implementation#
A practical minimum implementation should start with one vertical slice.
trigger
-> one flow
-> fan-out over input frames
-> per-frame chain of three or four tasks
-> cached master calibration from a CAL subflow
-> persisted products
-> dashboard
A recommended first implementation includes:
The three-layer directory structure and its import-boundary test
Three or four core primitives, at least one sourced from a community package through an adapter
The canonical data model for frames and structural metadata
The task-wrapper pattern with content-fingerprint caching
A CAL subflow producing one cached master calibration
A Quicklook flow consuming it
A batch run with one deliberately corrupt input, to verify failure isolation
This validates the architecture before expanding it to the full instrument.
11. Instrument-Specific Expansion Points#
Each instrument team shall extend this minimum specification with details specific to the instrument.
11.1 Primitive Inventory#
Define each primitive, its source, the canonical types it consumes and returns, and whether it carries a compiled backend.
Primitive |
Source |
Types in / out |
Dual-layer |
Notes |
|---|---|---|---|---|
Raw frame assembly |
Instrument-specific |
Raw HDU list / frame |
No |
Overscan, multi-amplifier, orientation |
Master bias combination |
Community package |
Frame list / frame |
Candidate |
Median combination over a bias stack |
Slit tracing |
Community package |
Frame / slit edge record |
No |
Wrapped through an array-level adapter |
Cosmic-ray rejection |
Community package |
Frame / frame |
Yes |
Dominates wall-clock time |
Where a primitive’s types are instrument-specific structural records, define them alongside this table. Where it wraps an external library, name the adapter module that converts to and from that library’s native types.
11.2 Calibration Structure#
Define calibration epochs, the products built per epoch, and their cache lifetimes.
11.3 Flow Catalog#
Define each flow using a consistent template:
Flow name:
Purpose:
Parameters:
Input discovery:
DAG shape:
Cached products consumed:
Products produced:
Concurrency model:
Timing requirement:
Failure behavior:
11.4 Quality Assurance Products#
Define the QA outputs each flow produces and the summary statistics each task logs at exit.
12. Design Rules#
The following rules should guide implementation decisions.
Rule |
Rationale |
|---|---|
Scientific code has no orchestration awareness. |
Keeps primitives runnable in a notebook, a test, and a pipeline. |
Task wrappers stay thin. |
Excess wrapper logic is reduction logic in the wrong layer. |
Tasks never call tasks. |
Nesting collapses the DAG into opaque nodes. |
Inter-task data is immutable. |
Content-derived cache keys require inputs that never change under them. |
Cache keys derive from input content. |
Makes selective re-execution decidable rather than assumed. |
Concurrency is a flow parameter. |
Lets the execution model change without touching scientific code. |
External library types stay inside primitives. |
Lets primitives from multiple sources compose under one DAG. |
Every primitive has a readable Python implementation. |
Compiled acceleration must not cost accessibility. |
Reduction parameters are configuration data. |
Enables review, versioning, and reproducibility. |
Product headers record provenance but never drive control flow. |
Gives a file inspectable history without making header parsing load-bearing. |
Failures are isolated and documented, not fatal to the batch. |
One bad frame should not invalidate a night. |
13. Anti-Patterns#
The following patterns should be avoided:
driver script with manual state tracking
hand-built caching and resume logic
reduction logic inside task wrappers
tasks wrapped inside tasks
in-place mutation of frames between steps
external library types in task signatures
cache keys derived from filenames or timestamps
pipeline control flow driven by parsing product headers
intermediate FITS written to disk as a side effect of every step
products written with no record of what produced them
diagnosis by grepping an interleaved global log
parameters recorded only as code defaults
one bad frame failing the whole batch
compiled-only primitives with no readable reference
thread-based parallelism for CPU-bound stages
validation only against synthetic pre-assembled data
These shortcuts may appear faster early in development, but they make the DRP harder to operate, diagnose, reproduce, and extend.
14. Notes on Scale#
The DAG-of-tasks abstraction is proven at survey scale on purpose-built middleware. This specification addresses the complementary case: obtaining the same properties at instrument scale, at an operational footprint proportionate to a team of a few engineers.
Two consequences follow for teams applying these guidelines:
The orchestration deployment should be a server process and a worker, not a platform. Where a heavier deployment is warranted by pipeline count rather than pipeline complexity, that is a separate decision.
Orchestration overhead per task is negligible against a multi-second reduction step and material against a microsecond array operation. This is why the task granularity rule of §5.2 exists: the architecture works because most functions are not tasks.
15. Summary#
This specification defines the minimum architecture needed for an observable, reproducible, selectively re-executable, and expandable observatory Data Reduction Pipeline.
The minimum architecture is not a large framework. It is a disciplined layering:
Pure scientific primitives below, thin observable task wrappers above, flows scoped per instrument as a CAL/Quicklook/Science triad joined by cross-flow caching, and a canonical data model that lets primitives from any source compose under one DAG.
Instrument-specific implementations should extend this specification by adding concrete primitives, calibration structures, data model records, flows, and quality assurance products.