Skip to main content

pytesscope

Live pulse viewer / dIdV scope for pytesdaqx, migrated and refactored from the pulse viewer in pytesdaq-obsolete.

Installation

pip install pytesscope

For live acquisition against real NI/PXI hardware, install the ni extra too:

pip install pytesscope[ni]

nidaqmx isn't pulled in by a plain install - it's pytesdaqx's own optional ni extra (plenty of pytesdaqx use, e.g. temperature control or signal generators, doesn't touch NI hardware at all), so pytesscope doesn't force it on file-replay/--backend fake users either. Without it, pytesscope --backend nidaq ... fails as soon as it tries to actually open a device.

A Qt styling gotcha worth knowing about

A "bare declaration" stylesheet - widget.setStyleSheet(f"background-color: {color};") with no selector - silently breaks QComboBox popup styling for any descendant combobox, anywhere in that widget's subtree, even if the popup styling itself lives in a completely separate stylesheet (app-level or on the combobox directly). Reproduced with a minimal repro outside this codebase before fixing it here. Always use theme.scoped_background(name, color) / theme.scoped_text_color(name, color) (with widget.setObjectName(name) first) instead of a bare declaration - see gui/theme.py and its usages in main_window.py/display_panel.py/ tools_window.py/channel_panel.py for the pattern.

Status

Under active development. Current layout:

