pyreduce-astro 0.6.0b5__cp311-cp311-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 (158) hide show
  1. pyreduce/__init__.py +67 -0
  2. pyreduce/__main__.py +106 -0
  3. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.exp +0 -0
  4. pyreduce/clib/Release/_slitfunc_2d.cp311-win_amd64.lib +0 -0
  5. pyreduce/clib/Release/_slitfunc_2d.obj +0 -0
  6. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.exp +0 -0
  7. pyreduce/clib/Release/_slitfunc_bd.cp311-win_amd64.lib +0 -0
  8. pyreduce/clib/Release/_slitfunc_bd.obj +0 -0
  9. pyreduce/clib/__init__.py +0 -0
  10. pyreduce/clib/_slitfunc_2d.cp311-win_amd64.pyd +0 -0
  11. pyreduce/clib/_slitfunc_bd.cp311-win_amd64.pyd +0 -0
  12. pyreduce/clib/build_extract.py +75 -0
  13. pyreduce/clib/slit_func_2d_xi_zeta_bd.c +1313 -0
  14. pyreduce/clib/slit_func_2d_xi_zeta_bd.h +55 -0
  15. pyreduce/clib/slit_func_bd.c +362 -0
  16. pyreduce/clib/slit_func_bd.h +17 -0
  17. pyreduce/clipnflip.py +147 -0
  18. pyreduce/combine_frames.py +855 -0
  19. pyreduce/configuration.py +186 -0
  20. pyreduce/continuum_normalization.py +329 -0
  21. pyreduce/cwrappers.py +404 -0
  22. pyreduce/datasets.py +231 -0
  23. pyreduce/echelle.py +413 -0
  24. pyreduce/estimate_background_scatter.py +129 -0
  25. pyreduce/extract.py +1361 -0
  26. pyreduce/extraction_width.py +77 -0
  27. pyreduce/instruments/__init__.py +0 -0
  28. pyreduce/instruments/andes.json +61 -0
  29. pyreduce/instruments/andes.py +102 -0
  30. pyreduce/instruments/common.json +46 -0
  31. pyreduce/instruments/common.py +675 -0
  32. pyreduce/instruments/crires_plus.json +63 -0
  33. pyreduce/instruments/crires_plus.py +103 -0
  34. pyreduce/instruments/filters.py +195 -0
  35. pyreduce/instruments/harpn.json +136 -0
  36. pyreduce/instruments/harpn.py +201 -0
  37. pyreduce/instruments/harps.json +155 -0
  38. pyreduce/instruments/harps.py +310 -0
  39. pyreduce/instruments/instrument_info.py +140 -0
  40. pyreduce/instruments/instrument_schema.json +221 -0
  41. pyreduce/instruments/jwst_miri.json +53 -0
  42. pyreduce/instruments/jwst_miri.py +29 -0
  43. pyreduce/instruments/jwst_niriss.json +52 -0
  44. pyreduce/instruments/jwst_niriss.py +98 -0
  45. pyreduce/instruments/lick_apf.json +53 -0
  46. pyreduce/instruments/lick_apf.py +35 -0
  47. pyreduce/instruments/mcdonald.json +59 -0
  48. pyreduce/instruments/mcdonald.py +123 -0
  49. pyreduce/instruments/metis_ifu.json +63 -0
  50. pyreduce/instruments/metis_ifu.py +45 -0
  51. pyreduce/instruments/metis_lss.json +65 -0
  52. pyreduce/instruments/metis_lss.py +45 -0
  53. pyreduce/instruments/micado.json +53 -0
  54. pyreduce/instruments/micado.py +45 -0
  55. pyreduce/instruments/neid.json +51 -0
  56. pyreduce/instruments/neid.py +154 -0
  57. pyreduce/instruments/nirspec.json +56 -0
  58. pyreduce/instruments/nirspec.py +215 -0
  59. pyreduce/instruments/nte.json +47 -0
  60. pyreduce/instruments/nte.py +42 -0
  61. pyreduce/instruments/uves.json +59 -0
  62. pyreduce/instruments/uves.py +46 -0
  63. pyreduce/instruments/xshooter.json +66 -0
  64. pyreduce/instruments/xshooter.py +39 -0
  65. pyreduce/make_shear.py +606 -0
  66. pyreduce/masks/mask_crires_plus_det1.fits.gz +0 -0
  67. pyreduce/masks/mask_crires_plus_det2.fits.gz +0 -0
  68. pyreduce/masks/mask_crires_plus_det3.fits.gz +0 -0
  69. pyreduce/masks/mask_ctio_chiron.fits.gz +0 -0
  70. pyreduce/masks/mask_elodie.fits.gz +0 -0
  71. pyreduce/masks/mask_feros3.fits.gz +0 -0
  72. pyreduce/masks/mask_flames_giraffe.fits.gz +0 -0
  73. pyreduce/masks/mask_harps_blue.fits.gz +0 -0
  74. pyreduce/masks/mask_harps_red.fits.gz +0 -0
  75. pyreduce/masks/mask_hds_blue.fits.gz +0 -0
  76. pyreduce/masks/mask_hds_red.fits.gz +0 -0
  77. pyreduce/masks/mask_het_hrs_2x5.fits.gz +0 -0
  78. pyreduce/masks/mask_jwst_miri_lrs_slitless.fits.gz +0 -0
  79. pyreduce/masks/mask_jwst_niriss_gr700xd.fits.gz +0 -0
  80. pyreduce/masks/mask_lick_apf_.fits.gz +0 -0
  81. pyreduce/masks/mask_mcdonald.fits.gz +0 -0
  82. pyreduce/masks/mask_nes.fits.gz +0 -0
  83. pyreduce/masks/mask_nirspec_nirspec.fits.gz +0 -0
  84. pyreduce/masks/mask_sarg.fits.gz +0 -0
  85. pyreduce/masks/mask_sarg_2x2a.fits.gz +0 -0
  86. pyreduce/masks/mask_sarg_2x2b.fits.gz +0 -0
  87. pyreduce/masks/mask_subaru_hds_red.fits.gz +0 -0
  88. pyreduce/masks/mask_uves_blue.fits.gz +0 -0
  89. pyreduce/masks/mask_uves_blue_binned_2_2.fits.gz +0 -0
  90. pyreduce/masks/mask_uves_middle.fits.gz +0 -0
  91. pyreduce/masks/mask_uves_middle_2x2_split.fits.gz +0 -0
  92. pyreduce/masks/mask_uves_middle_binned_2_2.fits.gz +0 -0
  93. pyreduce/masks/mask_uves_red.fits.gz +0 -0
  94. pyreduce/masks/mask_uves_red_2x2.fits.gz +0 -0
  95. pyreduce/masks/mask_uves_red_2x2_split.fits.gz +0 -0
  96. pyreduce/masks/mask_uves_red_binned_2_2.fits.gz +0 -0
  97. pyreduce/masks/mask_xshooter_nir.fits.gz +0 -0
  98. pyreduce/rectify.py +138 -0
  99. pyreduce/reduce.py +2205 -0
  100. pyreduce/settings/settings_ANDES.json +89 -0
  101. pyreduce/settings/settings_CRIRES_PLUS.json +89 -0
  102. pyreduce/settings/settings_HARPN.json +73 -0
  103. pyreduce/settings/settings_HARPS.json +69 -0
  104. pyreduce/settings/settings_JWST_MIRI.json +55 -0
  105. pyreduce/settings/settings_JWST_NIRISS.json +55 -0
  106. pyreduce/settings/settings_LICK_APF.json +62 -0
  107. pyreduce/settings/settings_MCDONALD.json +58 -0
  108. pyreduce/settings/settings_METIS_IFU.json +77 -0
  109. pyreduce/settings/settings_METIS_LSS.json +77 -0
  110. pyreduce/settings/settings_MICADO.json +78 -0
  111. pyreduce/settings/settings_NEID.json +73 -0
  112. pyreduce/settings/settings_NIRSPEC.json +58 -0
  113. pyreduce/settings/settings_NTE.json +60 -0
  114. pyreduce/settings/settings_UVES.json +54 -0
  115. pyreduce/settings/settings_XSHOOTER.json +78 -0
  116. pyreduce/settings/settings_pyreduce.json +178 -0
  117. pyreduce/settings/settings_schema.json +827 -0
  118. pyreduce/tools/__init__.py +0 -0
  119. pyreduce/tools/combine.py +117 -0
  120. pyreduce/trace_orders.py +645 -0
  121. pyreduce/util.py +1288 -0
  122. pyreduce/wavecal/MICADO_HK_3arcsec_chip5.npz +0 -0
  123. pyreduce/wavecal/atlas/thar.fits +4946 -13
  124. pyreduce/wavecal/atlas/thar_list.txt +4172 -0
  125. pyreduce/wavecal/atlas/une.fits +0 -0
  126. pyreduce/wavecal/convert.py +38 -0
  127. pyreduce/wavecal/crires_plus_J1228_Open_det1.npz +0 -0
  128. pyreduce/wavecal/crires_plus_J1228_Open_det2.npz +0 -0
  129. pyreduce/wavecal/crires_plus_J1228_Open_det3.npz +0 -0
  130. pyreduce/wavecal/harpn_harpn_2D.npz +0 -0
  131. pyreduce/wavecal/harps_blue_2D.npz +0 -0
  132. pyreduce/wavecal/harps_blue_pol_2D.npz +0 -0
  133. pyreduce/wavecal/harps_red_2D.npz +0 -0
  134. pyreduce/wavecal/harps_red_pol_2D.npz +0 -0
  135. pyreduce/wavecal/mcdonald.npz +0 -0
  136. pyreduce/wavecal/metis_lss_l_2D.npz +0 -0
  137. pyreduce/wavecal/metis_lss_m_2D.npz +0 -0
  138. pyreduce/wavecal/nirspec_K2.npz +0 -0
  139. pyreduce/wavecal/uves_blue_360nm_2D.npz +0 -0
  140. pyreduce/wavecal/uves_blue_390nm_2D.npz +0 -0
  141. pyreduce/wavecal/uves_blue_437nm_2D.npz +0 -0
  142. pyreduce/wavecal/uves_middle_2x2_2D.npz +0 -0
  143. pyreduce/wavecal/uves_middle_565nm_2D.npz +0 -0
  144. pyreduce/wavecal/uves_middle_580nm_2D.npz +0 -0
  145. pyreduce/wavecal/uves_middle_600nm_2D.npz +0 -0
  146. pyreduce/wavecal/uves_middle_665nm_2D.npz +0 -0
  147. pyreduce/wavecal/uves_middle_860nm_2D.npz +0 -0
  148. pyreduce/wavecal/uves_red_580nm_2D.npz +0 -0
  149. pyreduce/wavecal/uves_red_600nm_2D.npz +0 -0
  150. pyreduce/wavecal/uves_red_665nm_2D.npz +0 -0
  151. pyreduce/wavecal/uves_red_760nm_2D.npz +0 -0
  152. pyreduce/wavecal/uves_red_860nm_2D.npz +0 -0
  153. pyreduce/wavecal/xshooter_nir.npz +0 -0
  154. pyreduce/wavelength_calibration.py +1873 -0
  155. pyreduce_astro-0.6.0b5.dist-info/METADATA +113 -0
  156. pyreduce_astro-0.6.0b5.dist-info/RECORD +158 -0
  157. pyreduce_astro-0.6.0b5.dist-info/WHEEL +4 -0
  158. pyreduce_astro-0.6.0b5.dist-info/licenses/LICENSE +674 -0
