Python utilities for detector visualization and monitoring.
PyQt6 live colour-coded HyCal FADC scaler map. Polls B_DET_HYCAL_FADC_<name> EPICS channels every 10 s. Requires pyepics for real EPICS; --sim works without it.
python scripts/hycal_scaler_map.py # real EPICS (default)
python scripts/hycal_scaler_map.py --sim # simulation (random values)PyQt6 GUI for measuring and monitoring FADC250 pedestals on all 7 HyCal crates. Reads pedestal means from .cnf files, parses per-channel RMS from faV3peds stdout, and flags irregular channels. No numpy/matplotlib -- only PyQt6.
python scripts/hycal_pedestal_monitor.py # view existing data
python scripts/hycal_pedestal_monitor.py --sim # test with simulated dataPedestals must be measured before the first DAQ run of each shift while DAQ is idle.
- Make sure the DAQ is stopped.
- Launch:
python scripts/hycal_pedestal_monitor.py - Click Measure Pedestals and confirm. The tool SSHs to
adchycal1--adchycal7and runsfaV3peds(takes a few minutes). - Inspect the two maps (left: current mean, right: difference from configured) and the report panel for flagged channels:
DEAD-- avg < 1, rms < 0.1OUT OF RANGE-- mean outside 50--300HIGH RMS-- sigma > 1.5DRIFT-- shifted > 3 counts from configured
- Click Save Report to save for the shift log.
- If new issues appear, notify the run coordinator before starting data taking.
Thresholds are defined at the top of the script and can be adjusted.
PyQt6 visual editor for FAV3 trigger masks. Displays a HyCal geo view (with LMS1-3, LMSP, V1-V4 below) and lets you click or drag modules to toggle channels off/on. Generates trigger mask .cnf files -- only slots with disabled channels are written. Unmapped DAQ channels (slot positions with no module) are always masked off.
python scripts/trigger_mask_editor.py # start fresh
python scripts/trigger_mask_editor.py -i existing.cnf # load existing mask
python scripts/trigger_mask_editor.py -o output.cnf # set default save pathGEM-specific scripts and the gem_dump C++ binary live in the top-level
gem/ directory — see gem/README.md
for details and GEM detector reference facts.
PyQt6 viewer for V1190 TDC hits from the tagger crate (ROC 0x008E, bank
0xE107). Shows a per-slot bar chart of hits/channel, a single-channel
TDC-value histogram, plus event-wise correlation tabs (Δt = A − B and a
2-D tdc(A) vs tdc(B) heatmap). No matplotlib / pyqtgraph; plots are
drawn with QPainter (numpy is the only scientific dep). The bar chart
auto-sizes its x-axis to 16 / 32 / 64 / 128 channels based on the
highest channel actually hit. Human-readable counter names come from
database/tagger_map.json.
Two data sources:
Offline — evio file (decoded in-process by prad2py):
cmake -DBUILD_PYTHON=ON -S . -B build && cmake --build build
# optional — the viewer auto-adds build/python/ to sys.path
export PYTHONPATH="$PWD/build/python:$PYTHONPATH"
python scripts/tagger_viewer.py /data/stage6/prad_023667/prad_023667.evio.00000 \
-n 200000 # limit number of physics events (optional)
--roc 0x8E # restrict to the tagger ROC (optional)Online — live ET stream from a running prad2_server. The server
only decodes tagger TDC hits when at least one client is subscribed, so
regular monitoring is unaffected:
# DAQ machine (one-time)
./build/bin/prad2_server --online --port 5051
# Viewer (anywhere with PyQt6 + QtWebSockets installed)
python scripts/tagger_viewer.py --live ws://clondaq6:5051On startup with --live, a fast subscribe/ack round-trip is done
before the main window opens — if the server is unreachable or the
protocol doesn't match, tagger_viewer exits with a clear error rather
than showing an empty window. Pass --no-smoke-test to skip it.
The File menu also has Connect to prad2_server… (Ctrl+L) and Disconnect. Pause / Clear buttons sit next to the Bins spinner. Memory is capped at 10 M hits (rolling — oldest half is dropped).
Wire protocol — WebSocket JSON messages tagger_subscribe /
tagger_subscribed / tagger_unsubscribe. Binary frame format
(useful for anyone writing a different client):
magic "TGR1" (4 bytes)
flags u32 (bit 0 = some frames have been dropped)
n_hits u32
first_seq u32
last_seq u32
dropped u32 (cumulative since server start)
records n_hits × 16-byte packed BinHit
u32 event_num, u32 trigger_bits, u16 roc_tag,
u8 slot, u8 channel_edge (bit 7 = edge, bits 6:0 = channel),
u32 tdc
prad2py exposes the decoder through a dec submodule — useful for custom
offline analysis that goes beyond what the tagger_viewer does. Build it with
-DBUILD_PYTHON=ON once, then:
import prad2py
from prad2py import dec # evio reader + event types
cfg = dec.load_daq_config() # installed daq_config.json
ch = dec.EvChannel()
ch.set_config(cfg)
st = ch.open("/data/.../prad_023671.evio.00000")
assert st == dec.Status.success
while ch.read() == dec.Status.success:
if not ch.scan() or ch.get_event_type() != dec.EventType.Physics:
continue
for i in range(ch.get_n_events()):
ch.select_event(i) # picks sub-event + clears cache
# Cheapest accessor — TI/trigger metadata only (no detector decode):
info = ch.info()
# …do something with info.event_number / .trigger_bits / .timestamp …
# Decode detector products on demand — each is cached for repeat calls
# on the same sub-event, so asking for fadc() then gem() costs nothing
# extra if you only need one of them:
fadc_evt = ch.fadc() # FADC250/ADC1881M waveforms
tdc_evt = ch.tdc() # V1190 tagger hits
# gem_evt = ch.gem(); vtp_evt = ch.vtp() # when needed
for roc_idx in range(fadc_evt.nrocs):
roc = fadc_evt.roc(roc_idx)
for s in roc.present_slots():
slot = roc.slot(s)
for c in slot.present_channels():
samples = slot.channel(c).samples # numpy uint16 array
…
for j in range(tdc_evt.n_hits):
h = tdc_evt.hit(j) # TdcHit
…The helper prad2py.load_tdc_hits(path, ...) is still available for the
common "one-shot flat table of hits" workflow and lives on top of the
dec submodule (decoder) and the det submodule (reconstruction: GEM
- HyCal + DetectorTransform + EpicsStore).
from prad2py import dec, det
# --- decoder -----------------------------------------------------------
cfg = dec.load_daq_config()
ch = dec.EvChannel(); ch.set_config(cfg); ch.open("run.evio.00000")
# --- GEM reconstruction -----------------------------------------------
gsys = det.GemSystem()
gsys.init("database/gem_map.json")
gsys.load_pedestals("database/gem_ped.json") # optional
gcl = det.GemCluster()
# --- HyCal reconstruction ---------------------------------------------
hsys = det.HyCalSystem()
hsys.init("database/hycal_modules.json", "database/daq_map.json")
hsys.load_calibration("database/hycal_calib.json")
hcl = det.HyCalCluster(hsys)
while ch.read() == dec.Status.success:
if not ch.scan() or ch.get_event_type() != dec.EventType.Physics:
continue
for i in range(ch.get_n_events()):
ch.select_event(i)
# GEM 2-D hits
gsys.clear()
gsys.process_event(ch.gem())
gsys.reconstruct(gcl)
for h in gsys.get_all_hits():
print("GEM", h.det_id, h.x, h.y, h.x_charge, h.y_charge)
# HyCal clusters — feed per-module energies yourself (e.g. from
# ch.fadc() + your calibration), then cluster:
# hcl.clear()
# for module_idx, energy_mev in my_hycal_hits(ch.fadc()):
# hcl.add_hit(module_idx, energy_mev)
# hcl.form_clusters()
# for c in hcl.reconstruct_hits():
# print("HyCal", c.center_id, c.x, c.y, c.energy)See analysis/scripts/tagger_hycal_correlation.C — a self-contained
ROOT/ACLiC macro that builds ΔT histograms for (T10R, E49…E53) pairs,
Gaussian-fits each coincidence peak, applies a ±Nσ timing cut, and
plots the W1156 peak height/integral for the selected events.
cd build
root -l ../analysis/scripts/rootlogon.C
.x ../analysis/scripts/tagger_hycal_correlation.C+( \
"/data/stage6/prad_023671/prad_023671.evio.00000", \
"tagger_w1156_corr.root", 500000)