HealPixConv — how the geometry is built#

What happens between the constructor and the forward pass, and the private helpers that do it. You do not need this page to use the layer (HealPixConv — gauge-equivariant convolution) or to look up an argument (HealPixConv — API reference) – it is here for anyone modifying the operator or checking its correctness.


The three stages#

The layer operates in three stages that are all precomputed at construction time. The forward pass reduces to a single gather + einsum.

Stage A — kernel definition and rotation#

A kernel_sz × kernel_sz grid of P = kernel_sz² unit vectors is placed at the North Pole (z = 1) with angular spacing equal to one HEALPix pixel width:

alpha_pix = sqrt(4π / (12 · nside²))   ← angular size of one pixel

stencil point (i, j):
    dtheta = sqrt(i² + j²) · alpha_pix
    dphi   = atan2(j, i)
    vec    = [sin(dtheta)·cos(dphi),
              sin(dtheta)·sin(dphi),
              cos(dtheta)]

This stencil template is fixed and never changes. For every target pixel k (colatitude θ_k, longitude φ_k) and every gauge orientation g, a total rotation matrix is assembled as:

R_total[k, g] = R_gauge(α_g)  @  Rz(φ_k)  @  Ry(θ_k)
                └─ gauge roll ─┘  └─── carry North Pole → pixel k ───┘

where R_gauge(α_g) is the Rodrigues rotation around the surface normal n_k by the gauge angle α_g (see HealPixConv — gauge-equivariant convolution).

Each stencil point is then rotated to its position on the sphere around pixel k:

rotated[k, g, p] = R_total[k, g] @ vec_pole[p]    ∈ ℝ³

producing a tensor of shape [K, G, P, 3].

Stage B — bilinear binding#

For each of the K × G × P rotated direction vectors, the function get_interp_weights returns the 4 nearest HEALPix pixel indices and their bilinear interpolation weights:

idx[4, K·G·P]    — absolute NESTED pixel ids of the 4 neighbours
w  [4, K·G·P]    — bilinear weights (sum to 1 per stencil point)

These are reshaped to [G, 4, K·P] and stored as persistent buffers _pos_safe and _w_norm. For partial-sky inputs, neighbours outside the patch are masked and weights are renormalised; stencil points with no available neighbour fall back to the centre pixel.

Stage C — forward pass#

Important: it is the data that is brought to the fixed kernel, not the kernel that is deformed per pixel.

At inference, for each stencil point p of pixel k under gauge g, the signal value is obtained by bilinear interpolation of the input map:

x_interp[b, c, g, k, p] = Σ_{j=0}^{3}  w[g, j, k, p] · x[b, c, nbr[g, j, k, p]]

The interpolated values are then contracted with the learned kernel via a single einsum over all gauges simultaneously:

y[b, g·C_out + o, k] = Σ_{c, p}  W[g, c, o, p] · x_interp[b, c, g, k, p]

In index notation: "bcgkp, gcop -> bgok".

The full forward in code reduces to:

pos_flat  = pos.reshape(-1)                        # [G·4·K·P]
vals_flat = t_sorted.index_select(2, pos_flat)     # [B, C_in, G·4·K·P]
vals      = vals_flat.view(B, C_in, G, 4, K, P)
gathered  = (vals * w_shaped).sum(dim=3)           # [B, C_in, G, K, P]
y         = einsum("bcgkp, gcop -> bgok", gathered, W)  # [B, G, C_out, K]

Private helpers#

These functions are not part of the public API but are documented here for developers who want to extend or debug the module.

_local_kernel_grid(kernel_sz, nside) → np.ndarray [P, 3]#

Builds the kernel_sz × kernel_sz stencil at the North Pole. Angular spacing equals one pixel width (sqrt(4π / (12·nside²))). The centre point (index kernel_sz//2 · (kernel_sz + 1)) maps to the exact North Pole direction [0, 0, 1].

_build_rotation_matrices(th, ph, G, gauge_type, ...) → torch.Tensor [K, G, 3, 3]#

Assembles the full rotation matrix R_total = R_gauge(α_g) @ Rz(φ) @ Ry(θ) for every pixel and gauge. R_gauge is computed via the Rodrigues formula (axis-angle rotation around the surface normal n):

R_gauge = I·cos(α) + K_skew·sin(α) + (n⊗n)·(1 − cos(α))

where K_skew is the skew-symmetric matrix of n.

_get_interp_weights(nside, vecs, nest, device, dtype) → (idx [4, M], w [4, M])#

Converts M direction vectors to bilinear interpolation weights and neighbour indices on the HEALPix grid. Internally delegates to healpix_analyse.healpix_interp.get_interp_weights (one vectorised call, no Python loop).

_bind_support_batched(idx_t, w_t, ids_sorted, ...) → (pos_safe, w_norm)#

Maps absolute NESTED pixel ids to column indices within the current pixel patch via torch.searchsorted. Handles three edge cases: neighbours outside the patch (zeroed), stencil points with no valid neighbour (fall back to the centre pixel), and zero-sum weight columns (assign weight 1 to the first present neighbour). All G gauges are processed in a single searchsorted call over the full [G · 4 · K · P] index array.