pyreduce/make_shear.py ADDED
@@ -0,0 +1,606 @@
1
+ """
2
+ Calculate the tilt based on a reference spectrum with high SNR, e.g. Wavelength calibration image
3
+
4
+ Authors
5
+ -------
6
+ Nikolai Piskunov
7
+ Ansgar Wehrhahn
8
+
9
+ Version
10
+ --------
11
+ 0.9 - NP - IDL Version
12
+ 1.0 - AW - Python Version
13
+
14
+ License
15
+ -------
16
+ ....
17
+ """
18
+
19
+ import logging
20
+
21
+ import matplotlib.pyplot as plt
22
+ import numpy as np
23
+ from numpy.polynomial.polynomial import polyval2d
24
+ from scipy import signal
25
+ from scipy.optimize import least_squares
26
+ from tqdm import tqdm
27
+
28
+ from .extract import fix_parameters
29
+ from .util import make_index
30
+ from .util import polyfit2d_2 as polyfit2d
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class ProgressPlot: # pragma: no cover
36
+ def __init__(self, ncol, width, title=None):
37
+ plt.ion()
38
+
39
+ fig, (ax1, ax2, ax3) = plt.subplots(ncols=3)
40
+
41
+ plot_title = "Curvature in each order"
42
+ if title is not None:
43
+ plot_title = f"{title}\n{plot_title}"
44
+ fig.suptitle(plot_title)
45
+
46
+ (line1,) = ax1.plot(np.arange(ncol) + 1)
47
+ (line2,) = ax1.plot(0, 0, "d")
48
+ ax1.set_yscale("log")
49
+
50
+ self.ncol = ncol
51
+ self.width = width * 2 + 1
52
+
53
+ self.fig = fig
54
+ self.ax1 = ax1
55
+ self.ax2 = ax2
56
+ self.ax3 = ax3
57
+ self.line1 = line1
58
+ self.line2 = line2
59
+
60
+ def update_plot1(self, vector, peak, offset=0):
61
+ data = np.ones(self.ncol)
62
+ data[offset : len(vector) + offset] = np.clip(vector, 1, None)
63
+ self.line1.set_ydata(data)
64
+ self.line2.set_xdata(peak)
65
+ self.line2.set_ydata(data[peak])
66
+ self.ax1.set_ylim((data.min(), data.max()))
67
+ self.fig.canvas.draw()
68
+ self.fig.canvas.flush_events()
69
+
70
+ def update_plot2(self, img, model, tilt, shear, peak):
71
+ self.ax2.clear()
72
+ self.ax3.clear()
73
+
74
+ self.ax2.imshow(img)
75
+ self.ax3.imshow(model)
76
+
77
+ nrows, _ = img.shape
78
+ middle = nrows // 2
79
+ y = np.arange(-middle, -middle + nrows)
80
+ x = peak + (tilt + shear * y) * y
81
+ y += middle
82
+
83
+ self.ax2.plot(x, y, "r")
84
+ self.ax3.plot(x, y, "r")
85
+
86
+ self.fig.canvas.draw()
87
+ self.fig.canvas.flush_events()
88
+
89
+ def close(self):
90
+ plt.close()
91
+ plt.ioff()
92
+
93
+
94
+ class Curvature:
95
+ def __init__(
96
+ self,
97
+ orders,
98
+ extraction_width=0.5,
99
+ column_range=None,
100
+ order_range=None,
101
+ window_width=9,
102
+ peak_threshold=10,
103
+ peak_width=1,
104
+ fit_degree=2,
105
+ sigma_cutoff=3,
106
+ mode="1D",
107
+ plot=False,
108
+ plot_title=None,
109
+ peak_function="gaussian",
110
+ curv_degree=2,
111
+ ):
112
+ self.orders = orders
113
+ self.extraction_width = extraction_width
114
+ self.column_range = column_range
115
+ if order_range is None:
116
+ order_range = (0, self.nord)
117
+ self.order_range = order_range
118
+ self.window_width = window_width
119
+ self.threshold = peak_threshold
120
+ self.peak_width = peak_width
121
+ self.fit_degree = fit_degree
122
+ self.sigma_cutoff = sigma_cutoff
123
+ self.mode = mode
124
+ self.plot = plot
125
+ self.plot_title = plot_title
126
+ self.curv_degree = curv_degree
127
+ self.peak_function = peak_function
128
+
129
+ if self.mode == "1D":
130
+ # fit degree is an integer
131
+ if not np.isscalar(self.fit_degree):
132
+ self.fit_degree = self.fit_degree[0]
133
+ elif self.mode == "2D":
134
+ # fit degree is a 2 tuple
135
+ if np.isscalar(self.fit_degree):
136
+ self.fit_degree = (self.fit_degree, self.fit_degree)
137
+
138
+ @property
139
+ def nord(self):
140
+ return self.orders.shape[0]
141
+
142
+ @property
143
+ def n(self):
144
+ return self.order_range[1] - self.order_range[0]
145
+
146
+ @property
147
+ def mode(self):
148
+ return self._mode
149
+
150
+ @mode.setter
151
+ def mode(self, value):
152
+ if value not in ["1D", "2D"]:
153
+ raise ValueError(
154
+ f"Value for 'mode' not understood. Expected one of ['1D', '2D'] but got {value}"
155
+ )
156
+ self._mode = value
157
+
158
+ def _fix_inputs(self, original):
159
+ orders = self.orders
160
+ extraction_width = self.extraction_width
161
+ column_range = self.column_range
162
+
163
+ nrow, ncol = original.shape
164
+ nord = len(orders)
165
+
166
+ extraction_width, column_range, orders = fix_parameters(
167
+ extraction_width, column_range, orders, nrow, ncol, nord
168
+ )
169
+
170
+ self.column_range = column_range[self.order_range[0] : self.order_range[1]]
171
+ self.extraction_width = extraction_width[
172
+ self.order_range[0] : self.order_range[1]
173
+ ]
174
+ self.orders = orders[self.order_range[0] : self.order_range[1]]
175
+ self.order_range = (0, self.n)
176
+
177
+ def _find_peaks(self, vec, cr):
178
+ # This should probably be the same as in the wavelength calibration
179
+ vec -= np.ma.median(vec)
180
+ vec = np.ma.filled(vec, 0)
181
+ height = np.percentile(vec, 68) * self.threshold
182
+ peaks, _ = signal.find_peaks(
183
+ vec, prominence=height, width=self.peak_width, distance=self.window_width
184
+ )
185
+
186
+ # Remove peaks at the edge
187
+ peaks = peaks[
188
+ (peaks >= self.window_width + 1)
189
+ & (peaks < len(vec) - self.window_width - 1)
190
+ ]
191
+ # Remove the offset, due to vec being a subset of extracted
192
+ peaks += cr[0]
193
+ return vec, peaks
194
+
195
+ def _determine_curvature_single_line(self, original, peak, ycen, ycen_int, xwd):
196
+ """
197
+ Fit the curvature of a single peak in the spectrum
198
+
199
+ This is achieved by fitting a model, that consists of gaussians
200
+ in spectrum direction, that are shifted by the curvature in each row.
201
+
202
+ Parameters
203
+ ----------
204
+ original : array of shape (nrows, ncols)
205
+ whole input image
206
+ peak : int
207
+ column position of the peak
208
+ ycen : array of shape (ncols,)
209
+ row center of the order of the peak
210
+ xwd : 2 tuple
211
+ extraction width above and below the order center to use
212
+
213
+ Returns
214
+ -------
215
+ tilt : float
216
+ first order curvature
217
+ shear : float
218
+ second order curvature
219
+ """
220
+ _, ncol = original.shape
221
+
222
+ # look at +- width pixels around the line
223
+ # Extract short horizontal strip for each row in extraction width
224
+ # Then fit a gaussian to each row, to find the center of the line
225
+ x = peak + np.arange(-self.window_width, self.window_width + 1)
226
+ x = x[(x >= 0) & (x < ncol)]
227
+ xmin, xmax = x[0], x[-1] + 1
228
+
229
+ # Look above and below the line center
230
+ y = np.arange(-xwd[0], xwd[1] + 1)[:, None] - ycen[xmin:xmax][None, :]
231
+
232
+ x = x[None, :]
233
+ idx = make_index(ycen_int - xwd[0], ycen_int + xwd[1], xmin, xmax)
234
+ img = original[idx]
235
+ img_compressed = np.ma.compressed(img)
236
+
237
+ img -= np.percentile(img_compressed, 1)
238
+ img /= np.percentile(img_compressed, 99)
239
+ img = np.ma.clip(img, 0, 1)
240
+
241
+ sl = np.ma.mean(img, axis=1)
242
+ sl = sl[:, None]
243
+
244
+ peak_func = {"gaussian": gaussian, "lorentzian": lorentzian}
245
+ peak_func = peak_func[self.peak_function]
246
+
247
+ def model(coef):
248
+ A, middle, sig, *curv = coef
249
+ mu = middle + shift(curv)
250
+ mod = peak_func(x, A, mu, sig)
251
+ mod *= sl
252
+ return (mod - img).ravel()
253
+
254
+ def model_compressed(coef):
255
+ return np.ma.compressed(model(coef))
256
+
257
+ A = np.nanpercentile(img_compressed, 95)
258
+ sig = (xmax - xmin) / 4 # TODO
259
+ if self.curv_degree == 1:
260
+
261
+ def shift(curv):
262
+ return curv[0] * y
263
+ elif self.curv_degree == 2:
264
+
265
+ def shift(curv):
266
+ return (curv[0] + curv[1] * y) * y
267
+ else:
268
+ raise ValueError("Only curvature degrees 1 and 2 are supported")
269
+ # res = least_squares(model, x0=[A, middle, sig, 0], loss="soft_l1", bounds=([0, xmin, 1, -10],[np.inf, xmax, xmax, 10]))
270
+ x0 = [A, peak, sig] + [0] * self.curv_degree
271
+ res = least_squares(
272
+ model_compressed, x0=x0, method="trf", loss="soft_l1", f_scale=0.1
273
+ )
274
+
275
+ if self.curv_degree == 1:
276
+ tilt, shear = res.x[3], 0
277
+ elif self.curv_degree == 2:
278
+ tilt, shear = res.x[3], res.x[4]
279
+ else:
280
+ tilt, shear = 0, 0
281
+
282
+ # model = model(res.x).reshape(img.shape) + img
283
+ # vmin = 0
284
+ # vmax = np.max(model)
285
+
286
+ # y = y.ravel()
287
+ # x = res.x[1] - xmin + (tilt + shear * y) * y
288
+ # y += xwd[0]
289
+
290
+ # plt.subplot(121)
291
+ # plt.imshow(img, vmin=vmin, vmax=vmax, origin="lower")
292
+ # plt.plot(xwd[0] + ycen[xmin:xmax], "r")
293
+ # plt.title("Input Image")
294
+ # plt.xlabel("x [pixel]")
295
+ # plt.ylabel("y [pixel]")
296
+
297
+ # plt.subplot(122)
298
+ # plt.imshow(model, vmin=vmin, vmax=vmax, origin="lower")
299
+ # plt.plot(x, y, "r", label="curvature")
300
+ # plt.ylim((-0.5, model.shape[0] - 0.5))
301
+ # plt.title("Model")
302
+ # plt.xlabel("x [pixel]")
303
+ # plt.ylabel("y [pixel]")
304
+
305
+ # plt.show()
306
+
307
+ if self.plot >= 2:
308
+ model = res.fun.reshape(img.shape) + img
309
+ self.progress.update_plot2(img, model, tilt, shear, res.x[1] - xmin)
310
+
311
+ return tilt, shear
312
+
313
+ def _fit_curvature_single_order(self, peaks, tilt, shear):
314
+ try:
315
+ middle = np.median(tilt)
316
+ sigma = np.percentile(tilt, (32, 68))
317
+ sigma = middle - sigma[0], sigma[1] - middle
318
+ mask = (tilt >= middle - 5 * sigma[0]) & (tilt <= middle + 5 * sigma[1])
319
+ peaks, tilt, shear = peaks[mask], tilt[mask], shear[mask]
320
+
321
+ coef_tilt = np.zeros(self.fit_degree + 1)
322
+ res = least_squares(
323
+ lambda coef: np.polyval(coef, peaks) - tilt,
324
+ x0=coef_tilt,
325
+ loss="arctan",
326
+ )
327
+ coef_tilt = res.x
328
+
329
+ coef_shear = np.zeros(self.fit_degree + 1)
330
+ res = least_squares(
331
+ lambda coef: np.polyval(coef, peaks) - shear,
332
+ x0=coef_shear,
333
+ loss="arctan",
334
+ )
335
+ coef_shear = res.x
336
+
337
+ except:
338
+ logger.error(
339
+ "Could not fit the curvature of this order. Using no curvature instead"
340
+ )
341
+ coef_tilt = np.zeros(self.fit_degree + 1)
342
+ coef_shear = np.zeros(self.fit_degree + 1)
343
+
344
+ return coef_tilt, coef_shear, peaks
345
+
346
+ def _determine_curvature_all_lines(self, original, extracted):
347
+ ncol = original.shape[1]
348
+ # Store data from all orders
349
+ all_peaks = []
350
+ all_tilt = []
351
+ all_shear = []
352
+ plot_vec = []
353
+
354
+ for j in tqdm(range(self.n), desc="Order"):
355
+ logger.debug("Calculating tilt of order %i out of %i", j + 1, self.n)
356
+
357
+ cr = self.column_range[j]
358
+ xwd = self.extraction_width[j]
359
+ ycen = np.polyval(self.orders[j], np.arange(ncol))
360
+ ycen_int = ycen.astype(int)
361
+ ycen -= ycen_int
362
+
363
+ # Find peaks
364
+ vec = extracted[j, cr[0] : cr[1]]
365
+ vec, peaks = self._find_peaks(vec, cr)
366
+
367
+ npeaks = len(peaks)
368
+
369
+ # Determine curvature for each line seperately
370
+ tilt = np.zeros(npeaks)
371
+ shear = np.zeros(npeaks)
372
+ mask = np.full(npeaks, True)
373
+ for ipeak, peak in tqdm(
374
+ enumerate(peaks), total=len(peaks), desc="Peak", leave=False
375
+ ):
376
+ if self.plot >= 2: # pragma: no cover
377
+ self.progress.update_plot1(vec, peak, cr[0])
378
+ try:
379
+ tilt[ipeak], shear[ipeak] = self._determine_curvature_single_line(
380
+ original, peak, ycen, ycen_int, xwd
381
+ )
382
+ except RuntimeError: # pragma: no cover
383
+ mask[ipeak] = False
384
+
385
+ # Store results
386
+ all_peaks += [peaks[mask]]
387
+ all_tilt += [tilt[mask]]
388
+ all_shear += [shear[mask]]
389
+ plot_vec += [vec]
390
+ return all_peaks, all_tilt, all_shear, plot_vec
391
+
392
+ def fit(self, peaks, tilt, shear):
393
+ if self.mode == "1D":
394
+ coef_tilt = np.zeros((self.n, self.fit_degree + 1))
395
+ coef_shear = np.zeros((self.n, self.fit_degree + 1))
396
+ for j in range(self.n):
397
+ coef_tilt[j], coef_shear[j], _ = self._fit_curvature_single_order(
398
+ peaks[j], tilt[j], shear[j]
399
+ )
400
+ elif self.mode == "2D":
401
+ x = np.concatenate(peaks)
402
+ y = [np.full(len(p), i) for i, p in enumerate(peaks)]
403
+ y = np.concatenate(y)
404
+ z = np.concatenate(tilt)
405
+ coef_tilt = polyfit2d(x, y, z, degree=self.fit_degree, loss="arctan")
406
+
407
+ z = np.concatenate(shear)
408
+ coef_shear = polyfit2d(x, y, z, degree=self.fit_degree, loss="arctan")
409
+
410
+ return coef_tilt, coef_shear
411
+
412
+ def eval(self, peaks, order, coef_tilt, coef_shear):
413
+ if self.mode == "1D":
414
+ tilt = np.zeros(peaks.shape)
415
+ shear = np.zeros(peaks.shape)
416
+ for i in np.unique(order):
417
+ idx = order == i
418
+ tilt[idx] = np.polyval(coef_tilt[i], peaks[idx])
419
+ shear[idx] = np.polyval(coef_shear[i], peaks[idx])
420
+ elif self.mode == "2D":
421
+ tilt = polyval2d(peaks, order, coef_tilt)
422
+ shear = polyval2d(peaks, order, coef_shear)
423
+ return tilt, shear
424
+
425
+ def plot_results(
426
+ self, ncol, plot_peaks, plot_vec, plot_tilt, plot_shear, tilt_x, shear_x
427
+ ): # pragma: no cover
428
+ fig, axes = plt.subplots(nrows=self.n // 2 + self.n % 2, ncols=2, squeeze=False)
429
+
430
+ title = "Peaks"
431
+ if self.plot_title is not None:
432
+ title = f"{self.plot_title}\n{title}"
433
+ fig.suptitle(title)
434
+ fig1, axes1 = plt.subplots(
435
+ nrows=self.n // 2 + self.n % 2, ncols=2, squeeze=False
436
+ )
437
+
438
+ title = "1st Order Curvature"
439
+ if self.plot_title is not None:
440
+ title = f"{self.plot_title}\n{title}"
441
+ fig1.suptitle(title)
442
+ fig2, axes2 = plt.subplots(
443
+ nrows=self.n // 2 + self.n % 2, ncols=2, squeeze=False
444
+ )
445
+
446
+ title = "2nd Order Curvature"
447
+ if self.plot_title is not None:
448
+ title = f"{self.plot_title}\n{title}"
449
+ fig2.suptitle(title)
450
+ plt.subplots_adjust(hspace=0)
451
+
452
+ def trim_axs(axs, N):
453
+ """little helper to massage the axs list to have correct length..."""
454
+ axs = axs.flat
455
+ for ax in axs[N:]:
456
+ ax.remove()
457
+ return axs[:N]
458
+
459
+ t, s = [None for _ in range(self.n)], [None for _ in range(self.n)]
460
+ for j in range(self.n):
461
+ cr = self.column_range[j]
462
+ x = np.arange(cr[0], cr[1])
463
+ order = np.full(len(x), j)
464
+ t[j], s[j] = self.eval(x, order, tilt_x, shear_x)
465
+
466
+ t_lower = min(t.min() * (0.5 if t.min() > 0 else 1.5) for t in t)
467
+ t_upper = max(t.max() * (1.5 if t.max() > 0 else 0.5) for t in t)
468
+
469
+ s_lower = min(s.min() * (0.5 if s.min() > 0 else 1.5) for s in s)
470
+ s_upper = max(s.max() * (1.5 if s.max() > 0 else 0.5) for s in s)
471
+
472
+ for j in range(self.n):
473
+ cr = self.column_range[j]
474
+ peaks = plot_peaks[j]
475
+ vec = np.clip(plot_vec[j], 0, None)
476
+ tilt = plot_tilt[j]
477
+ shear = plot_shear[j]
478
+ x = np.arange(cr[0], cr[1])
479
+ # Figure Peaks found (and used)
480
+ axes[j // 2, j % 2].plot(np.arange(cr[0], cr[1]), vec)
481
+ axes[j // 2, j % 2].plot(peaks, vec[peaks - cr[0]], "X")
482
+ axes[j // 2, j % 2].set_xlim([0, ncol])
483
+ # axes[j // 2, j % 2].set_yscale("log")
484
+ if j not in (self.n - 1, self.n - 2):
485
+ axes[j // 2, j % 2].get_xaxis().set_ticks([])
486
+
487
+ # Figure 1st order
488
+ axes1[j // 2, j % 2].plot(peaks, tilt, "rX")
489
+ axes1[j // 2, j % 2].plot(x, t[j])
490
+ axes1[j // 2, j % 2].set_xlim(0, ncol)
491
+
492
+ axes1[j // 2, j % 2].set_ylim(t_lower, t_upper)
493
+ if j not in (self.n - 1, self.n - 2):
494
+ axes1[j // 2, j % 2].get_xaxis().set_ticks([])
495
+ else:
496
+ axes1[j // 2, j % 2].set_xlabel("x [pixel]")
497
+ if j == self.n // 2 + 1:
498
+ axes1[j // 2, j % 2].set_ylabel("tilt [pixel/pixel]")
499
+
500
+ # Figure 2nd order
501
+ axes2[j // 2, j % 2].plot(peaks, shear, "rX")
502
+ axes2[j // 2, j % 2].plot(x, s[j])
503
+ axes2[j // 2, j % 2].set_xlim(0, ncol)
504
+
505
+ axes2[j // 2, j % 2].set_ylim(s_lower, s_upper)
506
+ if j not in (self.n - 1, self.n - 2):
507
+ axes2[j // 2, j % 2].get_xaxis().set_ticks([])
508
+ else:
509
+ axes2[j // 2, j % 2].set_xlabel("x [pixel]")
510
+ if j == self.n // 2 + 1:
511
+ axes2[j // 2, j % 2].set_ylabel("shear [pixel/pixel**2]")
512
+
513
+ axes1 = trim_axs(axes1, self.n)
514
+ axes2 = trim_axs(axes2, self.n)
515
+
516
+ plt.show()
517
+
518
+ def plot_comparison(self, original, tilt, shear, peaks): # pragma: no cover
519
+ _, ncol = original.shape
520
+ output = np.zeros((np.sum(self.extraction_width) + self.nord, ncol))
521
+ pos = [0]
522
+ x = np.arange(ncol)
523
+ for i in range(self.nord):
524
+ ycen = np.polyval(self.orders[i], x)
525
+ yb = ycen - self.extraction_width[i, 0]
526
+ yt = ycen + self.extraction_width[i, 1]
527
+ xl, xr = self.column_range[i]
528
+ index = make_index(yb, yt, xl, xr)
529
+ yl = pos[i]
530
+ yr = pos[i] + index[0].shape[0]
531
+ output[yl:yr, xl:xr] = original[index]
532
+ pos += [yr]
533
+
534
+ vmin, vmax = np.percentile(output[output != 0], (5, 95))
535
+ plt.imshow(output, vmin=vmin, vmax=vmax, origin="lower", aspect="auto")
536
+
537
+ for i in range(self.nord):
538
+ for p in peaks[i]:
539
+ ew = self.extraction_width[i]
540
+ x = np.zeros(ew[0] + ew[1] + 1)
541
+ y = np.arange(-ew[0], ew[1] + 1)
542
+ for j, yt in enumerate(y):
543
+ x[j] = p + yt * tilt[i, p] + yt**2 * shear[i, p]
544
+ y += pos[i] + ew[0]
545
+ plt.plot(x, y, "r")
546
+
547
+ locs = np.sum(self.extraction_width, axis=1) + 1
548
+ locs = np.array([0, *np.cumsum(locs)[:-1]])
549
+ locs[:-1] += (np.diff(locs) * 0.5).astype(int)
550
+ locs[-1] += ((output.shape[0] - locs[-1]) * 0.5).astype(int)
551
+
552
+ plt.yticks(locs, range(len(locs)))
553
+ if self.plot_title is not None:
554
+ plt.title(self.plot_title)
555
+ plt.xlabel("x [pixel]")
556
+ plt.ylabel("order")
557
+ plt.show()
558
+
559
+ def execute(self, extracted, original):
560
+ logger.info("Determining the Slit Curvature")
561
+
562
+ _, ncol = original.shape
563
+
564
+ self._fix_inputs(original)
565
+
566
+ if self.plot >= 2: # pragma: no cover
567
+ self.progress = ProgressPlot(ncol, self.window_width, title=self.plot_title)
568
+
569
+ peaks, tilt, shear, vec = self._determine_curvature_all_lines(
570
+ original, extracted
571
+ )
572
+
573
+ coef_tilt, coef_shear = self.fit(peaks, tilt, shear)
574
+
575
+ if self.plot >= 2: # pragma: no cover
576
+ self.progress.close()
577
+
578
+ if self.plot: # pragma: no cover
579
+ self.plot_results(ncol, peaks, vec, tilt, shear, coef_tilt, coef_shear)
580
+
581
+ iorder, ipeaks = np.indices(extracted.shape)
582
+ tilt, shear = self.eval(ipeaks, iorder, coef_tilt, coef_shear)
583
+
584
+ if self.plot: # pragma: no cover
585
+ self.plot_comparison(original, tilt, shear, peaks)
586
+
587
+ return tilt, shear
588
+
589
+
590
+ # TODO allow other line shapes
591
+ def gaussian(x, A, mu, sig):
592
+ """
593
+ A: height
594
+ mu: offset from central line
595
+ sig: standard deviation
596
+ """
597
+ return A * np.exp(-np.power(x - mu, 2.0) / (2 * np.power(sig, 2.0)))
598
+
599
+
600
+ def lorentzian(x, A, x0, mu):
601
+ """
602
+ A: height
603
+ x0: offset from central line
604
+ mu: width of lorentzian
605
+ """
606
+ return A * mu / ((x - x0) ** 2 + 0.25 * mu**2)
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file