pyreduce-astro 0.6.0b5__cp313-cp313-macosx_11_0_arm64.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.
- pyreduce/__init__.py +67 -0
- pyreduce/__main__.py +106 -0
- pyreduce/clib/__init__.py +0 -0
- pyreduce/clib/_slitfunc_2d.cpython-311-darwin.so +0 -0
- pyreduce/clib/_slitfunc_2d.cpython-312-darwin.so +0 -0
- pyreduce/clib/_slitfunc_2d.cpython-313-darwin.so +0 -0
- pyreduce/clib/_slitfunc_bd.cpython-311-darwin.so +0 -0
- pyreduce/clib/_slitfunc_bd.cpython-312-darwin.so +0 -0
- pyreduce/clib/_slitfunc_bd.cpython-313-darwin.so +0 -0
- pyreduce/clib/build_extract.py +75 -0
- pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
- pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
- pyreduce/clib/slit_func_bd.c +362 -0
- pyreduce/clib/slit_func_bd.h +17 -0
- pyreduce/clipnflip.py +147 -0
- pyreduce/combine_frames.py +855 -0
- pyreduce/configuration.py +186 -0
- pyreduce/continuum_normalization.py +329 -0
- pyreduce/cwrappers.py +404 -0
- pyreduce/datasets.py +231 -0
- pyreduce/echelle.py +413 -0
- pyreduce/estimate_background_scatter.py +129 -0
- pyreduce/extract.py +1361 -0
- pyreduce/extraction_width.py +77 -0
- pyreduce/instruments/__init__.py +0 -0
- pyreduce/instruments/andes.json +61 -0
- pyreduce/instruments/andes.py +102 -0
- pyreduce/instruments/common.json +46 -0
- pyreduce/instruments/common.py +675 -0
- pyreduce/instruments/crires_plus.json +63 -0
- pyreduce/instruments/crires_plus.py +103 -0
- pyreduce/instruments/filters.py +195 -0
- pyreduce/instruments/harpn.json +136 -0
- pyreduce/instruments/harpn.py +201 -0
- pyreduce/instruments/harps.json +155 -0
- pyreduce/instruments/harps.py +310 -0
- pyreduce/instruments/instrument_info.py +140 -0
- pyreduce/instruments/instrument_schema.json +221 -0
- pyreduce/instruments/jwst_miri.json +53 -0
- pyreduce/instruments/jwst_miri.py +29 -0
- pyreduce/instruments/jwst_niriss.json +52 -0
- pyreduce/instruments/jwst_niriss.py +98 -0
- pyreduce/instruments/lick_apf.json +53 -0
- pyreduce/instruments/lick_apf.py +35 -0
- pyreduce/instruments/mcdonald.json +59 -0
- pyreduce/instruments/mcdonald.py +123 -0
- pyreduce/instruments/metis_ifu.json +63 -0
- pyreduce/instruments/metis_ifu.py +45 -0
- pyreduce/instruments/metis_lss.json +65 -0
- pyreduce/instruments/metis_lss.py +45 -0
- pyreduce/instruments/micado.json +53 -0
- pyreduce/instruments/micado.py +45 -0
- pyreduce/instruments/neid.json +51 -0
- pyreduce/instruments/neid.py +154 -0
- pyreduce/instruments/nirspec.json +56 -0
- pyreduce/instruments/nirspec.py +215 -0
- pyreduce/instruments/nte.json +47 -0
- pyreduce/instruments/nte.py +42 -0
- pyreduce/instruments/uves.json +59 -0
- pyreduce/instruments/uves.py +46 -0
- pyreduce/instruments/xshooter.json +66 -0
- pyreduce/instruments/xshooter.py +39 -0
- pyreduce/make_shear.py +606 -0
- pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
- pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
- pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
- pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
- pyreduce/masks/mask_elodie.fits.gz +0 -0
- pyreduce/masks/mask_feros3.fits.gz +0 -0
- pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
- pyreduce/masks/mask_harps_blue.fits.gz +0 -0
- pyreduce/masks/mask_harps_red.fits.gz +0 -0
- pyreduce/masks/mask_hds_blue.fits.gz +0 -0
- pyreduce/masks/mask_hds_red.fits.gz +0 -0
- pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
- pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
- pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
- pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
- pyreduce/masks/mask_mcdonald.fits.gz +0 -0
- pyreduce/masks/mask_nes.fits.gz +0 -0
- pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
- pyreduce/masks/mask_sarg.fits.gz +0 -0
- pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
- pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
- pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
- pyreduce/masks/mask_uves_blue.fits.gz +0 -0
- pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
- pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_uves_red.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
- pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
- pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
- pyreduce/rectify.py +138 -0
- pyreduce/reduce.py +2205 -0
- pyreduce/settings/settings_ANDES.json +89 -0
- pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
- pyreduce/settings/settings_HARPN.json +73 -0
- pyreduce/settings/settings_HARPS.json +69 -0
- pyreduce/settings/settings_JWST_MIRI.json +55 -0
- pyreduce/settings/settings_JWST_NIRISS.json +55 -0
- pyreduce/settings/settings_LICK_APF.json +62 -0
- pyreduce/settings/settings_MCDONALD.json +58 -0
- pyreduce/settings/settings_METIS_IFU.json +77 -0
- pyreduce/settings/settings_METIS_LSS.json +77 -0
- pyreduce/settings/settings_MICADO.json +78 -0
- pyreduce/settings/settings_NEID.json +73 -0
- pyreduce/settings/settings_NIRSPEC.json +58 -0
- pyreduce/settings/settings_NTE.json +60 -0
- pyreduce/settings/settings_UVES.json +54 -0
- pyreduce/settings/settings_XSHOOTER.json +78 -0
- pyreduce/settings/settings_pyreduce.json +178 -0
- pyreduce/settings/settings_schema.json +827 -0
- pyreduce/tools/__init__.py +0 -0
- pyreduce/tools/combine.py +117 -0
- pyreduce/trace_orders.py +645 -0
- pyreduce/util.py +1288 -0
- pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
- pyreduce/wavecal/atlas/thar.fits +4946 -13
- pyreduce/wavecal/atlas/thar_list.txt +4172 -0
- pyreduce/wavecal/atlas/une.fits +0 -0
- pyreduce/wavecal/convert.py +38 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
- pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
- pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
- pyreduce/wavecal/harps_blue_2D.npz +0 -0
- pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
- pyreduce/wavecal/harps_red_2D.npz +0 -0
- pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
- pyreduce/wavecal/mcdonald.npz +0 -0
- pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
- pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
- pyreduce/wavecal/nirspec_K2.npz +0 -0
- pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
- pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
- pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
- pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
- pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
- pyreduce/wavecal/xshooter_nir.npz +0 -0
- pyreduce/wavelength_calibration.py +1873 -0
- pyreduce_astro-0.6.0b5.dist-info/METADATA +113 -0
- pyreduce_astro-0.6.0b5.dist-info/RECORD +156 -0
- pyreduce_astro-0.6.0b5.dist-info/WHEEL +6 -0
- pyreduce_astro-0.6.0b5.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract parent module for all other instruments
|
|
3
|
+
Contains some general functionality, which may be overridden by the children of course
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import datetime
|
|
7
|
+
import glob
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os.path
|
|
11
|
+
from itertools import product
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from astropy.io import fits
|
|
15
|
+
from astropy.time import Time
|
|
16
|
+
from dateutil import parser
|
|
17
|
+
from tqdm import tqdm
|
|
18
|
+
|
|
19
|
+
from ..clipnflip import clipnflip
|
|
20
|
+
from .filters import Filter, InstrumentFilter, ModeFilter, NightFilter, ObjectFilter
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_first_index(arr, value):
|
|
26
|
+
"""find the first element equal to value in the array arr"""
|
|
27
|
+
try:
|
|
28
|
+
return next(i for i, v in enumerate(arr) if v == value)
|
|
29
|
+
except StopIteration as e:
|
|
30
|
+
raise KeyError(f"Value {value} not found") from e
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def observation_date_to_night(observation_date):
|
|
34
|
+
"""Convert an observation timestamp into the date of the observation night
|
|
35
|
+
Nights start at 12am and end at 12 am the next day
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
observation_date : datetime
|
|
40
|
+
timestamp of the observation
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
night : datetime.date
|
|
45
|
+
night of the observation
|
|
46
|
+
"""
|
|
47
|
+
if observation_date == "":
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
observation_date = parser.parse(observation_date)
|
|
51
|
+
oneday = datetime.timedelta(days=1)
|
|
52
|
+
|
|
53
|
+
if observation_date.hour < 12:
|
|
54
|
+
observation_date -= oneday
|
|
55
|
+
return observation_date.date()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class getter:
|
|
59
|
+
"""Get data from a header/dict, based on the given mode, and applies replacements"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, header, info, mode):
|
|
62
|
+
self.header = header
|
|
63
|
+
self.info = info.copy()
|
|
64
|
+
try:
|
|
65
|
+
self.index = find_first_index(info["modes"], mode.upper())
|
|
66
|
+
except KeyError:
|
|
67
|
+
logger.warning("No instrument modes found in instrument info")
|
|
68
|
+
self.index = 0
|
|
69
|
+
|
|
70
|
+
# Pick values for the given mode
|
|
71
|
+
for k, v in self.info.items():
|
|
72
|
+
if isinstance(v, list):
|
|
73
|
+
self.info[k] = v[self.index]
|
|
74
|
+
|
|
75
|
+
def __call__(self, key, alt=None):
|
|
76
|
+
return self.get(key, alt)
|
|
77
|
+
|
|
78
|
+
def get(self, key, alt=None):
|
|
79
|
+
"""Get data
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
key : str
|
|
84
|
+
key of the data in the header
|
|
85
|
+
alt : obj, optional
|
|
86
|
+
alternative value, if key does not exist (default: None)
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
value : obj
|
|
91
|
+
value found in header (or alternatively alt)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
value = self.info.get(key, key)
|
|
95
|
+
# if isinstance(value, list):
|
|
96
|
+
# value = value[self.index]
|
|
97
|
+
if isinstance(value, str):
|
|
98
|
+
value = value.format(**self.info)
|
|
99
|
+
value = self.header.get(value, alt)
|
|
100
|
+
return value
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Instrument:
|
|
104
|
+
"""
|
|
105
|
+
Abstract parent class for all instruments
|
|
106
|
+
Handles the instrument specific information
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(self):
|
|
110
|
+
#:str: Name of the instrument (lowercase)
|
|
111
|
+
self.name = self.__class__.__name__.lower()
|
|
112
|
+
#:dict: Information about the instrument
|
|
113
|
+
self.info = self.load_info()
|
|
114
|
+
|
|
115
|
+
self.filters = {
|
|
116
|
+
"instrument": InstrumentFilter(self.info["instrument"], regex=True),
|
|
117
|
+
"night": NightFilter(
|
|
118
|
+
self.info["date"], timeformat=self.info.get("date_format", "fits")
|
|
119
|
+
),
|
|
120
|
+
"target": ObjectFilter(self.info["target"], regex=True),
|
|
121
|
+
"bias": Filter(self.info["kw_bias"]),
|
|
122
|
+
"flat": Filter(self.info["kw_flat"]),
|
|
123
|
+
"orders": Filter(self.info["kw_orders"]),
|
|
124
|
+
"curvature": Filter(self.info["kw_curvature"]),
|
|
125
|
+
"scatter": Filter(self.info["kw_scatter"]),
|
|
126
|
+
"wave": Filter(self.info["kw_wave"]),
|
|
127
|
+
"comb": Filter(self.info["kw_comb"]),
|
|
128
|
+
"spec": Filter(self.info["kw_spec"]),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
self.night = "night"
|
|
132
|
+
self.science = "science"
|
|
133
|
+
self.shared = ["instrument", "night"]
|
|
134
|
+
self.find_closest = [
|
|
135
|
+
"bias",
|
|
136
|
+
"flat",
|
|
137
|
+
"wavecal_master",
|
|
138
|
+
"freq_comb_master",
|
|
139
|
+
"orders",
|
|
140
|
+
"scatter",
|
|
141
|
+
"curvature",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
def __str__(self):
|
|
145
|
+
return self.name
|
|
146
|
+
|
|
147
|
+
def get(self, key, header, mode, alt=None):
|
|
148
|
+
get = getter(header, self.info, mode)
|
|
149
|
+
return get(key, alt=alt)
|
|
150
|
+
|
|
151
|
+
def get_extension(self, header, mode):
|
|
152
|
+
mode = mode.upper()
|
|
153
|
+
extension = self.info.get("extension", 0)
|
|
154
|
+
|
|
155
|
+
if isinstance(extension, list):
|
|
156
|
+
imode = find_first_index(self.info["modes"], mode)
|
|
157
|
+
extension = extension[imode]
|
|
158
|
+
|
|
159
|
+
return extension
|
|
160
|
+
|
|
161
|
+
def load_info(self):
|
|
162
|
+
"""
|
|
163
|
+
Load static instrument information
|
|
164
|
+
Either as fits header keywords or static values
|
|
165
|
+
|
|
166
|
+
Returns
|
|
167
|
+
------
|
|
168
|
+
info : dict(str:object)
|
|
169
|
+
dictionary of REDUCE names for properties to Header keywords/static values
|
|
170
|
+
"""
|
|
171
|
+
# Tips & Tricks:
|
|
172
|
+
# if several modes are supported, use a list for modes
|
|
173
|
+
# if a value changes depending on the mode, use a list with the same order as "modes"
|
|
174
|
+
# you can also use values from this dictionary as placeholders using {name}, just like str.format
|
|
175
|
+
|
|
176
|
+
this = os.path.dirname(__file__)
|
|
177
|
+
fname = f"{self.name}.json"
|
|
178
|
+
fname = os.path.join(this, fname)
|
|
179
|
+
with open(fname) as f:
|
|
180
|
+
info = json.load(f)
|
|
181
|
+
return info
|
|
182
|
+
|
|
183
|
+
def load_fits(
|
|
184
|
+
self, fname, mode, extension=None, mask=None, header_only=False, dtype=None
|
|
185
|
+
):
|
|
186
|
+
"""
|
|
187
|
+
load fits file, REDUCE style
|
|
188
|
+
|
|
189
|
+
primary and extension header are combined
|
|
190
|
+
modeinfo is applied to header
|
|
191
|
+
data is clipnflipped
|
|
192
|
+
mask is applied
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
fname : str
|
|
197
|
+
filename
|
|
198
|
+
instrument : str
|
|
199
|
+
name of the instrument
|
|
200
|
+
mode : str
|
|
201
|
+
instrument mode
|
|
202
|
+
extension : int
|
|
203
|
+
data extension of the FITS file to load
|
|
204
|
+
mask : array, optional
|
|
205
|
+
mask to add to the data
|
|
206
|
+
header_only : bool, optional
|
|
207
|
+
only load the header, not the data
|
|
208
|
+
dtype : str, optional
|
|
209
|
+
numpy datatype to convert the read data to
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
--------
|
|
213
|
+
data : masked_array
|
|
214
|
+
FITS data, clipped and flipped, and with mask
|
|
215
|
+
header : fits.header
|
|
216
|
+
FITS header (Primary and Extension + Modeinfo)
|
|
217
|
+
|
|
218
|
+
ONLY the header is returned if header_only is True
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
mode = mode.upper()
|
|
222
|
+
|
|
223
|
+
hdu = fits.open(fname)
|
|
224
|
+
h_prime = hdu[0].header
|
|
225
|
+
if extension is None:
|
|
226
|
+
extension = self.get_extension(h_prime, mode)
|
|
227
|
+
|
|
228
|
+
header = hdu[extension].header
|
|
229
|
+
if extension != 0:
|
|
230
|
+
header.extend(h_prime, strip=False)
|
|
231
|
+
header = self.add_header_info(header, mode)
|
|
232
|
+
header["e_input"] = (os.path.basename(fname), "Original input filename")
|
|
233
|
+
|
|
234
|
+
if header_only:
|
|
235
|
+
hdu.close()
|
|
236
|
+
return header
|
|
237
|
+
|
|
238
|
+
data = clipnflip(hdu[extension].data, header)
|
|
239
|
+
|
|
240
|
+
if dtype is not None:
|
|
241
|
+
data = data.astype(dtype)
|
|
242
|
+
|
|
243
|
+
data = np.ma.masked_array(data, mask=mask)
|
|
244
|
+
|
|
245
|
+
hdu.close()
|
|
246
|
+
return data, header
|
|
247
|
+
|
|
248
|
+
def add_header_info(self, header, mode, **kwargs):
|
|
249
|
+
"""read data from header and add it as REDUCE keyword back to the header
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
header : fits.header, dict
|
|
254
|
+
header to read/write info from/to
|
|
255
|
+
mode : str
|
|
256
|
+
instrument mode
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
-------
|
|
260
|
+
header : fits.header, dict
|
|
261
|
+
header with added information
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
info = self.info
|
|
265
|
+
get = getter(header, info, mode)
|
|
266
|
+
|
|
267
|
+
header["e_instrument"] = get("instrument", self.__class__.__name__)
|
|
268
|
+
header["e_telescope"] = get("telescope", "")
|
|
269
|
+
header["e_exptime"] = get("exposure_time", 0)
|
|
270
|
+
|
|
271
|
+
jd = get("date")
|
|
272
|
+
if jd is not None:
|
|
273
|
+
jd = Time(jd, format=self.info.get("date_format", "fits"))
|
|
274
|
+
jd = jd.to_value("mjd")
|
|
275
|
+
|
|
276
|
+
header["e_orient"] = get("orientation", 0)
|
|
277
|
+
# As per IDL rotate if orient is 4 or larger and transpose is undefined
|
|
278
|
+
# the image is transposed
|
|
279
|
+
header["e_transpose"] = get("transpose", (header["e_orient"] % 8 >= 4))
|
|
280
|
+
|
|
281
|
+
naxis_x = get("naxis_x", 0)
|
|
282
|
+
naxis_y = get("naxis_y", 0)
|
|
283
|
+
|
|
284
|
+
prescan_x = get("prescan_x", 0)
|
|
285
|
+
overscan_x = get("overscan_x", 0)
|
|
286
|
+
prescan_y = get("prescan_y", 0)
|
|
287
|
+
overscan_y = get("overscan_y", 0)
|
|
288
|
+
|
|
289
|
+
header["e_xlo"] = prescan_x
|
|
290
|
+
header["e_xhi"] = naxis_x - overscan_x
|
|
291
|
+
|
|
292
|
+
header["e_ylo"] = prescan_y
|
|
293
|
+
header["e_yhi"] = naxis_y - overscan_y
|
|
294
|
+
|
|
295
|
+
header["e_gain"] = get("gain", 1)
|
|
296
|
+
header["e_readn"] = get("readnoise", 0)
|
|
297
|
+
|
|
298
|
+
header["e_sky"] = get("sky", 0)
|
|
299
|
+
header["e_drk"] = get("dark", 0)
|
|
300
|
+
header["e_backg"] = header["e_gain"] * (header["e_drk"] + header["e_sky"])
|
|
301
|
+
|
|
302
|
+
header["e_imtype"] = get("image_type")
|
|
303
|
+
header["e_ctg"] = get("category")
|
|
304
|
+
|
|
305
|
+
header["e_ra"] = get("ra", 0)
|
|
306
|
+
header["e_dec"] = get("dec", 0)
|
|
307
|
+
header["e_jd"] = jd
|
|
308
|
+
|
|
309
|
+
header["e_obslon"] = get("longitude")
|
|
310
|
+
header["e_obslat"] = get("latitude")
|
|
311
|
+
header["e_obsalt"] = get("altitude")
|
|
312
|
+
|
|
313
|
+
if info.get("wavecal_element", None) is not None:
|
|
314
|
+
header["HIERARCH e_wavecal_element"] = get(
|
|
315
|
+
"wavecal_element", info.get("wavecal_element", None)
|
|
316
|
+
)
|
|
317
|
+
return header
|
|
318
|
+
|
|
319
|
+
def find_files(self, input_dir):
|
|
320
|
+
"""Find fits files in the given folder
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
input_dir : string
|
|
325
|
+
directory to look for fits and fits.gz files in, may include bash style wildcards
|
|
326
|
+
|
|
327
|
+
Returns
|
|
328
|
+
-------
|
|
329
|
+
files: array(string)
|
|
330
|
+
absolute path filenames
|
|
331
|
+
"""
|
|
332
|
+
files = glob.glob(input_dir + "/*.fits")
|
|
333
|
+
files += glob.glob(input_dir + "/*.fits.gz")
|
|
334
|
+
files = np.array(files)
|
|
335
|
+
return files
|
|
336
|
+
|
|
337
|
+
def get_expected_values(self, target, night, *args, **kwargs):
|
|
338
|
+
expectations = {
|
|
339
|
+
"bias": {
|
|
340
|
+
"instrument": self.info["id_instrument"],
|
|
341
|
+
"night": night,
|
|
342
|
+
"bias": self.info["id_bias"],
|
|
343
|
+
},
|
|
344
|
+
"flat": {
|
|
345
|
+
"instrument": self.info["id_instrument"],
|
|
346
|
+
"night": night,
|
|
347
|
+
"flat": self.info["id_flat"],
|
|
348
|
+
},
|
|
349
|
+
"orders": {
|
|
350
|
+
"instrument": self.info["id_instrument"],
|
|
351
|
+
"night": night,
|
|
352
|
+
"orders": self.info["id_orders"],
|
|
353
|
+
},
|
|
354
|
+
"scatter": {
|
|
355
|
+
"instrument": self.info["id_instrument"],
|
|
356
|
+
"night": night,
|
|
357
|
+
"scatter": self.info["id_scatter"],
|
|
358
|
+
},
|
|
359
|
+
"curvature": {
|
|
360
|
+
"instrument": self.info["id_instrument"],
|
|
361
|
+
"night": night,
|
|
362
|
+
"curvature": self.info["id_curvature"],
|
|
363
|
+
},
|
|
364
|
+
"wavecal_master": {
|
|
365
|
+
"instrument": self.info["id_instrument"],
|
|
366
|
+
"night": night,
|
|
367
|
+
"wave": self.info["id_wave"],
|
|
368
|
+
},
|
|
369
|
+
"freq_comb_master": {
|
|
370
|
+
"instrument": self.info["id_instrument"],
|
|
371
|
+
"night": night,
|
|
372
|
+
"comb": self.info["id_comb"],
|
|
373
|
+
},
|
|
374
|
+
"science": {
|
|
375
|
+
"instrument": self.info["id_instrument"],
|
|
376
|
+
"night": night,
|
|
377
|
+
"target": target,
|
|
378
|
+
"spec": self.info["id_spec"],
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
return expectations
|
|
382
|
+
|
|
383
|
+
def populate_filters(self, files):
|
|
384
|
+
"""Extract values from the fits headers and store them in self.filters
|
|
385
|
+
|
|
386
|
+
Parameters
|
|
387
|
+
----------
|
|
388
|
+
files : list(str)
|
|
389
|
+
list of fits files
|
|
390
|
+
|
|
391
|
+
Returns
|
|
392
|
+
-------
|
|
393
|
+
filters: list(Filter)
|
|
394
|
+
list of populated filters (identical to self.filters)
|
|
395
|
+
"""
|
|
396
|
+
# Empty filters
|
|
397
|
+
for _, fil in self.filters.items():
|
|
398
|
+
fil.clear()
|
|
399
|
+
|
|
400
|
+
for f in tqdm(files):
|
|
401
|
+
with fits.open(f) as hdu:
|
|
402
|
+
h = hdu[0].header
|
|
403
|
+
for _, fil in self.filters.items():
|
|
404
|
+
fil.collect(h)
|
|
405
|
+
|
|
406
|
+
return self.filters
|
|
407
|
+
|
|
408
|
+
def apply_filters(self, files, expected, allow_calibration_only=False):
|
|
409
|
+
"""
|
|
410
|
+
Determine the relevant files for a given set of expected values.
|
|
411
|
+
|
|
412
|
+
Parameters
|
|
413
|
+
----------
|
|
414
|
+
files : list(files)
|
|
415
|
+
list if fits files
|
|
416
|
+
expected : dict
|
|
417
|
+
dictionary with expected header values for each reduction step
|
|
418
|
+
|
|
419
|
+
Returns
|
|
420
|
+
-------
|
|
421
|
+
files: list((dict, dict))
|
|
422
|
+
list of files. The first element of each tuple is the used setting,
|
|
423
|
+
and the second are the files for each step.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
# Fill the filters with header information
|
|
427
|
+
self.populate_filters(files)
|
|
428
|
+
|
|
429
|
+
# Use the header information determined in populate filters
|
|
430
|
+
# to find potential science and calibration files in the list of files
|
|
431
|
+
# result = {step : [ {setting : value}, [files] ] }
|
|
432
|
+
result = {}
|
|
433
|
+
for step, values in expected.items():
|
|
434
|
+
result[step] = []
|
|
435
|
+
data = {}
|
|
436
|
+
for name, value in values.items():
|
|
437
|
+
if isinstance(value, list):
|
|
438
|
+
for v in value:
|
|
439
|
+
data[name] = self.filters[name].classify(v)
|
|
440
|
+
if len(data[name]) > 0:
|
|
441
|
+
break
|
|
442
|
+
else:
|
|
443
|
+
data[name] = self.filters[name].classify(value)
|
|
444
|
+
# Get all combinations of possible filter values
|
|
445
|
+
# e.g. if several nights are allowed
|
|
446
|
+
for thingy in product(*data.values()):
|
|
447
|
+
mask = np.copy(thingy[0][1])
|
|
448
|
+
for i in range(1, len(thingy)):
|
|
449
|
+
mask &= thingy[i][1]
|
|
450
|
+
if np.count_nonzero(mask) == 0:
|
|
451
|
+
continue
|
|
452
|
+
d = {k: v[0] for k, v in zip(values.keys(), thingy, strict=False)}
|
|
453
|
+
f = files[mask]
|
|
454
|
+
result[step].append((d, f))
|
|
455
|
+
|
|
456
|
+
# Filter for only nights that have a science observation
|
|
457
|
+
# files = [{setting: value}, {step: files}]
|
|
458
|
+
files = []
|
|
459
|
+
if allow_calibration_only:
|
|
460
|
+
# Use all unique nights
|
|
461
|
+
settings = {}
|
|
462
|
+
for shared in self.shared:
|
|
463
|
+
keys = [k for k in set(self.filters[shared].data) if k is not None]
|
|
464
|
+
settings[shared] = keys
|
|
465
|
+
else:
|
|
466
|
+
# Or use only science nights
|
|
467
|
+
settings = {}
|
|
468
|
+
for shared in self.shared:
|
|
469
|
+
keys = [key[shared] for key, _ in result[self.science]]
|
|
470
|
+
settings[shared] = keys
|
|
471
|
+
|
|
472
|
+
values = [settings[k] for k in self.shared]
|
|
473
|
+
for setting in product(*values):
|
|
474
|
+
setting = dict(zip(self.shared, setting, strict=False))
|
|
475
|
+
night = setting[self.night]
|
|
476
|
+
f = {}
|
|
477
|
+
# For each step look for files with matching settings
|
|
478
|
+
for step, step_data in result.items():
|
|
479
|
+
f[step] = []
|
|
480
|
+
for step_key, step_files in step_data:
|
|
481
|
+
match = [
|
|
482
|
+
setting[shared] == step_key[shared]
|
|
483
|
+
for shared in self.shared
|
|
484
|
+
if shared in step_key.keys()
|
|
485
|
+
]
|
|
486
|
+
if all(match):
|
|
487
|
+
f[step] = step_files
|
|
488
|
+
break
|
|
489
|
+
# If no matching files are found ...
|
|
490
|
+
if len(f[step]) == 0:
|
|
491
|
+
if step not in self.find_closest:
|
|
492
|
+
# Show a warning
|
|
493
|
+
logger.warning(
|
|
494
|
+
"Could not find any files for step '%s' with settings %s, sharing parameters %s",
|
|
495
|
+
step,
|
|
496
|
+
setting,
|
|
497
|
+
self.shared,
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
# Or find the closest night instead
|
|
501
|
+
j = None
|
|
502
|
+
for i, (step_key, _) in enumerate(step_data):
|
|
503
|
+
match = [
|
|
504
|
+
setting[shared] == step_key[shared]
|
|
505
|
+
for shared in self.shared
|
|
506
|
+
if shared in step_key.keys() and shared != self.night
|
|
507
|
+
]
|
|
508
|
+
if all(match):
|
|
509
|
+
if j is None:
|
|
510
|
+
j = i
|
|
511
|
+
else:
|
|
512
|
+
diff_old = abs(step_data[j][0][self.night] - night)
|
|
513
|
+
diff_new = abs(step_data[i][0][self.night] - night)
|
|
514
|
+
if diff_new < diff_old:
|
|
515
|
+
j = i
|
|
516
|
+
if j is None:
|
|
517
|
+
# We still dont find any files
|
|
518
|
+
logger.warning(
|
|
519
|
+
"Could not find any files for step '%s' in any night with settings %s, sharing parameters %s",
|
|
520
|
+
step,
|
|
521
|
+
setting,
|
|
522
|
+
self.shared,
|
|
523
|
+
)
|
|
524
|
+
else:
|
|
525
|
+
# We found files in a close night
|
|
526
|
+
closest_key, closest_files = step_data[j]
|
|
527
|
+
logger.warning(
|
|
528
|
+
"Using '%s' files from night %s for observations of night %s",
|
|
529
|
+
step,
|
|
530
|
+
night,
|
|
531
|
+
closest_key["night"],
|
|
532
|
+
)
|
|
533
|
+
f[step] = closest_files
|
|
534
|
+
|
|
535
|
+
if any(len(a) > 0 for a in f.values()):
|
|
536
|
+
files.append((setting, f))
|
|
537
|
+
if len(files) == 0:
|
|
538
|
+
logger.warning(
|
|
539
|
+
"No %s files found matching the expected values %s",
|
|
540
|
+
self.science,
|
|
541
|
+
expected[self.science],
|
|
542
|
+
)
|
|
543
|
+
return files
|
|
544
|
+
|
|
545
|
+
def sort_files(
|
|
546
|
+
self, input_dir, target, night, *args, allow_calibration_only=False, **kwargs
|
|
547
|
+
):
|
|
548
|
+
"""
|
|
549
|
+
Sort a set of fits files into different categories
|
|
550
|
+
types are: bias, flat, wavecal, orderdef, spec
|
|
551
|
+
|
|
552
|
+
Parameters
|
|
553
|
+
----------
|
|
554
|
+
input_dir : str
|
|
555
|
+
input directory containing the files to sort
|
|
556
|
+
target : str
|
|
557
|
+
name of the target as in the fits headers
|
|
558
|
+
night : str
|
|
559
|
+
observation night, possibly with wildcards
|
|
560
|
+
mode : str
|
|
561
|
+
instrument mode
|
|
562
|
+
Returns
|
|
563
|
+
-------
|
|
564
|
+
files_per_night : list[dict{str:dict{str:list[str]}}]
|
|
565
|
+
a list of file sets, one entry per night, where each night consists of a dictionary with one entry per setting,
|
|
566
|
+
each fileset has five lists of filenames: "bias", "flat", "order", "wave", "spec", organised in another dict
|
|
567
|
+
nights_out : list[datetime]
|
|
568
|
+
a list of observation times, same order as files_per_night
|
|
569
|
+
"""
|
|
570
|
+
input_dir = input_dir.format(
|
|
571
|
+
**kwargs, target=target, night=night, instrument=self.name
|
|
572
|
+
)
|
|
573
|
+
files = self.find_files(input_dir)
|
|
574
|
+
ev = self.get_expected_values(target, night, *args, **kwargs)
|
|
575
|
+
files = self.apply_filters(
|
|
576
|
+
files, ev, allow_calibration_only=allow_calibration_only
|
|
577
|
+
)
|
|
578
|
+
return files
|
|
579
|
+
|
|
580
|
+
def get_wavecal_filename(self, header, mode, **kwargs):
|
|
581
|
+
"""Get the filename of the pre-existing wavelength solution for the current setting
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
header : fits.header, dict
|
|
586
|
+
header of the wavelength calibration file
|
|
587
|
+
mode : str
|
|
588
|
+
instrument mode
|
|
589
|
+
|
|
590
|
+
Returns
|
|
591
|
+
-------
|
|
592
|
+
filename : str
|
|
593
|
+
name of the wavelength solution file
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
specifier = header.get(self.info.get("wavecal_specifier", ""), "")
|
|
597
|
+
instrument = "wavecal"
|
|
598
|
+
|
|
599
|
+
cwd = os.path.dirname(__file__)
|
|
600
|
+
fname = f"{instrument.lower()}_{mode}_{specifier}.npz"
|
|
601
|
+
fname = os.path.join(cwd, "..", "wavecal", fname)
|
|
602
|
+
return fname
|
|
603
|
+
|
|
604
|
+
def get_supported_modes(self):
|
|
605
|
+
return self.info["modes"]
|
|
606
|
+
|
|
607
|
+
def get_mask_filename(self, mode, **kwargs):
|
|
608
|
+
i = self.name.lower()
|
|
609
|
+
m = mode.lower()
|
|
610
|
+
fname = f"mask_{i}_{m}.fits.gz"
|
|
611
|
+
cwd = os.path.dirname(__file__)
|
|
612
|
+
fname = os.path.join(cwd, "..", "masks", fname)
|
|
613
|
+
return fname
|
|
614
|
+
|
|
615
|
+
def get_wavelength_range(self, header, mode, **kwargs):
|
|
616
|
+
return self.get("wavelength_range", header, mode)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class InstrumentWithModes(Instrument):
|
|
620
|
+
def __init__(self):
|
|
621
|
+
super().__init__()
|
|
622
|
+
|
|
623
|
+
# replacement = {k: v for k, v in zip(self.info["id_modes"], self.info["modes"])}
|
|
624
|
+
self.filters["mode"] = ModeFilter(self.info["kw_modes"])
|
|
625
|
+
self.shared += ["mode"]
|
|
626
|
+
|
|
627
|
+
def get_expected_values(self, target, night, mode):
|
|
628
|
+
expectations = super().get_expected_values(target, night, mode)
|
|
629
|
+
|
|
630
|
+
id_mode = [
|
|
631
|
+
self.info["id_modes"][i]
|
|
632
|
+
for i, m in enumerate(self.info["modes"])
|
|
633
|
+
if m == mode
|
|
634
|
+
][0]
|
|
635
|
+
|
|
636
|
+
for key in expectations.keys():
|
|
637
|
+
expectations[key]["mode"] = id_mode
|
|
638
|
+
|
|
639
|
+
return expectations
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
class COMMON(Instrument):
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def create_custom_instrument(
|
|
647
|
+
name, extension=0, info=None, mask_file=None, wavecal_file=None, hasModes=False
|
|
648
|
+
):
|
|
649
|
+
cls = Instrument if not hasModes else InstrumentWithModes
|
|
650
|
+
|
|
651
|
+
class CUSTOM(cls):
|
|
652
|
+
def __init__(self):
|
|
653
|
+
super().__init__()
|
|
654
|
+
self.name = name
|
|
655
|
+
|
|
656
|
+
def load_info(self):
|
|
657
|
+
if info is None:
|
|
658
|
+
return COMMON().info
|
|
659
|
+
try:
|
|
660
|
+
with open(info) as f:
|
|
661
|
+
data = json.load(f)
|
|
662
|
+
return data
|
|
663
|
+
except:
|
|
664
|
+
return info
|
|
665
|
+
|
|
666
|
+
def get_extension(self, header, mode):
|
|
667
|
+
return extension
|
|
668
|
+
|
|
669
|
+
def get_mask_filename(self, mode, **kwargs):
|
|
670
|
+
return mask_file
|
|
671
|
+
|
|
672
|
+
def get_wavecal_filename(self, header, mode, **kwargs):
|
|
673
|
+
return wavecal_file
|
|
674
|
+
|
|
675
|
+
return CUSTOM()
|