stdpipe 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stdpipe/resolve.py ADDED
@@ -0,0 +1,267 @@
1
+ import xml.dom.minidom as minidom
2
+ import requests
3
+ import json
4
+ import re
5
+ import io
6
+
7
+ from astropy.coordinates import SkyCoord
8
+
9
+
10
+ def simbadResolve(name='m31'):
11
+ url = 'http://cdsweb.u-strasbg.fr/viz-bin/nph-sesame/-oxpi/SNVA'
12
+ res = requests.get(url, params=requests.utils.quote(name))
13
+
14
+ try:
15
+ xml = minidom.parseString(res.content)
16
+
17
+ r = xml.getElementsByTagName('Resolver')[0]
18
+
19
+ name = r.getElementsByTagName('oname')[0].childNodes[0].nodeValue
20
+ ra = float(r.getElementsByTagName('jradeg')[0].childNodes[0].nodeValue)
21
+ dec = float(r.getElementsByTagName('jdedeg')[0].childNodes[0].nodeValue)
22
+
23
+ return name, ra, dec
24
+ except:
25
+ return None, None, None
26
+
27
+
28
+ def parseSexadecimal(string):
29
+ value = 0
30
+
31
+ m = re.search("^\s*([+-])?\s*(\d{1,3})\s+(\d{1,2})\s+(\d{1,2}\.?\d*)\s*$", string) or re.search(
32
+ "^\s*([+-])?\s*(\d{1,3})\:(\d{1,2})\:(\d{1,2}\.?\d*)\s*$", string
33
+ )
34
+ if m:
35
+ value = float(m.group(2)) + float(m.group(3)) / 60 + float(m.group(4)) / 3600
36
+
37
+ if m.group(1) == '-':
38
+ value = -value
39
+
40
+ return value
41
+
42
+
43
+ def tnsResolve(name='AT2023lxx'):
44
+ # Normalize AT name to have whitespace before year
45
+ m = re.match('^(AT)\s*(2\w+)$', name)
46
+ if m:
47
+ name = m[1] + ' ' + m[2]
48
+
49
+ for reverse in [False, True]:
50
+ params = {'resolver': 'tns', 'name': name}
51
+ if reverse:
52
+ params.update({'reverse': True})
53
+
54
+ r = requests.post("https://api.ztf.fink-portal.org/api/v1/resolver", json=params)
55
+
56
+ res = json.loads(r.content)
57
+ if res:
58
+ break
59
+
60
+ if res:
61
+ return res[0]['d:fullname'], res[0]['d:ra'], res[0]['d:declination']
62
+
63
+ return None, None, None
64
+
65
+
66
+ def resolve(string='M33', verbose=False):
67
+ """
68
+ Resolve the object name (or coordinates string) into proper coordinates on the sky.
69
+
70
+ This function attempts multiple resolution methods in order:
71
+ 1. Decimal degrees (RA Dec in degrees)
72
+ 2. Sexagesimal coordinates (hours/degrees with minutes and seconds)
73
+ 3. Simbad name resolution (astronomical objects)
74
+ 4. TNS name resolution (transients)
75
+ 5. Astropy SkyCoord.from_name() (fallback)
76
+
77
+ Parameters
78
+ ----------
79
+ string : str, optional
80
+ Input string to resolve (object name or coordinates).
81
+ verbose : bool or callable, optional
82
+ Enable verbose output. Can be True/False or a print-like function.
83
+
84
+ Returns
85
+ -------
86
+ astropy.coordinates.SkyCoord or None
87
+ Resolved sky coordinates, or None if resolution fails.
88
+
89
+ **Supported Coordinate Formats:**
90
+
91
+ 1. **Decimal degrees** (RA and Dec in degrees, space or comma separated):
92
+ - ``"123.456 45.678"`` → RA=123.456°, Dec=+45.678°
93
+ - ``"123.456, 45.678"`` → RA=123.456°, Dec=+45.678° (comma-separated)
94
+ - ``"123.456 -45.678"`` → RA=123.456°, Dec=-45.678°
95
+ - ``"10.68, 41.27"`` → RA=10.68°, Dec=+41.27° (M31)
96
+
97
+ 2. **Sexagesimal coordinates** (space-separated or colon-separated):
98
+ - Space format: ``"HH MM SS.ss ±DD MM SS.ss"``
99
+ - Colon format: ``"HH:MM:SS.ss ±DD:MM:SS.ss"``
100
+ - Comma can separate RA from Dec: ``"HH MM SS.ss, ±DD MM SS.ss"``
101
+
102
+ Examples:
103
+ - ``"12 34 56.7 +45 12 34.5"`` → RA=12h 34m 56.7s, Dec=+45° 12' 34.5"
104
+ - ``"12 34 56.7, +45 12 34.5"`` → Same (comma between RA and Dec)
105
+ - ``"12:34:56.7 +45:12:34.5"`` → RA=12h 34m 56.7s, Dec=+45° 12' 34.5"
106
+ - ``"12:34:56.7, +45:12:34.5"`` → Same (comma between RA and Dec)
107
+ - ``"12 34 56 45 12 34"`` → Dec assumed positive if no sign
108
+ - ``"00 42 44.3, +41 16 09"`` → M31 coordinates (comma-separated)
109
+
110
+ **Note:** RA is interpreted as **hours** (multiplied by 15 to convert to degrees),
111
+ Dec is interpreted as **degrees**. This is the standard astronomical convention.
112
+
113
+ 3. **Object names** (resolved via Simbad, TNS, or Astropy):
114
+ - Messier objects: ``"M31"``, ``"M 31"``, ``"m31"``
115
+ - NGC/IC objects: ``"NGC1234"``, ``"IC 5146"``
116
+ - Named stars: ``"Betelgeuse"``, ``"Vega"``
117
+ - Transients: ``"AT2023lxx"``, ``"AT 2023lxx"``, ``"SN2023ixf"``
118
+ - Coordinates as names: ``"12h34m56s +45d12m34s"`` (via Astropy)
119
+
120
+ **Resolution Priority:**
121
+
122
+ The function tries methods in order and returns the first successful match:
123
+ 1. Decimal degrees parsing (fastest, no network)
124
+ 2. Sexagesimal parsing (fast, no network)
125
+ 3. Simbad query (network required)
126
+ 4. TNS query via Fink API (network required, for transients)
127
+ 5. Astropy SkyCoord.from_name() (network required, CDS Sesame)
128
+
129
+ **Examples:**
130
+
131
+ >>> from stdpipe.resolve import resolve
132
+ >>>
133
+ >>> # Decimal degrees
134
+ >>> target = resolve("10.68 41.27")
135
+ >>> print(target.ra.deg, target.dec.deg)
136
+ 10.68 41.27
137
+ >>>
138
+ >>> # Decimal degrees (comma-separated)
139
+ >>> target = resolve("10.68, 41.27")
140
+ >>> print(target.ra.deg, target.dec.deg)
141
+ 10.68 41.27
142
+ >>>
143
+ >>> # Sexagesimal (space-separated)
144
+ >>> target = resolve("12 34 56 +45 12 34")
145
+ >>> print(f"{target.ra.deg:.2f} {target.dec.deg:.2f}")
146
+ 188.73 45.21
147
+ >>>
148
+ >>> # Sexagesimal (comma between RA and Dec)
149
+ >>> target = resolve("12 34 56, +45 12 34")
150
+ >>> print(f"{target.ra.deg:.2f} {target.dec.deg:.2f}")
151
+ 188.73 45.21
152
+ >>>
153
+ >>> # Sexagesimal (colon-separated)
154
+ >>> target = resolve("00:42:44.3 +41:16:09")
155
+ >>> print(f"{target.ra.deg:.2f} {target.dec.deg:.2f}")
156
+ 10.68 41.27
157
+ >>>
158
+ >>> # Sexagesimal (colon-separated with comma)
159
+ >>> target = resolve("00:42:44.3, +41:16:09")
160
+ >>> print(f"{target.ra.deg:.2f} {target.dec.deg:.2f}")
161
+ 10.68 41.27
162
+ >>>
163
+ >>> # Object name (requires network)
164
+ >>> target = resolve("M31", verbose=True)
165
+ Resolved by Simbad as M 31
166
+ RA = 10.6847 deg, Dec = 41.2688 deg
167
+ >>>
168
+ >>> # Transient name (requires network)
169
+ >>> target = resolve("AT2023lxx", verbose=True)
170
+ Resolved by TNS as AT 2023lxx
171
+ RA = 123.4560 deg, Dec = -45.6780 deg
172
+ >>>
173
+ >>> # Failed resolution returns None
174
+ >>> target = resolve("InvalidObject123")
175
+ >>> print(target)
176
+ None
177
+
178
+ **Verbose Mode:**
179
+
180
+ When ``verbose=True``, prints resolution method and result:
181
+
182
+ >>> target = resolve("123.456 45.678", verbose=True)
183
+ Resolved as two values in degrees
184
+ RA = 123.4560 deg, Dec = 45.6780 deg
185
+
186
+ You can also provide a custom logging function:
187
+
188
+ >>> def my_log(*args):
189
+ ... print("[RESOLVER]", *args)
190
+ >>> target = resolve("M31", verbose=my_log)
191
+ [RESOLVER] Resolved by Simbad as M 31
192
+ [RESOLVER] RA = 10.6847 deg, Dec = 41.2688 deg
193
+
194
+ **Network Requirements:**
195
+
196
+ - Decimal/sexagesimal parsing: No network required
197
+ - Simbad resolution: Queries http://cdsweb.u-strasbg.fr
198
+ - TNS resolution: Queries https://api.ztf.fink-portal.org
199
+ - Astropy fallback: Queries CDS Sesame service
200
+
201
+ If network is unavailable, only coordinate parsing will work. Object name
202
+ resolution will fail silently and return None.
203
+ """
204
+
205
+ # Simple wrapper around print for logging in verbose mode only
206
+ log = (verbose if callable(verbose) else print) if verbose else lambda *args, **kwargs: None
207
+
208
+ target = None
209
+
210
+ if target is None:
211
+ m = re.search(r"^\s*(\d+\.?\d*)\s*[,\s]+\s*([+-]?\d+\.?\d*)\s*$", string)
212
+ if m:
213
+ log("Resolved as two values in degrees")
214
+
215
+ ra = float(m.group(1))
216
+ dec = float(m.group(2))
217
+
218
+ target = SkyCoord(ra, dec, unit='deg')
219
+
220
+ if target is None:
221
+ m = re.search(
222
+ r"^\s*(\d{1,2})\s+(\d{1,2})\s+(\d{1,2}\.?\d*)\s*[,\s]+\s*([+-])?\s*(\d{1,3})\s+(\d{1,2})\s+(\d{1,2}\.?\d*)\s*$",
223
+ string,
224
+ ) or re.search(
225
+ r"^\s*(\d{1,2})\:(\d{1,2})\:(\d{1,2}\.?\d*)\s*[,\s]+\s*([+-])?\s*(\d{1,3})\:(\d{1,2})\:(\d{1,2}\.?\d*)\s*$",
226
+ string,
227
+ )
228
+ if m:
229
+ log("Resolved as two sexadecimal values, interpreted as hours and degrees")
230
+
231
+ ra = (float(m.group(1)) + float(m.group(2)) / 60 + float(m.group(3)) / 3600) * 15
232
+ dec = float(m.group(5)) + float(m.group(6)) / 60 + float(m.group(7)) / 3600
233
+
234
+ if m.group(4) == '-':
235
+ dec = -dec
236
+
237
+ target = SkyCoord(ra, dec, unit='deg')
238
+
239
+ if target is None:
240
+ name, ra, dec = simbadResolve(string)
241
+
242
+ if name:
243
+ log("Resolved by Simbad as", name)
244
+
245
+ target = SkyCoord(ra, dec, unit='deg')
246
+
247
+ if target is None:
248
+ name, ra, dec = tnsResolve(string)
249
+
250
+ if name:
251
+ log("Resolved by TNS as", name)
252
+
253
+ target = SkyCoord(ra, dec, unit='deg')
254
+
255
+ if target is None:
256
+ try:
257
+ target = SkyCoord.from_name(string, parse=True)
258
+ log("Resolved by Astropy SkyCoord.from_name()")
259
+ except:
260
+ pass
261
+
262
+ if target is not None:
263
+ log(f"RA = {target.ra.deg:.4f} deg, Dec = {target.dec.deg:.4f} deg")
264
+ else:
265
+ log(f"Failed to resolve", string)
266
+
267
+ return target