teareduce 0.5.2__tar.gz → 0.5.4__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 (53) hide show
  1. {teareduce-0.5.2/src/teareduce.egg-info → teareduce-0.5.4}/PKG-INFO +1 -1
  2. teareduce-0.5.4/src/teareduce/cleanest/__init__.py +10 -0
  3. teareduce-0.5.4/src/teareduce/cleanest/cleanest.py +139 -0
  4. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/cosmicraycleanerapp.py +90 -30
  5. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/definitions.py +20 -9
  6. teareduce-0.5.4/src/teareduce/cleanest/dilatemask.py +41 -0
  7. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/interpolation_a.py +29 -7
  8. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/interpolation_x.py +18 -0
  9. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/interpolation_y.py +18 -0
  10. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/interpolationeditor.py +4 -0
  11. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/parametereditor.py +67 -22
  12. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/reviewcosmicray.py +34 -13
  13. teareduce-0.5.4/src/teareduce/tests/test_cleanest.py +131 -0
  14. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/version.py +1 -1
  15. {teareduce-0.5.2 → teareduce-0.5.4/src/teareduce.egg-info}/PKG-INFO +1 -1
  16. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce.egg-info/SOURCES.txt +3 -0
  17. teareduce-0.5.2/src/teareduce/tests/__init__.py +0 -0
  18. {teareduce-0.5.2 → teareduce-0.5.4}/LICENSE.txt +0 -0
  19. {teareduce-0.5.2 → teareduce-0.5.4}/README.md +0 -0
  20. {teareduce-0.5.2 → teareduce-0.5.4}/pyproject.toml +0 -0
  21. {teareduce-0.5.2 → teareduce-0.5.4}/setup.cfg +0 -0
  22. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/__init__.py +0 -0
  23. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/avoid_astropy_warnings.py +0 -0
  24. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/__main__.py +0 -0
  25. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/find_closest_true.py +0 -0
  26. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cleanest/imagedisplay.py +0 -0
  27. {teareduce-0.5.2/src/teareduce/cleanest → teareduce-0.5.4/src/teareduce/cookbook}/__init__.py +0 -0
  28. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cookbook/get_cookbook_file.py +0 -0
  29. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/correct_pincushion_distortion.py +0 -0
  30. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/cosmicrays.py +0 -0
  31. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/ctext.py +0 -0
  32. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/draw_rectangle.py +0 -0
  33. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/elapsed_time.py +0 -0
  34. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/histogram1d.py +0 -0
  35. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/imshow.py +0 -0
  36. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/numsplines.py +0 -0
  37. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/peaks_spectrum.py +0 -0
  38. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/polfit.py +0 -0
  39. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/robust_std.py +0 -0
  40. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/sdistortion.py +0 -0
  41. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/simulateccdexposure.py +0 -0
  42. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/sliceregion.py +0 -0
  43. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/statsummary.py +0 -0
  44. {teareduce-0.5.2/src/teareduce/cookbook → teareduce-0.5.4/src/teareduce/tests}/__init__.py +0 -0
  45. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/tests/test_sliceregion.py +0 -0
  46. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/tests/test_version.py +0 -0
  47. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/wavecal.py +0 -0
  48. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/write_array_to_fits.py +0 -0
  49. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce/zscale.py +0 -0
  50. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce.egg-info/dependency_links.txt +0 -0
  51. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce.egg-info/entry_points.txt +0 -0
  52. {teareduce-0.5.2 → teareduce-0.5.4}/src/teareduce.egg-info/requires.txt +0 -0
  53. {teareduce-0.5.2 → teareduce-0.5.4}/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.5.2
3
+ Version: 0.5.4
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
@@ -0,0 +1,10 @@
1
+ #
2
+ # Copyright 2023-2025 Universidad Complutense de Madrid
3
+ #
4
+ # This file is part of teareduce
5
+ #
6
+ # SPDX-License-Identifier: GPL-3.0-or-later
7
+ # License-Filename: LICENSE.txt
8
+ #
9
+
10
+ from .cleanest import cleanest
@@ -0,0 +1,139 @@
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
+ """Interpolate pixels identified in a mask."""
11
+
12
+ from scipy import ndimage
13
+ import numpy as np
14
+
15
+ from .dilatemask import dilatemask
16
+ from .interpolation_x import interpolation_x
17
+ from .interpolation_y import interpolation_y
18
+ from .interpolation_a import interpolation_a
19
+
20
+
21
+ def cleanest(data, mask_crfound, dilation=0,
22
+ interp_method=None, npoints=None, degree=None,
23
+ debug=False):
24
+ """Interpolate pixels identified in a mask.
25
+
26
+ The original data and mask are not modified. A copy of both
27
+ arrays are created and returned with the interpolated pixels.
28
+
29
+ Parameters
30
+ ----------
31
+ data : 2D numpy.ndarray
32
+ The image data array to be processed.
33
+ mask_crfound : 2D numpy.ndarray of bool
34
+ A boolean mask array indicating which pixels are affected by
35
+ cosmic rays.
36
+ dilation : int, optional
37
+ The number of pixels to dilate the masked pixels before
38
+ interpolation.
39
+ interp_method : str, optional
40
+ The interpolation method to use. Options are:
41
+ 'x' : Polynomial interpolation in the X direction.
42
+ 'y' : Polynomial interpolation in the Y direction.
43
+ 's' : Surface fit (degree 1) interpolation.
44
+ 'd' : Median of border pixels interpolation.
45
+ 'm' : Mean of border pixels interpolation.
46
+ npoints : int, optional
47
+ The number of points to use for interpolation. This
48
+ parameter is relevant for 'x', 'y', 's', 'd', and 'm' methods.
49
+ degree : int, optional
50
+ The degree of the polynomial to fit. This parameter is
51
+ relevant for 'x' and 'y' methods.
52
+ debug : bool, optional
53
+ If True, print debug information.
54
+
55
+ Returns
56
+ -------
57
+ cleaned_data : 2D numpy.ndarray
58
+ The image data array with cosmic rays cleaned.
59
+ mask_fixed : 2D numpy.ndarray of bool
60
+ The updated boolean mask array indicating which pixels
61
+ have been fixed.
62
+
63
+ Notes
64
+ -----
65
+ This function has been created to clean cosmic rays without
66
+ the need of a GUI interaction. It can be used in scripts
67
+ or batch processing of images.
68
+ """
69
+ if interp_method is None:
70
+ raise ValueError("interp_method must be specified.")
71
+ if interp_method not in ['x', 'y', 's', 'd', 'm']:
72
+ raise ValueError(f"Unknown interp_method: {interp_method}")
73
+ if npoints is None:
74
+ raise ValueError("npoints must be specified.")
75
+ if degree is None and interp_method in ['x', 'y']:
76
+ raise ValueError("degree must be specified for the chosen interp_method.")
77
+
78
+ # Apply dilation to the cosmic ray mask if needed
79
+ if dilation > 0:
80
+ updated_mask_crfound = dilatemask(mask_crfound, dilation)
81
+ else:
82
+ updated_mask_crfound = mask_crfound.copy()
83
+
84
+ # Create a mask to keep track of cleaned pixels
85
+ mask_fixed = np.zeros_like(mask_crfound, dtype=bool)
86
+
87
+ # Determine number of CR features
88
+ structure = [[1, 1, 1],
89
+ [1, 1, 1],
90
+ [1, 1, 1]]
91
+ cr_labels, num_features = ndimage.label(updated_mask_crfound, structure=structure)
92
+ if debug:
93
+ print(f"Number of cosmic ray pixels to be cleaned: {np.sum(updated_mask_crfound)}")
94
+ print(f"Number of cosmic rays (grouped pixels)...: {num_features}")
95
+
96
+ # Fix cosmic rays using the specified interpolation method
97
+ cleaned_data = data.copy()
98
+ num_cr_cleaned = 0
99
+ for cr_index in range(1, num_features + 1):
100
+ if interp_method in ['x', 'y']:
101
+ if 2 * npoints <= degree:
102
+ raise ValueError("2*npoints must be greater than degree for polynomial interpolation.")
103
+ if interp_method == 'x':
104
+ interp_func = interpolation_x
105
+ else:
106
+ interp_func = interpolation_y
107
+ interpolation_performed, _, _ = interp_func(
108
+ data=cleaned_data,
109
+ mask_fixed=mask_fixed,
110
+ cr_labels=cr_labels,
111
+ cr_index=cr_index,
112
+ npoints=npoints,
113
+ degree=degree)
114
+ if interpolation_performed:
115
+ num_cr_cleaned += 1
116
+ elif interp_method in ['s', 'd', 'm']:
117
+ if interp_method == 's':
118
+ method = 'surface'
119
+ elif interp_method == 'd':
120
+ method = 'median'
121
+ elif interp_method == 'm':
122
+ method = 'mean'
123
+ interpolation_performed, _, _ = interpolation_a(
124
+ data=cleaned_data,
125
+ mask_fixed=mask_fixed,
126
+ cr_labels=cr_labels,
127
+ cr_index=cr_index,
128
+ npoints=npoints,
129
+ method=method
130
+ )
131
+ if interpolation_performed:
132
+ num_cr_cleaned += 1
133
+ else:
134
+ raise ValueError(f"Unknown interpolation method: {interp_method}")
135
+
136
+ if debug:
137
+ print(f"Number of cosmic rays cleaned............: {num_cr_cleaned}")
138
+
139
+ return cleaned_data, mask_fixed
@@ -19,15 +19,17 @@ from ccdproc import cosmicray_lacosmic
19
19
  import matplotlib.pyplot as plt
