pyreduce-astro 0.6.0__cp312-cp312-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.
Files changed (154) hide show
  1. pyreduce/__init__.py +67 -0
  2. pyreduce/__main__.py +106 -0
  3. pyreduce/clib/__init__.py +0 -0
  4. pyreduce/clib/_slitfunc_2d.cpython-311-darwin.so +0 -0
  5. pyreduce/clib/_slitfunc_2d.cpython-312-darwin.so +0 -0
  6. pyreduce/clib/_slitfunc_bd.cpython-311-darwin.so +0 -0
  7. pyreduce/clib/_slitfunc_bd.cpython-312-darwin.so +0 -0
  8. pyreduce/clib/build_extract.py +75 -0
  9. pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
  10. pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
  11. pyreduce/clib/slit_func_bd.c +362 -0
  12. pyreduce/clib/slit_func_bd.h +17 -0
  13. pyreduce/clipnflip.py +147 -0
  14. pyreduce/combine_frames.py +855 -0
  15. pyreduce/configuration.py +186 -0
  16. pyreduce/continuum_normalization.py +329 -0
  17. pyreduce/cwrappers.py +404 -0
  18. pyreduce/datasets.py +231 -0
  19. pyreduce/echelle.py +413 -0
  20. pyreduce/estimate_background_scatter.py +129 -0
  21. pyreduce/extract.py +1361 -0
  22. pyreduce/extraction_width.py +77 -0
  23. pyreduce/instruments/__init__.py +0 -0
  24. pyreduce/instruments/andes.json +61 -0
  25. pyreduce/instruments/andes.py +102 -0
  26. pyreduce/instruments/common.json +46 -0
  27. pyreduce/instruments/common.py +683 -0
  28. pyreduce/instruments/crires_plus.json +63 -0
  29. pyreduce/instruments/crires_plus.py +103 -0
  30. pyreduce/instruments/filters.py +195 -0
  31. pyreduce/instruments/harpn.json +136 -0
  32. pyreduce/instruments/harpn.py +201 -0
  33. pyreduce/instruments/harps.json +155 -0
  34. pyreduce/instruments/harps.py +310 -0
  35. pyreduce/instruments/instrument_info.py +140 -0
  36. pyreduce/instruments/instrument_schema.json +318 -0
  37. pyreduce/instruments/jwst_miri.json +53 -0
  38. pyreduce/instruments/jwst_miri.py +29 -0
  39. pyreduce/instruments/jwst_niriss.json +52 -0
  40. pyreduce/instruments/jwst_niriss.py +98 -0
  41. pyreduce/instruments/lick_apf.json +53 -0
  42. pyreduce/instruments/lick_apf.py +35 -0
  43. pyreduce/instruments/mcdonald.json +59 -0
  44. pyreduce/instruments/mcdonald.py +123 -0
  45. pyreduce/instruments/metis_ifu.json +63 -0
  46. pyreduce/instruments/metis_ifu.py +45 -0
  47. pyreduce/instruments/metis_lss.json +65 -0
  48. pyreduce/instruments/metis_lss.py +45 -0
  49. pyreduce/instruments/micado.json +53 -0
  50. pyreduce/instruments/micado.py +45 -0
  51. pyreduce/instruments/neid.json +51 -0
  52. pyreduce/instruments/neid.py +154 -0
  53. pyreduce/instruments/nirspec.json +56 -0
  54. pyreduce/instruments/nirspec.py +215 -0
  55. pyreduce/instruments/nte.json +47 -0
  56. pyreduce/instruments/nte.py +42 -0
  57. pyreduce/instruments/uves.json +59 -0
  58. pyreduce/instruments/uves.py +46 -0
  59. pyreduce/instruments/xshooter.json +66 -0
  60. pyreduce/instruments/xshooter.py +39 -0
  61. pyreduce/make_shear.py +606 -0
  62. pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
  63. pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
  64. pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
  65. pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
  66. pyreduce/masks/mask_elodie.fits.gz +0 -0
  67. pyreduce/masks/mask_feros3.fits.gz +0 -0
  68. pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
  69. pyreduce/masks/mask_harps_blue.fits.gz +0 -0
  70. pyreduce/masks/mask_harps_red.fits.gz +0 -0
  71. pyreduce/masks/mask_hds_blue.fits.gz +0 -0
  72. pyreduce/masks/mask_hds_red.fits.gz +0 -0
  73. pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
  74. pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
  75. pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
  76. pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
  77. pyreduce/masks/mask_mcdonald.fits.gz +0 -0
  78. pyreduce/masks/mask_nes.fits.gz +0 -0
  79. pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
  80. pyreduce/masks/mask_sarg.fits.gz +0 -0
  81. pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
  82. pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
  83. pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
  84. pyreduce/masks/mask_uves_blue.fits.gz +0 -0
  85. pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
  86. pyreduce/masks/mask_uves_middle.fits.gz +0 -0
  87. pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
  88. pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
  89. pyreduce/masks/mask_uves_red.fits.gz +0 -0
  90. pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
  91. pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
  92. pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
  93. pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
  94. pyreduce/rectify.py +138 -0
  95. pyreduce/reduce.py +2205 -0
  96. pyreduce/settings/settings_ANDES.json +89 -0
  97. pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
  98. pyreduce/settings/settings_HARPN.json +73 -0
  99. pyreduce/settings/settings_HARPS.json +69 -0
  100. pyreduce/settings/settings_JWST_MIRI.json +55 -0
  101. pyreduce/settings/settings_JWST_NIRISS.json +55 -0
  102. pyreduce/settings/settings_LICK_APF.json +62 -0
  103. pyreduce/settings/settings_MCDONALD.json +58 -0
  104. pyreduce/settings/settings_METIS_IFU.json +77 -0
  105. pyreduce/settings/settings_METIS_LSS.json +77 -0
  106. pyreduce/settings/settings_MICADO.json +78 -0
  107. pyreduce/settings/settings_NEID.json +73 -0
  108. pyreduce/settings/settings_NIRSPEC.json +58 -0
  109. pyreduce/settings/settings_NTE.json +60 -0
  110. pyreduce/settings/settings_UVES.json +54 -0
  111. pyreduce/settings/settings_XSHOOTER.json +78 -0
  112. pyreduce/settings/settings_pyreduce.json +178 -0
  113. pyreduce/settings/settings_schema.json +827 -0
  114. pyreduce/tools/__init__.py +0 -0
  115. pyreduce/tools/combine.py +117 -0
  116. pyreduce/trace_orders.py +645 -0
  117. pyreduce/util.py +1288 -0
  118. pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
  119. pyreduce/wavecal/atlas/thar.fits +4946 -13
  120. pyreduce/wavecal/atlas/thar_list.txt +4172 -0
  121. pyreduce/wavecal/atlas/une.fits +0 -0
  122. pyreduce/wavecal/convert.py +38 -0
  123. pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
  124. pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
  125. pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
  126. pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
  127. pyreduce/wavecal/harps_blue_2D.npz +0 -0
  128. pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
  129. pyreduce/wavecal/harps_red_2D.npz +0 -0
  130. pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
  131. pyreduce/wavecal/mcdonald.npz +0 -0
  132. pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
  133. pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
  134. pyreduce/wavecal/nirspec_K2.npz +0 -0
  135. pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
  136. pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
  137. pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
  138. pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
  139. pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
  140. pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
  141. pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
  142. pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
  143. pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
  144. pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
  145. pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
  146. pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
  147. pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
  148. pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
  149. pyreduce/wavecal/xshooter_nir.npz +0 -0
  150. pyreduce/wavelength_calibration.py +1873 -0
  151. pyreduce_astro-0.6.0.dist-info/METADATA +114 -0
  152. pyreduce_astro-0.6.0.dist-info/RECORD +154 -0
  153. pyreduce_astro-0.6.0.dist-info/WHEEL +6 -0
  154. pyreduce_astro-0.6.0.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,63 @@
