Feat/trigger timing consistency #310

Merged
menzel merged 5 commits from feat/trigger-timing-consistency into main 2026-09-08 14:25:55 +02:00
Member

exp_time is the trigger period, not the exposure. DDG2 gated for exp_time − readout_time but the Eiger was sent the full exp_time as image_time_us, so it integrated ~180 µs past the falling edge of its own gate — 172.8 µs overhang measured on a scope. Four independent notions of "readout time" are now one: DDG2 derives the gap from the scan's readout_time floored by its configured value and exposes effective_readout_times(); the Eiger subtracts the same number. MIN_EXP_TIME is split out so validation behaviour is unchanged. 64c2fff also fixes a deviceConfig override that never worked — readout_time wasn't a named parameter of the subclasses, so the device server silently dropped it (same trap as prefix); a test now pins the signatures.
Both devices log their exposure window at on_stage; the test is that the Eiger's acquisition equals DDG2's pulse width.
Behaviour change: scans that don't set readout_time deliver 200 µs less exposure (0.5 % at 40 ms) — matters for anyone normalising against pre-2026-09-03 data.
Deployed at cSAXS on 2026-09-03 as 011d403/f450f29/934d8d2 and in production since; timing has been tested extensively at the beamline with no remaining inconsistencies found. Adds 26 tests; merged into current main: 101 failed / 509 passed / 15 skipped against a baseline of 101 failed / 483 passed (the 101 are pre-existing motor-controller failures from a local ophyd_devices skew).

exp_time is the trigger period, not the exposure. DDG2 gated for exp_time − readout_time but the Eiger was sent the full exp_time as image_time_us, so it integrated ~180 µs past the falling edge of its own gate — 172.8 µs overhang measured on a scope. Four independent notions of "readout time" are now one: DDG2 derives the gap from the scan's readout_time floored by its configured value and exposes effective_readout_times(); the Eiger subtracts the same number. MIN_EXP_TIME is split out so validation behaviour is unchanged. 64c2fff also fixes a deviceConfig override that never worked — readout_time wasn't a named parameter of the subclasses, so the device server silently dropped it (same trap as prefix); a test now pins the signatures. Both devices log their exposure window at on_stage; the test is that the Eiger's acquisition equals DDG2's pulse width. Behaviour change: scans that don't set readout_time deliver 200 µs less exposure (0.5 % at 40 ms) — matters for anyone normalising against pre-2026-09-03 data. Deployed at cSAXS on 2026-09-03 as 011d403/f450f29/934d8d2 and in production since; timing has been tested extensively at the beamline with no remaining inconsistencies found. Adds 26 tests; merged into current main: 101 failed / 509 passed / 15 skipped against a baseline of 101 failed / 483 passed (the 101 are pre-existing motor-controller failures from a local ophyd_devices skew).
menzel added 4 commits 2026-09-08 14:18:16 +02:00
feat(ddg2): make the detector-trigger readout time configurable
CI for csaxs_bec / test (push) Successful in 3m48s
bb39833d83
The gap between consecutive detector triggers is exp_time minus a readout
time that was a module constant fixed at 0.2 ms. Some detectors need more:
FalconcSAXS declares MIN_READOUT = 3 ms, fifteen times longer. Nothing
reconciled the two -- DDG2 does not know which detectors are in the scan,
and the falcon only validates its exposure time, never the gap -- so a
detector that cannot keep up silently dropped frames.

There was also no way to change it. on_stage recomputes the pulse width
from the constant on every scan, so a value set by hand from the client did
not survive to the next acquisition.

The readout times are now per-instance, settable two ways: a readout_times
key in deviceConfig for a per-deployment default, and set_readout_times()
via USER_ACCESS for a change between scans. Raising the value widens the gap
and shortens the exposure by the same amount; burst_period stays at exp_time,
so the frame rate is unaffected.

Channel pair 'ab' is the one multiplexed to the detectors and normally the
only one worth changing. A value that would exceed the exposure time is
rejected, and a non-default value is logged at on_stage so a scan taken with
a widened gap is recoverable from the logs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
scan_info's exp_time is the trigger PERIOD. DDG2 gates for exp_time minus a
readout time, but the Eiger was sent the full exp_time as image_time_us, so
the detector acquired 180 us past the falling edge of its own gate and
finished only 20 us before the next trigger -- that 20 us being JungfrauJoch's
internal board-readout allowance, and the entire margin available. Measured on
a scope as a 172.8 us overhang.

Four independent notions of "readout time" existed:

  500 us  EIGER*_READOUT_TIME_US   validation floor only, no effect on anything
  200 us  DDG2 DEFAULT_READOUT_TIMES["ab"]   sets the gate width
   20 us  JungfrauJoch deployment config     applied internally by JFJoch
    -     scan parameter readout_time        honoured by the Falcon, ignored by DDG2

They are now one. DDG2 derives the gap from the scan's readout_time, floored by
its configured value (scans default it to 0), and exposes effective_readout_times().
The Eiger subtracts the same number from image_time_us. The Falcon already used
the scan value, so it needs no change.

The Eiger's 500 us constant is split in two, because it was doing two jobs: a
MIN_EXP_TIME validation floor keeps today's behaviour exactly, while the new
EIGER_READOUT_TIME defaults to 2e-4 to match the delay generator. The _US suffix
on constants holding seconds is dropped in all three modules.

Both devices log the effective exposure window at on_stage, so a period/exposure
mismatch is visible in the logs rather than only on an oscilloscope.

NOTE: this shortens the delivered exposure by the readout time -- 200 us, i.e.
0.5% at 40 ms -- for scans that do not set readout_time. Users should be told
before this is deployed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
Readout is a property of the detector, not a beamline constant: a 9M has more
modules than a 1.5M, and the Falcon needs 3 ms against the delay generator's
200 us. The per-model constants therefore stay, with comments saying the
duplication is deliberate so nobody consolidates them again. They all hold 2e-4
today only because no measured per-model value exists yet.

Writing a test for the deviceConfig override exposed that it never worked. Both
subclasses passed readout_time to super() while also forwarding **kwargs, so
supplying it raised "got multiple values for keyword argument" -- and through
BEC it never even got that far, because readout_time was not a named parameter
of the subclass signature and the device server drops config keys it cannot see
(the same rule behind the recent prefix incident). Both subclasses now name it
with the model constant as default, and a test asserts the signatures keep it.

Also documents frame_time_us in DetectorSettings as required-but-ignored for the
Eiger, and warns that its 500 is microseconds while every other time in the
module is seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
docs(eiger): record that triggering emulates gating, and why
CI for csaxs_bec / test (push) Failing after 5s
CI for csaxs_bec / test (pull_request) Successful in 2m0s
cfc7271d40
The Eiger is triggered rather than gated for stability, and the pulse train is
shaped so its internal timer coincides with the gate -- it is meant to behave as
if gated, so that every detector in a scan integrates the same window. Nothing
in the code said so, which is why sending the full exp_time as image_time_us
looked reasonable and stayed wrong until a scope showed the 173 us overhang.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLnmUurqcNd1FiDY5M2uZr
menzel added 1 commit 2026-09-08 14:21:30 +02:00
Merge branch 'main' into feat/trigger-timing-consistency
CI for csaxs_bec / test (pull_request) Successful in 1m48s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 1m53s
e1a8f88164
menzel merged commit e1a8f88164 into main 2026-09-08 14:25:55 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bec/csaxs_bec#310