Skip to content

Spectrometer viewer: alignment & centroiding tabs, streaming reliability fixes, offline analysis tools#13

Merged
gabrielgazollalbl merged 41 commits into
mainfrom
spec-viewer
Jul 17, 2026
Merged

Spectrometer viewer: alignment & centroiding tabs, streaming reliability fixes, offline analysis tools#13
gabrielgazollalbl merged 41 commits into
mainfrom
spec-viewer

Conversation

@gabrielgazollalbl

Copy link
Copy Markdown
Collaborator

Summary

This branch turns the viewer into a full spectrometer operations tool: two new UI tabs (Alignment and Centroiding), a set of streaming/backend reliability fixes, richer end-of-scan outputs, and a suite of offline analysis and diagnostics tools under tools/.

UI

  • New Alignment tab with world-coordinate crosshairs for beamline alignment work.
  • New Centroiding tab for live centroiding diagnostics (two-peak structure, dispersive-axis projections).
  • Preferences dialog, persistent log manager, and a single-instance guard so only one UI can run at a time.
  • Operator tab improvements: cursor averages now respect heatmap zoom, last-flush heatmap clears on stop, general UX polish, app icon and desktop integration script.

Streaming & acquisition reliability

  • Fixed ZMQ start/stop message handling; flush gating switched from cycle-count to wall-clock, with a regression test for bursty traffic.
  • Ingest queue is drained before snapshotting cycle_count on stop, so final flush counts are accurate.
  • Configurable UDP receive buffer (with documentation in info/UDP_RECEIVER_BUFFER_LIMITS.md).
  • Fixed the x/y axis regression and the dispersive-axis convention (tpx3dump labels the dispersive axis as y/cy, not x/cx).

End-of-scan outputs

Each acquisition now also saves an energy axis CSV (from UI calibration), a time-axis CSV in ns (from TDC metadata), and a UUID file matching the scan_name broadcast over ZMQ, so downstream services can correlate saved files with the data stream. scan_name is now a plain UUID4.

Tooling

  • tools/centroider/: offline centroiding pipeline (API, histogramming, parameter sweeps, runner) with a tutorial in info/centroider_tutorial.md.
  • tools/sniffer/ and tools/diagnostics/: flush-pacing listener and burstiness analysis for debugging wire-level timing.
  • Repo scripts reorganized under tools/dev/ and tools/ui/.

Tests

New streaming_rig fixture spins up the streaming server on dynamic ports with pre-wired ZMQ sockets; integration tests were rewritten on top of it with tighter assertions (scan_name cohesion, contiguous flush numbers, reconnect behavior). New test suites cover burstiness, log manager, preferences, single-instance, and simulator batching.

Misc

  • live-cli updated to beta 0.4.3; simulator gained burst-mode support.
  • Docs refreshed: README, testing guide, sample start message; removed the obsolete implementation overview.

InfinityMonkeyAtWork and others added 30 commits April 16, 2026 16:56
At the end of each acquisition, save three additional files alongside
the existing PNG, CSV, and JSON:
- {stem}_energy.csv: 1D eV axis derived from the UI calibration
  spinboxes (eV @ x=0, eV/pixel), one value per x-pixel.
- {stem}_time_ns.csv: 1D time axis in nanoseconds from the TDC trigger
  (t=0), one value per time bin, using t_delta_ns from ZMQ metadata
  with a fallback to reconstruct from tdc_frequency_hz and n_bins.
- {stem}_uuid.txt: 36-char UUID4 that matches the scan_name broadcast
  by the streaming server via ZMQ, enabling downstream services to
  correlate saved files with the data stream.
To support the UUID linkage, app.py now generates scan_name as a plain
uuid.uuid4() string (replacing the datetime+8-hex format) and includes
it in every flush's ZMQ metadata so the UI can read it back at save
time.
Simulator
Start being sent when cli starts up
Stop not being sent when DAQ time runs through
Stop being sent in between event messages if buffer somewhat full during disconnect (stop trigger)
Flush numbers not reset and scan UUID not updated during consecutive starts in the same cli session
start N produced one fewer event message than expected
ZMQ start message not sent in real-acquisition mode with zero counts
Factory fixture spins up the streaming server on dynamic ports with
pre-subscribed ZMQ data + heartbeat sockets, and provides
make_simulator() / spawn_simulator_cli() so TDC / cps / flush-interval
live in one place and can't drift between server and simulator.
Rewrites the four integration tests in test_main_workflow and
test_start_stop_messages to use the rig, and tightens assertions:
scan_name cohesion across start/event/stop, contiguous 1..N
flush_number, stop.total_flushes == len(events), flush_number restart
at 1 on reconnect.
Reproduce the Serval+luna-iterator upstream-batching symptom described
in solution.md as a CI-only regression test.  Adds an opt-in bolus-
batching mode to the simulator (tcp_batch_interval_s, default 0) that
buffers packets in-memory for N seconds then emits them as a single
sendall, mimicking the way the production sorter delivers multi-second
sorted boluses over TCP.

- simulator.py / simulator_cli.py: new tcp_batch_interval_s knob,
  threaded through SimulatorSource and the Typer CLI as
  --tcp-batch-interval.  Default 0 preserves the original per-packet
  send profile; all existing tests and production paths are unchanged.
- tests/conftest.py: StreamingRig.spawn_simulator_cli gains a
  tcp_batch_interval_s kwarg that forwards to the CLI flag.
- tests/test_simulator_batching.py (new): false-green guard proving
  the knob actually batches (multi-packet sendall payloads, pacing
  near the configured interval).  Pure unit test, no subprocesses.