20
20
  from matplotlib.backend_bases import key_press_handler
21
21
  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
22
+ from scipy import ndimage
22
23
  import numpy as np
23
24
  import os
24
25
  from rich import print
25
- from scipy import ndimage
26
+ from tqdm import tqdm
26
27
 
27
28
  from .definitions import lacosmic_default_dict
28
29
  from .definitions import DEFAULT_NPOINTS_INTERP
29
30
  from .definitions import DEFAULT_DEGREE_INTERP
30
31
  from .definitions import MAX_PIXEL_DISTANCE_TO_CR
32
+ from .dilatemask import dilatemask
31
33
  from .find_closest_true import find_closest_true
32
34
  from .interpolation_a import interpolation_a
33
35
  from .interpolation_x import interpolation_x
@@ -134,8 +136,11 @@ class CosmicRayCleanerApp(ImageDisplay):
134
136
  Flag to indicate if the review window is active.
135
137
  """
136
138
  self.root = root
139
+ # self.root.geometry("800x800+50+0") # This does not work in Fedora
140
+ self.root.minsize(800, 800)
141
+ self.root.update_idletasks()
142
+ self.root.geometry("+50+0")
137
143
  self.root.title("Cosmic Ray Cleaner")
138
- self.root.geometry("800x700+50+0")
139
144
  self.lacosmic_params = lacosmic_default_dict.copy()
140
145
  self.input_fits = input_fits
141
146
  self.extension = extension
@@ -311,7 +316,8 @@ class CosmicRayCleanerApp(ImageDisplay):
311
316
  self.canvas.mpl_connect("key_press_event", self.on_key)
312
317
  self.canvas.mpl_connect("button_press_event", self.on_click)
313
318
  canvas_widget = self.canvas.get_tk_widget()
314
- canvas_widget.pack(fill=tk.BOTH, expand=True)
319
+ # canvas_widget.pack(fill=tk.BOTH, expand=True) # This does not work in Fedora
320
+ canvas_widget.pack(expand=True)
315
321
 
316
322
  # Matplotlib toolbar
317
323
  self.toolbar_frame = tk.Frame(self.root)
@@ -357,41 +363,81 @@ class CosmicRayCleanerApp(ImageDisplay):
357
363
  self.last_xmax = updated_params['xmax']['value']
358
364
  self.last_ymin = updated_params['ymin']['value']
359
365
  self.last_ymax = updated_params['ymax']['value']
366
+ usefulregion = SliceRegion2D(f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]",
367
+ mode="fits").python
368
+ usefulmask = np.zeros_like(self.data)
369
+ usefulmask[usefulregion] = 1.0
360
370
  # Update parameter dictionary with new values
361
371
  self.lacosmic_params = updated_params
362
372
  print("Parameters updated:")
363
373
  for key, info in self.lacosmic_params.items():
364
374
  print(f" {key}: {info['value']}")
375
+ if self.lacosmic_params['nruns']['value'] not in [1, 2]:
376
+ raise ValueError("nruns must be 1 or 2")
365
377
  # Execute L.A.Cosmic with updated parameters
366
378
  cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
367
379
  self.data,
368
- gain=self.lacosmic_params['gain']['value'],
369
- readnoise=self.lacosmic_params['readnoise']['value'],
370
- sigclip=self.lacosmic_params['sigclip']['value'],
371
- sigfrac=self.lacosmic_params['sigfrac']['value'],
372
- objlim=self.lacosmic_params['objlim']['value'],
373
- niter=self.lacosmic_params['niter']['value'],
374
- verbose=self.lacosmic_params['verbose']['value']
380
+ gain=self.lacosmic_params['run1_gain']['value'],
381
+ readnoise=self.lacosmic_params['run1_readnoise']['value'],
382
+ sigclip=self.lacosmic_params['run1_sigclip']['value'],
383
+ sigfrac=self.lacosmic_params['run1_sigfrac']['value'],
384
+ objlim=self.lacosmic_params['run1_objlim']['value'],
385
+ niter=self.lacosmic_params['run1_niter']['value'],
386
+ verbose=self.lacosmic_params['run1_verbose']['value']
375
387
  )
388
+ # Apply usefulmask to consider only selected region
389
+ cleandata_lacosmic *= usefulmask
390
+ mask_crfound = mask_crfound & (usefulmask.astype(bool))
391
+ # Second execution if nruns == 2
392
+ if self.lacosmic_params['nruns']['value'] == 2:
393
+ cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
394
+ self.data,
395
+ gain=self.lacosmic_params['run2_gain']['value'],
396
+ readnoise=self.lacosmic_params['run2_readnoise']['value'],
397
+ sigclip=self.lacosmic_params['run2_sigclip']['value'],
398
+ sigfrac=self.lacosmic_params['run2_sigfrac']['value'],
399
+ objlim=self.lacosmic_params['run2_objlim']['value'],
400
+ niter=self.lacosmic_params['run2_niter']['value'],
401
+ verbose=self.lacosmic_params['run2_verbose']['value']
402
+ )
403
+ # Apply usefulmask to consider only selected region
404
+ cleandata_lacosmic2 *= usefulmask
405
+ mask_crfound2 = mask_crfound2 & (usefulmask.astype(bool))
406
+ # Combine results from both runs
407
+ if np.any(mask_crfound):
408
+ print(f"Number of cosmic ray pixels (run1).......: {np.sum(mask_crfound)}")
409
+ print(f"Number of cosmic ray pixels (run2).......: {np.sum(mask_crfound2)}")
410
+ # find features in second run
411
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
412
+ cr_labels2, num_features2 = ndimage.label(mask_crfound2, structure=structure)
413
+ # generate mask of ones at CR pixels found in first run
414
+ mask_peaks = np.zeros(mask_crfound.shape, dtype=float)
415
+ mask_peaks[mask_crfound] = 1.0
416
+ # preserve only those CR pixels in second run that are in the first run
417
+ cr_labels2_preserved = mask_peaks * cr_labels2
418
+ # generate new mask with preserved CR pixels from second run
419
+ mask_crfound = np.zeros_like(mask_crfound, dtype=bool)
420
+ for icr in np.unique(cr_labels2_preserved):
421
+ if icr > 0:
422
+ mask_crfound[cr_labels2 == icr] = True
423
+ print(f'Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}')
424
+ # Use the cleandata from the second run
425
+ cleandata_lacosmic = cleandata_lacosmic2
376
426
  # Select the image region to process
377
- fits_region = f"[{updated_params['xmin']['value']}:{updated_params['xmax']['value']}"
378
- fits_region += f",{updated_params['ymin']['value']}:{updated_params['ymax']['value']}]"
379
- region = SliceRegion2D(fits_region, mode="fits").python
380
427
  self.cleandata_lacosmic = self.data.copy()
381
- self.cleandata_lacosmic[region] = cleandata_lacosmic[region]
428
+ self.cleandata_lacosmic[usefulregion] = cleandata_lacosmic[usefulregion]
382
429
  self.mask_crfound = np.zeros_like(self.data, dtype=bool)
383
- self.mask_crfound[region] = mask_crfound[region]
430
+ self.mask_crfound[usefulregion] = mask_crfound[usefulregion]
384
431
  # Process the mask: dilation and labeling
385
432
  if np.any(self.mask_crfound):
386
433
  num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
387
434
  dilation = self.lacosmic_params['dilation']['value']
388
435
  if dilation > 0:
389
436
  # Dilate the mask by the specified number of pixels
390
- structure = ndimage.generate_binary_structure(2, 2) # 8-connectivity
391
- self.mask_crfound = ndimage.binary_dilation(
392
- self.mask_crfound,
393
- structure=structure,
394
- iterations=self.lacosmic_params['dilation']['value']
437
+ self.mask_crfound = dilatemask(
438
+ mask=self.mask_crfound,
439
+ iterations=self.lacosmic_params['dilation']['value'],
440
+ connectivity=1
395
441
  )
396
442
  num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
397
443
  sdum = str(num_cr_pixels_after_dilation)
@@ -407,7 +453,7 @@ class CosmicRayCleanerApp(ImageDisplay):
407
453
  # diagonal connections too, so we define a 3x3 square.
408
454
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
409
455
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
410
- print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:{len(sdum)}}")
456
+ print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:>{len(sdum)}}")
411
457
  self.apply_lacosmic_button.config(state=tk.NORMAL)
412
458
  self.examine_detected_cr_button.config(state=tk.NORMAL)
413
459
  self.update_cr_overlay()
@@ -452,10 +498,13 @@ class CosmicRayCleanerApp(ImageDisplay):
452
498
  """Apply the selected cleaning method to the detected cosmic rays."""
453
499
  if np.any(self.mask_crfound):
454
500
  # recalculate labels and number of features
455
- structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
501
+ structure = [[1, 1, 1],
502
+ [1, 1, 1],
503
+ [1, 1, 1]]
456
504
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
457
- print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {np.sum(self.mask_crfound)}")
458
- print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features}")
505
+ sdum = str(np.sum(self.mask_crfound))
506
+ print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {sdum}")
507
+ print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features:>{len(sdum)}}")
459
508
  # Define parameters for L.A.Cosmic from default dictionary
460
509
  editor_window = tk.Toplevel(self.root)
461
510
  editor = InterpolationEditor(
@@ -463,7 +512,7 @@ class CosmicRayCleanerApp(ImageDisplay):
463
512
  last_dilation=self.lacosmic_params['dilation']['value'],
464
513
  last_npoints=self.last_npoints,
465
514
  last_degree=self.last_degree,
466
- auxdata=self.auxdata
515
+ auxdata=self.auxdata,
467
516
  )
468
517
  # Make it modal (blocks interaction with main window)
469
518
  editor_window.transient(self.root)
@@ -498,7 +547,7 @@ class CosmicRayCleanerApp(ImageDisplay):
498
547
  self.mask_crfound[self.mask_crfound] = False
499
548
  num_cr_cleaned = self.num_features
500
549
  else:
501
- for i in range(1, self.num_features + 1):
550
+ for i in tqdm(range(1, self.num_features + 1)):
502
551
  tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
503
552
  if cleaning_method == 'x':
504
553
  interpolation_performed, _, _ = interpolation_x(
@@ -536,6 +585,15 @@ class CosmicRayCleanerApp(ImageDisplay):
536
585
  npoints=editor.npoints,
537
586
  method='median'
538
587
  )
588
+ elif cleaning_method == 'a-mean':
589
+ interpolation_performed, _, _ = interpolation_a(
590
+ data=self.data,
591
+ mask_fixed=tmp_mask_fixed,
592
+ cr_labels=self.cr_labels,
593
+ cr_index=i,
594
+ npoints=editor.npoints,
595
+ method='mean'
596
+ )
539
597
  else:
540
598
  raise ValueError(f"Unknown cleaning method: {cleaning_method}")
541
599
  if interpolation_performed:
@@ -548,8 +606,9 @@ class CosmicRayCleanerApp(ImageDisplay):
548
606
  # recalculate labels and number of features
549
607
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
550
608
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
551
- print(f"Remaining number of cosmic ray pixels...........: {np.sum(self.mask_crfound)}")
552
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features}")
609
+ sdum = str(np.sum(self.mask_crfound))
610
+ print(f"Remaining number of cosmic ray pixels...........: {sdum}")
611
+ print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
553
612
  # redraw image to show the changes
554
613
  self.image.set_data(self.data)
555
614
  self.canvas.draw_idle()
@@ -612,8 +671,9 @@ class CosmicRayCleanerApp(ImageDisplay):
612
671
  # recalculate labels and number of features
613
672
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
614
673
  self.cr_labels, self.num_features = ndimage.label(self.mask_crfound, structure=structure)
615
- print(f"Remaining number of cosmic ray pixels...........: {np.sum(self.mask_crfound)}")
616
- print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features}")
674
+ sdum = str(np.sum(self.mask_crfound))
675
+ print(f"Remaining number of cosmic ray pixels...........: {sdum}")
676
+ print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
617
677
  # redraw image to show the changes
618
678
  self.image.set_data(self.data)
619
679
  self.canvas.draw_idle()
@@ -14,21 +14,31 @@
14
14
  # using the intrinsic Python types, so that they can be easily cast
15
15
  # when reading user input.
16
16
  lacosmic_default_dict = {
17
- # L.A.Cosmic parameters
18
- 'gain': {'value': 1.0, 'type': float, 'positive': True},
19
- 'readnoise': {'value': 6.5, 'type': float, 'positive': True},
20
- 'sigclip': {'value': 4.5, 'type': float, 'positive': True},
21
- 'sigfrac': {'value': 0.3, 'type': float, 'positive': True},
22
- 'objlim': {'value': 5.0, 'type': float, 'positive': True},
23
- 'niter': {'value': 4, 'type': int, 'positive': True},
24
- 'verbose': {'value': False, 'type': bool},
17
+ # L.A.Cosmic parameters for run 1
18
+ 'run1_gain': {'value': 1.0, 'type': float, 'positive': True},
19
+ 'run1_readnoise': {'value': 6.5, 'type': float, 'positive': True},
20
+ 'run1_sigclip': {'value': 5.0, 'type': float, 'positive': True},
21
+ 'run1_sigfrac': {'value': 0.3, 'type': float, 'positive': True},
22
+ 'run1_objlim': {'value': 5.0, 'type': float, 'positive': True},
23
+ 'run1_niter': {'value': 4, 'type': int, 'positive': True},
24
+ 'run1_verbose': {'value': False, 'type': bool},
25
+ # L.A.Cosmic parameters for run 2
26
+ 'run2_gain': {'value': 1.0, 'type': float, 'positive': True},
27
+ 'run2_readnoise': {'value': 6.5, 'type': float, 'positive': True},
28
+ 'run2_sigclip': {'value': 3.0, 'type': float, 'positive': True},
29
+ 'run2_sigfrac': {'value': 0.3, 'type': float, 'positive': True},
30
+ 'run2_objlim': {'value': 5.0, 'type': float, 'positive': True},
31
+ 'run2_niter': {'value': 4, 'type': int, 'positive': True},
32
+ 'run2_verbose': {'value': False, 'type': bool},
25
33
  # Dilation of the mask
26
34
  'dilation': {'value': 0, 'type': int, 'positive': True},
27
35
  # Limits for the image section to process (pixels start at 1)
28
36
  'xmin': {'value': 1, 'type': int, 'positive': True},
29
37
  'xmax': {'value': None, 'type': int, 'positive': True},
30
38
  'ymin': {'value': 1, 'type': int, 'positive': True},
31
- 'ymax': {'value': None, 'type': int, 'positive': True}
39
+ 'ymax': {'value': None, 'type': int, 'positive': True},
40
+ # Number of runs to execute L.A.Cosmic
41
+ 'nruns': {'value': 1, 'type': int, 'positive': True}
32
42
  }
33
43
 
34
44
  # Default parameters for cleaning methods
@@ -37,6 +47,7 @@ VALID_CLEANING_METHODS = [
37
47
  'y interp.',
38
48
  'surface interp.',
39
49
  'median',
50
+ 'mean',
40
51
  'lacosmic',
41
52
  'auxdata'
42
53
  ]
@@ -0,0 +1,41 @@
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
+ """Dilate cosmic ray mask"""
11
+
12
+ from scipy import ndimage
13
+
14
+
15
+ def dilatemask(mask, iterations, rank=None, connectivity=1):
16
+ """Dilate mask by a given number of points.
17
+
18
+ Parameters
19
+ ----------
20
+ mask : numpy.ndarray of bool
21
+ A boolean mask array indicating cosmic ray affected pixels.
22
+ iterations : int
23
+ The number of dilation iterations to perform. Each iteration
24
+ expands the mask by one pixel in the specified connectivity.
25
+ rank : int, optional
26
+ The rank of the array. If None, it is inferred from crmask.
27
+ See scipy.ndimage.generate_binary_structure for details.
28
+ connectivity : int, optional
29
+ The connectivity for the structuring element. Default is 1.
30
+ See scipy.ndimage.generate_binary_structure for details.
31
+
32
+ Returns
33
+ -------
34
+ dilated_mask : numpy.ndarray of bool
35
+ The dilated mask.
36
+ """
37
+ if rank is None:
38
+ rank = mask.ndim
39
+ structure = ndimage.generate_binary_structure(rank, connectivity)
40
+ dilated_mask = ndimage.binary_dilation(mask, structure=structure, iterations=iterations)
41
+ return dilated_mask
@@ -10,11 +10,12 @@
10
10
  """Surface interpolation (plane fit) or median interpolation"""
11
11
 
12
12
  import numpy as np
13
- from scipy.ndimage import binary_dilation
13
+
14
+ from .dilatemask import dilatemask
14
15
 
15
16
 
16
17
  def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
17
- """Interpolate cosmic ray pixels using surface fit or median of border pixels.
18
+ """Fix cosmic ray pixels using surface fit, median or mean.
18
19
 
19
20
  Parameters
20
21
  ----------
@@ -29,7 +30,7 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
29
30
  npoints : int
30
31
  The number of points to use for interpolation.
31
32
  method : str
32
- The interpolation method to use ('surface' or 'median').
33
+ The interpolation method to use ('surface', 'median' or 'mean').
33
34
 
34
35
  Returns
35
36
  -------
@@ -39,11 +40,27 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
39
40
  X-coordinates of border pixels used for interpolation.
40
41
  yfit_all : list
41
42
  Y-coordinates of border pixels used for interpolation.
43
+
44
+ Notes
45
+ -----
46
+ The `data` array is modified in place with interpolated values for the
47
+ cosmic ray pixels. This function also returns an updated `mask_fixed`
48
+ array with interpolated pixels marked as fixed.
49
+
50
+ It is important to highlight that contrary to what is performed when
51
+ using the X- and Y-interpolation, this function does not fill the gaps
52
+ between the marked pixels. Only the pixels explicitly marked as affected
53
+ by cosmic rays are interpolated.
42
54
  """
