Skip to content

fix: stop live mode + pump Qt event loop during run_experiment#9

Draft
hinderling wants to merge 3 commits into
pertzlab:mainfrom
hinderling:fix/controller-qt-pump-and-stop-sequence
Draft

fix: stop live mode + pump Qt event loop during run_experiment#9
hinderling wants to merge 3 commits into
pertzlab:mainfrom
hinderling:fix/controller-qt-pump-and-stop-sequence

Conversation

@hinderling
Copy link
Copy Markdown
Collaborator

Summary

Two related fixes inside Controller._run_mda_with_events:

  1. Auto-stop continuous sequence acquisition before MDA. If live mode is still running when the MDA's first snapImage fires, napari-micromanager's _core_link._image_snapped handler reads the snap buffer with getImage() before the engine can, and the engine raises RuntimeError: Camera image buffer read failed.. Stopping the sequence unconditionally before MDA starts removes that contention without requiring the user to remember to press Stop Live.

  2. Pump Qt events instead of plain time.sleep / thread.join. _run_mda_with_events runs on the main thread — the same thread napari paints from. The backpressure throttle (time.sleep(0.1) while the MDA worker drains the queue) and the trailing mda_thread.join() both block the Qt event loop, so napari freezes for the entire run and any ensure_main_thread-queued callbacks (notably napari-micromanager's own per-frame preview update) pile up until the cell exits. Two helpers, _pump_qt_and_sleep and _qt_join, call QCoreApplication.processEvents() between waits so napari stays interactive throughout. Both fall back to plain blocking if Qt isn't loaded, so non-GUI test runs are unaffected.

Why

With the two fixes together: ctrl.run_experiment(events, ...) runs end-to-end without the Camera image buffer read failed race, napari stays responsive, and napari-mm's existing preview layer updates live as frames arrive — no controller-side viewer wrapper needed.

Verification

Tested on the Moench rig against random_stim_per_cell_circle.ipynb:

  • Previously the run failed on the first MDA event with Camera image buffer read failed. whenever the user had pressed Live during the focus-check cells.
  • Previously napari froze for the duration of the run, with preview-layer updates flushing only after the cell exited (or was interrupted).
  • Both behaviours now gone; the run progresses cleanly and the preview layer in napari refreshes per acquired frame.

Test plan

  • CI passes — change is purely additive (two helper methods, two replaced waits, one extra stop-sequence call); no public-API change.
  • On a real rig: confirm ctrl.run_experiment runs to completion with napari-micromanager open and live-mode either on or off at the start.
  • Confirm napari stays responsive during a multi-FOV run (drag/zoom the viewer mid-acquisition).

hinderling and others added 3 commits May 15, 2026 13:36
Two related fixes inside _run_mda_with_events:

1. Auto-stop continuous sequence acquisition before MDA.
   If live mode is still running when the MDA's first snapImage fires,
   napari-micromanager's _core_link._image_snapped handler reads the
   snap buffer with getImage() before the engine can, and the engine
   raises "Camera image buffer read failed". Stopping the sequence
   unconditionally before MDA starts removes that contention.

2. Pump Qt events instead of plain time.sleep / thread.join.
   _run_mda_with_events runs on the main thread, the same thread napari
   paints from. The backpressure throttle (time.sleep(0.1)) and the
   trailing mda_thread.join() block the Qt event loop, so napari freezes
   for the duration of the run and any ensure_main_thread-queued
   callbacks accumulate until the cell exits.

   Replace with _pump_qt_and_sleep / _qt_join helpers that call
   QCoreApplication.processEvents() between waits. Both fall back to
   plain blocking if Qt isn't loaded, so headless/test runs are
   unaffected.
The previous try/except swallowed three distinct failure modes —
missing ``mmc`` attribute, RPC failure on proxy microscopes, and real
core/hardware errors from ``isSequenceRunning`` or
``stopSequenceAcquisition`` — all silently. Surface the latter two so
genuine failures aren't hidden behind a pre-emptive cleanup.
Controller._build_stim_slm (main thread) calls Analyzer.get_stim_mask,
which blocks on FrameDispenser.wait_for_frame -- a plain
threading.Condition.wait() with no Qt awareness. The actual stim-mask
compute is already off-main (it runs in the Analyzer's
ThreadPoolExecutor alongside cellpose / DINOv3 / tracking), but the
main thread's wait for that result still freezes the Qt event loop, so
napari hangs for the GPU's worth of seconds on every stim frame.

Slice the wait into 50 ms chunks and call
QCoreApplication.processEvents() between attempts, preserving the
caller-supplied total timeout. Falls back to a plain wait_for_frame
if Qt isn't loaded, so headless / test runs keep their original
behaviour.
@hinderling hinderling marked this pull request as draft May 15, 2026 12:56
@hinderling
Copy link
Copy Markdown
Collaborator Author

resolved with #10 (running acquisition in separate thread)

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.

1 participant