healpix_resample.bicubic (radial cubic-convolution HEALPix regridding)#
healpix_resample.bicubic.BicubicResampler maps unstructured (lon_deg, lat_deg, val) samples onto a
HEALPix grid using a radial generalization of Keys’ cubic convolution kernel — the same interpolation
kernel behind cv2.INTER_CUBIC / PIL.Image.BICUBIC on regular pixel grids, adapted to scattered data.
It sits between NearestResampler / BilinearResampler (fixed, non-iterative, few neighbours) and
PSFResampler (iterative CG deconvolution): a fixed, non-iterative local interpolation using more
neighbours than bilinear, for users who want smoother results than bilinear without paying for a full
CG solve.
Note on geodesy: the package manages the HEALPix authalic definition and the Earth ellipsoid with WGS84 through its geometry helper (
healpix_geo).
What “bicubic” means here#
This package operates on unstructured samples found via a distance-based Gaussian-neighbourhood
search (healpix_weighted_nearest in knn.py), not a structured pixel grid with clean (fx, fy)
fractional offsets — so a textbook 2D bicubic convolution kernel doesn’t directly apply. What does
generalize cleanly to scattered data is the radial profile of Keys’ cubic convolution kernel: replace
Euclidean pixel distance with the geodesic distance self.d_m and length scale self.sigma_m already used
by every other resampler in this package, and evaluate the standard piecewise-cubic weight (a = -0.5, the
common default matching PIL/cv2):
u = d / sigma
w(u) = (a+2)|u|^3 - (a+3)|u|^2 + 1 for |u| <= 1
= a|u|^3 - 5a|u|^2 + 8a|u| - 4a for 1 < |u| < 2
= 0 for |u| >= 2
Npt = 16 is the default neighbourhood size — the natural analogue of the classic 4x4 bicubic stencil on
a structured grid, since Keys’ kernel has support |u| < 2 (roughly twice the reach of bilinear’s
|u| < 1-ish support).
Unlike the nonnegative Gaussian/inverse-distance weights used elsewhere in this package, Keys’ kernel is
signed — it goes negative for 1 < |u| < 2, which is what gives cubic convolution its sharpening
property relative to bilinear. Two consequences:
The per-cell/per-sample weight sums used to normalize the sparse operators can land close to zero from cancellation between the positive and negative lobes.
BicubicResampler.comp_matrix()floors these sums (relative to the unsigned accumulated weight) before dividing, rather than dropping affected cells.invert()can genuinely overshoot/ring outside the local sample-value range — this is expected cubic-convolution behaviour, not a bug.
Class hierarchy and construction#
BicubicResampler is a KNeighborsResampler subclass, exactly like BilinearResampler: it overrides
__init__ (to fix Npt=16, auto-correct ring_search_max, and accept area=), comp_matrix() (to build
the sparse operators from the cubic-convolution weight instead of the Gaussian/IDW weight, plus a
conservative-mode operator, see below), and resample() (to add conservative=True, see below). invert()
is inherited unchanged from KNeighborsResampler.
from healpix_resample import BicubicResampler
op = BicubicResampler(
lon_deg=lon, # (N,) sample longitudes, degrees
lat_deg=lat, # (N,) sample latitudes, degrees
level=level, # HEALPix level, nside = 2**level
device="cuda",
dtype=torch.float32,
nest=True,
)
Key parameters#
lon_deg, lat_deg: unstructured sample coordinates in degrees, shape(N,).level: HEALPix level (nside = 2**level).Npt: number of HEALPix neighbours per sample (default 16).nest: HEALPix indexing scheme.device,dtype: PyTorch placement and numerical type.threshold: minimum accumulated (unsigned, Gaussian-weighted) support for a HEALPix cell to be kept — same meaning as for every other resampler in this package.area: per-sample pixel area/weight, shape(N,). Only used byresample(conservative=True)(see below); ignored otherwise. Defaults to1.0for every sample.
Stored attributes#
After construction, BicubicResampler exposes the same attributes as every KNeighborsResampler
subclass:
N: number of samples.K: number of kept HEALPix cells (cell_ids).cell_ids:(K,)HEALPix cell ids retained.hi:(N, Npt)cell indices per sample (intocell_ids).d_m:(N, Npt)geodesic distances (metres) to those cells.M: sparse CSR(N, K)operator (per-cell-normalized),hval = y @ M.MT: sparse CSR(K, N)operator,val_hat = hval @ MT.M_cons: sparse CSR(N, K)operator (per-sample-normalized — a partition of unity per sample, up to the signed-kernel floor caveat below), used byresample(conservative=True).area:(N,)per-sample area/weight (see above).
Methods#
resample(val, conservative=False)#
Sample space → HEALPix cell space.
conservative=False(default):hval = val @ self.M(per-cell-normalized weights) — smooth, but not exactly mass-conserving.conservative=True:hval = (val * self.area) @ self.M_cons— each sample’s own (area-weighted) value is redistributed across its 16 nearest cells using weights normalized so each sample’s own weights sum to 1, guaranteeingsum_k hval[k] == sum_i (valid i) val[i] * area[i]exactly — see “Conservative mode” below.
Accepts val of shape (N,) or (B, N), np.ndarray or torch.Tensor; returns the same type/batch shape
it was given, wrapped in a ResampleResults(cell_data, cell_ids).
invert(hval)#
HEALPix cell space → sample space: val_hat = hval @ self.MT (inherited, no CG; unaffected by
conservative/area). Same (K,)/(B, K) and NumPy/Torch symmetry as resample.
Conservative mode (area=, resample(conservative=True))#
Added for issue #44 (“conservative bi-linear is
missing”), and applied to BicubicResampler for consistency with BilinearResampler. Same construction as
the bilinear case (see docs/user-guide/regrid_to_healpix_bilinear.md for the full derivation): M_cons
reuses the exact weights already computed for MT (per-sample-normalized) under M’s (sample, cell)
index layout, so each sample’s own contribution sums to 1 across the cells it links to and no value is
invented or lost globally.
Caveat specific to this class’s signed kernel: unlike BilinearResampler’s non-negative
inverse-distance weights, Keys’ cubic kernel is signed, so M_cons’s per-sample rows only sum to exactly
1 for samples whose norm_row wasn’t floored by _floor_signed in comp_matrix() — true for the vast
majority of well-conditioned samples. For the rare, pathologically-cancelled sample that does hit the
floor, that sample’s own contribution to the conservation identity is only approximate, not bit-exact —
the same accepted trade-off _floor_signed already makes for ordinary (non-conservative) interpolation.
NaN handling under conservative=True is the same as BilinearResampler’s: a NaN sample’s value and area
are both excluded, so the conservation identity holds over exactly the valid samples; a batch row where
every sample is NaN comes back entirely nan.
Notes and practical tips#
Bicubic gives no benefit over bilinear on a perfectly linear field (both interpolate a plane exactly); its advantage shows up on fields with curvature.
Because the kernel is signed,
invert()can produce values slightly outside the local sample range (overshoot/ringing) — expected, not a bug.If your data is very sparse relative to the HEALPix
level(few samples within2*sigmaof a cell), preferBilinearResamplerorNearestResampler, which need fewer nearby samples to produce a well-conditioned result.