pyreduce-astro 0.7a4__cp314-cp314-win_amd64.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 +322 -0
- pyreduce/cli.py +342 -0
- pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.exp +0 -0
- pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.lib +0 -0
- pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
- pyreduce/clib/__init__.py +0 -0
- pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
- pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +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 +861 -0
- pyreduce/configuration.py +191 -0
- pyreduce/continuum_normalization.py +329 -0
- pyreduce/cwrappers.py +404 -0
- pyreduce/datasets.py +238 -0
- pyreduce/echelle.py +413 -0
- pyreduce/estimate_background_scatter.py +130 -0
- pyreduce/extract.py +1362 -0
- pyreduce/extraction_width.py +77 -0
- pyreduce/instruments/__init__.py +0 -0
- pyreduce/instruments/aj.py +9 -0
- pyreduce/instruments/aj.yaml +51 -0
- pyreduce/instruments/andes.py +102 -0
- pyreduce/instruments/andes.yaml +72 -0
- pyreduce/instruments/common.py +711 -0
- pyreduce/instruments/common.yaml +57 -0
- pyreduce/instruments/crires_plus.py +103 -0
- pyreduce/instruments/crires_plus.yaml +101 -0
- pyreduce/instruments/filters.py +195 -0
- pyreduce/instruments/harpn.py +203 -0
- pyreduce/instruments/harpn.yaml +140 -0
- pyreduce/instruments/harps.py +312 -0
- pyreduce/instruments/harps.yaml +144 -0
- pyreduce/instruments/instrument_info.py +140 -0
- pyreduce/instruments/jwst_miri.py +29 -0
- pyreduce/instruments/jwst_miri.yaml +53 -0
- pyreduce/instruments/jwst_niriss.py +98 -0
- pyreduce/instruments/jwst_niriss.yaml +60 -0
- pyreduce/instruments/lick_apf.py +35 -0
- pyreduce/instruments/lick_apf.yaml +60 -0
- pyreduce/instruments/mcdonald.py +123 -0
- pyreduce/instruments/mcdonald.yaml +56 -0
- pyreduce/instruments/metis_ifu.py +45 -0
- pyreduce/instruments/metis_ifu.yaml +62 -0
- pyreduce/instruments/metis_lss.py +45 -0
- pyreduce/instruments/metis_lss.yaml +62 -0
- pyreduce/instruments/micado.py +45 -0
- pyreduce/instruments/micado.yaml +62 -0
- pyreduce/instruments/models.py +257 -0
- pyreduce/instruments/neid.py +156 -0
- pyreduce/instruments/neid.yaml +61 -0
- pyreduce/instruments/nirspec.py +215 -0
- pyreduce/instruments/nirspec.yaml +63 -0
- pyreduce/instruments/nte.py +42 -0
- pyreduce/instruments/nte.yaml +55 -0
- pyreduce/instruments/uves.py +46 -0
- pyreduce/instruments/uves.yaml +65 -0
- pyreduce/instruments/xshooter.py +39 -0
- pyreduce/instruments/xshooter.yaml +63 -0
- pyreduce/make_shear.py +607 -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/pipeline.py +619 -0
- pyreduce/rectify.py +138 -0
- pyreduce/reduce.py +2065 -0
- pyreduce/settings/settings_AJ.json +19 -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 +184 -0
- pyreduce/settings/settings_schema.json +850 -0
- pyreduce/tools/__init__.py +0 -0
- pyreduce/tools/combine.py +117 -0
- pyreduce/trace.py +979 -0
- pyreduce/util.py +1366 -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 +1871 -0
- pyreduce_astro-0.7a4.dist-info/METADATA +106 -0
- pyreduce_astro-0.7a4.dist-info/RECORD +182 -0
- pyreduce_astro-0.7a4.dist-info/WHEEL +4 -0
- pyreduce_astro-0.7a4.dist-info/entry_points.txt +2 -0
- pyreduce_astro-0.7a4.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# COMMON instrument configuration
|
|
2
|
+
# Default/base configuration used as fallback for custom instruments
|
|
3
|
+
|
|
4
|
+
__instrument__: COMMON
|
|
5
|
+
instrument: INSTRUME
|
|
6
|
+
id_instrument: COMMON
|
|
7
|
+
telescope: TELESCOP
|
|
8
|
+
target: OBJECT
|
|
9
|
+
|
|
10
|
+
date: DATE-OBS
|
|
11
|
+
date_format: fits
|
|
12
|
+
|
|
13
|
+
extension: 0
|
|
14
|
+
orientation: 0
|
|
15
|
+
transpose: false
|
|
16
|
+
|
|
17
|
+
prescan_x: 0
|
|
18
|
+
overscan_x: 0
|
|
19
|
+
prescan_y: 0
|
|
20
|
+
overscan_y: 0
|
|
21
|
+
naxis_x: NAXIS1
|
|
22
|
+
naxis_y: NAXIS2
|
|
23
|
+
|
|
24
|
+
gain: 1
|
|
25
|
+
readnoise: 0
|
|
26
|
+
dark: 0
|
|
27
|
+
sky: 0
|
|
28
|
+
exposure_time: EXPTIME
|
|
29
|
+
|
|
30
|
+
ra: RA
|
|
31
|
+
dec: DEC
|
|
32
|
+
longitude: null
|
|
33
|
+
latitude: null
|
|
34
|
+
altitude: null
|
|
35
|
+
|
|
36
|
+
# File classification keywords and patterns
|
|
37
|
+
# These define the keywords we look for, and the values they should have
|
|
38
|
+
# in the fits header, to be sorted into that category
|
|
39
|
+
kw_bias: ""
|
|
40
|
+
kw_flat: ""
|
|
41
|
+
kw_curvature: ""
|
|
42
|
+
kw_scatter: ""
|
|
43
|
+
kw_orders: ""
|
|
44
|
+
kw_wave: ""
|
|
45
|
+
kw_comb: ""
|
|
46
|
+
kw_spec: ""
|
|
47
|
+
|
|
48
|
+
id_bias: BIAS
|
|
49
|
+
id_flat: FLAT
|
|
50
|
+
id_orders: ORDER
|
|
51
|
+
id_curvature: CURV
|
|
52
|
+
id_scatter: SCATTER
|
|
53
|
+
id_wave: WAVE
|
|
54
|
+
id_comb: COMB
|
|
55
|
+
id_spec: SPEC
|
|
56
|
+
|
|
57
|
+
wavelength_range: null
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handles instrument specific info for the HARPS spectrograph
|
|
3
|
+
|
|
4
|
+
Mostly reading data from the header
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os.path
|
|
9
|
+
import re
|
|
10
|
+
from itertools import product
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from .common import Instrument
|
|
15
|
+
from .filters import Filter
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CRIRES_PLUS(Instrument):
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.filters["lamp"] = Filter(self.info["id_lamp"])
|
|
24
|
+
self.filters["band"] = Filter(self.info["id_band"])
|
|
25
|
+
self.filters["decker"] = Filter(self.info["id_decker"])
|
|
26
|
+
self.shared += ["band", "decker"]
|
|
27
|
+
|
|
28
|
+
def add_header_info(self, header, arm, **kwargs):
|
|
29
|
+
"""read data from header and add it as REDUCE keyword back to the header"""
|
|
30
|
+
# "Normal" stuff is handled by the general version, specific changes to values happen here
|
|
31
|
+
# alternatively you can implement all of it here, whatever works
|
|
32
|
+
band, decker, detector = self.parse_arm(arm)
|
|
33
|
+
header = super().add_header_info(header, band)
|
|
34
|
+
self.load_info()
|
|
35
|
+
|
|
36
|
+
return header
|
|
37
|
+
|
|
38
|
+
def get_supported_arms(self):
|
|
39
|
+
settings = self.info["settings"]
|
|
40
|
+
deckers = self.info["deckers"]
|
|
41
|
+
detectors = self.info["chips"]
|
|
42
|
+
arms = [
|
|
43
|
+
"_".join([s, d, c]) for s, d, c in product(settings, deckers, detectors)
|
|
44
|
+
]
|
|
45
|
+
return arms
|
|
46
|
+
|
|
47
|
+
def parse_arm(self, arm):
|
|
48
|
+
pattern = r"([YJHKLM]\d{4})(_(Open|pos1|pos2))?_det(\d)"
|
|
49
|
+
match = re.match(pattern, arm, flags=re.IGNORECASE)
|
|
50
|
+
if not match:
|
|
51
|
+
logger.error("no arm match")
|
|
52
|
+
else:
|
|
53
|
+
band = match.group(1).upper()
|
|
54
|
+
if match.group(3) is not None:
|
|
55
|
+
decker = match.group(3).lower().capitalize()
|
|
56
|
+
else:
|
|
57
|
+
decker = "Open"
|
|
58
|
+
detector = match.group(4)
|
|
59
|
+
return band, decker, detector
|
|
60
|
+
|
|
61
|
+
def get_expected_values(self, target, night, arm):
|
|
62
|
+
expectations = super().get_expected_values(target, night)
|
|
63
|
+
band, decker, detector = self.parse_arm(arm)
|
|
64
|
+
|
|
65
|
+
for key in expectations.keys():
|
|
66
|
+
if key == "bias":
|
|
67
|
+
continue
|
|
68
|
+
expectations[key]["band"] = band
|
|
69
|
+
expectations[key]["decker"] = decker
|
|
70
|
+
|
|
71
|
+
return expectations
|
|
72
|
+
|
|
73
|
+
def get_extension(self, header, arm):
|
|
74
|
+
band, decker, detector = self.parse_arm(arm)
|
|
75
|
+
extension = int(detector)
|
|
76
|
+
return extension
|
|
77
|
+
|
|
78
|
+
def get_wavecal_filename(self, header, arm, **kwargs):
|
|
79
|
+
"""Get the filename of the wavelength calibration config file"""
|
|
80
|
+
cwd = os.path.dirname(__file__)
|
|
81
|
+
fname = f"{self.name}_{arm}.npz"
|
|
82
|
+
fname = os.path.join(cwd, "..", "wavecal", fname)
|
|
83
|
+
return fname
|
|
84
|
+
|
|
85
|
+
def get_mask_filename(self, arm, **kwargs):
|
|
86
|
+
i = self.name.lower()
|
|
87
|
+
band, decker, detector = self.parse_arm(arm)
|
|
88
|
+
|
|
89
|
+
fname = f"mask_{i}_det{detector}.fits.gz"
|
|
90
|
+
cwd = os.path.dirname(__file__)
|
|
91
|
+
fname = os.path.join(cwd, "..", "masks", fname)
|
|
92
|
+
return fname
|
|
93
|
+
|
|
94
|
+
def get_wavelength_range(self, header, arm, **kwargs):
|
|
95
|
+
wmin = [header["ESO INS WLEN MIN%i" % i] for i in range(1, 11)]
|
|
96
|
+
wmax = [header["ESO INS WLEN MAX%i" % i] for i in range(1, 11)]
|
|
97
|
+
|
|
98
|
+
wavelength_range = np.array([wmin, wmax]).T
|
|
99
|
+
# Invert the order numbering
|
|
100
|
+
wavelength_range = wavelength_range[::-1]
|
|
101
|
+
# Convert from nm to Angstrom
|
|
102
|
+
wavelength_range *= 10
|
|
103
|
+
return wavelength_range
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# CRIRES+ instrument configuration
|
|
2
|
+
# The upgraded CRIRES at ESO VLT, a cross-dispersed IR spectrograph for YJHKLM bands.
|
|
3
|
+
|
|
4
|
+
__instrument__: CRIRES_PLUS
|
|
5
|
+
instrument: INSTRUME
|
|
6
|
+
id_instrument: CRIRES
|
|
7
|
+
telescope: VLT
|
|
8
|
+
|
|
9
|
+
date: DATE-OBS
|
|
10
|
+
date_format: fits
|
|
11
|
+
|
|
12
|
+
id_mode: "ESO INS MODE"
|
|
13
|
+
id_band: "ESO INS WLEN ID"
|
|
14
|
+
id_decker: "ESO INS OPTI8 ID"
|
|
15
|
+
id_lamp: "ESO INS1 LAMP? ID"
|
|
16
|
+
|
|
17
|
+
arms: [J1228]
|
|
18
|
+
deckers: [OPEN, pos1, pos2]
|
|
19
|
+
bands: [Y, J, H, K, L, M]
|
|
20
|
+
settings:
|
|
21
|
+
- Y1029
|
|
22
|
+
- Y1028
|
|
23
|
+
- J1232
|
|
24
|
+
- J1228
|
|
25
|
+
- J1226
|
|
26
|
+
- H1582
|
|
27
|
+
- H1575
|
|
28
|
+
- H1567
|
|
29
|
+
- H1559
|
|
30
|
+
- K2217
|
|
31
|
+
- K2192
|
|
32
|
+
- K2166
|
|
33
|
+
- K2148
|
|
34
|
+
- L3426
|
|
35
|
+
- L3412
|
|
36
|
+
- L3377
|
|
37
|
+
- L3340
|
|
38
|
+
- L3302
|
|
39
|
+
- L3262
|
|
40
|
+
- L3244
|
|
41
|
+
- M4519
|
|
42
|
+
- M4504
|
|
43
|
+
- M4461
|
|
44
|
+
- M4416
|
|
45
|
+
- M4368
|
|
46
|
+
- M4318
|
|
47
|
+
- M4266
|
|
48
|
+
- M4211
|
|
49
|
+
- M4187
|
|
50
|
+
chips: [det1, det2, det3]
|
|
51
|
+
|
|
52
|
+
extension: CHIP1.INT1
|
|
53
|
+
orientation: 0
|
|
54
|
+
transpose: false
|
|
55
|
+
|
|
56
|
+
prescan_x: 5
|
|
57
|
+
overscan_x: 5
|
|
58
|
+
prescan_y: 5
|
|
59
|
+
overscan_y: 5
|
|
60
|
+
naxis_x: NAXIS1
|
|
61
|
+
naxis_y: NAXIS2
|
|
62
|
+
|
|
63
|
+
gain: "HIERARCH ESO DET CHIP GAIN"
|
|
64
|
+
readnoise: "HIERARCH ESO DET CHIP RON"
|
|
65
|
+
dark: "HIERARCH ESO DET DIT"
|
|
66
|
+
sky: 0
|
|
67
|
+
exposure_time: EXPTIME
|
|
68
|
+
|
|
69
|
+
image_type: OBJECT
|
|
70
|
+
category: "HIERARCH ESO DPR CATG"
|
|
71
|
+
ra: RA
|
|
72
|
+
dec: DEC
|
|
73
|
+
jd: MJD-OBS
|
|
74
|
+
longitude: "HIERARCH ESO TEL GEOLON"
|
|
75
|
+
latitude: "HIERARCH ESO TEL GEOLAT"
|
|
76
|
+
altitude: "HIERARCH ESO TEL GEOELEV"
|
|
77
|
+
target: OBJECT
|
|
78
|
+
observation_type: "ESO DPR TYPE"
|
|
79
|
+
|
|
80
|
+
id_detcheck: "FLAT,LAMP,DETCHECK"
|
|
81
|
+
id_lamp_wavecal: UNe_HCL
|
|
82
|
+
id_lamp_etalon: Etalon_Halogen
|
|
83
|
+
|
|
84
|
+
# File classification keywords and patterns
|
|
85
|
+
kw_bias: "ESO DPR TYPE"
|
|
86
|
+
kw_flat: "ESO DPR TYPE"
|
|
87
|
+
kw_curvature: "ESO DPR TYPE"
|
|
88
|
+
kw_scatter: "ESO DPR TYPE"
|
|
89
|
+
kw_orders: "ESO DPR TYPE"
|
|
90
|
+
kw_wave: "ESO DPR TYPE"
|
|
91
|
+
kw_comb: "ESO DPR TYPE"
|
|
92
|
+
kw_spec: "ESO DPR TYPE"
|
|
93
|
+
|
|
94
|
+
id_bias: DARK
|
|
95
|
+
id_flat: FLAT
|
|
96
|
+
id_orders: FLAT
|
|
97
|
+
id_curvature: "WAVE,FPET"
|
|
98
|
+
id_scatter: FLAT
|
|
99
|
+
id_wave: "WAVE,UNE"
|
|
100
|
+
id_comb: "WAVE,FPET"
|
|
101
|
+
id_spec: "STAR,*,*"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from fnmatch import fnmatch
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from astropy import units as u
|
|
8
|
+
from astropy.time import Time
|
|
9
|
+
from dateutil import parser
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Filter:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
keyword,
|
|
18
|
+
dtype="U20",
|
|
19
|
+
wildcards=False,
|
|
20
|
+
regex=False,
|
|
21
|
+
flags=0,
|
|
22
|
+
unique=True,
|
|
23
|
+
ignorecase=True,
|
|
24
|
+
):
|
|
25
|
+
self.keyword = keyword
|
|
26
|
+
self.dtype = dtype
|
|
27
|
+
self.wildcards = wildcards
|
|
28
|
+
self.regex = regex
|
|
29
|
+
self.flags = flags
|
|
30
|
+
self.data = []
|
|
31
|
+
self.unique = unique
|
|
32
|
+
self.ignorecase = ignorecase
|
|
33
|
+
|
|
34
|
+
if self.ignorecase and not self.flags & re.IGNORECASE:
|
|
35
|
+
self.flags += re.IGNORECASE
|
|
36
|
+
|
|
37
|
+
def _collect_value(self, header):
|
|
38
|
+
if self.keyword is None:
|
|
39
|
+
value = ""
|
|
40
|
+
elif "{" in self.keyword:
|
|
41
|
+
kws = re.findall(r"{([^{}]+)}", self.keyword)
|
|
42
|
+
values = {kw: header.get(kw, "") for kw in kws}
|
|
43
|
+
value = self.keyword.format(**values)
|
|
44
|
+
else:
|
|
45
|
+
value = header.get(self.keyword)
|
|
46
|
+
if value.__class__ == header.__class__:
|
|
47
|
+
if len(value) > 0:
|
|
48
|
+
value = value[0]
|
|
49
|
+
else:
|
|
50
|
+
value = ""
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
def collect(self, header):
|
|
54
|
+
value = self._collect_value(header)
|
|
55
|
+
self.data.append(value)
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
def match(self, value):
|
|
59
|
+
if self.keyword is None:
|
|
60
|
+
result = np.full(len(self.data), False)
|
|
61
|
+
else:
|
|
62
|
+
try:
|
|
63
|
+
if self.regex:
|
|
64
|
+
regex = re.compile(f"^(?:{value})$", flags=self.flags)
|
|
65
|
+
elif self.wildcards:
|
|
66
|
+
regex = re.compile(fnmatch.translate(value), flags=self.flags)
|
|
67
|
+
else:
|
|
68
|
+
regex = re.compile(value, flags=self.flags)
|
|
69
|
+
|
|
70
|
+
result = [
|
|
71
|
+
regex.match(f) is not None if f is not None else False
|
|
72
|
+
for f in self.data
|
|
73
|
+
]
|
|
74
|
+
except TypeError:
|
|
75
|
+
result = [f == value for f in self.data]
|
|
76
|
+
result = np.asarray(result, dtype=bool)
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
def classify(self, value):
|
|
80
|
+
if self.unique:
|
|
81
|
+
if value is not None and value != "":
|
|
82
|
+
match = self.match(value)
|
|
83
|
+
data = np.asarray(self.data)
|
|
84
|
+
data = np.unique(data[match])
|
|
85
|
+
else:
|
|
86
|
+
data = set(self.data)
|
|
87
|
+
data = [(d, self.match(d)) for d in data]
|
|
88
|
+
else:
|
|
89
|
+
if value is not None and value != "":
|
|
90
|
+
match = self.match(value)
|
|
91
|
+
else:
|
|
92
|
+
match = np.full(len(self.data), True)
|
|
93
|
+
data = [(value, match)]
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
def clear(self):
|
|
97
|
+
self.data = []
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class InstrumentFilter(Filter):
|
|
101
|
+
def __init__(self, keyword="INSTRUME", **kwargs):
|
|
102
|
+
kwargs["dtype"] = "U20"
|
|
103
|
+
kwargs["unique"] = False
|
|
104
|
+
super().__init__(keyword, **kwargs)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ObjectFilter(Filter):
|
|
108
|
+
def __init__(self, keyword="OBJECT", **kwargs):
|
|
109
|
+
kwargs["dtype"] = "U20"
|
|
110
|
+
kwargs["unique"] = False
|
|
111
|
+
super().__init__(keyword, **kwargs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class NightFilter(Filter):
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
keyword="DATE-OBS",
|
|
118
|
+
timeformat="fits",
|
|
119
|
+
timezone="utc",
|
|
120
|
+
timezone_local=None,
|
|
121
|
+
**kwargs,
|
|
122
|
+
):
|
|
123
|
+
super().__init__(keyword, dtype=datetime, **kwargs)
|
|
124
|
+
self.timeformat = timeformat
|
|
125
|
+
self.timezone = timezone
|
|
126
|
+
self.timezone_local = timezone_local
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def observation_date_to_night(observation_date):
|
|
130
|
+
"""Convert an observation timestamp into the date of the observation night
|
|
131
|
+
Nights start at 12am and end at 12 am the next day
|
|
132
|
+
"""
|
|
133
|
+
if observation_date.to_datetime().hour < 12:
|
|
134
|
+
observation_date -= 1 * u.day
|
|
135
|
+
return observation_date.to_datetime().date()
|
|
136
|
+
|
|
137
|
+
def collect(self, header):
|
|
138
|
+
value = super()._collect_value(header)
|
|
139
|
+
if value is not None:
|
|
140
|
+
try:
|
|
141
|
+
value = Time(value, format=self.timeformat, scale=self.timezone)
|
|
142
|
+
value = self.observation_date_to_night(value)
|
|
143
|
+
except ValueError:
|
|
144
|
+
logger.warning(
|
|
145
|
+
"Could not determine the observation date of %s, skipping it",
|
|
146
|
+
header,
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
logger.warning(
|
|
150
|
+
"Could not determine the observation date of %s, skipping it", header
|
|
151
|
+
)
|
|
152
|
+
self.data.append(value)
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
def match(self, value):
|
|
156
|
+
try:
|
|
157
|
+
value = parser.parse(value).date()
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
match = super().match(value)
|
|
161
|
+
return match
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ArmFilter(Filter):
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
keyword,
|
|
168
|
+
dtype="U20",
|
|
169
|
+
wildcards=False,
|
|
170
|
+
regex=False,
|
|
171
|
+
flags=0,
|
|
172
|
+
unique=True,
|
|
173
|
+
ignorecase=True,
|
|
174
|
+
replacement=None,
|
|
175
|
+
):
|
|
176
|
+
if replacement is None:
|
|
177
|
+
replacement = {}
|
|
178
|
+
super().__init__(
|
|
179
|
+
keyword,
|
|
180
|
+
dtype=dtype,
|
|
181
|
+
wildcards=wildcards,
|
|
182
|
+
regex=regex,
|
|
183
|
+
flags=flags,
|
|
184
|
+
unique=unique,
|
|
185
|
+
ignorecase=ignorecase,
|
|
186
|
+
)
|
|
187
|
+
self.replacement = replacement
|
|
188
|
+
|
|
189
|
+
def classify(self, value):
|
|
190
|
+
data = super().classify(value)
|
|
191
|
+
data = [
|
|
192
|
+
(self.replacement[d] if d in self.replacement.keys() else d, m)
|
|
193
|
+
for d, m in data
|
|
194
|
+
]
|
|
195
|
+
return data
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handles instrument specific info for the HARPN spectrograph
|
|
3
|
+
|
|
4
|
+
Mostly reading data from the header
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from os.path import dirname, join
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from .common import Instrument
|
|
14
|
+
from .filters import Filter, InstrumentFilter, NightFilter, ObjectFilter
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TypeFilter(Filter):
|
|
20
|
+
def __init__(self, keyword="TNG DPR TYPE"):
|
|
21
|
+
super().__init__(keyword, regex=True)
|
|
22
|
+
|
|
23
|
+
def classify(self, value):
|
|
24
|
+
if value is not None:
|
|
25
|
+
match = self.match(value)
|
|
26
|
+
data = np.asarray(self.data)
|
|
27
|
+
data = np.unique(data[match])
|
|
28
|
+
try:
|
|
29
|
+
regex = re.compile(value)
|
|
30
|
+
keys = [regex.match(f) for f in data]
|
|
31
|
+
keys = [[g for g in d.groups() if g is not None][0] for d in keys]
|
|
32
|
+
unique = np.unique(keys)
|
|
33
|
+
assign = {
|
|
34
|
+
u: [d for k, d in zip(keys, data, strict=False) if k == u]
|
|
35
|
+
for u in unique
|
|
36
|
+
}
|
|
37
|
+
data = [(u, self.match("|".join(a))) for u, a in assign.items()]
|
|
38
|
+
except IndexError:
|
|
39
|
+
data = np.asarray(self.data)
|
|
40
|
+
data = np.unique(data[match])
|
|
41
|
+
data = [(d, self.match(d)) for d in data]
|
|
42
|
+
else:
|
|
43
|
+
data = np.unique(self.data)
|
|
44
|
+
data = [(d, self.match(d)) for d in data]
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HARPN(Instrument):
|
|
49
|
+
def __init__(self):
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.filters = {
|
|
52
|
+
"instrument": InstrumentFilter(self.config.instrument),
|
|
53
|
+
"night": NightFilter(self.config.date),
|
|
54
|
+
# "branch": Filter(, regex=True),
|
|
55
|
+
"mode": Filter(
|
|
56
|
+
self.config.instrument_mode, regex=True, flags=re.IGNORECASE
|
|
57
|
+
),
|
|
58
|
+
"type": TypeFilter(self.config.observation_type),
|
|
59
|
+
"target": ObjectFilter(self.config.target, regex=True),
|
|
60
|
+
}
|
|
61
|
+
self.night = "night"
|
|
62
|
+
self.science = "science"
|
|
63
|
+
self.shared = [
|
|
64
|
+
"instrument",
|
|
65
|
+
"night",
|
|
66
|
+
"mode",
|
|
67
|
+
]
|
|
68
|
+
self.find_closest = [
|
|
69
|
+
"bias",
|
|
70
|
+
"flat",
|
|
71
|
+
"wavecal_master",
|
|
72
|
+
"freq_comb_master",
|
|
73
|
+
"orders",
|
|
74
|
+
"scatter",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
def get_expected_values(
|
|
78
|
+
self, target, night, arm=None, mode=None, fiber=None, **kwargs
|
|
79
|
+
):
|
|
80
|
+
"""Determine the default expected values in the headers for a given observation configuration
|
|
81
|
+
|
|
82
|
+
Any parameter may be None, to indicate that all values are allowed
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
target : str
|
|
87
|
+
Name of the star / observation target
|
|
88
|
+
night : str
|
|
89
|
+
Observation night/nights
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
expectations: dict
|
|
93
|
+
Dictionary of expected header values, with one entry per step.
|
|
94
|
+
The entries for each step refer to the filters defined in self.filters
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
ValueError
|
|
99
|
+
Invalid combination of parameters
|
|
100
|
+
"""
|
|
101
|
+
if target is not None:
|
|
102
|
+
target = target.replace(" ", r"(?:\s*|-)")
|
|
103
|
+
else:
|
|
104
|
+
target = ".*"
|
|
105
|
+
|
|
106
|
+
id_orddef = "LAMP,DARK,TUN"
|
|
107
|
+
id_spec = "STAR,WAVE"
|
|
108
|
+
|
|
109
|
+
expectations = {
|
|
110
|
+
"bias": {"instrument": "HARPN", "night": night, "type": r"BIAS,BIAS"},
|
|
111
|
+
"flat": {"instrument": "HARPN", "night": night, "type": r"LAMP,LAMP,TUN"},
|
|
112
|
+
"orders": {
|
|
113
|
+
"instrument": "HARPN",
|
|
114
|
+
"night": night,
|
|
115
|
+
"type": id_orddef,
|
|
116
|
+
},
|
|
117
|
+
"scatter": {
|
|
118
|
+
"instrument": "HARPN",
|
|
119
|
+
"night": night,
|
|
120
|
+
"type": id_orddef, # Same as orders or same as flat?
|
|
121
|
+
},
|
|
122
|
+
"wavecal_master": {
|
|
123
|
+
"instrument": "HARPN",
|
|
124
|
+
"night": night,
|
|
125
|
+
"type": r"WAVE,WAVE,THAR2",
|
|
126
|
+
},
|
|
127
|
+
"freq_comb_master": {
|
|
128
|
+
"instrument": "HARPN",
|
|
129
|
+
"night": night,
|
|
130
|
+
"type": r"WAVE,WAVE,COMB",
|
|
131
|
+
},
|
|
132
|
+
"science": {
|
|
133
|
+
"instrument": "HARPN",
|
|
134
|
+
"night": night,
|
|
135
|
+
"mode": mode,
|
|
136
|
+
"type": id_spec,
|
|
137
|
+
"target": target,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
return expectations
|
|
141
|
+
|
|
142
|
+
def get_extension(self, header, arm):
|
|
143
|
+
extension = super().get_extension(header, arm)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
if (
|
|
147
|
+
header["NAXIS"] == 2
|
|
148
|
+
and header["NAXIS1"] == 4296
|
|
149
|
+
and header["NAXIS2"] == 4096
|
|
150
|
+
):
|
|
151
|
+
extension = 0
|
|
152
|
+
except KeyError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
return extension
|
|
156
|
+
|
|
157
|
+
def add_header_info(self, header, arm, **kwargs):
|
|
158
|
+
"""read data from header and add it as REDUCE keyword back to the header"""
|
|
159
|
+
# "Normal" stuff is handled by the general version, specific changes to values happen here
|
|
160
|
+
# alternatively you can implement all of it here, whatever works
|
|
161
|
+
header = super().add_header_info(header, arm)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
header["e_ra"] /= 15
|
|
165
|
+
header["e_jd"] += header["e_exptim"] / (7200 * 24) + 0.5
|
|
166
|
+
|
|
167
|
+
except:
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if (
|
|
172
|
+
header["NAXIS"] == 2
|
|
173
|
+
and header["NAXIS1"] == 4296
|
|
174
|
+
and header["NAXIS2"] == 4096
|
|
175
|
+
):
|
|
176
|
+
# both arms are in the same image
|
|
177
|
+
prescan_x = 50
|
|
178
|
+
overscan_x = 50
|
|
179
|
+
naxis_x = 2148
|
|
180
|
+
if arm == "BLUE":
|
|
181
|
+
header["e_xlo"] = prescan_x
|
|
182
|
+
header["e_xhi"] = naxis_x - overscan_x
|
|
183
|
+
elif arm == "RED":
|
|
184
|
+
header["e_xlo"] = naxis_x + prescan_x
|
|
185
|
+
header["e_xhi"] = 2 * naxis_x - overscan_x
|
|
186
|
+
except KeyError:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
return header
|
|
190
|
+
|
|
191
|
+
def get_wavecal_filename(self, header, arm, **kwargs):
|
|
192
|
+
"""Get the filename of the wavelength calibration config file"""
|
|
193
|
+
cwd = dirname(__file__)
|
|
194
|
+
fname = f"harpn_{arm.lower()}_2D.npz"
|
|
195
|
+
fname = join(cwd, "..", "wavecal", fname)
|
|
196
|
+
return fname
|
|
197
|
+
|
|
198
|
+
def get_wavelength_range(self, header, arm, **kwargs):
|
|
199
|
+
wave_range = super().get_wavelength_range(header, arm, **kwargs)
|
|
200
|
+
# The wavelength orders are in inverse order in the .json file
|
|
201
|
+
# because I was to lazy to invert them in the file
|
|
202
|
+
wave_range = wave_range[::-1]
|
|
203
|
+
return wave_range
|