diff --git a/astrocut/__init__.py b/astrocut/__init__.py index 9249d8e4..fc30bdd3 100644 --- a/astrocut/__init__.py +++ b/astrocut/__init__.py @@ -27,3 +27,4 @@ class UnsupportedPythonError(Exception): from .cutouts import fits_cut, img_cut, normalize_img # noqa from .cutout_processing import (path_to_footprints, center_on_path, # noqa CutoutsCombiner, build_default_combine_function) # noqa + from .asdf_cutouts import asdf_cut, get_center_pixel # noqa diff --git a/astrocut/asdf_cutouts.py b/astrocut/asdf_cutouts.py index b72ef6dc..3f039a44 100644 --- a/astrocut/asdf_cutouts.py +++ b/astrocut/asdf_cutouts.py @@ -16,7 +16,8 @@ def _get_cloud_http(s3_uri: str) -> str: - """ Get the HTTP URI of a cloud resource from an S3 URI + """ + Get the HTTP URI of a cloud resource from an S3 URI. Parameters ---------- @@ -24,7 +25,7 @@ def _get_cloud_http(s3_uri: str) -> str: the S3 URI of the cloud resource """ # create file system - fs = s3fs.S3FileSystem(anon=True) + fs = s3fs.S3FileSystem() # open resource and get URL with fs.open(s3_uri, 'rb') as f: @@ -32,7 +33,8 @@ def _get_cloud_http(s3_uri: str) -> str: def get_center_pixel(gwcsobj: gwcs.wcs.WCS, ra: float, dec: float) -> tuple: - """ Get the center pixel from a roman 2d science image + """ + Get the center pixel from a Roman 2D science image. For an input RA, Dec sky coordinate, get the closest pixel location on the input Roman image. @@ -40,16 +42,16 @@ def get_center_pixel(gwcsobj: gwcs.wcs.WCS, ra: float, dec: float) -> tuple: Parameters ---------- gwcsobj : gwcs.wcs.WCS - the Roman GWCS object + The Roman GWCS object. ra : float - the input Right Ascension + The input right ascension. dec : float - the input Declination + The input declination. Returns ------- tuple - the pixel position, FITS wcs object + The pixel position, FITS wcs object """ # Convert the gwcs object to an astropy FITS WCS header @@ -73,11 +75,12 @@ def get_center_pixel(gwcsobj: gwcs.wcs.WCS, ra: float, dec: float) -> tuple: return (row, col), wcs_updated -def get_cutout(data: asdf.tags.core.ndarray.NDArrayType, coords: Union[tuple, SkyCoord], - wcs: astropy.wcs.wcs.WCS = None, size: int = 20, outfile: str = "example_roman_cutout.fits", - write_file: bool = True, fill_value: Union[int, float] = np.nan, - gwcsobj: gwcs.wcs.WCS = None) -> astropy.nddata.Cutout2D: - """ Get a Roman image cutout +def _get_cutout(data: asdf.tags.core.ndarray.NDArrayType, coords: Union[tuple, SkyCoord], + wcs: astropy.wcs.wcs.WCS = None, size: int = 20, outfile: str = "example_roman_cutout.fits", + write_file: bool = True, fill_value: Union[int, float] = np.nan, + gwcsobj: gwcs.wcs.WCS = None) -> astropy.nddata.Cutout2D: + """ + Get a Roman image cutout. Cut out a square section from the input image data array. The ``coords`` can either be a tuple of x, y pixel coordinates or an astropy SkyCoord object, in which case, a wcs is required. Writes out a @@ -154,7 +157,8 @@ def get_cutout(data: asdf.tags.core.ndarray.NDArrayType, coords: Union[tuple, Sk def _write_fits(cutout: astropy.nddata.Cutout2D, outfile: str = "example_roman_cutout.fits"): - """ Write cutout as FITS file + """ + Write cutout as FITS file. Parameters ---------- @@ -173,7 +177,8 @@ def _write_fits(cutout: astropy.nddata.Cutout2D, outfile: str = "example_roman_c def _slice_gwcs(gwcsobj: gwcs.wcs.WCS, slices: Tuple[slice, slice]) -> gwcs.wcs.WCS: - """ Slice the original gwcs object + """ + Slice the original gwcs object. "Slices" the original gwcs object down to the cutout shape. This is a hack until proper gwcs slicing is in place a la fits WCS slicing. The ``slices`` @@ -211,7 +216,8 @@ def _slice_gwcs(gwcsobj: gwcs.wcs.WCS, slices: Tuple[slice, slice]) -> gwcs.wcs. def _write_asdf(cutout: astropy.nddata.Cutout2D, gwcsobj: gwcs.wcs.WCS, outfile: str = "example_roman_cutout.asdf"): - """ Write cutout as ASDF file + """ + Write cutout as ASDF file. Parameters ---------- @@ -236,32 +242,33 @@ def _write_asdf(cutout: astropy.nddata.Cutout2D, gwcsobj: gwcs.wcs.WCS, outfile: def asdf_cut(input_file: str, ra: float, dec: float, cutout_size: int = 20, output_file: str = "example_roman_cutout.fits", write_file: bool = True, fill_value: Union[int, float] = np.nan) -> astropy.nddata.Cutout2D: - """ Preliminary proof-of-concept functionality. + """ + Takes a single ASDF input file (`input_file`) and generates a cutout of designated size `cutout_size` + around the given coordinates (`coordinates`). - Takes a single ASDF input file (``input_file``) and generates a cutout of designated size ``cutout_size`` - around the given coordinates (``coordinates``). + Preliminary proof-of-concept functionality. Parameters ---------- input_file : str - the input ASDF file + The input ASDF file. ra : float - the Right Ascension of the central cutout + The right ascension of the central cutout. dec : float - the Declination of the central cutout - cutout_size : int, optional - the image cutout pixel size, by default 20 - output_file : str, optional - the name of the output cutout file, by default "example_roman_cutout.fits" - write_file : bool, by default True - Flag to write the cutout to a file or not - fill_value: int | float, by default np.nan - The fill value for pixels outside the original image. + The declination of the central cutout. + cutout_size : int + Optional, default 20. The image cutout pixel size. + output_file : str + Optional, default "example_roman_cutout.fits". The name of the output cutout file. + write_file : bool + Optional, default True. Flag to write the cutout to a file or not. + fill_value: int | float + Optional, default `np.nan`. The fill value for pixels outside the original image. Returns ------- astropy.nddata.Cutout2D: - an image cutout object + An image cutout object. """ # if file comes from AWS cloud bucket, get HTTP URL to open with asdf @@ -278,5 +285,5 @@ def asdf_cut(input_file: str, ra: float, dec: float, cutout_size: int = 20, pixel_coordinates, wcs = get_center_pixel(gwcsobj, ra, dec) # create the 2d image cutout - return get_cutout(data, pixel_coordinates, wcs, size=cutout_size, outfile=output_file, - write_file=write_file, fill_value=fill_value, gwcsobj=gwcsobj) + return _get_cutout(data, pixel_coordinates, wcs, size=cutout_size, outfile=output_file, + write_file=write_file, fill_value=fill_value, gwcsobj=gwcsobj) diff --git a/astrocut/tests/test_asdf_cut.py b/astrocut/tests/test_asdf_cut.py index 0193674d..70c0c968 100644 --- a/astrocut/tests/test_asdf_cut.py +++ b/astrocut/tests/test_asdf_cut.py @@ -13,7 +13,7 @@ from astropy.wcs.utils import pixel_to_skycoord from gwcs import wcs from gwcs import coordinate_frames as cf -from astrocut.asdf_cutouts import get_center_pixel, get_cutout, asdf_cut, _slice_gwcs, _get_cloud_http +from astrocut.asdf_cutouts import get_center_pixel, asdf_cut, _get_cutout, _slice_gwcs, _get_cloud_http def make_wcs(xsize, ysize, ra=30., dec=45.): @@ -137,7 +137,7 @@ def test_get_cutout(output, fakedata, quantity): data = data.value # create cutout - cutout = get_cutout(data, skycoord, wcs, size=10, outfile=output_file) + cutout = _get_cutout(data, skycoord, wcs, size=10, outfile=output_file) assert_same_coord(5, 10, cutout, wcs) @@ -185,7 +185,7 @@ def test_fail_write_asdf(fakedata, output): data, gwcs = fakedata skycoord = gwcs(25, 25, with_units=True) wcs = WCS(gwcs.to_fits_sip()) - get_cutout(data, skycoord, wcs, size=10, outfile=output_file) + _get_cutout(data, skycoord, wcs, size=10, outfile=output_file) def test_cutout_nofile(make_file, output): @@ -217,7 +217,7 @@ def test_cutout_poles(makefake): wcs = WCS(gwcs.to_fits_sip()) # get cutout - cutout = get_cutout(data, cc, wcs, size=50, write_file=False) + cutout = _get_cutout(data, cc, wcs, size=50, write_file=False) assert_same_coord(5, 10, cutout, wcs) # check cutout contains all data @@ -232,7 +232,7 @@ def test_fail_cutout_outside(fakedata): with pytest.raises(RuntimeError, match='Could not create 2d cutout. The requested ' 'cutout does not overlap with the original image'): - get_cutout(data, cc, wcs, size=50, write_file=False) + _get_cutout(data, cc, wcs, size=50, write_file=False) def assert_same_coord(x, y, cutout, wcs): @@ -251,7 +251,7 @@ def test_partial_cutout(makefake, asint, fill): wcs = WCS(gwcs.to_fits_sip()) cc = coord.SkyCoord(29.999, 44.998, unit=u.degree) - cutout = get_cutout(data, cc, wcs, size=50, write_file=False, fill_value=fill) + cutout = _get_cutout(data, cc, wcs, size=50, write_file=False, fill_value=fill) assert cutout.shape == (50, 50) if asint: assert -9999 in cutout.data @@ -266,7 +266,7 @@ def test_bad_fill(makefake): wcs = WCS(gwcs.to_fits_sip()) cc = coord.SkyCoord(29.999, 44.998, unit=u.degree) with pytest.raises(ValueError, match='fill_value is inconsistent with the data type of the input array'): - get_cutout(data, cc, wcs, size=50, write_file=False) + _get_cutout(data, cc, wcs, size=50, write_file=False) def test_cutout_raedge(makefake): @@ -284,7 +284,7 @@ def test_cutout_raedge(makefake): wcs = WCS(gg.to_fits_sip()) # get cutout - cutout = get_cutout(data, cc, wcs, size=100, write_file=False) + cutout = _get_cutout(data, cc, wcs, size=100, write_file=False) assert_same_coord(5, 10, cutout, wcs) # assert the RA cutout bounds are > 359 and < 0 @@ -299,7 +299,7 @@ def test_slice_gwcs(fakedata): skycoord = gwcsobj(250, 250) wcs = WCS(gwcsobj.to_fits_sip()) - cutout = get_cutout(data, skycoord, wcs, size=50, write_file=False) + cutout = _get_cutout(data, skycoord, wcs, size=50, write_file=False) sliced = _slice_gwcs(gwcsobj, cutout.slices_original) @@ -329,6 +329,6 @@ def test_get_cloud_http(mock_s3fs): http_uri = _get_cloud_http(s3_uri) assert http_uri == HTTP_URI - mock_s3fs.assert_called_once_with(anon=True) + mock_s3fs.assert_called_once_with() mock_fs.open.assert_called_once_with(s3_uri, 'rb') mock_file.url.assert_called_once() diff --git a/docs/astrocut/file_formats.rst b/docs/astrocut/file_formats.rst index 5ca7fc0b..685381fa 100644 --- a/docs/astrocut/file_formats.rst +++ b/docs/astrocut/file_formats.rst @@ -39,6 +39,26 @@ it contains the name of the file the cutout comes from. +ASDF Cutout Files +================== + +ASDF files output by `asdf_cut` are a minimal tree structure that mirrors the format of the original Roman image file. + +.. code-block:: python + + asdf_cutout = { + "roman": { + "meta": { + "wcs" - the gwcs of the cutout + }, + "data" - the cutout data + } + } + +`wcs` is the original `gwcs` object from the input ASDF file that has been sliced into the shape of the cutout. + + + Cube Files ========== diff --git a/docs/astrocut/index.rst b/docs/astrocut/index.rst index deace98f..5b93052e 100644 --- a/docs/astrocut/index.rst +++ b/docs/astrocut/index.rst @@ -17,7 +17,7 @@ Three main areas of functionality are included: -FITS file image cutouts +FITS File Image Cutouts ======================= These functions provide general purpose astronomical cutout functionality for FITS files. @@ -25,7 +25,7 @@ There are two main cutout functions, `~astrocut.fits_cut` for creating cutout FI and `~astrocut.img_cut` for creating cutout JPG or PNG files. An image normalization (`~astrocut.normalize_img`) function is also available. -Creating FITS cutouts +Creating FITS Cutouts --------------------- The function `~astrocut.fits_cut` takes one or more FITS files and performs the same cutout @@ -107,7 +107,7 @@ The cutout(s) can also be returned in memory as `~astropy.io.fits.HDUList` objec 1 CUTOUT 1 ImageHDU 97 (100, 100) float32 -Creating image cutouts +Creating Image Cutouts ---------------------- The function `~astrocut.img_cut` takes one or more FITS files and performs the same cutout @@ -179,7 +179,7 @@ the same Sector, camera, and CCD. If you are creating a small number of cutouts, the TESSCut web service may suit your needs: `mast.stsci.edu/tesscut `_ -Making image cubes +Making Image Cubes ------------------ .. important:: @@ -250,7 +250,7 @@ The output image cube file format is described `here `__. + +.. code-block:: python + + >>> from astrocut import asdf_cut + >>> from astropy.coordinates import SkyCoord + >>> from astropy.io import fits + + >>> input_file = "" # Path to local ASDF file or URI + + >>> center_coord = SkyCoord("80.15189743 29.74561219", unit='deg') + + >>> cutout_file = asdf_cut(input_file, center_coord.ra, center_coord.dec, cutout_size=200, + ... output_file="roman-demo.fits") #doctest: +SKIP + + >>> cutout_hdulist = fits.open(cutout_file) #doctest: +SKIP + >>> cutout_hdulist.info() #doctest: +SKIP + Filename: roman-demo.fits + No. Name Ver Type Cards Dimensions Format + 0 PRIMARY 1 PrimaryHDU 25 (200, 200) float32 + +`asdf_cut` accepts S3 URIs to perform cutouts on ASDF files from the cloud. +In this example, a cutout is performed on a cloud file and written as an ASDF file. The cutout ASDF file format is described `here `__. + +.. code-block:: python + + >>> from astrocut import asdf_cut + >>> from astropy.coordinates import SkyCoord + + >>> s3_uri = "s3://..." # Complete URI + + >>> center_coord = SkyCoord("80.15189743 29.74561219", unit='deg') + + >>> cutout_file = asdf_cut(s3_uri, center_coord.ra, center_coord.dec, cutout_size=200, + ... output_file="roman-demo.asdf") #doctest: +SKIP + +When requesting a cutout that is partially outside of image bounds, the `fill_value` parameter is used to preserve the cutout shape and fill outside pixels. + + + Additional Cutout Processing ============================ -Path-based cutouts +Path-based Cutouts ------------------ The `~astrocut.center_on_path` function allows the user to take one or more Astrocut cutout @@ -436,7 +487,7 @@ cutout location/size(s) necesary to cover the entire path. 2 APERTURE 1 ImageHDU 97 (2136, 2078) int32 -Combining cutouts +Combining Cutouts ----------------- The `~astrocut.CutoutsCombiner` class allows the user to take one or more Astrocut cutout