13. Preserve Hardware State in Implicit Step-Scan Trigger#
Date: 2026-03-06
Status#
Accepted
Context#
When a StandardDetector has not been explicitly prepared with a TriggerInfo before the first call
to trigger() (e.g. in a bp.count or bps.trigger_and_read without a preceding bps.prepare),
StandardDetector.trigger() performs an implicit prepare using a default TriggerInfo().
Before this decision the implicit prepare always used TriggerInfo() — a bare default with
collections_per_event=1 and number_of_events=1. For EPICS areaDetector (AD) drivers this meant
that num_images was unconditionally reset to 1 on the driver before every step-scan point,
even if a preceding fly scan had left num_images set to a larger value.
This had two undesirable consequences:
Surprising behaviour after manual setup. If an operator sets
acquire_time, then runs acountplan (which callstrigger()without aprepare()), then the detector will not change it. However if they setnum_imagesthen it will be overridden with 1, discarding the operator’s hardware configuration without warning.Inconsistency with ophyd-sync. ophyd-sync devices do not alter detector hardware state that the user did not explicitly request changing. ophyd-async should be a drop-in replacement.
Decision#
Opt-in default_trigger_info() on DetectorTriggerLogic#
Add an optional default_trigger_info(self) -> TriggerInfo method to
DetectorTriggerLogic. If it doesn’t exist then keep the existing behaviour of using TriggerInfo()
trigger_info_from_num_images() free function for AD detectors#
A free async helper function is provided in ophyd_async.epics.adcore:
async def trigger_info_from_num_images(driver: ADBaseIO) -> TriggerInfo:
num = await driver.num_images.get_value()
return TriggerInfo(collections_per_event=max(1, num))
All EPICS areaDetector TriggerLogic subclasses implement default_trigger_info by delegating
to this function, reading back the current num_images value from the driver and returning it
as collections_per_event. This preserves the hardware state rather than resetting it.
Opt-in via environment variable#
Because changing the default behaviour of trigger() is a breaking change for existing
deployments, the new behaviour is opt-in. Set the environment variable
OPHYD_ASYNC_PRESERVE_DETECTOR_STATE=YES.
When the variable is not set (or set to any value other than YES), trigger() without a prior
prepare() falls back to the original TriggerInfo() default, preserving backward compatibility.
The env-var approach was chosen to minimise the code change while still exposing the new behaviour
as a supported path — default_trigger_info() is placed on the class that creates the
TriggerInfo rather than on the detector itself, keeping the logic close to where it belongs.
Consequences#
Step scans no longer reset
num_imageson AD detectors when no explicitprepare()is called andOPHYD_ASYNC_PRESERVE_DETECTOR_STATE=YESis set, making ophyd-async a closer drop-in replacement for ophyd-sync.A fly scan that leaves
num_images=500on the driver will cause a subsequent implicit step scan (with the env var set) to capture 500 frames per trigger point instead of 1. This is the correct behaviour (honour the hardware state) but may surprise users who expected the scan to reset the detector. Plans that care aboutnum_imagesshould callbps.prepare(det, TriggerInfo(...))explicitly.Without the environment variable the behaviour is unchanged from before.