43
55
  # Mask of CR pixels
44
56
  mask = (cr_labels == cr_index)
45
57
  # Dilate the mask to find border pixels
46
- dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
58
+ # dilated_mask = binary_dilation(mask, structure=np.ones((3, 3)), iterations=npoints)
59
+ dilated_mask = dilatemask(
60
+ mask=mask,
61
+ iterations=npoints,
62
+ connectivity=1
63
+ )
47
64
  # Border pixels are those in the dilated mask but not in the original mask
48
65
  border_mask = dilated_mask & (~mask)
49
66
  # Get coordinates of border pixels
@@ -67,16 +84,21 @@ def interpolation_a(data, mask_fixed, cr_labels, cr_index, npoints, method):
67
84
  interpolation_performed = True
68
85
  else:
69
86
  print("Not enough points to fit a plane")
70
- elif method == 'median':
87
+ elif method in ['median', 'mean']:
71
88
  # Compute median of all surrounding points
72
89
  if len(zfit_all) > 0:
73
- zmed = np.median(zfit_all)
90
+ if method == 'median':
91
+ zval = np.median(zfit_all)
92
+ else:
93
+ zval = np.mean(zfit_all)
74
94
  # recompute all CR pixels to take into account "holes" between marked pixels
75
95
  ycr_list, xcr_list = np.where(cr_labels == cr_index)
