astro-otter 0.6.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.
- astro_otter-0.6.0.dist-info/METADATA +161 -0
- astro_otter-0.6.0.dist-info/RECORD +18 -0
- astro_otter-0.6.0.dist-info/WHEEL +5 -0
- astro_otter-0.6.0.dist-info/licenses/LICENSE +21 -0
- astro_otter-0.6.0.dist-info/top_level.txt +1 -0
- otter/__init__.py +19 -0
- otter/_version.py +5 -0
- otter/exceptions.py +74 -0
- otter/io/__init__.py +0 -0
- otter/io/data_finder.py +1045 -0
- otter/io/host.py +186 -0
- otter/io/otter.py +1594 -0
- otter/io/transient.py +1453 -0
- otter/plotter/__init__.py +0 -0
- otter/plotter/otter_plotter.py +76 -0
- otter/plotter/plotter.py +266 -0
- otter/schema.py +312 -0
- otter/util.py +850 -0
otter/io/data_finder.py
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Host object that stores information on the Transient DataFinder and provides utility
|
|
3
|
+
methods for pulling in data corresponding to that host
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import os
|
|
8
|
+
import csv
|
|
9
|
+
import io
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
import math
|
|
13
|
+
from urllib.request import urlopen
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from astropy import units as u
|
|
17
|
+
from astropy.coordinates import SkyCoord
|
|
18
|
+
from astropy.time import Time
|
|
19
|
+
from astropy.table import Table
|
|
20
|
+
from astropy.io.votable import parse_single_table
|
|
21
|
+
from astropy.io import ascii
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import pandas as pd
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
from fundamentals.stats import rolling_window_sigma_clip
|
|
28
|
+
from operator import itemgetter
|
|
29
|
+
|
|
30
|
+
from ..util import VIZIER_LARGE_CATALOGS
|
|
31
|
+
from ..exceptions import MissingEnvVarError
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DataFinder(object):
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
ra: str | float,
|
|
40
|
+
dec: str | float,
|
|
41
|
+
ra_units: str | u.Unit,
|
|
42
|
+
dec_units: str | u.Unit,
|
|
43
|
+
name: str = None,
|
|
44
|
+
redshift: float = None,
|
|
45
|
+
reference: list[str] = None,
|
|
46
|
+
**kwargs,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Object to store DataFinder info to query public data sources of host galaxies
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
ra (str|float) : The RA of the host to be passed to an astropy SkyCoord
|
|
53
|
+
dec (str|float) : The declination of the host to be passed to an
|
|
54
|
+
astropy SkyCoord
|
|
55
|
+
ra_units (str|astropy.units.Unit) : units of the RA, to be passed to
|
|
56
|
+
the unit keyword of SkyCoord
|
|
57
|
+
dec_units (str|astropy.units.Unit) : units of the declination, to be
|
|
58
|
+
passed to the unit keyword of
|
|
59
|
+
SkyCoord
|
|
60
|
+
name (str) : The name of the host galaxy
|
|
61
|
+
redshift (float) : The redshift of the host galaxy
|
|
62
|
+
reference (list[str]) : a list of bibcodes that found this to be the host
|
|
63
|
+
kwargs : Just here so we can pass **Transient['host'] into this constructor
|
|
64
|
+
and any extraneous properties will be ignored.
|
|
65
|
+
"""
|
|
66
|
+
self.coord = SkyCoord(ra, dec, unit=(ra_units, dec_units))
|
|
67
|
+
self.name = name
|
|
68
|
+
self.z = redshift
|
|
69
|
+
self.redshift = redshift # just here for ease of use
|
|
70
|
+
self.bibcodes = reference
|
|
71
|
+
|
|
72
|
+
def __repr__(self) -> str:
|
|
73
|
+
"""
|
|
74
|
+
String representation of the DataFinder for printing
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
if self.name is None:
|
|
78
|
+
print_name = "No Name DataFinder"
|
|
79
|
+
else:
|
|
80
|
+
print_name = self.name
|
|
81
|
+
|
|
82
|
+
return f"{print_name} @ (RA, Dec)=({self.coord.ra},{self.coord.dec})"
|
|
83
|
+
|
|
84
|
+
def __iter__(self) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
Provides an iterator for the properties of this DataFinder. Yields (key, value)
|
|
87
|
+
"""
|
|
88
|
+
out = dict(
|
|
89
|
+
host_ra=self.coord.ra.value,
|
|
90
|
+
host_dec=self.coord.dec.value,
|
|
91
|
+
host_ra_units="deg",
|
|
92
|
+
host_dec_units="deg",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if self.name is not None:
|
|
96
|
+
out["host_name"] = self.name
|
|
97
|
+
|
|
98
|
+
if self.z is not None:
|
|
99
|
+
out["host_redshift"] = self.z
|
|
100
|
+
|
|
101
|
+
if self.bibcodes is not None:
|
|
102
|
+
out["reference"] = self.bibcodes
|
|
103
|
+
|
|
104
|
+
for k, v in out.items():
|
|
105
|
+
yield (k, v)
|
|
106
|
+
|
|
107
|
+
###################################################################################
|
|
108
|
+
################### CONVENIENCE METHODS FOR QUERYING HOST METADATA ################
|
|
109
|
+
###################################################################################
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _wrap_astroquery(module, *args, **kwargs):
|
|
113
|
+
"""
|
|
114
|
+
Private convenience method that just standardizes how we call the query_region
|
|
115
|
+
method in astroquery
|
|
116
|
+
"""
|
|
117
|
+
return module.query_region(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
def query_simbad(self, radius="5 arcsec", **kwargs):
|
|
120
|
+
"""
|
|
121
|
+
Query SIMBAD through astroquery to provide any other "meta" information on this
|
|
122
|
+
host that may not be stored in the OTTER
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
radius (str|astropy.quantity.Quantity) : search radius for astroquery
|
|
126
|
+
**kwargs : any other arguments for astroquery.vizier.Vizier.query_region
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
astropy Table of the simbad results.
|
|
130
|
+
"""
|
|
131
|
+
from astroquery.simbad import Simbad
|
|
132
|
+
|
|
133
|
+
return DataFinder._wrap_astroquery(Simbad, self.coord, radius=radius, **kwargs)
|
|
134
|
+
|
|
135
|
+
def query_vizier(self, radius="5 arcsec", **kwargs):
|
|
136
|
+
"""
|
|
137
|
+
Query the ViZier catalog for TIME-AVERAGED data from their major/large catalogs.
|
|
138
|
+
|
|
139
|
+
ViZier Catalogs Queried:
|
|
140
|
+
- 2MASS-PSC
|
|
141
|
+
- 2MASX
|
|
142
|
+
- AC2000.2
|
|
143
|
+
- AKARI
|
|
144
|
+
- ALLWISE
|
|
145
|
+
- ASCC-2.5
|
|
146
|
+
- B/DENIS
|
|
147
|
+
- CMC14
|
|
148
|
+
- Gaia-DR1
|
|
149
|
+
- GALEX
|
|
150
|
+
- GLIMPSE
|
|
151
|
+
- GSC-ACT
|
|
152
|
+
- GSC1.2
|
|
153
|
+
- GSC2.2
|
|
154
|
+
- GSC2.3
|
|
155
|
+
- HIP
|
|
156
|
+
- HIP2
|
|
157
|
+
- IRAS
|
|
158
|
+
- NOMAD1
|
|
159
|
+
- NVSS
|
|
160
|
+
- PanSTARRS-DR1
|
|
161
|
+
- PGC
|
|
162
|
+
- Planck-DR1
|
|
163
|
+
- PPMX
|
|
164
|
+
- PPMXL
|
|
165
|
+
- SDSS-DR12
|
|
166
|
+
- SDSS-DR7
|
|
167
|
+
- SDSS-DR9
|
|
168
|
+
- Tycho-2
|
|
169
|
+
- UCAC2
|
|
170
|
+
- UCAC3
|
|
171
|
+
- UCAC4
|
|
172
|
+
- UKIDSS
|
|
173
|
+
- USNO-A2
|
|
174
|
+
- USNO-B1
|
|
175
|
+
- WISE
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
radius (str|astropy.quantity.Quantity) : search radius for astroquery
|
|
179
|
+
**kwargs : any other arguments for astroquery.vizier.Vizier.query_region
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
astropy TableList of the time-averaged photometry associated with this host.
|
|
183
|
+
"""
|
|
184
|
+
from astroquery.vizier import Vizier
|
|
185
|
+
|
|
186
|
+
return DataFinder._wrap_astroquery(
|
|
187
|
+
Vizier, self.coord, radius=radius, catalog=VIZIER_LARGE_CATALOGS
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
###################################################################################
|
|
191
|
+
######### CONVENIENCE METHODS FOR QUERYING HOST TIME SERIES PHOTOMETRY ###########
|
|
192
|
+
###################################################################################
|
|
193
|
+
|
|
194
|
+
def query_atlas(
|
|
195
|
+
self, days_ago: int = 365, disc_date: float = None, clip_sigma: float = 2.0
|
|
196
|
+
) -> pd.DataFrame:
|
|
197
|
+
"""
|
|
198
|
+
Query ATLAS forced photometry for photometry for this host
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
days_ago (int) : Number of days before the transients discovery date
|
|
202
|
+
(or today if no disc_date is given) to get ATLAS
|
|
203
|
+
forced photometry for.
|
|
204
|
+
disc_date (float) : The discovery date of the transient in MJD.
|
|
205
|
+
clip_sigma (float) : amount to sigma clip the ATLAS data by
|
|
206
|
+
|
|
207
|
+
Return:
|
|
208
|
+
pandas DataFrame of the ATLAS forced photometry for this host
|
|
209
|
+
"""
|
|
210
|
+
base_url = "https://fallingstar-data.com/forcedphot"
|
|
211
|
+
|
|
212
|
+
token = os.environ.get("ATLAS_API_TOKEN", None)
|
|
213
|
+
if token is None:
|
|
214
|
+
logger.warn(
|
|
215
|
+
"Getting your token from ATLAS. Please add ATLAS_API_TOKEN to your \
|
|
216
|
+
environment variables to avoid this!"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
uname = os.environ.get("ATLAS_UNAME", default=None)
|
|
220
|
+
pword = os.environ.get("ATLAS_PWORD", default=None)
|
|
221
|
+
|
|
222
|
+
if uname is None and pword is None:
|
|
223
|
+
raise MissingEnvVarError(["ATLAS_UNAME", "ATLAS_PWORD"], base_url)
|
|
224
|
+
elif uname is None and pword is not None:
|
|
225
|
+
raise MissingEnvVarError(["ATLAS_UNAME"], base_url)
|
|
226
|
+
elif uname is not None and pword is None:
|
|
227
|
+
raise MissingEnvVarError(["ATLAS_PWORD"], base_url)
|
|
228
|
+
|
|
229
|
+
resp = requests.post(
|
|
230
|
+
url=f"{base_url}/api-token-auth/",
|
|
231
|
+
data={"username": uname, "password": pword},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
token = resp.json()["token"]
|
|
235
|
+
|
|
236
|
+
headers = {"Authorization": f"Token {token}", "Accept": "application/json"}
|
|
237
|
+
|
|
238
|
+
# compute the query start
|
|
239
|
+
if disc_date is None:
|
|
240
|
+
t_queryend = Time.now().mjd
|
|
241
|
+
logger.warn(
|
|
242
|
+
"Since no transient name is given we are using today \
|
|
243
|
+
as the query end!"
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
t_queryend = Time(disc_date, format="mjd").mjd
|
|
247
|
+
|
|
248
|
+
t_querystart = t_queryend - days_ago
|
|
249
|
+
|
|
250
|
+
# submit the query to the ATLAS forced photometry server
|
|
251
|
+
task_url = None
|
|
252
|
+
while not task_url:
|
|
253
|
+
with requests.Session() as s:
|
|
254
|
+
resp = s.post(
|
|
255
|
+
f"{base_url}/queue/",
|
|
256
|
+
headers=headers,
|
|
257
|
+
data={
|
|
258
|
+
"ra": self.coord.ra.value,
|
|
259
|
+
"dec": self.coord.ra.value,
|
|
260
|
+
"send_email": False,
|
|
261
|
+
"mjd_min": t_querystart,
|
|
262
|
+
"mjd_max": t_queryend,
|
|
263
|
+
"use_reduced": False,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
if resp.status_code == 201: # success
|
|
267
|
+
task_url = resp.json()["url"]
|
|
268
|
+
logger.info(f"The task URL is {task_url}")
|
|
269
|
+
elif resp.status_code == 429: # throttled
|
|
270
|
+
message = resp.json()["detail"]
|
|
271
|
+
logger.info(f"{resp.status_code} {message}")
|
|
272
|
+
t_sec = re.findall(r"available in (\d+) seconds", message)
|
|
273
|
+
t_min = re.findall(r"available in (\d+) minutes", message)
|
|
274
|
+
if t_sec:
|
|
275
|
+
waittime = int(t_sec[0])
|
|
276
|
+
elif t_min:
|
|
277
|
+
waittime = int(t_min[0]) * 60
|
|
278
|
+
else:
|
|
279
|
+
waittime = 10
|
|
280
|
+
logger.info(f"Waiting {waittime} seconds")
|
|
281
|
+
time.sleep(waittime)
|
|
282
|
+
else:
|
|
283
|
+
raise Exception(f"ERROR {resp.status_code}\n{resp.text}")
|
|
284
|
+
|
|
285
|
+
# Now wait for the result
|
|
286
|
+
result_url = None
|
|
287
|
+
taskstarted_printed = False
|
|
288
|
+
while not result_url:
|
|
289
|
+
with requests.Session() as s:
|
|
290
|
+
resp = s.get(task_url, headers=headers)
|
|
291
|
+
|
|
292
|
+
if resp.status_code == 200: # HTTP OK
|
|
293
|
+
if resp.json()["finishtimestamp"]:
|
|
294
|
+
result_url = resp.json()["result_url"]
|
|
295
|
+
logger.info(
|
|
296
|
+
f"Task is complete with results available at {result_url}"
|
|
297
|
+
)
|
|
298
|
+
elif resp.json()["starttimestamp"]:
|
|
299
|
+
if not taskstarted_printed:
|
|
300
|
+
print(
|
|
301
|
+
f"Task is running (started at\
|
|
302
|
+
{resp.json()['starttimestamp']})"
|
|
303
|
+
)
|
|
304
|
+
taskstarted_printed = True
|
|
305
|
+
time.sleep(2)
|
|
306
|
+
else:
|
|
307
|
+
# print(f"Waiting for job to start (queued at {timestamp})")
|
|
308
|
+
time.sleep(4)
|
|
309
|
+
else:
|
|
310
|
+
raise Exception(f"ERROR {resp.status_code}\n{resp.text}")
|
|
311
|
+
|
|
312
|
+
# get and clean up the result
|
|
313
|
+
with requests.Session() as s:
|
|
314
|
+
textdata = s.get(result_url, headers=headers).text
|
|
315
|
+
|
|
316
|
+
atlas_phot = DataFinder._atlas_stack(textdata, clipping_sigma=clip_sigma)
|
|
317
|
+
|
|
318
|
+
return pd.DataFrame(atlas_phot)
|
|
319
|
+
|
|
320
|
+
def query_ptf(self, radius: str | u.Quantity = "5 arcsec", **kwargs) -> Table:
|
|
321
|
+
"""
|
|
322
|
+
Query the palomer transient facility's light curve catalog for this host
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
radius (str|astropy.quantity.Quantity) : search radius
|
|
326
|
+
**kwargs : other optional arguments for astroquery's query_region
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
An astropy Table of the resulting light curve
|
|
330
|
+
"""
|
|
331
|
+
from astroquery.ipac.irsa import Irsa
|
|
332
|
+
|
|
333
|
+
ptf_lc_catalog = "ptf_lightcurves"
|
|
334
|
+
return DataFinder._wrap_astroquery(
|
|
335
|
+
Irsa, self.coord, radius=radius, catalog=ptf_lc_catalog
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def query_ztf(self, radius: float = 5):
|
|
339
|
+
"""
|
|
340
|
+
Query ZTF photometry/forced photometry for photometry for this host
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
radius (float) : The search radius in arcseconds
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
An astropy table of the time series data from the cone search in ZTF
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
base_url = "https://irsa.ipac.caltech.edu/cgi-bin/ZTF/nph_light_curves?"
|
|
350
|
+
|
|
351
|
+
ra, dec = self.coord.ra.value, self.coord.dec.value
|
|
352
|
+
search_radius_arcseconds = radius # in arcseconds
|
|
353
|
+
search_radius_degree = search_radius_arcseconds / 3600
|
|
354
|
+
|
|
355
|
+
query_url = f"{base_url}POS=CIRCLE%20{ra}%20{dec}%20{search_radius_degree}"
|
|
356
|
+
|
|
357
|
+
resp = urlopen(query_url)
|
|
358
|
+
|
|
359
|
+
votab = parse_single_table(io.BytesIO(resp.read()))
|
|
360
|
+
|
|
361
|
+
return Table(votab.array)
|
|
362
|
+
|
|
363
|
+
def query_asassn(self, radius: float = 5.0, nthreads: int = 2) -> pd.DataFrame:
|
|
364
|
+
"""
|
|
365
|
+
Query ASASSN photometry/forced photometry for photometry for this host
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
radius (float) : search radius in arcseconds
|
|
369
|
+
nthreads (int) : number of threads to utilize during download, default is 2
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
A pandas dataframe with the ASASSN lightcurve for this object
|
|
373
|
+
"""
|
|
374
|
+
from pyasassn.client import SkyPatrolClient
|
|
375
|
+
|
|
376
|
+
client = SkyPatrolClient()
|
|
377
|
+
light_curve = client.cone_search(
|
|
378
|
+
self.coord.ra.value,
|
|
379
|
+
self.coord.dec.value,
|
|
380
|
+
radius=radius,
|
|
381
|
+
units="arcsec",
|
|
382
|
+
download=True,
|
|
383
|
+
threads=nthreads,
|
|
384
|
+
)
|
|
385
|
+
return light_curve.data
|
|
386
|
+
|
|
387
|
+
def query_wise(
|
|
388
|
+
self,
|
|
389
|
+
radius: float = 5,
|
|
390
|
+
datadir: str = "ipac/",
|
|
391
|
+
overwrite: bool = False,
|
|
392
|
+
verbose=False,
|
|
393
|
+
**kwargs,
|
|
394
|
+
) -> pd.DataFrame:
|
|
395
|
+
"""
|
|
396
|
+
Query NEOWISE for their multiepoch photometry
|
|
397
|
+
|
|
398
|
+
The method used to query wise here was taken from this github repo:
|
|
399
|
+
https://github.com/HC-Hwang/wise_light_curves/tree/master
|
|
400
|
+
and you should cite this other paper that the authors of this code developed
|
|
401
|
+
it for: https://ui.adsabs.harvard.edu/abs/2020MNRAS.493.2271H/abstract
|
|
402
|
+
|
|
403
|
+
This will download the ipac data files to the "datadir" argument. by default,
|
|
404
|
+
these will go into os.getcwd()/ipac
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
radius (float) : The cone search radius in arcseconds
|
|
408
|
+
overwrite (bool) : Overwrite the existing datasets downloaded from wise
|
|
409
|
+
**kwargs : Other optional arguments for the astroquery query_region
|
|
410
|
+
Returns:
|
|
411
|
+
An astropy Table of the multiepoch wise data for this host
|
|
412
|
+
"""
|
|
413
|
+
# from https://www.cambridge.org/core/journals/
|
|
414
|
+
# publications-of-the-astronomical-society-of-australia/article/
|
|
415
|
+
# recalibrating-the-widefield-infrared-survey-explorer-wise-w4-filter/
|
|
416
|
+
# B238BFFE19A533A2D2638FE88CCC2E89
|
|
417
|
+
band_vals = {"w1": 3.4, "w2": 4.6, "w3": 12, "w4": 22} # in um
|
|
418
|
+
|
|
419
|
+
ra, dec = self.coord.ra.value, self.coord.dec.value
|
|
420
|
+
|
|
421
|
+
fbasename = f"wise_{self.name}"
|
|
422
|
+
allwise_name = f"{fbasename}_allwise.ipac"
|
|
423
|
+
neowise_name = f"{fbasename}_neowise.ipac"
|
|
424
|
+
|
|
425
|
+
if not os.path.exists(datadir):
|
|
426
|
+
os.makedirs(datadir)
|
|
427
|
+
|
|
428
|
+
self._download_single_data(
|
|
429
|
+
name=fbasename,
|
|
430
|
+
ra=ra,
|
|
431
|
+
dec=dec,
|
|
432
|
+
root_path=datadir,
|
|
433
|
+
radius=radius,
|
|
434
|
+
overwrite=overwrite,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
allwise = ascii.read(f"ipac/{allwise_name}", format="ipac")
|
|
438
|
+
neowise = ascii.read(f"ipac/{neowise_name}", format="ipac")
|
|
439
|
+
|
|
440
|
+
allwise, neowise = self._only_good_data(allwise, neowise, verbose=verbose)
|
|
441
|
+
if verbose and (allwise is None or neowise is None):
|
|
442
|
+
print(f"Limited good infrared data for {self.name}, skipping!")
|
|
443
|
+
|
|
444
|
+
mjd, mag, mag_err, filts = self._make_full_lightcurve_multibands(
|
|
445
|
+
allwise, neowise, bands=["w1", "w2", "w3", "w4"]
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
df = pd.DataFrame(
|
|
449
|
+
dict(
|
|
450
|
+
name=[self.name] * len(mjd),
|
|
451
|
+
date_mjd=mjd,
|
|
452
|
+
filter=filts,
|
|
453
|
+
filter_eff=[band_vals[f] for f in filts],
|
|
454
|
+
filter_eff_unit=["um"] * len(mjd),
|
|
455
|
+
flux=mag,
|
|
456
|
+
flux_err=mag_err,
|
|
457
|
+
flux_unit=["mag(AB)"] * len(mjd),
|
|
458
|
+
upperlimit=[False] * len(mjd),
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# clean up the wise data by filtering out negative flux
|
|
463
|
+
wise = df[df.flux > 0].reset_index(drop=True)
|
|
464
|
+
return wise
|
|
465
|
+
|
|
466
|
+
def query_alma(self, radius: float = 5, **kwargs) -> Table:
|
|
467
|
+
"""
|
|
468
|
+
Query ALMA to see if there are observations of this host.
|
|
469
|
+
|
|
470
|
+
NOTE: Since this is radio/mm data, it is unlikely that the output table will
|
|
471
|
+
simply have fluxes in it. Instead you will need to use the access_url column
|
|
472
|
+
to download and reduce this data.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
radius (float) : The cone search radius in arcseconds
|
|
476
|
+
**kwargs : Other optional arguments for the astroquery query_region
|
|
477
|
+
Returns:
|
|
478
|
+
An astropy Table of the multiepoch wise data for this host
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
logger.warn(
|
|
482
|
+
"This method may not work if you are using a conda environment!\
|
|
483
|
+
This is a known issue in setuptools that is not resolved!"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
from astroquery.alma import Alma
|
|
487
|
+
|
|
488
|
+
res = DataFinder._wrap_astroquery(
|
|
489
|
+
Alma, self.coord, radius=5 * u.arcsec, **kwargs
|
|
490
|
+
)
|
|
491
|
+
return res
|
|
492
|
+
|
|
493
|
+
def query_first(
|
|
494
|
+
self, radius: u.Quantity = 5 * u.arcmin, get_image: bool = False, **kwargs
|
|
495
|
+
) -> list:
|
|
496
|
+
"""
|
|
497
|
+
Query the FIRST radio survey and return an astropy table of the flux density
|
|
498
|
+
|
|
499
|
+
This queries Table 2 from Ofek & Frail (2011); 2011ApJ...737...45O
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
radius (u.Quantity) : An astropy Quantity with the image height/width
|
|
503
|
+
get_image (bool) : If True, download and return a list of the associated
|
|
504
|
+
images too.
|
|
505
|
+
**kwargs : any other arguments to pass to the astroquery.image_cutouts
|
|
506
|
+
get_images method
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Astropy table of the flux densities. If get_image is True, it also returns
|
|
510
|
+
a list of FIRST radio survey images
|
|
511
|
+
"""
|
|
512
|
+
from astroquery.vizier import Vizier
|
|
513
|
+
|
|
514
|
+
res = DataFinder._wrap_astroquery(
|
|
515
|
+
Vizier, self.coord, radius=radius, catalog="J/ApJ/737/45/table2"
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
if get_image:
|
|
519
|
+
from astroquery.image_cutouts.first import First
|
|
520
|
+
|
|
521
|
+
res_img = First.get_images(self.coord, image_size=radius, **kwargs)
|
|
522
|
+
return res, res_img
|
|
523
|
+
|
|
524
|
+
return res
|
|
525
|
+
|
|
526
|
+
def query_nvss(self, radius: u.Quantity = 5 * u.arcsec, **kwargs) -> Table:
|
|
527
|
+
"""
|
|
528
|
+
Query the NRAO VLA Sky Survey (NVSS) and return a table list of the
|
|
529
|
+
result
|
|
530
|
+
|
|
531
|
+
This queries Table 1 from Ofek & Frail (2011); 2011ApJ...737...45O
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
radius (u.Quantity) : An astropy Quantity with the radius
|
|
535
|
+
**kwargs : Any other arguments to pass to query_region
|
|
536
|
+
"""
|
|
537
|
+
from astroquery.vizier import Vizier
|
|
538
|
+
|
|
539
|
+
res = DataFinder._wrap_astroquery(
|
|
540
|
+
Vizier, self.coord, radius=radius, catalog="J/ApJ/737/45/table1"
|
|
541
|
+
)
|
|
542
|
+
return res
|
|
543
|
+
|
|
544
|
+
def query_heasarc(self, radius: u.Quantity = 5 * u.arcsec, **kwargs) -> Table:
|
|
545
|
+
"""
|
|
546
|
+
Query Heasarc by the argument "heasarc_key" for the ra/dec associated with this
|
|
547
|
+
DataLoader object.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
radius (u.Quantity) : An astropy Quantity with the radius
|
|
551
|
+
heasarc_table (str) : String with name of heasarc table to query. Default is
|
|
552
|
+
'xray' which queries the heasarc master x-ray catalog,
|
|
553
|
+
'radio' will query the heasarc master radio catalog. See
|
|
554
|
+
https://heasarc.gsfc.nasa.gov/cgi-bin/W3Browse/w3catindex.pl
|
|
555
|
+
for a complete list.
|
|
556
|
+
**kwargs : Any other arguments to pass to query_region
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Astropy table of the rows in `heasarc_table` that match self.coord.
|
|
560
|
+
"""
|
|
561
|
+
from astroquery.heasarc import Heasarc
|
|
562
|
+
|
|
563
|
+
res = DataFinder._wrap_astroquery(Heasarc, self.coord, radius=radius, **kwargs)
|
|
564
|
+
|
|
565
|
+
return res
|
|
566
|
+
|
|
567
|
+
###################################################################################
|
|
568
|
+
######### CONVENIENCE METHODS FOR QUERYING HOST SPECTR ###########################
|
|
569
|
+
###################################################################################
|
|
570
|
+
|
|
571
|
+
def query_sparcl(
|
|
572
|
+
self, radius: u.Quantity = 5 * u.arcsec, include: str | list = "DEFAULT"
|
|
573
|
+
) -> Table:
|
|
574
|
+
"""
|
|
575
|
+
Query the NOIRLab DataLabs Sparcl database for spectra for this host
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
radius (Quantity) : search radius as an Astropy.unit.Quantity
|
|
579
|
+
include [list|str] : list or string of columns to include in the result. See
|
|
580
|
+
the sparcl client documentation for more info. The
|
|
581
|
+
default returns specid, ra, dec, sparcl_id, flux,
|
|
582
|
+
wavelength, and the spectroscopic surveyu (_dr)
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
astropy Table of the results, one row per spectrum
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
from sparcl.client import SparclClient
|
|
589
|
+
from dl import queryClient as qc # noqa: N813
|
|
590
|
+
|
|
591
|
+
client = SparclClient()
|
|
592
|
+
|
|
593
|
+
# first do a cone search on sparcl.main
|
|
594
|
+
ra, dec = self.coord.ra.value, self.coord.dec.value
|
|
595
|
+
radius_deg = radius.to(u.deg).value
|
|
596
|
+
|
|
597
|
+
adql = f"""
|
|
598
|
+
SELECT *
|
|
599
|
+
FROM sparcl.main
|
|
600
|
+
WHERE 't'=Q3C_RADIAL_QUERY(ra,dec,{ra},{dec},{radius_deg})
|
|
601
|
+
"""
|
|
602
|
+
cone_search_res = qc.query(adql=adql, fmt="pandas")
|
|
603
|
+
|
|
604
|
+
# then retrieve all of the spectra corresponding to those sparcl_ids
|
|
605
|
+
spec_ids = cone_search_res.targetid.tolist()
|
|
606
|
+
if len(spec_ids) == 0:
|
|
607
|
+
logger.warn("Object not found in Sparcl!")
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
res = client.retrieve_by_specid(spec_ids, include=include)
|
|
611
|
+
if res.count == 0:
|
|
612
|
+
logger.warn("No Spectra available in sparcl!")
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
all_spec = pd.concat([pd.DataFrame([record]) for record in res.records])
|
|
616
|
+
return Table.from_pandas(all_spec)
|
|
617
|
+
|
|
618
|
+
###################################################################################
|
|
619
|
+
######### PRIVATE HELPER METHODS FOR THE QUERYING #################################
|
|
620
|
+
###################################################################################
|
|
621
|
+
@staticmethod
|
|
622
|
+
def _atlas_stack(filecontent, clipping_sigma, log=logger):
|
|
623
|
+
"""
|
|
624
|
+
Function adapted from David Young's :func:`plotter.plot_single_result`
|
|
625
|
+
https://github.com/thespacedoctor/plot-results-from-atlas-force-photometry-service/blob/main/plot_atlas_fp.py
|
|
626
|
+
|
|
627
|
+
And again adapted from https://github.com/SAGUARO-MMA/kne-cand-vetting/blob/master/kne_cand_vetting/survey_phot.py
|
|
628
|
+
"""
|
|
629
|
+
epochs = DataFinder._atlas_read_and_sigma_clip_data(
|
|
630
|
+
filecontent, log=log, clipping_sigma=clipping_sigma
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# c = cyan, o = arange
|
|
634
|
+
magnitudes = {
|
|
635
|
+
"c": {"mjds": [], "mags": [], "magErrs": [], "lim5sig": []},
|
|
636
|
+
"o": {"mjds": [], "mags": [], "magErrs": [], "lim5sig": []},
|
|
637
|
+
"I": {"mjds": [], "mags": [], "magErrs": [], "lim5sig": []},
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# SPLIT BY FILTER
|
|
641
|
+
for epoch in epochs:
|
|
642
|
+
if epoch["F"] in ["c", "o", "I"]:
|
|
643
|
+
magnitudes[epoch["F"]]["mjds"].append(epoch["MJD"])
|
|
644
|
+
magnitudes[epoch["F"]]["mags"].append(epoch["uJy"])
|
|
645
|
+
magnitudes[epoch["F"]]["magErrs"].append(epoch["duJy"])
|
|
646
|
+
magnitudes[epoch["F"]]["lim5sig"].append(epoch["mag5sig"])
|
|
647
|
+
|
|
648
|
+
# STACK PHOTOMETRY IF REQUIRED
|
|
649
|
+
stacked_magnitudes = DataFinder._stack_photometry(magnitudes, binningdays=1)
|
|
650
|
+
|
|
651
|
+
return stacked_magnitudes
|
|
652
|
+
|
|
653
|
+
@staticmethod
|
|
654
|
+
def _atlas_read_and_sigma_clip_data(filecontent, log, clipping_sigma=2.2):
|
|
655
|
+
"""
|
|
656
|
+
Function adapted from David Young's :func:`plotter.read_and_sigma_clip_data`
|
|
657
|
+
https://github.com/thespacedoctor/plot-results-from-atlas-force-photometry-service/blob/main/plot_atlas_fp.py
|
|
658
|
+
|
|
659
|
+
And again adapted from
|
|
660
|
+
https://github.com/SAGUARO-MMA/kne-cand-vetting/blob/master/kne_cand_vetting/survey_phot.py
|
|
661
|
+
|
|
662
|
+
*clean up rouge data from the files by performing some basic clipping*
|
|
663
|
+
**Key Arguments:**
|
|
664
|
+
- `fpFile` -- path to single force photometry file
|
|
665
|
+
- `clippingSigma` -- the level at which to clip flux data
|
|
666
|
+
**Return:**
|
|
667
|
+
- `epochs` -- sigma clipped and cleaned epoch data
|
|
668
|
+
"""
|
|
669
|
+
|
|
670
|
+
# CLEAN UP FILE FOR EASIER READING
|
|
671
|
+
fpdata = (
|
|
672
|
+
filecontent.replace("###", "")
|
|
673
|
+
.replace(" ", ",")
|
|
674
|
+
.replace(",,", ",")
|
|
675
|
+
.replace(",,", ",")
|
|
676
|
+
.replace(",,", ",")
|
|
677
|
+
.replace(",,", ",")
|
|
678
|
+
.splitlines()
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# PARSE DATA WITH SOME FIXED CLIPPING
|
|
682
|
+
oepochs = []
|
|
683
|
+
cepochs = []
|
|
684
|
+
csvreader = csv.DictReader(
|
|
685
|
+
fpdata, dialect="excel", delimiter=",", quotechar='"'
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
for row in csvreader:
|
|
689
|
+
for k, v in row.items():
|
|
690
|
+
try:
|
|
691
|
+
row[k] = float(v)
|
|
692
|
+
except Exception:
|
|
693
|
+
pass
|
|
694
|
+
# REMOVE VERY HIGH ERROR DATA POINTS, POOR CHI SQUARED, OR POOR EPOCHS
|
|
695
|
+
if row["duJy"] > 4000 or row["chi/N"] > 100 or row["mag5sig"] < 17.0:
|
|
696
|
+
continue
|
|
697
|
+
if row["F"] == "c":
|
|
698
|
+
cepochs.append(row)
|
|
699
|
+
if row["F"] == "o":
|
|
700
|
+
oepochs.append(row)
|
|
701
|
+
|
|
702
|
+
# SORT BY MJD
|
|
703
|
+
cepochs = sorted(cepochs, key=itemgetter("MJD"), reverse=False)
|
|
704
|
+
oepochs = sorted(oepochs, key=itemgetter("MJD"), reverse=False)
|
|
705
|
+
|
|
706
|
+
# SIGMA-CLIP THE DATA WITH A ROLLING WINDOW
|
|
707
|
+
cdataflux = []
|
|
708
|
+
cdataflux[:] = [row["uJy"] for row in cepochs]
|
|
709
|
+
odataflux = []
|
|
710
|
+
odataflux[:] = [row["uJy"] for row in oepochs]
|
|
711
|
+
|
|
712
|
+
masklist = []
|
|
713
|
+
for flux in [cdataflux, odataflux]:
|
|
714
|
+
fullmask = rolling_window_sigma_clip(
|
|
715
|
+
log=log, array=flux, clippingSigma=clipping_sigma, windowSize=11
|
|
716
|
+
)
|
|
717
|
+
masklist.append(fullmask)
|
|
718
|
+
|
|
719
|
+
try:
|
|
720
|
+
cepochs = [e for e, m in zip(cepochs, masklist[0]) if m == False]
|
|
721
|
+
except Exception:
|
|
722
|
+
cepochs = []
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
oepochs = [e for e, m in zip(oepochs, masklist[1]) if m == False]
|
|
726
|
+
except Exception:
|
|
727
|
+
oepochs = []
|
|
728
|
+
|
|
729
|
+
logger.info("Completed the ``read_and_sigma_clip_data`` function")
|
|
730
|
+
# Returns ordered dictionary of all parameters
|
|
731
|
+
return cepochs + oepochs
|
|
732
|
+
|
|
733
|
+
@staticmethod
|
|
734
|
+
def _stack_photometry(magnitudes, binningdays=1.0):
|
|
735
|
+
"""
|
|
736
|
+
Function adapted from David Young's :func:`plotter.stack_photometry`
|
|
737
|
+
https://github.com/thespacedoctor/plot-results-from-atlas-force-photometry-service/blob/main/plot_atlas_fp.py
|
|
738
|
+
|
|
739
|
+
And again adapted from
|
|
740
|
+
https://github.com/SAGUARO-MMA/kne-cand-vetting/blob/master/kne_cand_vetting/survey_phot.py
|
|
741
|
+
|
|
742
|
+
*stack the photometry for the given temporal range*
|
|
743
|
+
**Key Arguments:**
|
|
744
|
+
- `magnitudes` -- dictionary of photometry divided into filter sets
|
|
745
|
+
- `binningDays` -- the binning to use (in days)
|
|
746
|
+
**Return:**
|
|
747
|
+
- `summedMagnitudes` -- the stacked photometry
|
|
748
|
+
"""
|
|
749
|
+
|
|
750
|
+
# IF WE WANT TO 'STACK' THE PHOTOMETRY
|
|
751
|
+
summed_magnitudes = {
|
|
752
|
+
"c": {"mjds": [], "mags": [], "magErrs": [], "n": [], "lim5sig": []},
|
|
753
|
+
"o": {"mjds": [], "mags": [], "magErrs": [], "n": [], "lim5sig": []},
|
|
754
|
+
"I": {"mjds": [], "mags": [], "magErrs": [], "n": [], "lim5sig": []},
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
# MAGNITUDES/FLUXES ARE DIVIDED IN UNIQUE FILTER SETS - SO ITERATE OVER
|
|
758
|
+
# FILTERS
|
|
759
|
+
alldata = []
|
|
760
|
+
for fil, data in list(magnitudes.items()):
|
|
761
|
+
# WE'RE GOING TO CREATE FURTHER SUBSETS FOR EACH UNQIUE MJD
|
|
762
|
+
# (FLOORED TO AN INTEGER)
|
|
763
|
+
# MAG VARIABLE == FLUX (JUST TO CONFUSE YOU)
|
|
764
|
+
distinctmjds = {}
|
|
765
|
+
for mjd, flx, err, lim in zip(
|
|
766
|
+
data["mjds"], data["mags"], data["magErrs"], data["lim5sig"]
|
|
767
|
+
):
|
|
768
|
+
# DICT KEY IS THE UNIQUE INTEGER MJD
|
|
769
|
+
key = str(int(math.floor(mjd / float(binningdays))))
|
|
770
|
+
# FIRST DATA POINT OF THE NIGHTS? CREATE NEW DATA SET
|
|
771
|
+
if key not in distinctmjds:
|
|
772
|
+
distinctmjds[key] = {
|
|
773
|
+
"mjds": [mjd],
|
|
774
|
+
"mags": [flx],
|
|
775
|
+
"magErrs": [err],
|
|
776
|
+
"lim5sig": [lim],
|
|
777
|
+
}
|
|
778
|
+
# OR NOT THE FIRST? APPEND TO ALREADY CREATED LIST
|
|
779
|
+
else:
|
|
780
|
+
distinctmjds[key]["mjds"].append(mjd)
|
|
781
|
+
distinctmjds[key]["mags"].append(flx)
|
|
782
|
+
distinctmjds[key]["magErrs"].append(err)
|
|
783
|
+
distinctmjds[key]["lim5sig"].append(lim)
|
|
784
|
+
|
|
785
|
+
# ALL DATA NOW IN MJD SUBSETS. SO FOR EACH SUBSET (I.E. INDIVIDUAL
|
|
786
|
+
# NIGHTS) ...
|
|
787
|
+
for k, v in list(distinctmjds.items()):
|
|
788
|
+
# GIVE ME THE MEAN MJD
|
|
789
|
+
meanmjd = sum(v["mjds"]) / len(v["mjds"])
|
|
790
|
+
summed_magnitudes[fil]["mjds"].append(meanmjd)
|
|
791
|
+
# GIVE ME THE MEAN FLUX
|
|
792
|
+
meanflux = sum(v["mags"]) / len(v["mags"])
|
|
793
|
+
summed_magnitudes[fil]["mags"].append(meanflux)
|
|
794
|
+
# GIVE ME THE COMBINED ERROR
|
|
795
|
+
sum_of_squares = sum(x**2 for x in v["magErrs"])
|
|
796
|
+
comberror = math.sqrt(sum_of_squares) / len(v["magErrs"])
|
|
797
|
+
summed_magnitudes[fil]["magErrs"].append(comberror)
|
|
798
|
+
# 5-sigma limits
|
|
799
|
+
comb5siglimit = 23.9 - 2.5 * math.log10(5.0 * comberror)
|
|
800
|
+
summed_magnitudes[fil]["lim5sig"].append(comb5siglimit)
|
|
801
|
+
# GIVE ME NUMBER OF DATA POINTS COMBINED
|
|
802
|
+
n = len(v["mjds"])
|
|
803
|
+
summed_magnitudes[fil]["n"].append(n)
|
|
804
|
+
alldata.append(
|
|
805
|
+
{
|
|
806
|
+
"mjd": meanmjd,
|
|
807
|
+
"uJy": meanflux,
|
|
808
|
+
"duJy": comberror,
|
|
809
|
+
"F": fil,
|
|
810
|
+
"n": n,
|
|
811
|
+
"mag5sig": comb5siglimit,
|
|
812
|
+
}
|
|
813
|
+
)
|
|
814
|
+
print("completed the ``stack_photometry`` method")
|
|
815
|
+
|
|
816
|
+
return alldata
|
|
817
|
+
|
|
818
|
+
"""
|
|
819
|
+
The following code was taken and modified for the purposes of this package from
|
|
820
|
+
https://github.com/HC-Hwang/wise_light_curves/blob/master/wise_light_curves.py
|
|
821
|
+
|
|
822
|
+
Original Authors:
|
|
823
|
+
- Matthew Hill
|
|
824
|
+
- Hsiang-Chih Hwang
|
|
825
|
+
|
|
826
|
+
Update Author:
|
|
827
|
+
- Noah Franz
|
|
828
|
+
"""
|
|
829
|
+
|
|
830
|
+
@staticmethod
|
|
831
|
+
def _get_by_position(ra, dec, radius=2.5):
|
|
832
|
+
allwise_cat = "allwise_p3as_mep"
|
|
833
|
+
neowise_cat = "neowiser_p1bs_psd"
|
|
834
|
+
query_url = "http://irsa.ipac.caltech.edu/cgi-bin/Gator/nph-query"
|
|
835
|
+
payload = {
|
|
836
|
+
"catalog": allwise_cat,
|
|
837
|
+
"spatial": "cone",
|
|
838
|
+
"objstr": " ".join([str(ra), str(dec)]),
|
|
839
|
+
"radius": str(radius),
|
|
840
|
+
"radunits": "arcsec",
|
|
841
|
+
"outfmt": "1",
|
|
842
|
+
}
|
|
843
|
+
r = requests.get(query_url, params=payload)
|
|
844
|
+
allwise = ascii.read(r.text)
|
|
845
|
+
payload = {
|
|
846
|
+
"catalog": neowise_cat,
|
|
847
|
+
"spatial": "cone",
|
|
848
|
+
"objstr": " ".join([str(ra), str(dec)]),
|
|
849
|
+
"radius": str(radius),
|
|
850
|
+
"radunits": "arcsec",
|
|
851
|
+
"outfmt": "1",
|
|
852
|
+
"selcols": "ra,dec,sigra,sigdec,sigradec,glon,glat,elon,elat,w1mpro,w1sigmpro,w1snr,w1rchi2,w2mpro,w2sigmpro,w2snr,w2rchi2,rchi2,nb,na,w1sat,w2sat,satnum,cc_flags,det_bit,ph_qual,sso_flg,qual_frame,qi_fact,saa_sep,moon_masked,w1frtr,w2frtr,mjd,allwise_cntr,r_allwise,pa_allwise,n_allwise,w1mpro_allwise,w1sigmpro_allwise,w2mpro_allwise,w2sigmpro_allwise,w3mpro_allwise,w3sigmpro_allwise,w4mpro_allwise,w4sigmpro_allwise", # noqa: E501
|
|
853
|
+
}
|
|
854
|
+
r = requests.get(query_url, params=payload)
|
|
855
|
+
|
|
856
|
+
neowise = ascii.read(r.text, guess=False, format="ipac")
|
|
857
|
+
|
|
858
|
+
return allwise, neowise
|
|
859
|
+
|
|
860
|
+
@staticmethod
|
|
861
|
+
def _download_single_data(
|
|
862
|
+
name, ra, dec, root_path="ipac/", radius=2.5, overwrite=False
|
|
863
|
+
):
|
|
864
|
+
# ra, dec: in degree
|
|
865
|
+
# name, ra, dec = row['Name'], row['RAJ2000'], row['DEJ2000']
|
|
866
|
+
# name = 'J' + ra + dec
|
|
867
|
+
if root_path[-1] != "/":
|
|
868
|
+
root_path += "/"
|
|
869
|
+
if (
|
|
870
|
+
not overwrite
|
|
871
|
+
and os.path.isfile(root_path + name + "_allwise.ipac")
|
|
872
|
+
and os.path.isfile(root_path + name + "_neowise.ipac")
|
|
873
|
+
):
|
|
874
|
+
pass
|
|
875
|
+
else:
|
|
876
|
+
allwise, neowise = DataFinder._get_by_position(ra, dec, radius=radius)
|
|
877
|
+
allwise.write(
|
|
878
|
+
root_path + name + "_allwise.ipac", format="ascii.ipac", overwrite=True
|
|
879
|
+
)
|
|
880
|
+
neowise.write(
|
|
881
|
+
root_path + name + "_neowise.ipac", format="ascii.ipac", overwrite=True
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
@staticmethod
|
|
885
|
+
def _get_data_arrays(table, t, mag, magerr):
|
|
886
|
+
"""Get the time series from a potentially masked astropy table"""
|
|
887
|
+
if table.masked:
|
|
888
|
+
full_mask = table[t].mask | table[mag].mask | table[magerr].mask
|
|
889
|
+
t = table[t].data
|
|
890
|
+
mag = table[mag].data
|
|
891
|
+
magerr = table[magerr].data
|
|
892
|
+
|
|
893
|
+
t.mask = full_mask
|
|
894
|
+
mag.mask = full_mask
|
|
895
|
+
magerr.mask = full_mask
|
|
896
|
+
|
|
897
|
+
return t.compressed(), mag.compressed(), magerr.compressed()
|
|
898
|
+
|
|
899
|
+
else:
|
|
900
|
+
return table[t].data, table[mag].data, table[magerr].data
|
|
901
|
+
|
|
902
|
+
@staticmethod
|
|
903
|
+
def _make_full_lightcurve(allwise, neowise, band):
|
|
904
|
+
"""band = 'w1', 'w2', 'w3', or 'w4'"""
|
|
905
|
+
"""Get a combined AllWISE and NEOWISE lightcurve from their Astropy tables"""
|
|
906
|
+
|
|
907
|
+
if band not in ["w1", "w2", "w3", "w4"]:
|
|
908
|
+
raise ValueError("band can only be w1, w2, w3, or w4")
|
|
909
|
+
|
|
910
|
+
use_neowise = band in {"w1", "w2"}
|
|
911
|
+
use_allwise = allwise is not None
|
|
912
|
+
|
|
913
|
+
if use_neowise and use_allwise:
|
|
914
|
+
t, m, e = DataFinder._get_data_arrays(
|
|
915
|
+
allwise, "mjd", band + "mpro_ep", band + "sigmpro_ep"
|
|
916
|
+
)
|
|
917
|
+
t_n, m_n, e_n = DataFinder._get_data_arrays(
|
|
918
|
+
neowise, "mjd", band + "mpro", band + "sigmpro"
|
|
919
|
+
)
|
|
920
|
+
t, m, e = (
|
|
921
|
+
np.concatenate((t, t_n)),
|
|
922
|
+
np.concatenate((m, m_n)),
|
|
923
|
+
np.concatenate((e, e_n)),
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
elif use_neowise and not use_allwise:
|
|
927
|
+
t, m, e = DataFinder._get_data_arrays(
|
|
928
|
+
neowise, "mjd", band + "mpro", band + "sigmpro"
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
elif not use_neowise and use_allwise:
|
|
932
|
+
t, m, e = DataFinder._get_data_arrays(
|
|
933
|
+
allwise, "mjd", band + "mpro_ep", band + "sigmpro_ep"
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
else:
|
|
937
|
+
raise Exception("No good allwise or neowise data!")
|
|
938
|
+
|
|
939
|
+
t_index = t.argsort()
|
|
940
|
+
t, m, e = map(lambda e: e[t_index], [t, m, e])
|
|
941
|
+
|
|
942
|
+
return t, m, e
|
|
943
|
+
|
|
944
|
+
@staticmethod
|
|
945
|
+
def _make_full_lightcurve_multibands(allwise, neowise, bands=["w1", "w2"]):
|
|
946
|
+
t, m, e = DataFinder._make_full_lightcurve(allwise, neowise, bands[0])
|
|
947
|
+
filts = [bands[0] for i in range(len(t))]
|
|
948
|
+
for band in bands[1:]:
|
|
949
|
+
try:
|
|
950
|
+
t_tmp, m_tmp, e_tmp = DataFinder._make_full_lightcurve(
|
|
951
|
+
allwise, neowise, band
|
|
952
|
+
)
|
|
953
|
+
except Exception:
|
|
954
|
+
continue
|
|
955
|
+
t = np.concatenate((t, t_tmp))
|
|
956
|
+
m = np.concatenate((m, m_tmp))
|
|
957
|
+
e = np.concatenate((e, e_tmp))
|
|
958
|
+
filts += [band for i in range(len(t_tmp))]
|
|
959
|
+
return t, m, e, np.array(filts)
|
|
960
|
+
|
|
961
|
+
@staticmethod
|
|
962
|
+
def _cntr_to_source_id(cntr):
|
|
963
|
+
cntr = str(cntr)
|
|
964
|
+
|
|
965
|
+
# fill leanding 0s
|
|
966
|
+
if len(cntr) < 19:
|
|
967
|
+
num_leading_zeros = 19 - len(cntr)
|
|
968
|
+
cntr = "0" * num_leading_zeros + cntr
|
|
969
|
+
|
|
970
|
+
pm = "p"
|
|
971
|
+
if cntr[4] == "0":
|
|
972
|
+
pm = "m"
|
|
973
|
+
|
|
974
|
+
t = chr(96 + int(cntr[8:10]))
|
|
975
|
+
|
|
976
|
+
return "%s%s%s_%cc%s-%s" % (
|
|
977
|
+
cntr[0:4],
|
|
978
|
+
pm,
|
|
979
|
+
cntr[5:8],
|
|
980
|
+
t,
|
|
981
|
+
cntr[11:13],
|
|
982
|
+
cntr[13:19],
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
@staticmethod
|
|
986
|
+
def _only_good_data(allwise, neowise, verbose=False):
|
|
987
|
+
"""
|
|
988
|
+
Select good-quality data. The criteria include:
|
|
989
|
+
- matching the all-wise ID
|
|
990
|
+
|
|
991
|
+
To be done:
|
|
992
|
+
- deal with multiple cntr
|
|
993
|
+
|
|
994
|
+
This filtering is described here:
|
|
995
|
+
https://wise2.ipac.caltech.edu/docs/release/neowise/expsup/sec2_3.html
|
|
996
|
+
"""
|
|
997
|
+
|
|
998
|
+
neowise_prefilter_n = len(neowise)
|
|
999
|
+
neowise = neowise[
|
|
1000
|
+
(neowise["qual_frame"] > 0.0)
|
|
1001
|
+
* (neowise["qi_fact"] > 0.9)
|
|
1002
|
+
* (neowise["saa_sep"] > 0)
|
|
1003
|
+
* (neowise["moon_masked"] == "00")
|
|
1004
|
+
]
|
|
1005
|
+
neowise_postfilter_n = len(neowise)
|
|
1006
|
+
if verbose:
|
|
1007
|
+
print(
|
|
1008
|
+
f"Filtered out {neowise_prefilter_n-neowise_postfilter_n} neowise \
|
|
1009
|
+
points, leaving {neowise_postfilter_n}"
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
cntr_list = []
|
|
1013
|
+
for data in neowise:
|
|
1014
|
+
if data["allwise_cntr"] not in cntr_list and data["allwise_cntr"] > 10.0:
|
|
1015
|
+
cntr_list.append(data["allwise_cntr"])
|
|
1016
|
+
|
|
1017
|
+
if len(cntr_list) >= 2:
|
|
1018
|
+
print("multiple cntr:")
|
|
1019
|
+
print(cntr_list)
|
|
1020
|
+
return None, neowise
|
|
1021
|
+
|
|
1022
|
+
if len(cntr_list) == 0:
|
|
1023
|
+
# import pdb; pdb.set_trace()
|
|
1024
|
+
# raise Exception('No center!')
|
|
1025
|
+
return None, neowise
|
|
1026
|
+
|
|
1027
|
+
cntr = cntr_list[0]
|
|
1028
|
+
|
|
1029
|
+
source_id = DataFinder._cntr_to_source_id(cntr)
|
|
1030
|
+
|
|
1031
|
+
allwise_prefilter_n = len(allwise)
|
|
1032
|
+
allwise = allwise[
|
|
1033
|
+
(allwise["source_id_mf"] == source_id)
|
|
1034
|
+
* (allwise["saa_sep"] > 0.0)
|
|
1035
|
+
* (allwise["moon_masked"] == "0000")
|
|
1036
|
+
* (allwise["qi_fact"] > 0.9)
|
|
1037
|
+
]
|
|
1038
|
+
allwise_postfilter_n = len(neowise)
|
|
1039
|
+
if verbose:
|
|
1040
|
+
print(
|
|
1041
|
+
f"Filtered out {allwise_prefilter_n-allwise_postfilter_n} allwise \
|
|
1042
|
+
points, leaving {allwise_postfilter_n}"
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
return allwise, neowise
|