Skip to content

Rework StandardDetector to extend from StandardReadable, to follow a more ophyd-v1-like pattern for read signals for step scanning and config signals. - #1395

Open
jwlodek wants to merge 6 commits into
bluesky:mainfrom
jwlodek:rework-standard-det-to-use-child-config-describe-read
Open

Rework StandardDetector to extend from StandardReadable, to follow a more ophyd-v1-like pattern for read signals for step scanning and config signals.#1395
jwlodek wants to merge 6 commits into
bluesky:mainfrom
jwlodek:rework-standard-det-to-use-child-config-describe-read

Conversation

@jwlodek

@jwlodek jwlodek commented Aug 12, 2026

Copy link
Copy Markdown
Member

StandardDetector now inherits StandardReadable, and areaDetector IO classes
(ADBaseIO, NDStatsIO, NDROIStatNIO, NDROIIO) declare their own signal
formats using StandardReadableFormat annotations. This means detector plugins
and drivers follow the same pattern as any other StandardReadable device —
they self-describe which signals are configuration, which are hinted reads, and
which are uncached.

Motivation

Readables and detectors now follow the same pattern for signal data. Before this change,
StandardDetector had its own separate mechanism for configuration and reading,
while StandardReadable had add_readables() with format annotations. Now
both use the same StandardReadable machinery. This makes adding step-scan signal reading functionality
to a file-writing detector identical to how it is handled for just StandardReadable. A plugin like NDStatsIO is
just a StandardReadable that declares its signals, and adding it to a detector
with Format.CHILD is identical to composing any two StandardReadable devices.
Data logics are still the required approach for any file-based data.
Adding a data logic that is readable only is also still supported.

IO classes own their signal roles. Previously, the detector or a separate
PluginSignalDataLogic class had to know which plugin signals were
configuration vs. read vs. hinted. Now ADBaseIO annotates acquire_time,
model, etc. as Format.CONFIG_SIGNAL, and NDStatsIO annotates total,
mean_value as Format.HINTED_UNCACHED_SIGNAL — the knowledge lives where
the signals are defined, not in an external wiring class. Signals or other readables
can also be added outside of the constructor via det.add_readables, just as with
StandardReadable.

Closer to ophyd v1. In ophyd v1, a detector's Component declarations
determined what appeared in read() and read_configuration(), and plugins
composed naturally as sub-devices. StandardReadable already followed this pattern,
but StandardDetector did not, which caused confusion in some cases during transitions.
This change restores that composability:
adding a plugin to a detector automatically pulls in its read and config
signals, just as adding a component did in v1. Users migrating from ophyd v1
will find the mental model familiar.

Automatic wait_for_plugins management. AreaDetector._update_prepare_context
now uses has_child_readables(ignore_filter=[self.driver]) to detect whether
any plugin has read signals that need to complete before the detector reads.
During step scans (number_of_events == 1), it sets wait_for_plugins = True
automatically. Fly scans skip this since they stream data asynchronously. An explicit
PluginSignalDataLogic (which was a somewhat verbose solution) is no longer required.

  • Add helper function to standard readable that checks if there are any registered child readables
  • Make standard detector extend from standard readable. Allow for pulling read/config/describe from children automatically
  • Update EPICS areaDetector classes to use standard readable child components for config/read
  • Update implementations to move config_sigs -> deadtime_sigs for trigger logics

Comment on lines +180 to +184
def __init__(self, prefix: str = "", with_pvi: bool = False, name: str = ""):
super().__init__(prefix, with_pvi, name)
self.add_readables(
[self.min_x, self.min_y, self.size_x, self.size_y], Format.CONFIG_SIGNAL
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why add the readables in __init__ for these driver IO classes? Can't you do it the type annotation way like in ADBaseIO?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I had more signals with formats set, and there were a lot of Format.* repeated, so this was more concise. I removed most and I think I can go back to using the declarative style.

Comment on lines +63 to +69
async def _update_prepare_context(self, trigger_info: TriggerInfo) -> None:
# Step scans (trigger+read) need plugins to finish before reading scalars
if trigger_info.number_of_events == 1 and self.has_child_readables(
ignore_filter=[self.driver]
):
await self.driver.wait_for_plugins.set(True)
await super()._update_prepare_context(trigger_info)

@thopkins32 thopkins32 Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the assumption that every AreaDetector wants WaitForPlugins = true if they have child readables. I don't really want to have to subclass AreaDetector to turn this off. I have step scans where I want more custom waiting than a generic wait for all plugins.

For example, a very long plugin chain that is only for display purposes, I don't want displays to slow down my scans. WaitForPlugins is too coarse for this case.

The data logic abstraction allows you to add this without subclassing. Is the same thing possible to achieve with a data logic? Where the IO classes are still StandardReadable (that part I like a lot).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants