This repository has been archived by the owner on Jul 24, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcoord.py
499 lines (440 loc) · 16.6 KB
/
coord.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
"""Astronomical coordinate functions."""
import re,pdb
import numpy as np
from numpy import arccos,sin,cos
from math import pi
# constants
DEG_PER_HR = 360. / 24. # degrees per hour
DEG_PER_MIN = DEG_PER_HR / 60. # degrees per min
DEG_PER_S = DEG_PER_MIN / 60. # degrees per sec
DEG_PER_AMIN = 1./60. # degrees per arcmin
DEG_PER_ASEC = DEG_PER_AMIN / 60. # degrees per arcsec
RAD_PER_DEG = pi / 180. # radians per degree
def ang_sep(ra1, dec1, ra2, dec2):
""" Returns the angular separation (in units of degrees) on the
celestial sphere between two ra/dec coordinates given in degrees.
Accepts numpy arrays.
Note: only works for separations larger than about 0.1 arcsec.
Smaller separations are always returned as zero, because of
floating point effects.
>>> np.allclose(ang_sep(2, 0, 4, 0), 2.)
True
>>> np.allclose(ang_sep(359, 0, 1, 0), 2.)
True
>>> np.allclose(ang_sep(0, 20, 0, -10), 30)
True
>>> np.allclose(ang_sep(7, 20, 8, 40), 20.018358)
True
>>> np.allclose(ang_sep(7, 20, 250, -50.3), 122.388401)
True
>>> ang_sep(-1, 10, 240, -10) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: RA outside sensible limits. -1
>>> ras = [24.5,23.6]; decs = [66.89,67.01]
>>> ref_sep = np.array([3.520032, 3.26675])
>>> np.allclose(ang_sep(20. ,70., ras, decs), ref_sep)
True
>>> ras = [24.5, 23.6]; decs = [91.89, 67.01]
>>> ang_sep(20.0, 70.0, ras, decs) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: Dec outside sensible limits. [ 91.89 67.01]
"""
ra1 = np.asarray(ra1); ra2 = np.asarray(ra2)
dec1 = np.asarray(dec1); dec2 = np.asarray(dec2)
# error checking
# for ra in ra1,ra2:
# if not ((0. <= ra)&(ra < 360.)).all():
# raise ValueError("RA outside sensible limits. %s" % ra)
# for dec in dec1,dec2:
# if np.any(np.abs(dec) > 90.):
# raise ValueError("Dec outside sensible limits. %s" % dec)
ra1 = ra1 * RAD_PER_DEG # convert to radians
ra2 = ra2 * RAD_PER_DEG
dec1 = dec1 * RAD_PER_DEG
dec2 = dec2 * RAD_PER_DEG
sra1 = sin(ra1); sra2 = sin(ra2)
cra1 = cos(ra1); cra2 = cos(ra2)
sdec1 = sin(dec1); sdec2 = sin(dec2)
cdec1 = cos(dec1); cdec2 = cos(dec2)
csep = cdec1*cdec2*(cra1*cra2 + sra1*sra2) + sdec1*sdec2
# An ugly work-around for floating point issues.
#if np.any(csep > 1): print csep
csep = np.where(csep > 1., 1., csep)
degsep = arccos(csep) / RAD_PER_DEG
# only works for separations > 0.1 of an arcsec or >~2.7e-5 dec
degsep = np.where(degsep < 1e-5, 0, degsep)
return degsep
def _dec2s(ra, dec,
raformat='%02.0f %02.0f %06.3f', decformat='%02.0f %02.0f %05.2f'):
"""
Converts decimal RA and Dec (both in degrees) to sexigesimal RA
(hours/minutes/seconds) and Dec (degrees/arcmin/arcsec). Returns
two strings, RA and Dec.
doctests:
>>> dec2s(156.1125638,-10.12986)
('10 24 27.015', '-10 07 47.50')
>>> dec2s(0.0,-90.0)
('00 00 00.000', '-90 00 00.00')
>>> dec2s(148.2,95.0)
Traceback (most recent call last):
...
ValueError: Decimal RA or Dec outside sensible limits.
>>> dec2s(360.0,-30.1)
Traceback (most recent call last):
...
ValueError: Decimal RA or Dec outside sensible limits.
"""
ra = float(ra)
dec = float(dec)
if dec < 0.:
dec *= -1.
negdec = True
else: negdec = False
# error checking
if not (0.0 <= ra < 360.) or dec > 90.:
raise ValueError("Decimal RA or Dec outside sensible limits.")
rah, temp = divmod(ra, DEG_PER_HR)
ram, temp = divmod(temp, DEG_PER_MIN)
ras = temp / DEG_PER_S
s_ra = raformat % (rah, ram, ras)
decd, temp = divmod(dec, 1)
decm, temp = divmod(temp, DEG_PER_AMIN)
decs = temp / DEG_PER_ASEC
if negdec:
s_dec = '-' + decformat % (decd, decm, decs)
else: s_dec = '+' + decformat % (decd, decm, decs)
return s_ra,s_dec
def dec2s(ra,dec):
""" Convert and ra and dec (or list of ras and decs) to decimal
degrees.
"""
try:
return _dec2s(ra, dec)
except TypeError:
pass
radec = [_dec2s(r,d) for r, d in zip(ra, dec)]
return zip(*radec)
def _s2dec(ra,dec):
""" Converts two strings of sexigesimal RA (hms) and Dec (dms) to
decimal RA and Dec (degrees). The separators between h/m/s and
deg/arcmin/arcsec can be whitespace or colons. Returns a tuple of
two floats, (ra, dec).
doctests:
>>> s2dec('00:00:00', '90:00:00')
(0.0, 90.0)
>>> temp = np.array(s2dec ('10 24 27.015', '-10 07 47.50'))
>>> reference = np.array([156.1125625,-10.129861111111111])
>>> np.all(temp - reference < 1.e-10)
True
>>> s2dec('25:11:19', '-18:4:88')
Traceback (most recent call last):
...
ValueError: Either RA or Dec is outside sensible limits.
RA = 25 11 19, Dec = -18 4 88
"""
# Convert to floats, noting sign of dec
ra = re.sub('[:hms]', ' ', ra)
dec = re.sub('[:dms]', ' ', dec)
rah,ram,ras = [float(item) for item in ra.split()]
if dec.lstrip()[0] == '-':
negdec = True
else: negdec = False
decd,decm,decs = [float(item) for item in dec.split()]
if negdec: decd *= -1.
# Error checking
if (not 0. <= rah < 24. or not 0. <= ram <= 60. or not 0. <= ras <= 60.
or decd > 90. or decm >= 60. or decs > 60):
raise ValueError('Either RA or Dec is outside sensible '
'limits.\nRA = %s, Dec = %s' % (ra,dec))
# Conversion
d_ra = DEG_PER_HR * rah + DEG_PER_MIN * ram + DEG_PER_S * ras
d_dec = decd + DEG_PER_AMIN * decm + DEG_PER_ASEC * decs
if negdec: d_dec *= -1.
return d_ra, d_dec
def s2dec(ra, dec):
""" Convert a sexigesimal ra and dec (or list of ras and decs) to
decimal degrees.
"""
try:
return _s2dec(ra, dec)
except TypeError:
pass
radec = [_s2dec(r,d) for r, d in zip(ra, dec)]
return map(np.array, zip(*radec))
def match(ra1, dec1, ra2, dec2, tol, allmatches=False):
"""
match(ra1, dec1, ra2, dec2, tol)
Given two sets of numpy arrays of ra,dec and a tolerance tol
(float), returns an array of indices and separations with the same
length as the first input array. If and index is > 0, it is the
index of the closest matching second array element within tol
arcsec. If it's -1, then there was no matching ra/dec within tol
arcsec.
if allmatches = True, then for each object in the first array,
return the index and separation of everything in the second array
within the search tolerance, not just the closest match.
Note to get the indices of objects in ra2, dec2 without a match, use
imatch = match(ra1, dec1, ra2, dec2, 2.)
inomatch = numpy.setdiff1d(np.arange(len(ra2)), set(imatch))
"""
from numpy.core.records import fromarrays
ra1,ra2,dec1,dec2 = map(np.asarray, (ra1, ra2, dec1, dec2))
abs = np.abs
isorted = ra2.argsort()
sdec2 = dec2[isorted]
sra2 = ra2[isorted]
LIM = tol * DEG_PER_ASEC
match = []
# use mean dec, assumes decs similar
decav = np.mean(sdec2.mean() + dec1.mean())
RA_LIM = LIM / cos(decav * RAD_PER_DEG)
for ra,dec in zip(ra1,dec1):
i1 = sra2.searchsorted(ra - RA_LIM)
i2 = i1 + sra2[i1:].searchsorted(ra + RA_LIM)
#print i1,i2
close = []
for j in xrange(i1,i2):
if abs(dec - sdec2[j]) > LIM:
continue
else:
# if ras and decs are within LIM arcsec, then
# calculate actual separation:
disq = ang_sep(ra, dec, sra2[j], sdec2[j])
close.append((disq, j))
close.sort()
if not allmatches:
# Choose the object with the closest separation inside the
# requested tolerance, if one was found.
if len(close) > 0:
min_dist, jmin = close[0]
if min_dist < LIM:
match.append((isorted[jmin], min_dist))
continue
# otherwise no match
match.append((-1,-1))
else:
# append all the matching objects
jclose = []
seps = []
for dist,j in close:
if dist < LIM:
jclose.append(j)
seps.append(dist)
else:
break
match.append(fromarrays([isorted[jclose], seps],
dtype=[('ind','i8'),('sep','f8')]))
if not allmatches:
# return both indices and separations in a recarray
temp = np.rec.fromrecords(match, names='ind,sep')
# change to arcseconds
temp.sep *= 3600.
temp.sep[temp.sep < 0] = -1.
return temp
else:
return match
def indmatch(ra1, dec1, ra2, dec2, tol):
"""
Finds objects in ra1, dec1 that have a matching object in ra2,dec2
within tol arcsec.
Returns i1, i2 where i1 are indices into ra1,dec1 that have
matches, and i2 are the indices into ra2, dec2 giving the matching
objects.
"""
m = match(ra1, dec1, ra2, dec2, tol)
c = m.ind > -1
i1 = c.nonzero()[0]
i2 = m.ind[c]
return i1, i2
def unique_radec(ra, dec, tol):
""" Find unique ras and decs in a list of coordinates.
RA and Dec must be array sof the same length, and in degrees.
tol is the tolerance for matching in arcsec. Any coord separated by
less that this amount are assumed to be the same.
Returns two arrays. The first is an array of indices giving the
first occurence of a unique coordinate in the input list. The
second is a list giving the indices of all coords that were
matched to a given unique coord.
The matching algorithm is confusing, but hopefully correct and not too
slow. Potential for improvement...
Example
-------
>>> ra,dec = np.loadtxt('radec.txt.gz', unpack=1)
>>> iunique, iextra = unique_radec(ra,dec,2)
>>> iknown, extraknown = np.loadtxt('radec_known.txt.gz', unpack=1)
>>> np.allclose(iunique, iknown)
>>> np.allclose(iextra, extraknown)
"""
matches = match(ra, dec, ra, dec, tol, allmatches=True)
imatchflat = []
for m in matches:
imatchflat.extend(m.ind)
#pdb.set_trace()
inomatch = np.setdiff1d(np.arange(len(ra)), list(set(imatchflat)))
assert len(inomatch) == 0
# Indices giving unique ra, decs
iunique = []
# Will be same length as iunique. Gives all indices in original
# coords that are matched to each unique coord.
iextras = []
assigned = set()
for j,m in enumerate(matches):
if not (j % 1000):
print j
# get the lowest index in this group
isort = sorted(m.ind)
ilow = isort[0]
if ilow not in assigned:
iunique.append(ilow)
assigned.add(ilow)
iextras.append([ilow])
# assign any extra indices to this unique coord.
for i in isort[1:]:
# check not already been assigned to another coord
if i not in assigned:
iextras[-1].append(i)
assigned.add(i)
return np.array(iunique), iextras
def parse_ra( inn ):
'''
Parse input Right Ascension string, either decimal degrees or sexagesimal HH:MM:SS.SS (or similar variants).
Returns decimal degrees.
'''
# if simple float, assume decimal degrees
try:
ra = float(inn)
return ra
except:
# try to parse with phmsdms:
res = parse_sexagesimal(inn)
ra = 15.*( res['vals'][0] + res['vals'][1]/60. + res['vals'][2]/3600. )
return ra
def parse_dec( inn ):
'''
Parse input Declination string, either decimal degrees or sexagesimal DD:MM:SS.SS (or similar variants).
Returns decimal degrees.
'''
# if simple float, assume decimal degrees
try:
dec = float(inn)
return dec
except:
# try to parse with phmsdms:
res = parse_sexagesimal(inn)
dec = res['sign']*( res['vals'][0] + res['vals'][1]/60. + res['vals'][2]/3600. )
return dec
def parse_sexagesimal(hmsdms):
"""
+++ Pulled from python package 'angles' +++
Parse a string containing a sexagesimal number.
This can handle several types of delimiters and will process
reasonably valid strings. See examples.
Parameters
----------
hmsdms : str
String containing a sexagesimal number.
Returns
-------
d : dict
parts : a 3 element list of floats
The three parts of the sexagesimal number that were
identified.
vals : 3 element list of floats
The numerical values of the three parts of the sexagesimal
number.
sign : int
Sign of the sexagesimal number; 1 for positive and -1 for
negative.
units : {"degrees", "hours"}
The units of the sexagesimal number. This is infered from
the characters present in the string. If it a pure number
then units is "degrees".
"""
units = None
sign = None
# Floating point regex:
# http://www.regular-expressions.info/floatingpoint.html
#
# pattern1: find a decimal number (int or float) and any
# characters following it upto the next decimal number. [^0-9\-+]*
# => keep gathering elements until we get to a digit, a - or a
# +. These three indicates the possible start of the next number.
pattern1 = re.compile(r"([-+]?[0-9]*\.?[0-9]+[^0-9\-+]*)")
# pattern2: find decimal number (int or float) in string.
pattern2 = re.compile(r"([-+]?[0-9]*\.?[0-9]+)")
hmsdms = hmsdms.lower()
hdlist = pattern1.findall(hmsdms)
parts = [None, None, None]
def _fill_right_not_none():
# Find the pos. where parts is not None. Next value must
# be inserted to the right of this. If this is 2 then we have
# already filled seconds part, raise exception. If this is 1
# then fill 2. If this is 0 fill 1. If none of these then fill
# 0.
rp = reversed(parts)
for i, j in enumerate(rp):
if j is not None:
break
if i == 0:
# Seconds part already filled.
raise ValueError("Invalid string.")
elif i == 1:
parts[2] = v
elif i == 2:
# Either parts[0] is None so fill it, or it is filled
# and hence fill parts[1].
if parts[0] is None:
parts[0] = v
else:
parts[1] = v
for valun in hdlist:
try:
# See if this is pure number.
v = float(valun)
# Sexagesimal part cannot be determined. So guess it by
# seeing which all parts have already been identified.
_fill_right_not_none()
except ValueError:
# Not a pure number. Infer sexagesimal part from the
# suffix.
if "hh" in valun or "h" in valun:
m = pattern2.search(valun)
parts[0] = float(valun[m.start():m.end()])
units = "hours"
if "dd" in valun or "d" in valun:
m = pattern2.search(valun)
parts[0] = float(valun[m.start():m.end()])
units = "degrees"
if "mm" in valun or "m" in valun:
m = pattern2.search(valun)
parts[1] = float(valun[m.start():m.end()])
if "ss" in valun or "s" in valun:
m = pattern2.search(valun)
parts[2] = float(valun[m.start():m.end()])
if "'" in valun:
m = pattern2.search(valun)
parts[1] = float(valun[m.start():m.end()])
if '"' in valun:
m = pattern2.search(valun)
parts[2] = float(valun[m.start():m.end()])
if ":" in valun:
# Sexagesimal part cannot be determined. So guess it by
# seeing which all parts have already been identified.
v = valun.replace(":", "")
v = float(v)
_fill_right_not_none()
if not units:
units = "degrees"
# Find sign. Only the first identified part can have a -ve sign.
for i in parts:
if i and i < 0.0:
if sign is None:
sign = -1
else:
raise ValueError("Only one number can be negative.")
if sign is None: # None of these are negative.
sign = 1
vals = [abs(i) if i is not None else 0.0 for i in parts]
return dict(sign=sign, units=units, vals=vals, parts=parts)