Fiducial PSDs & Recolouring PSDs

Sage needs two flavours of PSD on disk before training can start:

  • Segment PSDs — one PSD per downloaded noise segment, used for per-segment whitening before recolouring.

  • Raw (recolouring) PSDs — a large collection of PSDs estimated from random draws of the noise data; used to resample the spectral shape of a noise batch during training. The median of these forms the fiducial detector PSD used by the whitener.

Both are produced by EstimatePSD.

Setup

from sage.data.primer import EstimatePSD, NoBlackout
from sage.data.noise import MemmapSingleNoiseSampler
from sage.dsp.welch import TorchWelch
from sage.data.psd import smoothing

detector = "H1"

torch_welch = TorchWelch(
    delta_t=1 / 2048,
    seg_len=int(2048.0 * 4),       # 4-second Welch segments
    seg_stride=int(2048.0 * 2),    # 50% overlap
    avg_method="median",
)

psd_smoothener = smoothing.LogSplineSmoothing(
    smooth_factor=None,
    noise_low_frequency_cutoff=data_cfg.noise_low_frequency_cutoff,
)

epsd = EstimatePSD(
    detector=detector,
    apply_inverse_spectrum_truncation=False,
    max_filter_len=int(round(2048.0 * 2)),
    low_frequency_cutoff=15.0,
    trunc_method="hann",
    psd_method=torch_welch,
    num_samples=250_000,
    store_psds_as_bin=True,
    blackout_policy=NoBlackout(),
    interpolate_psd=True,
    training_sample_length=int(2048.0 * 16),
    psd_smoothener=psd_smoothener,
)

Estimating segment PSDs

This step computes one PSD per mini-segment in the .bin file. These are used for per-segment whitening before any recolouring is applied.

epsd.estimate_segment_psds(
    noise_segments_file=f"./data_release/data_{detector}_O3b.bin"
)
# Computing PSDs per segment: 100%|██| 20286/20286

Output files are written under data_cfg.data_dir / "segment_psds"/:

data_H1_O3b_psds.bin           ← concatenated float32 PSD array
data_H1_O3b_psds_segments.json ← per-segment metadata (length, GPS times, …)

Estimating raw (recolouring) PSDs

This step draws num_samples random noise windows from the data, estimates a PSD for each, and saves the full collection. The median of this collection becomes the fiducial detector PSD stored separately.

noise_sampler = MemmapSingleNoiseSampler(
    f"./data_release/data_{detector}_O3b.bin",
    return_tensor=True,
)

epsd.estimate_raw_psds(
    noise_sampler=noise_sampler,
    duration=int(round(2048.0 * 16)),
)

Note

Estimating 250 000 PSDs is I/O-bound and takes several hours. Run it once and cache the output; it does not need to be repeated unless the noise data changes.

Output files are written under data_cfg.data_dir / "recolour_psds"/:

raw_H1_psds.bin    ← (num_psds × num_freq_bins) float32 array
raw_H1_psds.json   ← metadata: num_psds, num_freq_bins, delta_f, …

Reading PSD files

Recolouring PSDs:

import json
import numpy as np
import torch
from pathlib import Path

psd_dir = Path(data_cfg.data_dir) / "recolour_psds"
meta = json.loads((psd_dir / "raw_H1_psds.json").read_text())

psds = np.fromfile(
    psd_dir / "raw_H1_psds.bin", dtype=np.float32
).reshape(meta["num_psds"], meta["num_freq_bins"])

recolour_psds = torch.from_numpy(psds)

Segment PSDs:

psd_dir = Path(data_cfg.data_dir) / "segment_psds"
meta = json.loads((psd_dir / "data_H1_O3b_psds_segments.json").read_text())

raw = np.fromfile(psd_dir / "data_H1_O3b_psds.bin", dtype=np.float32)

psds, cursor = [], 0
for m in meta:
    n = m["psd_len"]
    psds.append(raw[cursor : cursor + n])
    cursor += n

segment_psds = np.stack(psds)   # (N_seg, F)

Spline smoothing

The raw PSDs contain spectral lines (power-line harmonics, violin modes, etc.). LogSplineSmoothing fits a log-cubic spline to each PSD to suppress these without distorting the broadband shape.

freqs = np.arange(meta["num_freq_bins"]) * (1.0 / 16.0)

smoothener = smoothing.LogSplineSmoothing(
    smooth_factor=0.005 * len(freqs),
    noise_low_frequency_cutoff=data_cfg.noise_low_frequency_cutoff,
)

for psd in recolour_psds:
    psd_np = psd.detach().cpu().numpy().copy()
    smoothed = smoothener.smooth(freqs, psd_np)

Pass smooth_factor=None to use an automatic estimate based on the number of frequency bins.