wdflow
A maintained, standalone package for the WDF (Wavelet Detection Filter) trigger-generation
pipeline for transient time-series signals, built on the C++ core
p4TSA (exposed to Python as pytsa), plus the downstream
trigger analysis that turns raw per-window triggers into candidate events.
It runs in real time, at a latency that is fixed and known before the filter runs, and its per-window arithmetic – an autoregressive filter of known order, an orthonormal transform of a power-of-two window, a threshold and a sum of squares, with no iteration to convergence, no dynamic memory and no data-dependent branching – is simple enough to put on an FPGA. No template and no signal model: every trigger carries the coefficients that produced it, so what a transient is can be decided afterwards.
Layout
wdf.config,wdf.processes,wdf.observers,wdf.structures– trigger generation. Needs the compiledpytsa/p4TSA core (pip install -e ".[pipeline]").wdf.analysis– clustering, multi-detector coincidence (classical + GNN), background/ false-alarm-probability, ROC analysis, sky localisation, and the submission writer that puts the surviving candidates in a challenge’s own columns. Operates on plain pandas DataFrames / saved trigger files, nopytsadependency, so it works standalone (pip install -e ., no extras needed).wdf.mock– the simulated two-detector data set: coloured Gaussian noise, compact-binary injections projected through the antenna responses, single-detector glitch morphologies, and waveforms read from a catalogue when a class has no closed form. Paired foreground and background frames with a truth table (pip install -e ".[mock]").
Relationship to the legacy wdf package
wdflow carries forward the parts of the retired wdf package
that are actually used by the real trigger-search pipeline (wdfUnitDSWorker and everything it
calls) – same file/class/module names and shapes, no gratuitous renaming – plus the
wdf.analysis layer (formerly a separate wdfLib package), merged in here as one namespace to
install and import from.
Changes from the legacy wdf package:
AR-whitening lookahead is fixed-size, independent of
par.len.DoubleWhitening’s backward pass needs a lookahead buffer of real future data to settle before producing a good estimate for the current chunk. This lookahead (WhiteningExtraSize, default 20 resampled-rate seconds) is now a fixed size, decoupled frompar.len(the streaming chunk size, an I/O batching/throughput knob). Setparameters.WhiteningExtraSize = 0to reproduce the legacy behavior.The detection loop stops
par.lenseconds before the requested segment end (unchanged from the legacy package): it checks the previous read’s start before issuing the next one, so its last read can extend up topar.lenpast the checked bound, andFrameIChanneldoes not always raise cleanly when asked to read past the last available frame data. This margin keeps the last read within legitimately available data. A largerpar.lentherefore analyzes up topar.lenfewer seconds at a segment’s tail.Per-trigger statistics remain somewhat sensitive to
par.leneven with the fixed whitening lookahead above:WaveletThreshold’sdohonojohnstonmode recomputes its detection threshold fresh per window from that window’s own coefficient median, so triggers whose statistic sits close to that threshold can flip in or out of detection depending on floating-point roundoff accumulated differently across different chunk sizes. This is an inherent sensitivity of any hard/soft-threshold statistic to input near its decision boundary, not a data-corruption bug – triggers well away from threshold are unaffected.EnWDFis the detection statistic, everywhere. It is the norm of a window’s surviving wavelet coefficients on the noise scale,||c||/sigma, computed in p4TSA and carried on every trigger. The candidate bases are orthonormal, so by Parseval this is also||x_hat||/sigma, the matched-filter signal-to-noise ratio of the reconstructed transient. Clustering, coincidence, GNN scoring and the false-alarm-probability estimate all rank on it, per window, per event and per network – foreground and background have to be ranked on the same quantity for a probability to mean anything.snrPeakandsnrMeansit alongside it as amplitudes on the same scale,max|x|/sigmaand the r.m.s. ofxover its own support divided bysigma.EnWDFgrows as the square root of the number of samples a transient occupies, as an energy statistic must; the other two do not.Every frequency a trigger reports is a moment over the wavelet tiles, not a periodogram.
freqMeanis the energy-weighted band, taken in the log-frequency the dyadic tiling is uniform in;freqMin/freqMaxare the support the surviving tiles cover, which is what the band overlap tests read;freqQ05/freqQ95are the band the energy occupies, which follows the signal rather than its faintest coefficient. There is no single peak frequency: a band an octave wide is what the transform can resolve, and naming one frequency inside it would state more than was measured.Events are clustered on the wavegram; the energy that measures is not the statistic that selects.
wdf.analysis.clustering.wavegram_eventspercolates over the time-frequency tiles of the surviving coefficients to decide which windows belong to the same transient. The event’s energy is then||x_hat||/sigmaover the stitched reconstruction of those windows, which counts each sample once and is what the event is worth. What ranks it for detection is its loudest block: a hard threshold admits every tile at a floor of2 ln Nin normalised energy, so a sum over tiles accumulates that floor in the noise as well as in the signal, while a maximum over blocks the search has already scored has a background that is a subset of the ungrouped one — the grouping reduces the trials factor without being able to lose a candidate.TriggerClusterer(DBSCAN or a greedy merge on the per-window scalar summaries) is kept as a cross-check.A pair is admitted on the events’ extents and ranked on their instants. The two events’ stretches of time have to meet once one may shift by the light travel time plus their own spreads: a transient longer than one window is assembled as several events and two detectors need not keep the same one, so gating on an instant would take evidence away from a candidate the detectors did assemble. The difference of the two events’ instants — the centre of the tile carrying each one’s largest coefficient, the order
wdf.analysis.robust_events.INSTANT_COLUMNSnames — then ranks the survivors. It is a property of one event, so a time slide carries it with the event and a background of millions of accidental pairs costs nothing per pair, which is what such a background requires. What that difference resolves is bounded by the tiling: a tile’s length is one over the upper edge of its own band, so where an event’s loudest coefficient sits low the tile is longer than the light travel time itself, andtSpreadis what declares that. It is a bound on what a pair’s timing is worth, not a bound on the search: nothing before it reads a time at all.Trigger output is Parquet, not CSV (
wdf.observers.SingleEventPrintFileObserver), written incrementally in row-group batches (flush_every, default 500 triggers) and finalized by aclose()call at the end ofsegmentProcess.wdf.analysis.io’s loaders accept both*.parquet(default) and*.csv(for older runs), dispatching on file extension.The downstream analysis layer (formerly the separate
wdfLibpackage) is merged in as thewdf.analysissubpackage: clustering, multi-detector coincidence (classical + GNN), background/false-alarm-probability, and ROC analysis. It has nopytsadependency and operates on plain pandas DataFrames / saved trigger files, so it works standalone.
Left behind deliberately (not used by wdfUnitDSWorker’s pipeline, not ported or audited):
AdaptiveWhitening, createsegmentsMinMax, CreateSegments, DownSamplingLF,
DownSampling, StateVectorSegments, wdf_reconstruct, wdfUnitBPDSWorker, wdfUnitWorker
(the last two share the pre-fix ExtraSize=0 whitening issue – worth the same fix if/when
ported), structures.ClusteredEvent (an empty data holder), structures.segment, utility.*.
Tutorials
tutorials/ holds four runnable notebooks. Everything in them is built in the notebook itself –
no data set to download, no frame file to point at:
01_the_statistic_and_the_parameters– one window of whitened data, the transform, thresholding, basis competition, and what the estimated parameters mean. Needspytsa.02_the_wavegram_and_long_signals– reading parameters off the time-frequency tiles, and recovering a signal that spans several analysis windows. Needspytsa.03_coincidence_and_significance– coincidence, time-slide background, false-alarm probability and ROC, using onlywdf.analysis. Nopytsarequired.04_reconstruction_and_phase– the event’s own coefficients inverted and stitched across windows, compared against the waveform that was injected, and the phase read sample by sample. Needspytsa.
Install
Requirements
Python 3.10 or newer. wdflow is not on an index: it installs from a checkout
of this repository, and everything it depends on except the compiled core comes
from PyPI.
git clone https://github.com/elenacuoco/wdflow
cd wdflow
pip install -e ".[all]"
Always installed — the analysis layer runs on these alone:
Package |
Used for |
|---|---|
|
arrays, signal processing |
|
trigger and event tables |
|
reading and writing the Parquet trigger files |
|
the AR and lattice-filter coefficient files |
|
DBSCAN, in |
|
the report figures |
Optional groups, each installed with pip install -e ".[name]":
Group |
Packages |
Needed for |
|---|---|---|
|
|
|
|
|
fetching public strain, e.g. from GWOSC |
|
|
trigger-generation logging |
|
|
|
|
|
running |
|
|
building the documentation |
|
|
the test suite |
|
all of the above |
Not from an index:
Requirement |
Needed for |
Where it comes from |
|---|---|---|
|
|
built from p4TSA |
a GWF backend for gwpy |
reading frame files through gwpy |
|
The compiled core
Trigger generation needs p4TSA, imported as pytsa. It is deliberately not
declared as a dependency of any extra: p4TSA has no PyPI distribution — FrameL
has no wheel — so the declaration could not resolve, and pip install pytsa
is a different project (an unrelated Python decorator library). Build it from
p4TSA instead, with its conda recipe or
pip install . from a checkout. p4TSA in turn needs GSL, FFTW3, FrameL, the
Boost.uBLAS headers and the Cereal headers, all on conda-forge.
If import pytsa behaves oddly, check what you actually have:
pip show p4tsa
python -c "import pytsa; print(pytsa.__file__)" # must be a compiled .so, not a .py
The legacy wdf package must not be installed alongside
wdflow’s importable package is wdf, the same top-level name the retired
wdf package used. Whichever of the two sits directly in site-packages wins,
and it shadows even an editable install of the other, silently:
python -c "import wdf; print(wdf.__file__)" # must point at your wdflow checkout
pip uninstall wdf # if it points into site-packages
What comes next
Parameter estimation by normalising flow, on the coefficient matrix an event already carries. The coefficients a trigger keeps are a fixed-length description of the transient on the time-frequency plane, already on the noise scale, so a flow can be conditioned on them directly without a spectrogram in between. That is where the question of what a transient is belongs – after the selection, never inside it – and it is the reason the search stores the coefficients rather than a summary of them.
Contributing
Changes reach master through pull requests only, and a pull request merges
only once CI is green. See CONTRIBUTING.md.
How to cite
Use of this code in published work requires citation of the following.
The Wavelet Detection Filter:
E. Cuoco, The Wavelet Detection Filter: a real-time un-modelled search for gravitational wave transients, ranking coincidences with a graph neural network, (2026), in preparation. VIR-0605A-26
WDFX:
E. Cuoco, M. Razzano, A. Utina, Wavelet-based classification of transient signals for gravitational wave detectors, 26th European Signal Processing Conference (EUSIPCO), 2648–2652 (2018). 10.23919/EUSIPCO.2018.8553393
Time-domain whitening, which the conditioning stage implements:
E. Cuoco et al., On-line power spectra identification and whitening for the noise in interferometric gravitational wave detectors, Class. Quantum Grav. 18, 1727 (2001). 10.1088/0264-9381/18/9/309
E. Cuoco et al., Noise parametric identification and whitening for LIGO 40-m interferometer data, Phys. Rev. D 64, 122002 (2001). 10.1103/PhysRevD.64.122002
CITATION.cff in this repository carries the same list in machine-readable
form, the pipeline paper marked in-preparation; GitHub’s Cite this
repository button reads it.
Use of generative AI
The Wavelet Detection Filter, the p4TSA core it runs on and the design of this
pipeline are the author’s own work. Claude (Opus 5, Anthropic), used through
Claude Code, wrote parts of the implementation, its tests and its documentation,
and produced the logo and stylesheet of the project page. Every such
contribution was reviewed and is covered by the test suite that runs in CI: the
golden-output fixture pins trigger generation end to end, so a generated change
that moves the numerics fails the build rather than passing silently.
Responsibility for the method and for everything published here rests with the
author.
Status
tests/ includes a golden-output regression fixture pinning trigger generation end to end on a
small synthetic frame (the legacy wdf package has none), plus the wdf.analysis test suite
(synthetic trigger data, no WDF run required):
pytest tests # needs pytsa for the golden fixture
pytest tests/test_clustering.py tests/test_coincidence.py tests/test_significance.py # no pytsa
tests/test_gnn.py needs the gnn extra and tests/test_mock_dataset.py needs pycbc; both are
skipped by deselecting them if those are not installed.