python/ library structure: modules, function tables, and status markers

Note 1a · Author(s): Anthony Villano · 22 Sept 2026 (inventory as of 25 Sept 2026, revised 22 Oct 2026) · Status: In progress

Caution (2026-10-22): the sample time has changed. The phonon sample period is 0.8 µs (1.25 MHz), not the 1.6 µs (625 kHz) this note was written with (see The sample period in python/README.md). What changes in this note:

This is the reference for how the shared pulse library is organized, placed early because every later note uses it. The history of how it came about and how it was validated is in Note 4.

Code Repository

Why a shared library

Several notebooks had each grown their own copy of the same handful of computations (pretrigger mean and standard deviation, the excursion ratio, baseline subtraction, an FFT power spectrum) with the same constants hardcoded separately in each. That logic now lives in one place, and the notebooks import it.

Framework

Module contracts

ModuleSignatureReturns
pulse_quantities.py(pulses, config, **params)one number (or spectrum) per pulse, shape (...,) or (..., n_freq)
pulse_cuts.py(pulses, config, **params)one bool per pulse, shape (...,) (True = keep)
pulse_operations.py(pulses, config, **params)new pulses, same pulse-shaped family as the input
pulse_io.py(cdms, detector_ids, channel) and id helpersplain arrays and id strings; the only module that touches cdms objects
pulse_archive.py(path, series, detector, channel, ...)saves or reads a cut’s selected ids in an HDF5 file; the only module that needs h5py
pulse_config.py—the PulseConfig dataclass: pretrigger_samples = 1000, glitch_samples = 10, sample_period_s = 0.8e-6 (1.25 MHz; this note first said 1.6e-6 (625 kHz), which was wrong)
pulse_status.py—the status decorator and list_statuses(module), described next

Typical use

import sys
sys.path.insert(0, "/path/to/NSDF-Data/python")
import pulse_io as pio, pulse_cuts as pc
import pulse_operations as op, pulse_quantities as pq
from pulse_config import PulseConfig

config = PulseConfig(pretrigger_samples=500, glitch_samples=10)     # sample_period_s defaults to 0.8e-6
ids, pulses = pio.load_channel_batch(cdms, pio.filter_by_detector(cdms.get_detector_ids(), 0), 0)

mask = pc.excursion_band_AI(pulses, config)     # under development: emits a UserWarning
power = pq.power_spectrum(op.baseline_subtract(pulses[mask], config), config)

Marking functions done or under development

Every function in the four function modules and the archive module is tagged @status(DONE) or @status(UNDER_DEVELOPMENT, note=...). A done function is called normally. An under-development function still runs, but emits a UserWarning carrying its note on every call, so a notebook that depends on one says so in its own output instead of looking as settled as everything else. list_statuses(module) returns the inventory as {name: (status, note)}; the tables on this page are generated from it.

ModuleFunctionsDoneUnder development
pulse_quantities.py14113
pulse_cuts.py1165
pulse_operations.py330
pulse_io.py660
pulse_archive.py330
Total37298

(PulseConfig and the status machinery themselves are not tagged.)

Function tables by category

Quantities — pulse_quantities.py

Pulses in, one number (or one spectrum) per pulse out. The primitives that cuts threshold. 14 functions: 11 done, 3 under development.

FunctionPurposeStatus
pretrigger_meanMean of the pretrigger window, glitch samples skippeddone
pretrigger_stdStandard deviation of the same window, whose length follows the config (default 1000 samples)done
bstdBaseline scatter (“bstd”): standard deviation of the first 500 samples, glitch samples skipped. The window is fixed by n_samples (default 500), not by the configdone
bstd_1000bstd over the first 1000 samplesdone
blineBaseline level (“bline”): mean of the first 500 samples, glitch samples skipped; the companion of bstd. The window is fixed by n_samples (default 500), not by the configdone
bline_1000bline over the first 1000 samplesdone
max_deviationLargest |post-pretrigger sample − pretrigger mean| (“dev”)done
excursion_ratiomax_deviation / pretrigger_std (“ratio”)done
log_excursion_ratiolog10 of the ratiodone
excursion_durationLongest run of consecutive post-pretrigger samples at or above half of the trace’s own devunder development
rise_sampleFirst post-pretrigger sample crossing a fraction of dev (half-maximum heuristic)under development
half_max_time_usTime in µs from the start of the trace at which the pulse first reaches 50% of its own dev (rise_sample × sample period). Needs a config whose window ends before the pulse starts, e.g. 500 samplesunder development
power_spectrumReal part of rfft × conj(rfft), i.e. raw |FFT|2done
frequencies_hzFrequency bins (Hz) matching power_spectrumdone

