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.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytesscope-0.3.0.tar.gz | 84.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytesscope-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 146.8 kB
Release files / pytesscope-0.3.0.tar.gz
| Download URL | pytesscope-0.3.0.tar.gz |
|---|---|
| Size | 84.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
28678884cbb3d9d93c392c53aea4306d7e160f9d4cb7a948f42db09f2c549174
|
|
BLAKE2b-256 checksum How to use checksums |
6856fe04bfa39dd89a349abea929ab528afa92167e811e9670d16336fbf9ad5e
|
| 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.3.0-py3-none-any.whl
| Download URL | pytesscope-0.3.0-py3-none-any.whl |
|---|---|
| Size | 62.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9197ddf938f9e2cbdbe49b2d83dd1899860ab40d0d778912ec78e3b84bcd66c0
|
|
BLAKE2b-256 checksum How to use checksums |
353eff07b1562e9d8678c4f6654bd032d757426d43d140ac6d1883bf3ee345e7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|