teareduce 0.5.9__py3-none-any.whl → 0.6.1__py3-none-any.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.
@@ -13,14 +13,34 @@ import tkinter as tk
13
13
  from tkinter import messagebox
14
14
  from tkinter import ttk
15
15
 
16
+ from rich import print
17
+
16
18
  from .centerchildparent import center_on_parent
17
19
  from .definitions import VALID_CLEANING_METHODS
20
+ from .definitions import MASKFILL_OPERATOR_VALUES
18
21
 
19
22
 
20
23
  class InterpolationEditor:
21
24
  """Dialog to select interpolation cleaning parameters."""
22
25
 
23
- def __init__(self, root, last_dilation, last_npoints, last_degree, auxdata, xmin, xmax, ymin, ymax, imgshape):
26
+ def __init__(
27
+ self,
28
+ root,
29
+ last_dilation,
30
+ last_npoints,
31
+ last_degree,
32
+ last_maskfill_size,
33
+ last_maskfill_operator,
34
+ last_maskfill_smooth,
35
+ last_maskfill_verbose,
36
+ auxdata,
37
+ cleandata_lacosmic,
38
+ xmin,
39
+ xmax,
40
+ ymin,
41
+ ymax,
42
+ imgshape,
43
+ ):
24
44
  """Initialize the interpolation editor dialog.
25
45
 
26
46
  Parameters
@@ -33,8 +53,18 @@ class InterpolationEditor:
33
53
  The last used number of points for interpolation.
34
54
  last_degree : int
35
55
  The last used degree for interpolation.
56
+ last_maskfill_size : int
57
+ The last used maskfill size parameter.
58
+ last_maskfill_operator : str
59
+ The last used maskfill operator parameter.
60
+ last_maskfill_smooth : bool
61
+ The last used maskfill smooth parameter.
62
+ last_maskfill_verbose : bool
63
+ The last used maskfill verbose parameter.
36
64
  auxdata : array-like or None
37
65
  Auxiliary data for cleaning, if available.
66
+ cleandata_lacosmic : array-like or None
67
+ Cleaned data from L.A.Cosmic, if available.
38
68
  xmin : float
39
69
  Minimum x value of the data. From 1 to NAXIS1.
40
70
  xmax : float
@@ -56,8 +86,6 @@ class InterpolationEditor:
56
86
  Handle the Cancel button click event.
57
87
  action_on_method_change()
58
88
  Handle changes in the selected cleaning method.
59
- check_interp_methods()
60
- Check that all interpolation methods are valid.
61
89
 
62
90
  Attributes
63
91
  ----------
@@ -67,6 +95,8 @@ class InterpolationEditor:
67
95
  The last used dilation parameter.
68
96
  auxdata : array-like or None
69
97
  Auxiliary data for cleaning, if available.
98
+ cleandata_lacosmic : array-like or None
99
+ Cleaned data from L.A.Cosmic, if available.
70
100
  dict_interp_methods : dict
71
101
  Mapping of interpolation method names to their codes.
72
102
  cleaning_method : str or None
@@ -75,6 +105,12 @@ class InterpolationEditor:
75
105
  The number of points for interpolation.
76
106
  degree : int
77
107
  The degree for interpolation.
108
+ maskfill_size : int
109
+ The size parameter for maskfill.
110
+ maskfill_operator : str
111
+ The operator parameter for maskfill.
112
+ maskfill_smooth : bool
113
+ The smooth parameter for maskfill.
78
114
  xmin : float
79
115
  Minimum x value of the data. From 1 to NAXIS1.
80
116
  xmax : float
@@ -90,21 +126,15 @@ class InterpolationEditor:
90
126
  self.root.title("Cleaning Parameters")
91
127
  self.last_dilation = last_dilation
92
128
  self.auxdata = auxdata
93
- self.dict_interp_methods = {
94
- "x interp.": "x",
95
- "y interp.": "y",
96
- "surface interp.": "a-plane",
97
- "median": "a-median",
98
- "mean": "a-mean",
99
- "lacosmic": "lacosmic",
100
- "maskfill": "maskfill",
101
- "auxdata": "auxdata",
102
- }
103
- self.check_interp_methods()
129
+ self.cleandata_lacosmic = cleandata_lacosmic
104
130
  # Initialize parameters
105
131
  self.cleaning_method = None
106
132
  self.npoints = last_npoints
107
133
  self.degree = last_degree
134
+ self.maskfill_size = last_maskfill_size
135
+ self.maskfill_operator = last_maskfill_operator
136
+ self.maskfill_smooth = last_maskfill_smooth
137
+ self.maskfill_verbose = last_maskfill_verbose
108
138
  self.xmin = xmin
109
139
  self.xmax = xmax
110
140
  self.ymin = ymin
@@ -129,38 +159,48 @@ class InterpolationEditor:
129
159
  bold_font = default_font.copy()
130
160
  bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
131
161
  subtitle_label = tk.Label(main_frame, text="Select Cleaning Method", font=bold_font)
132
- subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
162
+ subtitle_label.grid(row=row, column=0, columnspan=7, pady=(0, 15))
133
163
  row += 1
134
164
 
135
165
  # Create labels and entry fields for each cleaning method
136
166
  row = 1
167
+ column = 0
137
168
  self.cleaning_method_var = tk.StringVar(value="surface interp.")
138
- for interp_method in self.dict_interp_methods.keys():
139
- valid_method = True
169
+ for interp_method in VALID_CLEANING_METHODS.keys():
170
+ state = "normal"
140
171
  # Skip replace by L.A.Cosmic values if last dilation is not zero
141
- if interp_method == "lacosmic" and self.last_dilation != 0:
142
- valid_method = False
172
+ if interp_method == "lacosmic":
173
+ if self.last_dilation != 0:
174
+ state = "disabled"
175
+ if self.cleandata_lacosmic is None:
176
+ state = "disabled"
143
177
  # Skip auxdata method if auxdata is not available
144
178
  if interp_method == "auxdata" and self.auxdata is None:
145
- valid_method = False
146
- if valid_method:
147
- tk.Radiobutton(
148
- main_frame,
149
- text=interp_method,
150
- variable=self.cleaning_method_var,
151
- value=interp_method,
152
- command=self.action_on_method_change,
153
- ).grid(row=row, column=1, sticky="w")
179
+ state = "disabled"
180
+ tk.Radiobutton(
181
+ main_frame,
182
+ text=interp_method,
183
+ variable=self.cleaning_method_var,
184
+ value=interp_method,
185
+ command=self.action_on_method_change,
186
+ state=state,
187
+ ).grid(row=row, column=column, sticky="w", padx=5, pady=5)
188
+ column += 1
189
+ if column > 6:
190
+ column = 0
154
191
  row += 1
192
+ row += 1
155
193
 
156
194
  # Separator
157
195
  separator1 = ttk.Separator(main_frame, orient="horizontal")
158
- separator1.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
196
+ separator1.grid(row=row, column=0, columnspan=7, sticky="ew", pady=(10, 10))
159
197
  row += 1
160
198
 
161
199
  # Subtitle for additional parameters
162
- subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=bold_font)
163
- subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
200
+ subtitle_label1 = tk.Label(main_frame, text="Fitting Parameters", font=bold_font)
201
+ subtitle_label1.grid(row=row, column=0, columnspan=3, pady=(0, 15))
202
+ subtitle_label2 = tk.Label(main_frame, text="Maskfill Parameters", font=bold_font)
203
+ subtitle_label2.grid(row=row, column=3, columnspan=3, pady=(0, 15))
164
204
  row += 1
165
205
 
166
206
  # Create labels and entry fields for each additional parameter
@@ -175,27 +215,61 @@ class InterpolationEditor:
175
215
  self.entry_degree = tk.Entry(main_frame, width=10)
176
216
  self.entry_degree.insert(0, self.degree)
177
217
  self.entry_degree.grid(row=row, column=1, sticky="w")
218
+ row -= 1
219
+ label = tk.Label(main_frame, text="Size:")
220
+ label.grid(row=row, column=3, sticky="e", padx=(0, 10))
221
+ self.entry_maskfill_size = tk.Entry(main_frame, width=10)
222
+ self.entry_maskfill_size.insert(0, self.maskfill_size)
223
+ self.entry_maskfill_size.grid(row=row, column=4, sticky="w")
224
+ label = tk.Label(main_frame, text="Operator:")
225
+ label.grid(row=row, column=5, sticky="e", padx=(0, 10))
226
+ self.entry_maskfill_operator = ttk.Combobox(
227
+ main_frame, width=10, values=MASKFILL_OPERATOR_VALUES, state="readonly"
228
+ )
229
+ self.entry_maskfill_operator.set(self.maskfill_operator)
230
+ # self.entry_maskfill_operator = tk.Entry(main_frame, width=10)
231
+ # self.entry_maskfill_operator.insert(0, self.maskfill_operator)
232
+ self.entry_maskfill_operator.grid(row=row, column=6, sticky="w")
178
233
  row += 1
234
+ label = tk.Label(main_frame, text="Smooth:")
235
+ label.grid(row=row, column=3, sticky="e", padx=(0, 10))
236
+ self.entry_maskfill_smooth = tk.Entry(main_frame, width=10)
237
+ self.entry_maskfill_smooth.insert(0, str(self.maskfill_smooth))
238
+ self.entry_maskfill_smooth.grid(row=row, column=4, sticky="w")
239
+ label = tk.Label(main_frame, text="Verbose:")
240
+ label.grid(row=row, column=5, sticky="e", padx=(0, 10))
241
+ self.entry_maskfill_verbose = tk.Entry(main_frame, width=10)
242
+ self.entry_maskfill_verbose.insert(0, str(self.maskfill_verbose))
243
+ self.entry_maskfill_verbose.grid(row=row, column=6, sticky="w")
244
+ row += 1
245
+
246
+ # Vertical separator
247
+ separatorv1 = ttk.Separator(main_frame, orient="vertical")
248
+ separatorv1.grid(row=row - 3, column=2, rowspan=3, sticky="ns", padx=5)
179
249
 
180
250
  # Separator
181
251
  separator2 = ttk.Separator(main_frame, orient="horizontal")
182
- separator2.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
252
+ separator2.grid(row=row, column=0, columnspan=7, sticky="ew", pady=(10, 10))
183
253
  row += 1
184
254
 
185
255
  # Subtitle for region to be examined
186
256
  subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=bold_font)
187
- subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
257
+ subtitle_label.grid(row=row, column=0, columnspan=7, pady=(0, 15))
188
258
  row += 1
189
259
 
190
260
  # Region to be examined label and entries
191
261
  for key in ["xmin", "xmax", "ymin", "ymax"]:
192
262
  # Parameter name label
193
- label = tk.Label(main_frame, text=f"{key}:", anchor="e", width=15)
194
- label.grid(row=row, column=0, sticky="w", pady=5)
263
+ label = tk.Label(main_frame, text=f"{key}:", anchor="e")
264
+ if key in ["xmin", "xmax"]:
265
+ coloff = 0
266
+ else:
267
+ coloff = 4
268
+ label.grid(row=row, column=coloff, sticky="e", pady=5)
195
269
  # Entry field
196
270
  entry = tk.Entry(main_frame, width=10)
197
271
  entry.insert(0, str(self.__dict__[key]))
198
- entry.grid(row=row, column=1, padx=10, pady=5)
272
+ entry.grid(row=row, column=coloff + 1, padx=10, pady=5)
199
273
  self.entries[key] = entry # dictionary to hold entry widgets
200
274
  # Type label
201
275
  dumtext = "(int)"
@@ -204,32 +278,42 @@ class InterpolationEditor:
204
278
  else:
205
279
  dumtext += f" --> [1, {self.imgshape[0]}]"
206
280
  type_label = tk.Label(main_frame, text=dumtext, fg="gray", anchor="w", width=15)
207
- type_label.grid(row=row, column=2, sticky="w", pady=5)
208
- row += 1
281
+ type_label.grid(row=row, column=coloff + 2, sticky="w", pady=5)
282
+ if key == "xmax":
283
+ row -= 1
284
+ else:
285
+ row += 1
286
+
287
+ # Vertical separator
288
+ separatorv2 = ttk.Separator(main_frame, orient="vertical")
289
+ separatorv2.grid(row=row - 2, column=3, rowspan=2, sticky="ns", padx=5)
209
290
 
210
291
  # Separator
211
292
  separator3 = ttk.Separator(main_frame, orient="horizontal")
212
- separator3.grid(row=row, column=0, columnspan=3, sticky="ew", pady=(10, 10))
293
+ separator3.grid(row=row, column=0, columnspan=7, sticky="ew", pady=(10, 10))
213
294
  row += 1
214
295
 
215
296
  # Button frame
216
297
  self.button_frame = tk.Frame(main_frame)
217
- self.button_frame.grid(row=row, column=0, columnspan=3, pady=(15, 0))
298
+ self.button_frame.grid(row=row, column=0, columnspan=7, pady=(5, 0))
218
299
 
219
300
  # OK button
220
- self.ok_button = tk.Button(self.button_frame, text="OK", width=5, command=self.on_ok)
301
+ self.ok_button = ttk.Button(self.button_frame, text="OK", command=self.on_ok)
221
302
  self.ok_button.pack(side="left", padx=5)
222
303
 
223
304
  # Cancel button
224
- self.cancel_button = tk.Button(self.button_frame, text="Cancel", width=5, command=self.on_cancel)
305
+ self.cancel_button = ttk.Button(self.button_frame, text="Cancel", command=self.on_cancel)
225
306
  self.cancel_button.pack(side="left", padx=5)
226
307
 
308
+ # Initial focus on OK button
309
+ self.ok_button.focus_set()
310
+
227
311
  # Initial action depending on the default method
228
312
  self.action_on_method_change()
229
313
 
230
314
  def on_ok(self):
231
315
  """Handle the OK button click event."""
232
- self.cleaning_method = self.dict_interp_methods[self.cleaning_method_var.get()]
316
+ self.cleaning_method = VALID_CLEANING_METHODS[self.cleaning_method_var.get()]
233
317
  try:
234
318
  self.npoints = int(self.entry_npoints.get())
235
319
  except ValueError:
@@ -252,6 +336,44 @@ class InterpolationEditor:
252
336
  messagebox.showerror("Input Error", "2*Npoints must be greater than Degree for x and y interpolation.")
253
337
  return
254
338
 
339
+ try:
340
+ self.maskfill_size = int(self.entry_maskfill_size.get())
341
+ except ValueError:
342
+ messagebox.showerror("Input Error", "Maskfill size must be an integer.")
343
+ return
344
+ if self.maskfill_size < 1:
345
+ messagebox.showerror("Input Error", "Maskfill size must be at least 1.")
346
+ return
347
+ if self.maskfill_size % 2 == 0:
348
+ messagebox.showerror("Input Error", "Maskfill size must be an odd integer.")
349
+ return
350
+
351
+ self.maskfill_operator = self.entry_maskfill_operator.get().strip()
352
+ if not self.maskfill_operator:
353
+ messagebox.showerror("Input Error", "Maskfill operator cannot be empty.")
354
+ return
355
+ if self.maskfill_operator not in ["median", "mean"]:
356
+ messagebox.showerror("Input Error", "Maskfill operator must be 'median' or 'mean'.")
357
+ return
358
+
359
+ smooth_str = self.entry_maskfill_smooth.get().strip().lower()
360
+ if smooth_str == "true":
361
+ self.maskfill_smooth = True
362
+ elif smooth_str == "false":
363
+ self.maskfill_smooth = False
364
+ else:
365
+ messagebox.showerror("Input Error", "Maskfill smooth must be True or False.")
366
+ return
367
+
368
+ verbose_str = self.entry_maskfill_verbose.get().strip().lower()
369
+ if verbose_str == "true":
370
+ self.maskfill_verbose = True
371
+ elif verbose_str == "false":
372
+ self.maskfill_verbose = False
373
+ else:
374
+ messagebox.showerror("Input Error", "Maskfill verbose must be True or False.")
375
+ return
376
+
255
377
  # Retrieve and validate region parameters
256
378
  try:
257
379
  xmin = int(self.entries["xmin"].get())
@@ -303,31 +425,55 @@ class InterpolationEditor:
303
425
  def action_on_method_change(self):
304
426
  """Handle changes in the selected cleaning method."""
305
427
  selected_method = self.cleaning_method_var.get()
306
- print(f"Selected cleaning method: {selected_method}")
428
+ print(f"Selected cleaning method: [red bold]{selected_method}[/red bold]")
307
429
  if selected_method in ["x interp.", "y interp."]:
308
430
  self.entry_npoints.config(state="normal")
309
431
  self.entry_degree.config(state="normal")
432
+ self.entry_maskfill_size.config(state="disabled")
433
+ self.entry_maskfill_operator.config(state="disabled")
434
+ self.entry_maskfill_smooth.config(state="disabled")
435
+ self.entry_maskfill_verbose.config(state="disabled")
310
436
  elif selected_method == "surface interp.":
311
437
  self.entry_npoints.config(state="normal")
312
438
  self.entry_degree.config(state="disabled")
439
+ self.entry_maskfill_size.config(state="disabled")
440
+ self.entry_maskfill_operator.config(state="disabled")
441
+ self.entry_maskfill_smooth.config(state="disabled")
442
+ self.entry_maskfill_verbose.config(state="disabled")
313
443
  elif selected_method == "median":
314
444
  self.entry_npoints.config(state="normal")
315
445
  self.entry_degree.config(state="disabled")
446
+ self.entry_maskfill_size.config(state="disabled")
447
+ self.entry_maskfill_operator.config(state="disabled")
448
+ self.entry_maskfill_smooth.config(state="disabled")
449
+ self.entry_maskfill_verbose.config(state="disabled")
316
450
  elif selected_method == "mean":
317
451
  self.entry_npoints.config(state="normal")
318
452
  self.entry_degree.config(state="disabled")
453
+ self.entry_maskfill_size.config(state="disabled")
454
+ self.entry_maskfill_operator.config(state="disabled")
455
+ self.entry_maskfill_smooth.config(state="disabled")
456
+ self.entry_maskfill_verbose.config(state="disabled")
319
457
  elif selected_method == "lacosmic":
320
458
  self.entry_npoints.config(state="disabled")
321
459
  self.entry_degree.config(state="disabled")
460
+ self.entry_maskfill_size.config(state="disabled")
461
+ self.entry_maskfill_operator.config(state="disabled")
462
+ self.entry_maskfill_smooth.config(state="disabled")
463
+ self.entry_maskfill_verbose.config(state="disabled")
322
464
  elif selected_method == "auxdata":
323
465
  self.entry_npoints.config(state="disabled")
324
466
  self.entry_degree.config(state="disabled")
325
-
326
- def check_interp_methods(self):
327
- """Check that all interpolation methods are valid."""
328
- for method in self.dict_interp_methods.keys():
329
- if method not in VALID_CLEANING_METHODS:
330
- raise ValueError(f"Invalid interpolation method: {method}")
331
- for method in VALID_CLEANING_METHODS:
332
- if method not in self.dict_interp_methods.keys():
333
- raise ValueError(f"Interpolation method not mapped: {method}")
467
+ self.entry_maskfill_size.config(state="disabled")
468
+ self.entry_maskfill_operator.config(state="disabled")
469
+ self.entry_maskfill_smooth.config(state="disabled")
470
+ self.entry_maskfill_verbose.config(state="disabled")
471
+ elif selected_method == "maskfill":
472
+ self.entry_npoints.config(state="disabled")
473
+ self.entry_degree.config(state="disabled")
474
+ self.entry_maskfill_size.config(state="normal")
475
+ self.entry_maskfill_operator.config(state="normal")
476
+ self.entry_maskfill_smooth.config(state="normal")
477
+ self.entry_maskfill_verbose.config(state="normal")
478
+ else:
479
+ messagebox.showerror("Error", f"Unknown cleaning method selected: {selected_method}")
@@ -18,14 +18,19 @@ except ModuleNotFoundError as e:
18
18
  "`pip install teareduce[cleanest]`."
19
19
  ) from e
20
20
  import numpy as np
21
+ from rich import print
21
22
 
23
+ from .definitions import VALID_LACOSMIC_PSFMODEL_VALUES
24
+ from .gausskernel2d_elliptical import gausskernel2d_elliptical
22
25
 
23
- def lacosmicpad(pad_width, **kwargs):
26
+
27
+ def lacosmicpad(pad_width, show_arguments=False, **kwargs):
24
28
  """Execute LACosmic algorithm on a padded array.
