From 3b64d5c623e1220bb80ecb3537ef95d4de7342dc Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 13:33:59 -0500 Subject: [PATCH 1/9] set jax requirement --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index d2b0ecc0..6f262c2c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ corner emcee graphviz ipywidgets -jax +jax<0.8.0 jupyter-book<2.0 matplotlib nbformat From ba6747f4ff019f9f2cc23f9dd7e32e2217bb5d51 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 14:00:01 -0500 Subject: [PATCH 2/9] fix gaussian ellipsoid --- astrophot/models/func/gaussian_ellipsoid.py | 2 +- astrophot/models/gaussian_ellipsoid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/astrophot/models/func/gaussian_ellipsoid.py b/astrophot/models/func/gaussian_ellipsoid.py index 2a989f61..4b07e9cf 100644 --- a/astrophot/models/func/gaussian_ellipsoid.py +++ b/astrophot/models/func/gaussian_ellipsoid.py @@ -17,7 +17,7 @@ def euler_rotation_matrix(alpha: ArrayLike, beta: ArrayLike, gamma: ArrayLike) - ( backend.stack((ca * cg - cb * sa * sg, -ca * sg - cb * cg * sa, sb * sa)), backend.stack((cg * sa + ca * cb * sg, ca * cb * cg - sa * sg, -ca * sb)), - backend.stack((sb * cg, sb * cg, cb)), + backend.stack((sb * sg, sb * cg, cb)), ), dim=-1, ) diff --git a/astrophot/models/gaussian_ellipsoid.py b/astrophot/models/gaussian_ellipsoid.py index 02cd54bd..2d14da51 100644 --- a/astrophot/models/gaussian_ellipsoid.py +++ b/astrophot/models/gaussian_ellipsoid.py @@ -130,6 +130,6 @@ def brightness( v = backend.stack(self.transform_coordinates(x, y), dim=0).reshape(2, -1) return ( flux - * backend.sum(backend.exp(-0.5 * (v * (inv_Sigma @ v))), dim=0) + * backend.exp(-0.5 * backend.sum(v * (inv_Sigma @ v), dim=0)) / (2 * np.pi * backend.sqrt(backend.linalg.det(Sigma2D))) ).reshape(x.shape) From b536fcbc656fac4bb4f0dedb19cff610fa389518 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 14:02:43 -0500 Subject: [PATCH 3/9] set jax limit in toml file too --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7bf255b0..ca6367a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Repository = "https://github.com/Autostronomy/AstroPhot" Issues = "https://github.com/Autostronomy/AstroPhot/issues" [project.optional-dependencies] -dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax"] +dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax<0.8.0"] [project.scripts] astrophot = "astrophot:run_from_terminal" From 4585c4969297c1495c825fa77e128fadf8cb6920 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 14:46:05 -0500 Subject: [PATCH 4/9] hard fix jax version see if that works --- docs/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6f262c2c..aee8317d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ corner emcee graphviz ipywidgets -jax<0.8.0 +jax=0.7.0 jupyter-book<2.0 matplotlib nbformat diff --git a/pyproject.toml b/pyproject.toml index ca6367a7..a7ed09a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Repository = "https://github.com/Autostronomy/AstroPhot" Issues = "https://github.com/Autostronomy/AstroPhot/issues" [project.optional-dependencies] -dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax<0.8.0"] +dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax=0.7.0"] [project.scripts] astrophot = "astrophot:run_from_terminal" From 7b2665dbe4d59b6ea7ae70b5a5c1aef76b047870 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 14:49:30 -0500 Subject: [PATCH 5/9] my bad --- docs/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index aee8317d..7b147e6f 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ corner emcee graphviz ipywidgets -jax=0.7.0 +jax==0.7.0 jupyter-book<2.0 matplotlib nbformat diff --git a/pyproject.toml b/pyproject.toml index a7ed09a2..8d4a1896 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Repository = "https://github.com/Autostronomy/AstroPhot" Issues = "https://github.com/Autostronomy/AstroPhot/issues" [project.optional-dependencies] -dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax=0.7.0"] +dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax==0.7.0"] [project.scripts] astrophot = "astrophot:run_from_terminal" From fad3febc5b85fc39a567b3fadf086c1d1b992190 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 21:01:10 -0500 Subject: [PATCH 6/9] set max jax version 0.7.0 as 0.7.2 has breaking change --- docs/requirements.txt | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 7b147e6f..6a5e0c1b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,7 +3,7 @@ corner emcee graphviz ipywidgets -jax==0.7.0 +jax<=0.7.0 jupyter-book<2.0 matplotlib nbformat diff --git a/pyproject.toml b/pyproject.toml index 8d4a1896..1ccadbe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Repository = "https://github.com/Autostronomy/AstroPhot" Issues = "https://github.com/Autostronomy/AstroPhot/issues" [project.optional-dependencies] -dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax==0.7.0"] +dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax<=0.7.0"] [project.scripts] astrophot = "astrophot:run_from_terminal" From 00a3f85b6732b64001fd68a51b94fcdcf784235c Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Tue, 18 Nov 2025 21:20:19 -0500 Subject: [PATCH 7/9] fix segmap auto init --- astrophot/utils/initialize/segmentation_map.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/astrophot/utils/initialize/segmentation_map.py b/astrophot/utils/initialize/segmentation_map.py index 6364ba40..053d257b 100644 --- a/astrophot/utils/initialize/segmentation_map.py +++ b/astrophot/utils/initialize/segmentation_map.py @@ -56,7 +56,7 @@ def centroids_from_segmentation_map( if sky_level is None: sky_level = np.nanmedian(backend.to_numpy(image.data)) - data = backend.to_numpy(image.data) - sky_level + data = backend.to_numpy(image._data) - sky_level centroids = {} II, JJ = np.meshgrid(np.arange(seg_map.shape[0]), np.arange(seg_map.shape[1]), indexing="ij") @@ -94,7 +94,7 @@ def PA_from_segmentation_map( if sky_level is None: sky_level = np.nanmedian(backend.to_numpy(image.data)) - data = backend.to_numpy(image.data) - sky_level + data = backend.to_numpy(image._data) - sky_level if centroids is None: centroids = centroids_from_segmentation_map( @@ -141,7 +141,7 @@ def q_from_segmentation_map( if sky_level is None: sky_level = np.nanmedian(backend.to_numpy(image.data)) - data = backend.to_numpy(image.data) - sky_level + data = backend.to_numpy(image._data) - sky_level if centroids is None: centroids = centroids_from_segmentation_map( @@ -232,8 +232,8 @@ def scale_windows(windows, image: "Image" = None, expand_scale=1.0, expand_borde new_window = [ [max(0, new_window[0][0]), max(0, new_window[0][1])], [ - min(image.data.shape[0], new_window[1][0]), - min(image.data.shape[1], new_window[1][1]), + min(image._data.shape[0], new_window[1][0]), + min(image._data.shape[1], new_window[1][1]), ], ] new_windows[index] = new_window @@ -296,7 +296,7 @@ def filter_windows( if min_flux is not None: if ( np.sum( - backend.to_numpy(image.data)[ + backend.to_numpy(image._data)[ windows[w][0][0] : windows[w][1][0], windows[w][0][1] : windows[w][1][1], ] @@ -307,7 +307,7 @@ def filter_windows( if max_flux is not None: if ( np.sum( - backend.to_numpy(image.data)[ + backend.to_numpy(image._data)[ windows[w][0][0] : windows[w][1][0], windows[w][0][1] : windows[w][1][1], ] From 507564bf540ce60a14c7c579750a6ae13a173c28 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Wed, 19 Nov 2025 13:54:27 -0500 Subject: [PATCH 8/9] add function to collect legacy survey cutouts --- astrophot/image/image_object.py | 7 +- astrophot/utils/__init__.py | 2 + astrophot/utils/fitsopen.py | 108 ++++++++++++++++++++++++ docs/source/tutorials/GroupModels.ipynb | 7 +- 4 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 astrophot/utils/fitsopen.py diff --git a/astrophot/image/image_object.py b/astrophot/image/image_object.py index b8b05e26..6926877a 100644 --- a/astrophot/image/image_object.py +++ b/astrophot/image/image_object.py @@ -448,13 +448,16 @@ def save(self, filename: str): hdulist = fits.HDUList(self.fits_images()) hdulist.writeto(filename, overwrite=True) - def load(self, filename: str, hduext: int = 0): + def load(self, filename: Union[str, fits.HDUList], hduext: int = 0): """Load an image from a FITS file. This will load the primary HDU and set the data, CD, crpix, crval, and crtan attributes accordingly. If the WCS is not tangent plane, it will warn the user. """ - hdulist = fits.open(filename) + if isinstance(filename, str): + hdulist = fits.open(filename) + else: + hdulist = filename self.data = np.array(hdulist[hduext].data, dtype=np.float64) self.CD = ( diff --git a/astrophot/utils/__init__.py b/astrophot/utils/__init__.py index b66971a3..33925367 100644 --- a/astrophot/utils/__init__.py +++ b/astrophot/utils/__init__.py @@ -6,6 +6,7 @@ interpolate, parametric_profiles, ) +from .fitsopen import ls_open __all__ = [ "decorators", @@ -14,4 +15,5 @@ "parametric_profiles", "initialize", "conversions", + "ls_open", ] diff --git a/astrophot/utils/fitsopen.py b/astrophot/utils/fitsopen.py new file mode 100644 index 00000000..0d1b8a80 --- /dev/null +++ b/astrophot/utils/fitsopen.py @@ -0,0 +1,108 @@ +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 +from pyvo.dal import sia +import os + +# Suppress common Astropy warnings that can clutter CI logs +warnings.simplefilter("ignore", category=AstropyWarning) + + +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 + + +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. + """ + + # 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 diff --git a/docs/source/tutorials/GroupModels.ipynb b/docs/source/tutorials/GroupModels.ipynb index b4a719eb..f442b811 100644 --- a/docs/source/tutorials/GroupModels.ipynb +++ b/docs/source/tutorials/GroupModels.ipynb @@ -37,11 +37,8 @@ "outputs": [], "source": [ "# first let's download an image to play with\n", - "hdu = fits.open(\n", - " \"https://www.legacysurvey.org/viewer/fits-cutout?ra=155.7720&dec=15.1494&size=150&layer=ls-dr9&pixscale=0.262&bands=r\"\n", - ")\n", + "hdu = ap.utils.ls_open(155.7720, 15.1494, 150 * 0.262, band=\"r\")\n", "target_data = np.array(hdu[0].data, dtype=np.float64)\n", - "\n", "fig1, ax1 = plt.subplots(figsize=(8, 8))\n", "plt.imshow(np.arctan(target_data / 0.05), origin=\"lower\", cmap=\"inferno\")\n", "plt.axis(\"off\")\n", @@ -61,7 +58,7 @@ "#########################################\n", "from photutils.segmentation import detect_sources, deblend_sources\n", "\n", - "initsegmap = detect_sources(target_data, threshold=0.02, npixels=5)\n", + "initsegmap = detect_sources(target_data, threshold=0.02, npixels=6)\n", "segmap = deblend_sources(target_data, initsegmap, npixels=5).data\n", "fig8, ax8 = plt.subplots(figsize=(8, 8))\n", "ax8.imshow(segmap, origin=\"lower\", cmap=\"inferno\")\n", From ac7b90a6f80122dc4d81c120223f22aa080bbe41 Mon Sep 17 00:00:00 2001 From: Connor Stone Date: Wed, 19 Nov 2025 13:59:27 -0500 Subject: [PATCH 9/9] add pyvo requirement --- astrophot/utils/fitsopen.py | 11 ++++++++++- docs/requirements.txt | 1 + pyproject.toml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/astrophot/utils/fitsopen.py b/astrophot/utils/fitsopen.py index 0d1b8a80..5c9a8d70 100644 --- a/astrophot/utils/fitsopen.py +++ b/astrophot/utils/fitsopen.py @@ -4,7 +4,11 @@ from astropy.io import fits from astropy.utils.exceptions import AstropyWarning from numpy.core.defchararray import startswith -from pyvo.dal import sia + +try: + from pyvo.dal import sia +except: + sia = None import os # Suppress common Astropy warnings that can clutter CI logs @@ -49,6 +53,11 @@ def ls_open(ra, dec, size_arcsec, band="r", release="ls_dr9"): 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()}" diff --git a/docs/requirements.txt b/docs/requirements.txt index 6a5e0c1b..07b09906 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -9,6 +9,7 @@ matplotlib nbformat nbsphinx photutils +pyvo scikit-image sphinx sphinx-rtd-theme diff --git a/pyproject.toml b/pyproject.toml index 1ccadbe8..faaf81cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ Repository = "https://github.com/Autostronomy/AstroPhot" Issues = "https://github.com/Autostronomy/AstroPhot/issues" [project.optional-dependencies] -dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax<=0.7.0"] +dev = ["pre-commit", "nbval", "nbconvert", "graphviz", "ipywidgets", "jupyter-book", "matplotlib", "photutils", "scikit-image", "caustics", "emcee", "corner", "jax<=0.7.0", "pyvo"] [project.scripts] astrophot = "astrophot:run_from_terminal"