Working with Waveforms¤
ripple provides a set of gravitational-wave waveform models, each identified by a registered name and constructed via ripplegw.waveform(name, **config).
Every model takes an evaluation grid (a frequency or time array) and a dictionary of parameters, and returns the gravitational-wave polarizations as JAX arrays — so every waveform is differentiable, JIT-compilable, and vectorisable.
See JAX Transformations for jax.grad / jax.jit / jax.vmap in depth.
Discovering waveforms¤
import ripplegw
ripplegw.list_waveforms() # every registered model name
ripplegw.list_waveforms(domain="FD") # filter by metadata
ripplegw.list_waveforms(is_precessing=True)
ripplegw.get_waveform_metadata("IMRPhenomD") # {"domain": "FD", "is_tidal": False, "is_precessing": False}
See the Waveform Catalogue for the full list with each waveform's domain and capabilities.
Note
An unrecognized filter key doesn't raise — it just matches nothing.
If a filter returns fewer names than you expect, check get_waveform_metadata(...) on a known model to see the real key names.
Constructing a waveform¤
waveform = ripplegw.waveform("IMRPhenomD", f_ref=20.0)
ripplegw.waveform(name, **config) is the only construction path.
**config is forwarded to the model's constructor; for most models this is just f_ref, the reference frequency in Hz.
name is positional-only
ripplegw.waveform(name="IMRPhenomD") raises TypeError — always pass the name as the first positional argument, ripplegw.waveform("IMRPhenomD", ...).
Evaluating a waveform¤
import jax
import jax.numpy as jnp
jax.config.update("jax_enable_x64", True) # see JAX Transformations for why this matters
frequency = jnp.arange(20.0, 1024.0, 0.25)
params = {
"M_c": 28.3, # chirp mass [solar masses]
"eta": 0.247, # symmetric mass ratio
"s1_z": 0.0, # primary aligned spin
"s2_z": 0.0, # secondary aligned spin
"d_L": 440.0, # luminosity distance [Mpc]
"phase_c": 0.0,
"iota": 0.0,
}
polarizations = waveform(frequency, params)
hp, hc = polarizations["p"], polarizations["c"]
Every model returns a dict with exactly two keys: "p" (plus polarization, \(h_+\)) and "c" (cross polarization, \(h_\times\)), each the same length as the input axis.
Frequency-domain models return complex arrays; time-domain models return real arrays. ripple never performs an FFT internally: a frequency-domain model builds its strain analytically as \(A(f) e^{i\psi(f)}\).
See Parameters and Conventions for what each parameter name means and its units.
Visualising the spectrum¤
import matplotlib.pyplot as plt
plt.loglog(frequency, jnp.abs(hp), label=r"$|h_+|$")
plt.loglog(frequency, jnp.abs(hc), label=r"$|h_\times|$", ls="--")
plt.xlabel("Frequency [Hz]")
plt.ylabel("Strain amplitude")
plt.legend()
plt.show()
Amplitude and phase¤
For single-mode, aligned-spin models — where an amplitude and phase as a function of frequency are individually well-defined — you can evaluate them separately:
wf = ripplegw.waveform("IMRPhenomD", f_ref=20.0)
amp = wf.amplitude(frequency, params) # includes distance scaling
phase = wf.phase(frequency, params) # the exponent in exp(1j * phase)
strain = wf.strain(frequency, params) # == amp * exp(1j * phase), the pre-polarization strain
amplitude(f, p) * exp(1j * phase(f, p)) reproduces strain(f, p) exactly; __call__ then applies the inclination-dependent plus/cross prefactors on top of strain to build hp/hc.
Not every model supports this — check directly via isinstance(wf, ripplegw.AmplitudePhaseWaveform).
Evaluating at a fixed distance¤
Any model whose parameters include d_L can be evaluated at 1 Mpc directly, ignoring whatever d_L is in params:
h_at_1mpc = wf.at_unit_distance(frequency, params)
This is exact by construction — at_unit_distance(axis, params) == wf(axis, {**params, "d_L": 1.0}).
Check availability the same way: isinstance(wf, ripplegw.DistanceScaledWaveform).
Switching between models¤
All waveform models share the same calling interface, so swapping models only requires changing the registered name:
for name in ["IMRPhenomD", "IMRPhenomXAS"]:
h = ripplegw.waveform(name)(frequency, params)
print(f"{name}: max|h+| = {jnp.max(jnp.abs(h['p'])):.3e}")
This only works cleanly when the models share parameter_names (e.g. precessing models need s1_x/s1_y in addition to s1_z, and tidal models need lambda_1/lambda_2).