Tutorial 2: Forward-Mode AD & Fisher Information Matrix Analysis
This tutorial demonstrates how to compute derivatives of the implemented waveform with forward-mode automatic differentiation, construct the noise-weighted Fisher Information Matrix (FIM), and construct local uncertainty forecasts from its inverse.
[!TIP] A complete, runnable Python script companion for this tutorial is located at
examples/02_fisher_analysis.py.
1. Physical Formulation of the Fisher Matrix
For a gravitational-wave signal \(h(f; \theta)\) parameterized by \(\theta = \{\theta_1, \dots, \theta_P\}\) detected in Gaussian stationary noise with power spectral density \(S_n(f)\), the Fisher Information Matrix \(\Gamma\) is defined by:
Under the regularity assumptions of the Cramér-Rao inequality and for an invertible Fisher matrix, an unbiased estimator obeys the matrix bound
Using \(\Sigma_F=\Gamma^{-1}\) as a posterior covariance is a local Gaussian approximation, not an identity. Check conditioning, signal strength, parameter degeneracies, and priors before interpreting \(\sqrt{(\Sigma_F)_{ii}}\) as uncertainty. See Vallisneri (2008).
2. Setting Up Detector Noise & Parameters
We use the plus polarization, without a detector-network projection, for a binary black hole system with parameters \(\theta = \{m_1, m_2, \chi_{1z}, \chi_{2z}\}\) against the Advanced LIGO Zero-Detuned High-Power PSD:
import torch
import torchwave
from torchwave.inference.fisher import fisher_information_matrix as torch_fim, covariance_matrix as torch_cov
from torchwave.detectors import aLIGOZeroDetHighPower
flow, fhigh, n_freqs = 20.0, 512.0, 500
freqs = torch.linspace(flow, fhigh, n_freqs, dtype=torch.float64)
df = float(freqs[1] - freqs[0])
psd = aLIGOZeroDetHighPower(freqs)
params = {"mass1": 30.0, "mass2": 25.0, "chi1": 0.1, "chi2": -0.05}
param_order = ["mass1", "mass2", "chi1", "chi2"]
3. PyTorch Forward-Mode AD (fwAD)
torchwave.inference.fisher_information_matrix evaluates directional derivatives using PyTorch dual levels (torch.autograd.forward_ad), differentiating one parameter direction at a time:
def waveform_model(p):
return torchwave.get_fd_waveform(
"IMRPhenomD",
mass1=p["mass1"],
mass2=p["mass2"],
chi1=p["chi1"],
chi2=p["chi2"],
sample_frequencies=freqs,
)
gamma, param_names = torch_fim(
params, waveform_model, psd, df=df, parameter_order=param_order
)
cov = torch_cov(gamma)
sigmas = torch.sqrt(torch.diag(cov))
print("Matrix condition number kappa(Gamma):", torch.linalg.cond(gamma).item())
for i, name in enumerate(param_names):
print(f"sigma({name}): {sigmas[i].item():.4e}")
4. JAX Forward-Mode AD (jvp) & Cross-Framework Parity
In JAX, directional derivatives are computed using jax.jvp:
import numpy as np
import jax
import jax.numpy as jnp
jax.config.update("jax_enable_x64", True)
import jaxwave
from jaxwave.inference.fisher import fisher_information_matrix as jax_fim
from jaxwave.detectors import aLIGOZeroDetHighPower as jax_psd
freqs_j = jnp.linspace(flow, fhigh, n_freqs, dtype=jnp.float64)
psd_j = jax_psd(freqs_j)
def jax_waveform(p):
return jaxwave.get_fd_waveform(
"IMRPhenomD",
mass1=p["mass1"],
mass2=p["mass2"],
chi1=p["chi1"],
chi2=p["chi2"],
sample_frequencies=freqs_j,
)
gamma_jax, _ = jax_fim(
params, jax_waveform, psd_j, df=df, parameter_order=param_order, method="autodiff"
)
# Compare PyTorch and JAX Fisher matrices
gamma_np = gamma.detach().cpu().numpy()
gamma_jax_np = np.asarray(gamma_jax)
rel_diff = np.linalg.norm(gamma_np - gamma_jax_np) / np.linalg.norm(gamma_jax_np)
print(f"Relative matrix norm difference: {rel_diff:.4e}")
This comparison measures agreement for one model, grid, parameter set, and derivative calculation. It does not establish the accuracy of a posterior forecast or all waveform derivatives. The helper covariance_matrix floors small eigenvalues before inversion; uncertainty forecasts in regularized directions depend on that choice. The companion script checks a separate elementwise relative-error tolerance of 1e-10 for this example.