Skip to content

Commit 33f23ee

Browse files
committed
Add spectral cutout capability
1 parent 13562b1 commit 33f23ee

File tree

4 files changed

+311
-77
lines changed

4 files changed

+311
-77
lines changed

astroquery/casda/core.py

Lines changed: 138 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -101,24 +101,58 @@ def _args_to_payload(self, **kwargs):
101101

102102
# Convert the coordinates to FK5
103103
coordinates = kwargs.get('coordinates')
104-
fk5_coords = commons.parse_coordinates(coordinates).transform_to(coord.FK5)
105-
106-
if kwargs['radius'] is not None:
107-
radius = u.Quantity(kwargs['radius']).to(u.deg)
108-
pos = 'CIRCLE {} {} {}'.format(fk5_coords.ra.degree, fk5_coords.dec.degree, radius.value)
109-
elif kwargs['width'] is not None and kwargs['height'] is not None:
110-
width = u.Quantity(kwargs['width']).to(u.deg).value
111-
height = u.Quantity(kwargs['height']).to(u.deg).value
112-
top = fk5_coords.dec.degree - (height/2)
113-
bottom = fk5_coords.dec.degree + (height/2)
114-
left = fk5_coords.ra.degree - (width/2)
115-
right = fk5_coords.ra.degree + (width/2)
116-
pos = 'RANGE {} {} {} {}'.format(left, right, top, bottom)
117-
else:
118-
raise ValueError("Either 'radius' or both 'height' and 'width' must be supplied.")
119-
120-
request_payload['POS'] = pos
121-
104+
if coordinates is not None:
105+
fk5_coords = commons.parse_coordinates(coordinates).transform_to(coord.FK5)
106+
107+
if kwargs.get('radius') is not None:
108+
radius = u.Quantity(kwargs['radius']).to(u.deg)
109+
pos = 'CIRCLE {} {} {}'.format(fk5_coords.ra.degree, fk5_coords.dec.degree, radius.value)
110+
elif kwargs.get('width') is not None and kwargs.get('height') is not None:
111+
width = u.Quantity(kwargs['width']).to(u.deg).value
112+
height = u.Quantity(kwargs['height']).to(u.deg).value
113+
top = fk5_coords.dec.degree + (height/2)
114+
bottom = fk5_coords.dec.degree - (height/2)
115+
left = fk5_coords.ra.degree - (width/2)
116+
right = fk5_coords.ra.degree + (width/2)
117+
pos = 'RANGE {} {} {} {}'.format(left, right, bottom, top)
118+
else:
119+
pos = 'CIRCLE {} {} {}'.format(fk5_coords.ra.degree, fk5_coords.dec.degree, 1*u.arcmin.to(u.deg))
120+
121+
request_payload['POS'] = pos
122+
123+
if kwargs.get('band') is not None:
124+
if kwargs.get('channel') is not None:
125+
raise ValueError("Either 'channel' or 'band' values may be provided but not both.")
126+
127+
band = kwargs.get('band')
128+
if not isinstance(band, (list,tuple)) or len(band) != 2:
129+
raise ValueError("The 'band' value must be a list of 2 wavelength or frequency values.")
130+
if (band[0] is not None and not isinstance(band[0], u.Quantity)) or (band[1] is not None and not isinstance(band[1], u.Quantity)):
131+
raise ValueError("The 'band' value must be a list of 2 wavelength or frequency values.")
132+
if band[0] is not None and band[1] is not None and band[0].unit.physical_type != band[1].unit.physical_type:
133+
raise ValueError("The 'band' values must have the same kind of units.")
134+
if band[0] is not None or band[1] is not None:
135+
unit = band[0].unit if band[0] is not None else band[1].unit
136+
if unit.physical_type == 'length':
137+
min_band = '-Inf' if band[0] is None else str(band[0].to(u.m).value)
138+
max_band = '+Inf' if band[1] is None else str(band[1].to(u.m).value)
139+
elif unit.physical_type == 'frequency':
140+
# Swap the order when changing frequency to wavelength
141+
min_band = '-Inf' if band[1] is None else str(band[1].to(u.m, equivalencies=u.spectral()).value)
142+
max_band = '+Inf' if band[0] is None else str(band[0].to(u.m, equivalencies=u.spectral()).value)
143+
else:
144+
raise ValueError("The 'band' values must be wavelengths or frequencies.")
145+
146+
request_payload['BAND'] = '{} {}'.format(min_band, max_band)
147+
148+
if kwargs.get('channel') is not None:
149+
channel = kwargs.get('channel')
150+
if not isinstance(channel, (list,tuple)) or len(channel) != 2:
151+
raise ValueError("The 'channel' value must be a list of 2 integer values.")
152+
if (not isinstance(channel[0], int)) or (not isinstance(channel[1], int)):
153+
raise ValueError("The 'channel' value must be a list of 2 integer values.")
154+
request_payload['CHANNEL'] = '{} {}'.format(channel[0], channel[1])
155+
122156
return request_payload
123157

