Skip to content

Quick Start

This guide walks you through the basic usage of nullcal.

1. Import the Package

import numpy as np
from nullcal.data import InterferometerData
from nullcal.null_stream.null_stream import NullStream
from nullcal.time_frequency_transform.wavelet_transforms import WaveletTransform

2. Set Up a Detector Network

nullcal is designed for closed-geometry networks. Load detector data from your strain source into the immutable array container. The convenience loader InterferometerData.from_interferometers(...) accepts detector objects with the corresponding attributes.

duration = 4.0
sampling_frequency = 4096.0
frequency_array = np.fft.rfftfreq(int(duration * sampling_frequency), 1 / sampling_frequency)
data = InterferometerData(
    psd=np.ones((3, frequency_array.size)),
    strain=np.zeros((3, frequency_array.size), dtype=complex),
    mask=np.broadcast_to(frequency_array >= 4.0, (3, frequency_array.size)).copy(),
    frequency_array=frequency_array,
    duration=duration,
    sampling_frequency=sampling_frequency,
    start_time=0.0,
    name=("ET1", "ET2", "ET3"),
)

3. Compute the Null Stream

wavelet_transform = WaveletTransform(
    duration=duration, sampling_frequency=sampling_frequency, nx=4, frequency_resolution=4
)
time_frequency_filter = np.ones(wavelet_transform.shape, dtype=bool)
null_stream = NullStream(
    interferometers=data,
    time_frequency_transform=wavelet_transform,
    time_frequency_filter=time_frequency_filter,
)
null_data = null_stream.compute_calibrated_frequency_domain_null_stream(calibration_factor=np.ones_like(data.strain))

The null stream is a data combination that cancels the gravitational-wave signal while preserving noise. In a perfectly calibrated network, the null stream contains only noise.

4. Constrain Calibration Errors

from nullcal.likelihood import RecalibrationLikelihood

likelihood = RecalibrationLikelihood(
    interferometers=data,
    knot_frequencies=np.geomspace(4.0, 2048.0, 10),
    time_frequency_filter=time_frequency_filter,
    wavelet_transform_frequency_resolution=4,
    wavelet_transform_nx=4,
)

params = {
    "amplitude": np.zeros((3, 10)),
    "phase": np.zeros((3, 10)),
}
log_posterior = likelihood.logdensity_fn(params)

logdensity_fn is a pure JAX function and can be passed directly to BlackJAX. Use nullcal.sampler.sample_nuts when the standard window-adapted NUTS run and R-hat, ESS, and divergence diagnostics are desired together.

5. Summarize and Apply a Calibration Posterior

from nullcal.calibration import posterior_median_calibration_factor

# result is the output of nullcal.sampler.sample_nuts(...).
frequencies = data.frequency_array[np.all(data.mask, axis=0)]
factor = posterior_median_calibration_factor(
    frequencies,
    likelihood.knot_frequencies,
    result.samples["amplitude"],
    result.samples["phase"],
)
corrected_strain = np.asarray(data.strain).copy()
corrected_strain[:, np.all(data.mask, axis=0)] /= np.asarray(factor)

The likelihood multiplies the whitened antenna response by the calibration factor. Correcting observed strain uses its inverse, as above. The point estimate evaluates the complex factor for every posterior sample at every frequency, then takes separate medians of its real and imaginary parts. Evaluating a spline from median knot values is a different estimator.

Next Steps