Skip to content

Continuous waves

Typical gravitational-wave sources in this package are transients: signals placed at a single event time generated once, projected to a detector and injected into a segment. A continuous wave (CW) is a signal from a rotating, non-axisymmetric isolated neutron star. It is on for the whole observation, which is typically months, and it is quasi-monochromatic rather than chirping.

The simulator produces a time-domain CW strain signal \(h\), that is already projected onto a detector frame: \(h = F_+ \cdot h_+ + F_\times \cdot h_\times\).

API reference

Use Continuous waves API for the signatures, types, and raised exceptions. This page contains copy-paste examples and the reasoning behind the constraints.

Optional JAX dependency

ContinuousWaveSimulator needs the [jax] extra as the polarizations are produced with rippleGW, and the default detector projection backend runs on JAX (pip install 'gwmock-signal[jax]').

See Installation for details.

Import

Unlike CBCSimulator or GWSimulator, which are exported from the top-level package, ContinuousWaveSimulator is reached through the CW submodule:

from gwmock_signal.continuous import ContinuousWaveSimulator

Ephemeris files

Barycentring of the signal needs an Earth and a Sun ephemeris table.

These should be provided as explicit paths to the files. If a path does not exist locally but is a standard LALPulsar name (e.g. 'earth00-40-DE405.dat.gz'), the tables will be downloaded and cached (Internet access is required once). The run records what it used.

sim = ContinuousWaveSimulator(
    earth_ephemeris="earth00-40-DE405.dat.gz", sun_ephemeris="sun00-40-DE405.dat.gz", reference_time_ssb=1470600018.0
)

The reference epoch

reference_time_ssb is a solar-system barycentre (SSB) reference epoch for the source parameters, provided in seconds.

It is a required parameter, and it is fixed for the whole run: the same value is reused across every ContinuousWaveSimulator.simulate(...) call, which is what keeps the segments coherent with each other. rippleGW's own default derives the SSB reference epoch from the first sample it is given. As it is called once per segment, that default silently restarts the phase at every segment boundary. Each segment on its own still looks like a perfectly good CW signal; only a multi-segment run would disagree with itself.

Projection backend

"jax" is the default projection backend, as the projection is roughly 99% of a segment's cost and rippleGW already brings JAX in for the polarizations, so the device path is essentially free to default to. However, it comes with one limit: this backend linearly extrapolates sidereal time across the span it is given, and refuses a span longer than 86400 s.

That limit applies to the padded span, not to the nominal segment. The projection is given a buffer padded at both ends (see below), so a segment whose padded span exceeds 86400 s is refused even when its nominal length is a little under. To stay on the device path, keep the nominal segment below 86400 s - 2 x padding. If you need a longer single segment, pass "numpy" instead:

sim = ContinuousWaveSimulator(
    earth_ephemeris="earth00-40-DE405.dat.gz",
    sun_ephemeris="sun00-40-DE405.dat.gz",
    reference_time_ssb=1470600018.0,
    projection_backend="numpy",
)

Note that the span a segment measures is padded (edge_padding): the resampling kernel's taps reach past the end of the buffer, and for a continuous wave the signal continues into the neighbouring segment rather than stopping there. So each segment is generated with extra samples at both ends and trimmed back afterwards, and the usable background is shorter than 86400 s by that padding. edge_padding returns the padding in samples, so its duration in seconds scales inversely with the sample rate. With the default resampling kernel:

Sample rate Padding per end Usable segment shortened by
32 Hz 2.06 s 4.12 s
64 Hz 1.05 s 2.09 s
128 Hz 0.53 s 1.06 s
256 Hz 0.28 s 0.55 s
512 Hz 0.15 s 0.30 s

These are the values at the sample rates the examples use. Read the padding for your own rate from edge_padding(sampling_frequency), and treat a nominal segment of 86400 s - 2 x padding as the longest one the JAX backend accepts.

Accepted parameters

When using ContinuousWaveSimulator.simulate(...), the following parameter keys are accepted in the params argument. The six listed as required must be present, or ValueError names the ones that are missing; extra keys are ignored rather than rejected, so a typo in a key name passes silently.

Canonical name Default Description
right_ascension required Source's right ascension (rad)
declination required Source's declination (rad)
frequency required Wave frequency (Hz)
initial_phase required Signal's initial phase (rad)
amplitude_plus required CW plus polarization amplitude (strain)
amplitude_cross required CW cross polarization amplitude (strain)
polarization_angle 0.0 Polarization angle (rad); optional