76
96
  for iy, ix in zip(ycr_list, xcr_list):
77
- data[iy, ix] = zmed
97
+ data[iy, ix] = zval
78
98
  mask_fixed[iy, ix] = True
79
99
  interpolation_performed = True
100
+ else:
101
+ print("No surrounding points found for median interpolation")
80
102
  else:
81
103
  print(f"Unknown interpolation method: {method}")
82
104
 
@@ -37,6 +37,24 @@ def interpolation_x(data, mask_fixed, cr_labels, cr_index, npoints, degree):
37
37
  X-coordinates of border pixels used for interpolation.
38
38
  yfit_all : list
39
39
  Y-coordinates of border pixels used for interpolation.
40
+
41
+ Notes
42
+ -----
43
+ The `data` array is modified in place with interpolated values for the
44
+ cosmic ray pixels. This function also returns an updated `mask_fixed`
45
+ array with interpolated pixels marked as fixed.
46
+
47
+ It is important to highlight that this function assumes that at
48
+ every y-coordinate where cosmic ray pixels are found, the pixels
49
+ form a contiguous horizontal segment. In this sense, gaps in the
50
+ x-direction are assumed to be also part of the same cosmic ray
51
+ feature, and the `cr_labels` array is updated accordingly. In other
52
+ words, all the pixels between the minimum and maximum x-coordinates
53
+ of the cosmic ray pixels at a given y-coordinate are treated as
54
+ affected by the cosmic ray. This simplyfies the interactive marking
55
+ of cosmic rays, as the user does not need to ensure that all pixels
56
+ in a horizontal segment are marked; marking just the extreme pixels
57
+ is sufficient.
40
58
  """
41
59
  ycr_list, xcr_list = np.where(cr_labels == cr_index)
42
60
  ycr_min = np.min(ycr_list)
@@ -37,6 +37,24 @@ def interpolation_y(data, mask_fixed, cr_labels, cr_index, npoints, degree):
37
37
  X-coordinates of border pixels used for interpolation.
38
38
  yfit_all : list
39
39
  Y-coordinates of border pixels used for interpolation.
40
+
41
+ Notes
42
+ -----
43
+ The `data` array is modified in place with interpolated values for the
44
+ cosmic ray pixels. This function also returns an updated `mask_fixed`
45
+ array with interpolated pixels marked as fixed.
46
+
47
+ It is important to highlight that this function assumes that at
48
+ every x-coordinate where cosmic ray pixels are found, the pixels
49
+ form a contiguous vertical segment. In this sense, gaps in the
50
+ y-direction are assumed to be also part of the same cosmic ray
51
+ feature, and the `cr_labels` array is updated accordingly. In other
52
+ words, all the pixels between the minimum and maximum y-coordinates
53
+ of the cosmic ray pixels at a given x-coordinate are treated as
54
+ affected by the cosmic ray. This simplyfies the interactive marking
55
+ of cosmic rays, as the user does not need to ensure that all pixels
56
+ in a vertical segment are marked; marking just the extreme pixels
57
+ is sufficient.
40
58
  """
