tglc 0.6.5__py3-none-any.whl → 0.7.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.
@@ -0,0 +1,48 @@
1
+ # TESS Ephemeris Files
2
+
3
+ This directory contains files with TESS ephemeris data used for barycentric time correction by QLP. The files are CSV files with X,Y,Z coordinates in AU from the solar system barycenter and the reference frame is ICRF.
4
+
5
+ Each file has a set of sectors it is meant to be used for:
6
+
7
+ - Sectors 1-5: [20180720_tess_ephem.csv](20180720_tess_ephem.csv)
8
+ - Sectors 6-19: [20190101_tess_ephem.csv](20190101_tess_ephem.csv)
9
+ - Sectors 20-32: [20200101_tess_ephem.csv](20200101_tess_ephem.csv)
10
+ - Sectors 33-45: [20210101_tess_ephem.csv](20210101_tess_ephem.csv)
11
+ - Sectors 46-59: [20211215_tess_ephem.csv](20211215_tess_ephem.csv)
12
+ - Sectors 60-73: [20221201_tess_ephem.csv](20221201_tess_ephem.csv)
13
+ - Sectors 74-87: [20231201_tess_ephem.csv](20231201_tess_ephem.csv)
14
+ - Sectors 88-101: [20241201_tess_ephem.csv](20241201_tess_ephem.csv)
15
+
16
+ ## Downloading the files
17
+
18
+ At the start of every year, we need to add a TESS predicted ephemeris file so we can correct spacecraft time to BJD TDB.These ephemerides are downloaded from the horizons website: <https://ssd.jpl.nasa.gov/horizons.cgi>
19
+
20
+ **Ephemeris Type:** Vector Table
21
+
22
+ **Target Body:** TESS
23
+
24
+ **Coordinate Origin:** Solar System Barycenter (SSB)
25
+
26
+ **Time Span:** The required time space for the year. We want the step size to be at least one hour
27
+
28
+ **Table Settings:**
29
+
30
+ - Quantities code = 4 (position, LT, range, and range-rate)
31
+ - Reference frame = ICRF
32
+ - Reference plane = x-y axes of reference frame
33
+ - Vector correction = geometric states
34
+ - Output units = AU and days
35
+ - CSV format = YES
36
+ - Object summary = up to you. The summary will need to be commented out anyways, so I prefer to not include it.
37
+
38
+ Make sure you comment out the header and footer information in the csv file. Also, when last generated (Jan 15 2023) I needed to add the following line before the values:
39
+
40
+ ```
41
+ JDTDB,Calendar Date (TDB),X,Y,Z,LT,RG,RR,
42
+ ```
43
+
44
+ (see the format of the other .csv files in the directory for guidance)
45
+
46
+ After adding the file to this directory, plot all the ephemerides by running [plot_ephemerides.py](plot_ephemerides.py). This ensures the files can all be read successfully. Examine the plot it produces and check that the transition between years is smooth, and that the new file continues the existing sine wave patterns.
47
+
48
+ If you have a function that specifies where the ephemeris file is for each sector (like in the [notebook in this repository](/TESS%20Time%20Correction.ipynb)), make sure to update that as well.
@@ -0,0 +1,54 @@
1
+ """
2
+ Plot X,Y,Z coordinates from JPL horizons TESS ephemeris files to ensure the
3
+ transitions are smooth.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import matplotlib.pyplot as plt
10
+ import pandas as pd
11
+
12
+
13
+ def plot_ephemerides(
14
+ ephemeris_directory: Optional[Path] = None, output_file: Optional[Path] = None
15
+ ) -> None:
16
+ """
17
+ Plot ephemerides from all files in given directory.
18
+
19
+ Parameters
20
+ ----------
21
+ ephemeris_directory : Path, optional
22
+ Directory containing files with ephemeris data to plot
23
+ output_file : Path, optional
24
+ File to save plot to. If omitted, calls plt.show()
25
+ """
26
+ if ephemeris_directory is None:
27
+ ephemeris_directory = Path(__file__).parent
28
+ ephemeris_files = sorted(list(ephemeris_directory.iterdir()))
29
+ ephemerides = [
30
+ pd.read_csv(ephemeris_file, comment="#")
31
+ for ephemeris_file in ephemeris_files
32
+ if ephemeris_file.suffix == ".csv"
33
+ ]
34
+
35
+ fig, (ax_x, ax_y, ax_z) = plt.subplots(3, 1, figsize=(9, 6), layout="constrained")
36
+
37
+ for ephemeris, path in zip(ephemerides, ephemeris_files):
38
+ ax_x.plot(ephemeris["JDTDB"], ephemeris["X"], label=path.stem, alpha=0.8)
39
+ ax_y.plot(ephemeris["JDTDB"], ephemeris["Y"], label=path.stem, alpha=0.8)
40
+ ax_z.plot(ephemeris["JDTDB"], ephemeris["Z"], label=path.stem, alpha=0.8)
41
+
42
+ ax_x.set(xlabel="JD TDB", ylabel="TESS X coordinate [AU]")
43
+ ax_y.set(xlabel="JD TDB", ylabel="TESS Y coordinate [AU]")
44
+ ax_z.set(xlabel="JD TDB", ylabel="TESS Z coordinate [AU]")
45
+ fig.suptitle("JPL TESS Ephemerides for QLP")
46
+
47
+ if output_file is None:
48
+ plt.show()
49
+ else:
50
+ fig.savefig(output_file)
51
+
52
+
53
+ if __name__ == "__main__":
54
+ plot_ephemerides(Path(__file__).parent)
tglc/ffi.py CHANGED
@@ -4,7 +4,7 @@ import pickle
4
4
  import sys
5
5
  import astropy.units as u
6
6
  import numpy as np
7
- import pkg_resources
7
+ import importlib_resources
8
8
  import requests
9
9
  import time
10
10
 
@@ -86,7 +86,7 @@ def convert_gaia_id(catalogdata_tic):
86
86
  FROM gaiadr3.dr2_neighbourhood
87
87
  WHERE dr2_source_id IN {gaia_ids}
88
88
  """
