Skip to content

Commit c5332fa

Browse files
authored
Merge pull request #2663 from weaverba137/restore-rectangle
Restore rectangle search
2 parents 88d4921 + dd63c3b commit c5332fa

File tree

5 files changed

+371
-68
lines changed

5 files changed

+371
-68
lines changed

CHANGES.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ gaia
211211
sdss
212212
^^^^
213213

214-
- ``query_region()`` now does a cone search around the specified
215-
coordinates. [#2477]
214+
- ``query_region()`` can perform cone search or a rectangular
215+
search around the specified coordinates. [#2477, #2663]
216216

217217
- The default data release has been changed to DR17. [#2478]
218218

astroquery/sdss/core.py

Lines changed: 169 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def query_crossid_async(self, coordinates, *, radius=5. * u.arcsec, timeout=TIME
6666
6767
This query returns the nearest `primary object`_.
6868
69+
Note that there is a server-side limit of 3 arcmin on ``radius``.
70+
6971
.. _`primary object`: https://www.sdss.org/dr17/help/glossary/#surveyprimary
7072
7173
Parameters
@@ -124,6 +126,14 @@ def query_crossid_async(self, coordinates, *, radius=5. * u.arcsec, timeout=TIME
124126
cache : bool, optional
125127
If ``True`` use the request caching mechanism.
126128
129+
Raises
130+
------
131+
TypeError
132+
If the ``radius`` keyword could not be parsed as an angle.
133+
ValueError
134+
If the ``radius`` exceeds 3 arcmin, or if the sizes of
135+
``coordinates`` and ``obj_names`` do not match.
136+
127137
Returns
128138
-------
129139
result : `~astropy.table.Table`
@@ -193,16 +203,31 @@ def query_crossid_async(self, coordinates, *, radius=5. * u.arcsec, timeout=TIME
193203
timeout=timeout, cache=cache)
194204
return response
195205

196-
def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEOUT,
206+
def query_region_async(self, coordinates, *, radius=None,
207+
width=None, height=None, timeout=TIMEOUT,
197208
fields=None, photoobj_fields=None, specobj_fields=None, obj_names=None,
198209
spectro=False, field_help=False, get_query_payload=False,
199210
data_release=conf.default_release, cache=True):
200-
"""
201-
Used to query a circular region (a "cone search") around given coordinates.
202-
203-
This function is equivalent to the object cross-ID (`query_crossid`),
204-
with slightly different parameters. It returns all objects within the
205-
search radius; this could potentially include duplicate observations
211+
r"""
212+
Used to query a region around given coordinates. Either ``radius`` or
213+
``width`` must be specified.
214+
215+
When called with keyword ``radius``, a radial or "cone" search is
216+
performed, centered on each of the given coordinates. In this mode, internally,
217+
this function is equivalent to the object cross-ID (`query_crossid`),
218+
with slightly different parameters. Note that in this mode there is a server-side
219+
limit of 3 arcmin on ``radius``.
220+
221+
When called with keyword ``width``, and optionally a different ``height``,
222+
a rectangular search is performed, centered on each of the given
223+
coordinates. In this mode, internally, this function is equivalent to
224+
a general SQL query (`query_sql`). The shape of the rectangle is
225+
not corrected for declination (*i.e.* no :math:`\cos \delta` correction);
226+
conceptually, this means that the rectangle will become increasingly
227+
trapezoidal-shaped at high declination.
228+
229+
In both radial and rectangular modes, this function returns all objects
230+
within the search area; this could potentially include duplicate observations
206231
of the same object.
207232
208233
Parameters
@@ -221,8 +246,17 @@ def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEO
221246
radius : str or `~astropy.units.Quantity` object, optional
222247
The string must be parsable by `~astropy.coordinates.Angle`. The
223248
appropriate `~astropy.units.Quantity` object from
224-
`astropy.units` may also be used. Defaults to 2 arcsec.
249+
`astropy.units` may also be used.
225250
The maximum allowed value is 3 arcmin.
251+
width : str or `~astropy.units.Quantity` object, optional
252+
The string must be parsable by `~astropy.coordinates.Angle`. The
253+
appropriate `~astropy.units.Quantity` object from
254+
`astropy.units` may also be used.
255+
height : str or `~astropy.units.Quantity` object, optional
256+
The string must be parsable by `~astropy.coordinates.Angle`. The
257+
appropriate `~astropy.units.Quantity` object from
258+
`astropy.units` may also be used. If not specified, it will be
259+
set to the same value as ``width``.
226260
timeout : float, optional
227261
Time limit (in seconds) for establishing successful connection with
228262
remote server. Defaults to `SDSSClass.TIMEOUT`.
@@ -256,6 +290,16 @@ def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEO
256290
cache : bool, optional
257291
If ``True`` use the request caching mechanism.
258292
293+
Raises
294+
------
295+
TypeError
296+
If the ``radius``, ``width`` or ``height`` keywords could not be parsed as an angle.
297+
ValueError
298+
If both ``radius`` and ``width`` are set (or neither),
299+
or if the ``radius`` exceeds 3 arcmin,
300+
or if the sizes of ``coordinates`` and ``obj_names`` do not match,
301+
or if the units of ``width`` or ``height`` could not be parsed.
302+
259303
Examples
260304
--------
261305
>>> from astroquery.sdss import SDSS
@@ -277,16 +321,69 @@ def query_region_async(self, coordinates, *, radius=2. * u.arcsec, timeout=TIMEO
277321
The result of the query as a `~astropy.table.Table` object.
278322
279323
"""
280-
request_payload, files = self.query_crossid_async(coordinates=coordinates,
281-
radius=radius, fields=fields,
282-
photoobj_fields=photoobj_fields,
283-
specobj_fields=specobj_fields,
284-
obj_names=obj_names,
285-
spectro=spectro,
286-
region=True,
287-
field_help=field_help,
288-
get_query_payload=True,
289-
data_release=data_release)
324+
# Allow field_help requests to pass without requiring a radius or width.
325+
if field_help and radius is None and width is None:
326+
radius = 2.0 * u.arcsec
327+
328+
if radius is None and width is None:
329+
raise ValueError("Either radius or width must be specified!")
330+
if radius is not None and width is not None:
331+
raise ValueError("Either radius or width must be specified, not both!")
332+
333+
if radius is not None:
334+
request_payload, files = self.query_crossid_async(coordinates=coordinates,
335+
radius=radius, fields=fields,
336+
photoobj_fields=photoobj_fields,
337+
specobj_fields=specobj_fields,
338+
obj_names=obj_names,
339+
spectro=spectro,
340+
region=True,
341+
field_help=field_help,
342+
get_query_payload=True,
343+
data_release=data_release)
344+
345+
if width is not None:
346+
width = u.Quantity(width, u.degree).value
347+
if height is None:
348+
height = width
349+
else:
350+
height = u.Quantity(height, u.degree).value
351+
352+
dummy_payload = self._args_to_payload(coordinates=coordinates,
353+
fields=fields,
354+
spectro=spectro, region=True,
355+
photoobj_fields=photoobj_fields,
356+
specobj_fields=specobj_fields, field_help=field_help,
357+
data_release=data_release)
358+
359+
sql_query = dummy_payload['uquery'].replace('#upload u JOIN #x x ON x.up_id = u.up_id JOIN ', '')
360+
361+
if 'SpecObjAll' in dummy_payload['uquery']:
362+
sql_query = sql_query.replace('ON p.objID = x.objID ', '').replace(' ORDER BY x.up_id', '')
363+
else:
364+
sql_query = sql_query.replace(' ON p.objID = x.objID ORDER BY x.up_id', '')
365+
366+
if (not isinstance(coordinates, list) and not isinstance(coordinates, Column)
367+
and not (isinstance(coordinates, commons.CoordClasses) and not coordinates.isscalar)):
368+
coordinates = [coordinates]
369+
370+
rectangles = list()
371+
for target in coordinates:
372+
# Query for a rectangle
373+
target = commons.parse_coordinates(target).transform_to('fk5')
374+
rectangles.append(self._rectangle_sql(target.ra.degree, target.dec.degree, width, height=height))
375+
376+
rect = ' OR '.join(rectangles)
377+
378+
# self._args_to_payload only returns a WHERE if e.g. plate, mjd, fiber
379+
# are set, which will not happen in this function.
380+
sql_query += f' WHERE ({rect})'
381+
382+
return self.query_sql_async(sql_query, timeout=timeout,
383+
data_release=data_release,
384+
cache=cache,
385+
field_help=field_help,
386+
get_query_payload=get_query_payload)
290387

291388
if get_query_payload or field_help:
292389
return request_payload
@@ -510,7 +607,7 @@ class = 'galaxy' \
510607
if data_release > 11:
511608
request_payload['searchtool'] = 'SQL'
512609

513-
if kwargs.get('get_query_payload'):
610+
if kwargs.get('get_query_payload') or kwargs.get('field_help'):
514611
return request_payload
515612

516613
url = self._get_query_url(data_release)
@@ -1163,5 +1260,58 @@ def _get_crossid_url(self, data_release):
11631260
self._last_url = url
11641261
return url
11651262

1263+
def _rectangle_sql(self, ra, dec, width, height=None, cosdec=False):
1264+
"""
1265+
Generate SQL for a rectangular query centered on ``ra``, ``dec``.
1266+
1267+
This assumes that RA is defined on the range ``[0, 360)``, and Dec on
1268+
``[-90, 90]``.
1269+
1270+
Parameters
1271+
----------
1272+
ra : float
1273+
Right Ascension in degrees.
1274+
dec : float
1275+
Declination in degrees.
1276+
width : float
1277+
Width of rectangle in degrees.
1278+
height : float, optional
1279+
Height of rectangle in degrees. If not specified, ``width`` is used.
1280+
cosdec : bool, optional
1281+
If ``True`` apply ``cos(dec)`` correction to the rectangle.
1282+
Otherwise, rectangles become increasingly triangle-like
1283+
near the poles.
1284+
1285+
Returns
1286+
-------
1287+
:class:`str`
1288+
A string defining the rectangle in SQL notation.
1289+
"""
1290+
if height is None:
1291+
height = width
1292+
dr = width/2.0
1293+
dd = height/2.0
1294+
d0 = dec - dd
1295+
if d0 < -90:
1296+
d0 = -90.0
1297+
d1 = dec + dd
1298+
if d1 > 90.0:
1299+
d1 = 90.0
1300+
ra_wrap = False
1301+
r0 = ra - dr
1302+
if r0 < 0:
1303+
ra_wrap = True
1304+
r0 += 360.0
1305+
r1 = ra + dr
1306+
if r1 > 360.0:
1307+
ra_wrap = True
1308+
r1 -= 360.0
1309+
# BETWEEN is inclusive, so it is equivalent to the <=, >= operators.
1310+
if ra_wrap:
1311+
sql = f"(((p.ra >= {r0:g}) OR (p.ra <= {r1:g}))"
1312+
else:
1313+
sql = f"((p.ra BETWEEN {r0:g} AND {r1:g})"
1314+
return sql + f" AND (p.dec BETWEEN {d0:g} AND {d1:g}))"
1315+
11661316

11671317
SDSS = SDSSClass()

0 commit comments

Comments
 (0)