FFT filtering

Added in version 2.0.0.

Important

The brick-wall FFT filter is available only when MembraneCurvature runs with surface_method='binning'.

Note that per-frame arrays are not FFT-filtered!

For average maps, MembraneCurvature applies the brick-wall filter once to the averaged z_surface and computes mean and Gaussian curvature on that filtered average.

For binning, filtering defaults to fft_filter='auto'. Pass fft_filter=None to disable FFT filtering.

This module implements a brick-wall filter on a binned height field in the reciprocal space. This implementation uses numpy.fft.fft2() with physical bin widths \(\Delta x\) and \(\Delta y\) in Å along the x and y dimensions, respectively.

Usage

Users can control filtering in three ways via the fft_filter argument:

  • Automatic (default for binning): fft_filter='auto'

    Low-pass with q_high = 0.5 * q_Nyq, resolved at runtime via resolve_fft_filter().

  • Manual: fft_filter={'q': (q_low, q_high)}

    Users explicitly specify the (q_low, q_high) pair in rad/Å.

  • Disabled: fft_filter=None

    No FFT filtering is applied to the time-averaged surface.

Warning

We recommend fft_filter='auto' when enabling the brick-wall filter, unless there is a specific reason to choose bounds manually. With fft_filter='auto', bounds are (0, 0.5 * q_Nyq) from the bin grid dx and dy.

When using the manual mode, the following rules apply:

  • Keep q_low=0 for a normal smoothing filter. If you set q_low above zero, the filter drops the average height of the membrane and behaves more like a high-pass (ripples only, no overall level).

  • With fft_filter='auto', the upper cutoff 0.5 * q_Nyq is a safe default for most grids. Note that it is not a single correct value for every system. Change q_high only if you know you need sharper or smoother results.

Filter cutoffs q_low and q_high are given in rad/Å. They describe how rippled the membrane height can be along the grid: small \(|q|\) represents a broad, gentle undulation, while large \(|q|\) represents fine, short-wavelength detail. The finest detail the bin grid can represent is set by the Nyquist frequency nyquist_q(), here in this module referred to as q_Nyq, and which depends on dx and dy.

The filter is applied in two steps. First, resolve_fft_filter() picks the pass band. Then apply_fft_filter() FFTs the height, zeros modes outside the band, and inverse-FFTs back to the surface in real space.

Limitations

  • numpy.fft.fft2() treats the grid as periodic. Prefer wrap=True for binning on trajectories with periodic boundaries, or accept edge artifacts when the patch is non-periodic.

  • The filter is a brick-wall mask in \(|q|\), which can cause ringing (Gibbs oscillations). Curvature uses second derivatives of the height field, so ringing can be amplified. fft_filter='auto' uses a conservative cutoff. Tune manually only if you know what you are doing.

  • Empty bins (NaN after binning): before filtering, empty bins are filled with the average height of occupied bins (not zero or neighbour interpolation). Large gaps can therefore add artifacts near empty regions in the filtered surface. Finer binning or fewer empty bins usually helps. See _height_for_fft().

  • Non-square bins (dx != dy): the pass band uses an isotropic cutoff on \(|q| = \sqrt{q_x^2 + q_y^2}\), while nyquist_q() uses \(\min(\pi/\Delta x, \pi/\Delta y)\). Modes that are Nyquist-resolvable along the finer-spaced axis but have \(|q|\) above the coarse-axis limit are removed by the filter even though the grid could represent them along that axis.

Functions

membrane_curvature.fft_filtering._validate_bin_widths(dx, dy)[source]

Validate bin widths before calculating the Nyquist wavevector magnitude.

Parameters:
  • dx (float) – Bin width along x (Å).

  • dy (float) – Bin width along y (Å).

Raises:

ValueError – If dx or dy is not strictly positive.

membrane_curvature.fft_filtering.nyquist_q(dx, dy)[source]

Return a conservative Nyquist wavevector magnitude for a bin grid after validation. The Nyquist wavevector magnitude is calculated as \(q_{\mathrm{Nyq}} = \min(\pi/\Delta x, \pi/\Delta y)\) in \(\mathrm{rad}/\mathrm{\AA}\).

Parameters:
  • dx (float) – Bin width along x (Å).

  • dy (float) – Bin width along y (Å).

Returns:

q_nyq – Nyquist wavevector magnitude in rad/Å.

Return type:

float

Raises:

