Skip to content

Scan Header Ingest and Tomography Handling - #3

Open
RamyaGuru wants to merge 33 commits into
mainfrom
feat/scan-header-tomo
Open

Scan Header Ingest and Tomography Handling#3
RamyaGuru wants to merge 33 commits into
mainfrom
feat/scan-header-tomo

Conversation

@RamyaGuru

Copy link
Copy Markdown
Collaborator

Streaming tomography: live headers, dynamic geometry, flush-on-completion, double-buffering

Adds multi-projection tomography to the Holoscan ptycho pipeline, driven by a live
scan-geometry header, with reconstruction that keeps up with a continuous stream.
Six commits (dcdcbda761fd35) on top of main (a15ab2d).

What's in it

  • PR0 — unwire PublishToCloudOp (class kept, removed from the live graph; the disk save already lives in SinkAndPublishOp).
  • PR1 — flush-on-completion + a skip-if-clean safety flush at scan start; all state-mutating flushes deferred/race-safe.
  • PR2HeaderRxOp (dedicated ZMQ SUB) sets scan geometry live; grid params removed from config; GPU buffers allocated once at max capacity (never realloced); mid-reconstruction preemption via a quiescence handshake (finish iteration → save → flush → reconfigure).
  • PR3 — tomography: one arm wraps num_projections back-to-back projections segmented by frame count; per-projection reconstruction + STXM files ({series}_projNN[_recon].h5); each projection early-stops when its frames are in.
  • PR4 — double-buffering (projection ping-pong): the accumulator fills projection N+1 while the recon finalizes N, so a free-running detector is never backpressured across the finalize window; clean backpressure fallback if the recon is lapped.

Testing

All validated in the ptycho-holoscan:ptyrex container on the A400, driven by the daqsim simulator (scan 409907, 32×32/projection).

PR Test Result
PR0 STXM + ptycho scans with PublishToCloudOp unwired ✅ identical output; pipeline composes & runs
PR1 Single scan → runs to total_iterations, idles, recon_complete
PR1 Back-to-back scan → start safety-flush fires; no double-flush
PR2 Launch with grid params removed from config ✅ buffers at capacity, geometry configured
PR2 Header before data → reconfigure + full scan ✅ full handshake, all 1024 valid
PR2 Header mid-reconstruction (preemption) ✅ partial saved before flush/reconfigure
PR2 Recovery after preemption ✅ fresh scan reconstructs to completion
PR2 Oversized grid (64×64 > capacity) ✅ rejected pre-staging, pipeline alive
PR3 2-projection tomography ✅ both projections reconstruct + save (STXM + recon), exact 1024 counts, correct completion signals
PR4 Regression (2 proj, double-buffered) ✅ exact 1024/proj, same iteration budget as single-buffer
PR4 Overlap (stretched finalize) ✅ accumulator fills next projection during finalize; no backpressure
PR4 Lapping fallback (3 proj, slow recon) ✅ clean backpressure + recovery; no drops, no deadlock

RamyaGuru and others added 6 commits July 2, 2026 12:46
PublishToCloudOp consolidated per-batch temp HDF files into a final file,
but SinkAndPublishOp.write_scan_file now does the single end-of-scan write
directly to the publish folder (commit 2d089e3). The temp files it consumed
are no longer produced, so it no-ops on every trigger.

Rather than delete it (it may be useful for a future external/temp-file
workflow), this just removes it from the live pipeline. That still simplifies
the control path for the upcoming flush/header work: ControlOp no longer needs
an output port or a processing_end branch.

- Keep PublishToCloudOp class in publish.py, documented as not-currently-wired
- pipeline.py: drop its construction, import, and both control flows
- Stop SinkAndPublishOp emitting processing_end (+ drop its output port)
- ControlOp: drop output port and processing_end forwarding

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Foundational plumbing for the header/tomography work. No behavior change for
existing single-projection scans (flush still happens at the next start).

- PtychoReconstructionOp: emit a one-shot "recon_complete" on a new "complete"
  output port when the final iteration is reached (reuses the existing is_last
  predicate, S9); guard + reset via _completed in flush().
- PtychoAccumulatorOp.flush: zero the GPU buffers INSIDE ptycho_state["lock"]
  so recon can't read half-zeroed buffers (R-3); no-op when nothing has been
  accumulated since the last flush (R-2 _dirty guard) so the unconditional
  start-flush is free when clean.
- ControlOp: log "recon_complete" only — NO flush here, so a completed
  single-projection scan keeps its result until the next start/header. The
  flush-on-completion / per-projection semantics arrive with PR3.