25
29
 
26
30
  This function pads the input image array before applying the LACosmic
27
31
  cosmic ray cleaning algorithm. After processing, the padding is removed
28
- to return an array of the original size.
32
+ to return an array of the original size. If `inbkg` or `invar` arrays
33
+ are provided, they are also padded accordingly.
29
34
 
30
35
  The padding helps to mitigate edge effects that can occur during the
31
36
  cosmic ray detection and cleaning process.
@@ -39,6 +44,8 @@ def lacosmicpad(pad_width, **kwargs):
39
44
  pad_width : int
40
45
  Width of the padding to be applied to the image before executing
41
46
  the LACosmic algorithm.
47
+ show_arguments : bool
48
+ If True, display LACosmic arguments being employed.
42
49
  **kwargs : dict
43
50
  Keyword arguments to be passed to the `cosmicray_lacosmic` function.
44
51
 
@@ -49,6 +56,7 @@ def lacosmicpad(pad_width, **kwargs):
49
56
  mask_array : 2D numpy.ndarray of bool
50
57
  The mask array indicating detected cosmic rays.
51
58
  """
59
+ # Check for required 'ccd' argument
52
60
  if "ccd" not in kwargs:
53
61
  raise ValueError("The 'ccd' keyword argument must be provided.")
54
62
  array = kwargs.pop("ccd")
@@ -56,6 +64,91 @@ def lacosmicpad(pad_width, **kwargs):
56
64
  raise TypeError("The 'ccd' keyword argument must be a numpy ndarray.")
57
65
  # Pad the array
58
66
  padded_array = np.pad(array, pad_width, mode="reflect")
67
+
68
+ # Pad inbkg if provided
69
+ if "inbkg" in kwargs:
70
+ if kwargs["inbkg"] is not None:
71
+ inbkg = kwargs["inbkg"]
72
+ if not isinstance(inbkg, np.ndarray):
73
+ raise TypeError("The 'inbkg' keyword argument must be a numpy ndarray.")
74
+ kwargs["inbkg"] = np.pad(inbkg, pad_width, mode="reflect")
75
+ else:
76
+ kwargs["inbkg"] = None
77
+
78
+ # Pad invar if provided
79
+ if "invar" in kwargs:
80
+ if kwargs["invar"] is not None:
81
+ invar = kwargs["invar"]
82
+ if not isinstance(invar, np.ndarray):
83
+ raise TypeError("The 'invar' keyword argument must be a numpy ndarray.")
84
+ kwargs["invar"] = np.pad(invar, pad_width, mode="reflect")
85
+ else:
86
+ kwargs["invar"] = None
87
+
88
+ # check for fsmode
89
+ if "fsmode" not in kwargs:
90
+ raise ValueError("The 'fsmode' keyword argument must be provided.")
91
+ else:
92
+ fsmode = kwargs["fsmode"]
93
+ if fsmode == "convolve":
94
+ if "psfmodel" not in kwargs:
95
+ raise ValueError("The 'psfmodel' keyword argument must be provided when fsmode is 'convolve'.")
96
+ psfmodel = kwargs["psfmodel"]
97
+ if psfmodel not in VALID_LACOSMIC_PSFMODEL_VALUES:
98
+ raise ValueError(f"The 'psfmodel' keyword argument must be one of {VALID_LACOSMIC_PSFMODEL_VALUES}.")
99
+ if "psffwhm" in kwargs:
100
+ raise ValueError(
101
+ "When 'fsmode' is 'convolve', 'psffwhm' should not be provided; use 'psffwhm_x' and 'psffwhm_y' instead."
102
+ )
103
+ if "psffwhm_x" not in kwargs or "psffwhm_y" not in kwargs:
104
+ raise ValueError("When 'fsmode' is 'convolve', both 'psffwhm_x' and 'psffwhm_y' must be provided.")
105
+ fwhm_x = kwargs["psffwhm_x"]
106
+ fwhm_y = kwargs["psffwhm_y"]
107
+ if "psfsize" not in kwargs:
108
+ raise ValueError("When 'fsmode' is 'convolve', 'psfsize' must be provided.")
109
+ psfsize = kwargs["psfsize"]
110
+ if kwargs["psfmodel"] == "gaussxy":
111
+ if "psfk" in kwargs:
112
+ raise ValueError(
113
+ "When 'fsmode' is 'convolve' and 'psfmodel' is 'gaussxy', 'psfk' should not be provided; it will be generated from 'psffwhm_x' and 'psffwhm_y'."
114
+ )
115
+ kwargs["psfk"] = gausskernel2d_elliptical(fwhm_x, fwhm_y, psfsize)
116
+ if show_arguments:
117
+ print(
118
+ f"Generated elliptical Gaussian kernel with fwhm_x={fwhm_x}, fwhm_y={fwhm_y}, size={psfsize}."
119
+ )
120
+ elif kwargs["psfmodel"] in ["gauss", "moffat"]:
121
+ kwargs["psffwhm"] = (fwhm_x + fwhm_y) / 2.0 # average for circular psf
122
+ if show_arguments:
123
+ print(f"Set psffwhm to average of fwhm_x and fwhm_y: {kwargs['psffwhm']}.")
124
+ elif kwargs["psfmodel"] == "gaussx":
125
+ kwargs["psffwhm"] = fwhm_x
126
+ if show_arguments:
127
+ print(f"Set psffwhm to fwhm_x: {fwhm_x}.")
128
+ elif kwargs["psfmodel"] == "gaussy":
129
+ kwargs["psffwhm"] = fwhm_y
130
+ if show_arguments:
131
+ print(f"Set psffwhm to fwhm_y: {fwhm_y}.")
132
+ else:
133
+ raise ValueError(f"Unsupported psfmodel: {kwargs['psfmodel']}")
134
+ if show_arguments:
135
+ print("Deleting 'psffwhm_x' and 'psffwhm_y' from kwargs.")
136
+ del kwargs["psffwhm_x"]
137
+ del kwargs["psffwhm_y"]
138
+ elif fsmode == "median":
139
+ # Remove unnecessary parameters for median fsmode
140
+ for param in ["psfmodel", "psfsize", "psffwhm", "psffwhm_x", "psffwhm_y"]:
141
+ if param in kwargs:
142
+ if show_arguments:
143
+ print(f"Removing '{param}' argument since fsmode is 'median'.")
144
+ del kwargs[param]
145
+ else:
146
+ raise ValueError("The 'fsmode' keyword argument must be either 'convolve' or 'median'.")
147
+
148
+ # Show LACosmic arguments if requested
149
+ if show_arguments:
150
+ for key, value in kwargs.items():
151
+ print(f"LACosmic parameter: {key} = {value}")
59
152
  # Apply LACosmic algorithm to the padded array
60
153
  cleaned_padded_array, mask_padded_array = cosmicray_lacosmic(ccd=padded_array, **kwargs)
61
154
  # Remove padding
@@ -11,9 +11,10 @@
11
11
 
12
12
  import numpy as np
13
13
  from scipy import ndimage
14
+ from rich import print
14
15
 
15
16
 
16
- def merge_peak_tail_masks(mask_peaks, mask_tails):
17
+ def merge_peak_tail_masks(mask_peaks, mask_tails, verbose=False):
17
18
  """Merge peak and tail masks for cosmic ray cleaning.
