teareduce 0.4.4__tar.gz → 0.4.6__tar.gz

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 (36) hide show
  1. {teareduce-0.4.4/src/teareduce.egg-info → teareduce-0.4.6}/PKG-INFO +1 -1
  2. {teareduce-0.4.4 → teareduce-0.4.6}/pyproject.toml +3 -0
  3. teareduce-0.4.6/src/teareduce/cleanest.py +694 -0
  4. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/ctext.py +3 -4
  5. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/draw_rectangle.py +5 -5
  6. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/imshow.py +40 -17
  7. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/peaks_spectrum.py +4 -4
  8. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/sliceregion.py +3 -2
  9. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/statsummary.py +5 -5
  10. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/version.py +1 -1
  11. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/wavecal.py +28 -31
  12. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/zscale.py +4 -4
  13. {teareduce-0.4.4 → teareduce-0.4.6/src/teareduce.egg-info}/PKG-INFO +1 -1
  14. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce.egg-info/SOURCES.txt +2 -0
  15. teareduce-0.4.6/src/teareduce.egg-info/entry_points.txt +2 -0
  16. {teareduce-0.4.4 → teareduce-0.4.6}/LICENSE.txt +0 -0
  17. {teareduce-0.4.4 → teareduce-0.4.6}/README.md +0 -0
  18. {teareduce-0.4.4 → teareduce-0.4.6}/setup.cfg +0 -0
  19. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/__init__.py +0 -0
  20. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/avoid_astropy_warnings.py +0 -0
  21. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/correct_pincushion_distortion.py +0 -0
  22. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/cosmicrays.py +0 -0
  23. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/elapsed_time.py +0 -0
  24. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/histogram1d.py +0 -0
  25. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/numsplines.py +0 -0
  26. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/polfit.py +0 -0
  27. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/robust_std.py +0 -0
  28. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/sdistortion.py +0 -0
  29. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/simulateccdexposure.py +0 -0
  30. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/tests/__init__.py +0 -0
  31. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/tests/test_sliceregion.py +0 -0
  32. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/tests/test_version.py +0 -0
  33. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce/write_array_to_fits.py +0 -0
  34. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce.egg-info/dependency_links.txt +0 -0
  35. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce.egg-info/requires.txt +0 -0
  36. {teareduce-0.4.4 → teareduce-0.4.6}/src/teareduce.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: teareduce
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Utilities for astronomical data reduction
5
5
  Author-email: Nicolás Cardiel <cardiel@ucm.es>
6
6
  License: GPL-3.0-or-later