124158
# the methods above implicitly call the private _parse_result method.
@@ -159,54 +193,11 @@ def filter_out_unreleased(self, table):
159193
now = str(datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f'))
160194
return table[(table['obs_release_date'] != '') & (table['obs_release_date'] < now)]
161195

162-
def stage_data(self, table, coordinates=None, radius=None, height=None, width=None, verbose=False):
163-
"""
164-
Request access to a set of data files. All requests for data must use authentication. If you have access to the
165-
data, the requested files will be brought online and a set of URLs to download the files will be returned.
166-
167-
This method can also be used to produce a cutout from the data files. If a set of coordinates is provided along
168-
with either a radius or a box height and width, then CASDA will produce a cutout at that location from each
169-
data file specified in the table.
170-
171-
Parameters
172-
----------
173-
table: `astropy.table.Table`
174-
A table describing the files to be staged, such as produced by query_region. It must include an
175-
access_url column.
176-
coordinates : str or `astropy.coordinates`, optional
177-
coordinates around which to produce a cutout
178-
radius : str or `astropy.units.Quantity`, optional
179-
the radius of the cutout
180-
height : str or `astropy.units.Quantity`, optional
181-
the height for a box cutout
182-
width : str or `astropy.units.Quantity`, optional
183-
the width for a box cutout
184-
verbose: bool, optional
185-
Should status message be logged periodically, defaults to False
186-
187-
Returns
188-
-------
189-
A list of urls of both the requested files/cutouts and the checksums for the files/cutouts
190-
"""
191-
if not self._authenticated:
192-
raise ValueError("Credentials must be supplied to download CASDA image data")
193-
194-
if table is None or len(table) == 0:
195-
return []
196-
197-
198-
if coordinates is not None:
199-
cutout_spec = self._args_to_payload(coordinates=coordinates, radius=radius, height=height,
200-
width=width)
201-
is_cutout = True
202-
else:
203-
cutout_spec = None
204-
is_cutout = False
205196

197+
def _create_job(self, table, service_name, verbose):
206198
# Use datalink to get authenticated access for each file
207199
tokens = []
208200
soda_url = None
209-
service_name = 'cutout_service' if is_cutout else 'async_service'
210201
for row in table:
211202
access_url = row['access_url']
212203
if access_url:
@@ -226,11 +217,10 @@ def stage_data(self, table, coordinates=None, radius=None, height=None, width=No
226217
job_url = self._create_soda_job(tokens, soda_url=soda_url)
227218
if verbose:
228219
log.info("Created data staging job " + job_url)
229-
230-
# Add cutout parameters, if they have been specified
231-
if cutout_spec is not None:
232-
self._add_cutout_params(job_url, verbose, cutout_spec)
233220

221+
return job_url
222+
223+
def _complete_job(self, job_url, verbose):
234224
# Wait for job to be complete
235225
final_status = self._run_job(job_url, verbose, poll_interval=self.POLL_INTERVAL)
236226
if final_status != 'COMPLETED':
@@ -247,6 +237,88 @@ def stage_data(self, table, coordinates=None, radius=None, height=None, width=No
247237

248238
return fileurls
249239

240+
def stage_data(self, table, verbose=False):
241+
"""
242+
Request access to a set of data files. All requests for data must use authentication. If you have access to the
243+
data, the requested files will be brought online and a set of URLs to download the files will be returned.
244+
245+
Parameters
246+
----------
247+
table: `astropy.table.Table`
248+
A table describing the files to be staged, such as produced by query_region. It must include an
249+
access_url column.
250+
verbose: bool, optional
251+
Should status message be logged periodically, defaults to False
252+
253+
Returns
254+
-------
255+
A list of urls of both the requested files/cutouts and the checksums for the files/cutouts
256+
"""
257+
if not self._authenticated:
258+
raise ValueError("Credentials must be supplied to download CASDA image data")
259+
260+
if table is None or len(table) == 0:
261+
return []
262+
263+
job_url = self._create_job(table, 'async_service', verbose)
264+
265+
return self._complete_job(job_url, verbose)
266+
267+
268+
def cutout(self, table, coordinates=None, radius=None, height=None, width=None, band=None, channel=None, verbose=False):
269+
"""
270+
Produce a cutout from each selected file. All requests for data must use authentication. If you have access to
271+
the data, the requested files will be brought online, a cutout produced from each file and a set of URLs to
272+
download the cutouts will be returned.
273+
274+
If a set of coordinates is provided along with either a radius or a box height and width, then CASDA will produce a
275+
spatial cutout at that location from each data file specified in the table. If a band or channel pair is provided
276+
then CASDA will produce a spectral cutout of that range from each data file. These can be combined to produce
277+
subcubes with restrictions in both spectral and spatial axes.
278+
279+
Parameters
280+
----------
281+
table: `astropy.table.Table`
282+
A table describing the files to be staged, such as produced by query_region. It must include an
283+
access_url column.
284+
coordinates : str or `astropy.coordinates`, optional
285+
coordinates around which to produce a cutout, the radius will be 1 arcmin if no radius, height or width is provided.
286+
radius : str or `astropy.units.Quantity`, optional
287+
the radius of the cutout
288+
height : str or `astropy.units.Quantity`, optional
289+
the height for a box cutout
290+
width : str or `astropy.units.Quantity`, optional
291+
the width for a box cutout
292+
band : list of `astropy.units.Quantity` with two elements, optional
293+
the spectral range to be included, may be low and high wavelengths in metres or low and high frequencies in Hertz. Use None for an open bound.
294+
channel : list of int with two elements, optional
295+
the spectral range to be included, the low and high channels (i.e. planes of a cube) inclusive
296+
verbose: bool, optional
297+
Should status messages be logged periodically, defaults to False
298+
299+
Returns
300+
-------
301+
A list of urls of both the requested files/cutouts and the checksums for the files/cutouts
302+
"""
303+
if not self._authenticated:
304+
raise ValueError("Credentials must be supplied to download CASDA image data")
305+
306+
if table is None or len(table) == 0:
307+
return []
308+
309+
job_url = self._create_job(table, 'cutout_service', verbose)
310+
311+
cutout_spec = self._args_to_payload(coordinates=coordinates, radius=radius, height=height,
312+
width=width, band=band, channel=channel)
313+
314+
if not cutout_spec:
315+
raise ValueError("Please provide cutout parameters such as coordinates, band or channel.")
316+
317+
self._add_cutout_params(job_url, verbose, cutout_spec)
318+
319+
return self._complete_job(job_url, verbose)
320+
321+
250322
def download_files(self, urls, savedir=''):
251323
"""
252324
Download a series of files

0 commit comments

Comments
 (0)