HEALPix mapmaking¶
An image-plane coadd describes the sky on one flat grid, tied to a projection
and a tangent point. A HEALPix map describes it on the whole sphere: the
HEALPix pixelization divides the sky into
\(12\,N_{\rm side}^2\) equal-area cells, so a map built from any number of
pointings has one pixel layout, no projection distortion, and no edges between
tiles. coadd_healpix
projects calibrated images directly onto such a map, and
resample_healpix takes a
map back onto any image WCS.
Use it for products that span many fields or the whole sky, for maps that must be combined with other HEALPix data (Planck, WMAP, survey depth and mask maps), and as input to spherical harmonic analysis. For a single field that you will measure with image tools, a coadd on a TAN grid is simpler.
How It Works¶
flowchart LR
O["exposure<br/>data, WCS, weight"] --> M["MOC of the<br/>footprint at Nside"]
M --> P["HEALPix cells<br/>inside the footprint"]
P -->|"cells finer than<br/>kernel support"| S["interpolate data and<br/>weight at cell centre"]
P -->|"coarser cells"| I["integrate pixels over<br/>their overlap with each cell"]
S --> A["accumulate Σ w·d, Σ w<br/>dense or sparse"]
I --> A
A --> R["HealpixResult<br/>data, weight, overlap_count,<br/>coverage MOC"]
Values are surface brightness. Each exposure enters in one of two regimes,
chosen from the cell size \(L\) (nside2resol) and the exposure's pixel size
\(p\) (the square root of the solid angle of its central pixel).
Centre sampling, when \(L \le s\,p\) with \(s\) the kernel support (1 pixel
for nearest, 2 bilinear, 4 bicubic and lanczos2, 6 lanczos3, 8
lanczos4). torchsky lists the HEALPix cells inside the footprint, maps each
cell centre into the exposure's pixel coordinates, and interpolates data and
weight there with the kernel:
with \(\mathbf x_{kp}\) the position of the centre of cell \(p\) in exposure \(k\).
Overlap integration, when \(L > s\,p\) (centre samples would leave image
pixels unused) or when the footprint contains no cell centre. Each image pixel
\(i\) is shared among the cells it overlaps, as drizzle does at pixfrac = 1:
with \(A_{ip}\) the area of pixel \(i\) inside cell \(p\) in units of the cell area. A cell only partly covered by an exposure gets that exposure's mean over the covered part, weighted by the covered fraction; a fully covered cell gets weight \(w\), as in centre sampling. The overlap is computed exactly in HEALPix's equal-area lattice coordinates, where every cell is a unit square: each pixel is the quadrilateral of its projected corners, clipped against the cells and the equatorial or polar zone it falls in. The kernel is not used.
Cells are processed in chunks, so memory is the map plus one chunk. Optional
sky= and outliers= configurations work as in coadds.
Assumptions. The map is in ICRS: exposures with Galactic, ecliptic, FK4, or FK5 WCS are converted.
An FK5 (J2000) header gets the roughly 20 mas FK5 to ICRS frame bias and emits
CelestialFrameBiasWarning, as in the image-plane path; set RADESYS = 'ICRS'
if the astrometry was solved against Gaia. Validation. HEALPix indexing
matches healpy exactly (parity), and the
multiband example checks the map
against an analytic sky at every cell centre. The package tests check the
overlap weights against an independent 24 × 24 sub-pixel integration to
\(2\times10^{-3}\) of a cell, and the total covered area to \(10^{-5}\),
including cells that straddle the polar zones, the 0° meridian, and a pole.
Four MegaPipe Cutouts on HEALPix¶
The four CFHT MegaPipe cutouts of the mosaic example (0.186 arcsec pixels) go straight onto a sparse map with 0.40 arcsec cells, without an intermediate flat mosaic:
# megapipe_mosaic_[abcd].fits: examples/megapipe_mosaic.py downloads them from CADC.
import torch
import torchfits
from torchsky.map import Observation, coadd_healpix, resample_healpix
exposures = []
for name in ("megapipe_mosaic_a", "megapipe_mosaic_b", "megapipe_mosaic_c", "megapipe_mosaic_d"):
image = torchfits.read(f"{name}.fits", 0).to(torch.float32)
header = dict(torchfits.read_header(f"{name}.fits", 0))
exposures.append(Observation(data=image, wcs=header))
sky = coadd_healpix(exposures, nside=2**19, nest=True, sparse=True, kernel="lanczos3")
print(sky.coverage_moc.area_sq_deg * 3600) # about 5.5 arcmin^2 covered
cutout = resample_healpix(sky, exposures[0].wcs) # back onto the first cutout's grid
The map takes about 6 s on a laptop CPU. Projected back onto the first cutout, it differs from the original by a median of 0.003 counts.
Four Bands of the Cartwheel, One Map Each¶
The Legacy Surveys example
starts from two DR10 cutouts on different grids: g/r/i/z at 0.524 arcsec, and
g/z at 0.35 arcsec. Their values are nanomaggies per pixel, so each image is
first divided by its exact pixel area
(pixel_area_map). Each band is then
coadded with its inverse variance onto a sparse nested map at
nside = 2**19 (0.40 arcsec cells). Only 27 454 cells per band are stored; a
dense map at this resolution would need 13 TB.
# fragment: per_band maps a band to its surface-brightness Observations
from torchsky.map import coadd_healpix
maps = {band: coadd_healpix(obs, 2**19, nest=True, sparse=True, kernel="lanczos3")
for band, obs in per_band.items()}

