Skip to content

nullcal

Python CI Documentation Status codecov PyPI Version Python Versions License: MIT Ruff DOI

A Python package for constraining calibration errors of a closed-geometry network of gravitational-wave detectors. It provides the null stream formalism for noise-only data combinations and Bayesian recalibration likelihoods to quantify detector miscalibration.

Features

  • Null stream construction for closed-geometry detector networks
  • Calibration error constraint via Bayesian recalibration likelihood
  • Time-frequency transforms (Short-Time Fourier Transform, wavelet transforms)
  • Clustering algorithms for time-frequency map analysis
  • Modular architecture with clean separation between null stream, likelihood, calibration, immutable data, result, and utility layers

Installation

We recommend using uv to manage virtual environments for installing nullcal.

If you don't have uv installed, you can install it with pip. See the project pages for more details:

  • Install via pip: pip install --upgrade pip && pip install uv
  • Project pages: uv on PyPI | uv on GitHub
  • Full documentation and usage guide: uv docs

Note: The package requires Python 3.12 or later and is built and tested against Python 3.12–3.14. When creating a virtual environment with uv, specify the Python version to ensure compatibility: uv venv --python 3.12 (replace 3.12 with your preferred supported version: 3.12, 3.13, or 3.14). This avoids potential issues with unsupported Python versions.

From PyPI

# Create a virtual environment (recommended with uv)
uv venv --python 3.12
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv pip install nullcal

From Source

git clone git@github.com:Leuven-Gravity-Institute/nullcal.git
cd nullcal
# Create a virtual environment (recommended with uv)
uv venv --python 3.12
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
uv sync --extra jax

Quick Start

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

# Load arrays from your strain source. The loader also accepts detector objects
# exposing these fields via InterferometerData.from_interferometers(...).
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"),
)

# 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))

# Set up a recalibration likelihood for calibration error constraints
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)

Development

Pre-commit hooks

Install prek (a wrapper around pre-commit):

uv run prek install

Run all hooks against all files:

uv run prek run --all-files

Documentation

Full documentation is available at https://leuven-gravity-institute.github.io/nullcal/.

Contributing

Contributions are welcome!

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. Submit a pull request

Release Schedule

Releases follow a fixed schedule: every Tuesday at 00:00 UTC, unless an emergent bugfix is required. This ensures predictable updates while allowing flexibility for critical issues. Users can view upcoming changes in the draft release on the GitHub Releases page.

Testing

Run the test suite:

uv run pytest

License

This project is licensed under the MIT License. See the LICENSE file for the full license text.

Support

For questions or issues, please open an issue on GitHub or contact the maintainers.