API reference¶
Likelihoods¶
Likelihoods for gapped/windowed stationary Gaussian data.
Four tiers, in decreasing order of fidelity and cost:
TimeDomainExact— the exact likelihood of the observed samples (marginalization of a stationary Gaussian over the gap samples = row/column deletion). No window appears anywhere. With two noise components entering linearly, one simultaneous diagonalization (“pencil”) gives an O(m) likelihood per evaluation with a closed-form determinant.FullCovariance— complex-normal likelihood of the windowed, band-restricted rfft data with the DENSE windowed covarianceSigma(lam) = 10^(lam_0) C_0 + 10^(lam_1) C_1; same two-component pencil trick, reduced to the rank retained by the gaps.DiagonalLikelihood(convolved) — Whittle-like likelihood with the EXACT diagonal of the windowed covariance (leakage-corrected PSD).DiagonalLikelihood(whittle) — the raw PSD on the band, optionally rescaled by the window powerW_2(“normalizing constant” approximation).
Common conventions¶
Noise parameters
lam = (lam_0, lam_1, ...): log10 deviations of the component POWERS (PSDs),Sigma(lam) = sum_k 10^(lam_k) C_k; truth at 0. (v0.1 used amplitude deviations,10^(2 lam): lam_new = 2 lam_old.)Residuals are multichannel, shape
(nch, .)— channels independent with identical covariances (e.g. LISA A and E).Frequency-domain classes expose
transform(x_td)mapping time series to the internally normalized data vector, andloglike_tdfor convenience;loglike(resid, lam)takes already-transformed residuals.Log-likelihoods contain constant offsets that depend on the internal normalization
s_ref; every DIFFERENCE of log-likelihoods (between parameter points, or between templates) is independent of it.
- class gaplike.likelihood.DiagonalLikelihood(variances)[source]¶
Bases:
_FDLikelihoodComplex-normal likelihood with a diagonal covariance model
var_j(lam) = sum_k 10^(lam_k) v_kjon the analysis band.- classmethod convolved(window, components, dt, f_lo, f_hi, s_ref=None)[source]¶
Exact leading diagonal of the windowed covariance, per component (O(n log n) each; no dense matrix is ever formed).
- class gaplike.likelihood.FullCovariance(components, rtol_rank=1e-08)[source]¶
Bases:
_FDLikelihoodDense-covariance complex-normal likelihood,
Sigma(lam) = 10^(lam_0) C_0 + 10^(lam_1) C_1(exactly two components), diagonalized ONCE in the rank retained by the gaps:eigendecomposition of
C_0 + C_1truncated atrtol_rankdiscards the singular directions annihilated by the gaps,C_1is whitened and the remaining symmetric pencil diagonalized, producing a fixed projectorTprojwithTproj C_0 Tproj^H = diag(mu),Tproj C_1 Tproj^H = I,every likelihood evaluation is then O(rank) per channel with a closed-form log-determinant.
Extending to K > 2 components (or nonlinearly-parametrized shapes) would require a fresh factorization per evaluation — use the convolved diagonal or the time-domain solver instead.
- classmethod from_window(window, components, dt, f_lo, f_hi, rtol_rank=1e-08, s_ref=None)[source]¶
Build the two banded windowed covariance matrices from a window and two PSD components (callables or two-sided grids), then diagonalize.
- property r¶
- class gaplike.likelihood.TimeDomainExact(mask, components, dt)[source]¶
Bases:
objectEXACT likelihood of gapped stationary data: the observed samples of a stationary Gaussian are jointly Gaussian with covariance
Sigma_OO(lam) = 10^(2 lam_0) C_0,OO + 10^(2 lam_1) C_1,OO— the stationary (circulant) covariance with the gap rows/columns deleted. No window or taper appears anywhere; scatter/width and model/true width ratios are 1 by construction.With two components entering linearly, one simultaneous diagonalization (cost O(m^3), once) reduces every likelihood evaluation to O(m) with a closed-form determinant — practical for m up to ~10^4 observed samples.
- Parameters:
- loglike(resid_obs, lam)[source]¶
resid_obs: (nch, m) real residuals on the observed samples. (Constant-nch m log(2 pi)/2omitted.)
Matrix-free solver¶
Matrix-free preconditioned conjugate gradients for gapped stationary data.
The exact likelihood of the observed samples of a stationary Gaussian needs quadratic forms in the inverse of
Sigma_OO = R Sigma R^T,
the stationary (circulant) covariance restricted to the observed samples
(rows/columns of the gaps deleted). Sigma_OO is neither circulant nor
Toeplitz, and at large N it cannot even be stored: with an 80% duty cycle
it reaches 5.5 GB at N = 2^15 and ~350 GB at N = 2^18.
Conjugate gradients sidesteps the matrix entirely. It solves
Sigma_OO u = b given only the action of Sigma_OO, which costs three
steps, two of them FFTs:
embed with zeros -> multiply by the circulant eigenvalues -> restrict.
The identity R Sigma R^T = Sigma_OO makes this exact, not approximate:
the zero-fill is the embedding R^T, never a statement about the data.
Convergence is governed by the conditioning after preconditioning with the
gap-free (Whittle) covariance — the same FFT pair, dividing by the
eigenvalues instead — which removes the dynamic range of the PSD and leaves
only the geometry of the gaps. Per iteration: four length-N FFTs.
Nothing here assumes anything about the noise model beyond stationarity —
in particular, none of the two-component (TM/OMS) structure that the
closed-form routes in gaplike.likelihood exploit. This is the general
route: it survives spline-parametrized spectra, extra components, anything
that defeats a simultaneous diagonalization.
The log-determinant is not provided here: pair the quadratic form with the
closed-form determinant of gaplike.likelihood.TimeDomainExact when
there are exactly two components (as in the companion paper), or with
stochastic Lanczos quadrature in general.
Conventions match gaplike.covariance.restricted_autocov(): the
circulant eigenvalues are eig = S2 / (2 dt) with S2 the two-sided PSD
on the full DFT grid, so gamma = Re ifft(eig) is the autocovariance.
- gaplike.cg.circulant_eigenvalues(components, n, dt)[source]¶
DFT eigenvalues of the stationary circulant covariance for a sum of PSD components (callables of
for two-sided grids):sum_k S2_k/(2 dt).
- gaplike.cg.sigma_oo(eig, obs)[source]¶
LinearOperatorapplyingSigma_OO = R Sigma R^Tmatrix-free: zero-embed, multiply byeigin Fourier space, restrict. Two FFTs.
- gaplike.cg.whittle_preconditioner(eig, obs, floor_rel=1e-06)[source]¶
Inverse of the gap-free (Whittle) covariance applied by the same FFT pair, as a preconditioner.
floor_relfloors the eigenvalues at a fraction of their maximum, taming transfer-function nulls (e.g. the TDI2 zeros) that the restricted operator does not share.
- gaplike.cg.solve(eig, obs, b, rtol=1e-08, x0=None, maxiter=None, floor_rel=1e-06, precondition=True)[source]¶
Solve
Sigma_OO u = bby (preconditioned) conjugate gradients.- Parameters:
eig (ndarray, shape (n,)) – Circulant eigenvalues of the full covariance.
obs (ndarray) – Observed-sample indices, or a boolean/gate mask over the full grid.
b (ndarray, shape (m,)) – Right-hand side, on the observed samples.
rtol (float, optional) – Relative residual tolerance
||Sigma_OO u - b|| / ||b||.x0 (ndarray, optional) – Warm start – e.g. the previously accepted solution in an MCMC, which typically cuts the iteration count by around 20%.
maxiter (int, optional) – Iteration cap;
Noneleaves it to SciPy.floor_rel (float, optional) – Preconditioner eigenvalue floor, as a fraction of the maximum.
precondition (bool, optional) – Set
Falseto run unpreconditioned – far slower, and useful mainly for measuring what the preconditioner buys.
- Returns:
u (ndarray, shape (m,)) – Solution of
Sigma_OO u = b.iterations (int) – Number of iterations taken.
- Raises:
RuntimeError – If the iteration does not reach
rtolwithinmaxiter.
- gaplike.cg.quad_form(eig, obs, b, **kw)[source]¶
b^T Sigma_OO^{-1} bby CG.bmay be(m,)or multichannel(nch, m)(channels independent, identical covariance — the forms are summed). Returns(value, [iterations per solve]).
- gaplike.cg.dense_restricted(eig, obs, block_bytes=200000000.0)[source]¶
Dense
Sigma_OObuilt from the autocovariance, in row blocks so the integer lag array never rivals the matrix itself. Reference/benchmark only: this is exactly the object the CG route exists to avoid.
- class gaplike.cg.RestrictedCG(mask, components, dt, rtol=1e-08, floor_rel=1e-06)[source]¶
Bases:
objectQuadratic forms in
Sigma_OO(lam)^{-1}for a component-parametrized spectrumSigma(lam) = sum_k 10^(lam_k) C_k, evaluated matrix-free.The companion of
gaplike.likelihood.TimeDomainExact: same model, same parametrization, any number of components, no O(m^3) setup and no O(m^2) storage — but no determinant (see the module docstring).- Parameters:
mask (ndarray, shape (n,)) – Observed-sample mask or gate; strictly-positive entries are observed.
components (sequence of callable or ndarray) – PSD components: callables of
f, or precomputed two-sided grids. Any number of them – unlike the pencil classes, which need exactly two.dt (float) – Sample cadence [s].
rtol (float, optional) – Relative residual tolerance passed to
solve().floor_rel (float, optional) – Preconditioner eigenvalue floor, as a fraction of the maximum.
Log-determinants¶
Stochastic Lanczos quadrature (SLQ) log-determinants for gapped data.
The exact time-domain likelihood needs, besides the quadratic form that
gaplike.cg provides matrix-free, the log-determinant
log|Sigma_OO(theta)|,
which changes with every noise-parameter update. With exactly two PSD
components the closed form of gaplike.likelihood.TimeDomainExact
applies; for anything richer — in particular the ratio-spline spectra this
module also provides — there is no closed form, and the dense route costs
O(m^3). This module supplies the general, matrix-free alternative:
logdet_slqHutchinson + Lanczos quadrature estimate oftr log Sigma_OO(Ubaru, Chen & Saad 2017).
logdet_diff_slqthe quantity an MCMC accept ratio actuallyneeds,
log|Sigma_OO(theta')| - log|Sigma_OO(theta)|, estimated with SHARED probe vectors so that the estimator sees only the (small) operator difference, not the two (large) absolute determinants.
logdet_denseCholesky ground truth, for validation at smallm(this is the object everything else exists to avoid).
circulant_logdetthe gap-free (Whittle) determinant in closedform — context lines and sanity checks.
ratio_splinea flexible PSD componentS_ref(f) * exp(sum_k c_k B_k(log10 f))that plugs intocirculant_eigenvalueslike any other component.
Probes are Rademacher: for a fixed symmetric operator D the estimator
variance is (2/M) * (||D||_F^2 - sum_i D_ii^2) — the diagonal does not
contribute. For the difference operator log Sigma(2) - log Sigma(1)
under a smooth spectral perturbation most of the energy sits on the
diagonal, which is why shared-probe differences can be dramatically better
than differencing two absolute estimates; measuring that gain on realistic
spectra is the point of paper/fig_det_flexible.py.
Everything is deterministic given rng; no state is kept between calls.
- gaplike.slq.lanczos_tridiag(matvec, v0, k, reorth=True)[source]¶
kLanczos steps fromv0with full reorthogonalisation.Returns
(alpha, beta, V)— diagonal, off-diagonal and the basis actually built (early breakdown truncates all three). Memory ism * kdoubles; at them,kof this study that is trivial and full reorthogonalisation keeps the Ritz values honest.
- gaplike.slq.quad_log(matvec, z, k, eig_floor=0.0)[source]¶
Lanczos-quadrature estimate of
z^T log(A) zfor SPDA.eig_floor > 0clips non-positive Ritz values (finite-precision guards near-singular operators); the number clipped is returned for monitoring.
- gaplike.slq.logdet_slq(eig, obs, n_probes, k, rng, eig_floor=0.0, batched=True, workers=-1, max_bytes=1000000000.0, reorth='none')[source]¶
SLQ estimate of
log|Sigma_OO|.- Parameters:
eig ((n,) circulant eigenvalues of the full covariance) – (
gaplike.cg.circulant_eigenvalues())obs (observed-sample indices, or mask/gate over the full grid)
n_probes (Rademacher probes and Lanczos steps per probe)
k (Rademacher probes and Lanczos steps per probe)
rng (
numpy.random.Generator)dict (Returns a)
estimates
scatter) (for empirical)
matvecs
clipped.
batched (batched=True (default) executes all probes together --- one)
step (real FFT per Lanczos)
identical (multithreaded via workers ---)
probes
mathematics (identical)
only. (floating-point-order differences)
chunked). (max_bytes caps the stored Lanczos bases (probes are)
- gaplike.slq.logdet_diff_slq(eig1, eig2, obs, n_probes, k, rng, shared=True, eig_floor=0.0, batched=True, workers=-1, max_bytes=1000000000.0, reorth='none')[source]¶
log|Sigma_OO(eig2)| - log|Sigma_OO(eig1)|by SLQ.shared=Trueuses the same Rademacher probes on both operators (the estimator of the difference operator);shared=Falsedraws fresh probes for each — the naive differencing of two absolute estimates, kept for comparison. Same return structure aslogdet_slq()withper_probeholding per-probe differences;batchedas there.
- gaplike.slq.logdet_dense(eig, obs)[source]¶
Ground truth
log|Sigma_OO|by dense Cholesky (O(m^3); smallmonly). Returns(logdet, seconds_spent_in_factorisation).
- gaplike.slq.circulant_logdet(eig, skip_nonpositive=False)[source]¶
Closed-form
log|Sigma|of the FULL (gap-free) circulant — the Whittle determinant. Reference/context only: restriction to the observed samples has no closed form (that is the whole point).NB the library’s grid convention zeroes the DC bin (mean removal), so eigenvalue grids built from callables contain an exact zero and the unrestricted determinant is
-infby design;skip_nonpositive=Truesums the strictly positive (in-band) bins instead, which is the Whittle practice.
- gaplike.slq.ratio_spline(S_ref, knots, coeffs, degree=3)[source]¶
Flexible PSD component
S_ref(f) * exp(spline(log10 f)).S_refis any PSD callable off;coeffsare the log-ratio B-spline coefficients onknots(fromspline_knots()). The spline argument is clipped to the knot span, so the ratio extrapolates flat — deliberately: tails behave like the reference.coeffs = 0reproducesS_refexactly. The return is a callable component accepted anywheregapliketakes PSD components.
- class gaplike.slq.ComplementFactor(eig, obs, dc_scale=0.001)[source]¶
Bases:
objectOne g x g Cholesky of
(Sigma_c^-1)_GGand everything it unlocks: the exact log-determinant (.logdet) and exact quadratic forms (quad_form()) ofSigma_OO.The library’s grid convention zeroes the DC bin, making the full circulant singular; the identity is therefore evaluated on a model with the DC eigenvalue set to
dc_scale * max(eig), and the exact rank-one downdate (matrix determinant lemma / Sherman–Morrison along the constant mode) removes it again EXACTLY — the floor is a conditioning choice, not an approximation. Any zeroed bin other than DC is not handled and raises.- Parameters:
eig ((n,) circulant eigenvalues of the full covariance)
obs (observed-sample indices, or mask/gate over the full grid)
dc_scale (DC floor as a fraction of the largest eigenvalue)
- gaplike.slq.logdet_complement(eig, obs, dc_scale=0.001)[source]¶
Exact
log|Sigma_OO|via the complement identity (seeComplementFactor); cost is one g x g Cholesky on the GAP samples.
Gap patterns, gates and windows¶
Gap patterns, gates and effective windows.
Conventions¶
A mask is a boolean array of length
n:True= sample observed,False= sample lost to a gap.A gate is a float array in
[0, 1]:0inside gaps, with optionally tapered (Planck) edges.The effective window is
gate * segment_window; it multiplies the time series before any Fourier transform. The exact time-domain likelihood (gaplike.likelihood.TimeDomainExact) uses the mask only — no window ever appears there.
All builders accept arbitrary patterns: explicit intervals, periodic combs, random (Poisson) gaps, or any user-supplied boolean mask.
- gaplike.gaps.mask_from_intervals(n, dt, gaps)[source]¶
Boolean mask with the samples inside each
(t_start, t_end)interval (seconds, gap = removed data) set toFalse.
- gaplike.gaps.periodic_mask(n, gap_samples, period_samples, offset=0)[source]¶
Rectangular comb:
gap_samplesconsecutive samples removed at the start of everyperiod_samplesblock (the paper’s ‘drastic gaps’).
- gaplike.gaps.random_mask(n, dt, rate_per_day, duration_s, duration_jitter_s=0.0, rng=None)[source]¶
Poisson-distributed gaps:
rate_per_dayexpected gaps per day, each ofduration_sseconds (Gaussian-jittered byduration_jitter_s).
- gaplike.gaps.window_power(w)[source]¶
Mean squared window,
W_2 = sum(w^2)/n— the normalizing constant of the scaled-Whittle (‘normalizing constant’) approximation.
- gaplike.gaps.gap_intervals(mask, dt)[source]¶
List of gap intervals
(t_start, t_end)in seconds from a mask.
- gaplike.gaps.gate_from_gaps(gaps, taper, n=None, dt=None, t=None)[source]¶
Gate with gaps given as
(t_center, duration)pairs and Planck-tapered edges extending overtaperOUTSIDE each gap.Times can be in any unit as long as
gaps,taperand the time grid agree: pass eithern, dt(gridt_k = k*dt) or an explicitt. Overlapping tapers multiply.
- gaplike.gaps.gate_from_mask(mask, dt, taper_s=0.0)[source]¶
Gate from an ARBITRARY boolean mask: 0 on removed samples, Planck ramp over
taper_sseconds as a function of the distance to the nearest removed sample (taper_s = 0gives the rectangular gate).
Covariances¶
Windowed frequency-domain covariances and restricted time-domain autocovariances of stationary noise.
Frequency-domain data convention¶
The banded data vector of a windowed real series x is
y_j = sqrt(2 dt / n) * rfft(w * x)_j , j in the analysis band,
so that E[y y^H] carries one-sided-PSD units and, for w = 1, its
diagonal is the PSD. full_covariance returns exactly E[y y^H] for
stationary noise of two-sided spectrum S2; convolved_diag returns its
exact diagonal on the full one-sided grid at O(n log n) cost.
- gaplike.covariance.band_indices(n, dt, f_lo, f_hi)[source]¶
Indices of the rfft bins strictly inside
(f_lo, f_hi).
- gaplike.covariance.transform(x_td, window, idx, dt)[source]¶
Banded, periodogram-normalized rfft of the windowed series (
x_td: shape(..., n)).
- gaplike.covariance.convolved_diag(window, S2)[source]¶
Exact diagonal of the windowed covariance on the one-sided grid: circular convolution of
|W|^2/n^2with the two-sided PSD, O(n log n).
- gaplike.covariance.full_covariance(window, S2, idx)[source]¶
Dense windowed covariance
E[y y^H]restricted to the rfft binsidx(band-restricted Algorithm 1 of Burke et al. 2025): the inner sum runs over the ENTIRE two-sided grid, so out-of-band leakage into the band is fully retained. CostO(len(idx)^2 n)via one matrix product; never forms ann x nobject.
Power spectral densities¶
Noise power spectral densities.
A noise model is a sequence of components: callables S_k(f) returning
the one-sided PSD of component k at its reference amplitude. The
modelled spectrum is
S(f; lam) = sum_k 10^(2 lam_k) S_k(f),
with lam_k the (dimensionless) log10 deviations of the component
amplitudes from their reference values — the noise parameters inferred by
every likelihood in gaplike.likelihood.
The built-in model is the two-component (test-mass + OMS) LISA TDI-2 A/E spectrum (SciRDv1 shapes). Any user-defined callables work the same way; components may also be passed to the likelihoods directly as precomputed grids.
- gaplike.psd.lisa_tdi2_ae(tm_asd=3e-15, oms_asd=1.5e-11, arm_m=2500000000.0, generation=2)[source]¶
The two A/E-channel PSD components of second-generation TDI.
Returns
{"tm": S_tm(f), "oms": S_oms(f)}— callables giving the channel PSD (fractional frequency) of each single-link noise, transferred through the TDI combination, at the reference amplitudestm_asd[m s^-2 Hz^-1/2] andoms_asd[m Hz^-1/2].
- gaplike.psd.two_sided_grid(S, n, dt)[source]¶
Two-sided PSD on the full DFT grid
fftfreq(n, dt)(0 at DC).
Simulation¶
Simulation of stationary Gaussian noise in the time domain.
- gaplike.simulate.noise_td(S_onesided, dt, rng, nch=1)[source]¶
(nch, n)independent stationary noise realizations with one-sided PSDS_onesidedon the rfft grid (lengthn//2 + 1,neven,S[0]ignored / DC-free).Exact circulant sampling: independent complex-normal rfft coefficients of variance
S * n / (4 dt)per real/imaginary part (real Nyquist bin), inverse-transformed to the time domain.
Waveforms¶
Waveform interfaces.
The package is waveform-agnostic: a Waveform wraps ANY callable
theta -> h~(f) returning the multichannel continuous-Fourier-transform
waveform on the rfft grid rfftfreq(n, dt) (numpy sign convention), shape
(nch, n//2 + 1). From there:
.td(theta)gives the physical time-domain template via the CFT->DFT ruleh = irfft(h~ / dt)(so Whittle SNRs match the continuous integral),any
gaplike.likelihoodobject consumes.tdoutput through itstransform/loglike_tdmethods.
An adapter for lisabeta massive-black-hole-binary TDI waveforms is provided
(lisabeta_mbhb_ae()); lisabeta is an optional dependency imported only
inside the factory.
- class gaplike.waveform.Waveform(fd_func, n, dt)[source]¶
Bases:
objectWrap a frequency-domain waveform callable.
- Parameters:
- gaplike.waveform.central_derivs(f, theta, steps)[source]¶
Generic central finite differences of
f(theta)(any array output): returns a list ofdf/dtheta_afor the parameters withsteps[a]not None; entries withsteps[a] is Noneare returned asNone(caller supplies analytic derivatives).
- gaplike.waveform.default_mbhb_params(x)[source]¶
Physical vector -> lisabeta parameter dict.
x = (Mtot, q, chi1, chi2, log10 dL[Gpc], iota, phi, lambda, beta, psi, tc_frac)with L-frame angles;tc_frac(element 10) is applied separately as a frequency-domain phase.
- gaplike.waveform.lisabeta_mbhb_ae(n, dt, f_lo, f_hi, wf_kw=None, to_params=None, tc_index=10)[source]¶
Waveformfor the (A, E) TDI channels of an MBHB, evaluated by lisabeta on the analysis band only and summed over all modes of the approximant (default IMRPhenomHM).tc_index: index of the coalescence-time parameter (as a fraction of the segment), applied as the FD phaseexp(-2 i pi f tc T)common to every mode;Nonedisables it.to_paramsmaps the parameter vector to a lisabeta dict (defaultdefault_mbhb_params()).