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,191 @@
1
+ """Loads configuration files
2
+
3
+ This module loads json configuration files from disk,
4
+ and combines them with the default settings,
5
+ to create one dict that contains all parameters.
6
+ It also checks that all parameters exists, and that
7
+ no new parameters have been added by accident.
8
+ """
9
+
10
+ import json
11
+ import logging
12
+ from os.path import dirname, join
13
+
14
+ import jsonschema
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ if int(jsonschema.__version__[0]) < 3: # pragma: no cover
19
+ logger.warning(
20
+ "Jsonschema %s found, but at least 3.0.0 is required to check configuration. Skipping the check.",
21
+ jsonschema.__version__,
22
+ )
23
+ hasJsonSchema = False
24
+ else:
25
+ hasJsonSchema = True
26
+
27
+
28
+ def get_configuration_for_instrument(instrument, **kwargs):
29
+ local = dirname(__file__)
30
+ instrument = str(instrument)
31
+ if instrument in ["pyreduce", None]:
32
+ fname = join(local, "settings", "settings_pyreduce.json")
33
+ else:
34
+ fname = join(local, "settings", f"settings_{instrument.upper()}.json")
35
+
36
+ config = load_config(fname, instrument)
37
+
38
+ for kwarg_key, kwarg_value in kwargs.items():
39
+ for key, _value in config.items():
40
+ if isinstance(config[key], dict) and kwarg_key in config[key].keys():
41
+ config[key][kwarg_key] = kwarg_value
42
+
43
+ return config
44
+
45
+
46
+ def load_config(configuration, instrument, j=0):
47
+ if configuration is None:
48
+ logger.info(
49
+ "No configuration specified, using default values for this instrument"
50
+ )
51
+ config = get_configuration_for_instrument(instrument, plot=False)
52
+ elif isinstance(configuration, dict):
53
+ if instrument in configuration.keys():
54
+ config = configuration[str(instrument)]
55
+ elif (
56
+ "__instrument__" in configuration.keys()
57
+ and configuration["__instrument__"] == str(instrument).upper()
58
+ ):
59
+ config = configuration
60
+ else:
61
+ raise KeyError("This configuration is for a different instrument")
62
+ elif isinstance(configuration, list):
63
+ config = configuration[j]
64
+ elif isinstance(configuration, str):
65
+ config = configuration
66
+
67
+ if isinstance(config, str):
68
+ logger.info("Loading configuration from %s", config)
69
+ try:
70
+ with open(config) as f:
71
+ config = json.load(f)
72
+ except FileNotFoundError:
73
+ fname = dirname(__file__)
74
+ fname = join(fname, "settings", config)
75
+ with open(fname) as f:
76
+ config = json.load(f)
77
+
78
+ # Combine instrument specific settings, with default values
79
+ settings = read_config()
80
+ settings = update(settings, config)
81
+
82
+ # If it doesn't raise an Exception everything is as expected
83
+ validate_config(settings)
84
+ logger.debug("Configuration succesfully validated")
85
+
86
+ return settings
87
+
88
+
89
+ def update(dict1, dict2, check=True, name="dict1"):
90
+ """
91
+ Update entries in dict1 with entries of dict2 recursively,
92
+ i.e. if the dict contains a dict value, values inside the dict will
93
+ also be updated
94
+
95
+ Parameters
96
+ ----------
97
+ dict1 : dict
98
+ dict that will be updated
99
+ dict2 : dict
100
+ dict that contains the values to update
101
+ check : bool
102
+ If True, will check that the keys from dict2 exist in dict1 already.
103
+ Except for those contained in field "instrument"
104
+
105
+ Returns
106
+ -------
107
+ dict1 : dict
108
+ the updated dict
109
+
110
+ Raises
111
+ ------
112
+ KeyError
113
+ If dict2 contains a key that is not in dict1
114
+ """
115
+ # Instrument is a 'special' section as it may include any number of values
116
+ # In that case we don't want to raise an error for new keys
117
+ exclude = ["instrument"]
118
+ for key, value in dict2.items():
119
+ if check and key not in dict1.keys():
120
+ logger.warning(f"{key} is not contained in {name}")
121
+ if isinstance(value, dict):
122
+ if dict1.get(key) is None:
123
+ dict1[key] = value
124
+ else:
125
+ dict1[key] = update(
126
+ dict1[key], value, check=key not in exclude, name=key
127
+ )
128
+ else:
129
+ dict1[key] = value
130
+ return dict1
131
+
132
+
133
+ def read_config(fname="settings_pyreduce.json"):
134
+ """Read the configuration file from disk
135
+
136
+ If no filename is given it will load the default configuration.
137
+ The configuration file must be a json file.
138
+
139
+ Parameters
140
+ ----------
141
+ fname : str, optional
142
+ Filename of the configuration. By default "settings_pyreduce.json",
143
+ i.e. the default configuration
144
+
145
+ Returns
146
+ -------
147
+ config : dict
148
+ The read configuration file
149
+ """
150
+ this_dir = dirname(__file__)
151
+ fname = join(this_dir, "settings", fname)
152
+
153
+ with open(fname) as file:
154
+ settings = json.load(file)
155
+ return settings
156
+
157
+
158
+ def validate_config(config):
159
+ """Test that the input configuration complies with the expected schema
160
+
161
+ Since it requires features from jsonschema 3+, it will only run if that is installed.
162
+ Otherwise show a warning but continue. This is incase some other module needs an earlier,
163
+ jsonschema (looking at you jwst).
164
+
165
+ If the function runs through without raising an exception, the check was succesful or skipped.
166
+
167
+ Parameters
168
+ ----------
169
+ config : dict
170
+ Configurations to check
171
+
172
+ Raises
173
+ ------
174
+ ValueError
175
+ If there is a problem with the configuration.
176
+ Usually that means a setting has an unallowed value.
177
+ """
178
+ if not hasJsonSchema: # pragma: no cover
179
+ # Can't check with old version
180
+ return
181
+ fname = "settings_schema.json"
182
+ this_dir = dirname(__file__)
183
+ fname = join(this_dir, "settings", fname)
184
+
185
+ with open(fname) as f:
186
+ schema = json.load(f)
187
+ try:
188
+ jsonschema.validate(schema=schema, instance=config)
189
+ except jsonschema.ValidationError as ve:
190
+ logger.error("Configuration failed validation check.\n%s", ve.message)
191
+ raise ValueError(ve.message) from ve
@@ -0,0 +1,329 @@
1
+ """
2
+ Find the continuum level
3
+
4
+ Currently only splices orders together
5
+ First guess of the continuum is provided by the flat field
6
+ """
7
+
8
+ import logging
9
+ from itertools import chain
10
+
11
+ import matplotlib.pyplot as plt
12
+ import numpy as np
13
+
14
+ from . import util
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # np.seterr("raise")
19
+
20
+
21
+ def splice_orders(spec, wave, cont, sigm, scaling=True, plot=False, plot_title=None):
22
+ """
23
+ Splice orders together so that they form a continous spectrum
24
+ This is achieved by linearly combining the overlaping regions
25
+
26
+ Parameters
27
+ ----------
28
+ spec : array[nord, ncol]
29
+ Spectrum to splice, with seperate orders
30
+ wave : array[nord, ncol]
31
+ Wavelength solution for each point
32
+ cont : array[nord, ncol]
33
+ Continuum, blaze function will do fine as well
34
+ sigm : array[nord, ncol]
35
+ Errors on the spectrum
36
+ scaling : bool, optional
37
+ If true, the spectrum/continuum will be scaled to 1 (default: False)
38
+ plot : bool, optional
39
+ If true, will plot the spliced spectrum (default: False)
40
+
41
+ Raises
42
+ ------
43
+ NotImplementedError
44
+ If neighbouring orders dont overlap
45
+
46
+ Returns
47
+ -------
48
+ spec, wave, cont, sigm : array[nord, ncol]
49
+ spliced spectrum
50
+ """
51
+ nord, _ = spec.shape # Number of sp. orders, Order length in pixels
52
+
53
+ if cont is None:
54
+ cont = np.ones_like(spec)
55
+
56
+ # Just to be extra safe that they are all the same
57
+ mask = (
58
+ np.ma.getmaskarray(spec)
59
+ | (np.ma.getdata(spec) == 0)
60
+ | (np.ma.getdata(cont) == 0)
61
+ )
62
+ spec = np.ma.masked_array(spec, mask=mask)
63
+ wave = np.ma.masked_array(np.ma.getdata(wave), mask=mask)
64
+ cont = np.ma.masked_array(np.ma.getdata(cont), mask=mask)
65
+ sigm = np.ma.masked_array(np.ma.getdata(sigm), mask=mask)
66
+
67
+ if scaling:
68
+ # Scale everything to roughly the same size, around spec/blaze = 1
69
+ scale = np.ma.median(spec / cont, axis=1)
70
+ cont *= scale[:, None]
71
+
72
+ if plot: # pragma: no cover
73
+ plt.subplot(411)
74
+ if plot_title is not None:
75
+ plt.suptitle(plot_title)
76
+ plt.title("Before")
77
+ for i in range(spec.shape[0]):
78
+ plt.plot(wave[i], spec[i] / cont[i])
79
+ plt.ylim([0, 2])
80
+
81
+ plt.subplot(412)
82
+ plt.title("Before Error")
83
+ for i in range(spec.shape[0]):
84
+ plt.plot(wave[i], sigm[i] / cont[i])
85
+ plt.ylim((0, np.ma.median(sigm[i] / cont[i]) * 2))
86
+
87
+ # Order with largest signal, everything is scaled relative to this order
88
+ iord0 = np.argmax(np.ma.median(spec / cont, axis=1))
89
+
90
+ # Loop from iord0 outwards, first to the top, then to the bottom
91
+ tmp0 = chain(range(iord0, 0, -1), range(iord0, nord - 1))
92
+ tmp1 = chain(range(iord0 - 1, -1, -1), range(iord0 + 1, nord))
93
+
94
+ # Looping over order pairs
95
+ for iord0, iord1 in zip(tmp0, tmp1, strict=False):
96
+ # Get data for current order
97
+ # Note that those are just references to parts of the original data
98
+ # any changes will also affect spec, wave, cont, and sigm
99
+ s0, s1 = spec[iord0], spec[iord1]
100
+ w0, w1 = wave[iord0], wave[iord1]
101
+ c0, c1 = cont[iord0], cont[iord1]
102
+ u0, u1 = sigm[iord0], sigm[iord1]
103
+
104
+ # Calculate Overlap
105
+ i0 = np.ma.where((w0 >= np.ma.min(w1)) & (w0 <= np.ma.max(w1)))
106
+ i1 = np.ma.where((w1 >= np.ma.min(w0)) & (w1 <= np.ma.max(w0)))
107
+
108
+ # Orders overlap
109
+ if i0[0].size > 0 and i1[0].size > 0:
110
+ # Interpolate the overlapping region onto the wavelength grid of the other order
111
+ tmpS0 = util.bezier_interp(w1, s1, w0[i0])
112
+ tmpB0 = util.bezier_interp(w1, c1, w0[i0])
113
+ tmpU0 = util.bezier_interp(w1, u1, w0[i0])
114
+
115
+ tmpS1 = util.bezier_interp(w0, s0, w1[i1])
116
+ tmpB1 = util.bezier_interp(w0, c0, w1[i1])
117
+ tmpU1 = util.bezier_interp(w0, u0, w1[i1])
118
+
119
+ # Combine the two orders weighted by the relative error
120
+ wgt0 = np.ma.vstack([c0[i0].data / u0[i0].data, tmpB0 / tmpU0]) ** 2
121
+ wgt1 = np.ma.vstack([c1[i1].data / u1[i1].data, tmpB1 / tmpU1]) ** 2
122
+
123
+ s0[i0], utmp = np.ma.average(
124
+ np.ma.vstack([s0[i0], tmpS0]), axis=0, weights=wgt0, returned=True
125
+ )
126
+ c0[i0] = np.ma.average([c0[i0], tmpB0], axis=0, weights=wgt0)
127
+ u0[i0] = c0[i0] * utmp**-0.5
128
+
129
+ s1[i1], utmp = np.ma.average(
130
+ np.ma.vstack([s1[i1], tmpS1]), axis=0, weights=wgt1, returned=True
131
+ )
132
+ c1[i1] = np.ma.average([c1[i1], tmpB1], axis=0, weights=wgt1)
133
+ u1[i1] = c1[i1] * utmp**-0.5
134
+ else: # pragma: no cover
135
+ # TODO: Orders dont overlap
136
+ continue
137
+
138
+ if plot: # pragma: no cover
139
+ plt.subplot(413)
140
+ plt.title("After")
141
+ for i in range(nord):
142
+ plt.plot(wave[i], spec[i] / cont[i], label="order=%i" % i)
143
+ plt.ylim((0, 2))
144
+
145
+ plt.subplot(414)
146
+ plt.title("Error")
147
+ for i in range(nord):
148
+ plt.plot(wave[i], sigm[i] / cont[i], label="order=%i" % i)
149
+ plt.ylim((0, np.ma.median(sigm[i] / cont[i]) * 2))
150
+ util.show_or_save("continuum_sigma")
151
+
152
+ return spec, wave, cont, sigm
153
+
154
+
155
+ class Plot_Normalization: # pragma: no cover
156
+ def __init__(self, wsort, sB, new_wave, contB, iteration=0, title=None):
157
+ plt.ion()
158
+ self.fig = plt.figure()
159
+ self.title = title
160
+ suptitle = f"Iteration: {iteration}"
161
+ if self.title is not None:
162
+ suptitle = f"{self.title}\n{suptitle}"
163
+ self.fig.suptitle(suptitle)
164
+
165
+ self.ax = self.fig.add_subplot(111)
166
+ self.line1 = self.ax.plot(wsort, sB, label="Spectrum")[0]
167
+ self.line2 = self.ax.plot(new_wave, contB, label="Continuum Fit")[0]
168
+ plt.legend()
169
+
170
+ util.show_or_save("continuum_fit")
171
+
172
+ def plot(self, wsort, sB, new_wave, contB, iteration):
173
+ suptitle = f"Iteration: {iteration}"
174
+ if self.title is not None:
175
+ suptitle = f"{self.title}\n{suptitle}"
176
+ self.fig.suptitle(suptitle)
177
+
178
+ self.line1.set_xdata(wsort)
179
+ self.line1.set_ydata(sB)
180
+ self.line2.set_xdata(new_wave)
181
+ self.line2.set_ydata(contB)
182
+
183
+ self.fig.canvas.draw()
184
+ self.fig.canvas.flush_events()
185
+
186
+ def close(self):
187
+ plt.ioff()
188
+ plt.close()
189
+
190
+
191
+ def continuum_normalize(
192
+ spec,
193
+ wave,
194
+ cont,
195
+ sigm,
196
+ iterations=10,
197
+ smooth_initial=1e5,
198
+ smooth_final=5e6,
199
+ scale_vert=1,
200
+ plot=True,
201
+ plot_title=None,
202
+ ):
203
+ """Fit a continuum to a spectrum by slowly approaching it from the top.
204
+ We exploit here that the continuum varies only on large wavelength scales, while individual lines act on much smaller scales
205
+
206
+ TODO automatically find good parameters for smooth_initial and smooth_final
207
+ TODO give variables better names
208
+
209
+ Parameters
210
+ ----------
211
+ spec : masked array of shape (nord, ncol)
212
+ Observed input spectrum, masked values describe column ranges
213
+ wave : masked array of shape (nord, ncol)
214
+ Wavelength solution of the spectrum
215
+ cont : masked array of shape (nord, ncol)
216
+ Initial continuum guess, for example based on the blaze
217
+ sigm : masked array of shape (nord, ncol)
218
+ Uncertainties of the spectrum
219
+ iterations : int, optional
220
+ Number of iterations of the algorithm,
221
+ note that runtime roughly scales with the number of iterations squared
222
+ (default: 10)
223
+ smooth_initial : float, optional
224
+ Smoothing parameter in the initial runs, usually smaller than smooth_final (default: 1e5)
225
+ smooth_final : float, optional
226
+ Smoothing parameter of the final run (default: 5e6)
227
+ scale_vert : float, optional
228
+ Vertical scale of the spectrum. Usually 1 if a previous normalization exists (default: 1)
229
+ plot : bool, optional
230
+ Wether to plot the current status and results or not (default: True)
231
+
232
+ Returns
233
+ -------
234
+ cont : masked array of shape (nord, ncol)
235
+ New continuum
236
+ """
237
+
238
+ nord, ncol = spec.shape
239
+
240
+ par2 = 1e-4
241
+ par4 = 0.01 * (1 - np.clip(2, None, 1 / np.sqrt(np.ma.median(spec))))
242
+
243
+ b = np.clip(cont, 1, None)
244
+ mask = ~np.ma.getmaskarray(b)
245
+ for i in range(nord):
246
+ b[i, mask[i]] = util.middle(b[i, mask[i]], 1)
247
+ cont = b
248
+
249
+ # Create new equispaced wavelength grid
250
+ tmp = wave.compressed()
251
+ wmin = np.min(tmp)
252
+ wmax = np.max(tmp)
253
+ dwave = np.abs(tmp[tmp.size // 2] - tmp[tmp.size // 2 - 1]) * 0.5
254
+ nwave = np.ceil((wmax - wmin) / dwave) + 1
255
+ new_wave = np.linspace(wmin, wmax, int(nwave), endpoint=True)
256
+
257
+ # Combine all orders into one big spectrum, sorted by wavelength
258
+ wsort, j, index = np.unique(tmp, return_index=True, return_inverse=True)
259
+ sB = (spec / cont).compressed()[j]
260
+
261
+ # Get initial weights for each point
262
+ weight = util.middle(sB, 0.5, x=wsort - wmin)
263
+ weight = weight / util.middle(weight, 3 * smooth_initial) + np.concatenate(
264
+ ([0], 2 * weight[1:-1] - weight[0:-2] - weight[2:], [0])
265
+ )
266
+ weight = np.clip(weight, 0, None)
267
+ # TODO for some reason the interpolation messes up, use linear instead for now
268
+ # weight = util.safe_interpolation(wsort, weight, new_wave)
269
+ weight = np.interp(new_wave, wsort, weight)
270
+ weight /= np.max(weight)
271
+
272
+ # Interpolate Spectrum onto the new grid
273
+ # ssB = util.safe_interpolation(wsort, sB, new_wave)
274
+ ssB = np.interp(new_wave, wsort, sB)
275
+ # Keep the scale of the continuum
276
+ bbb = util.middle(cont.compressed()[j], 1)
277
+
278
+ contB = np.ones_like(ssB)
279
+ if plot: # pragma: no cover
280
+ p = Plot_Normalization(wsort, sB, new_wave, contB, 0, title=plot_title)
281
+
282
+ try:
283
+ for i in range(iterations):
284
+ # Find new approximation of the top, smoothed by some parameter
285
+ c = ssB / contB
286
+ for _ in range(iterations):
287
+ _c = util.top(
288
+ c, smooth_initial, eps=par2, weight=weight, lambda2=smooth_final
289
+ )
290
+ c = np.clip(_c, c, None)
291
+ c = (
292
+ util.top(
293
+ c, smooth_initial, eps=par4, weight=weight, lambda2=smooth_final
294
+ )
295
+ * contB
296
+ )
297
+
298
+ # Scale it and update the weights of each point
299
+ contB = c * scale_vert
300
+ contB = util.middle(contB, 1)
301
+ weight = np.clip(ssB / contB, None, contB / np.clip(ssB, 1, None))
302
+
303
+ # Plot the intermediate results
304
+ if plot: # pragma: no cover
305
+ p.plot(wsort, sB, new_wave, contB, i)
306
+ except ValueError:
307
+ logger.error("Continuum fitting aborted")
308
+ finally:
309
+ if plot: # pragma: no cover
310
+ p.close()
311
+
312
+ # Calculate the new continuum from intermediate values
313
+ # new_cont = util.safe_interpolation(new_wave, contB, wsort)
314
+ new_cont = np.interp(wsort, new_wave, contB)
315
+ mask = np.ma.getmaskarray(cont)
316
+ cont[~mask] = (new_cont * bbb)[index]
317
+
318
+ # Final output plot
319
+ if plot: # pragma: no cover
320
+ plt.plot(wave.ravel(), spec.ravel(), label="spec")
321
+ plt.plot(wave.ravel(), cont.ravel(), label="cont")
322
+ plt.legend(loc="best")
323
+ if plot_title is not None:
324
+ plt.title(plot_title)
325
+ plt.xlabel("Wavelength [A]")
326
+ plt.ylabel("Flux")
327
+ util.show_or_save("continuum_final")
328
+
329
+ return cont