89
- gaia_array = np.array(catalogdata_tic['GAIA'])
89
+ gaia_array = np.array([str(item) for item in catalogdata_tic['GAIA']], dtype=object)
90
90
  gaia_array = gaia_array[gaia_array != 'None']
91
91
  # np.save('gaia_array.npy', gaia_array)
92
92
  segment = (len(gaia_array) - 1) // 10000
@@ -327,7 +327,7 @@ def ffi(ccd=1, camera=1, sector=1, size=150, local_directory='', producing_mask=
327
327
  np.save(f'{local_directory}mask/mask_sector{sector:04d}_cam{camera}_ccd{ccd}.npy', mask)
328
328
  return
329
329
  # load mask
330
- mask = pkg_resources.resource_stream(__name__, f'background_mask/median_mask.fits')
330
+ mask = importlib_resources.files(__package__).joinpath("background_mask/median_mask.fits").open("rb")
331
331
  mask = fits.open(mask)[0].data[(camera - 1) * 4 + (ccd - 1), :]
332
332
  mask = np.repeat(mask.reshape(1, 2048), repeats=2048, axis=0)
333
333
  bad_pixels = np.zeros(np.shape(flux[0]))
tglc/ffi_cut.py CHANGED
@@ -4,6 +4,10 @@ import sys
4
4
  import pickle
5
5
  import numpy as np
6
6
  import astropy.units as u
7
+ import requests
8
+ import time
9
+ import contextlib
10
+ import threading
7
11
 
8
12
  from os.path import exists
9
13
  from astroquery.gaia import Gaia
@@ -22,8 +26,34 @@ Gaia.ROW_LIMIT = -1
22
26
  Gaia.MAIN_GAIA_TABLE = "gaiadr3.gaia_source"
23
27
 
24
28
 
29
+ @contextlib.contextmanager
30
+ def _dot_wait(message, interval=0.6, done_format=None):
31
+ stop_event = threading.Event()
32
+ start = time.time()
33
+
34
+ def _run():
35
+ dots = 0
36
+ while not stop_event.is_set():
37
+ dots = (dots % 3) + 1
38
+ print(f"\r{message} {'.' * dots}", end="", flush=True)
39
+ time.sleep(interval)
40
+
41
+ thread = threading.Thread(target=_run, daemon=True)
42
+ thread.start()
43
+ try:
44
+ yield
45
+ finally:
46
+ stop_event.set()
47
+ thread.join()
48
+ if done_format is None:
49
+ done_format = "{message} ... done in {elapsed:.1f}s."
50
+ done_line = done_format.format(message=message, elapsed=time.time() - start)
51
+ print(f"\r{done_line} ")
52
+
53
+
25
54
  class Source_cut(object):
26
- def __init__(self, name, size=50, sector=None, cadence=None, limit_mag=None, transient=None):
55
+ def __init__(self, name, size=50, sector=None, cadence=None, limit_mag=None, transient=None, ffi='TICA',
56
+ mast_timeout=3600):
27
57
  """
28
58
  Source_cut object that includes all data from TESS and Gaia DR3
29
59
  :param name: str, required
@@ -51,35 +81,100 @@ class Source_cut(object):
51
81
  self.quality = []
52
82
  self.mask = []
53
83
  self.transient = transient
84
+ self.ffi = ffi
85
+ Tesscut._service_api_connection.TIMEOUT = mast_timeout
86
+ print(f'MAST Tesscut timeout set to {mast_timeout}s.')
87
+
88
+ def _parse_tic_id(t):
89
+ if not isinstance(t, str):
90
+ return None
91
+ s = t.strip()
92
+ if s.upper().startswith('TIC'):
93
+ parts = s.split()
94
+ if len(parts) > 1 and parts[1].isdigit():
95
+ return int(parts[1])
96
+ s = s[3:].strip()
97
+ return int(s) if s.isdigit() else None
98
+
99
+ def _is_tic_id(t):
100
+ if not isinstance(t, str):
101
+ return False
102
+ s = t.strip()
103
+ return s.upper().startswith('TIC') or s.isdigit()
104
+
105
+ target = None
106
+ is_tic = _is_tic_id(self.name) and _parse_tic_id(self.name) is not None
107
+ try:
108
+ target = Catalogs.query_object(self.name, radius=21 * 0.707 / 3600, catalog="Gaia", version=2)
109
+ except requests.exceptions.RequestException as e:
110
+ warnings.warn(f'MAST name lookup failed for "{self.name}": {e}')
111
+
112
+ if target is None or len(target) == 0:
113
+ try:
114
+ target = Catalogs.query_object(self.name, radius=5 * 21 * 0.707 / 3600, catalog="Gaia", version=2)
115
+ except requests.exceptions.RequestException as e:
116
+ warnings.warn(f'MAST name lookup failed for "{self.name}": {e}')
117
+
118
+ if target is None or len(target) == 0:
119
+ if is_tic:
120
+ raise RuntimeError(
121
+ f'MAST name lookup failed for TIC target "{self.name}". Please retry when MAST is available.'
122
+ )
123
+ raise RuntimeError(
124
+ f'Unable to resolve target "{self.name}". MAST name lookup appears unavailable.'
125
+ )
54
126
 
55
- target = Catalogs.query_object(self.name, radius=21 * 0.707 / 3600, catalog="Gaia", version=2)
56
- if len(target) == 0:
57
- target = Catalogs.query_object(self.name, radius=5 * 21 * 0.707 / 3600, catalog="Gaia", version=2)
58
127
  ra = target[0]['ra']
59
128
  dec = target[0]['dec']
129
+ target_designation = target[0].get('designation', None)
60
130
  coord = SkyCoord(ra=ra, dec=dec, unit=(u.degree, u.degree), frame='icrs')
61
131
  radius = u.Quantity((self.size + 6) * 21 * 0.707 / 3600, u.deg)
62
- print(f'Target Gaia: {target[0]["designation"]}')
63
- catalogdata = Gaia.cone_search_async(coord, radius=radius,
64
- columns=['DESIGNATION', 'phot_g_mean_mag', 'phot_bp_mean_mag',
65
- 'phot_rp_mean_mag', 'ra', 'dec', 'pmra', 'pmdec']).get_results()
132
+ if target_designation:
133
+ print(f'Target Gaia: {target_designation}')
134
+ with _dot_wait('Querying Gaia DR3 cone search'):
135
+ catalogdata = Gaia.cone_search_async(
136
+ coord,
137
+ radius=radius,
138
+ columns=['DESIGNATION', 'phot_g_mean_mag', 'phot_bp_mean_mag',
139
+ 'phot_rp_mean_mag', 'ra', 'dec', 'pmra', 'pmdec']
140
+ ).get_results()
66
141
  print(f'Found {len(catalogdata)} Gaia DR3 objects.')
67
- catalogdata_tic = tic_advanced_search_position_rows(ra=ra, dec=dec, radius=(self.size + 2) * 21 * 0.707 / 3600, limit_mag=limit_mag)
142
+ with _dot_wait('Querying TIC around target'):
143
+ catalogdata_tic = tic_advanced_search_position_rows(
144
+ ra=ra,
145
+ dec=dec,
146
+ radius=(self.size + 2) * 21 * 0.707 / 3600,
147
+ limit_mag=limit_mag
148
+ )
68
149
  print(f'Found {len(catalogdata_tic)} TIC objects.')
69
- self.tic = convert_gaia_id(catalogdata_tic)
150
+ with _dot_wait('Crossmatching TIC -> Gaia DR3 (this may take a while)'):
151
+ self.tic = convert_gaia_id(catalogdata_tic)
70
152
  sector_table = Tesscut.get_sectors(coordinates=coord)
71
153
  if len(sector_table) == 0:
72
154
  warnings.warn('TESS has not observed this position yet :(')
155
+ wait_note = 'later sectors with 200s cadence can take ~20 minutes'
73
156
  if sector is None:
74
- hdulist = Tesscut.get_cutouts(coordinates=coord, size=self.size)
157
+ wait_message = f'Requesting Tesscut cutouts (all sectors). Waiting on MAST response ({wait_note})'
158
+ with _dot_wait(wait_message):
159
+ hdulist = Tesscut.get_cutouts(coordinates=coord, size=self.size, product=ffi)
75
160
  elif sector == 'first':
76
- hdulist = Tesscut.get_cutouts(coordinates=coord, size=self.size, sector=sector_table['sector'][0])
161
+ wait_message = f'Requesting Tesscut cutouts (first sector). Waiting on MAST response ({wait_note})'
162
+ with _dot_wait(wait_message):
163
+ hdulist = Tesscut.get_cutouts(
164
+ coordinates=coord, size=self.size, product=ffi, sector=sector_table['sector'][0]
165
+ )
77
166
  sector = sector_table['sector'][0]
78
167
  elif sector == 'last':
79
- hdulist = Tesscut.get_cutouts(coordinates=coord, size=self.size, sector=sector_table['sector'][-1])
168
+ wait_message = f'Requesting Tesscut cutouts (last sector). Waiting on MAST response ({wait_note})'
169
+ with _dot_wait(wait_message):
170
+ hdulist = Tesscut.get_cutouts(
171
+ coordinates=coord, size=self.size, product=ffi, sector=sector_table['sector'][-1]
172
+ )
80
173
  sector = sector_table['sector'][-1]
81
174
  else:
82
- hdulist = Tesscut.get_cutouts(coordinates=coord, size=self.size, sector=sector)
175
+ wait_message = f'Requesting Tesscut cutouts (sector {sector}). Waiting on MAST response ({wait_note})'
176
+ with _dot_wait(wait_message):
177
+ hdulist = Tesscut.get_cutouts(coordinates=coord, size=self.size, product=ffi, sector=sector)
83
178
  self.catalogdata = catalogdata
84
179
  self.sector_table = sector_table
85
180
  self.camera = int(sector_table[0]['camera'])
@@ -114,8 +209,14 @@ class Source_cut(object):
114
209
  self.ccd = int(hdu[0].header['CCD'])
115
210
  wcs = WCS(hdu[2].header)
116
211
  data_time = hdu[1].data['TIME']
117
- data_flux = hdu[1].data['FLUX']
118
- data_flux_err = hdu[1].data['FLUX_ERR']
212
+ if self.ffi == 'SPOC':
213
+ data_flux = hdu[1].data['FLUX']
214
+ data_flux_err = hdu[1].data['FLUX_ERR']
215
+ elif self.ffi == 'TICA':
216
+ data_flux = hdu[1].data['FLUX'] / (200 * 0.8 * 0.99)
217
+ data_flux_err = hdu[1].data['FLUX_ERR'] / (200 * 0.8 * 0.99)
218
+ else:
219
+ raise Exception(f'FFI type {self.ffi} not supported')
119
220
  data_quality = hdu[1].data['QUALITY']
120
221
  # data_time = data_time[np.where(data_quality == 0)]
121
222
  # data_flux = data_flux[np.where(data_quality == 0), :, :][0]
@@ -245,7 +346,8 @@ class Source_cut_pseudo(object):
245
346
  self.gaia = gaia_targets
246
347
 
247
348
 
248
- def ffi_cut(target='', local_directory='', size=90, sector=None, limit_mag=None, transient=None):
349
+ def ffi_cut(target='', local_directory='', size=90, sector=None, limit_mag=None, transient=None, ffi='TICA',
350
+ mast_timeout=3600):
249
351
  """
250
352
  Function to generate Source_cut objects
251
353
  :param target: string, required
@@ -259,13 +361,13 @@ def ffi_cut(target='', local_directory='', size=90, sector=None, limit_mag=None,
259
361
  :return: tglc.ffi_cut.Source_cut
260
362
  """
261
363
  if sector is None:
262
- source_name = f'source_{target}'
364
+ source_name = f'source_{ffi}_{target}'
263
365
  elif sector == 'first':
264
- source_name = f'source_{target}_earliest_sector'
366
+ source_name = f'source_{ffi}_{target}_earliest_sector'
265
367
  elif sector == 'last':
266
- source_name = f'source_{target}_last_sector'
368
+ source_name = f'source_{ffi}_{target}_last_sector'
267
369
  else:
268
- source_name = f'source_{target}_sector_{sector}'
370
+ source_name = f'source_{ffi}_{target}_sector_{sector}'
269
371
  source_exists = exists(f'{local_directory}source/{source_name}.pkl')
270
372
  if source_exists and os.path.getsize(f'{local_directory}source/{source_name}.pkl') > 0:
271
373
  with open(f'{local_directory}source/{source_name}.pkl', 'rb') as input_:
@@ -274,6 +376,7 @@ def ffi_cut(target='', local_directory='', size=90, sector=None, limit_mag=None,
274
376
  print('Loaded ffi_cut from directory. ')
275
377
  else:
276
378
  with open(f'{local_directory}source/{source_name}.pkl', 'wb') as output:
277
- source = Source_cut(target, size=size, sector=sector, limit_mag=limit_mag, transient=transient)
379
+ source = Source_cut(target, size=size, sector=sector, limit_mag=limit_mag, transient=transient, ffi=ffi,
380
+ mast_timeout=mast_timeout)
278
381
  pickle.dump(source, output, pickle.HIGHEST_PROTOCOL)
279
382
  return source