ADR 0009: Generate the per-instrument stream registry at development time#
Status: accepted
Deciders: Simon
Date: 2026-07-22
This ADR is retroactive: it records a decision already implemented, written up after the fact.
Context#
NeXus-rich instruments (bifrost, dream, estia, loki, nmx, odin, tbl) each carry O(100) f144
log streams — motion, sample environment, choppers. Every one needs its NeXus path, Kafka
topic, source, and units recorded as an F144Stream in Instrument.streams
(src/ess/livedata/config/stream.py) before the corresponding workflow can be wired up.
That data already exists, authoritatively, as group attributes in the instrument’s NeXus
geometry file — the same file the filewriter and essreduce consume.
Both the backend services and the dashboard import an instrument’s config.instruments.<inst>
package at startup to build Instrument. The dashboard in particular has no file-access path
at that point: it runs wherever a user’s browser can reach it, not necessarily co-located with
a NeXus geometry file or a Pooch cache.
Decision#
Extract the f144 stream declarations from a NeXus geometry file offline, via the CLI in
nexus_helpers.py (python -m ess.livedata.nexus_helpers <geometry.nxs> --generate), and
check the generated config/instruments/<inst>/streams_parsed.py into git as an ordinary
Python module. The instrument’s hand-edited specs.py imports the generated
PARSED_STREAMS dict and composes the final registry via name_streams, supplying renames
and any hand-written synthetic entries.
Instruments with few streams (today: dummy) skip codegen entirely and hand-write the
dict[str, F144Stream] literal.
Alternatives considered#
Option |
Notes |
|---|---|
Codegen from NeXus at development time, checked in (chosen) |
Both backend and dashboard start from a plain importable module — no file I/O, no network, no cache-state dependency at import time. The generated diff is reviewable like any other code change against the previous geometry. |
Parse the NeXus geometry file at process startup |
The dashboard has no guaranteed file-access path to the geometry file; it would need a |
Hand-maintain the registry (YAML or Python) for every instrument |
O(100) entries per instrument, sourced from filewriter attributes nobody wants to retype. Selectively already done for |
Key design choices#
Generated output is a plain Python module, not a data file#
generate_streams_parsed_module emits a .py file with an F144Stream dict literal rather
than YAML or JSON. It imports and type-checks like any other module, needs no bespoke parser
in specs.py, and diffs the same way as hand-written code in review.
The generated dict is keyed by NeXus path, not instrument-facing name#
streams_parsed.py only extracts what the geometry file states directly; assigning
instrument-facing names (auto-suggestion, collision handling, device detection, renames) is
name_streams’s job in specs.py. This keeps the generator a thin, deterministic extraction
step and puts every naming decision in one reviewable place per instrument.
Consequences#
streams_parsed.pyis regenerated by rerunning the CLI against an updated geometry file and committing the diff; nothing currently detects staleness against the latest geometry automatically. An earlier drift test was dropped because it depended on a geometry file not available in CI’s Pooch cache, so it skipped in CI and gave false confidence rather than catching the realistic failure mode. A real check needs a stable geometry-file input pinned in Pooch first.Streamis a frozen dataclass that round-trips through any serialisation layer, so a future move to runtime or dynamically-distributed stream data would only change who populatesInstrument.streams, not the shape consumers see.