18
19
 
19
20
  Tail pixels are preserved only if they correspond to CR features
@@ -25,6 +26,8 @@ def merge_peak_tail_masks(mask_peaks, mask_tails):
25
26
  Boolean array indicating the pixels identified as cosmic ray peaks.
26
27
  mask_tails : ndarray
27
28
  Boolean array indicating the pixels identified as cosmic ray tails.
29
+ verbose: bool
30
+ If True, print additional information during processing.
28
31
 
29
32
  Returns
30
33
  -------
@@ -41,6 +44,10 @@ def merge_peak_tail_masks(mask_peaks, mask_tails):
41
44
  if mask_peaks.dtype != bool or mask_tails.dtype != bool:
42
45
  raise TypeError("Input masks must be boolean arrays.")
43
46
 
47
+ if verbose:
48
+ ndigits = np.max([len(str(np.sum(mask_peaks))), len(str(np.sum(mask_tails)))])
49
+ print(f"Number of cosmic ray pixels (peaks)...............: {np.sum(mask_peaks):{ndigits}d}")
50
+ print(f"Number of cosmic ray pixels (tails)...............: {np.sum(mask_tails):{ndigits}d}")
44
51
  # find structures in tail mask
45
52
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
46
53
  cr_labels_tails, num_crs_tails = ndimage.label(mask_tails, structure=structure)
@@ -55,4 +62,7 @@ def merge_peak_tail_masks(mask_peaks, mask_tails):
55
62
  if icr > 0:
56
63
  merged_mask[cr_labels_tails == icr] = True
57
64
 
65
+ if verbose:
66
+ print(f"Number of cosmic ray pixels (merged peaks & tails): {np.sum(merged_mask):{ndigits}d}")
67
+
58
68
  return merged_mask