Skip to content

Commit 157da27

Browse files
author
John Tobin
committed
add tapsql.py for nrao, updates to data columns supported by NRAO TAP
1 parent 63fded4 commit 157da27

File tree

2 files changed

+306
-11
lines changed

2 files changed

+306
-11
lines changed

astroquery/nrao/core.py

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,60 @@
4242

4343
__doctest_skip__ = ['NraoClass.*']
4444

45-
NRAO_BANDS = {}
45+
NRAO_BANDS = {
46+
'L': (1*u.GHz, 2*u.GHz),
47+
'S': (2*u.GHz, 4*u.GHz),
48+
'C': (4*u.GHz, 8*u.GHz),
49+
'X': (8*u.GHz, 12*u.GHz),
50+
'U': (12*u.GHz, 18*u.GHz),
51+
'K': (18*u.GHz, 26*u.GHz),
52+
'A': (26*u.GHz, 39*u.GHz),
53+
'Q': (39*u.GHz, 50*u.GHz),
54+
'W': (80*u.GHz, 115*u.GHz)
55+
}
4656

4757
TAP_SERVICE_PATH = 'tap'
4858

4959
NRAO_FORM_KEYS = {
5060
'Position': {
5161
'Source name (astropy Resolver)': ['source_name_resolver',
5262
'SkyCoord.from_name', _gen_pos_sql],
53-
'Source name (NRAO)': ['source_name_alma', 'target_name', _gen_str_sql],
63+
'Source name (NRAO)': ['source_name', 'target_name', _gen_str_sql],
5464
'RA Dec (Sexagesimal)': ['ra_dec', 's_ra, s_dec', _gen_pos_sql],
5565
'Galactic (Degrees)': ['galactic', 'gal_longitude, gal_latitude',
5666
_gen_pos_sql],
5767
'Angular resolution (arcsec)': ['spatial_resolution',
5868
'spatial_resolution', _gen_numeric_sql],
59-
'Largest angular scale (arcsec)': ['spatial_scale_max',
60-
'spatial_scale_max', _gen_numeric_sql],
61-
'Field of view (arcsec)': ['fov', 's_fov', _gen_numeric_sql]
69+
'Field of view (arcsec)': ['fov', 's_fov', _gen_numeric_sql],
70+
'Configuration': ['configuration', 'configuration', _gen_numeric_sql],
71+
'Maximum UV Distance (meters)': ['max_uv_dist', 'max_uv_dist', _gen_numeric_sql]
72+
73+
74+
},
75+
'Project': {
76+
'Project code': ['project_code', 'project_code', _gen_str_sql],
77+
'Telescope': ['instrument', 'instrument_name', _gen_str_sql],
78+
'Number of Antennas': ['n_ants', 'num_antennas', _gen_str_sql],
79+
6280
},
81+
'Time': {
82+
'Observation start': ['start_date', 't_min', _gen_datetime_sql],
83+
'Observation end': ['end_date', 't_max', _gen_datetime_sql],
84+
'Integration time (s)': ['integration_time', 't_exptime',
85+
_gen_numeric_sql]
86+
},
87+
'Polarization': {
88+
'Polarisation type (Single, Dual, Full)': ['polarisation_type',
89+
'pol_states', _gen_pol_sql]
90+
},
91+
'Energy': {
92+
'Frequency (GHz)': ['frequency', 'center_frequencies', _gen_numeric_sql],
93+
'Bandwidth (Hz)': ['bandwidth', 'aggregate_bandwidth', _gen_numeric_sql],
94+
'Spectral resolution (KHz)': ['spectral_resolution',
95+
'em_resolution', _gen_spec_res_sql],
96+
'Band': ['band_list', 'band_list', _gen_band_list_sql]
97+
},
98+
6399
}
64100

65101

