Kernel Design#
Introduction#
The spatial Q-test framework unifies pattern detection under the quadratic form,
The kernel matrix \(\mathbf{K}\) encodes spatial structure. Test consistency requires positive definiteness; indefinite kernels suffer spectral cancellation and produce false negatives.
See theory: Theoretical Results for Theorems 1–2, CAR kernel solution, null distributions, and FFT acceleration.
Kernel spectrum and power#
The Q-test power for a spatial pattern \(f\) is governed by the spectrum
Key principles:
Spectrum is a frequency filter: \(\lambda(\omega)\) determines which spatial frequencies the test amplifies
No universally optimal kernel: with fixed trace, boosting some frequencies suppresses others
Data-driven kernels are risky: gene-wise tuning inflates Type I error; global tuning biases toward dominant patterns
Kernel comparison:
Gaussian (distance): Soft low-pass. The exponential spectral decay may cause loss of mid/high-frequency signals.
Matérn (distance): Polynomial decay with tunable smoothness.
Moran’s I (graph): Indefinite spectrum → spectral cancellation. Do not recommend for SVG detection.
Graph Laplacian (graph): High-pass filter. Detects roughness/textures, weak for clustering.
CAR / Inverse Laplacian (graph, default): Polynomial low-pass with heavy tail. Best choice for clustering with better mid/high-frequency sensitivity.
FFT-CAR (grid, default): Same as CAR but O(N log N) via FFT. Use for Visium HD and regular grids.
Practical takeaway: Use CAR (graphs) or FFT-CAR (grids) as robust defaults. Use Matérn for distance-based kernels.
Quick Start examples#
Spatial transcriptomics (Visium, MERFISH)
from quadsv.kernels import SpatialKernel
kernel = SpatialKernel.from_coordinates(
coords, method='car', k_neighbors=4, rho=0.9
)
Large-scale grids (Visium HD)
from quadsv.fft import FFTKernel
# the spatial data is presented as a 1000x1000 rasterized grid (e.g., via SpatialData)
kernel = FFTKernel(shape=(1000, 1000), method='car', rho=0.9, neighbor_degree=1)
Small datasets or distance-based
kernel = SpatialKernel.from_coordinates(
coords, method='matern', bandwidth=1.0, nu=1.5
)
Parameter tuning#
Kernel bandwidth (Gaussian, Matérn)
Use median pairwise distance as starting point:
import numpy as np
from scipy.spatial.distance import pdist
dist = pdist(coords)
bandwidth_init = np.median(dist)
kernel = SpatialKernel.from_coordinates(
coords, method='gaussian', bandwidth=bandwidth_init
)
CAR spatial dependence (rho)
rho = 0.5: Moderate dependence
rho = 0.9: Strong dependence (recommended)
rho → 1: Maximum smoothing
rhos = [0.5, 0.7, 0.9, 0.95]
for rho in rhos:
kernel = SpatialKernel.from_coordinates(
coords, method='car', rho=rho
)
Q, pval = spatial_q_test(data, kernel)
print(f"rho={rho}: Q={Q:.4f}, pval={pval:.4e}")
k-Neighbors (graph kernels)
k = 5–10: Sparse, local
k = 30+: Dense, global
ks = [5, 10, 15, 20, 30]
for k in ks:
kernel = SpatialKernel.from_coordinates(
coords, method='car', k_neighbors=k, rho=0.9
)
trace = kernel.trace()
print(f"k={k}: trace={trace:.4f}")
Custom kernel design#
Option 1: Build from custom adjacency or distance matrix
Use SpatialKernel.from_matrix():
import numpy as np
from quadsv.kernels import SpatialKernel
# From adjacency matrix
W = ... # (N, N) adjacency, e.g., from graph structure
L = np.eye(W.shape[0]) - 0.9 * W # CAR precision (K^-1)
kernel = SpatialKernel.from_matrix(L, is_inverse=True, method='precomputed')
Option 2: Subclass ``Kernel`` for custom implementations
Implement required methods:
_build_kernel(): Return kernel matrix (explicit) or precision (implicit)realization(): Return dense (N, N) kernel matrixxtKx(x): Compute \(x^\top K x\)trace(): Return trace of kerneleigenvalues(k): Return top k eigenvaluessquare_trace(): Return \(\text{tr}(K^2)\)
from quadsv.kernels import Kernel
import numpy as np
class MyKernel(Kernel):
def _build_kernel(self):
# Return your custom (N, N) SPD matrix
K = self.params["K"]
return K
def realization(self):
return self._K
def xtKx(self, x):
return x.T @ self._K @ x
def trace(self):
return np.trace(self._K)
def eigenvalues(self, k=None):
evals = np.linalg.eigvalsh(self._K)
return evals[-k:] if k else evals
def square_trace(self):
return np.trace(self._K @ self._K)
# Usage
K_custom = np.eye(100) # Replace with your SPD kernel
kernel = MyKernel(n=100, method="custom", K=K_custom)
Option 3: Subclass ``FFTKernel`` for grid data
Override _compute_eigenvalues() to define custom spectrum. Store result in self.spectrum:
import numpy as np
import scipy.fft
from quadsv.fft import FFTKernel
class CustomFFTKernel(FFTKernel):
def _compute_eigenvalues(self):
# Define custom spectral filter
fy = scipy.fft.fftfreq(self.ny, d=self.dy)
fx = scipy.fft.fftfreq(self.nx, d=self.dx)
FY, FX = np.meshgrid(fy, fx, indexing="ij")
r2 = FX**2 + FY**2
lam = 1.0 / (1.0 + 10.0 * r2) # Polynomial decay
if self.fft_solver == "rfft2":
lam = lam[:, :(self.nx // 2 + 1)]
return lam.ravel() # Must return flattened spectrum
kernel_fft = CustomFFTKernel(shape=(256, 256), method="custom")
Tip: Keep \(\lambda(\omega) > 0\) for all frequencies to ensure consistency.
Spectrum debugging#
Verify kernel properties by inspecting eigenvalues:
import numpy as np
import matplotlib.pyplot as plt
# Get eigenvalues
evals = kernel.eigenvalues(k=100)
# Check positivity
print(f"Min eigenvalue: {evals[-1]:.6e}")
print(f"All positive: {np.all(evals > 1e-9)}")
# Plot spectrum
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
ax1.semilogy(evals, 'o-')
ax1.set_ylabel('Eigenvalue magnitude')
ax1.set_xlabel('Rank')
ax1.set_title('Spectrum (log scale)')
ax2.plot(evals, 'o-')
ax2.axhline(0, color='red', linestyle='--', alpha=0.5)
ax2.set_ylabel('Eigenvalue')
ax2.set_xlabel('Rank')
ax2.set_title('Spectrum (linear scale)')
plt.tight_layout()
plt.show()
Interpretation:
All evals > 0: Strictly positive definite (consistent Q-test)
Some evals ≤ 0: Indefinite (spectral cancellation risk)
Monotonic decay: Low-frequency bias
Power-law decay: Scale-free spectrum
See also#
Quick Start for practical examples
Theoretical Results for mathematical background
quadsv.kernels and quadsv.fft for detailed kernel API documentation