41
59
  ycr_list, xcr_list = np.where(cr_labels == cr_index)
42
60
  xcr_min = np.min(xcr_list)
@@ -72,6 +72,7 @@ class InterpolationEditor:
72
72
  "y interp.": "y",
73
73
  "surface interp.": "a-plane",
74
74
  "median": "a-median",
75
+ "mean": "a-mean",
75
76
  "lacosmic": "lacosmic",
76
77
  "auxdata": "auxdata"
77
78
  }
@@ -190,6 +191,9 @@ class InterpolationEditor:
190
191
  elif selected_method == 'median':
191
192
  self.entry_npoints.config(state='normal')
192
193
  self.entry_degree.config(state='disabled')
194
+ elif selected_method == 'mean':
195
+ self.entry_npoints.config(state='normal')
196
+ self.entry_degree.config(state='disabled')
193
197
  elif selected_method == 'lacosmic':
194
198
  self.entry_npoints.config(state='disabled')
195
199
  self.entry_degree.config(state='disabled')
@@ -75,7 +75,7 @@ class ParameterEditor:
75
75
  self.param_dict['ymin']['value'] = ymin
76
76
  self.param_dict['ymax']['value'] = ymax
77
77
  self.imgshape = imgshape
78
- self.entries = {} # dictionary to hold entry widgets
78
+ self.entries = {'run1': {}, 'run2': {}} # dictionary to hold entry widgets
79
79
  self.result_dict = None
80
80
 
81
81
  # Create the form
@@ -91,33 +91,48 @@ class ParameterEditor:
91
91
 
92
92
  # Subtitle for L.A.Cosmic parameters
93
93
  subtitle_label = tk.Label(main_frame, text="L.A.Cosmic Parameters", font=("Arial", 14, "bold"))
94
- subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
94
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
95
95
  row += 1
96
96
 
97
- # Create labels and entry fields for each parameter
97
+ # Create labels and entry fields for each parameter.
98
+ # Note: here we are using entry_vars to trace changes in the entries
99
+ # so that we can update the color of run2 entries if they differ from run1.
100
+ self.entry_vars = {}
98
101
  for key, info in self.param_dict.items():
99
- if key.lower() not in ['dilation', 'xmin', 'xmax', 'ymin', 'ymax']:
100
- # Parameter name label
101
- label = tk.Label(main_frame, text=f"{key}:", anchor='e', width=15)
102
- label.grid(row=row, column=0, sticky='w', pady=5)
103
- # Entry field
104
- entry = tk.Entry(main_frame, width=10)
105
- entry.insert(0, str(info['value']))
106
- entry.grid(row=row, column=1, padx=10, pady=5)
107
- self.entries[key] = entry # dictionary to hold entry widgets
108
- # Type label
109
- type_label = tk.Label(main_frame, text=f"({info['type'].__name__})", fg='gray', anchor='w', width=10)
110
- type_label.grid(row=row, column=2, sticky='w', pady=5)
111
- row += 1
102
+ if not key.startswith('run1_'):
103
+ continue
104
+ # Parameter name label
105
+ label = tk.Label(main_frame, text=f"{key[5:]}:", anchor='e', width=15)
106
+ label.grid(row=row, column=0, sticky='w', pady=5)
107
+ # Entry field for run1
108
+ self.entry_vars[key] = tk.StringVar()
109
+ self.entry_vars[key].trace_add('write', lambda *args: self.update_colour_param_run1_run2())
110
+ entry = tk.Entry(main_frame, textvariable=self.entry_vars[key], width=10)
111
+ entry.insert(0, str(info['value']))
112
+ entry.grid(row=row, column=1, padx=10, pady=5)
113
+ self.entries[key] = entry # dictionary to hold entry widgets
114
+ # Entry field for run2
115
+ key2 = 'run2_' + key[5:]
116
+ self.entry_vars[key2] = tk.StringVar()
117
+ self.entry_vars[key2].trace_add('write', lambda *args: self.update_colour_param_run1_run2())
118
+ entry = tk.Entry(main_frame, textvariable=self.entry_vars[key2], width=10)
119
+ entry.insert(0, str(self.param_dict[key2]['value']))
120
+ entry.grid(row=row, column=2, padx=10, pady=5)
121
+ self.entries['run2_'+key[5:]] = entry # dictionary to hold entry widgets
122
+ # Type label
123
+ type_label = tk.Label(main_frame, text=f"({info['type'].__name__})", fg='gray', anchor='w', width=10)
124
+ type_label.grid(row=row, column=3, sticky='w', pady=5)
125
+ row += 1
126
+ # self.update_colour_param_run1_run2()
112
127
 
113
128
  # Separator
114
129
  separator1 = ttk.Separator(main_frame, orient='horizontal')