@@ -200,12 +236,6 @@ def query_object_async(self, object_name, *, payload=None, **kwargs):
200236
----------
201237
object_name : str
202238
The object name. Will be resolved by astropy.coord.SkyCoord
203-
public : bool
204-
True to return only public datasets, False to return private only,
205-
None to return both
206-
science : bool
207-
True to return only science datasets, False to return only
208-
calibration, None to return both
209239
payload : dict
210240
Dictionary of additional keywords. See `help`.
211241
"""

astroquery/nrao/tapsql.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""
2+
Utilities for generating ADQL for ALMA TAP service
3+
"""
4+
from datetime import datetime
5+
6+
from astropy import units as u
7+
import astropy.coordinates as coord
8+
from astropy.time import Time
9+
10+
ALMA_DATE_FORMAT = '%d-%m-%Y'
11+
12+
13+
def _gen_pos_sql(field, value):
14+
result = ''
15+
if field == 'SkyCoord.from_name':
16+
# resolve the source first
17+
if value:
18+
obj_coord = coord.SkyCoord.from_name(value)
19+
frame = 'icrs'
20+
ras = [str(obj_coord.icrs.ra.to(u.deg).value)]
21+
decs = [str(obj_coord.icrs.dec.to(u.deg).value)]
22+
radius = 10 * u.arcmin
23+
else:
24+
raise ValueError('Object name missing')
25+
else:
26+
if field == 's_ra, s_dec':
27+
frame = 'icrs'
28+
else:
29+
frame = 'galactic'
30+
radius = 10*u.arcmin
31+
if ',' in value:
32+
center_coord, rad = value.split(',')
33+
try:
34+
radius = float(rad.strip())*u.degree
35+
except ValueError:
36+
raise ValueError('Cannot parse radius in ' + value)
37+
else:
38+
center_coord = value.strip()
39+
try:
40+
ra, dec = center_coord.split(' ')
41+
except ValueError:
42+
raise ValueError('Cannot find ra/dec in ' + value)
43+
ras = _val_parse(ra, val_type=str)
44+
decs = _val_parse(dec, val_type=str)
45+
46+
for ra in ras:
47+
for dec in decs:
48+
if result:
49+
result += ' OR '
50+
if isinstance(ra, str) and isinstance(dec, str):
51+
# circle
52+
center = coord.SkyCoord(ra, dec,
53+
unit=(u.deg, u.deg),
54+
frame=frame)
55+
56+
result += \
57+
"CONTAINS(POINT('ICRS',s_ra,s_dec),CIRCLE('ICRS',{},{},{}))=1".\
58+
format(center.icrs.ra.to(u.deg).value,
59+
center.icrs.dec.to(u.deg).value,
60+
radius.to(u.deg).value)
61+
else:
62+
raise ValueError('Cannot interpret ra({}), dec({}'.
63+
format(ra, dec))
64+
if ' OR ' in result:
65+
# use brackets for multiple ORs
66+
return '(' + result + ')'
67+
else:
68+
return result
69+
70+
71+
def _gen_numeric_sql(field, value):
72+
result = ''
73+
for interval in _val_parse(value, float):
74+
if result:
75+
result += ' OR '
76+
if isinstance(interval, tuple):
77+
int_min, int_max = interval
78+
if int_min is None:
79+
if int_max is None:
80+
# no constraints on bandwith
81+
pass
82+
else:
83+
result += '{}<={}'.format(field, int_max)
84+
elif int_max is None:
85+
result += '{}>={}'.format(field, int_min)
86+
else:
87+
result += '({1}<={0} AND {0}<={2})'.format(field, int_min,
88+
int_max)
89+
else:
90+
result += '{}={}'.format(field, interval)
91+
if ' OR ' in result:
92+
# use brakets for multiple ORs
93+
return '(' + result + ')'
94+
else:
95+
return result
96+
97+
98+
def _gen_str_sql(field, value):
99+
result = ''
100+
for interval in _val_parse(value, str):
101+
if result:
102+
result += ' OR '
103+
if '*' in interval:
104+
# use LIKE
105+
# escape wildcards if they exists in the value
106+
interval = interval.replace('%', r'\%') # noqa
107+
interval = interval.replace('_', r'\_') # noqa
108+
# ADQL wild cards are % and _
109+
interval = interval.replace('*', '%')
110+
interval = interval.replace('?', '_')
111+
result += "{} LIKE '{}'".format(field, interval)
112+
else:
113+
result += "{}='{}'".format(field, interval)
114+
if ' OR ' in result:
115+
# use brackets for multiple ORs
116+
return '(' + result + ')'
117+
else:
118+
return result
119+
120+
121+
def _gen_datetime_sql(field, value):
122+
result = ''
123+
for interval in _val_parse(value, str):
124+
if result:
125+
result += ' OR '
126+
if isinstance(interval, tuple):
127+
min_datetime, max_datetime = interval
128+
if max_datetime is None:
129+
result += "{}>={}".format(
130+
field, Time(datetime.strptime(min_datetime, ALMA_DATE_FORMAT)).mjd)
131+
elif min_datetime is None:
132+
result += "{}<={}".format(
133+
field, Time(datetime.strptime(max_datetime, ALMA_DATE_FORMAT)).mjd)
134+
else:
135+
result += "({1}<={0} AND {0}<={2})".format(
136+
field, Time(datetime.strptime(min_datetime, ALMA_DATE_FORMAT)).mjd,
137+
Time(datetime.strptime(max_datetime, ALMA_DATE_FORMAT)).mjd)
138+
else:
139+
# TODO is it just a value (midnight) or the entire day?
140+
result += "{}={}".format(
141+
field, Time(datetime.strptime(interval, ALMA_DATE_FORMAT)).mjd)
142+
if ' OR ' in result:
143+
# use brackets for multiple ORs
144+
return '(' + result + ')'
145+
else:
146+
return result
147+
148+
149+
def _gen_spec_res_sql(field, value):
150+
# This needs special treatment because spectral_resolution in AQ is in
151+
# KHz while corresponding em_resolution is in m
152+
result = ''
153+
for interval in _val_parse(value):
154+
if result:
155+
result += ' OR '
156+
if isinstance(interval, tuple):
157+
min_val, max_val = interval
158+
if max_val is None:
159+
result += "{}<={}".format(
160+
field,
161+
min_val*u.kHz.to(u.m, equivalencies=u.spectral()))
162+
elif min_val is None:
163+
result += "{}>={}".format(
164+
field,
165+
max_val*u.kHz.to(u.m, equivalencies=u.spectral()))
166+
else:
167+
result += "({1}<={0} AND {0}<={2})".format(
168+
field,
169+
max_val*u.kHz.to(u.m, equivalencies=u.spectral()),
170+
min_val*u.kHz.to(u.m, equivalencies=u.spectral()))
171+
else:
172+
result += "{}={}".format(
173+
field, interval*u.kHz.to(u.m, equivalencies=u.spectral()))
174+
if ' OR ' in result:
175+
# use brackets for multiple ORs
176+
return '(' + result + ')'
177+
else:
178+
return result
179+
180+
181+
def _gen_pub_sql(field, value):
182+
if value is True:
183+
return "{}='Public'".format(field)
184+
elif value is False:
185+
return "{}='Proprietary'".format(field)
186+
else:
187+
return None
188+
189+
190+
def _gen_science_sql(field, value):
191+
if value is True:
192+
return "{}='T'".format(field)
193+
elif value is False:
194+
return "{}='F'".format(field)
195+
else:
196+
return None
197+
198+
199+
def _gen_band_list_sql(field, value):
200+
# band list value is expected to be space separated list of bands
201+
if isinstance(value, list):
202+
val = value
203+
else:
204+
val = value.split(' ')
205+
return _gen_str_sql(field, '|'.join(
206+
['*{}*'.format(_) for _ in val]))
207+
208+
209+
def _gen_pol_sql(field, value):
210+
# band list value is expected to be space separated list of bands
211+
val = ''
212+
states_map = {'Stokes I': '*I*',
213+
'Single': '/LL/',
214+
'Dual': '/LL/RR/',
215+
'Full': '/LL/LR/RL/RR/'}
216+
for state in states_map:
217+
if state in value:
218+
if val:
219+
val += '|'
220+
val += states_map[state]
221+
return _gen_str_sql(field, val)
222+
223+
224+
def _val_parse(value, val_type=float):
225+
# parses an ALMA query field and returns a list of values (of type
226+
# val_type) or tuples representing parsed values or intervals. Open
227+
# intervals have None at one of the ends
228+
def _one_val_parse(value, val_type=float):
229+
# parses the value and returns corresponding interval for
230+
# sia to work with. E.g <2 => (None, 2)
231+
if value.startswith('<'):
232+
return (None, val_type(value[1:]))
233+
elif value.startswith('>'):
234+
return (val_type(value[1:]), None)
235+
else:
236+
return val_type(value)
237+
result = []
238+
if isinstance(value, str):
239+
try:
240+
if value.startswith('!'):
241+
start, end = _val_parse(value[2:-1].strip(), val_type=val_type)[0]
242+
result.append((None, start))
243+
result.append((end, None))
244+
elif value.startswith('('):
245+
result += _val_parse(value[1:-1], val_type=val_type)
246+
elif '|' in value:
247+
for vv in value.split('|'):
248+
result += _val_parse(vv.strip(), val_type=val_type)
249+
elif '..' in value:
250+
start, end = value.split('..')
251+
if not start or not end:
252+
raise ValueError('start or end interval missing in {}'.
253+
format(value))
254+
result.append((_one_val_parse(start.strip(), val_type=val_type),
255+
_one_val_parse(end.strip(), val_type=val_type)))
256+
else:
257+
result.append(_one_val_parse(value, val_type=val_type))
258+
except Exception as e:
259+
raise ValueError(
260+
'Error parsing {}. Details: {}'.format(value, str(e)))
261+
elif isinstance(value, list):
262+
result = value
263+
else:
264+
result.append(value)
265+
return result

0 commit comments

Comments
 (0)