teareduce 0.5.2__tar.gz → 0.5.3__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.3}/PKG-INFO +1 -1
  2. teareduce-0.5.3/src/teareduce/cleanest/__init__.py +10 -0
  3. teareduce-0.5.3/src/teareduce/cleanest/cleanest.py +139 -0
  4. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/cosmicraycleanerapp.py +85 -29
  5. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/definitions.py +20 -9
  6. teareduce-0.5.3/src/teareduce/cleanest/dilatemask.py +41 -0
  7. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/interpolation_a.py +29 -7
  8. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/interpolation_x.py +18 -0
  9. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/interpolation_y.py +18 -0
  10. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/interpolationeditor.py +4 -0
  11. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/parametereditor.py +67 -22
  12. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/reviewcosmicray.py +24 -8
  13. teareduce-0.5.3/src/teareduce/tests/test_cleanest.py +131 -0
  14. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/version.py +1 -1
  15. {teareduce-0.5.2 → teareduce-0.5.3/src/teareduce.egg-info}/PKG-INFO +1 -1
  16. {teareduce-0.5.2 → teareduce-0.5.3}/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.3}/LICENSE.txt +0 -0
  19. {teareduce-0.5.2 → teareduce-0.5.3}/README.md +0 -0
  20. {teareduce-0.5.2 → teareduce-0.5.3}/pyproject.toml +0 -0
  21. {teareduce-0.5.2 → teareduce-0.5.3}/setup.cfg +0 -0
  22. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/__init__.py +0 -0
  23. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/avoid_astropy_warnings.py +0 -0
  24. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/__main__.py +0 -0
  25. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/find_closest_true.py +0 -0
  26. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cleanest/imagedisplay.py +0 -0
  27. {teareduce-0.5.2/src/teareduce/cleanest → teareduce-0.5.3/src/teareduce/cookbook}/__init__.py +0 -0
  28. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cookbook/get_cookbook_file.py +0 -0
  29. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/correct_pincushion_distortion.py +0 -0
  30. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/cosmicrays.py +0 -0
  31. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/ctext.py +0 -0
  32. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/draw_rectangle.py +0 -0
  33. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/elapsed_time.py +0 -0
  34. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/histogram1d.py +0 -0
  35. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/imshow.py +0 -0
  36. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/numsplines.py +0 -0
  37. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/peaks_spectrum.py +0 -0
  38. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/polfit.py +0 -0
  39. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/robust_std.py +0 -0
  40. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/sdistortion.py +0 -0
  41. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/simulateccdexposure.py +0 -0
  42. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/sliceregion.py +0 -0
  43. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/statsummary.py +0 -0
  44. {teareduce-0.5.2/src/teareduce/cookbook → teareduce-0.5.3/src/teareduce/tests}/__init__.py +0 -0
  45. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/tests/test_sliceregion.py +0 -0
  46. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/tests/test_version.py +0 -0
  47. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/wavecal.py +0 -0
  48. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/write_array_to_fits.py +0 -0
  49. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce/zscale.py +0 -0
  50. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce.egg-info/dependency_links.txt +0 -0
  51. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce.egg-info/entry_points.txt +0 -0
  52. {teareduce-0.5.2 → teareduce-0.5.3}/src/teareduce.egg-info/requires.txt +0 -0
  53. {teareduce-0.5.2 → teareduce-0.5.3}/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.3
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
@@ -135,7 +137,7 @@ class CosmicRayCleanerApp(ImageDisplay):
135
137
  """
136
138
  self.root = root
137
139
  self.root.title("Cosmic Ray Cleaner")
138
- self.root.geometry("800x700+50+0")
140
+ self.root.geometry("800x800+50+0")
139
141
  self.lacosmic_params = lacosmic_default_dict.copy()
140
142
  self.input_fits = input_fits
141
143
  self.extension = extension
@@ -357,41 +359,81 @@ class CosmicRayCleanerApp(ImageDisplay):
357
359
  self.last_xmax = updated_params['xmax']['value']
358
360
  self.last_ymin = updated_params['ymin']['value']
359
361
  self.last_ymax = updated_params['ymax']['value']
362
+ usefulregion = SliceRegion2D(f"[{self.last_xmin}:{self.last_xmax},{self.last_ymin}:{self.last_ymax}]",
363
+ mode="fits").python
364
+ usefulmask = np.zeros_like(self.data)
365
+ usefulmask[usefulregion] = 1.0
360
366
  # Update parameter dictionary with new values
361
367
  self.lacosmic_params = updated_params
362
368
  print("Parameters updated:")
363
369
  for key, info in self.lacosmic_params.items():
364
370
  print(f" {key}: {info['value']}")
371
+ if self.lacosmic_params['nruns']['value'] not in [1, 2]:
372
+ raise ValueError("nruns must be 1 or 2")
365
373
  # Execute L.A.Cosmic with updated parameters
366
374
  cleandata_lacosmic, mask_crfound = cosmicray_lacosmic(
367
375
  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']
376
+ gain=self.lacosmic_params['run1_gain']['value'],
377
+ readnoise=self.lacosmic_params['run1_readnoise']['value'],
378
+ sigclip=self.lacosmic_params['run1_sigclip']['value'],
379
+ sigfrac=self.lacosmic_params['run1_sigfrac']['value'],
380
+ objlim=self.lacosmic_params['run1_objlim']['value'],
381
+ niter=self.lacosmic_params['run1_niter']['value'],
382
+ verbose=self.lacosmic_params['run1_verbose']['value']
375
383
  )
384
+ # Apply usefulmask to consider only selected region
385
+ cleandata_lacosmic *= usefulmask
386
+ mask_crfound = mask_crfound & (usefulmask.astype(bool))
387
+ # Second execution if nruns == 2
388
+ if self.lacosmic_params['nruns']['value'] == 2:
389
+ cleandata_lacosmic2, mask_crfound2 = cosmicray_lacosmic(
390
+ self.data,
391
+ gain=self.lacosmic_params['run2_gain']['value'],
392
+ readnoise=self.lacosmic_params['run2_readnoise']['value'],
393
+ sigclip=self.lacosmic_params['run2_sigclip']['value'],
394
+ sigfrac=self.lacosmic_params['run2_sigfrac']['value'],
395
+ objlim=self.lacosmic_params['run2_objlim']['value'],
396
+ niter=self.lacosmic_params['run2_niter']['value'],
397
+ verbose=self.lacosmic_params['run2_verbose']['value']
398
+ )
399
+ # Apply usefulmask to consider only selected region
400
+ cleandata_lacosmic2 *= usefulmask
401
+ mask_crfound2 = mask_crfound2 & (usefulmask.astype(bool))
402
+ # Combine results from both runs
403
+ if np.any(mask_crfound):
404
+ print(f"Number of cosmic ray pixels (run1).......: {np.sum(mask_crfound)}")
405
+ print(f"Number of cosmic ray pixels (run2).......: {np.sum(mask_crfound2)}")
406
+ # find features in second run
407
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
408
+ cr_labels2, num_features2 = ndimage.label(mask_crfound2, structure=structure)
409
+ # generate mask of ones at CR pixels found in first run
410
+ mask_peaks = np.zeros(mask_crfound.shape, dtype=float)
411
+ mask_peaks[mask_crfound] = 1.0
412
+ # preserve only those CR pixels in second run that are in the first run
413
+ cr_labels2_preserved = mask_peaks * cr_labels2
414
+ # generate new mask with preserved CR pixels from second run
415
+ mask_crfound = np.zeros_like(mask_crfound, dtype=bool)
416
+ for icr in np.unique(cr_labels2_preserved):
417
+ if icr > 0:
418
+ mask_crfound[cr_labels2 == icr] = True
419
+ print(f'Number of cosmic ray pixels (run1 & run2): {np.sum(mask_crfound)}')
420
+ # Use the cleandata from the second run
421
+ cleandata_lacosmic = cleandata_lacosmic2
376
422
  # 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
423
  self.cleandata_lacosmic = self.data.copy()
381
- self.cleandata_lacosmic[region] = cleandata_lacosmic[region]
424
+ self.cleandata_lacosmic[usefulregion] = cleandata_lacosmic[usefulregion]
382
425
  self.mask_crfound = np.zeros_like(self.data, dtype=bool)
383
- self.mask_crfound[region] = mask_crfound[region]
426
+ self.mask_crfound[usefulregion] = mask_crfound[usefulregion]
384
427
  # Process the mask: dilation and labeling
385
428
  if np.any(self.mask_crfound):
386
429
  num_cr_pixels_before_dilation = np.sum(self.mask_crfound)
387
430
  dilation = self.lacosmic_params['dilation']['value']
388
431
  if dilation > 0:
389
432
  # 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']
433
+ self.mask_crfound = dilatemask(
434
+ mask=self.mask_crfound,
435
+ iterations=self.lacosmic_params['dilation']['value'],
436
+ connectivity=1
395
437
  )
396
438
  num_cr_pixels_after_dilation = np.sum(self.mask_crfound)
397
439
  sdum = str(num_cr_pixels_after_dilation)
@@ -407,7 +449,7 @@ class CosmicRayCleanerApp(ImageDisplay):
407
449
  # diagonal connections too, so we define a 3x3 square.
408
450
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
409
451
  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)}}")
452
+ print(f"Number of cosmic rays features (grouped pixels)...: {self.num_features:>{len(sdum)}}")
411
453
  self.apply_lacosmic_button.config(state=tk.NORMAL)
412
454
  self.examine_detected_cr_button.config(state=tk.NORMAL)
413
455
  self.update_cr_overlay()
@@ -452,10 +494,13 @@ class CosmicRayCleanerApp(ImageDisplay):
452
494
  """Apply the selected cleaning method to the detected cosmic rays."""
453
495
  if np.any(self.mask_crfound):
454
496
  # recalculate labels and number of features
455
- structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
497
+ structure = [[1, 1, 1],
498
+ [1, 1, 1],
499
+ [1, 1, 1]]
456
500
  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}")
501
+ sdum = str(np.sum(self.mask_crfound))
502
+ print(f"Number of cosmic ray pixels detected by L.A.Cosmic...........: {sdum}")
503
+ print(f"Number of cosmic rays (grouped pixels) detected by L.A.Cosmic: {self.num_features:>{len(sdum)}}")
459
504
  # Define parameters for L.A.Cosmic from default dictionary
460
505
  editor_window = tk.Toplevel(self.root)
461
506
  editor = InterpolationEditor(
@@ -463,7 +508,7 @@ class CosmicRayCleanerApp(ImageDisplay):
463
508
  last_dilation=self.lacosmic_params['dilation']['value'],
464
509
  last_npoints=self.last_npoints,
465
510
  last_degree=self.last_degree,
466
- auxdata=self.auxdata
511
+ auxdata=self.auxdata,
467
512
  )
468
513
  # Make it modal (blocks interaction with main window)
469
514
  editor_window.transient(self.root)
@@ -498,7 +543,7 @@ class CosmicRayCleanerApp(ImageDisplay):
498
543
  self.mask_crfound[self.mask_crfound] = False
499
544
  num_cr_cleaned = self.num_features
500
545
  else:
501
- for i in range(1, self.num_features + 1):
546
+ for i in tqdm(range(1, self.num_features + 1)):
502
547
  tmp_mask_fixed = np.zeros_like(self.data, dtype=bool)
503
548
  if cleaning_method == 'x':
504
549
  interpolation_performed, _, _ = interpolation_x(
@@ -536,6 +581,15 @@ class CosmicRayCleanerApp(ImageDisplay):
536
581
  npoints=editor.npoints,
537
582
  method='median'
538
583
  )
584
+ elif cleaning_method == 'a-mean':
585
+ interpolation_performed, _, _ = interpolation_a(
586
+ data=self.data,
587
+ mask_fixed=tmp_mask_fixed,
588
+ cr_labels=self.cr_labels,
589
+ cr_index=i,
590
+ npoints=editor.npoints,
591
+ method='mean'
592
+ )
539
593
  else:
540
594
  raise ValueError(f"Unknown cleaning method: {cleaning_method}")
541
595
  if interpolation_performed:
@@ -548,8 +602,9 @@ class CosmicRayCleanerApp(ImageDisplay):
548
602
  # recalculate labels and number of features
549
603
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
550
604
  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}")
605
+ sdum = str(np.sum(self.mask_crfound))
606
+ print(f"Remaining number of cosmic ray pixels...........: {sdum}")
607
+ print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
553
608
  # redraw image to show the changes
554
609
  self.image.set_data(self.data)
555
610
  self.canvas.draw_idle()
@@ -612,8 +667,9 @@ class CosmicRayCleanerApp(ImageDisplay):
612
667
  # recalculate labels and number of features
613
668
  structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
614
669
  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}")
670
+ sdum = str(np.sum(self.mask_crfound))
671
+ print(f"Remaining number of cosmic ray pixels...........: {sdum}")
672
+ print(f"Remaining number of cosmic rays (grouped pixels): {self.num_features:>{len(sdum)}}")
617
673
  # redraw image to show the changes
618
674
  self.image.set_data(self.data)
619
675
  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')
@@ -193,8 +193,11 @@ class ReviewCosmicRay(ImageDisplay):
193
193
  self.interp_s_button = tk.Button(self.button_frame2, text="[s]urface interp.",
194
194
  command=lambda: self.interp_a('surface'))
195
195
  self.interp_s_button.pack(side=tk.LEFT, padx=5)
196
- self.interp_m_button = tk.Button(self.button_frame2, text="[m]edian",
196
+ self.interp_d_button = tk.Button(self.button_frame2, text="me[d]ian",
197
197
  command=lambda: self.interp_a('median'))
198
+ self.interp_d_button.pack(side=tk.LEFT, padx=5)
199
+ self.interp_m_button = tk.Button(self.button_frame2, text="[m]ean",
200
+ command=lambda: self.interp_a('mean'))
198
201
  self.interp_m_button.pack(side=tk.LEFT, padx=5)
199
202
  self.interp_l_button = tk.Button(self.button_frame2, text="[l]acosmic", command=self.use_lacosmic)
200
203
  self.interp_l_button.pack(side=tk.LEFT, padx=5)
@@ -262,20 +265,24 @@ class ReviewCosmicRay(ImageDisplay):
262
265
  # to avoid image shifts when some pixels are unmarked or new ones are marked
263
266
  i0 = int(np.mean(ycr_list_original) + 0.5)
264
267
  j0 = int(np.mean(xcr_list_original) + 0.5)
265
- semiwidth = MAX_PIXEL_DISTANCE_TO_CR
268
+ max_distance_from_center = np.max(
269
+ [np.max(np.abs(ycr_list_original - i0)),
270
+ np.max(np.abs(xcr_list_original - j0))]
271
+ )
272
+ semiwidth = int(np.max([max_distance_from_center, MAX_PIXEL_DISTANCE_TO_CR]))
266
273
  jmin = j0 - semiwidth if j0 - semiwidth >= 0 else 0
267
274
  jmax = j0 + semiwidth if j0 + semiwidth < self.data.shape[1] else self.data.shape[1] - 1
268
275
  imin = i0 - semiwidth if i0 - semiwidth >= 0 else 0
269
276
  imax = i0 + semiwidth if i0 + semiwidth < self.data.shape[0] else self.data.shape[0] - 1
270
277
  # Force the region to be of size (2*semiwidth + 1) x (2*semiwidth + 1)
271
278
  if jmin == 0:
272
- jmax = 2 * semiwidth
279
+ jmax = np.min([2 * semiwidth, self.data.shape[1] - 1])
273
280
  elif jmax == self.data.shape[1] - 1:
274
- jmin = self.data.shape[1] - 1 - 2 * semiwidth
281
+ jmin = np.max([0, self.data.shape[1] - 1 - 2 * semiwidth])
275
282
  if imin == 0:
276
- imax = 2 * semiwidth
283
+ imax = np.min([2 * semiwidth, self.data.shape[0] - 1])
277
284
  elif imax == self.data.shape[0] - 1:
278
- imin = self.data.shape[0] - 1 - 2 * semiwidth
285
+ imin = np.max([0, self.data.shape[0] - 1 - 2 * semiwidth])
279
286
  self.region = SliceRegion2D(f'[{jmin+1}:{jmax+1}, {imin+1}:{imax+1}]', mode='fits').python
280
287
  self.ax.clear()
281
288
  vmin = self.get_vmin()
@@ -333,6 +340,7 @@ class ReviewCosmicRay(ImageDisplay):
333
340
  self.interp_x_button.config(state=tk.DISABLED)
334
341
  self.interp_y_button.config(state=tk.DISABLED)
335
342
  self.interp_s_button.config(state=tk.DISABLED)
343
+ self.interp_d_button.config(state=tk.DISABLED)
336
344
  self.interp_m_button.config(state=tk.DISABLED)
337
345
  self.interp_l_button.config(state=tk.DISABLED)
338
346
  self.interp_aux_button.config(state=tk.DISABLED)
@@ -387,7 +395,7 @@ class ReviewCosmicRay(ImageDisplay):
387
395
  Parameters
388
396
  ----------
389
397
  method : str
390
- The interpolation method to use ('surface' or 'median').
398
+ The interpolation method to use ('surface', 'median' or 'mean').
391
399
  """
