16. Replace AreaDetector writer parameters with ADWriterFactory#
Date: 2026-05-15
Status#
Accepted
Context#
ADR 0012 introduced the composition-based AreaDetector baseclass. Its
__init__ accepted three writer-related constructor parameters side by side
with every other parameter:
AreaDetector(
driver,
prefix,
path_provider=path_provider,
writer_type=NDFileHDF5IO,
writer_suffix="HDF1:",
...
)
Several problems arose as the codebase matured:
Three parameters always travel together.
path_provider,writer_type, andwriter_suffixare only meaningful as a unit; passing them individually obscures the relationship and requires callers to remember which combinations are valid.No first-class support for multiple writers. The original API exposed a single writer slot, so attaching a second HDF writer for a ROI plugin required calling
add_detector_logics()after construction with low-level arguments that duplicated information already present at call time.No way to override array shape or data-type signals. The writer logic hard-coded
driver.array_size_{x,y,z},driver.data_type, anddriver.color_modeas the source of NDArray metadata. This was wrong for ROI plugins (which have their ownsize_x/size_ysignals) and for processing plugins that remap the data type or colour mode.Deferred driver construction. Detector subclasses (e.g.
AravisDetector) create their driver internally; the driver signals therefore do not exist at the point where the caller constructsADWriterFactory. Any eagerNDArrayDescription(data_type_signal=driver.data_type, ...)call would need the driver to already exist—which it does not.
Decision#
Replace the three writer parameters with a single *writer_factories varargs
of ADWriterFactory instances:
AreaDetector(
driver,
prefix,
ADWriterFactory.hdf(path_provider),
...
)
ADWriterFactory#
A @dataclass (generic over the plugin IO type) with fields:
Field |
Type |
Purpose |
|---|---|---|
|
|
Plugin class to instantiate |
|
|
PV suffix appended to |
|
|
Attribute name on the detector ( |
|
|
Suffix appended to the datakey name in stream resources |
|
|
Override for array shape/type metadata |
|
callable |
Builds the |
Three static constructors—hdf(), jpeg(), and tiff()—provide sensible
defaults. They are named after the file format they produce, and their
writer_name defaults to the same string ("hdf", "jpeg", "tiff"),
so the writer is automatically stored at det.hdf etc.
__call__(prefix, driver, plugins) runs at AreaDetector.__init__ time, when
the driver already exists, and returns (writer_plugin, DetectorDataLogic).
NDArrayDescription and the callable override#
NDArrayDescription bundles the three signals that describe an NDArray frame:
@dataclass
class NDArrayDescription:
shape_signals: Sequence[SignalR[int]]
data_type_signal: SignalR[ADBaseDataType]
color_mode_signal: SignalR[ADBaseColorMode]
When array_description is None, __call__ auto-builds it from
driver.array_size_{z,y,x}, driver.data_type, and driver.color_mode.
When a caller needs signals from a different source (an ROI plugin, a processing plugin) and the driver is only created inside the detector subclass, a callable is used:
ADWriterFactory.hdf(
path_provider,
writer_name="hdf1",
datakey_suffix="-roi1",
array_description=lambda driver: NDArrayDescription(
shape_signals=(roi1.size_y, roi1.size_x),
data_type_signal=driver.data_type, # driver available here
color_mode_signal=driver.color_mode,
),
)
The callable receives the fully-constructed driver at __call__ time, which is
the first moment all driver signals exist. This design:
keeps
data_type_signalandcolor_mode_signalmandatory onNDArrayDescription(no silent defaults, no silent fall-through);allows any signal to be overridden, not just the shape—so a plugin that remaps the data type or colour mode can supply its own signal;
avoids adding a new
Callabletype toNDArrayDescriptionitself, keeping that dataclass a plain value object.
Multiple writers#
Passing multiple factories gives each writer a distinct name and datakey suffix:
det = adaravis.AravisDetector(
"PREFIX:",
ADWriterFactory.hdf(path_provider, writer_name="hdf1", datakey_suffix="-roi1",
array_description=lambda driver: NDArrayDescription(...)),
ADWriterFactory.hdf(path_provider, writer_name="hdf2", datakey_suffix="-roi2",
array_description=lambda driver: NDArrayDescription(...)),
plugins={"roi1": roi1, "roi2": roi2},
)
det.hdf1 # NDFileHDF5IO for ROI 1
det.hdf2 # NDFileHDF5IO for ROI 2
No post-construction add_detector_logics() call is required.
prefix placement and guard#
prefix is placed before *writer_factories (as a positional parameter with a
default of None) so that callers using the standard single-writer pattern can
pass it positionally. A guard raises ValueError if factories are supplied but
prefix is None:
if writer_factories and prefix is None:
raise ValueError("prefix is required when writer_factories are given")
Consequences#
AreaDetector.__init__and all seven bundled subclasses are updated; no compatibility shim is provided (the library is pre-1.0).writer_namemust be unique across factories passed to the same detector; duplicate names raise anAttributeErrorat construction time.Callers that previously passed
writer_type=Noneto suppress file writing now simply omit all factory arguments.ADHDFDataLogicandADMultipartDataLogicexpose theirNDArrayDescriptionasarray_descriptionto avoid ambiguity with the similarly-named Python concept.