@@ -46,6 +46,9 @@ test = [
46
46
  Homepage = "https://github.com/nicocardiel/teareduce"
47
47
  Repository = "https://github.com/nicocardiel/teareduce.git"
48
48
 
49
+ [project.scripts]
50
+ tea-cleanest = "teareduce.cleanest:main"
51
+
49
52
  [tool.setuptools.dynamic]
50
53
  version = {attr = "teareduce.version.VERSION"}
51
54
 
@@ -0,0 +1,694 @@
1
+ #
2
+ # Copyright 2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0+
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ """Interactive Cosmic Ray cleaning tool."""
11
+
12
+ import argparse
13
+ import tkinter as tk
14
+ from tkinter import filedialog
15
+ from tkinter import simpledialog
16
+
17
+ from astropy.io import fits
18
+ from ccdproc import cosmicray_lacosmic
19
+ import matplotlib.pyplot as plt
20
+ from matplotlib.backend_bases import key_press_handler
21
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
22
+ import numpy as np
23
+ import os
24
+ from scipy import ndimage
25
+
26
+ from .imshow import imshow
27
+ from .sliceregion import SliceRegion2D
28
+ from .zscale import zscale
29
+
30
+ import matplotlib
31
+ matplotlib.use("TkAgg")
32
+
33
+
34
+ class ReviewCosmicRay():
35
+ """Class to review suspected cosmic ray pixels."""
36
+
37
+ def __init__(self, root, data, mask_fixed, mask_crfound):
38
+ """Initialize the review window.
39
+
40
+ Parameters
41
+ ----------
42
+ root : tk.Tk
43
+ The main Tkinter window.
44
+ data : 2D numpy array
45
+ The original image data.
46
+ mask_fixed : 2D numpy array
47
+ Mask of previously corrected pixels.
48
+ mask_crfound : 2D numpy array
49
+ Mask of new pixels identified as cosmic rays.
50
+ """
51
+ self.root = root
52
+ self.data = data
53
+ self.data_original = data.copy()
54
+ self.mask_fixed = mask_fixed
55
+ self.mask_crfound = mask_crfound
56
+ self.first_plot = True
57
+ self.degree = 1 # Degree of polynomial for interpolation
58
+ self.npoints = 2 # Number of points at each side of the CR pixel for interpolation
59
+ # Label connected components in the mask; note that by default,
60
+ # structure is a cross [0,1,0;1,1,1;0,1,0], but we want to consider
61
+ # diagonal connections too, so we define a 3x3 square.
62
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
63
+ self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
64
+ # Make a copy of the original labels to allow pixel re-marking
65
+ self.cr_labels_original = self.cr_labels.copy()
66
+ print(f"Number of cosmic ray pixels detected: {np.sum(self.mask_crfound)}")
67
+ print(f"Number of cosmic rays detected: {self.num_features}")
68
+ if self.num_features == 0:
69
+ print('No CR hits found!')
70
+ else:
71
+ self.cr_index = 1
72
+ self.create_widgets()
73
+
74
+ def create_widgets(self):
75
+ self.review_window = tk.Toplevel(self.root)
76
+ self.review_window.title("Review Cosmic Rays")
77
+ self.review_window.geometry("800x700+100+100")
78
+
79
+ self.button_frame1 = tk.Frame(self.review_window)
80
+ self.button_frame1.pack(pady=5)
81
+ self.remove_crosses_button = tk.Button(self.button_frame1, text="remove all X's", command=self.remove_crosses)
82
+ self.remove_crosses_button.pack(side=tk.LEFT, padx=5)
83
+ self.restore_cr_button = tk.Button(self.button_frame1, text="[r]estore CR", command=self.restore_cr)
84
+ self.restore_cr_button.pack(side=tk.LEFT, padx=5)
85
+ self.restore_cr_button.config(state=tk.DISABLED)
86
+ self.next_button = tk.Button(self.button_frame1, text="[c]ontinue", command=self.continue_cr)
87
+ self.next_button.pack(side=tk.LEFT, padx=5)
88
+
89
+ self.button_frame2 = tk.Frame(self.review_window)
90
+ self.button_frame2.pack(pady=5)
91
+ self.ndeg_label = tk.Button(self.button_frame2, text=f"deg={self.degree}, n={self.npoints}",
92
+ command=self.set_ndeg)
93
+ self.ndeg_label.pack(side=tk.LEFT, padx=5)
94
+ self.interp_x_button = tk.Button(self.button_frame2, text="[x] interp.", command=self.interp_x)
95
+ self.interp_x_button.pack(side=tk.LEFT, padx=5)
96
+ self.interp_y_button = tk.Button(self.button_frame2, text="[y] interp.", command=self.interp_y)
97
+ self.interp_y_button.pack(side=tk.LEFT, padx=5)
98
+ self.interp_s_button = tk.Button(self.button_frame2, text="[s] interp.", command=self.interp_s)
99
+ self.interp_s_button.pack(side=tk.LEFT, padx=5)
100
+
101
+ self.button_frame3 = tk.Frame(self.review_window)
102
+ self.button_frame3.pack(pady=5)
103
+ vmin, vmax = zscale(self.data)
104
+ self.vmin_button = tk.Button(self.button_frame3, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
105
+ self.vmin_button.pack(side=tk.LEFT, padx=5)
106
+ self.vmax_button = tk.Button(self.button_frame3, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
107
+ self.vmax_button.pack(side=tk.LEFT, padx=5)
108
+ self.set_minmax_button = tk.Button(self.button_frame3, text="minmax [,]", command=self.set_minmax)
109
+ self.set_minmax_button.pack(side=tk.LEFT, padx=5)
110
+ self.set_zscale_button = tk.Button(self.button_frame3, text="zscale [/]", command=self.set_zscale)
111
+ self.set_zscale_button.pack(side=tk.LEFT, padx=5)
112
+ self.exit_button = tk.Button(self.button_frame3, text="[e]xit review", command=self.exit_review)
113
+ self.exit_button.pack(side=tk.LEFT, padx=5)
114
+
115
+ self.fig, self.ax = plt.subplots(figsize=(8, 5))
116
+ self.canvas = FigureCanvasTkAgg(self.fig, master=self.review_window)
117
+ # The next two instructions prevent a segmentation fault when pressing "q"
118
+ self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
119
+ self.canvas.mpl_connect("key_press_event", self.on_key)
120
+ self.canvas.mpl_connect("button_press_event", self.on_click)
121
+ self.canvas_widget = self.canvas.get_tk_widget()
122
+ self.canvas_widget.pack(fill=tk.BOTH, expand=True)
123
+
124
+ # Matplotlib toolbar
125
+ self.toolbar_frame = tk.Frame(self.review_window)
126
+ self.toolbar_frame.pack(fill=tk.X, expand=False, pady=5)
127
+ self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
128
+ self.toolbar.update()
129
+
130
+ self.update_display()
131
+
132
+ self.root.wait_window(self.review_window)
133
+
134
+ def update_display(self):
135
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
136
+ ycr_list_original, xcr_list_original = np.where(self.cr_labels_original == self.cr_index)
137
+ if self.first_plot:
138
+ print(f"Cosmic ray {self.cr_index}: "
139
+ f"Number of pixels = {len(xcr_list)}, "
140
+ f"Centroid = ({np.mean(xcr_list):.2f}, {np.mean(ycr_list):.2f})")
141
+ # Use original positions to define the region to display in order
142
+ # to avoid image shifts when some pixels are unmarked or new ones are marked
143
+ i0 = int(np.mean(ycr_list_original) + 0.5)
144
+ j0 = int(np.mean(xcr_list_original) + 0.5)
145
+ jmin = j0 - 15 if j0 - 15 >= 0 else 0
146
+ jmax = j0 + 15 if j0 + 15 < self.data.shape[1] else self.data.shape[1] - 1
147
+ imin = i0 - 15 if i0 - 15 >= 0 else 0
148
+ imax = i0 + 15 if i0 + 15 < self.data.shape[0] else self.data.shape[0] - 1
149
+ self.region = SliceRegion2D(f'[{jmin+1}:{jmax+1}, {imin+1}:{imax+1}]', mode='fits').python
150
+ self.ax.clear()
151
+ vmin = self.get_vmin()
152
+ vmax = self.get_vmax()
153
+ xlabel = 'X pixel (from 1 to NAXIS1)'
154
+ ylabel = 'Y pixel (from 1 to NAXIS2)'
155
+ self.image_review, _, _ = imshow(self.fig, self.ax, self.data[self.region], colorbar=False,
156
+ xlabel=xlabel, ylabel=ylabel,
157
+ vmin=vmin, vmax=vmax)
158
+ self.image_review.set_extent([jmin + 0.5, jmax + 1.5, imin + 0.5, imax + 1.5])
159
+ xlim = self.ax.get_xlim()
160
+ ylim = self.ax.get_ylim()
161
+ for xcr, ycr in zip(xcr_list, ycr_list):
162
+ xcr += 1 # from index to pixel
163
+ ycr += 1 # from index to pixel
164
+ self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr + 0.5, ycr - 0.5], 'r-')
165
+ self.ax.plot([xcr - 0.5, xcr + 0.5], [ycr - 0.5, ycr + 0.5], 'r-')
166
+ self.ax.set_xlim(xlim)
167
+ self.ax.set_ylim(ylim)
168
+ self.ax.set_title(f"Cosmic ray #{self.cr_index}/{self.num_features}")
169
+ if self.first_plot:
170
+ self.first_plot = False
171
+ self.fig.tight_layout()
172
+ self.canvas.draw()
173
+
174
+ def set_vmin(self):
175
+ old_vmin = self.get_vmin()
176
+ new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
177
+ if new_vmin is None:
178
+ return
179
+ self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
180
+ self.image_review.set_clim(vmin=new_vmin)
181
+ self.canvas.draw()
182
+
183
+ def set_vmax(self):
184
+ old_vmax = self.get_vmax()
185
+ new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
186
+ if new_vmax is None:
187
+ return
188
+ self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
189
+ self.image_review.set_clim(vmax=new_vmax)
190
+ self.canvas.draw()
191
+
192
+ def get_vmin(self):
193
+ return float(self.vmin_button.cget("text").split(":")[1])
194
+
195
+ def get_vmax(self):
196
+ return float(self.vmax_button.cget("text").split(":")[1])
197
+
198
+ def set_minmax(self):
199
+ vmin_new = np.min(self.data[self.region])
200
+ vmax_new = np.max(self.data[self.region])
201
+ self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
202
+ self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
203
+ self.image_review.set_clim(vmin=vmin_new)
204
+ self.image_review.set_clim(vmax=vmax_new)
205
+ self.canvas.draw()
206
+
207
+ def set_zscale(self):
208
+ vmin_new, vmax_new = zscale(self.data[self.region])
209
+ self.vmin_button.config(text=f"vmin: {vmin_new:.2f}")
210
+ self.vmax_button.config(text=f"vmax: {vmax_new:.2f}")
211
+ self.image_review.set_clim(vmin=vmin_new)
212
+ self.image_review.set_clim(vmax=vmax_new)
213
+ self.canvas.draw()
214
+
215
+ def set_ndeg(self):
216
+ new_degree = simpledialog.askinteger("Set degree", "Enter new degree (min=0):",
217
+ initialvalue=self.degree, minvalue=0)
218
+ if new_degree is None:
219
+ return
220
+ new_npoints = simpledialog.askinteger("Set n", f"Enter new n (min={2*new_degree}):",
221
+ initialvalue=self.npoints, minvalue=2*new_degree)
222
+ if new_npoints is None:
223
+ return
224
+ self.degree = new_degree
225
+ self.npoints = new_npoints
226
+ self.ndeg_label.config(text=f"deg={self.degree}, n={self.npoints}")
227
+
228
+ def interp_x(self):
229
+ print(f"X-interpolation of cosmic ray {self.cr_index}")
230
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
231
+ ycr_min = np.min(ycr_list)
232
+ ycr_max = np.max(ycr_list)
233
+ xfit_all = []
234
+ yfit_all = []
235
+ for ycr in range(ycr_min, ycr_max + 1):
236
+ xmarked = xcr_list[np.where(ycr_list == ycr)]
237
+ if len(xmarked) > 0:
238
+ jmin = np.min(xmarked)
239
+ jmax = np.max(xmarked)
240
+ # mark intermediate pixels too
241
+ for ix in range(jmin, jmax + 1):
242
+ self.cr_labels[ycr, ix] = self.cr_index
243
+ xmarked = xcr_list[np.where(ycr_list == ycr)]
244
+ xfit = []
245
+ zfit = []
246
+ for i in range(jmin - self.npoints, jmin):
247
+ if 0 <= i < self.data.shape[1]:
248
+ xfit.append(i)
249
+ xfit_all.append(i)
250
+ yfit_all.append(ycr)
251
+ zfit.append(self.data[ycr, i])
252
+ for i in range(jmax + 1, jmax + 1 + self.npoints):
253
+ if 0 <= i < self.data.shape[1]:
254
+ xfit.append(i)
255
+ xfit_all.append(i)
256
+ yfit_all.append(ycr)
257
+ zfit.append(self.data[ycr, i])
258
+ if len(xfit) > self.degree:
259
+ p = np.polyfit(xfit, zfit, self.degree)
260
+ for i in range(jmin, jmax + 1):
261
+ if 0 <= i < self.data.shape[1]:
262
+ self.data[ycr, i] = np.polyval(p, i)
263
+ self.mask_fixed[ycr, i] = True
264
+ else:
265
+ print(f"Not enough points to fit at y={ycr+1}")
266
+ self.update_display()
267
+ return
268
+ self.restore_cr_button.config(state=tk.NORMAL)
269
+ self.remove_crosses_button.config(state=tk.DISABLED)
270
+ self.interp_x_button.config(state=tk.DISABLED)
271
+ self.interp_y_button.config(state=tk.DISABLED)
272
+ self.interp_s_button.config(state=tk.DISABLED)
273
+ self.update_display()
274
+ if len(xfit_all) > 0:
275
+ self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
276
+ self.canvas.draw()
277
+
278
+ def interp_y(self):
279
+ print(f"Y-interpolation of cosmic ray {self.cr_index}")
280
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
281
+ xcr_min = np.min(xcr_list)
282
+ xcr_max = np.max(xcr_list)
283
+ xfit_all = []
284
+ yfit_all = []
285
+ for xcr in range(xcr_min, xcr_max + 1):
286
+ ymarked = ycr_list[np.where(xcr_list == xcr)]
287
+ if len(ymarked) > 0:
288
+ imin = np.min(ymarked)
289
+ imax = np.max(ymarked)
290
+ # mark intermediate pixels too
291
+ for iy in range(imin, imax + 1):
292
+ self.cr_labels[iy, xcr] = self.cr_index
293
+ ymarked = ycr_list[np.where(xcr_list == xcr)]
294
+ yfit = []
295
+ zfit = []
296
+ for i in range(imin - self.npoints, imin):
297
+ if 0 <= i < self.data.shape[0]:
298
+ yfit.append(i)
299
+ yfit_all.append(i)
300
+ xfit_all.append(xcr)
301
+ zfit.append(self.data[i, xcr])
302
+ for i in range(imax + 1, imax + 1 + self.npoints):
303
+ if 0 <= i < self.data.shape[0]:
304
+ yfit.append(i)
305
+ yfit_all.append(i)
306
+ xfit_all.append(xcr)
307
+ zfit.append(self.data[i, xcr])
308
+ if len(yfit) > self.degree:
309
+ p = np.polyfit(yfit, zfit, self.degree)
310
+ for i in range(imin, imax + 1):
311
+ if 0 <= i < self.data.shape[1]:
312
+ self.data[i, xcr] = np.polyval(p, i)
313
+ self.mask_fixed[i, xcr] = True
314
+ else:
315
+ print(f"Not enough points to fit at x={xcr+1}")
316
+ self.update_display()
317
+ return
318
+ self.restore_cr_button.config(state=tk.NORMAL)
319
+ self.remove_crosses_button.config(state=tk.DISABLED)
320
+ self.interp_x_button.config(state=tk.DISABLED)
321
+ self.interp_y_button.config(state=tk.DISABLED)
322
+ self.interp_s_button.config(state=tk.DISABLED)
323
+ self.update_display()
324
+ if len(xfit_all) > 0:
325
+ self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
326
+ self.canvas.draw()
327
+
328
+ def interp_s(self):
329
+ print(f"S-interpolation of cosmic ray {self.cr_index}")
330
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
331
+ ycr_min = np.min(ycr_list)
332
+ ycr_max = np.max(ycr_list)
333
+ xfit_all = []
334
+ yfit_all = []
335
+ zfit_all = []
336
+ # First do horizontal lines
337
+ for ycr in range(ycr_min, ycr_max + 1):
338
+ xmarked = xcr_list[np.where(ycr_list == ycr)]
339
+ if len(xmarked) > 0:
340
+ jmin = np.min(xmarked)
341
+ jmax = np.max(xmarked)
342
+ # mark intermediate pixels too
343
+ for ix in range(jmin, jmax + 1):
344
+ self.cr_labels[ycr, ix] = self.cr_index
345
+ xmarked = xcr_list[np.where(ycr_list == ycr)]
346
+ for i in range(jmin - self.npoints, jmin):
347
+ if 0 <= i < self.data.shape[1]:
348
+ xfit_all.append(i)
349
+ yfit_all.append(ycr)
350
+ zfit_all.append(self.data[ycr, i])
351
+ for i in range(jmax + 1, jmax + 1 + self.npoints):
352
+ if 0 <= i < self.data.shape[1]:
353
+ xfit_all.append(i)
354
+ yfit_all.append(ycr)
355
+ zfit_all.append(self.data[ycr, i])
356
+ xcr_min = np.min(xcr_list)
357
+ # Now do vertical lines
358
+ xcr_max = np.max(xcr_list)
359
+ for xcr in range(xcr_min, xcr_max + 1):
360
+ ymarked = ycr_list[np.where(xcr_list == xcr)]
361
+ if len(ymarked) > 0:
362
+ imin = np.min(ymarked)
363
+ imax = np.max(ymarked)
364
+ # mark intermediate pixels too
365
+ for iy in range(imin, imax + 1):
366
+ self.cr_labels[iy, xcr] = self.cr_index
367
+ ymarked = ycr_list[np.where(xcr_list == xcr)]
368
+ for i in range(imin - self.npoints, imin):
369
+ if 0 <= i < self.data.shape[0]:
370
+ yfit_all.append(i)
371
+ xfit_all.append(xcr)
372
+ zfit_all.append(self.data[i, xcr])
373
+ for i in range(imax + 1, imax + 1 + self.npoints):
374
+ if 0 <= i < self.data.shape[0]:
375
+ yfit_all.append(i)
376
+ xfit_all.append(xcr)
377
+ zfit_all.append(self.data[i, xcr])
378
+ if len(xfit_all) > 3:
379
+ # Construct the design matrix for a 2D polynomial fit to a plane,
380
+ # where each row corresponds to a point (x, y, z) and the model
381
+ # is z = C[0]*x + C[1]*y + C[2]
382
+ A = np.c_[xfit_all, yfit_all, np.ones(len(xfit_all))]
383
+ # Least squares polynomial fit
384
+ C, _, _, _ = np.linalg.lstsq(A, zfit_all, rcond=None)
385
+ # recompute all CR pixels to take into account "holes" between marked pixels
386
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
387
+ for iy, ix in zip(ycr_list, xcr_list):
388
+ self.data[iy, ix] = C[0] * ix + C[1] * iy + C[2]
389
+ self.mask_fixed[iy, ix] = True
390
+ else:
391
+ print("Not enough points to fit a plane")
392
+ self.update_display()
393
+ return
394
+ self.restore_cr_button.config(state=tk.NORMAL)
395
+ self.remove_crosses_button.config(state=tk.DISABLED)
396
+ self.interp_x_button.config(state=tk.DISABLED)
397
+ self.interp_y_button.config(state=tk.DISABLED)
398
+ self.interp_s_button.config(state=tk.DISABLED)
399
+ self.update_display()
400
+ if len(xfit_all) > 0:
401
+ self.ax.plot(np.array(xfit_all) + 1, np.array(yfit_all) + 1, 'mo', markersize=4) # +1: from index to pixel
402
+ self.canvas.draw()
403
+
404
+ def remove_crosses(self):
405
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
406
+ for iy, ix in zip(ycr_list, xcr_list):
407
+ self.cr_labels[iy, ix] = 0
408
+ print(f"Removed all pixels of cosmic ray {self.cr_index}")
409
+ self.remove_crosses_button.config(state=tk.DISABLED)
410
+ self.interp_x_button.config(state=tk.DISABLED)
411
+ self.interp_y_button.config(state=tk.DISABLED)
412
+ self.interp_s_button.config(state=tk.DISABLED)
413
+ self.update_display()
414
+
415
+ def restore_cr(self):
416
+ ycr_list, xcr_list = np.where(self.cr_labels == self.cr_index)
417
+ for iy, ix in zip(ycr_list, xcr_list):
418
+ self.data[iy, ix] = self.data_original[iy, ix]
419
+ self.interp_x_button.config(state=tk.NORMAL)
420
+ self.interp_y_button.config(state=tk.NORMAL)
421
+ self.interp_s_button.config(state=tk.NORMAL)
422
+ print(f"Restored all pixels of cosmic ray {self.cr_index}")
423
+ self.remove_crosses_button.config(state=tk.NORMAL)
424
+ self.restore_cr_button.config(state=tk.DISABLED)
425
+ self.update_display()
426
+
427
+ def continue_cr(self):
428
+ self.cr_index += 1
429
+ if self.cr_index > self.num_features:
430
+ self.cr_index = 1
431
+ self.first_plot = True
432
+ self.restore_cr_button.config(state=tk.DISABLED)
433
+ self.interp_x_button.config(state=tk.NORMAL)
434
+ self.interp_y_button.config(state=tk.NORMAL)
435
+ self.interp_s_button.config(state=tk.NORMAL)
436
+ self.update_display()
437
+
438
+ def exit_review(self):
439
+ self.review_window.destroy()
440
+
441
+ def on_key(self, event):
442
+ if event.key == 'q':
443
+ pass # Ignore the "q" key to prevent closing the window
444
+ elif event.key == 'r':
445
+ if self.restore_cr_button.cget("state") != "disabled":
446
+ self.restore_cr()
447
+ elif event.key == 'x':
448
+ if self.interp_x_button.cget("state") != "disabled":
449
+ self.interp_x()
450
+ elif event.key == 'y':
451
+ if self.interp_y_button.cget("state") != "disabled":
452
+ self.interp_y()
453
+ elif event.key == 's':
454
+ if self.interp_s_button.cget("state") != "disabled":
455
+ self.interp_s()
456
+ elif event.key == 'right' or event.key == 'c':
457
+ self.continue_cr()
458
+ elif event.key == ',':
459
+ self.set_minmax()
460
+ elif event.key == '/':
461
+ self.set_zscale()
462
+ elif event.key == 'e':
463
+ self.exit_review()
464
+ else:
465
+ print(f"Key pressed: {event.key}")
466
+
467
+ def on_click(self, event):
468
+ if event.inaxes:
469
+ x, y = event.xdata, event.ydata
470
+ print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
471
+ ix = int(x+0.5) - 1 # from pixel to index
472
+ iy = int(y+0.5) - 1 # from pixel to index
473
+ if int(self.cr_labels[iy, ix]) == self.cr_index:
474
+ self.cr_labels[iy, ix] = 0
475
+ print(f"Pixel ({ix+1}, {iy+1}) unmarked as cosmic ray.")
476
+ else:
477
+ self.cr_labels[iy, ix] = self.cr_index
478
+ print(f"Pixel ({ix+1}, {iy+1}) marked as cosmic ray.")
479
+ xcr_list, ycr_list = np.where(self.cr_labels == self.cr_index)
480
+ if len(xcr_list) == 0:
481
+ self.interp_x_button.config(state=tk.DISABLED)
482
+ self.interp_y_button.config(state=tk.DISABLED)
483
+ self.interp_s_button.config(state=tk.DISABLED)
484
+ self.remove_crosses_button.config(state=tk.DISABLED)
485
+ else:
486
+ self.interp_x_button.config(state=tk.NORMAL)
487
+ self.interp_y_button.config(state=tk.NORMAL)
488
+ self.interp_s_button.config(state=tk.NORMAL)
489
+ self.remove_crosses_button.config(state=tk.NORMAL)
490
+ # Update the display to reflect the change
491
+ self.update_display()
492
+
493
+
494
+ class CosmicRayCleanerApp():
495
+ """Main application class for cosmic ray cleaning."""
496
+
497
+ def __init__(self, root, input_fits, extension=0, output_fits=None):
498
+ """
499
+ Initialize the application.
500
+
501
+ Parameters
502
+ ----------
503
+ root : tk.Tk
504
+ The main Tkinter window.
505
+ input_fits : str
506
+ Path to the FITS file to be cleaned.
507
+ extension : int, optional
508
+ FITS extension to use (default is 0).
509
+ output_fits : str, optional
510
+ Path to save the cleaned FITS file (default is None, which prompts
511
+ for a save location).
512
+ """
513
+ self.root = root
514
+ self.root.title("Cosmic Ray Cleaner")
515
+ self.root.geometry("800x700+50+0")
516
+ self.input_fits = input_fits
517
+ self.extension = extension
518
+ self.output_fits = output_fits
519
+ self.load_fits_file()
520
+ self.create_widgets()
521
+
522
+ def load_fits_file(self):
523
+ try:
524
+ with fits.open(self.input_fits, mode='readonly') as hdul:
525
+ self.data = hdul[self.extension].data
526
+ if 'CRMASK' in hdul:
527
+ self.mask_fixed = hdul['CRMASK'].data.astype(bool)
528
+ else:
529
+ self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
530
+ except Exception as e:
531
+ print(f"Error loading FITS file: {e}")
532
+
533
+ def save_fits_file(self):
534
+ if self.output_fits is None:
535
+ base, ext = os.path.splitext(self.input_fits)
536
+ suggested_name = f"{base}_cleaned"
537
+ else:
538
+ suggested_name, _ = os.path.splitext(self.output_fits)
539
+ self.output_fits = filedialog.asksaveasfilename(
540
+ initialdir=os.getcwd(),
541
+ title="Save cleaned FITS file",
542
+ defaultextension=".fits",
543
+ filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
544
+ initialfile=suggested_name
545
+ )
546
+ try:
547
+ with fits.open(self.input_fits, mode='readonly') as hdul:
548
+ hdul[self.extension].data = self.data
549
+ if 'CRMASK' in hdul:
550
+ hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
551
+ else:
552
+ crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name='CRMASK')
553
+ hdul.append(crmask_hdu)
554
+ hdul.writeto(self.output_fits, overwrite=True)
555
+ print(f"Cleaned data saved to {self.output_fits}")
556
+ except Exception as e:
557
+ print(f"Error saving FITS file: {e}")
558
+
559
+ def create_widgets(self):
560
+ # Row 1
561
+ self.button_frame1 = tk.Frame(self.root)
562
+ self.button_frame1.grid(row=0, column=0, pady=5)
563
+ self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
564
+ self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
565
+ self.save_button = tk.Button(self.button_frame1, text="Save cleaned FITS", command=self.save_fits_file)
566
+ self.save_button.pack(side=tk.LEFT, padx=5)
567
+
568
+ # Row 2
569
+ self.button_frame2 = tk.Frame(self.root)
570
+ self.button_frame2.grid(row=1, column=0, pady=5)
571
+ vmin, vmax = zscale(self.data)
572
+ self.vmin_button = tk.Button(self.button_frame2, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
573
+ self.vmin_button.pack(side=tk.LEFT, padx=5)
574
+ self.vmax_button = tk.Button(self.button_frame2, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
575
+ self.vmax_button.pack(side=tk.LEFT, padx=5)
576
+ self.stop_button = tk.Button(self.button_frame2, text="Stop program", command=self.stop_app)
577
+ self.stop_button.pack(side=tk.LEFT, padx=5)
578
+
579
+ # Main frame for figure and toolbar
580
+ self.main_frame = tk.Frame(self.root)
581
+ self.main_frame.grid(row=2, column=0, sticky="nsew")
582
+ self.root.grid_rowconfigure(2, weight=1)
583
+ self.root.grid_columnconfigure(0, weight=1)
584
+ self.main_frame.grid_rowconfigure(0, weight=1)
585
+ self.main_frame.grid_columnconfigure(0, weight=1)
586
+
587
+ # Create figure and axis
588
+ self.fig, self.ax = plt.subplots(figsize=(8, 6))
589
+ xlabel = 'X pixel (from 1 to NAXIS1)'
590
+ ylabel = 'Y pixel (from 1 to NAXIS2)'
591
+ extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
592
+ self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
593
+ xlabel=xlabel, ylabel=ylabel, extent=extent)
594
+ # Note: tight_layout should be called before defining the canvas
595
+ self.fig.tight_layout()
596
+
597
+ # Create canvas and toolbar
598
+ self.canvas = FigureCanvasTkAgg(self.fig, master=self.main_frame)
599
+ # The next two instructions prevent a segmentation fault when pressing "q"
600
+ self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
601
+ self.canvas.mpl_connect("key_press_event", self.on_key)
602
+ self.canvas.mpl_connect("button_press_event", self.on_click)
603
+ canvas_widget = self.canvas.get_tk_widget()
604
+ canvas_widget.grid(row=0, column=0, sticky="nsew")
605
+
606
+ # Matplotlib toolbar
607
+ self.toolbar_frame = tk.Frame(self.main_frame)
608
+ self.toolbar_frame.grid(row=1, column=0, sticky="ew")
609
+ self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
610
+ self.toolbar.update()
611
+
612
+ def set_vmin(self):
613
+ old_vmin = self.get_vmin()
614
+ new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
615
+ if new_vmin is None:
616
+ return
617
+ self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
618
+ self.image.set_clim(vmin=new_vmin)
619
+ self.canvas.draw()
620
+
621
+ def set_vmax(self):
622
+ old_vmax = self.get_vmax()
623
+ new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
624
+ if new_vmax is None:
625
+ return
626
+ self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
627
+ self.image.set_clim(vmax=new_vmax)
628
+ self.canvas.draw()
629
+
630
+ def get_vmin(self):
631
+ return float(self.vmin_button.cget("text").split(":")[1])
632
+
633
+ def get_vmax(self):
634
+ return float(self.vmax_button.cget("text").split(":")[1])
635
+
636
+ def run_lacosmic(self):
637
+ self.run_lacosmic_button.config(state=tk.DISABLED)
638
+ self.stop_button.config(state=tk.DISABLED)
639
+ # Parameters for L.A.Cosmic can be adjusted as needed
640
+ _, mask_crfound = cosmicray_lacosmic(self.data, sigclip=4.5, sigfrac=0.3, objlim=5.0, verbose=True)
641
+ ReviewCosmicRay(
642
+ root=self.root,
643
+ data=self.data,
644
+ mask_fixed=self.mask_fixed,
645
+ mask_crfound=mask_crfound
646
+ )
647
+ print("L.A.Cosmic cleaning applied.")
648
+ self.run_lacosmic_button.config(state=tk.NORMAL)
649
+ self.stop_button.config(state=tk.NORMAL)
650
+
651
+ def stop_app(self):
652
+ self.root.quit()
653
+ self.root.destroy()
654
+
655
+ def on_key(self, event):
656
+ if event.key == 'q':
657
+ pass # Ignore the "q" key to prevent closing the window
658
+ else:
659
+ print(f"Key pressed: {event.key}")
660
+
661
+ def on_click(self, event):
662
+ if event.inaxes:
663
+ x, y = event.xdata, event.ydata
664
+ print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
665
+
666
+
667
+ def main():
668
+ parser = argparse.ArgumentParser(description="Interactive cosmic ray cleaner for FITS images.")
669
+ parser.add_argument("input_fits", help="Path to the FITS file to be cleaned.")
670
+ parser.add_argument("--extension", type=int, default=0,
671
+ help="FITS extension to use (default: 0).")
672
+ parser.add_argument("--output_fits", type=str, default=None,
673
+ help="Path to save the cleaned FITS file")
674
+ args = parser.parse_args()
675
+
676
+ if not os.path.isfile(args.input_fits):
677
+ print(f"Error: File '{args.input_fits}' does not exist.")
678
+ return
679
+ if args.output_fits is not None and os.path.isfile(args.output_fits):
680
+ print(f"Error: Output file '{args.output_fits}' already exists.")
681
+ return
682
+
683
+ # Initialize Tkinter root
684
+ root = tk.Tk()
685
+
686
+ # Create and run the application
687
+ CosmicRayCleanerApp(root, args.input_fits, args.extension, args.output_fits)
688
+
689
+ # Execute
690
+ root.mainloop()
691
+
692
+
693
+ if __name__ == "__main__":
694
+ main()
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2022-2024 Universidad Complutense de Madrid
2
+ # Copyright 2022-2025 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -9,8 +9,8 @@
9
9
 
10
10
 
11
11
  def ctext(s=None,
12
- fg=None,
13
- bg=None,
12
+ fg=None,
13
+ bg=None,
14
14
  under=False,
15
15
  rev=False,
16
16
  faint=False,
@@ -95,4 +95,3 @@ def ctext(s=None,
95
95
  result = f'{final_style}{s}\x1B[0m'
96
96
 
97
97
  return result
98
-
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2022-2024 Universidad Complutense de Madrid
2
+ # Copyright 2022-2025 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -56,11 +56,11 @@ def draw_rectangle(ax, image_data, x1, x2, y1, y2,
56
56
  ax.plot((x1, x2), (y2, y2), color, lw=1)
57
57
 
58
58
  if text:
59
- ax.text((x1+x2)/2, y1+(y2-y1)/8,
60
- '{:.{prec}f}'.format(mean, prec=ndigits),
61
- ha='center', va='center', color=color, fontsize=fontsize)
59
+ ax.text((x1+x2)/2, y1+(y2-y1)/8,
60
+ '{:.{prec}f}'.format(mean, prec=ndigits),
61
+ ha='center', va='center', color=color, fontsize=fontsize)
62
62
  ax.text((x1+x2)/2, y2-(y2-y1)/8,
63
- '{:.{prec}f}'.format(std, prec=ndigits),
63
+ '{:.{prec}f}'.format(std, prec=ndigits),
64
64
  ha='center', va='top', color=color, fontsize=fontsize)
65
65
 
66
66
  return mean, std
@@ -30,13 +30,18 @@ def imshowme(data, **kwargs):
30
30
  Instance of Axes.
31
31
  img : matplotlib AxesImage
32
32
  Instance returned by ax.imshow()
33
+ cax : matplotlib.axes.Axes or None
34
+ Instance of Axes where the color bar is drawn, or None if
35
+ colorbar is False.
36
+ cbar : matplotlib.colorbar.Colorbar or None
37
+ Instance of Colorbar, or None if colorbar is False.
33
38
  """
34
39
  fig, ax = plt.subplots()
35
- img = imshow(fig=fig, ax=ax, data=data, **kwargs)
36
- return fig, ax, img
40
+ img, cax, cbar = imshow(fig=fig, ax=ax, data=data, **kwargs)
41
+ return fig, ax, img, cax, cbar
37
42
 
38
43
 
39
- def imshow(fig=None, ax=None, data=None,
44
+ def imshow(fig=None, ax=None, data=None, ds9mode=False,
40
45
  crpix1=1, crval1=None, cdelt1=None, cunit1=None, cunitx=Unit('Angstrom'),
41
46
  xlabel=None, ylabel=None, title=None,
42
47
  colorbar=True, cblabel='Number of counts',
@@ -55,6 +60,10 @@ def imshow(fig=None, ax=None, data=None,
55
60
  Instance of Axes.
56
61
  data : numpy array
57
62
  2D array to be displayed.
63
+ ds9mode : bool
64
+ If True, the extent parameter is set to
65
+ [0.5, NAXIS1+0.5, 0.5, NAXIS2+0.5]
66
+ to mimic the DS9 display.
58
67
  crpix1 : astropy.units.Quantity
59
68
  Float number providing the CRPIX1 value: the reference pixel
60
69
  for which CRVAL1 is given.
@@ -85,7 +94,11 @@ def imshow(fig=None, ax=None, data=None,
85
94
  ------
86
95
  img : matplotlib AxesImage
87
96
  Instance returned by ax.imshow()
88
-
97
+ cax : matplotlib.axes.Axes or None
98
+ Instance of Axes where the color bar is drawn, or None if
99
+ colorbar is False.
100
+ cbar : matplotlib.colorbar.Colorbar or None
101
+ Instance of Colorbar, or None if colorbar is False.
89
102
  """
90
103
 
91
104
  # protections
@@ -94,13 +107,6 @@ def imshow(fig=None, ax=None, data=None,
94
107
  if not isinstance(ax, Axes):
95
108
  raise ValueError("Unexpected 'ax' argument")
96
109
 
97
- # default labels
98
- if xlabel is None:
99
- xlabel = 'X axis (array index)'
100
-
101
- if ylabel is None:
102
- ylabel = 'Y axis (array index)'
103
-
104
110
  wavecalib = False
105
111
  if crpix1 is not None and crval1 is not None and cdelt1 is not None and cunit1 is not None:
106
112
  if 'extent' in kwargs:
@@ -120,11 +126,25 @@ def imshow(fig=None, ax=None, data=None,
120
126
  aspect = 'auto'
121
127
  wavecalib = True
122
128
  else:
123
- if 'extent' in kwargs:
124
- extent = kwargs['extent']
125
- del kwargs['extent']
129
+ if ds9mode:
130
+ if 'extent' in kwargs:
131
+ raise ValueError('extent parameter can not be used with ds9mode=True')
132
+ naxis2, naxis1 = data.shape
133
+ extent = [0.5, naxis1 + 0.5, 0.5, naxis2 + 0.5]
134
+ if xlabel is None:
135
+ xlabel = 'X pixel (from 1 to NAXIS1)'
136
+ if ylabel is None:
137
+ ylabel = 'Y pixel (from 1 to NAXIS2)'
126
138
  else:
127
- extent = None
139
+ if xlabel is None:
140
+ xlabel = 'X axis (array index)'
141
+ if ylabel is None:
142
+ ylabel = 'Y axis (array index)'
143
+ if 'extent' in kwargs:
144
+ extent = kwargs['extent']
145
+ del kwargs['extent']
146
+ else:
147
+ extent = None
128
148
  if 'aspect' in kwargs:
129
149
  aspect = kwargs['aspect']
130
150
  del kwargs['aspect']
@@ -163,6 +183,9 @@ def imshow(fig=None, ax=None, data=None,
163
183
  if colorbar:
164
184
  divider = make_axes_locatable(ax)
165
185
  cax = divider.append_axes("right", size="5%", pad=0.05, axes_class=Axes)
166
- fig.colorbar(img, cax=cax, label=cblabel)
186
+ cbar = fig.colorbar(img, cax=cax, label=cblabel)
187
+ else:
188
+ cax = None
189
+ cbar = None
167
190
 
168
- return img
191
+ return img, cax, cbar
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright 2015-2024 Universidad Complutense de Madrid
3
+ # Copyright 2015-2025 Universidad Complutense de Madrid
4
4
  #
5
5
  # This file is part of teareduce
6
6
  #
@@ -58,12 +58,12 @@ def find_peaks_spectrum(sx, nwinwidth, deltaflux=0, threshold=0, debugplot=False
58
58
  'pixels will be ignored')
59
59
 
60
60
  xpeaks = [] # list to store the peaks
61
-
61
+
62
62
  if sx_shape[0] < nwinwidth:
63
63
  print('find_peaks_spectrum> sx shape......:', sx_shape)
64
64
  print('find_peaks_spectrum> nwinwidth.....:', nwinwidth)
65
65
  raise ValueError('sx.shape < nwinwidth')
66
-
66
+
67
67
  i = nmed
68
68
  while i < sx_shape[0] - nmed:
69
69
  if sx[i] > threshold:
@@ -93,7 +93,7 @@ def find_peaks_spectrum(sx, nwinwidth, deltaflux=0, threshold=0, debugplot=False
93
93
  i += 1
94
94
  else:
95
95
  i += 1
96
-
96
+
97
97
  ixpeaks = np.array(xpeaks)
98
98
 
99
99
  if debugplot:
@@ -8,7 +8,7 @@
8
8
  #
9
9
  """Auxiliary classes to handle slicing regions in 1D, 2D, and 3D.
10
10
 
11
- These classes provide a way to define and manipulate slices in a
11
+ These classes provide a way to define and manipulate slices in a
12
12
  consistent manner, following both FITS and Python conventions.
13
13
  """
14
14
 
@@ -16,9 +16,10 @@ import re
16
16
 
17
17
  import numpy as np
18
18
 
19
+
19
20
  class SliceRegion1D:
20
21
  """Store indices for slicing of 1D regions.
21
-
22
+
22
23
  The attributes .python and .fits provide the indices following
23
24
  the Python and the FITS convention, respectively.
24
25
 
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2022-2024 Universidad Complutense de Madrid
2
+ # Copyright 2022-2025 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -22,7 +22,7 @@ def statsummary(x=None, rm_nan=False, show=True):
22
22
  Parameters
23
23
  ----------
24
24
  x : numpy array or None
25
- Input array with values which statistical properties are
25
+ Input array with values which statistical properties are
26
26
  requested.
27
27
  rm_nan : bool
28
28
  If True, filter out NaN values before computing statistics.
@@ -42,9 +42,9 @@ def statsummary(x=None, rm_nan=False, show=True):
42
42
 
43
43
  # protections
44
44
  if x is None:
45
- return ['npoints', 'minimum', 'maximum',
46
- 'mean', 'median', 'std', 'robust_std',
47
- 'percentile16', 'percentile25', 'percentile75', 'percentile84']
45
+ return ['npoints', 'minimum', 'maximum',
46
+ 'mean', 'median', 'std', 'robust_std',
47
+ 'percentile16', 'percentile25', 'percentile75', 'percentile84']
48
48
 
49
49
  if isinstance(x, np.ndarray):
50
50
  xx = np.copy(x.flatten())
@@ -9,7 +9,7 @@
9
9
  #
10
10
  """Module to define the version of the teareduce package."""
11
11
 
12
- VERSION = '0.4.4'
12
+ VERSION = '0.4.6'
13
13
 
14
14
 
15
15
  def main():
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  #
3
- # Copyright 2015-2024 Universidad Complutense de Madrid
3
+ # Copyright 2015-2025 Universidad Complutense de Madrid
4
4
  #
5
5
  # This file is part of teareduce
6
6
  #
@@ -30,10 +30,10 @@ from .sliceregion import SliceRegion1D
30
30
 
31
31
  class TeaWaveCalibration:
32
32
  """Auxiliary class to compute and apply wavelength calibration.
33
-
33
+
34
34
  It is assumed that the wavelength and spatial directions correspond
35
35
  to the X (array columns) and Y (array rows) axes, respectively.
36
-
36
+
37
37
  Attributes
38
38
  ----------
39
39
  ns_window : int
@@ -44,7 +44,7 @@ class TeaWaveCalibration:
44
44
  Standard deviation for Gaussian kernel to smooth median spectrum.
45
45
  A value of 0 means that no smoothing is performed.
46
46
  nx_window : int
47
- Number of pixels (spectral direction) of the window where the
47
+ Number of pixels (spectral direction) of the window where the
48
48
  peaks are sought. It must be odd.
49
49
  delta_flux : float
50
50
  Minimum difference between the flux at the line center and the
@@ -293,7 +293,7 @@ class TeaWaveCalibration:
293
293
  Standard deviation for Gaussian kernel to smooth median spectrum.
294
294
  A value of 0 means that no smoothing is performed.
295
295
  nx_window : int or None
296
- Number of pixels (spectral direction) of the window where the
296
+ Number of pixels (spectral direction) of the window where the
297
297
  peaks are sought. It must be odd.
298
298
  delta_flux : float
299
299
  Minimum difference between the flux at the line center and the
@@ -335,7 +335,7 @@ class TeaWaveCalibration:
335
335
  self.delta_flux = delta_flux
336
336
  if method is not None:
337
337
  self.method = method
338
-
338
+
339
339
  naxis2, naxis1 = data.shape
340
340
 
341
341
  if self._naxis1 is None:
@@ -343,7 +343,7 @@ class TeaWaveCalibration:
343
343
  else:
344
344
  if naxis1 != self._naxis1:
345
345
  raise ValueError(f'Unexpected naxis1: {naxis1}')
346
-
346
+
347
347
  if self._naxis2 is None:
348
348
  self._naxis2 = naxis2
349
349
  else:
@@ -363,7 +363,7 @@ class TeaWaveCalibration:
363
363
 
364
364
  # initial median spectrum
365
365
  xpeaks, ixpeaks, sp_median_smooth = self._find_peaks_scan(
366
- data=data,
366
+ data=data,
367
367
  ns1=ns1,
368
368
  ns2=ns2,
369
369
  plot_peaks=plot_peaks,
@@ -420,12 +420,12 @@ class TeaWaveCalibration:
420
420
  xpeaks_reference=None,
421
421
  ns_range=None,
422
422
  direction='up',
423
- ns_window = None,
424
- threshold = None,
425
- sigma_smooth = None,
426
- nx_window = None,
427
- delta_flux = None,
428
- method = None,
423
+ ns_window=None,
424
+ threshold=None,
425
+ sigma_smooth=None,
426
+ nx_window=None,
427
+ delta_flux=None,
428
+ method=None,
429
429
  plots=False,
430
430
  title=None,
431
431
  pdf_output=None,
@@ -455,7 +455,7 @@ class TeaWaveCalibration:
455
455
  Standard deviation for Gaussian kernel to smooth median spectrum.
456
456
  A value of 0 means that no smoothing is performed.
457
457
  nx_window : int or None
458
- Number of pixels (spectral direction) of the window where the
458
+ Number of pixels (spectral direction) of the window where the
459
459
  peaks are sought. It must be odd.
460
460
  delta_flux : float
461
461
  Minimum difference between the flux at the line center and the
@@ -540,7 +540,7 @@ class TeaWaveCalibration:
540
540
  color_previous = ['blue', 'cyan']
541
541
  fig, ax = plt.subplots(figsize=(15, 15*naxis2/naxis1))
542
542
  vmin, vmax = np.percentile(data, [5, 95])
543
- img = imshow(fig, ax, data, vmin=vmin, vmax=vmax, cmap='gray', title=title, aspect='auto')
543
+ imshow(fig, ax, data, vmin=vmin, vmax=vmax, cmap='gray', title=title, aspect='auto')
544
544
  # display previously identified lines
545
545
  yplot = np.arange(naxis2)[self._valid_scans]
546
546
  for i in range(self._nlines_reference):
@@ -549,18 +549,17 @@ class TeaWaveCalibration:
549
549
  else:
550
550
  fig = None
551
551
  ax = None
552
- img = None
553
552
 
554
553
  # search for peaks in the 2D image
555
554
  dict_xpeaks = dict()
556
- for ns in tqdm(range(ns_min_fits, ns_max_fits + ns_step, ns_step),
555
+ for ns in tqdm(range(ns_min_fits, ns_max_fits + ns_step, ns_step),
557
556
  desc='Finding peaks', disable=disable_tqdm):
558
557
  ns1 = ns - self.ns_window // 2
559
558
  ns1 = max([ns1, min(ns_min_fits, ns_max_fits)])
560
559
  ns2 = ns + self.ns_window // 2
561
560
  ns2 = min([ns2, max(ns_min_fits, ns_max_fits)])
562
561
  xpeaks, ixpeaks, sp_median_smooth = self._find_peaks_scan(
563
- data=data,
562
+ data=data,
564
563
  ns1=ns1,
565
564
  ns2=ns2,
566
565
  plot_peaks=False,
@@ -585,7 +584,7 @@ class TeaWaveCalibration:
585
584
  xpeaks_predicted = np.median(self._xpeaks_all_lines_array[(ns1-1):ns2, :], axis=0)
586
585
  for i in range(self._nlines_reference):
587
586
  value = xpeaks_predicted[i]
588
- # if there is no peak near to the expected location
587
+ # if there is no peak near to the expected location
589
588
  # (within a distance given by self.nx_window) the line
590
589
  # is probably weak and we need to avoid jumping into
591
590
  # another line
@@ -661,7 +660,7 @@ class TeaWaveCalibration:
661
660
  self.peak_wavelengths = wavelengths
662
661
 
663
662
  @u.quantity_input(xpeaks=u.pixel)
664
- def overplot_identified_lines(self, xpeaks, spectrum,
663
+ def overplot_identified_lines(self, xpeaks, spectrum,
665
664
  title=None, fontsize_title=16, fontsize_wave=10,
666
665
  pdf_output=None, pdf_only=False):
667
666
  """Overplot identified lines
@@ -790,7 +789,7 @@ class TeaWaveCalibration:
790
789
  xfit = np.arange(self._naxis2)[self._valid_scans]
791
790
  if len(xfit) <= self.degree_cdistortion:
792
791
  raise ValueError(f'Insufficient number of points to fit a polynomial of degree {self.degree_cdistortion}')
793
- for i in tqdm(range(self._nlines_reference),
792
+ for i in tqdm(range(self._nlines_reference),
794
793
  desc='Fitting C distortion', disable=disable_tqdm):
795
794
  yfit = self._xpeaks_all_lines_array[self._valid_scans, i]
796
795
  poly, yres, reject = polfit_residuals_with_sigma_rejection(
@@ -1124,11 +1123,11 @@ class TeaWaveCalibration:
1124
1123
  raise ValueError('You must set plots=True to make use of pdf_output')
1125
1124
 
1126
1125
  return poly_fits_yx, residual_std_yx, \
1127
- poly_fits_xy, residual_std_xy, \
1128
- crval1_linear, cdelt1_linear, crmax1_linear
1126
+ poly_fits_xy, residual_std_xy, \
1127
+ crval1_linear, cdelt1_linear, crmax1_linear
1129
1128
 
1130
1129
  def fit_wavelengths(self, degree_wavecalib=None,
1131
- output_filename=None, history_list=None,
1130
+ output_filename=None, history_list=None,
1132
1131
  plots=False, title=None,
1133
1132
  pdf_output=None, pdf_only=False,
1134
1133
  silent_mode=False, disable_tqdm=True):
@@ -1320,9 +1319,9 @@ class TeaWaveCalibration:
1320
1319
  # crval1, cdelt1, crmax1, residual_std
1321
1320
  fig, axarr = plt.subplots(nrows=1, ncols=5, figsize=(15, 3))
1322
1321
  axarr = axarr.flatten()
1323
- for i, item in enumerate(['_array_crval1_linear',
1322
+ for i, item in enumerate(['_array_crval1_linear',
1324
1323
  '_array_cdelt1_linear',
1325
- '_array_crmax1_linear',
1324
+ '_array_crmax1_linear',
1326
1325
  '_array_residual_std_wav',
1327
1326
  '_array_residual_std_pix']):
1328
1327
  ax = axarr[i]
@@ -1357,8 +1356,7 @@ class TeaWaveCalibration:
1357
1356
  nrows = int(ncoeff / npprow)
1358
1357
  if ncoeff % npprow != 0:
1359
1358
  nrows += 1
1360
- fig, axarr = plt.subplots(nrows=nrows, ncols=npprow,
1361
- figsize=(figwidth, 3*nrows))
1359
+ fig, axarr = plt.subplots(nrows=nrows, ncols=npprow, figsize=(figwidth, 3*nrows))
1362
1360
  axarr = axarr.flatten()
1363
1361
  for ax in axarr:
1364
1362
  ax.axis('off')
@@ -1435,7 +1433,7 @@ class TeaWaveCalibration:
1435
1433
 
1436
1434
  old_x_borders_fits = np.arange(naxis1 + 1) + 0.5 # FITS convention
1437
1435
 
1438
- for k in tqdm(range(naxis2),
1436
+ for k in tqdm(range(naxis2),
1439
1437
  desc='Applying wavelength calibration',
1440
1438
  disable=disable_tqdm):
1441
1439
  poly = Polynomial(self._array_poly_wav[k])
@@ -1678,4 +1676,3 @@ def apply_wavecal_ccddata(infile, wcalibfile, outfile,
1678
1676
  cdelt1=cdelt1,
1679
1677
  title=f'{title_}(UNCERT extension)'
1680
1678
  )
1681
-
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2022-2024 Universidad Complutense de Madrid
2
+ # Copyright 2022-2025 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -12,8 +12,8 @@ import numpy as np
12
12
 
13
13
  def zscale(image, factor=0.25):
14
14
  """Compute z1 and z2 cuts in a similar way to Iraf.
15
-
16
- If the total number of pixels is less than 10, the function simply
15
+
16
+ If the total number of pixels is less than 10, the function simply
17
17
  returns the minimum and the maximum values.
18
18
 
19
19
  Parameters
@@ -48,5 +48,5 @@ def zscale(image, factor=0.25):
48
48
  z1 = max(z1, q000)
49
49
  z2 = q500+(zslope*npixels/2)/factor
50
50
  z2 = min(z2, q1000)
51
-
51
+
52
52
  return z1, z2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: teareduce
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: Utilities for astronomical data reduction
5
5
  Author-email: Nicolás Cardiel <cardiel@ucm.es>
6
6
  License: GPL-3.0-or-later
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  src/teareduce/__init__.py
5
5
  src/teareduce/avoid_astropy_warnings.py
6
+ src/teareduce/cleanest.py
6
7
  src/teareduce/correct_pincushion_distortion.py
7
8
  src/teareduce/cosmicrays.py
8
9
  src/teareduce/ctext.py
@@ -25,6 +26,7 @@ src/teareduce/zscale.py
25
26
  src/teareduce.egg-info/PKG-INFO
26
27
  src/teareduce.egg-info/SOURCES.txt
27
28
  src/teareduce.egg-info/dependency_links.txt
29
+ src/teareduce.egg-info/entry_points.txt
28
30
  src/teareduce.egg-info/requires.txt
29
31
  src/teareduce.egg-info/top_level.txt
30
32
  src/teareduce/tests/__init__.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tea-cleanest = teareduce.cleanest:main
File without changes
File without changes
File without changes