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