python/README.md). What changes in this note:
PulseConfig default is now sample_period_s = 0.8e-6 (1.25 MHz). The module table and the example below were written with 1.6e-6; both are corrected. PulseConfig(sample_period_s=1.6e-6) now emits a UserWarning, and pulse_archive.load_good_events warns for an archive written with 1.6 µs.half_max_time_us and frequencies_hz follow the sample period: times in µs are half, and frequencies twice, what the old default gave for the same samples.pulse_channels and pulse_archive changes are not in the tables).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.
python/ — the library and its tests. Pinned to e6033f0.python/README.md — the conventions in short form. Pinned to 894ef9d.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.
(..., n_samples): one pulse (n_samples,) or a batch (n_pulses, n_samples) both work, because every reduction runs along axis=-1. A plain NumPy array is the pulse; there is no custom class. This is what gives uniform inputs across all the categories below.PulseConfig is passed explicitly to every function, replacing constants copied between notebooks.python/ into sys.path, the same convention as nrFanoII_paper2022’s python/ba_bknd_lines.py.| Module | Signature | Returns |
|---|---|---|
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 helpers | plain 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 |
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)
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.
| Module | Functions | Done | Under development |
|---|---|---|---|
pulse_quantities.py | 14 | 11 | 3 |
pulse_cuts.py | 11 | 6 | 5 |
pulse_operations.py | 3 | 3 | 0 |
pulse_io.py | 6 | 6 | 0 |
pulse_archive.py | 3 | 3 | 0 |
| Total | 37 | 29 | 8 |
(PulseConfig and the status machinery themselves are not tagged.)
pulse_quantities.pyPulses in, one number (or one spectrum) per pulse out. The primitives that cuts threshold. 14 functions: 11 done, 3 under development.
| Function | Purpose | Status |
|---|---|---|
pretrigger_mean | Mean of the pretrigger window, glitch samples skipped | done |
pretrigger_std | Standard deviation of the same window, whose length follows the config (default 1000 samples) | done |
bstd | Baseline scatter (“bstd”): standard deviation of the first 500 samples, glitch samples skipped. The window is fixed by n_samples (default 500), not by the config | done |
bstd_1000 | bstd over the first 1000 samples | done |
bline | Baseline 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 config | done |
bline_1000 | bline over the first 1000 samples | done |
max_deviation | Largest |post-pretrigger sample − pretrigger mean| (“dev”) | done |
excursion_ratio | max_deviation / pretrigger_std (“ratio”) | done |
log_excursion_ratio | log10 of the ratio | done |
excursion_duration | Longest run of consecutive post-pretrigger samples at or above half of the trace’s own dev | under development |
rise_sample | First post-pretrigger sample crossing a fraction of dev (half-maximum heuristic) | under development |
half_max_time_us | Time 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 samples | under development |
power_spectrum | Real part of rfft × conj(rfft), i.e. raw |FFT|2 | done |
frequencies_hz | Frequency bins (Hz) matching power_spectrum | done |
pulse_cuts.pyPulses in, one boolean per pulse out (True = keep). Combine with plain &, |, ~. 11 functions: 6 done, 5 under development.
| Function | Purpose | Status |
|---|---|---|
dead_channel | Pretrigger standard deviation is exactly zero (channel not read out) | done |
active | ~dead_channel | done |
quiet_baseline | bstd below its own percentile across the batch; dead channels never pass | done |
ratio_band | Linear ratio in [low, high), either bound optional (generic) | done |
excursion_band | log10(ratio) in [0.5, 0.7) by default: the tightened cut of Note 2 | done |
excursion_band_loose | log10(ratio) in [0.5, 1.0): the “best” cut of Note 2 | done |
excursion_below_percentile | ratio below its own percentile across the batch | under development |
looks_like_pulse | excursion_duration at or above min_duration (default 50) | under development |
excursion_band_AI | excursion_band with looks_like_pulse traces removed (Note 6) | under development |
randoms | Trigger type recorded as Physics, taken to be random triggers (labels assumed swapped, Note 2a). Takes the trigger-type array, not pulses | under development |
real_triggers | Trigger type recorded as Unknown, taken to be real triggers. Takes the trigger-type array, not pulses | under development |
pulse_operations.pyPulses in, new pulses out, same pulse-shaped family. Never modify their input in place. 3 functions: 3 done, 0 under development.
| Function | Purpose | Status |
|---|---|---|
baseline_subtract | Subtract each pulse’s own pretrigger mean (does not remove the glitch samples) | done |
trim_glitch | Drop the leading glitch samples from every pulse | done |
clip | Clip every sample to [low, high] | done |
cdms objects to arrays — pulse_io.pyThe only module that knows about nsdf_dark_matter’s cdms objects. 6 functions: 6 done, 0 under development.
| Function | Purpose | Status |
|---|---|---|
detector_index | Parse the detector component of a <event>_<detector>_Phonon_<n> id | done |
event_id | Parse the event component of the same id | done |
filter_by_detector | Keep only ids for a given detector | done |
load_channel_batch | cdms object to a plain (n_pulses, n_samples) array | done |
load_channel_batch_multi | Same, pooled across several dumps | done |
trigger_types | The trigger_type the dump’s .csv records for each id’s event, aligned with the ids; input to randoms / real_triggers | done |
pulse_archive.pyThe only module that needs h5py. Writes to and reads from archives/good_noise.h5 (see Note 6). 3 functions: 3 done, 0 under development.
| Function | Purpose | Status |
|---|---|---|
save_good_events | Write a cut’s selected detector_ids for one series/detector/channel (overwrites that group) | done |
load_good_events | Read one archived selection back, with its provenance attributes | done |
list_archive | Overview of everything saved in an archive file | done |
| Function | Module | What is not yet settled |
|---|---|---|
excursion_band_AI | pulse_cuts.py | excursion_band plus the looks_like_pulse veto; both are judgment calls. Check what it removes before trusting it on a new dataset. |
excursion_below_percentile | pulse_cuts.py | Matches the original “good noise” cut, which is biased toward contaminated-baseline traces when used alone; combine it with quiet_baseline. |
looks_like_pulse | pulse_cuts.py | A 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). |
randoms | pulse_cuts.py | The 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_triggers | pulse_cuts.py | The other half of the same assumption (Unknown taken to be real triggers). |
excursion_duration | pulse_quantities.py | Devised 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_us | pulse_quantities.py | Built on rise_sample, so it inherits its status. Only meaningful for a trace that contains a pulse; first used in Note 2a. |
rise_sample | pulse_quantities.py | Half-maximum crossing heuristic, used in one exploratory analysis (Cut A rise time, Note 2). Not yet checked against noisy or multi-peak traces. |
list_statuses(module) in the repository.min_duration = 50 were chosen on one dataset (Detector 0 / Channel 0, dump 1) and need re-checking elsewhere before the corresponding functions are promoted to done.