Images and maps: overview¶
This area turns many calibrated exposures into one sky product: an image on a
chosen grid, a spectral cube, or a HEALPix map. torchsky.map plans the
target grid, resamples each exposure onto it, and stacks the results with
explicit weights, masks, and outlier rejection. Use it when the unit of work is
a requested product rather than a single image.
The Core Idea: Plan, Resample, Stack¶
flowchart LR
O["Observations<br/>data + WCS + weight/mask"] --> P["plan<br/>target grid"]
P --> T["Target<br/>WCS + shape"]
O --> R["resample<br/>kernel: bilinear,<br/>Lanczos, drizzle, …"]
T --> R
R --> S["stack<br/>weights, sky match,<br/>outlier rejection"]
S --> M["MapResult<br/>data, weight, header,<br/>inverse_variance, provenance"]
- An observation (
Observation) is one calibrated exposure: pixel data, a WCS header, and optionally a weight or variance map, a data-quality (DQ) mask, a photometric scale, a background offset, or a wavelength. - A target is the output grid: WCS, shape, and bands.
planbuilds one that covers the inputs. - Resampling moves pixel values from one grid to another with a declared
kernel. Interpolating kernels (bilinear, bicubic, Lanczos) sample the input
at each output pixel. Drizzle instead drops each input pixel, shrunk by
pixfrac, onto the output grid and shares its flux by overlap area; it preserves flux and handles undersampled, dithered data. - Stacking combines the resampled exposures with their weights, optional sky matching, and sigma-clipping or median rejection.
Choose a Page¶
| Task | Page | Entry point |
|---|---|---|
| Put one image on another grid; choose a kernel; drizzle | Resampling | resample, ResampleConfig |
| Coadd exposures, propagate uncertainty, bound memory | Coadds | coadd, plan |
| Build a cube from filter images or IFU frames | Cubes and IFU | coadd_cube |
| Coadd images onto HEALPix, or project HEALPix onto an image | HEALPix mapmaking | coadd_healpix, resample_healpix |
| Run the same workflows from the shell | CLI | torchsky map |
A Weighted Coadd¶
The two images below are the same star in two CFHT MegaPipe g-band stacks:
tile 712.165 (240 s) and tile 712.166 (1224 s), 11 × 11 pixels each, copied
from the cutouts of the
mosaic example.
Each tile has its own TAN tangent point, half a degree apart, so the pixel grids
do not line up. plan builds one grid covering both, coadd resamples each
image with Lanczos3 and stacks them with inverse-variance weights from the
measured pixel noise (1.76 and 1.43 counts).
import torch
from torchsky.map import Observation, coadd, plan
from torchsky.wcs import WCS
tile_712_165 = torch.tensor([
[6, 4, 2, 3, 4, 6, 5, 4, 1, 0, -1], [1, 4, 5, 6, 6, 10, 6, 8, 6, 1, 2],
[1, 4, 6, 14, 18, 21, 19, 12, 8, 4, 2], [6, 9, 18, 27, 37, 48, 39, 21, 8, 1, 5],
[9, 11, 23, 40, 60, 71, 58, 33, 19, 11, 4], [9, 15, 34, 57, 80, 89, 70, 39, 23, 10, 5],
[9, 17, 33, 63, 90, 87, 64, 42, 18, 7, 6], [10, 21, 31, 55, 70, 69, 50, 24, 11, 6, 2],
[8, 11, 23, 36, 44, 42, 29, 18, 12, 6, 4], [5, 11, 14, 25, 27, 19, 11, 8, 8, 7, 6],
[6, 8, 6, 10, 12, 12, 7, 5, 7, 3, 2],
], dtype=torch.float32)
tile_712_166 = torch.tensor([
[4, 4, 2, 3, 3, 4, 4, 3, 0, -1, 0], [4, 4, 5, 3, 6, 8, 5, 7, 3, 0, 0],
[1, 3, 6, 10, 12, 14, 11, 10, 7, 3, 1], [4, 7, 14, 22, 30, 36, 27, 15, 5, 1, 3],
[8, 12, 22, 36, 53, 61, 46, 26, 11, 6, 3], [9, 16, 33, 54, 74, 83, 61, 33, 20, 10, 4],
[10, 19, 39, 68, 92, 88, 64, 39, 18, 7, 6], [12, 22, 37, 65, 83, 76, 53, 28, 12, 5, 3],
[10, 17, 30, 46, 56, 53, 35, 18, 10, 5, 2], [6, 11, 20, 31, 33, 26, 16, 12, 9, 7, 6],
[8, 10, 10, 17, 17, 13, 6, 7, 7, 5, 4],
], dtype=torch.float32)
grid = {"NAXIS": 2, "NAXIS1": 11, "NAXIS2": 11, "CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN",
"CD1_1": -5.160234650248e-05, "CD1_2": 0.0, "CD2_1": 0.0, "CD2_2": 5.160234650248e-05}
headers = [ # the tiles' CRVAL; CRPIX moved to each 11 x 11 corner
{**grid, "CRVAL1": 359.07190997, "CRVAL2": -7.5, "CRPIX1": -4960.0, "CRPIX2": -4798.0},
{**grid, "CRVAL1": 358.67349786, "CRVAL2": -7.0, "CRPIX1": 2699.0, "CRPIX2": 4891.0},
]
exposures = [
Observation(data=image, wcs=header, weight_map=torch.full((11, 11), 1 / noise**2),
weight_is_inverse_variance=True)
for image, header, noise in zip((tile_712_165, tile_712_166), headers, (1.76, 1.43))
]
target = plan(exposures, projection="TAN")
result = coadd(exposures, target, kernel="lanczos3")
def star_position(image, header): # flux-weighted centroid of the bright pixels, as RA, Dec
yy, xx = torch.meshgrid(*(torch.arange(n, dtype=torch.float64) for n in image.shape), indexing="ij")
w = torch.where(image > 20, image.double(), 0.0)
x, y = (w * xx).sum() / w.sum(), (w * yy).sum() / w.sum()
return torch.cat(WCS(header).pixel_to_world(x.reshape(1), y.reshape(1)))
inputs = [star_position(image, header) for image, header in zip((tile_712_165, tile_712_166), headers)]
stacked = star_position(result.data.nan_to_num(), result.header)
for position in inputs: # the star lands in the same place: within 0.02 arcsec
assert ((stacked - position) * 3600).abs().max() < 0.02
covered = result.weight[result.weight > 0] # every covered pixel, edges included
torch.testing.assert_close(covered, torch.full_like(covered, 1 / 1.76**2 + 1 / 1.43**2), rtol=1e-4, atol=0.0)
Both tiles reach every covered target pixel, so the weight is the sum of the two
inverse variances right up to the edge; result.inverse_variance holds the propagated per-pixel inverse
variance, which Coadds explains. The full example mosaics four 512 × 512 cutouts with Lanczos3 and drizzle and checks
every pairwise overlap.
Conventions and Pitfalls¶
- Value semantics. A product is either surface brightness (the default: values per unit area, unchanged by resampling to a finer grid) or flux (values per pixel, scaled by pixel area). Choose before comparing with another stack; differences with SWarp or Montage usually trace to value semantics, pixel-area treatment, mask policy, or weights.
- Weights.
weightalways controls the coadd denominator. It is treated as inverse variance, andresult.inverse_varianceis filled, only withweight_is_inverse_variance=True.build_weight_productattaches inverse-variance, confidence, and mask-polarity semantics when you build masks yourself. - Kernels are configuration.
ResampleConfig(kernel=...)sets support, interpolation, and flux behaviour; nothing switches kernels behind your back. See the kernel table. - Memory. Large mosaics are tiled and chunked by configuration
(
tile_size,response_chunk_size); the scaling guide lists the knobs.
Device Placement¶
Map APIs compute and return on the device of their tensor inputs; CPU tensors
stay on CPU even when CUDA is available. An explicit device= always wins, and
tensors on different devices without it raise ValueError. When observations
are file paths, torchsky.device.default_compute_device() applies. Float64-only
kernels (exact drizzle, conservative polygon and splat projection, spectral
resamplers) raise NotImplementedError on MPS. The general rules are in
Concepts.
Connections¶
Every observation and target carries a WCS. HEALPix mapmaking writes to the celestial sphere, and cubes connect to spectral axes. The observation operators that resample and convolve here are the same ones the experimental models differentiate through.
Resampling and coadds are compared with SWarp, Montage, STScI drizzle, and reproject in torchsky-harness; see performance highlights and the parity page. Examples: the MegaPipe mosaic, the MaNGA IFU cube, dithered SIP coadd, drizzle, coadd to HEALPix, and the examples index.