Python quickstart

This page is an executable notebook: every cell below is re-run on each docs build, so the outputs are guaranteed to match the current release.

Building a set of single-channel images

fitscube-rs combines many single-frequency (or single-time) FITS images into a single FITS cube. To demonstrate, we first write out a handful of synthetic single-channel images, each with its own frequency in the header.

import tempfile
from pathlib import Path

import numpy as np
from astropy.io import fits

workdir = Path(tempfile.mkdtemp())

freqs_hz = np.array([1.0e9, 1.1e9, 1.2e9, 1.3e9])  # evenly spaced in frequency
n = 64

paths = []
for i, freq in enumerate(freqs_hz):
    data = np.full((n, n), float(i), dtype=np.float32)
    header = fits.Header()
    header["CRVAL3"] = freq
    header["CTYPE3"] = "FREQ"
    header["CUNIT3"] = "Hz"
    path = workdir / f"chan_{i:04d}.fits"
    fits.writeto(path, data[np.newaxis, :, :], header, overwrite=True)
    paths.append(path)

sorted(p.name for p in paths)
['chan_0000.fits', 'chan_0001.fits', 'chan_0002.fits', 'chan_0003.fits']

Combining images into a cube

combine_fits() stacks the per-channel images along a new spectral (or time) axis, building the WCS for that axis from the per-image headers. The resulting cube is written to disk:

from fitscube_rs import combine_fits

out_cube = workdir / "cube.fits"
freqs = combine_fits(
    [str(p) for p in sorted(paths)],
    str(out_cube),
    overwrite=True,
)

with fits.open(out_cube) as hdul:
    print("cube shape:", hdul[0].data.shape)
    print("frequencies (Hz):", freqs)
cube shape: (4, 64, 64)
frequencies (Hz): [1000000000.0, 1100000000.0, 1200000000.0, 1300000000.0]

The frequency axis is detected as evenly spaced, so the cube header carries a linear CRVAL3/CDELT3 description. When the spacing is uneven, fitscube-rs instead writes an explicit per-plane frequency table so no information is lost (see Algorithm background).

Extracting a plane

extract_plane_from_cube() pulls a single plane back out of a cube — the inverse of combining — which is handy for inspecting one channel or feeding a downstream tool that expects a 2D image:

from fitscube_rs import extract_plane_from_cube

plane_path = workdir / "chan2.fits"
extract_plane_from_cube(
    str(out_cube), channel_index=2, output_path=str(plane_path), overwrite=True
)

with fits.open(plane_path) as hdul:
    print("plane shape:", hdul[0].data.shape)
    print("plane value:", float(np.nanmean(hdul[0].data)))
plane shape: (1, 64, 64)
plane value: 2.0

Per-channel beams

Radio images often carry a restoring beam (BMAJ/BMIN/BPA) that varies from channel to channel. When the input images have differing beams, the combined cube records them in a CASA-style BEAMS binary-table extension and sets CASAMBM=T in the primary header, matching the convention used by astropy and CASA:

with fits.open(out_cube) as hdul:
    has_beams = any(h.name == "BEAMS" for h in hdul)
    print("primary HDUs:", [h.name or "PRIMARY" for h in hdul])
    print("multi-beam table present:", has_beams)
primary HDUs: ['PRIMARY']
multi-beam table present: False

Working with the CLI

For batch jobs (hundreds of channels, large mosaics) prefer the fitscubers CLI — it parallelises the read/stack across workers and streams planes to disk so peak memory stays bounded:

fitscubers combine chan_*.fits cube.fits --overwrite
fitscubers extract cube.fits --channel-index 2 --output-path chan2.fits