115
- separator1.grid(row=row, column=0, columnspan=3, sticky='ew', pady=(10, 10))
130
+ separator1.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
116
131
  row += 1
117
132
 
118
133
  # Subtitle for additional parameters
119
134
  subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=("Arial", 14, "bold"))
120
- subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
135
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
121
136
  row += 1
122
137
 
123
138
  # Dilation label and entry
@@ -134,12 +149,12 @@ class ParameterEditor:
134
149
 
135
150
  # Separator
136
151
  separator2 = ttk.Separator(main_frame, orient='horizontal')
137
- separator2.grid(row=row, column=0, columnspan=3, sticky='ew', pady=(10, 10))
152
+ separator2.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
138
153
  row += 1
139
154
 
140
155
  # Subtitle for region to be examined
141
156
  subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=("Arial", 14, "bold"))
142
- subtitle_label.grid(row=row, column=0, columnspan=3, pady=(0, 15))
157
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
143
158
  row += 1
144
159
 
145
160
  # Region to be examined label and entries
@@ -165,12 +180,12 @@ class ParameterEditor:
165
180
 
166
181
  # Separator
167
182
  separator3 = ttk.Separator(main_frame, orient='horizontal')
168
- separator3.grid(row=row, column=0, columnspan=3, sticky='ew', pady=(10, 10))
183
+ separator3.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
169
184
  row += 1
170
185
 
171
186
  # Button frame
172
187
  button_frame = tk.Frame(main_frame)
173
- button_frame.grid(row=row, column=0, columnspan=3, pady=(5, 0))
188
+ button_frame.grid(row=row, column=0, columnspan=4, pady=(5, 0))
174
189
 
175
190
  # OK button
176
191
  ok_button = tk.Button(button_frame, text="OK", width=5, command=self.on_ok)
@@ -190,6 +205,8 @@ class ParameterEditor:
190
205
  updated_dict = {}
191
206
 
192
207
  for key, info in self.param_dict.items():
208
+ if key == 'nruns':
209
+ continue
193
210
  entry_value = self.entries[key].get()
194
211
  value_type = info['type']
195
212
 
@@ -212,6 +229,17 @@ class ParameterEditor:
212
229
  'type': value_type
213
230
  }
214
231
 
232
+ # Check whether any run1 and run2 parameters differ
233
+ nruns = 1
234
+ for key in self.param_dict.keys():
235
+ if key.startswith('run1_'):
236
+ parname = key[5:]
237
+ key2 = 'run2_' + parname
238
+ if updated_dict[key]['value'] != updated_dict[key2]['value']:
239
+ nruns = 2
240
+ print(f"Parameter '{parname}' differs between run1 and run2: "
241
+ f"{updated_dict[key]['value']} (run1) vs {updated_dict[key2]['value']} (run2)")
242
+
215
243
  # Additional validation for region limits
216
244
  try:
217
245
  if updated_dict['xmax']['value'] <= updated_dict['xmin']['value']:
@@ -219,6 +247,9 @@ class ParameterEditor:
219
247
  if updated_dict['ymax']['value'] <= updated_dict['ymin']['value']:
220
248
  raise ValueError("ymax must be greater than ymin")
221
249
  self.result_dict = updated_dict
250
+ self.result_dict['nruns'] = {'value': nruns, 'type': int, 'positive': True}
251
+ if nruns not in [1, 2]:
252
+ raise ValueError("nruns must be 1 or 2")
222
253
  self.root.destroy()
223
254
  except ValueError as e:
224
255
  messagebox.showerror("Invalid Inputs",
@@ -243,9 +274,23 @@ class ParameterEditor:
243
274
  self.param_dict['ymin']['value'] = 1
244
275
  self.param_dict['ymax']['value'] = self.imgshape[0]
245
276
  for key, info in self.param_dict.items():
277
+ if key == 'nruns':
278
+ continue
246
279
  self.entries[key].delete(0, tk.END)
247
280
  self.entries[key].insert(0, str(info['value']))
248
281
 
249
282
  def get_result(self):
250
283
  """Return the updated dictionary"""
251
284
  return self.result_dict
285
+
286
+ def update_colour_param_run1_run2(self):
287
+ """Update the foreground color of run1 and run2 entries."""
288
+ # Highlight run2 parameter if different from run1
289
+ for key in self.param_dict.keys():
290
+ if key.startswith('run1_'):
291
+ parname = key[5:]
292
+ if key in self.entries and 'run2_'+parname in self.entries:
293
+ if self.entries[key].get() != self.entries['run2_'+parname].get():
294
+ self.entries['run2_'+parname].config(fg='red')
295
+ else:
296
+ self.entries['run2_'+parname].config(fg='black')
@@ -133,9 +133,13 @@ class ReviewCosmicRay(ImageDisplay):
133
133
  self.root.title("Review Cosmic Rays")
134
134
  self.auxdata = auxdata
135
135
  if self.auxdata is not None:
136
- self.root.geometry("1200x700+100+100")
136
+ # self.root.geometry("1000x700+100+100") # This does not work in Fedora
137
+ self.root.minsize(1000, 700)
137
138
  else:
138
- self.root.geometry("800x700+100+100")
139
+ # self.root.geometry("800x700+100+100") # This does not work in Fedora
140
+ self.root.minsize(800, 700)
141
+ self.root.update_idletasks()
142
+ self.root.geometry("+100+100")
139
143
  self.data = data
140
144
  self.cleandata_lacosmic = cleandata_lacosmic
141
145
  self.data_original = data.copy()
@@ -193,8 +197,11 @@ class ReviewCosmicRay(ImageDisplay):
193
197
  self.interp_s_button = tk.Button(self.button_frame2, text="[s]urface interp.",
194
198
  command=lambda: self.interp_a('surface'))
195
199
  self.interp_s_button.pack(side=tk.LEFT, padx=5)
196
- self.interp_m_button = tk.Button(self.button_frame2, text="[m]edian",
200
+ self.interp_d_button = tk.Button(self.button_frame2, text="me[d]ian",
197
201
  command=lambda: self.interp_a('median'))
202
+ self.interp_d_button.pack(side=tk.LEFT, padx=5)
203
+ self.interp_m_button = tk.Button(self.button_frame2, text="[m]ean",
204
+ command=lambda: self.interp_a('mean'))
198
205
  self.interp_m_button.pack(side=tk.LEFT, padx=5)
199
206
  self.interp_l_button = tk.Button(self.button_frame2, text="[l]acosmic", command=self.use_lacosmic)
200
207
  self.interp_l_button.pack(side=tk.LEFT, padx=5)
@@ -223,9 +230,9 @@ class ReviewCosmicRay(ImageDisplay):
223
230
  # Figure
224
231
  if self.auxdata is not None:
225
232
  self.fig, (self.ax_aux, self.ax) = plt.subplots(
226
- ncols=2, figsize=(10, 5), constrained_layout=True)
233
+ ncols=2, figsize=(11, 5.5), constrained_layout=True)
227
234
  else:
228
- self.fig, self.ax = plt.subplots(figsize=(8, 5), constrained_layout=True)
235
+ self.fig, self.ax = plt.subplots(figsize=(8, 5.5), constrained_layout=True)
229
236
  self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
230
237
  self.canvas.get_tk_widget().pack(padx=5, pady=5)
231
238
  # The next two instructions prevent a segmentation fault when pressing "q"
@@ -233,7 +240,8 @@ class ReviewCosmicRay(ImageDisplay):
233
240
  self.canvas.mpl_connect("key_press_event", self.on_key)
234
241
  self.canvas.mpl_connect("button_press_event", self.on_click)
235
242
  self.canvas_widget = self.canvas.get_tk_widget()
236
- self.canvas_widget.pack(fill=tk.BOTH, expand=True)
243
+ # self.canvas_widget.pack(fill=tk.BOTH, expand=True) # This does not work in Fedora
244
+ self.canvas_widget.pack(expand=True)
237
245
 
238
246
  # Matplotlib toolbar
239
247
  self.toolbar_frame = tk.Frame(self.root)
@@ -262,20 +270,24 @@ class ReviewCosmicRay(ImageDisplay):
262
270
  # to avoid image shifts when some pixels are unmarked or new ones are marked
263
271
  i0 = int(np.mean(ycr_list_original) + 0.5)
264
272
  j0 = int(np.mean(xcr_list_original) + 0.5)
265
- semiwidth = MAX_PIXEL_DISTANCE_TO_CR
273
+ max_distance_from_center = np.max(
274
+ [np.max(np.abs(ycr_list_original - i0)),
275
+ np.max(np.abs(xcr_list_original - j0))]
276
+ )
277
+ semiwidth = int(np.max([max_distance_from_center, MAX_PIXEL_DISTANCE_TO_CR]))
266
278
  jmin = j0 - semiwidth if j0 - semiwidth >= 0 else 0
267
279
  jmax = j0 + semiwidth if j0 + semiwidth < self.data.shape[1] else self.data.shape[1] - 1
268
280
  imin = i0 - semiwidth if i0 - semiwidth >= 0 else 0
269
281
  imax = i0 + semiwidth if i0 + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
270
282
  # Force the region to be of size (2*semiwidth + 1) x (2*semiwidth + 1)
271
283
  if jmin == 0:
272
- jmax = 2 * semiwidth
284
+ jmax = np.min([2 * semiwidth, self.data.shape[1] - 1])
273
285
  elif jmax == self.data.shape[1] - 1:
274
- jmin = self.data.shape[1] - 1 - 2 * semiwidth
286
+ jmin = np.max([0, self.data.shape[1] - 1 - 2 * semiwidth])
275
287
  if imin == 0:
276
- imax = 2 * semiwidth
288
+ imax = np.min([2 * semiwidth, self.data.shape[0] - 1])
277
289
  elif imax == self.data.shape[0] - 1:
278
- imin = self.data.shape[0] - 1 - 2 * semiwidth
290
+ imin = np.max([0, self.data.shape[0] - 1 - 2 * semiwidth])
279
291
  self.region = SliceRegion2D(f'[{jmin+1}:{jmax+1}, {imin+1}:{imax+1}]', mode='fits').python
280
292
  self.ax.clear()
281
293
  vmin = self.get_vmin()
@@ -333,6 +345,7 @@ class ReviewCosmicRay(ImageDisplay):
333
345
  self.interp_x_button.config(state=tk.DISABLED)
334
346
  self.interp_y_button.config(state=tk.DISABLED)
335
347
  self.interp_s_button.config(state=tk.DISABLED)
348
+ self.interp_d_button.config(state=tk.DISABLED)
336
349
  self.interp_m_button.config(state=tk.DISABLED)
337
350
  self.interp_l_button.config(state=tk.DISABLED)
338
351
  self.interp_aux_button.config(state=tk.DISABLED)
@@ -387,7 +400,7 @@ class ReviewCosmicRay(ImageDisplay):
387
400
  Parameters
388
401
  ----------
389
402
  method : str
390
- The interpolation method to use ('surface' or 'median').
403
+ The interpolation method to use ('surface', 'median' or 'mean').
391
404
  """
392
405
  print(f"{method} interpolation of cosmic ray {self.cr_index}")
393
406
  interpolation_performed, xfit_all, yfit_all = interpolation_a(
@@ -444,6 +457,7 @@ class ReviewCosmicRay(ImageDisplay):
444
457
  self.interp_x_button.config(state=tk.DISABLED)
445
458
  self.interp_y_button.config(state=tk.DISABLED)
446
459
  self.interp_s_button.config(state=tk.DISABLED)
460
+ self.interp_d_button.config(state=tk.DISABLED)
447
461
  self.interp_m_button.config(state=tk.DISABLED)
448
462
  self.interp_l_button.config(state=tk.DISABLED)
449
463
  self.interp_aux_button.config(state=tk.DISABLED)
@@ -458,6 +472,7 @@ class ReviewCosmicRay(ImageDisplay):
458
472
  self.interp_x_button.config(state=tk.NORMAL)
459
473
  self.interp_y_button.config(state=tk.NORMAL)
460
474
  self.interp_s_button.config(state=tk.NORMAL)
475
+ self.interp_d_button.config(state=tk.NORMAL)
461
476
  self.interp_m_button.config(state=tk.NORMAL)
462
477
  if self.cleandata_lacosmic is not None:
463
478
  if self.last_dilation is None or self.last_dilation == 0:
@@ -484,6 +499,7 @@ class ReviewCosmicRay(ImageDisplay):
484
499
  self.interp_x_button.config(state=tk.NORMAL)
485
500
  self.interp_y_button.config(state=tk.NORMAL)
486
501
  self.interp_s_button.config(state=tk.NORMAL)
502
+ self.interp_d_button.config(state=tk.NORMAL)
487
503
  self.interp_m_button.config(state=tk.NORMAL)
488
504
  if self.cleandata_lacosmic is not None:
489
505
  if self.last_dilation is None or self.last_dilation == 0:
@@ -513,9 +529,12 @@ class ReviewCosmicRay(ImageDisplay):
513
529
  elif event.key == 's':
514
530
  if self.interp_s_button.cget("state") != "disabled":
515
531
  self.interp_a('surface')
532
+ elif event.key == 'd':
533
+ if self.interp_d_button.cget("state") != "disabled":
534
+ self.interp_a('median')
516
535
  elif event.key == 'm':
517
536
  if self.interp_m_button.cget("state") != "disabled":
518
- self.interp_a('median')
537
+ self.interp_a('mean')
519
538
  elif event.key == 'l':
520
539
  if self.interp_l_button.cget("state") != "disabled":
521
540
  self.use_lacosmic()
@@ -551,6 +570,7 @@ class ReviewCosmicRay(ImageDisplay):
551
570
  self.interp_x_button.config(state=tk.DISABLED)
552
571
  self.interp_y_button.config(state=tk.DISABLED)
553
572
  self.interp_s_button.config(state=tk.DISABLED)
573
+ self.interp_d_button.config(state=tk.DISABLED)
554
574
  self.interp_m_button.config(state=tk.DISABLED)
555
575
  self.interp_l_button.config(state=tk.DISABLED)
556
576
  self.interp_aux_button.config(state=tk.DISABLED)
@@ -559,6 +579,7 @@ class ReviewCosmicRay(ImageDisplay):
559
579
  self.interp_x_button.config(state=tk.NORMAL)
560
580
  self.interp_y_button.config(state=tk.NORMAL)
561
581
  self.interp_s_button.config(state=tk.NORMAL)
582
+ self.interp_d_button.config(state=tk.NORMAL)
562
583
  self.interp_m_button.config(state=tk.NORMAL)
563
584
  if self.cleandata_lacosmic is not None:
564
585
  if self.last_dilation is None or self.last_dilation == 0:
@@ -0,0 +1,131 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2025 Universidad Complutense de Madrid
4
+ #
5
+ # This file is part of teareduce
6
+ #
7
+ # SPDX-License-Identifier: GPL-3.0-or-later
8
+ # License-Filename: LICENSE.txt
9
+ #
10
+ """Tests for the cleanest module."""
11
+
12
+ import numpy as np
13
+
14
+
15
+ from ..cleanest.cleanest import cleanest
16
+
17
+
18
+ def test_cleanest_no_cr():
19
+ """Test cleanest function with no cosmic rays."""
20
+ data = np.array([[1, 2, 3],
21
+ [4, 5, 6],
22
+ [7, 8, 9]], dtype=float)
23
+ mask_crfound = np.zeros_like(data, dtype=bool)
24
+
25
+ for interp_method in ['x', 'y', 's', 'd', 'm']:
26
+ cleaned_data, mask_fixed = cleanest(data, mask_crfound, interp_method=interp_method, npoints=2, degree=1)
27
+
28
+ assert np.array_equal(cleaned_data, data), "Data should remain unchanged when no cosmic rays are present."
29
+ assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged when no cosmic rays are present."
30
+
31
+
32
+ def test_cleanest_interpolation_x():
33
+ """Test cleanest function with interpolation in X direction."""
34
+ data = np.array([[1, 1, 2, 1, 1],
35
+ [2, 2, 3, 2, 2],
36
+ [3, 3, 4, 3, 3]], dtype=float)
37
+ mask_crfound = np.array([[False, False, True, False, False],
38
+ [False, False, True, False, False],
39
+ [False, False, True, False, False]], dtype=bool)
40
+
41
+ cleaned_data, mask_fixed = cleanest(data, mask_crfound,
42
+ interp_method='x', npoints=2, degree=1)
43
+
44
+ expected_data = np.array([[1, 1, 1, 1, 1],
45
+ [2, 2, 2, 2, 2],
46
+ [3, 3, 3, 3, 3]], dtype=float)
47
+
48
+ assert np.allclose(cleaned_data, expected_data), "Interpolation in X direction failed."
49
+ assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
50
+
51
+
52
+ def test_cleanest_interpolation_y():
53
+ """Test cleanest function with interpolation in Y direction."""
54
+ data = np.array([[1, 2, 3],
55
+ [1, 2, 3],
56
+ [2, 3, 4],
57
+ [1, 2, 3],
58
+ [1, 2, 3]], dtype=float)
59
+ mask_crfound = np.array([[False, False, False],
60
+ [False, False, False],
61
+ [True, True, True],
62
+ [False, False, False],
63
+ [False, False, False]], dtype=bool)
64
+
65
+ cleaned_data, mask_fixed = cleanest(data, mask_crfound,
66
+ interp_method='y', npoints=2, degree=1)
67
+
68
+ expected_data = np.array([[1, 2, 3],
69
+ [1, 2, 3],
70
+ [1, 2, 3],
71
+ [1, 2, 3],
72
+ [1, 2, 3]], dtype=float)
73
+ assert np.allclose(cleaned_data, expected_data), "Interpolation in Y direction failed."
74
+ assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
75
+
76
+
77
+ def test_cleanest_interpolation_surface():
78
+ """Test cleanest function with surface interpolation."""
79
+ data = np.array([[1, 2, 3],
80
+ [4, 100, 6],
81
+ [7, 8, 9]], dtype=float)
82
+ mask_crfound = np.array([[False, False, False],
83
+ [False, True, False],
84
+ [False, False, False]], dtype=bool)
85
+
86
+ cleaned_data, mask_fixed = cleanest(data, mask_crfound,
87
+ interp_method='s', npoints=1)
88
+
89
+ expected_data = np.array([[1, 2, 3],
90
+ [4, 5, 6],
91
+ [7, 8, 9]], dtype=float)
92
+ assert np.allclose(cleaned_data, expected_data), "Surface interpolation failed."
93
+ assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
94
+
95
+
96
+ def test_cleanest_interpolation_median():
97
+ """Test cleanest function with median border pixel interpolation."""
98
+ data = np.array([[1, 2, 3],
99
+ [4, 100, 6],
100
+ [7, 8, 9]], dtype=float)
101
+ mask_crfound = np.array([[False, False, False],
102
+ [False, True, False],
103
+ [False, False, False]], dtype=bool)
104
+
105
+ cleaned_data, mask_fixed = cleanest(data, mask_crfound,
106
+ interp_method='d', npoints=1)
107
+
108
+ expected_data = np.array([[1, 2, 3],
109
+ [4, 5, 6],
110
+ [7, 8, 9]], dtype=float)
111
+ assert np.allclose(cleaned_data, expected_data), "Median border pixel interpolation failed."
112
+ assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
113
+
114
+
115
+ def test_cleanest_interpolation_mean():
116
+ """Test cleanest function with mean border pixel interpolation."""
117
+ data = np.array([[1, 2, 3],
118
+ [4, 100, 6],
119
+ [7, 8, 9]], dtype=float)
120
+ mask_crfound = np.array([[False, False, False],
121
+ [False, True, False],
122
+ [False, False, False]], dtype=bool)
123
+
124
+ cleaned_data, mask_fixed = cleanest(data, mask_crfound,
125
+ interp_method='m', npoints=1)
126
+
127
+ expected_data = np.array([[1, 2, 3],
128
+ [4, 5, 6],
129
+ [7, 8, 9]], dtype=float)
130
+ assert np.allclose(cleaned_data, expected_data), "Mean border pixel interpolation failed."
131
+ assert np.array_equal(mask_fixed, mask_crfound), "Mask should remain unchanged after interpolation."
@@ -9,7 +9,7 @@
9
9
  #
10
10
  """Module to define the version of the teareduce package."""
11
11
 
12
- VERSION = '0.5.2'
12
+ VERSION = '0.5.4'
13
13
 
14
14
 
15
15
  def main():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: teareduce
3
- Version: 0.5.2
3
+ Version: 0.5.4
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
@@ -30,8 +30,10 @@ src/teareduce.egg-info/requires.txt
30
30
  src/teareduce.egg-info/top_level.txt
31
31
  src/teareduce/cleanest/__init__.py
32
32
  src/teareduce/cleanest/__main__.py
33
+ src/teareduce/cleanest/cleanest.py
33
34
  src/teareduce/cleanest/cosmicraycleanerapp.py
34
35
  src/teareduce/cleanest/definitions.py
36
+ src/teareduce/cleanest/dilatemask.py
35
37
  src/teareduce/cleanest/find_closest_true.py
36
38
  src/teareduce/cleanest/imagedisplay.py
37
39
  src/teareduce/cleanest/interpolation_a.py
@@ -43,5 +45,6 @@ src/teareduce/cleanest/reviewcosmicray.py
43
45
  src/teareduce/cookbook/__init__.py
44
46
  src/teareduce/cookbook/get_cookbook_file.py
45
47
  src/teareduce/tests/__init__.py
48
+ src/teareduce/tests/test_cleanest.py
46
49
  src/teareduce/tests/test_sliceregion.py
47
50
  src/teareduce/tests/test_version.py
File without changes
File without changes
File without changes
File without changes
File without changes