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,62 @@
1
+ # METIS IFU instrument configuration
2
+
3
+ __instrument__: METIS_IFU
4
+ instrument: INSTRUME
5
+ id_instrument: METIS_IFU
6
+ telescope: ELT
7
+
8
+ date: MJD-OBS
9
+ date_format: mjd
10
+
11
+ arms: [IFU_NOMINAL, IFU_EXTENDED]
12
+ kw_arm: INSTMODE
13
+ id_arm: [ifu_nom, ifu_ext]
14
+ extension: 0
15
+ orientation: [1, 1, 1]
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: 4
26
+ dark: 10
27
+ exposure_time: "HIERARCH OBS_EXPTIME"
28
+
29
+ ra: RA
30
+ dec: DEC
31
+ jd: MJD-OBS
32
+ longitude: -70
33
+ latitude: -24
34
+ altitude: 3060
35
+ instrument_mode: "HIERARCH ESO INS MODE"
36
+ observation_type: "HIERARCH ESO DPR TYPE"
37
+ observation_category: "HIERARCH ESO DPR CATG"
38
+ target: ""
39
+ object: OBJECT
40
+
41
+ id_dark: DARK
42
+ id_format: "LAMP,FMTCHK"
43
+ id_tell: "STD,TELLURIC"
44
+
45
+ # File classification keywords and patterns
46
+ kw_bias: "HIERARCH ESO DPR TYPE"
47
+ kw_flat: "HIERARCH ESO DPR TYPE"
48
+ kw_curvature: "HIERARCH ESO DPR TYPE"
49
+ kw_scatter: "HIERARCH ESO DPR TYPE"
50
+ kw_orders: "HIERARCH ESO DPR TYPE"
51
+ kw_wave: "HIERARCH ESO DPR TYPE"
52
+ kw_comb: null
53
+ kw_spec: "HIERARCH ESO DPR TYPE"
54
+
55
+ id_bias: DARK
56
+ id_flat: "LAMP,FLAT"
57
+ id_orders: "FLAT,PINHOLE"
58
+ id_curvature: "SKY,WAVE"
59
+ id_scatter: "LAMP,FLAT"
60
+ id_wave: "SKY,WAVE"
61
+ id_comb: null
62
+ id_spec: OBJECT
@@ -0,0 +1,45 @@
1
+ """
2
+ Handles instrument specific info for the METIS_LSS LSS spectrograph
3
+
4
+ Mostly reading data from the header
5
+ """
6
+
7
+ import logging
8
+ import os.path
9
+
10
+ from .common import Instrument
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class METIS_LSS(Instrument):
16
+ def add_header_info(self, header, arm, **kwargs):
17
+ """read data from header and add it as REDUCE keyword back to the header"""
18
+ # "Normal" stuff is handled by the general version, specific changes to values happen here
19
+ # alternatively you can implement all of it here, whatever works
20
+ header = super().add_header_info(header, arm)
21
+
22
+ # header["e_backg"] = (
23
+ # header["e_readn"] + header["e_exptime"] * header["e_drk"] / 3600
24
+ # )
25
+ #
26
+ # header["e_ra"] /= 15
27
+ # if header["e_jd"] is not None:
28
+ # header["e_jd"] += header["e_exptime"] / 2 / 3600 / 24 + 0.5
29
+
30
+ return header
31
+
32
+ def get_extension(self, header, arm):
33
+ extension = 1
34
+
35
+ return extension
36
+
37
+ def get_wavecal_filename(self, header, arm, **kwargs):
38
+ """Get the filename of the wavelength calibration config file"""
39
+ # info = self.load_info()
40
+ cwd = os.path.dirname(__file__)
41
+ fname = f"metis_lss_{arm.lower()}_2D.npz"
42
+ # fname = f"metis_lss_LSS_L_2D.npz" ## f"micado_IJ_2D_det1.npz"
43
+ fname = os.path.join(cwd, "..", "wavecal", fname)
44
+
45
+ return fname
@@ -0,0 +1,62 @@
1
+ # METIS LSS instrument configuration
2
+
3
+ __instrument__: METIS_LSS
4
+ instrument: INSTRUME
5
+ id_instrument: METIS_LSS
6
+ telescope: ELT
7
+
8
+ date: MJD-OBS
9
+ date_format: mjd
10
+
11
+ arms: [LSS_L, LSS_M, LSS_N]
12
+ kw_arm: INSTMODE
13
+ id_arm: [lss_l, lss_m, lss_n]
14
+ extension: 0
15
+ orientation: [1, 1, 1]
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: 4
26
+ dark: 10
27
+ exposure_time: "HIERARCH OBS_EXPTIME"
28
+
29
+ ra: RA
30
+ dec: DEC
31
+ jd: MJD-OBS
32
+ longitude: -70
33
+ latitude: -24
34
+ altitude: 3060
35
+ instrument_mode: "HIERARCH ESO INS MODE"
36
+ observation_type: "HIERARCH ESO DPR TYPE"
37
+ observation_category: "HIERARCH ESO DPR CATG"
38
+ target: ""
39
+ object: OBJECT
40
+
41
+ id_dark: DARK
42
+ id_format: "LAMP,FMTCHK"
43
+ id_tell: "STD,TELLURIC"
44
+
45
+ # File classification keywords and patterns
46
+ kw_bias: "HIERARCH ESO DPR TYPE"
47
+ kw_flat: "HIERARCH ESO DPR TYPE"
48
+ kw_curvature: "HIERARCH ESO DPR TYPE"
49
+ kw_scatter: "HIERARCH ESO DPR TYPE"
50
+ kw_orders: "HIERARCH ESO DPR TYPE"
51
+ kw_wave: "HIERARCH ESO DPR TYPE"
52
+ kw_comb: null
53
+ kw_spec: "HIERARCH ESO DPR TYPE"
54
+
55
+ id_bias: DARK
56
+ id_flat: "LAMP,FLAT"
57
+ id_orders: "FLAT,PINHOLE"
58
+ id_curvature: "SKY,WAVE"
59
+ id_scatter: "LAMP,FLAT"
60
+ id_wave: "SKY,WAVE"
61
+ id_comb: null
62
+ id_spec: OBJECT
@@ -0,0 +1,45 @@
1
+ """
2
+ Handles instrument specific info for the MICADO spectrograph
3
+
4
+ Mostly reading data from the header
5
+ """
6
+
7
+ import logging
8
+ import os.path
9
+
10
+ from .common import Instrument
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class MICADO(Instrument):
16
+ def add_header_info(self, header, arm, **kwargs):
17
+ """read data from header and add it as REDUCE keyword back to the header"""
18
+ # "Normal" stuff is handled by the general version, specific changes to values happen here
19
+ # alternatively you can implement all of it here, whatever works
20
+ header = super().add_header_info(header, arm)
21
+
22
+ # header["e_backg"] = (
23
+ # header["e_readn"] + header["e_exptime"] * header["e_drk"] / 3600
24
+ # )
25
+ #
26
+ # header["e_ra"] /= 15
27
+ # if header["e_jd"] is not None:
28
+ # header["e_jd"] += header["e_exptime"] / 2 / 3600 / 24 + 0.5
29
+
30
+ return header
31
+
32
+ def get_extension(self, header, arm):
33
+ extension = 5
34
+
35
+ return extension
36
+
37
+ def get_wavecal_filename(self, header, arm, **kwargs):
38
+ """Get the filename of the wavelength calibration config file"""
39
+ # info = self.load_info()
40
+ cwd = os.path.dirname(__file__)
41
+ # fname = f"xshooter_{arm.lower()}.npz"
42
+ fname = "MICADO_HK_3arcsec_chip5.npz" ## f"micado_IJ_2D_det1.npz"
43
+ fname = os.path.join(cwd, "..", "wavecal", fname)
44
+
45
+ return fname
@@ -0,0 +1,62 @@
1
+ # MICADO instrument configuration
2
+
3
+ __instrument__: MICADO
4
+ instrument: INSTRUME
5
+ id_instrument: MICADO
6
+ telescope: ELT
7
+
8
+ date: MJD-OBS
9
+ date_format: mjd
10
+
11
+ arms: [NIR]
12
+ kw_arm: "ESO DET CHIP*NAME"
13
+ id_arm: [Hawaii4RG]
14
+ extension: 0
15
+ orientation: [1]
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: 4
26
+ dark: 10
27
+ exposure_time: "HIERARCH OBS_EXPTIME"
28
+
29
+ ra: RA
30
+ dec: DEC
31
+ jd: MJD-OBS
32
+ longitude: -70
33
+ latitude: -24
34
+ altitude: 3060
35
+ instrument_mode: "HIERARCH ESO SEQ ARM"
36
+ observation_type: "HIERARCH ESO DPR TYPE"
37
+ observation_category: "HIERARCH ESO DPR CATG"
38
+ target: ""
39
+ object: OBJECT
40
+
41
+ id_dark: DARK
42
+ id_format: WAVE_PINH
43
+ id_tell: "STD,TELLURIC"
44
+
45
+ # File classification keywords and patterns
46
+ kw_bias: "HIERARCH ESO DPR TYPE"
47
+ kw_flat: "HIERARCH ESO DPR TYPE"
48
+ kw_curvature: "HIERARCH ESO DPR TYPE"
49
+ kw_scatter: "HIERARCH ESO DPR TYPE"
50
+ kw_orders: "HIERARCH ESO DPR TYPE"
51
+ kw_wave: "HIERARCH ESO DPR TYPE"
52
+ kw_comb: null
53
+ kw_spec: "HIERARCH ESO DPR TYPE"
54
+
55
+ id_bias: DARK
56
+ id_flat: SFLATSLIT
57
+ id_orders: SFLAT_PINH
58
+ id_curvature: WAVE
59
+ id_scatter: "LAMP,FLAT"
60
+ id_wave: WAVE
61
+ id_comb: null
62
+ id_spec: OBJECT
@@ -0,0 +1,257 @@
1
+ """Pydantic models for instrument configuration validation.
2
+
3
+ These models provide type-safe validation for instrument configuration files.
4
+ Currently validates the flat YAML structure; will evolve toward the nested
5
+ structure described in REDESIGN.md.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel, ConfigDict, field_validator
13
+
14
+
15
+ class HeaderRef(BaseModel):
16
+ """Reference to a FITS header keyword."""
17
+
18
+ key: str
19
+
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+
23
+ # Type for values that can be either a literal or a header reference
24
+ HeaderOrValue = float | int | str | HeaderRef | None
25
+
26
+
27
+ class FileClassification(BaseModel):
28
+ """Keywords and patterns for file type classification."""
29
+
30
+ kw_bias: str | None = None
31
+ kw_flat: str | None = None
32
+ kw_curvature: str | None = None
33
+ kw_scatter: str | None = None
34
+ kw_orders: str | None = None
35
+ kw_wave: str | None = None
36
+ kw_comb: str | None = None
37
+ kw_spec: str | None = None
38
+
39
+ id_bias: str | None = None
40
+ id_flat: str | None = None
41
+ id_curvature: str | None = None
42
+ id_scatter: str | None = None
43
+ id_orders: str | None = None
44
+ id_wave: str | None = None
45
+ id_comb: str | None = None
46
+ id_spec: str | None = None
47
+
48
+ model_config = ConfigDict(extra="allow")
49
+
50
+
51
+ class InstrumentConfig(BaseModel):
52
+ """Configuration for an astronomical instrument.
53
+
54
+ This model validates the flat YAML structure used by instrument configs.
55
+ It allows extra fields for instrument-specific parameters.
56
+ """
57
+
58
+ # Required identification
59
+ __instrument__: str | None = None # Internal name (uses alias due to dunder)
60
+ instrument: str # Header keyword for instrument name
61
+ id_instrument: str # Value/pattern to match
62
+
63
+ # Telescope
64
+ telescope: str | None = None
65
+
66
+ # Date handling
67
+ date: str = "DATE-OBS"
68
+ date_format: str = "fits"
69
+
70
+ # Arm system (detectors/channels)
71
+ arms: list[str] | None = None
72
+ arms_id: list[str] | None = None
73
+ kw_arm: str | None = None
74
+ id_arm: list[str] | None = None
75
+ extension: int | str | list[int | str] = 0
76
+ orientation: int | list[int] = 0
77
+ transpose: bool = False
78
+
79
+ # Detector dimensions
80
+ naxis_x: str | int = "NAXIS1"
81
+ naxis_y: str | int = "NAXIS2"
82
+
83
+ # Overscan/prescan regions
84
+ prescan_x: int | str = 0
85
+ overscan_x: int | str = 0
86
+ prescan_y: int | str = 0
87
+ overscan_y: int | str = 0
88
+
89
+ # Calibration values (can be literals or header keywords)
90
+ gain: float | int | str = 1
91
+ readnoise: float | int | str = 0
92
+ dark: float | int | str = 0
93
+ sky: float | int | str = 0
94
+ exposure_time: str = "EXPTIME"
95
+
96
+ # Location (for barycentric correction)
97
+ ra: str | None = "RA"
98
+ dec: str | None = "DEC"
99
+ longitude: float | str | None = None
100
+ latitude: float | str | None = None
101
+ altitude: float | str | None = None
102
+
103
+ # Target identification
104
+ target: str = "OBJECT"
105
+ observation_type: str | None = None
106
+ category: str | None = None
107
+ image_type: str | None = None
108
+ instrument_mode: str | None = None
109
+
110
+ # File classification - header keywords
111
+ kw_bias: str | None = None
112
+ kw_flat: str | None = None
113
+ kw_curvature: str | None = None
114
+ kw_scatter: str | None = None
115
+ kw_orders: str | None = None
116
+ kw_wave: str | None = None
117
+ kw_comb: str | None = None
118
+ kw_spec: str | None = None
119
+
120
+ # File classification - identifier patterns
121
+ id_bias: str | None = None
122
+ id_flat: str | None = None
123
+ id_curvature: str | None = None
124
+ id_scatter: str | None = None
125
+ id_orders: str | None = None
126
+ id_wave: str | None = None
127
+ id_comb: str | None = None
128
+ id_spec: str | None = None
129
+
130
+ # Wavelength information
131
+ wavelength_range: list | None = None
132
+ wavecal_specifier: str | None = None
133
+
134
+ # Allow additional fields for instrument-specific parameters
135
+ model_config = ConfigDict(
136
+ extra="allow",
137
+ populate_by_name=True,
138
+ )
139
+
140
+ @field_validator("arms", "arms_id", "id_arm", mode="before")
141
+ @classmethod
142
+ def ensure_list(cls, v):
143
+ """Convert single values to lists."""
144
+ if v is None:
145
+ return None
146
+ if isinstance(v, str):
147
+ return [v]
148
+ return v
149
+
150
+ @field_validator("extension", "orientation", mode="before")
151
+ @classmethod
152
+ def normalize_list_or_scalar(cls, v):
153
+ """Keep as-is, validation handles both forms."""
154
+ return v
155
+
156
+
157
+ # Future models for the nested structure (REDESIGN.md)
158
+ # These will be used when migrating to the new architecture
159
+
160
+
161
+ class AmplifierConfig(BaseModel):
162
+ """Configuration for a detector readout amplifier."""
163
+
164
+ id: str
165
+ gain: float | dict[str, str] # literal or {key: "HEADER_KEY"}
166
+ readnoise: float | dict[str, str]
167
+ region: dict[str, list[int]] | None = None
168
+ linearity: dict | None = None
169
+ bad_pixel_mask: str | None = None
170
+
171
+ model_config = ConfigDict(extra="forbid")
172
+
173
+
174
+ class DetectorConfig(BaseModel):
175
+ """Configuration for a physical detector."""
176
+
177
+ name: str
178
+ naxis: tuple[int, int] | list[int]
179
+ orientation: int = 0
180
+ prescan: dict[str, list[int] | None] | None = None
181
+ overscan: dict[str, list[int] | None] | None = None
182
+ amplifiers: list[AmplifierConfig] = []
183
+ bad_pixel_mask: str | None = None
184
+
185
+ model_config = ConfigDict(extra="forbid")
186
+
187
+
188
+ class BeamArmConfig(BaseModel):
189
+ """Configuration for a beam-splitter arm."""
190
+
191
+ name: str
192
+ polarization: str | None = None
193
+ wavelength_shift: float = 0.0
194
+ trace_offset: float = 0.0
195
+
196
+ model_config = ConfigDict(extra="forbid")
197
+
198
+
199
+ class OpticalPathConfig(BaseModel):
200
+ """Configuration for an optical path (fiber, slit position)."""
201
+
202
+ name: str
203
+ beam_arms: list[BeamArmConfig] | None = None
204
+
205
+ model_config = ConfigDict(extra="forbid")
206
+
207
+
208
+ class DimensionConfig(BaseModel):
209
+ """Configuration for a varying dimension (mode, fiber, etc.)."""
210
+
211
+ values: list[str]
212
+ header_key: str | None = None
213
+ optional: bool = False
214
+
215
+ model_config = ConfigDict(extra="forbid")
216
+
217
+
218
+ class InstrumentConfigV2(BaseModel):
219
+ """Future nested instrument configuration structure.
220
+
221
+ This model represents the target architecture from REDESIGN.md.
222
+ Not yet used - will be activated when migrating instruments.
223
+ """
224
+
225
+ instrument: str
226
+ telescope: str | None = None
227
+ id_instrument: str
228
+
229
+ detectors: list[DetectorConfig] = []
230
+ optical_paths: list[OpticalPathConfig] = []
231
+ dimensions: dict[str, DimensionConfig] = {}
232
+
233
+ headers: dict[str, str] = {}
234
+ file_types: dict[str, dict[str, str]] = {}
235
+
236
+ model_config = ConfigDict(extra="allow")
237
+
238
+
239
+ def validate_instrument_config(data: dict[str, Any]) -> InstrumentConfig:
240
+ """Validate instrument configuration data.
241
+
242
+ Parameters
243
+ ----------
244
+ data : dict
245
+ Raw configuration data (from YAML or JSON)
246
+
247
+ Returns
248
+ -------
249
+ InstrumentConfig
250
+ Validated configuration
251
+
252
+ Raises
253
+ ------
254
+ pydantic.ValidationError
255
+ If validation fails
256
+ """
257
+ return InstrumentConfig(**data)
@@ -0,0 +1,156 @@
1
+ """
2
+ Handles instrument specific info for the NEID spectrograph
3
+ """
4
+
5
+ import logging
6
+ import re
7
+ from os.path import dirname, join
8
+
9
+ from .common import Instrument
10
+ from .filters import Filter, InstrumentFilter, NightFilter, ObjectFilter
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class NEID(Instrument):
16
+ def __init__(self):
17
+ super().__init__()
18
+ self.filters = {
19
+ "instrument": InstrumentFilter(self.config.instrument),
20
+ "night": NightFilter(self.config.date),
21
+ # "branch": Filter(, regex=True),
22
+ "mode": Filter(
23
+ self.config.instrument_mode, regex=True, flags=re.IGNORECASE
24
+ ),
25
+ "type": Filter(self.config.observation_type),
26
+ "target": ObjectFilter(self.config.target, regex=True),
27
+ }
28
+ self.night = "night"
29
+ self.science = "science"
30
+ self.shared = [
31
+ "instrument",
32
+ "night",
33
+ "mode",
34
+ ]
35
+ self.find_closest = [
36
+ "bias",
37
+ "flat",
38
+ "wavecal_master",
39
+ "freq_comb_master",
40
+ "orders",
41
+ "scatter",
42
+ ]
43
+
44
+ def get_expected_values(
45
+ self, target, night, arm=None, mode=None, fiber=None, **kwargs
46
+ ):
47
+ """Determine the default expected values in the headers for a given observation configuration
48
+
49
+ Any parameter may be None, to indicate that all values are allowed
50
+
51
+ Parameters
52
+ ----------
53
+ target : str
54
+ Name of the star / observation target
55
+ night : str
56
+ Observation night/nights
57
+ Returns
58
+ -------
59
+ expectations: dict
60
+ Dictionary of expected header values, with one entry per step.
61
+ The entries for each step refer to the filters defined in self.filters
62
+
63
+ Raises
64
+ ------
65
+ ValueError
66
+ Invalid combination of parameters
67
+ """
68
+ if target is not None:
69
+ target = target.replace(" ", r"(?:\s*|-)")
70
+ else:
71
+ target = ".*"
72
+
73
+ id_orddef = "LAMP,DARK,TUN"
74
+ id_spec = "STAR,WAVE"
75
+
76
+ expectations = {
77
+ "flat": {"instrument": "NEID", "night": night, "type": r"LAMP,LAMP,TUN"},
78
+ "orders": {
79
+ "instrument": "NEID",
80
+ "night": night,
81
+ "type": id_orddef,
82
+ },
83
+ "scatter": {
84
+ "instrument": "NEID",
85
+ "night": night,
86
+ "type": id_orddef, # Same as orders or same as flat?
87
+ },
88
+ "wavecal_master": {
89
+ "instrument": "NEID",
90
+ "night": night,
91
+ "type": r"WAVE,WAVE,THAR2",
92
+ },
93
+ "freq_comb_master": {
94
+ "instrument": "NEID",
95
+ "night": night,
96
+ "type": r"WAVE,WAVE,COMB",
97
+ },
98
+ "science": {
99
+ "instrument": "NEID",
100
+ "night": night,
101
+ "mode": mode,
102
+ "type": id_spec,
103
+ "target": target,
104
+ },
105
+ }
106
+ return expectations
107
+
108
+ def get_extension(self, header, arm):
109
+ extension = super().get_extension(header, arm)
110
+
111
+ return extension
112
+
113
+ def add_header_info(self, header, arm, **kwargs):
114
+ """read data from header and add it as REDUCE keyword back to the header"""
115
+ # "Normal" stuff is handled by the general version, specific changes to values happen here
116
+ # alternatively you can implement all of it here, whatever works
117
+ header = super().add_header_info(header, arm)
118
+
119
+ try:
120
+ header["e_ra"] /= 15
121
+ header["e_jd"] += header["e_exptim"] / (7200 * 24) + 0.5
122
+
123
+ except:
124
+ pass
125
+
126
+ try:
127
+ if (
128
+ header["NAXIS"] == 2
129
+ and header["NAXIS1"] == 4296
130
+ and header["NAXIS2"] == 4096
131
+ ):
132
+ # both arms are in the same image
133
+ prescan_x = 50
134
+ overscan_x = 50
135
+ naxis_x = 2148
136
+ if arm == "BLUE":
137
+ header["e_xlo"] = prescan_x
138
+ header["e_xhi"] = naxis_x - overscan_x
139
+ elif arm == "RED":
140
+ header["e_xlo"] = naxis_x + prescan_x
141
+ header["e_xhi"] = 2 * naxis_x - overscan_x
142
+ except KeyError:
143
+ pass
144
+
145
+ return header
146
+
147
+ def get_wavecal_filename(self, header, arm, **kwargs):
148
+ """Get the filename of the wavelength calibration config file"""
149
+ cwd = dirname(__file__)
150
+ fname = f"NEID_{arm.lower()}_2D.npz"
151
+ fname = join(cwd, "..", "wavecal", fname)
152
+ return fname
153
+
154
+ def get_wavelength_range(self, header, arm, **kwargs):
155
+ wave_range = super().get_wavelength_range(header, arm, **kwargs)
156
+ return wave_range