Cuts — pulse_cuts.py

Pulses in, one boolean per pulse out (True = keep). Combine with plain &, |, ~. 11 functions: 6 done, 5 under development.

FunctionPurposeStatus
dead_channelPretrigger standard deviation is exactly zero (channel not read out)done
active~dead_channeldone
quiet_baselinebstd below its own percentile across the batch; dead channels never passdone
ratio_bandLinear ratio in [low, high), either bound optional (generic)done
excursion_bandlog10(ratio) in [0.5, 0.7) by default: the tightened cut of Note 2done
excursion_band_looselog10(ratio) in [0.5, 1.0): the “best” cut of Note 2done
excursion_below_percentileratio below its own percentile across the batchunder development
looks_like_pulseexcursion_duration at or above min_duration (default 50)under development
excursion_band_AIexcursion_band with looks_like_pulse traces removed (Note 6)under development
randomsTrigger type recorded as Physics, taken to be random triggers (labels assumed swapped, Note 2a). Takes the trigger-type array, not pulsesunder development
real_triggersTrigger type recorded as Unknown, taken to be real triggers. Takes the trigger-type array, not pulsesunder development

Operations — pulse_operations.py

Pulses in, new pulses out, same pulse-shaped family. Never modify their input in place. 3 functions: 3 done, 0 under development.

FunctionPurposeStatus
baseline_subtractSubtract each pulse’s own pretrigger mean (does not remove the glitch samples)done
trim_glitchDrop the leading glitch samples from every pulsedone
clipClip every sample to [low, high]done

I/O: bridging cdms objects to arrays — pulse_io.py

The only module that knows about nsdf_dark_matter’s cdms objects. 6 functions: 6 done, 0 under development.

FunctionPurposeStatus
detector_indexParse the detector component of a <event>_<detector>_Phonon_<n> iddone
event_idParse the event component of the same iddone
filter_by_detectorKeep only ids for a given detectordone
load_channel_batchcdms object to a plain (n_pulses, n_samples) arraydone
load_channel_batch_multiSame, pooled across several dumpsdone
trigger_typesThe trigger_type the dump’s .csv records for each id’s event, aligned with the ids; input to randoms / real_triggersdone

Archive: persisting a cut’s result — pulse_archive.py

The only module that needs h5py. Writes to and reads from archives/good_noise.h5 (see Note 6). 3 functions: 3 done, 0 under development.

FunctionPurposeStatus
save_good_eventsWrite a cut’s selected detector_ids for one series/detector/channel (overwrites that group)done
load_good_eventsRead one archived selection back, with its provenance attributesdone
list_archiveOverview of everything saved in an archive filedone

What is still under development, and why

FunctionModuleWhat is not yet settled
excursion_band_AIpulse_cuts.pyexcursion_band plus the looks_like_pulse veto; both are judgment calls. Check what it removes before trusting it on a new dataset.
excursion_below_percentilepulse_cuts.pyMatches the original “good noise” cut, which is biased toward contaminated-baseline traces when used alone; combine it with quiet_baseline.
looks_like_pulsepulse_cuts.pyA judgment call, not a validated physical criterion. The default min_duration = 50 was picked by eye from a gap in one dataset (179 traces at ≤ 25 samples, 13 at ≥ 168, none between).
randomspulse_cuts.pyThe mapping of the recorded Physics label to random triggers is a working assumption from one dump, not confirmed against the DAQ configuration or the nsdf library.
real_triggerspulse_cuts.pyThe other half of the same assumption (Unknown taken to be real triggers).
excursion_durationpulse_quantities.pyDevised for the pulse veto and checked on one dataset only. Deliberately uses the longest contiguous run: a plain count of samples above half-maximum flagged every trace in the tightened cut.
half_max_time_uspulse_quantities.pyBuilt on rise_sample, so it inherits its status. Only meaningful for a trace that contains a pulse; first used in Note 2a.
rise_samplepulse_quantities.pyHalf-maximum crossing heuristic, used in one exploratory analysis (Cut A rise time, Note 2). Not yet checked against noisy or multi-peak traces.

Status / Next Steps