Each map is checked against coadd onto a planned TAN grid at the same scale:
- Flux. The flux in a 25 arcsec disc agrees within 0.04 % in every band.
- Pixels. The image-plane coadd, sampled at the HEALPix cell centres, differs from the map by a median of 0.02 σ.
Dense or Sparse?¶
| Situation | Choice | Why |
|---|---|---|
| Small fraction of the sky at high resolution | sparse=True |
only covered cells are stored |
| All-sky or broad survey field | sparse=False |
a dense tensor of length \(12 N_{\rm side}^2\) indexes directly |
| Downstream harmonic analysis | dense | map2alm takes a full map |
In sparse mode sky.data and sky.weight are
SparseHealpixMap
objects; nside_coverage sets the resolution of their coverage index (not the
map resolution). nside = 4096 is already 201 million cells, so use sparse
maps unless you need the full sphere.
Recipes¶
Coadd images onto a dense map¶
import torch
from torchsky.map import Observation, coadd_healpix
header = {
"NAXIS": 2, "NAXIS1": 64, "NAXIS2": 64,
"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN",
"CRPIX1": 32.5, "CRPIX2": 32.5, "CRVAL1": 150.0, "CRVAL2": 2.0,
"CDELT1": -0.02, "CDELT2": 0.02,
}
observations = [
Observation(data=torch.full((64, 64), value), wcs=header,
weight=1.0, weight_is_inverse_variance=True)
for value in (1.0, 2.0)
]
# 1.3 deg image, 0.23 deg cells (Nside 256).
result = coadd_healpix(observations, nside=256, nest=False, kernel="bilinear")
covered = result.weight > 0
assert result.data.numel() == 12 * 256**2 and int(covered.sum()) > 20
assert abs(float(result.data[covered].mean()) - 1.5) < 1e-5
print("covered cells:", int(covered.sum()), "MOC cells:", int(result.coverage_moc.uniq.numel()))
nest=False (the default) gives RING ordering; pass nest=True when the
consumer expects NESTED. Kernels are nearest, bilinear, bicubic, and
lanczos2 to lanczos4. The result carries data, weight, nside, nest,
overlap_count (exposures per cell), and the coverage MOC.
Choose Nside¶
The cell size is about \(58.6°/N_{\rm side}\)
(nside2resol). Cells
up to the kernel support (two pixels for bilinear) are centre-sampled and
keep the interpolation quality of the kernel. Coarser cells, down to cells
larger than the whole image, are overlap-integrated: each cell is the mean of
the pixels inside it, so a coarse map can be built directly from fine images.
Make one map per band¶
Call coadd_healpix once per band with the same nside and ordering; cells
then align across bands by construction, and a colour is a ratio of two tensors.
The multiband example builds three
bands from twelve overlapping tiles at \(N_{\rm side} = 1024\).

Project a map onto an image¶
import torch
from torchsky.map import Observation, coadd_healpix, resample_healpix
header = {
"NAXIS": 2, "NAXIS1": 64, "NAXIS2": 64,
"CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN",
"CRPIX1": 32.5, "CRPIX2": 32.5, "CRVAL1": 150.0, "CRVAL2": 2.0,
"CDELT1": -0.02, "CDELT2": 0.02,
}
result = coadd_healpix([Observation(data=torch.ones(64, 64), wcs=header)], nside=256)
image = resample_healpix(result, header, method="bilinear")
covered = image.weight > 0
assert tuple(image.data.shape) == (64, 64) and int(covered.sum()) > 1000
assert torch.allclose(image.data[covered], torch.tensor(1.0))
resample_healpix takes a dense tensor (with nside=), a sparse map, or a
HealpixResult, and a header, WCS, or
Target. It returns a
MapResult with the same
data and weight contract as an image coadd. method="harmonic" interpolates
through spherical harmonics; use it only when the map is band-limited and well
sampled.
Bound memory on large maps¶
reduction_chunk_size (default 262,144) bounds how many sampled values are
held before each reduction; lower it when many images overlap the same
high-resolution cells. Dense accumulation writes each chunk into the output, and
sparse accumulation grows with the covered sky, not with \(12 N_{\rm side}^2\).
Weights and uncertainty¶
Weights follow the coadd rules: relative by default,
inverse variance only with weight_is_inverse_variance=True. Exposures enter
in their own value semantics; for HEALPix products, surface brightness is the
natural choice because every cell has the same area. Cross-cell covariance is
not computed.
Pitfalls¶
- Ordering. RING and NESTED maps hold the same values in different orders. Pass the ordering along with the map; most bugs show as a scrambled sky.
- Frame. The map is ICRS. To compare with a Galactic HEALPix product, rotate one of them; see SHT for harmonic-space rotation.
- Edge cells. With overlap integration, a cell at the edge of an exposure
carries a weight equal to the covered fraction; its value is the mean of the
covered part only. Check
weightbefore treating such a cell as a full-cell average. - Zero means empty. Uncovered cells have
weight == 0and value 0; use the weight or the coverage MOC, not the value, to find them.
See Also¶
- HEALPix, MOC, and sparse maps for the pixelization and its data structures.
- Coadds for flat-grid mosaics; SHT for power spectra of the result.
- Reference:
coadd_healpix,HealpixResult. - Examples: multiband tiles onto HEALPix, Legacy Surveys g/r/i/z of the Cartwheel.