- pipeline.py: wire ptycho_recon "complete" -> control_op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… STXM safety

Builds on the flush plumbing so completion drives the flush (Task 3) and every
flush is race-safe — a prerequisite for PR2's mid-stream header preemption.

- ControlOp: recon_complete now flushes (result is already saved+published
  before it fires) and marks _flushed; the scan-start flush becomes a safety
  net that skips when already flushed (no double-flush) and only runs if the
  previous scan didn't complete. Factored into _do_flush().
- Deferred flush for GatherOp, PtychoAccumulatorOp, PtychoReconstructionOp:
  flush() sets a flag; the reset happens at the top of the next compute(), so
  it never mutates caches/GPU buffers/the PIE object concurrently with compute
  (fixes the GatherOp boolean-index race; safe for mid-stream preemption).
  recon flush also zeros the shared filled_until so it can't re-process the
  finished scan before the accumulator's own deferred flush runs.
- recon_complete is gated on all_data_arrived, so it only fires when the scan
  is genuinely complete (not when a low total_iterations exhausts mid-stream).
- SinkAndPublishOp: write_scan_file now saves-and-clears (self-contained), and
  flush() defensively writes any unwritten buffer before clearing so a scan's
  STXM file is never lost. Completion check uses >= (tolerates overshoot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On-the-fly scan-geometry reconfiguration from a live JSON header, with
mid-reconstruction preemption, on top of PR1's race-safe deferred flushes.

- header_io.py (NEW): HeaderRxOp — dedicated ZMQ SUB, validates the header
  {npoints_h,npoints_v,step_size_h,step_size_v,num_projections}, stages
  pending_geometry + requests preemption, updates shared scan_state, emits a
  "header" token to ControlOp. Rejects over-capacity grids before staging.
- ptychography_setup.py: split init_ptycho_state into load_ptycho_model
  (grid-independent) + configure_scan_geometry (grid-dependent object sizing,
  re-points views, needs_gpu_reinit, rejects no_frames > capacity) +
  init_ptycho_state (load + allocate GPU buffers ONCE at max capacity [R-6] +
  one default configure). New ptycho_state keys: H, W, capacity, scan_state,
  preempt_requested/quiesced Events, pending_geometry, needs_gpu_reinit.
- ptychography_ops.py: R-4 quiescence handshake at the top of recon.compute
  — on preempt, save the in-flight partial + emit recon_complete, set
  quiesced, then apply staged geometry while quiesced and re-init GPU for the
  new object. R-6: full-buffer summary sliced to [:no_frames].
- control.py: new "header" case -> flush for the reconfigured scan.
- pipeline.py: always-present scan_state (S11); build HeaderRxOp from the
  header_src config; wire header_src -> control_op; pass scan_state to
  init_ptycho_state.
- config_test/prod.yaml: remove npoints_*/step_size_*; add header_src block +
  max_npoints_h/v (buffer capacity) + default_step_size_h/v.

Validated in-container (A400): launch, back-to-back regression,
header-before-data + full recon on reconfigured geometry, mid-recon
preemption with partial save, post-preempt recovery, oversized-grid reject.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… + STXM output

One arm/start wraps all projections; num_projections × no_frames stream
continuously and the projection boundary is segmented by frame count
(series_id shared across projections). Each projection early-stops once its
frames are in and the in-flight PIE iteration finishes, then advances; single
scans still run to total_iterations.

- GatherOp: drain cached frames on series_finished (R-1).
- PtychoAccumulatorOp: projection-boundary split with carry, backpressure once
  filled >= no_frames, advance_projection() resets fill level while preserving
  carry + centre.
- PtychoReconstructionOp: tomography is_last = all_data_arrived; per-projection
  save ({series}_proj{NN}_recon.h5); self-advances on observing filled drop
  below no_frames (avoids double-complete); emits projection_complete
  (non-final) vs recon_complete (final).
- ControlOp: projection_complete → scoped advance (accumulator + recon only,
  NOT GatherOp — its next-projection cache must survive).
- SinkAndPublishOp: per-projection STXM save ({series}_proj{NN}.h5) reading
  scan_state; header_io sets scan_state no_frames; pipeline wires scan_state.
- ITER_TIMING per-iteration timing diagnostic retained (sub-ms overhead).
- Fix off-by-one in the dummy_img_index test path (series_frame_count - 1 →
  series_frame_count) exposed while testing; test-only path (default False).

Container-tested on the A400 with a 2-projection header over a 2048-frame
daqsim stream: both projections reconstruct and save (STXM + recon), correct
1024-frame counts, and the non-final/final completion signals distinguished.
See DECISIONS_header_tomo.md / TESTING_header_tomo.md (incl. open item O1 on
per-projection post-stream refinement iterations, deferred to PR4).

Assisted-By: Claude Opus 4.8 (1M context)
… from finalize

Motivated by moving to a much faster detector: single-buffer backpressures the
source across the ~one-iteration finalize window (measured ~490 ms), which
throttles a free-running detector and risks dropping positions (PUB/SUB, no
flow control). Double-buffering keeps the accumulator draining while the recon
finalizes.

Two GPU buffer sets (ping-pong). While the recon finalizes projection N on the
read buffer, the accumulator fills N+1 into the write buffer. On fill it flips
to the free buffer (else clean backpressure fallback when the recon is lapped
by a full projection — data is never clobbered). Single-projection scans always
use buffer 0 (no flip).

- ptychography_setup.py: allocate raw_gpu/positions_full/tilts_full as 2-element
  lists (once, at max capacity — R-6); add ping-pong state (filled_until[],
  write_idx, read_idx, buf_free[], num_buffers) reset in configure_scan_geometry.
- PtychoAccumulatorOp: _try_flip() on fill (else backpressure + a one-shot
  "Recon lagging" warning); all writes indexed by write_idx; _perform_flush zeros
  both buffers + resets the ping-pong idempotently.
- PtychoReconstructionOp: reads indexed by read_idx; _flip_read() releases the
  finished buffer, OWNS current_projection (no ControlOp round-trip → no
  save-vs-index race), advances to the next buffer, resets the object (probe
  carried); final projection emits recon_complete and idles.
- control.py: dropped the now-dead projection_complete branch; ControlOp keeps
  recon_complete / header / flush.

Container-validated on the A400 (see DESIGN_pr4_double_buffer.md): regression
(exact 1024/projection, same iteration budget), overlap (accumulator fills the
next projection during a stretched finalize without backpressure), and lapping
fallback (clean backpressure + recovery when the recon is lapped). No drops, no
deadlock. Open item O1 (per-projection post-stream iterations) is now more
visible — pre-filled projections finalize with few refinement iterations.

Assisted-By: Claude Opus 4.8 (1M context)
@RamyaGuru
RamyaGuru requested a review from UriRoche July 2, 2026 21:26
UriRoche added 23 commits July 31, 2026 11:23
…sing the recon to fail when a new header was sent mid-reconstruction. Plan can be found in fix_header_race.md. Start with PR 1: Control flush split (refactor, behavior-preserving)
…er race. Now PR 2: Transition state primitives + header phase start
…scan reacts well. Now PR 3: Phase-aware ControlOp handling. Header no longer triggers immediate ptycho flush request.

recon_complete in preemption path triggers ptycho flush request exactly once.
…sualisation wasn't flushing, it was just accumulating and looking strange. So we added a couple of lines in pipeline/control.py lines 124-126 which send a stxm_flush message to the visualiser
… PR 4: Ptycho flush execution ack + safe barrier release. Release transition only after ptycho flush has actually executed.
…low signaling; Enforce strict branch alignment and bounded blocked buffering. This implements the overflow fail error (ie determine the max number of buffered frames while ptycho state quiesces and flushes).
…th the data accumulation."

This reverts commit 013ef6e.
All PR 5 needs to be un-done and roll back to PR 4, but using git revert rather than git reset for future clarity.
…st overflow signaling; Enforce strict branch alignment and bounded blocked buffering. This implements the overflow fail error (ie determine the max number of buffered frames while ptycho state quiesces and flushes)."

This reverts commit 1eae67f.

Fully reverting PR 5.
… to clear/flush visualisation when a new projection comes in.
…n when buffer flips; this will hopefully help with out-of-bound problems at syn_probe_intensity_kernel
…e and probe phase to grayscal rather than twilight.
…nd flux is only calculated if json file value is < 0
…next projection current iteration doesn't do +1. This means that all projections will start from iteration 0. This makes sure they'll calculate flux and do probe normalisation.
…y). Also increased padding zone (this is important with fast scans selun/ck3m because they may overshoot). Stopped printing start message from detector (too long).
… batch - this requires the batch size to be at least 2x the nX, but it's useful for cases where there is a lot of overshoot in x. Also I've included a sign for the calculation of y centre, because it changes in a tomography projection to projection.
…an. In that case, we don't need to re-calculate the flux. Also changed config file for testing at the beamline.
…ppearing in the next projection. The main solution was initialising the pty_model.obj.array_global_kernel with every new projection. Also, simplified the scan_center calculation to be 0 for y and mean(px) for x -- this is not a long term solution but it'll do for now.
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