ValueError – If dx or dy is not strictly positive.

membrane_curvature.fft_filtering.resolve_fft_filter(fft_filter, dx, dy)[source]

Resolve fft_filter into (q_low, q_high) for binning analyses.

Parameters:
  • fft_filter (str or dict) – 'auto' applies a low-pass with q_high = 0.5 * q_Nyq. Otherwise {'q': (q_low, q_high)} in rad/Å.

  • dx (float) – Bin width along x (Å).

  • dy (float) – Bin width along y (Å).

Returns:

q_bounds – Resolved (q_low, q_high) in rad/Å.

Return type:

tuple of (float, float)

Raises:

ValueError – If fft_filter is not 'auto' or a dict with key 'q'. Invalid (q_low, q_high) values and bounds above Nyquist are raised or warned from _validate_q_pair(). Non-positive dx / dy are raised from nyquist_q() when bounds are resolved.

Warns:

UserWarning – See _validate_q_pair().

membrane_curvature.fft_filtering._auto_q_bounds(dx, dy)[source]

Calculates the default tuple (q_low, q_high) for fft_filter='auto'. Keeps modes with \(|q| \leq 0.5\, q_{\mathrm{Nyq}}\) (low-pass).

Returns:

tuple_low_high(0.0, 0.5 * nyquist_q(dx, dy)).

Return type:

tuple of (float, float)

membrane_curvature.fft_filtering._validate_q_pair(bounds, dx, dy)[source]

Parse and validate the bounds (q_low, q_high) for fft_filter. If valid, the pass band is defined by q_low <= |q| <= q_high in rad/Å.

Parameters:
  • bounds (tuple or list) – Pair (q_low, q_high) in rad/Å.

  • dx (float) – Bin width along x (Å).

  • dy (float) – Bin width along y (Å).

Returns:

(q_low, q_high) – Validated (q_low, q_high).

Return type:

tuple of (float, float)

Raises:

ValueError – If bounds is invalid, q_low == q_high, or q_low exceeds Nyquist.

Warns:

UserWarning – If q_high is above what the bin grid can represent (Nyquist), or if q_low > 0 (the filter no longer keeps the average height). Use q_low=0 for a usual low-pass).

membrane_curvature.fft_filtering.apply_fft_filter(height, dx, dy, q_bounds)[source]

Apply brick-wall FFT filtering to a binned height field.

Parameters:
  • height (numpy.ndarray) – Binned height field, shape (n_x_bins, n_y_bins).

  • dx (float) – Bin width along x (Å).

  • dy (float) – Bin width along y (Å).

  • q_bounds (tuple of (float, float)) – (q_low, q_high) in rad/Å from resolve_fft_filter().

Returns:

filtered_height – Filtered height field.

Return type:

numpy.ndarray

membrane_curvature.fft_filtering._squared_radius_grid(n_x_bins, n_y_bins, dx, dy)[source]

Build a per-bin grid of squared angular wavevector magnitude.

Each entry matches the corresponding bin in the output of numpy.fft.fft2() (via numpy.fft.fftfreq()).

Parameters:
  • n_x_bins (int) – Number of bins along x.

  • n_y_bins (int) – Number of bins along y.

  • dx (float) – Bin width along x (Å).

  • dy (float) – Bin width along y (Å).

Returns:

grid_squared_radius – Array of shape (n_x_bins, n_y_bins) with values \(|q|^2\) in (rad/Å)2.

Return type:

numpy.ndarray

membrane_curvature.fft_filtering._height_for_fft(height, nan_mask)[source]

Prepare a height field for numpy.fft.fft2() when some bins are empty.

Parameters:
  • height (numpy.ndarray) – Binned height field, shape (n_x_bins, n_y_bins).

  • nan_mask (numpy.ndarray) – Boolean mask of shape (n_x_bins, n_y_bins) with True for empty bins.

Returns:

filled_height – Height field with empty bins filled to the average height of occupied bins.

Return type:

numpy.ndarray

Notes

Empty bins are stored as NaN. Before the forward FFT, each empty bin is replaced with the mean height of occupied bins. The fill value is uniform across empty bins and it is neither zero nor obtained by spatial interpolation.

This is a simple placeholder to run the filter. It can make the surface look too smooth or wavy near large gaps, especially when many bins are empty. After the inverse FFT, empty bins are set back to NaN. If all bins are empty, the fill value is 0.0.