pytesscope/
  analysis/       Qt-free analysis pipeline (normalization, PSD, running
                   average, pileup rejection, dIdV pole fitting)

                   normalize() requires norm_list whenever norm_type != NoNorm
                   was actually requested; it used to silently skip the
                   volts-to-amps division when norm_list was missing (e.g. a
                   failed board/file read) and fall straight through to the
                   display unit's scale factor - producing a plausible but
                   wrong number (raw volts read as if it were current,
                   inflated further by uAmps' 1e6) instead of an error.
                   Controller.process_frame() catches this (and any other
                   per-frame analysis error) and reports it via on_status
                   instead of letting it recur silently on every poll tick.

                   didv_fit()'s data_array_truncated/fit_array add the
                   trace's baseline back after qetpy's fit (which works on
                   an AC-only, baseline-subtracted trace internally) -
                   without this they were zero-centered, which only matched
                   the *live* running-average trace (never baseline-
                   subtracted) on the exact frame the fit ran on. Since the
                   fit overlay is kept and redrawn on later frames too (see
                   MainWindow._fit_overlay), the overlay would visibly drift
                   away from the live trace's true DC level afterward.

                   A completed dIdV fit now also pauses the display (like
                   the old GUI): ControlPanel becomes a 3-state Start/Stop/
                   Resume button (yellow while paused), the poll timer stops
                   so the frame holding the fit result stays on screen for
                   inspection/zooming, and clicking Resume drops the overlay
                   and continues - without re-arming the data source, since
                   pausing never told it to stop in the first place.
  file_board_reader.py  FileBoardReader: BoardReader backed by a recording's
                   stored per-channel settings (close_loop_norm, preamp_gain,
                   signal-gen, TES bias) instead of a live board read - the
                   file-replay equivalent of board_reader.py, wired into
                   launcher.py's --file path. Without this, Amps/uAmps
                   selection during file replay had no normalization source
                   at all (NullBoardReader raised, but normalize()'s old
                   silent-skip bug masked that as bad numbers rather than a
                   visible error).
  acquisition/     Data source adapters over pytesdaqx: LiveSource (NI-DAQ,
                   via DAQSession/StreamingAcquisitionLoop) and FileSource
                   (zarr/hdf5 replay, via StreamReader). Both produce a
                   common Frame (raw ADC codes + calibration coefficients).

                   read_frame() returning None is ambiguous by itself: for
                   LiveSource it means "nothing new yet, poll again"; for
                   FileSource it means "recording is over". FileSource
                   exposes is_exhausted to disambiguate - MainWindow checks
                   it after every poll and auto-stops with a clear status
                   message instead of silently going quiet forever (which
                   read exactly like a freeze). FileSource.start() rewinds
                   automatically only when already exhausted, so Stop-then-
                   Start still resumes mid-file rather than always
                   restarting from the top.

                   Both sources support an optional trace_length_ms knob
                   (ControlPanel's "Trace Length [ms]" field), but only where
                   it's physically meaningful: LiveSource.trace_length_supported()
                   is true only for a continuous-mode measurement, since a
                   finite/triggered one (dIdV, threshold) has a fixed record
                   shape - a PXI-locked dIdV trace needs an integer number of
                   signal-generator periods, which a plain "ms" knob could
                   violate. FileSource.trace_length_supported() is true only
                   for a native continuous Zarr stream (raw_shape_model ==
                   "channel_sample"); HDF5 recordings and finite/trace Zarr
                   streams have a fixed length baked into the file itself, so
                   there's nothing to adjust. When supported, FileSource reads
                   fixed-size partitions via read_partition() instead of
                   read_next(); LiveSource folds it into adc.read_block_duration
                   for the next start(). MainWindow re-checks support on every
                   measurement change and disables the control accordingly.
  gui/             PySide6 GUI shell: MainWindow composition root plus
                   ControlPanel (measurement select + start/stop),
                   ChannelPanel, DisplayPanel (waveform/PSD toggle,
                   unit/norm controls, matplotlib canvas, dIdV fit overlay),
                   ToolsPanel (running avg / LP filter / read-from-board /
                   opens ToolsWindow), and ToolsWindow (dIdV pole-model fit
                   + pileup-rejection cuts, in a separate top-level window
                   like the old design). Widgets only emit signals; all
                   wiring to the Controller lives in MainWindow.

                   ChannelPanel is a vertical list (color swatch + checkbox)
                   for the first 8 channels, with a "More Channels..."
                   button opening a scrollable picker dialog for the rest
                   (channel count isn't hardcoded to 24 - it's however many
                   channels are passed in). Long names are middle-elided
                   ("Mv6BandG...insRight", not right-elided, since detector
                   names often differ only in a suffix) with the full name
                   in a tooltip; a fixed-cell button grid (the old AI0-AI7
                   style) doesn't work once names run 20+ characters.
  controller.py    Qt-free orchestrator wiring a DataSource into the
                   analysis pipeline. Board reads (normalization, signal
                   generator, TES bias) go through a BoardReader Protocol,
                   cached and refreshed only on explicit triggers (channel
                   change / norm-type change / user action) since they can
                   be slow.
  board_reader.py  PytesdaqxBoardReader: concrete BoardReader over
                   SQUIDService/TESBiasService. Supports "Open Loop" and
                   "Close Loop" normalization only (OpenLoop PreAmp+FB was
                   dropped - see module docstring); output_fix_gain/
                   preamp_fix_gain are assumed 1.0 until pytesdaqx's config
                   schema grows a place for them (StarCryo SQUID electronics).
  launcher.py      CLI entry point: `pytesscope --hardware ... --run-setup
                   ... --acquisition-config ... [--backend nidaq|fake]` for
                   live acquisition, or `pytesscope --file <acquisition>`
                   to replay a recorded zarr/hdf5 acquisition. Live vs. file
                   is chosen once at launch (a CLI arg), not a runtime GUI
                   toggle - a deliberate simplification now that the
                   config-driven design resolves channels/measurements from
                   YAML rather than a device combobox.

                   For live acquisition, also builds a
                   `pytesdaqx.control.coordinator.ControlCoordinator` (shared
                   HardwareRegistry with the board reader) and passes it to
                   `LiveSource`. `LiveSource` sequences a dIdV measurement's
                   PXI AC drive correctly: arm the AI task first, *then*
                   start the AO waveform, so the AI's `/ao/StartTrigger`
                   defines phase zero. `start_measurement_tes_ac` is a no-op
                   for measurements without a TES-AC waveform block, so this
                   is always safe to wire up even for background/threshold
                   runs.

Qt runs headless via QT_QPA_PLATFORM=offscreen in tests and in this (display-less) dev environment; pytest-qt's qtbot fixture drives widget interaction in tests without a real display.

Development

pip install -e ".[dev]"
pytest

Try the GUI shell against the fake backend (no hardware required), using pytesdaqx's example configs:

QT_QPA_PLATFORM=offscreen pytesscope \
  --hardware ../pytesdaqx/examples/configuration/basic/hardware_fake.yaml \
  --run-setup ../pytesdaqx/examples/configuration/basic/run_setup.yaml \
  --acquisition-config ../pytesdaqx/examples/configuration/basic/acquisition_background.yaml \
  --backend fake

(Drop QT_QPA_PLATFORM=offscreen on a machine with a real display.)

Real-display runtime gotcha (Linux/conda)

On a conda-managed Linux machine, PySide6's xcb platform plugin (needed to actually show a window - offscreen above doesn't need any of this) can fail to start because libxcb-cursor.so.1 isn't present, and/or because Qt's shared libraries in the conda env aren't on the loader's search path. If the GUI won't start outside QT_QPA_PLATFORM=offscreen, try:

conda install -c conda-forge xcb-util-cursor
export LD_LIBRARY_PATH="$CONDA_PREFIX/lib:$LD_LIBRARY_PATH"

(the export isn't persistent - add it to the env's activation script, or your shell profile, to avoid repeating it every session).

Release files for pytesscope 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytesscope 0.2.1
File Size Uploaded
pytesscope-0.2.1.tar.gz 81.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytesscope 0.2.1
File Interpreter ABI Platform
pytesscope-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 142.3 kB

Release files / pytesscope-0.2.1.tar.gz

Download URL pytesscope-0.2.1.tar.gz
Size 81.2 kB
Tags Source
SHA-256 checksum
How to use checksums
fb8516c7d0e8da270475221d0bc970225acca6e3800f3d907dc5b50aefeecdd9
BLAKE2b-256 checksum
How to use checksums
9d25b5674a599a7b598fc9971d60be42a436ee6a6c52b0df3259b023a80ba200
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / pytesscope-0.2.1-py3-none-any.whl

Download URL pytesscope-0.2.1-py3-none-any.whl
Size 61.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d1918b8c810fd26d2ba52c20491b3020f4aa5aacb95a31bb0d94b400b9a96380
BLAKE2b-256 checksum
How to use checksums
b320de37303ac159a1d166568254b9e2931bcaf0a09487531a5395b6945936c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release history Release notifications | RSS feed

0.3.1

2 release files

0.3.0

2 release files

This release

0.2.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page