Simulating the detector noise¶
GWForge can generate a realisation of coloured Gaussian noise from a power spectral density, and it also generates strain data without any noise at all.
For example, here is a noise configuration file for XG detectors:
[IFOS]
detectors = ['CE20', 'CE40', 'ET']
sampling-frequency = 8192
noise = Gaussian
Which you can run as follows:
gwforge_noise --gps-start-time 1893024018 --gps-end-time 1893187858 --config-file xg.ini --output-directory data
This will generate roughly a day’s worth of data for an XG detector network at a sampling frequency of 8192Hz. The strain data will be a realisation of coloured Gaussian noise, and the noise power spectrum is read from the packaged curves in GWForge/ifo/noise_curves. The generated strain data is written as HDF5 to the output directory, one file per detector per chunk, named {IFO}/{IFO}-{GPS-START}-{DURATION}.h5, with channel names {IFO}:INJ.
You can inspect a file with gwpy:
from gwpy.timeseries import TimeSeries
data = TimeSeries.read('data/CE40/CE40-1893024018-4096.h5')
print(data.t0, data.dt, data.channel)
Alternatively, you can define the configuration file as follows:
[IFOS]
detectors = ['CE20', 'CE40', 'ET']
sampling-frequency = 8192
noise = Gaussian
gps-start-time = 1893024018
duration = 86400
And remove the gps option.
For zero noise, set noise = zero. Both noise types are produced by the same code path, so zero noise is available for every detector including ET.
Finally, you can supply your own noise curves:
[IFOS]
detectors = ['H1', 'L1', 'V1']
psd-dict = {'H1' : 'H1.txt', 'L1': 'L1.txt', 'V1':'V1.txt'}
sampling-frequency = 8192
noise = Gaussian
gps-start-time = 1893024018
duration = 86400
How the noise is generated¶
Noise comes from GWForge.ifo.reproducible_noise, which depends on nothing beyond numpy. It is a faithful reimplementation of pycbc.noise.reproduceable, and tests/test_reproducible_noise.py asserts the two agree to max|dx| / rms < 1e-10 — the residual is FFT round-off, so the outputs are numerically identical, not merely statistically equivalent.
The property this preserves is the one the workflow depends on: the realisation is keyed on (seed, absolute GPS time), not on the requested segment. White noise is drawn in fixed 1638400-sample blocks aligned to GPS zero, so two 4096 s chunks generated by independent Condor jobs — which never communicate — concatenate into exactly the stretch a single long call would have produced.
Warning
BLOCK_SAMPLES must never change. It is the quantum that makes the noise reproducible across disjoint spans; changing it silently re-rolls every dataset GWForge has ever produced.
Note
The white noise stitches across chunks exactly, but the coloured noise does not: colouring is a circular convolution over the whole padded span, so asking for 4096 s and asking for 8192 s give different (statistically equivalent) realisations over the overlap, differing by ~1e-3 of the RMS at the default filter length. PyCBC behaves the same way. This is why GWForge.utils.split_duration pins a fixed 4096 s chunk length — holding the chunking fixed is what makes a generated dataset reproducible.
Optional settings¶
Key |
Meaning |
|---|---|
|
|
|
Dict of per-detector seeds, e.g. |
|
Length in seconds of the colouring filter [Default: 128]. The noise is generated this much longer on each side and then cropped, so filter wrap-around never reaches the returned data. |
|
Low-frequency cutoff in Hz. Defaults per detector to its own |
Deprecated since version 0.1: noise-type (which used to select a bilby or pycbc backend) and fft-scheme are ignored. GWForge now generates all noise itself, and numpy’s FFT has no size limit to work around. Both keys log a warning and are otherwise harmless.
Overriding a detector’s noise curve¶
Detector tokens carry a sensitivity with them. CE40, CE20 and ET come from
GWForge’s own .ifo files; everything else falls through to bilby, and bilby’s
choices are not always the ones a study wants. In particular, H1 and L1
carry aLIGO_O4_high_asd.txt and V1 carries AdV_psd.txt — of the 2G
detectors only A1 (LIGO-India) is A+ out of the box.
gwforge_optimal_snr --psd-dict replaces the curve without touching the
geometry:
gwforge_optimal_snr --injection-file bbh_population.h5 \
--output-file bbh_snr_aplus.h5 --ifos H1 L1 A1 \
--psd-dict "{'H1': 'GWForge/ifo/noise_curves/aplus.txt',
'L1': 'GWForge/ifo/noise_curves/aplus.txt',
'A1': 'GWForge/ifo/noise_curves/aplus.txt'}" \
--waveform-approximant IMRPhenomXPHM \
--minimum-frequency 20 --sampling-frequency 4096
gwforge_fisher takes the same thing as [IFOS] psd-dict in its ini. Both
route to GWForge.ifo.detectors.Network, which decides ASD versus PSD
from the magnitude of the array, so either file type works and no flag says
which. Detectors left out of the dict keep their shipped curve.
Important
--psd-dict sets the noise curve only. The integration band still comes
from the interferometer’s own minimum_frequency and maximum_frequency, which
--minimum-frequency does not change — that flag sets where the waveform
starts. So --minimum-frequency 5 against a detector whose .ifo says
minimum_frequency = 6 still yields an SNR integrated from 6 Hz.
A quick way to confirm an override took: run a handful of events with and without it. A no-op leaves the SNRs bit-identical, and swapping A+ for O4-high moves H1 and L1 by a factor of about two.