Skip to content

Extend spectraldiff to irregular grids via non-uniform FFT #201

Description

@pavelkomarov

Motivation

spectraldiff currently requires uniformly sampled data — it uses np.fft internally, which assumes equal spacing. Many real datasets are irregularly sampled (dropped frames, variable-rate sensors, merged data sources), and the other methods that support variable step size (polydiff, splinediff, rbfdiff, rtsdiff, robustdiff) are all in the local/basis-fit family. There is currently no global spectral method that handles irregular grids.

Approach

The Non-Uniform FFT (NUFFT) generalizes the FFT to arbitrary sample locations at near-FFT cost, $O(N \log N)$. The standard spectral differentiation pipeline maps cleanly onto it:

  1. Type 1 NUFFT: scatter irregular samples ${(t_i, x_i)}$ onto a uniform frequency grid $\hat{X}_k$
  2. Multiply by $ik$: spectral derivative operator, same as in the uniform case
  3. Type 2 NUFFT: evaluate the differentiated spectrum back at the original (irregular) sample locations

The recommended backend is finufft (Flatiron Institute), which is actively maintained, pip-installable, and has clean Python bindings:

pip install finufft

Sketch

import numpy as np
import finufft

def spectraldiff_nufft(x, t, high_freq_cutoff=1.0):
    """Spectral differentiation for irregularly-sampled data via NUFFT.

    :param np.array x: signal values at sample locations t
    :param np.array t: sample locations (not necessarily uniform)
    :param float high_freq_cutoff: fraction of Nyquist to retain (0, 1]
    :return: x_hat, dxdt_hat
    """
    N = len(x)
    T = t[-1] - t[0]  # total duration

    # Rescale t to [-pi, pi] as required by finufft
    t_scaled = 2 * np.pi * (t - t[0]) / T - np.pi

    # Type 1: nonuniform -> uniform Fourier coefficients
    X = finufft.nufft1d1(t_scaled, x.astype(complex), N)

    # Wavenumbers
    k = np.fft.fftfreq(N, d=1.0/N)  # integers 0..N/2-1, -N/2..-1

    # High-frequency cutoff (denoising)
    cutoff = int(high_freq_cutoff * N / 2)
    filt = np.ones(N)
    filt[cutoff:-cutoff] = 0

    # Spectral derivative: multiply by ik * (2pi/T)
    omega = 2 * np.pi / T
    X_smooth = filt * X
    X_deriv  = filt * (1j * k * omega) * X

    # Type 2: uniform Fourier coefficients -> values at original (nonuniform) points
    x_hat    = np.real(finufft.nufft1d2(t_scaled, X_smooth)) / N
    dxdt_hat = np.real(finufft.nufft1d2(t_scaled, X_deriv))  / N

    return x_hat, dxdt_hat

Integration notes

  • The existing spectraldiff signature already accepts dt_or_t; the irregular-grid path would activate when an array is passed, keeping the interface identical.
  • finufft would be an optional dependency (like cvxpy for tvrdiff/robustdiff), imported lazily with a clear error message if absent.
  • spectraldiff would gain a checkmark in the Variable step column of Table 1.

Caveats

  • NUFFT assumes the signal is approximately bandlimited. Heavily non-uniform sampling can alias; the high_freq_cutoff parameter helps but users should be aware.
  • Edge handling: the current uniform implementation uses even extension to suppress Gibbs-like artifacts; the NUFFT path may need an analogous treatment.

— Claude Sonnet 4.6

Metadata

Metadata

Assignees

Labels

enhancementNew feature or improvement

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions