Source code for astrophot.utils.fitsopen
import numpy as np
import warnings
from astropy.utils.data import download_file
from astropy.io import fits
from astropy.utils.exceptions import AstropyWarning
from numpy.core.defchararray import startswith
try:
from pyvo.dal import sia
except:
sia = None
import os
# Suppress common Astropy warnings that can clutter CI logs
warnings.simplefilter("ignore", category=AstropyWarning)
[docs]
def flip_hdu(hdu):
"""
Flips the image data in the FITS HDU on the RA axis to match the expected orientation.
Args:
hdu (astropy.io.fits.HDUList): The FITS HDU to be flipped.
Returns:
astropy.io.fits.HDUList: The flipped FITS HDU.
"""
assert "CD1_1" in hdu[0].header, "HDU does not contain WCS information."
assert "CD2_1" in hdu[0].header, "HDU does not contain WCS information."
assert "CRPIX1" in hdu[0].header, "HDU does not contain WCS information."
assert "NAXIS1" in hdu[0].header, "HDU does not contain WCS information."
hdu[0].data = hdu[0].data[:, ::-1].copy()
hdu[0].header["CD1_1"] = -hdu[0].header["CD1_1"]
hdu[0].header["CD2_1"] = -hdu[0].header["CD2_1"]
hdu[0].header["CRPIX1"] = int(hdu[0].header["NAXIS1"] / 2) + 1
hdu[0].header["CRPIX2"] = int(hdu[0].header["NAXIS2"] / 2) + 1
return hdu
[docs]
def ls_open(ra, dec, size_arcsec, band="r", release="ls_dr9"):
"""
Retrieves and opens a FITS cutout from the deepest stacked image in the
specified Legacy Survey data release using the Astro Data Lab SIA service.
Args:
ra (float): Right Ascension in decimal degrees.
dec (float): Declination in decimal degrees.
size_arcsec (float): Size of the square cutout (side length) in arcseconds.
band (str): The filter band (e.g., 'g', 'r', 'z'). Case-insensitive.
release (str): The Legacy Survey Data Release (e.g., 'DR9').
Returns:
astropy.io.fits.HDUList: The opened FITS file object.
"""
if sia is None:
raise ImportError(
"Cannot use ls_open without pyvo. Please install pyvo (pip install pyvo) before continuing."
)
# 1. Set the specific SIA service endpoint for the desired release
# SIA endpoints for specific surveys are listed in the notebook.
service_url = f"https://datalab.noirlab.edu/sia/{release.lower()}"
svc = sia.SIAService(service_url)
# 2. Convert size from arcseconds to degrees (FOV) for the SIA query
# and apply the cosine correction for RA.
fov_deg = size_arcsec / 3600.0
# The search method takes the position (RA, Dec) and the square FOV.
imgTable = svc.search(
(ra, dec), (fov_deg / np.cos(dec * np.pi / 180.0), fov_deg), verbosity=2
).to_table()
# 3. Filter the table for stacked images in the specified band
target_band = band.lower()
sel = (
(imgTable["proctype"] == "Stack")
& (imgTable["prodtype"] == "image")
& (startswith(imgTable["obs_bandpass"].astype(str), target_band))
)
Table = imgTable[sel]
if len(Table) == 0:
raise ValueError(
f"No stacked FITS image found for {release} band '{band}' at the requested RA {ra} and Dec {dec}."
)
# 4. Pick the "deepest" image (longest exposure time)
# Note: 'exptime' data needs explicit float conversion for np.argmax
max_exptime_index = np.argmax(Table["exptime"].data.data.astype("float"))
row = Table[max_exptime_index]
# 5. Download the file and open it
url = row["access_url"] # get the download URL
# Use astropy's download_file, which handles the large data transfer
# and automatically uses a long timeout (120s in the notebook example)
filename = download_file(url, cache=False, show_progress=False, timeout=120)
# Open the downloaded FITS file
hdu = fits.open(filename)
try:
hdu = flip_hdu(hdu)
except AssertionError:
pass # If WCS info is missing, skip flipping
# Clean up the temporary file created by download_file
try:
os.remove(filename)
except OSError:
pass # Ignore if cleanup fails
return hdu