1
+ {
2
+ "__comment__": "The upgraded CRIRES at ESO VLT, a cross-dispersed IR spectrograph for YJHKLM bands.",
3
+ "__instrument__": "CRIRES_PLUS",
4
+ "instrument": "INSTRUME",
5
+ "id_instrument": "CRIRES",
6
+ "telescope": "VLT",
7
+ "date": "DATE-OBS",
8
+ "date_format": "fits",
9
+ "id_mode": "ESO INS MODE",
10
+ "id_band": "ESO INS WLEN ID",
11
+ "id_decker": "ESO INS OPTI8 ID",
12
+ "id_lamp": "ESO INS1 LAMP? ID",
13
+ "modes": ["J1228"],
14
+ "deckers": ["OPEN", "pos1", "pos2"],
15
+ "bands": ["Y", "J", "H", "K", "L", "M"],
16
+ "settings": ["Y1029", "Y1028", "J1232", "J1228", "J1226", "H1582", "H1575", "H1567", "H1559", "K2217", "K2192", "K2166", "K2148", "L3426", "L3412", "L3377", "L3340", "L3302", "L3262", "L3244", "M4519", "M4504", "M4461", "M4416", "M4368", "M4318", "M4266", "M4211", "M4187"],
17
+ "chips": ["det1", "det2", "det3"],
18
+ "extension": "CHIP1.INT1",
19
+ "orientation": 0,
20
+ "transpose": false,
21
+ "prescan_x": 5,
22
+ "overscan_x": 5,
23
+ "prescan_y": 5,
24
+ "overscan_y": 5,
25
+ "naxis_x": "NAXIS1",
26
+ "naxis_y": "NAXIS2",
27
+ "gain": "HIERARCH ESO DET CHIP GAIN",
28
+ "readnoise": "HIERARCH ESO DET CHIP RON",
29
+ "dark": "HIERARCH ESO DET DIT",
30
+ "sky": 0,
31
+ "exposure_time": "EXPTIME",
32
+ "image_type": "OBJECT",
33
+ "category": "HIERARCH ESO DPR CATG",
34
+ "ra": "RA",
35
+ "dec": "DEC",
36
+ "jd": "MJD-OBS",
37
+ "longitude": "HIERARCH ESO TEL GEOLON",
38
+ "latitude": "HIERARCH ESO TEL GEOLAT",
39
+ "altitude": "HIERARCH ESO TEL GEOELEV",
40
+ "target": "OBJECT",
41
+ "observation_type": "ESO DPR TYPE",
42
+
43
+ "id_detcheck": "FLAT,LAMP,DETCHECK",
44
+ "id_lamp_wavecal" : "UNe_HCL",
45
+ "id_lamp_etalon": "Etalon_Halogen",
46
+
47
+ "kw_bias" : "ESO DPR TYPE",
48
+ "kw_flat" : "ESO DPR TYPE",
49
+ "kw_curvature": "ESO DPR TYPE",
50
+ "kw_scatter": "ESO DPR TYPE",
51
+ "kw_orders" : "ESO DPR TYPE",
52
+ "kw_wave": "ESO DPR TYPE",
53
+ "kw_comb": "ESO DPR TYPE",
54
+ "kw_spec": "ESO DPR TYPE",
55
+ "id_bias": "DARK",
56
+ "id_flat": "FLAT",
57
+ "id_orders": "FLAT",
58
+ "id_curvature": "WAVE,FPET",
59
+ "id_scatter": "FLAT",
60
+ "id_wave": "WAVE,UNE",
61
+ "id_comb": "WAVE,FPET",
62
+ "id_spec": "STAR,*,*"
63
+ }
@@ -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, mode, **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_mode(mode)
33
+ header = super().add_header_info(header, band)
34
+ self.load_info()
35
+
36
+ return header
37
+
38
+ def get_supported_modes(self):
39
+ settings = self.info["settings"]
40
+ deckers = self.info["deckers"]
41
+ detectors = self.info["chips"]
42
+ modes = [
43
+ "_".join([s, d, c]) for s, d, c in product(settings, deckers, detectors)
44
+ ]
45
+ return modes
46
+
47
+ def parse_mode(self, mode):
48
+ pattern = r"([YJHKLM]\d{4})(_(Open|pos1|pos2))?_det(\d)"
49
+ match = re.match(pattern, mode, flags=re.IGNORECASE)
50
+ if not match:
51
+ logger.error("no mode 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, mode):
62
+ expectations = super().get_expected_values(target, night)
63
+ band, decker, detector = self.parse_mode(mode)
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, mode):
74
+ band, decker, detector = self.parse_mode(mode)
75
+ extension = int(detector)
76
+ return extension
77
+
78
+ def get_wavecal_filename(self, header, mode, **kwargs):
79
+ """Get the filename of the wavelength calibration config file"""
80
+ cwd = os.path.dirname(__file__)
81
+ fname = f"{self.name}_{mode}.npz"
82
+ fname = os.path.join(cwd, "..", "wavecal", fname)
83
+ return fname
84
+
85
+ def get_mask_filename(self, mode, **kwargs):
86
+ i = self.name.lower()
87
+ band, decker, detector = self.parse_mode(mode)
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, mode, **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,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 ModeFilter(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,136 @@
1
+ {
2
+ "__comment__": "red and middle are in the same fits file, with different extensions, i.e. share the same mode identifier, but have different extensions",
3
+ "__instrument__": "HARPN",
4
+ "id_instrument": "HARPN",
5
+ "instrument": "INSTRUME",
6
+ "telescope": "TELESCOP",
7
+ "date": "DATE-OBS",
8
+ "date_format": "fits",
9
+ "modes": ["HARPN"],
10
+ "modes_id" : ["harpn"],
11
+ "extension": [ 1 ],
12
+ "id": [
13
+ [
14
+ 1,
15
+ 2
16
+ ]
17
+ ],
18
+ "orientation": 2,
19
+ "transpose": true,
20
+ "prescan_x": "HIERARCH TNG DET OUT{id[0]} PRSCX",
21
+ "overscan_x": "HIERARCH TNG DET OUT{id[0]} OVSCX",
22
+ "prescan_y": 0,
23
+ "overscan_y": 0,
24
+ "naxis_x": "NAXIS1",
25
+ "naxis_y": "NAXIS2",
26
+ "gain": "HIERARCH TNG DET OUT{id[0]} GAIN",
27
+ "readnoise": "HIERARCH TNG DET OUT{id[0]} RON",
28
+ "dark": "HIERARCH TNG INS DET{id[1]} OFFDRK",
29
+ "sky": "HIERARCH TNG INS DET{id[1]} OFFSKY",
30
+ "exposure_time": "EXPTIME",
31
+ "image_type": "HIERARCH TNG OBS TARG NAME",
32
+ "category": "HIERARCH TNG DPR CATG",
33
+ "ra": "RA",
34
+ "dec": "DEC",
35
+ "longitude": "GEOLON",
36
+ "latitude": "GEOLAT",
37
+ "altitude": "GEOELEV",
38
+ "target": "OBJECT",
39
+ "instrument_mode": "HIERARCH TNG INS MODE",
40
+ "instrument_mode_alternative": "TNG TPL NAME",
41
+ "observation_type": "TNG DPR TYPE",
42
+ "id_fiber_a": "LAMP,DARK,TUN",
43
+ "id_fiber_b": "DARK,LAMP,TUN",
44
+ "kw_bias": "HIERARCH TNG DPR TYPE",
45
+ "kw_flat": "HIERARCH TNG DPR TYPE",
46
+ "kw_curvature": "HIERARCH TNG DPR TYPE",
47
+ "kw_scatter": "HIERARCH TNG DPR TYPE",
48
+ "kw_orders": "HIERARCH TNG DPR TYPE",
49
+ "kw_wave": "HIERARCH TNG DPR TYPE",
50
+ "kw_comb": "HIERARCH TNG DPR TYPE",
51
+ "kw_spec": "HIERARCH TNG DPR TYPE",
52
+ "id_bias": "BIAS,BIAS",
53
+ "id_flat": "LAMP,LAMP,TUN",
54
+ "id_orders": "LAMP,LAMP,TUN",
55
+ "id_curvature": "WAVE,WAVE,THAR2",
56
+ "id_scatter": "LAMP,LAMP,TUN",
57
+ "id_wave": "WAVE,WAVE,THAR2",
58
+ "id_comb": "WAVE,WAVE,COMB",
59
+ "id_spec": "STAR,*",
60
+ "wavelength_range": [[
61
+ [5245.4, 5304.3],
62
+ [5200.5, 5259.0],
63
+ [5156.5, 5214.4],
64
+ [5113.2, 5170.6],
65
+ [5070.6, 5127.6],
66
+ [5028.7, 5085.2],
67
+ [4987.5, 5043.5],
68
+ [4946.9, 5002.5],
69
+ [4907.0, 4962.2],
70
+ [4867.8, 4922.5],
71
+ [4829.2, 4883.4],
72
+ [4791.1, 4845.0],
73
+ [4753.7, 4807.1],
74
+ [4716.9, 4769.9],
75
+ [4680.6, 4733.2],
76
+ [4644.9, 4697.1],
77
+ [4609.7, 4661.5],
78
+ [4575.1, 4626.5],
79
+ [4540.9, 4591.9],
80
+ [4507.3, 4557.9],
81
+ [4474.2, 4524.4],
82
+ [4441.5, 4491.4],
83
+ [4409.3, 4458.9],
84
+ [4377.6, 4426.8],
85
+ [4346.4, 4395.2],
86
+ [4315.5, 4364.0],
87
+ [4285.2, 4333.3],
88
+ [4255.2, 4303.0],
89
+ [4225.7, 4273.1],
90
+ [4196.5, 4243.6],
91
+ [4167.8, 4214.6],
92
+ [4139.4, 4185.9],
93
+ [4111.5, 4157.6],
94
+ [4083.9, 4129.7],
95
+ [4056.7, 4102.2],
96
+ [4029.8, 4075.0],
97
+ [4003.3, 4048.2],
98
+ [3977.1, 4021.8],
99
+ [3951.3, 3995.7],
100
+ [3925.8, 3969.9],
101
+ [3900.7, 3944.5],
102
+ [3875.8, 3919.3],
103
+ [3851.3, 3894.5],
104
+ [3827.1, 3870.0],
105
+ [3803.2, 3845.9],
106
+ [3779.6, 3822.0]
107
+ ],[
108
+ [6835.9, 6913.0],
109
+ [6760.0, 6836.2],
110
+ [6685.7, 6761.1],
111
+ [6613.1, 6687.6],
112
+ [6542.0, 6615.7],
113
+ [6472.4, 6545.4],
114
+ [6404.3, 6476.5],
115
+ [6337.7, 6409.1],
116
+ [6272.3, 6343.0],
117
+ [6208.4, 6278.3],
118
+ [6145.7, 6214.9],
119
+ [6084.2, 6152.8],
120
+ [6024.0, 6091.9],
121
+ [5965.0, 6032.1],
122
+ [5907.1, 5973.6],
123
+ [5850.3, 5916.2],
124
+ [5794.6, 5859.8],
125
+ [5739.9, 5804.6],
126
+ [5686.3, 5750.3],
127
+ [5633.7, 5697.1],
128
+ [5582.0, 5644.8],
129
+ [5531.3, 5593.5],
130
+ [5481.5, 5543.2],
131
+ [5432.5, 5493.7],
132
+ [5384.5, 5445.1],
133
+ [5337.3, 5397.3]
134
+ ]
135
+ ]
136
+ }
@@ -0,0 +1,201 @@
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.info["instrument"]),
53
+ "night": NightFilter(self.info["date"]),
54
+ # "branch": Filter(, regex=True),
55
+ "mode": Filter(
56
+ self.info["instrument_mode"], regex=True, flags=re.IGNORECASE
57
+ ),
58
+ "type": TypeFilter(self.info["observation_type"]),
59
+ "target": ObjectFilter(self.info["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(self, target, night, mode, fiber):
78
+ """Determine the default expected values in the headers for a given observation configuration
79
+
80
+ Any parameter may be None, to indicate that all values are allowed
81
+
82
+ Parameters
83
+ ----------
84
+ target : str
85
+ Name of the star / observation target
86
+ night : str
87
+ Observation night/nights
88
+ Returns
89
+ -------
90
+ expectations: dict
91
+ Dictionary of expected header values, with one entry per step.
92
+ The entries for each step refer to the filters defined in self.filters
93
+
94
+ Raises
95
+ ------
96
+ ValueError
97
+ Invalid combination of parameters
98
+ """
99
+ if target is not None:
100
+ target = target.replace(" ", r"(?:\s*|-)")
101
+ else:
102
+ target = ".*"
103
+
104
+ id_orddef = "LAMP,DARK,TUN"
105
+ id_spec = "STAR,WAVE"
106
+
107
+ expectations = {
108
+ "bias": {"instrument": "HARPN", "night": night, "type": r"BIAS,BIAS"},
109
+ "flat": {"instrument": "HARPN", "night": night, "type": r"LAMP,LAMP,TUN"},
110
+ "orders": {
111
+ "instrument": "HARPN",
112
+ "night": night,
113
+ "type": id_orddef,
114
+ },
115
+ "scatter": {
116
+ "instrument": "HARPN",
117
+ "night": night,
118
+ "type": id_orddef, # Same as orders or same as flat?
119
+ },
120
+ "wavecal_master": {
121
+ "instrument": "HARPN",
122
+ "night": night,
123
+ "type": r"WAVE,WAVE,THAR2",
124
+ },
125
+ "freq_comb_master": {
126
+ "instrument": "HARPN",
127
+ "night": night,
128
+ "type": r"WAVE,WAVE,COMB",
129
+ },
130
+ "science": {
131
+ "instrument": "HARPN",
132
+ "night": night,
133
+ "mode": mode,
134
+ "type": id_spec,
135
+ "target": target,
136
+ },
137
+ }
138
+ return expectations
139
+
140
+ def get_extension(self, header, mode):
141
+ extension = super().get_extension(header, mode)
142
+
143
+ try:
144
+ if (
145
+ header["NAXIS"] == 2
146
+ and header["NAXIS1"] == 4296
147
+ and header["NAXIS2"] == 4096
148
+ ):
149
+ extension = 0
150
+ except KeyError:
151
+ pass
152
+
153
+ return extension
154
+
155
+ def add_header_info(self, header, mode, **kwargs):
156
+ """read data from header and add it as REDUCE keyword back to the header"""
157
+ # "Normal" stuff is handled by the general version, specific changes to values happen here
158
+ # alternatively you can implement all of it here, whatever works
159
+ header = super().add_header_info(header, mode)
160
+
161
+ try:
162
+ header["e_ra"] /= 15
163
+ header["e_jd"] += header["e_exptim"] / (7200 * 24) + 0.5
164
+
165
+ except:
166
+ pass
167
+
168
+ try:
169
+ if (
170
+ header["NAXIS"] == 2
171
+ and header["NAXIS1"] == 4296
172
+ and header["NAXIS2"] == 4096
173
+ ):
174
+ # both modes are in the same image
175
+ prescan_x = 50
176
+ overscan_x = 50
177
+ naxis_x = 2148
178
+ if mode == "BLUE":
179
+ header["e_xlo"] = prescan_x
180
+ header["e_xhi"] = naxis_x - overscan_x
181
+ elif mode == "RED":
182
+ header["e_xlo"] = naxis_x + prescan_x
183
+ header["e_xhi"] = 2 * naxis_x - overscan_x
184
+ except KeyError:
185
+ pass
186
+
187
+ return header
188
+
189
+ def get_wavecal_filename(self, header, mode, **kwargs):
190
+ """Get the filename of the wavelength calibration config file"""
191
+ cwd = dirname(__file__)
192
+ fname = f"harpn_{mode.lower()}_2D.npz"
193
+ fname = join(cwd, "..", "wavecal", fname)
194
+ return fname
195
+
196
+ def get_wavelength_range(self, header, mode, **kwargs):
197
+ wave_range = super().get_wavelength_range(header, mode, **kwargs)
198
+ # The wavelength orders are in inverse order in the .json file
199
+ # because I was to lazy to invert them in the file
200
+ wave_range = wave_range[::-1]
201
+ return wave_range