Skip to content

Commit 608e9ec

Browse files
authored
Merge pull request #2613 from jaymedina/add-hapcut-mast
Adding HAPCut functionality to `astroquery.mast.Cutouts` tool
2 parents 5057cbc + e773121 commit 608e9ec

File tree

5 files changed

+300
-5
lines changed

5 files changed

+300
-5
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ mast
128128
- Add a ``flat`` option to ``Observation.download_products()`` to turn off the
129129
automatic creation and organizing of products into subdirectories. [#2511]
130130

131+
- Expanding ``Cutouts`` functionality to support making Hubble Advanced Product (HAP)
132+
cutouts via HAPCut. [#2613]
133+
131134
oac
132135
^^^
133136

astroquery/mast/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class Conf(_config.ConfigNamespace):
3030

3131
conf = Conf()
3232

33-
from .cutouts import TesscutClass, Tesscut, ZcutClass, Zcut
33+
from .cutouts import TesscutClass, Tesscut, ZcutClass, Zcut, HapcutClass, Hapcut
3434
from .observations import Observations, ObservationsClass, MastClass, Mast
3535
from .collections import Catalogs, CatalogsClass
3636
from .missions import MastMissions, MastMissionsClass
@@ -42,5 +42,6 @@ class Conf(_config.ConfigNamespace):
4242
'Mast', 'MastClass',
4343
'Tesscut', 'TesscutClass',
4444
'Zcut', 'ZcutClass',
45+
'Hapcut', 'HapcutClass',
4546
'Conf', 'conf', 'utils',
4647
]

astroquery/mast/cutouts.py

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def get_sectors(self, *, coordinates=None, radius=0*u.deg, objectname=None, movi
201201
return Table(sector_dict)
202202

203203
def download_cutouts(self, *, coordinates=None, size=5, sector=None, path=".", inflate=True,
204-
objectname=None, moving_target=False, mt_type=None):
204+
objectname=None, moving_target=False, mt_type=None, verbose=False):
205205
"""
206206
Download cutout target pixel file(s) around the given coordinates with indicated size.
207207
@@ -301,7 +301,7 @@ def download_cutouts(self, *, coordinates=None, size=5, sector=None, path=".", i
301301
localpath_table['Local Path'] = [zipfile_path]
302302
return localpath_table
303303

304-
print("Inflating...")
304+
if verbose: print("Inflating...")
305305
# unzipping the zipfile
306306
zip_ref = zipfile.ZipFile(zipfile_path, 'r')
307307
cutout_files = zip_ref.namelist()
@@ -477,7 +477,8 @@ def get_surveys(self, coordinates, *, radius="0d"):
477477
warnings.warn("Coordinates are not in an available deep field survey.", NoResultsWarning)
478478
return survey_json
479479

480-
def download_cutouts(self, coordinates, *, size=5, survey=None, cutout_format="fits", path=".", inflate=True, **img_params):
480+
def download_cutouts(self, coordinates, *, size=5, survey=None, cutout_format="fits", path=".", inflate=True,
481+
verbose=False, **img_params):
481482
"""
482483
Download cutout FITS/image file(s) around the given coordinates with indicated size.
483484
@@ -560,7 +561,7 @@ def download_cutouts(self, coordinates, *, size=5, survey=None, cutout_format="f
560561
localpath_table['Local Path'] = [zipfile_path]
561562
return localpath_table
562563

563-
print("Inflating...")
564+
if verbose: print("Inflating...")
564565
# unzipping the zipfile
565566
zip_ref = zipfile.ZipFile(zipfile_path, 'r')
566567
cutout_files = zip_ref.namelist()
@@ -637,3 +638,164 @@ def get_cutouts(self, coordinates, *, size=5, survey=None):
637638

638639

639640
Zcut = ZcutClass()
641+
642+
643+
class HapcutClass(MastQueryWithLogin):
644+
"""
645+
MAST Hubble Advanced Product (HAP) cutout query class.
646+
647+
Class for accessing HAP image cutouts.
648+
"""
649+
650+
def __init__(self):
651+
652+
super().__init__()
653+
654+
services = {"astrocut": {"path": "astrocut"}}
655+
656+
self._service_api_connection.set_service_params(services, "hapcut")
657+
658+
def download_cutouts(self, coordinates, *, size=5, path=".", inflate=True, verbose=False):
659+
"""
660+
Download cutout images around the given coordinates with indicated size.
661+
662+
Parameters
663+
----------
664+
coordinates : str or `astropy.coordinates` object
665+
The target around which to search. It may be specified as a
666+
string or as the appropriate `astropy.coordinates` object.
667+
size : int, array-like, `~astropy.units.Quantity`
668+
Optional, default 5 pixels.
669+
The size of the cutout array. If ``size`` is a scalar number or
670+
a scalar `~astropy.units.Quantity`, then a square cutout of ``size``
671+
will be created. If ``size`` has two elements, they should be in
672+
``(ny, nx)`` order. Scalar numbers in ``size`` are assumed to be in
673+
units of pixels. `~astropy.units.Quantity` objects must be in pixel or
674+
angular units.
675+
path : str
676+
Optional.
677+
The directory in which the cutouts will be saved.
678+
Defaults to current directory.
679+
inflate : bool
680+
Optional, default True.
681+
Cutout target pixel files are returned from the server in a zip file,
682+
by default they will be inflated and the zip will be removed.
683+
Set inflate to false to stop before the inflate step.
684+
685+
Returns
686+
-------
687+
response : `~astropy.table.Table`
688+
"""
689+
690+
# Get Skycoord object for coordinates/object
691+
coordinates = parse_input_location(coordinates)
692+
693+
# Build initial astrocut request
694+
astrocut_request = f"astrocut?ra={coordinates.ra.deg}&dec={coordinates.dec.deg}"
695+
696+
# Add size parameters to request
697+
size_dict = _parse_cutout_size(size)
698+
astrocut_request += f"&x={size_dict['x']}&y={size_dict['y']}&units={size_dict['units']}"
699+
700+
# Build the URL
701+
astrocut_url = self._service_api_connection.REQUEST_URL + astrocut_request
702+
703+
# Set up the download path
704+
path = os.path.join(path, '')
705+
zipfile_path = "{}hapcut_{}.zip".format(path, time.strftime("%Y%m%d%H%M%S"))
706+
707+
# Download
708+
self._download_file(astrocut_url, zipfile_path)
709+
localpath_table = Table(names=["Local Path"], dtype=[str])
710+
711+
# Checking if we got a zip file or a json no results message
712+
if not zipfile.is_zipfile(zipfile_path):
713+
with open(zipfile_path, 'r') as FLE:
714+
response = json.load(FLE)
715+
warnings.warn(response['msg'], NoResultsWarning)
716+
return localpath_table
717+
718+
if not inflate: # not unzipping
719+
localpath_table['Local Path'] = [zipfile_path]
720+
return localpath_table
721+
722+
if verbose: print("Inflating...")
723+
# unzipping the zipfile
724+
zip_ref = zipfile.ZipFile(zipfile_path, 'r')
725+
cutout_files = zip_ref.namelist()
726+
zip_ref.extractall(path, members=cutout_files)
727+
zip_ref.close()
728+
os.remove(zipfile_path)
729+
730+
localpath_table['Local Path'] = [path+x for x in cutout_files]
731+
return localpath_table
732+
733+
734+
def get_cutouts(self, coordinates, *, size=5):
735+
"""
736+
Get cutout image(s) around the given coordinates with indicated size,
737+
and return them as a list of `~astropy.io.fits.HDUList` objects.
738+
739+
Parameters
740+
----------
741+
coordinates : str or `astropy.coordinates` object
742+
The target around which to search. It may be specified as a
743+
string or as the appropriate `astropy.coordinates` object.
744+
size : int, array-like, `~astropy.units.Quantity`
745+
Optional, default 5 pixels.
746+
The size of the cutout array. If ``size`` is a scalar number or
747+
a scalar `~astropy.units.Quantity`, then a square cutout of ``size``
748+
will be created. If ``size`` has two elements, they should be in
749+
``(ny, nx)`` order. Scalar numbers in ``size`` are assumed to be in
750+
units of pixels. `~astropy.units.Quantity` objects must be in pixel or
751+
angular units.
752+
753+
Returns
754+
-------
755+
response : A list of `~astropy.io.fits.HDUList` objects.
756+
"""
757+
758+
# Get Skycoord object for coordinates/object
759+
coordinates = parse_input_location(coordinates)
760+
761+
param_dict = _parse_cutout_size(size)
762+
763+
# Need to convert integers from numpy dtypes
764+
# so we can convert the dictionary into a JSON object
765+
# in service_request_async(...)
766+
param_dict["x"] = float(param_dict["x"])
767+
param_dict["y"] = float(param_dict["y"])
768+
769+
# Adding RA and DEC to parameters dictionary
770+
param_dict["ra"] = coordinates.ra.deg
771+
param_dict["dec"] = coordinates.dec.deg
772+
773+
response = self._service_api_connection.service_request_async("astrocut", param_dict, use_json=True)
774+
response.raise_for_status() # Raise any errors
775+
776+
try:
777+
ZIPFILE = zipfile.ZipFile(BytesIO(response.content), 'r')
778+
except zipfile.BadZipFile:
779+
message = response.json()
780+
if len(message['results']) == 0:
781+
warnings.warn(message['msg'], NoResultsWarning)
782+
return []
783+
else:
784+
raise
785+
786+
# Open all the contained fits files:
787+
# Since we cannot seek on a compressed zip file,
788+
# we have to read the data, wrap it in another BytesIO object,
789+
# and then open that using fits.open
790+
cutout_hdus_list = []
791+
for name in ZIPFILE.namelist():
792+
CUTOUT = BytesIO(ZIPFILE.open(name).read())
793+
cutout_hdus_list.append(fits.open(CUTOUT))
794+
795+
# preserve the original filename in the fits object
796+
cutout_hdus_list[-1].filename = name
797+
798+
return cutout_hdus_list
799+
800+
801+
Hapcut = HapcutClass()

astroquery/mast/tests/test_mast_remote.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,3 +1088,76 @@ def test_zcut_get_cutouts(self):
10881088
assert isinstance(cutout_list, list)
10891089
assert len(cutout_list) == 1
10901090
assert isinstance(cutout_list[0], fits.HDUList)
1091+
1092+
###################
1093+
# HapcutClass tests #
1094+
###################
1095+
1096+
def test_hapcut_download_cutouts(self, tmpdir):
1097+
1098+
# Test 1: Simple API call with expected results
1099+
coord = SkyCoord(351.347812, 28.497808, unit="deg")
1100+
1101+
cutout_table = mast.Hapcut.download_cutouts(coordinates=coord, size=5, path=str(tmpdir))
1102+
assert isinstance(cutout_table, Table)
1103+
assert len(cutout_table) >= 1
1104+
for row in cutout_table:
1105+
assert os.path.isfile(row['Local Path'])
1106+
if 'fits' in os.path.basename(row['Local Path']):
1107+
assert fits.getdata(row['Local Path']).shape == (5, 5)
1108+
1109+
# Test 2: Make input size a list
1110+
cutout_table = mast.Hapcut.download_cutouts(coordinates=coord, size=[2, 3], path=str(tmpdir))
1111+
assert isinstance(cutout_table, Table)
1112+
assert len(cutout_table) >= 1
1113+
for row in cutout_table:
1114+
assert os.path.isfile(row['Local Path'])
1115+
if 'fits' in os.path.basename(row['Local Path']):
1116+
assert fits.getdata(row['Local Path']).shape == (3, 2)
1117+
1118+
# Test 3: Specify unit for input size
1119+
cutout_table = mast.Hapcut.download_cutouts(coordinates=coord, size=5*u.arcsec, path=str(tmpdir))
1120+
assert isinstance(cutout_table, Table)
1121+
assert len(cutout_table) >= 1
1122+
for row in cutout_table:
1123+
assert os.path.isfile(row['Local Path'])
1124+
1125+
# Test 4: Intentional API call with no results
1126+
bad_coord = SkyCoord(102.7, 70.50, unit="deg")
1127+
with pytest.warns(NoResultsWarning, match='Missing HAP files for input target. Cutout not performed.'):
1128+
cutout_table = mast.Hapcut.download_cutouts(coordinates=bad_coord, size=5, path=str(tmpdir))
1129+
assert isinstance(cutout_table, Table)
1130+
assert len(cutout_table) == 0
1131+
1132+
def test_hapcut_get_cutouts(self):
1133+
1134+
# Test 1: Simple API call with expected results
1135+
coord = SkyCoord(351.347812, 28.497808, unit="deg")
1136+
1137+
cutout_list = mast.Hapcut.get_cutouts(coordinates=coord)
1138+
assert isinstance(cutout_list, list)
1139+
assert len(cutout_list) >= 1
1140+
assert isinstance(cutout_list[0], fits.HDUList)
1141+
assert cutout_list[0][1].data.shape == (5, 5)
1142+
1143+
# Test 2: Make input size a list
1144+
cutout_list = mast.Hapcut.get_cutouts(coordinates=coord, size=[2, 3])
1145+
assert isinstance(cutout_list, list)
1146+
assert len(cutout_list) >= 1
1147+
assert isinstance(cutout_list[0], fits.HDUList)
1148+
assert cutout_list[0][1].data.shape == (3, 2)
1149+
1150+
# Test 3: Specify unit for input size
1151+
cutout_list = mast.Hapcut.get_cutouts(coordinates=coord, size=5*u.arcsec)
1152+
assert isinstance(cutout_list, list)
1153+
assert len(cutout_list) >= 1
1154+
assert isinstance(cutout_list[0], fits.HDUList)
1155+
assert cutout_list[0][1].data.shape == (42, 42)
1156+
1157+
# Test 4: Intentional API call with no results
1158+
bad_coord = SkyCoord(102.7, 70.50, unit="deg")
1159+
1160+
with pytest.warns(NoResultsWarning, match='Missing HAP files for input target. Cutout not performed.'):
1161+
cutout_list = mast.Hapcut.get_cutouts(coordinates=bad_coord)
1162+
assert isinstance(cutout_list, list)
1163+
assert len(cutout_list) == 0

docs/mast/mast.rst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,62 @@ To list the available deep field surveys at a particular location there is `~ast
11201120
['candels_gn_60mas', 'candels_gn_30mas', 'goods_north']
11211121

11221122

1123+
HAPCut
1124+
======
1125+
1126+
1127+
HAPCut for MAST allows users to request cutouts from various Hubble Advance Products (HAPs). The cutouts can
1128+
be returned as fits files (image files are not currently supported). This tool can be accessed in
1129+
Astroquery by using the Hapcut class. Documentation for the supported HAPCut API can be found here:
1130+
https://mast.stsci.edu/hapcut/
1131+
1132+
1133+
Cutouts
1134+
-------
1135+
1136+
The `~astroquery.mast.HapcutClass.get_cutouts` function takes a coordinate and cutout size (in pixels or
1137+
an angular quantity) and returns the cutout FITS file(s) as a list of `~astropy.io.fits.HDUList` objects.
1138+
1139+
If the given coordinate appears in more than one product, a FITS file will be produced for each.
1140+
1141+
.. doctest-remote-data::
1142+
1143+
>>> from astroquery.mast import Hapcut
1144+
>>> from astropy.coordinates import SkyCoord
1145+
...
1146+
>>> cutout_coord = SkyCoord(351.347812, 28.497808, unit="deg")
1147+
>>> hdulist = Hapcut.get_cutouts(coordinates=cutout_coord, size=5)
1148+
>>> hdulist[0].info() # doctest: +IGNORE_OUTPUT
1149+
Filename: <class '_io.BytesIO'>
1150+
No. Name Ver Type Cards Dimensions Format
1151+
0 PRIMARY 1 PrimaryHDU 754 ()
1152+
1 SCI 1 ImageHDU 102 (5, 5) float32
1153+
2 WHT 1 ImageHDU 56 (5, 5) float32
1154+
1155+
1156+
The `~astroquery.mast.HapcutClass.download_cutouts` function takes a coordinate and cutout size (in pixels or
1157+
an angular quantity) and downloads the cutout fits file(s) as fits files.
1158+
1159+
If the given coordinate appears in more than one product, a cutout will be produced for each.
1160+
1161+
.. doctest-remote-data::
1162+
1163+
>>> from astroquery.mast import Hapcut
1164+
>>> from astropy.coordinates import SkyCoord
1165+
...
1166+
>>> cutout_coord = SkyCoord(351.347812, 28.497808, unit="deg")
1167+
>>> manifest = Hapcut.download_cutouts(coordinates=cutout_coord, size=[50, 100]) # doctest: +IGNORE_OUTPUT
1168+
Downloading URL https://mast.stsci.edu/hapcut/api/v0.1/astrocut?ra=351.347812&dec=28.497808&x=100&y=50&units=px to ./hapcut_20221130112710.zip ... [Done]
1169+
Inflating...
1170+
...
1171+
>>> print(manifest) # doctest: +IGNORE_OUTPUT
1172+
Local Path
1173+
---------------------------------------------------------------------------------
1174+
./hst_cutout_skycell-p2007x09y05-ra351d3478-decn28d4978_wfc3_ir_f160w_coarse.fits
1175+
./hst_cutout_skycell-p2007x09y05-ra351d3478-decn28d4978_wfc3_uvis_f606w.fits
1176+
./hst_cutout_skycell-p2007x09y05-ra351d3478-decn28d4978_wfc3_uvis_f814w.fits
1177+
1178+
11231179
Accessing Proprietary Data
11241180
==========================
11251181

0 commit comments

Comments
 (0)