Note that amplitude_plus/amplitude_cross are taken directly, not derived from h0/cos_iota. If your source parameters are in the constant amplitude/inclination convention, convert to plus/cross amplitudes before calling.

polarization_angle is applied when the geocentre polarizations are projected onto a detector, which is the only place it enters: rippleGW's pulsar generator takes no polarization angle, because it fixes its own polarization basis internally, and this simulator passes it the amplitudes directly.

Spindowns are not a params key. They are passed to ContinuousWaveSimulator as the spindowns constructor argument, together with the reference epoch and the ephemeris tables, so that they are fixed for the whole run alongside them.

Example 1 — one segment in a detector network

import numpy as np
from gwpy.timeseries import TimeSeries

from gwmock_signal.continuous import ContinuousWaveSimulator

fs = 64.0
epoch = 1470600018.0
segment_seconds = 1800.0
n = int(segment_seconds * fs)

sim = ContinuousWaveSimulator(
    earth_ephemeris="earth00-40-DE405.dat.gz",
    sun_ephemeris="sun00-40-DE405.dat.gz",
    reference_time_ssb=epoch,
    spindowns=(-1.0e-10,),
)

params = {
    "right_ascension": 1.1,
    "declination": 0.3,
    "frequency": 20.0,
    "initial_phase": 0.4,
    "amplitude_plus": 1.0e-24,
    "amplitude_cross": 7.0e-25,
    "polarization_angle": 0.2,  # optional; defaults to 0.0
}

detectors = ["H1", "L1", "V1"]
background = {name: TimeSeries(np.zeros(n), t0=epoch, sample_rate=fs, unit="strain") for name in detectors}

strains = sim.simulate(
    params=params,
    detector_names=detectors,
    background=background,
    sampling_frequency=fs,
    minimum_frequency=0.0,  # required by the shared simulator signature; unused for CW
)

for name in strains.detector_names:
    rms = float(np.sqrt(np.mean(strains[name].value ** 2)))
    print(f"{name}: rms={rms:.4e}")

Swap the background parameter strain with your own noise data to inject the CW signal into it. See Strain injection for more information.

Example 2 — a coherent multi-segment run

A multi-segment run reuses the same simulator instance (that is, the same reference_time_ssb and the same ephemeris tables) across consecutive segments. The concatenated output then reproduces the coherent signal that a single long call would have produced.

detectors = ["V1"]
segments_count = 3
strains = []

for index in range(segments_count):
    seg_epoch = epoch + segment_seconds * index
    segment_background = {"V1": TimeSeries(np.zeros(n), t0=seg_epoch, sample_rate=fs, unit="strain")}

    seg_strain = sim.simulate(
        params,
        detectors,
        segment_background,
        sampling_frequency=fs,
        minimum_frequency=0.0,
        earth_rotation=True,
    )
    strains.append(seg_strain["V1"].value)

multisegment = np.concatenate(strains)

Constraints and errors

Situation Behavior
background missing or empty ValueError. A continuous wave has no duration of its own, so the segment's epoch and length come from the data it is added to.
earth_rotation=False ValueError. Not available. Holding the antenna pattern fixed is only a valid approximation for signals short enough that Earth barely turns across them. A CW run spans months, so this is refused rather than silently producing a plausible-looking wrong answer.
reference_time_ssb not finite ValueError. Must be a finite GPS-scale time in seconds.
reference_time_ssb ≥ ephemeris table time validity Caution for callers. The package does not emit a warning, but this may affect precision at the arcsec level.
frequency ≤ 0 or ≥ Nyquist ValueError. Frequency must be positive and below Nyquist frequency (½ of sampling rate). Otherwise it would alias.
Any required parameter is not finite ValueError, naming the key.
sampling_frequency ≤ 0 ValueError. The sample rate must be positive.
sampling_frequency differs from the background's ValueError. Otherwise the signal is generated on one time grid and added to another, stretching it by the ratio.
detector_names is empty ValueError.
A detector in detector_names has no background channel KeyError.
A background value is not a GWpy TimeSeries TypeError.
Background channels disagree on epoch, length, or sample rate ValueError. Background channels for all detectors must share these parameters.
The JAX backend is given a padded span over 86400 s ValueError naming the span; project in shorter segments or pass projection_backend="numpy".
Polarizations come back non-finite RuntimeError. Usually an old rippleGW (<0.3.1 returns NaN at the geocentre for every sample), or an extreme spindown/reference-epoch combination overflowing the phase. The spindown term grows as the square of the time from reference_time_ssb.

See also