Runtime#
Composes the pipeline over the data store: the acquisition gateway mints scan identity at the source boundary, the processor drives the module graph per scan, the orchestrator manages run lifecycle in the store registry, and the post-processor runs after-the-fact modules into an existing store.
Multi-threaded pipeline orchestration.
Coordinates downloader and processor threads with queue-based inter-thread communication. Manages lifecycle, monitoring, and graceful shutdown. Owns the single run lifecycle in the store registry and refuses to run against an uninitialized root.
- class adapt.runtime.orchestrator.PipelineOrchestrator(config, max_queue_size=None, close_repository_on_stop=True)#
Bases:
objectManages the multi-threaded radar processing pipeline.
This is the main entry point for running
adapt. It coordinates two worker threads (downloader, processor) using queues for inter-thread communication. The orchestrator handles startup, monitoring, and graceful shutdown of the processing pipeline.Pipeline Architecture:
Downloader Thread: Discovers and downloads NEXRAD Level-II files from AWS in realtime or historical mode.
Processor Thread: Processes downloaded files through the full scientific pipeline: - Load and regrid Level-II data to Cartesian grid - Segment cells using configurable threshold method - Compute motion projections (frame 2 and beyond) - Extract cell-level statistics - Persist segmentation to NetCDF and statistics to SQLite - Commit every artifact and table into the store
Modes:
Realtime: Polls for latest files within a rolling time window (e.g., “files from last 60 minutes”). Useful for operational monitoring.
Historical: Downloads all files within a fixed time range (start_time to end_time). Useful for batch reprocessing and research.
Queue Management:
Inter-thread queues have configurable size limits (default 100 items). Larger queues enable higher throughput but use more memory. Smaller queues provide backpressure (slow down downloader if processor falls behind).
Resumability:
Completed scans are skipped through the collection catalog (
scan_is_complete), so a restarted run resumes without reprocessing.Logging:
All output goes to both console and log file (logs/{radar}_pipeline.log). Log level controlled via config: “DEBUG”, “INFO”, “WARNING”, “ERROR”.
Example usage:
from adapt.runtime.orchestrator import PipelineOrchestrator config = { "mode": "realtime", "downloader": {"radar": "KDIX", ...}, "output_dirs": {...}, ... } orch = PipelineOrchestrator(config) orch.start(max_runtime=60) # Run for 60 minutes then stop
- Parameters:
config (InternalConfig)
max_queue_size (int | None)
close_repository_on_stop (bool)
- close_store()#
Close the store’s collection handles if open.
- Return type:
None
- request_stop()#
Ask the monitoring loop to exit so the orchestrator’s own thread finalizes.
Called from the CLI on SIGINT/SIGTERM. Distinct from
stop()so signalling the loop never tripsstop()’s “already finalized” guard — finalize + summary then run on the joined (non-daemon) orchestrator thread, not a daemon the process kills on exit.- Return type:
None
- start(max_runtime=None)#
Start the pipeline and run until completion or user interrupt.
This is a blocking call that starts the downloader and processor threads, then enters a monitoring loop. The loop logs status every 30 seconds and handles mode-specific exit conditions.
Realtime Mode: Runs until you press Ctrl+C or max_runtime is exceeded.
Historical Mode: Automatically exits when all files within the start_time/end_time range are queued, processed, and plotted.
All output (console + file) logged to logs/{radar}_pipeline.log at the level specified in config[“logging”][“level”].
- Parameters:
max_runtime (
int|None) – Maximum runtime in minutes (realtime mode only). If None, runs until KeyboardInterrupt (Ctrl+C). Ignored in historical mode (uses file completion instead).- Raises:
KeyboardInterrupt – User pressed Ctrl+C. Pipeline stops gracefully.
Notes
To stop the pipeline gracefully, press Ctrl+C. Do NOT force-kill. The stop() method is called automatically to close threads and save results.
Examples
Run for 60 minutes in realtime mode:
orch = PipelineOrchestrator(config) orch.start(max_runtime=60)
Run historical mode until all files processed:
orch = PipelineOrchestrator(config) # mode="historical" in config orch.start() # Runs until all files between start_time/end_time processed
- stop()#
Stop the pipeline gracefully and finalize all results.
Called automatically when start() exits (either by user interrupt or mode-specific completion). Safe to call multiple times.
Operations:
Signals all worker threads to stop
Waits for each thread to finish (bounded grace periods)
Finalizes the run record in the store registry
Closes store connections and logs the runtime
Notes
This method should complete within ~20 seconds. If a thread does not respond to the stop signal within its timeout, a warning is logged but the shutdown continues (graceful degradation).
Files processed before the final save() will have their statistics in the SQLite database. The database is safe to query even while the pipeline is running (uses WAL mode).
Radar data processor thread.
Reads acquired-scan messages from the source queue, resolves the raw object through the store, and delegates all scientific processing to GraphExecutors built at startup:
_single_executor: ingest + detection (runs every file)_multi_executor: projection + analysis + tracking (runs when 2-frame pair is ready)
Responsibilities of this class (orchestration only): - Queue management: pop acquired-scan messages, mark task done - Completed-scan skip via the collection catalog (scan_is_complete) - Frame pairing: accumulate segmented history, validate time gap - Context assembly: inject dataset_history before calling multi-executor - Persistence handoff: route module-declared outputs through the StoreOutputRouter - Stop/start lifecycle
- class adapt.runtime.processor.RadarProcessor(input_queue, config, *, collection, registry, run_id, history, name='RadarProcessor', observability=None, root_trace_id='', reporter=None)#
Bases:
ThreadWorker thread that processes NEXRAD files through two execution graphs.
Receives file paths from the downloader queue. For each file:
Runs the single-frame graph (ingest → detection) via
_single_executor.Accumulates segmented datasets in a rolling history.
When a valid 2-frame pair is ready, runs the multi-frame graph (projection → analysis → tracking) via
_multi_executor, passing the frame history in context.
Both executors enforce input/output contracts at every DAG edge via
GraphExecutor. The processor itself performs no validation.Example usage (called by PipelineOrchestrator):
processor = RadarProcessor( input_queue=downloader_queue, config=config, collection=collection, registry=registry, run_id=run_id, history=history, ) processor.start() ... processor.stop()
- Parameters:
input_queue (Queue)
config (InternalConfig)
collection (Collection)
registry (StoreRegistry)
run_id (str)
history (StoreExecutionHistory)
name (str)
observability (Observability | None)
root_trace_id (str)
- current_activity()#
Short ‘what am I doing now’ for the live status line; None when idle.
- Return type:
str|None
- process_file(filepath)#
Process a NEXRAD file with frame-pairing orchestration.
- Phase 1 — pipeline_phase=1 modules (every file):
Per-file executor. Contract-validated by GraphExecutor.
- Frame pairing:
Accumulates segmented datasets. Waits until 2 frames are ready.
- Phase 2 — pipeline_phase=2 modules (when pair is ready):
Injects dataset_history into context. Per-pair executor. Contract-validated by GraphExecutor.
- Phase 3 — pipeline_phase=3 modules (after persistence, when pair ran):
Post-persistence executor. Context: run_id, scan_time, plus the in-memory grid on demand. No-op if no phase-3 modules are registered.
- Returns:
True if processed or deferred (waiting for pair), False on error.
- Return type:
bool
- run()#
Main processor loop (runs in thread).
Binds this thread’s correlation context once — contextvars do not cross threads, so the root trace id is handed in from the orchestrator and re-bound here so every log/span emitted on this thread shares one trace.
- stop()#
Signal the processor to stop after the current file finishes.
- stopped()#
True if stop() has been called or a ContractViolation forced stop.
- Return type:
bool
Acquisition gateway: raw scans become cataloged store objects at the boundary.
The runtime builds one StoreAcquirer per run and injects it into the
ingress source — acquisition modules never import persistence. Acquisition is
idempotent per source_uri within a collection: bytes already in the store
are reused (never rewritten), and the scan is registered for the acquiring
run. The returned message is the processor queue contract:
{artifact_id, scan_id, scan_time, queued_at}.
- class adapt.runtime.acquire.StoreAcquirer(collection, run_id)#
Bases:
objectCommit raw scans into one collection’s store, once, at the source boundary.
- Parameters:
collection (Collection)
run_id (str)
- acquire_existing(source_uri, *, scan_time)#
Register this run against an already-stored source (skip-if-downloaded).
- Return type:
dict- Parameters:
source_uri (str)
scan_time (datetime | None)
- acquire_file(path, *, source_uri, scan_time)#
Commit a local raw file (or reuse the cataloged copy) and register its scan.
- Return type:
dict- Parameters:
path (str | Path)
source_uri (str)
scan_time (datetime | None)
- is_acquired(source_uri)#
True when this source’s bytes are already objects in the store.
- Return type:
bool- Parameters:
source_uri (str)
Out-of-band store enrichment by composing the live pipeline’s infra.
PostProcessor enriches an already-processed run with extension tables
(e.g. lightning stats), without touching the proven RadarProcessor. It is
an after-the-fact module execution: outputs attach to the ORIGINAL run_id, and
a re-run replaces the module’s own tables (never anyone else’s).
It reuses — by composition, not inheritance — the same components the pipeline
uses: the module registry, GraphBuilder/GraphExecutor for
dependency resolution, resolve_module_configs for config, and the
StoreOutputRouter for persistence (whose frozen-schema writer enforces
module table ownership). Post-process modules declare
pipeline_phase = POSTPROCESS_PHASE and are selected exclusively here; the
live pipeline never loads them.
- class adapt.runtime.postprocessor.PostProcessor(collection, registry, run_id, config, extensions=None)#
Bases:
objectRun post-process (phase
POSTPROCESS_PHASE) modules over a stored run.- Parameters:
collection (Collection)
registry (StoreRegistry)
run_id (str)
config (InternalConfig)
extensions (list[str] | None)
- run(modules)#
Resolve, execute, and persist the requested post-process modules.
- Parameters:
modules (
list[str]) – Names of post-process modules to run (from--moduleor thepostprocess.modulesconfig section).- Return type:
dict