Skip to content

Execution and differentiation

Backend, device, precision, compilation, batching, and autodiff guidance.

Hardware acceleration and precision

The published FP64 benchmarks cover CPU and NVIDIA CUDA for both frontends. Other framework targets are not established by those results. PyTorch MPS does not support FP64; use CPU for the FP64 examples on Apple Silicon. Model operations and available precision determine device compatibility.

PyTorch Device & Precision

device = "cuda" if torch.cuda.is_available() else "cpu"

hp_f64, hc_f64 = torchwave.get_fd_waveform(
    approximant="IMRPhenomD",
    mass1=30.0, mass2=25.0,
    f_lower=20.0, f_final=512.0, delta_f=0.25,
    device=device,
    dtype=torch.float64,
)

JAX frontend and acceleration (jaxwave)

jaxwave provides tensor waveform models for JAX scientific computing and machine learning workflows. Transformation and device support depend on the selected path; for example, the Kerr angular eigensolver uses a host CPU callback.

Waveform Generation & WaveformGenerator

Generate waveforms using the direct functional interface or preconfigured generator:

import jaxwave

# Direct functional interface
hp, hc = jaxwave.get_fd_waveform(
    approximant="IMRPhenomD",
    mass1=30.0, mass2=25.0,
    chi1=0.2, chi2=-0.1,
    f_lower=20.0, f_final=512.0, delta_f=0.25,
)

# Class-based generator for repeated evaluations (supports automatic JIT caching)
gen = jaxwave.WaveformGenerator(
    approximant="IMRPhenomXAS",
    f_lower=20.0, f_final=512.0, delta_f=0.25,
    use_jit=True,
)
hp, hc = gen.generate_fd_waveform(mass1=30.0, mass2=25.0, chi1=0.1, chi2=0.0)

Just-In-Time Compilation (@jax.jit)

The following IMRPhenomD path supports JAX tracing and XLA compilation. Transformation support is model- and option-dependent; external-data callbacks and numerical solvers require separate checks:

import jax
import jaxwave

@jax.jit
def fast_waveform(m1, m2, s1z, s2z):
    return jaxwave.get_fd_waveform(
        approximant="IMRPhenomD",
        mass1=m1, mass2=m2, chi1=s1z, chi2=s2z,
        f_lower=20.0, f_final=512.0, delta_f=0.5,
    )

# First call compiles; compatible subsequent calls reuse the compiled function
hp, hc = fast_waveform(35.0, 25.0, 0.2, -0.1)

Vectorized Batch Evaluation (jax.vmap)

Map waveform generation across parameter grids without Python loops:

import jax
import jax.numpy as jnp
import jaxwave

m1_grid = jnp.array([20.0, 30.0, 40.0])
m2_grid = jnp.array([15.0, 25.0, 35.0])
s1_grid = jnp.array([0.1, 0.2, -0.3])
s2_grid = jnp.array([-0.1, 0.0, 0.2])

vmapped_fn = jax.vmap(fast_waveform, in_axes=(0, 0, 0, 0))
hp_batch, hc_batch = vmapped_fn(m1_grid, m2_grid, s1_grid, s2_grid)
print("JAX batched shape:", hp_batch.shape)  # (3, 985)

Autodiff Parameter Gradients (jax.grad)

Compute derivatives \(\partial h / \partial \theta\) of the implemented numerical model using automatic differentiation. Piecewise branches, interpolation, solver settings, and supported derivative orders still matter:

import jax
import jax.numpy as jnp

def waveform_power(m1, m2):
    hp, hc = fast_waveform(m1, m2, 0.2, -0.1)
    return jnp.sum(jnp.abs(hp) ** 2)

# Compute gradient vector [dPower/dm1, dPower/dm2]
grad_fn = jax.grad(waveform_power, argnums=(0, 1))
dm1, dm2 = grad_fn(30.0, 25.0)
print(f"Gradients: dm1={dm1:.4e}, dm2={dm2:.4e}")

Shape Bucketing for MCMC Sampling (BucketedWaveformEvaluator)

In MCMC sampling, parallel tempering, or ensemble walkers where batch sizes vary dynamically, standard @jax.jit functions recompile whenever a new batch size is encountered.

jaxwave.inference.BucketedWaveformEvaluator reduces the number of distinct batch shapes by padding inputs to discrete buckets, caching each bucket evaluator, and unpadding its outputs. A new bucket compiles on first use; changes to dtype or other traced shapes may also trigger compilation:

from jaxwave.inference import BucketedWaveformEvaluator
import jax.numpy as jnp

evaluator = BucketedWaveformEvaluator(
    approximant="IMRPhenomD",
    f_lower=20.0, f_final=512.0, delta_f=0.5,
    buckets=(1, 4, 8, 16, 32, 64, 128),
)

# Evaluating 13 walkers pads to bucket 16, using the cached JIT kernel
m1_walkers = jnp.array([30.0] * 13)
m2_walkers = jnp.array([25.0] * 13)
hp, hc = evaluator.evaluate(mass1=m1_walkers, mass2=m2_walkers)
print(f"Output walkers: {hp.shape[0]}")  # 13

Unified Dispatch via diffgw

Use diffgw.get_fd_waveform to select backends dynamically:

import diffgw

hp_t, hc_t = diffgw.get_fd_waveform("IMRPhenomD", mass1=30.0, mass2=25.0, backend="torch")
hp_j, hc_j = diffgw.get_fd_waveform("IMRPhenomD", mass1=30.0, mass2=25.0, backend="jax")

Automatic differentiation in PyTorch

import torch
import torchwave

m1 = torch.tensor(35.0, requires_grad=True, dtype=torch.float64)
m2 = torch.tensor(25.0, requires_grad=True, dtype=torch.float64)

hp, _ = torchwave.get_fd_waveform(
    approximant="IMRPhenomD",
    mass1=m1, mass2=m2,
    f_lower=20.0, f_final=256.0, delta_f=1.0,
)
loss = torch.sum(torch.abs(hp) ** 2)
loss.backward()

print("d(loss)/dm1:", m1.grad.item())
print("d(loss)/dm2:", m2.grad.item())