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.
Files changed (182) hide show
  1. pyreduce/__init__.py +67 -0
  2. pyreduce/__main__.py +322 -0
  3. pyreduce/cli.py +342 -0
  4. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
  5. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
  6. pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.exp +0 -0
  7. pyreduce/clib/Release/_slitfunc_2d.cp312-win_amd64.lib +0 -0
  8. pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.exp +0 -0
  9. pyreduce/clib/Release/_slitfunc_2d.cp313-win_amd64.lib +0 -0
  10. pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.exp +0 -0
  11. pyreduce/clib/Release/_slitfunc_2d.cp314-win_amd64.lib +0 -0
  12. pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
  13. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
  14. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
  15. pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.exp +0 -0
  16. pyreduce/clib/Release/_slitfunc_bd.cp312-win_amd64.lib +0 -0
  17. pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.exp +0 -0
  18. pyreduce/clib/Release/_slitfunc_bd.cp313-win_amd64.lib +0 -0
  19. pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.exp +0 -0
  20. pyreduce/clib/Release/_slitfunc_bd.cp314-win_amd64.lib +0 -0
  21. pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
  22. pyreduce/clib/__init__.py +0 -0
  23. pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
  24. pyreduce/clib/_slitfunc_2d.cp312-win_amd64.pyd +0 -0
  25. pyreduce/clib/_slitfunc_2d.cp313-win_amd64.pyd +0 -0
  26. pyreduce/clib/_slitfunc_2d.cp314-win_amd64.pyd +0 -0
  27. pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
  28. pyreduce/clib/_slitfunc_bd.cp312-win_amd64.pyd +0 -0
  29. pyreduce/clib/_slitfunc_bd.cp313-win_amd64.pyd +0 -0
  30. pyreduce/clib/_slitfunc_bd.cp314-win_amd64.pyd +0 -0
  31. pyreduce/clib/build_extract.py +75 -0
  32. pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
  33. pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
  34. pyreduce/clib/slit_func_bd.c +362 -0
  35. pyreduce/clib/slit_func_bd.h +17 -0
  36. pyreduce/clipnflip.py +147 -0
  37. pyreduce/combine_frames.py +861 -0
  38. pyreduce/configuration.py +191 -0
  39. pyreduce/continuum_normalization.py +329 -0
  40. pyreduce/cwrappers.py +404 -0
  41. pyreduce/datasets.py +238 -0
  42. pyreduce/echelle.py +413 -0
  43. pyreduce/estimate_background_scatter.py +130 -0
  44. pyreduce/extract.py +1362 -0
  45. pyreduce/extraction_width.py +77 -0
  46. pyreduce/instruments/__init__.py +0 -0
  47. pyreduce/instruments/aj.py +9 -0
  48. pyreduce/instruments/aj.yaml +51 -0
  49. pyreduce/instruments/andes.py +102 -0
  50. pyreduce/instruments/andes.yaml +72 -0
  51. pyreduce/instruments/common.py +711 -0
  52. pyreduce/instruments/common.yaml +57 -0
  53. pyreduce/instruments/crires_plus.py +103 -0
  54. pyreduce/instruments/crires_plus.yaml +101 -0
  55. pyreduce/instruments/filters.py +195 -0
  56. pyreduce/instruments/harpn.py +203 -0
  57. pyreduce/instruments/harpn.yaml +140 -0
  58. pyreduce/instruments/harps.py +312 -0
  59. pyreduce/instruments/harps.yaml +144 -0
  60. pyreduce/instruments/instrument_info.py +140 -0
  61. pyreduce/instruments/jwst_miri.py +29 -0
  62. pyreduce/instruments/jwst_miri.yaml +53 -0
  63. pyreduce/instruments/jwst_niriss.py +98 -0
  64. pyreduce/instruments/jwst_niriss.yaml +60 -0
  65. pyreduce/instruments/lick_apf.py +35 -0
  66. pyreduce/instruments/lick_apf.yaml +60 -0
  67. pyreduce/instruments/mcdonald.py +123 -0
  68. pyreduce/instruments/mcdonald.yaml +56 -0
  69. pyreduce/instruments/metis_ifu.py +45 -0
  70. pyreduce/instruments/metis_ifu.yaml +62 -0
  71. pyreduce/instruments/metis_lss.py +45 -0
  72. pyreduce/instruments/metis_lss.yaml +62 -0
  73. pyreduce/instruments/micado.py +45 -0
  74. pyreduce/instruments/micado.yaml +62 -0
  75. pyreduce/instruments/models.py +257 -0
  76. pyreduce/instruments/neid.py +156 -0
  77. pyreduce/instruments/neid.yaml +61 -0
  78. pyreduce/instruments/nirspec.py +215 -0
  79. pyreduce/instruments/nirspec.yaml +63 -0
  80. pyreduce/instruments/nte.py +42 -0
  81. pyreduce/instruments/nte.yaml +55 -0
  82. pyreduce/instruments/uves.py +46 -0
  83. pyreduce/instruments/uves.yaml +65 -0
  84. pyreduce/instruments/xshooter.py +39 -0
  85. pyreduce/instruments/xshooter.yaml +63 -0
  86. pyreduce/make_shear.py +607 -0
  87. pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
  88. pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
  89. pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
  90. pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
  91. pyreduce/masks/mask_elodie.fits.gz +0 -0
  92. pyreduce/masks/mask_feros3.fits.gz +0 -0
  93. pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
  94. pyreduce/masks/mask_harps_blue.fits.gz +0 -0
  95. pyreduce/masks/mask_harps_red.fits.gz +0 -0
  96. pyreduce/masks/mask_hds_blue.fits.gz +0 -0
  97. pyreduce/masks/mask_hds_red.fits.gz +0 -0
  98. pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
  99. pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
  100. pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
  101. pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
  102. pyreduce/masks/mask_mcdonald.fits.gz +0 -0
  103. pyreduce/masks/mask_nes.fits.gz +0 -0
  104. pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
  105. pyreduce/masks/mask_sarg.fits.gz +0 -0
  106. pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
  107. pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
  108. pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
  109. pyreduce/masks/mask_uves_blue.fits.gz +0 -0
  110. pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
  111. pyreduce/masks/mask_uves_middle.fits.gz +0 -0
  112. pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
  113. pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
  114. pyreduce/masks/mask_uves_red.fits.gz +0 -0
  115. pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
  116. pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
  117. pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
  118. pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
  119. pyreduce/pipeline.py +619 -0
  120. pyreduce/rectify.py +138 -0
  121. pyreduce/reduce.py +2065 -0
  122. pyreduce/settings/settings_AJ.json +19 -0
  123. pyreduce/settings/settings_ANDES.json +89 -0
  124. pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
  125. pyreduce/settings/settings_HARPN.json +73 -0
  126. pyreduce/settings/settings_HARPS.json +69 -0
  127. pyreduce/settings/settings_JWST_MIRI.json +55 -0
  128. pyreduce/settings/settings_JWST_NIRISS.json +55 -0
  129. pyreduce/settings/settings_LICK_APF.json +62 -0
  130. pyreduce/settings/settings_MCDONALD.json +58 -0
  131. pyreduce/settings/settings_METIS_IFU.json +77 -0
  132. pyreduce/settings/settings_METIS_LSS.json +77 -0
  133. pyreduce/settings/settings_MICADO.json +78 -0
  134. pyreduce/settings/settings_NEID.json +73 -0
  135. pyreduce/settings/settings_NIRSPEC.json +58 -0
  136. pyreduce/settings/settings_NTE.json +60 -0
  137. pyreduce/settings/settings_UVES.json +54 -0
  138. pyreduce/settings/settings_XSHOOTER.json +78 -0
  139. pyreduce/settings/settings_pyreduce.json +184 -0
  140. pyreduce/settings/settings_schema.json +850 -0
  141. pyreduce/tools/__init__.py +0 -0
  142. pyreduce/tools/combine.py +117 -0
  143. pyreduce/trace.py +979 -0
  144. pyreduce/util.py +1366 -0
  145. pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
  146. pyreduce/wavecal/atlas/thar.fits +4946 -13
  147. pyreduce/wavecal/atlas/thar_list.txt +4172 -0
  148. pyreduce/wavecal/atlas/une.fits +0 -0
  149. pyreduce/wavecal/convert.py +38 -0
  150. pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
  151. pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
  152. pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
  153. pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
  154. pyreduce/wavecal/harps_blue_2D.npz +0 -0
  155. pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
  156. pyreduce/wavecal/harps_red_2D.npz +0 -0
  157. pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
  158. pyreduce/wavecal/mcdonald.npz +0 -0
  159. pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
  160. pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
  161. pyreduce/wavecal/nirspec_K2.npz +0 -0
  162. pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
  163. pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
  164. pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
  165. pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
  166. pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
  167. pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
  168. pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
  169. pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
  170. pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
  171. pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
  172. pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
  173. pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
  174. pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
  175. pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
  176. pyreduce/wavecal/xshooter_nir.npz +0 -0
  177. pyreduce/wavelength_calibration.py +1871 -0
  178. pyreduce_astro-0.7a4.dist-info/METADATA +106 -0
  179. pyreduce_astro-0.7a4.dist-info/RECORD +182 -0
  180. pyreduce_astro-0.7a4.dist-info/WHEEL +4 -0
  181. pyreduce_astro-0.7a4.dist-info/entry_points.txt +2 -0
  182. 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