- tests/test_flush_burstiness.py: add test_batched_wire_regression
  with pass/fail assertions on CV < 0.5, fraction_sub_50ms < 0.1,
  max_gap < 2x flush_interval, n_flushes within 30% of expected,
  flush_number sequence 1..N, total_cycles monotonic,
  sum(cycles_in_flush) == stop.total_cycles, and n_stops == 1.
  Writes a JSON artifact with per-combo metrics to
  /tmp/burstiness_latest.json for before/after diffing.

Red state against the current (unfixed) server: CV 0.84-0.96 on all
three combos, fraction_sub_50ms 42.9% on cps=1000/tdc=100, max_gap
>= 2x flush_interval on the low-rate combo.  These failures match the
production trace in solution.md.  The wall-clock flush gate fix (next
commit) is expected to flip this test to green.

Made-with: Cursor
Decouple flush emission from TDC cadence so that bursty TDC arrivals
(e.g. when an upstream batches a 2-second window of packets in one
TCP write) no longer translate into bursty ZMQ updates.  Previously
the gate fired on cycle_count % flush_every_n_cycles == 0, which is
event-driven by definition and therefore inherits any burstiness in
the wire.  The new gate is wall-clock: emit_flush_if_due() checks
time.monotonic() - last_flush_time >= flush_interval and resets the
flush state atomically under xyt_lock.

handle_tdc and the main loop's silence watchdog (after time.sleep(1))
both call emit_flush_if_due, so the wire being idle does not stop
flushes from going out within ~flush_interval + 1s.  cycles_since_last_flush
and cycle_count are now incremented in lockstep (only when t_zero is
already armed), which keeps the conservation invariant
sum(event.cycles_in_flush) == stop.total_cycles intact.  do_final_flush
now reports cycles_since_last_flush as the residual count, which is the
exact number of cycles still sitting in local_accumulator.

The bursty-wire regression test (3 combos, batch=2.0s) goes from CV
0.84-0.96 / 7-43% sub-50ms gaps to CV 0.19-0.27 / 0% sub-50ms gaps.
The 25-combo no-regression grid passes with CV <= 0.14 everywhere
including the previously fragile low-TDC race zone.

Made-with: Cursor
Trailing TDCs that arrived over TCP but had not yet been routed
through data_callback were silently dropped from stop.total_cycles
and from the published events.  Sequence (pre-fix):

  1. Producer disconnects; _handle_client flushes residual bytes
     onto SocketDataServer.message_queue and exits.
  2. app.py main loop sees client_disconnected_event and calls
     do_final_flush() + _wait_for_xyt_drain().
     _wait_for_xyt_drain() only waits for the *output* queue
     (xyt_queue, events to ZMQ); it does not wait for the *input*
     queue (message_queue, raw byte batches to the parser).
  3. cycle_count is read into TimePixStop.total_cycles.
  4. _data_processor continues to drain message_queue in the
     background, but the stop has already been published and the
     zmq_worker is shutting down, so any flushes triggered by
     those late TDCs are also lost.

Net effect: ~flush_interval x tdc_frequency_hz cycles (and the
binned counts associated with them) silently disappear at every
acquisition end.

Fix:
  - New SocketDataServer.wait_for_idle(timeout) helper that uses
    queue.Queue's existing unfinished_tasks / all_tasks_done
    machinery (populated by the task_done() calls already in
    _data_processor) to block until every batch currently queued
    has returned from data_callback.
  - Three new wait_for_idle(5.0) calls in app.py, one before each
    do_final_flush() site (exit_on_disconnect branch, natural
    disconnect branch, finally block).  Each logs a warning and
    proceeds on timeout so a stuck producer cannot hang shutdown.

Empirical: 100 Hz x 10 s simulator run drops loss vs sim_pulses
from 5.1% (pre-fix) to ~1% (post-fix), and the gap shrinks
further with longer runs.  Existing conservation invariant in
tests/test_flush_burstiness.py::test_batched_wire_regression
still holds; tests/test_main_workflow.py still passes.

A separate connect-time race (orphan flush published before the
main-loop reset) was found during validation but is not addressed
here; details in info/report04242026.md.

Made-with: Cursor
prevent user from openeing another instance of the UI if one is already running

save user settings inside a json file and apply last settings when opening the UI
previously when starting a new DAQ it would only flush the average map (right) not the last flush (left)
remove old plans bug reports

put all network sniffing / packet debugging into a new folder
saving logs
improve logging ui
converting print statemetns to logs
adding new logs
gabrielgazollalbl and others added 11 commits May 15, 2026 13:04
script for now, no UI

Co-authored-by: Cursor <cursoragent@cursor.com>
tpx3dump labels the dispersive (physically horizontal) detector axis as
y/cy, not x/cx. The centroider was histogramming cx/x, which is the
non-dispersive axis and produces a single blob. Switching to cy/y restores
the expected two-peak structure. Updated axis label in the summary plot and
developer docs to document the naming convention.
Co-authored-by: Cursor <cursoragent@cursor.com>
@gabrielgazollalbl
gabrielgazollalbl merged commit 9e259dd into main Jul 17, 2026
6 checks passed
@gabrielgazollalbl
gabrielgazollalbl deleted the spec-viewer branch July 17, 2026 23:17
gabrielgazollalbl added a commit that referenced this pull request Jul 22, 2026
Spectrometer viewer: alignment & centroiding tabs, streaming reliability fixes, offline analysis tools
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