392
400
  print(f"{method} interpolation of cosmic ray {self.cr_index}")
393
401
  interpolation_performed, xfit_all, yfit_all = interpolation_a(
@@ -444,6 +452,7 @@ class ReviewCosmicRay(ImageDisplay):
444
452
  self.interp_x_button.config(state=tk.DISABLED)
445
453
  self.interp_y_button.config(state=tk.DISABLED)
446
454
  self.interp_s_button.config(state=tk.DISABLED)
455
+ self.interp_d_button.config(state=tk.DISABLED)
447
456
  self.interp_m_button.config(state=tk.DISABLED)
448
457
  self.interp_l_button.config(state=tk.DISABLED)
449
458
  self.interp_aux_button.config(state=tk.DISABLED)
@@ -458,6 +467,7 @@ class ReviewCosmicRay(ImageDisplay):
458
467
  self.interp_x_button.config(state=tk.NORMAL)
459
468
  self.interp_y_button.config(state=tk.NORMAL)
460
469
  self.interp_s_button.config(state=tk.NORMAL)
470
+ self.interp_d_button.config(state=tk.NORMAL)
461
471
  self.interp_m_button.config(state=tk.NORMAL)
462
472
  if self.cleandata_lacosmic is not None:
463
473
  if self.last_dilation is None or self.last_dilation == 0:
@@ -484,6 +494,7 @@ class ReviewCosmicRay(ImageDisplay):
484
494
  self.interp_x_button.config(state=tk.NORMAL)
485
495
  self.interp_y_button.config(state=tk.NORMAL)
486
496
  self.interp_s_button.config(state=tk.NORMAL)
497
+ self.interp_d_button.config(state=tk.NORMAL)
487
498
  self.interp_m_button.config(state=tk.NORMAL)
488
499
  if self.cleandata_lacosmic is not None:
489
500
  if self.last_dilation is None or self.last_dilation == 0:
@@ -513,9 +524,12 @@ class ReviewCosmicRay(ImageDisplay):
513
524
  elif event.key == 's':
514
525
  if self.interp_s_button.cget("state") != "disabled":
515
526
  self.interp_a('surface')
527
+ elif event.key == 'd':
528
+ if self.interp_d_button.cget("state") != "disabled":
529
+ self.interp_a('median')
516
530
  elif event.key == 'm':
517
531
  if self.interp_m_button.cget("state") != "disabled":
518
- self.interp_a('median')
532
+ self.interp_a('mean')
519
533
  elif event.key == 'l':
520
534
  if self.interp_l_button.cget("state") != "disabled":
521
535
  self.use_lacosmic()
@@ -551,6 +565,7 @@ class ReviewCosmicRay(ImageDisplay):
551
565
  self.interp_x_button.config(state=tk.DISABLED)
552
566
  self.interp_y_button.config(state=tk.DISABLED)
553
567
  self.interp_s_button.config(state=tk.DISABLED)
568
+ self.interp_d_button.config(state=tk.DISABLED)
554
569
  self.interp_m_button.config(state=tk.DISABLED)
555
570
  self.interp_l_button.config(state=tk.DISABLED)
556
571
  self.interp_aux_button.config(state=tk.DISABLED)
@@ -559,6 +574,7 @@ class ReviewCosmicRay(ImageDisplay):
559
574
  self.interp_x_button.config(state=tk.NORMAL)
560
575
  self.interp_y_button.config(state=tk.NORMAL)
561
576
  self.interp_s_button.config(state=tk.NORMAL)
577
+ self.interp_d_button.config(state=tk.NORMAL)
562
578
  self.interp_m_button.config(state=tk.NORMAL)
563
579
  if self.cleandata_lacosmic is not None:
564
580
  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.3'
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.3
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