teareduce 0.6.8__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Universidad Complutense de Madrid
2
+ # Copyright 2025-2026 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -10,11 +10,10 @@
10
10
  """Interactive Cosmic Ray cleaning tool."""
11
11
 
12
12
  import argparse
13
+ import glob
13
14
  import tkinter as tk
14
15
  from tkinter import filedialog
15
- from tkinter import simpledialog
16
16
  import os
17
- from pathlib import Path
18
17
  import platform
19
18
  from rich import print
20
19
  from rich_argparse import RichHelpFormatter
@@ -38,14 +37,7 @@ def main():
38
37
  formatter_class=RichHelpFormatter,
39
38
  )
40
39
  parser.add_argument("input_fits", nargs="?", default=None, help="Path to the FITS file to be cleaned.")
41
- parser.add_argument("--extension", type=str, default="0", help="FITS extension to use (default: 0).")
42
- parser.add_argument("--auxfile", type=str, default=None, help="Auxiliary FITS file")
43
- parser.add_argument(
44
- "--extension_auxfile",
45
- type=str,
46
- default="0",
47
- help="FITS extension for auxiliary file (default: 0).",
48
- )
40
+ parser.add_argument("--auxfile", type=str, default=None, help="Auxiliary FITS files (comma-separated if several).")
49
41
  parser.add_argument(
50
42
  "--fontfamily",
51
43
  type=str,
@@ -114,17 +106,77 @@ def main():
114
106
  use_auxfile = True
115
107
  if use_auxfile:
116
108
  print(f"Selected auxiliary FITS file: {args.auxfile}")
117
- args.extension_auxfile = ask_extension_input_image(args.auxfile, imgshape=None)
109
+ extension_auxfile = ask_extension_input_image(args.auxfile, imgshape=None)
110
+ args.auxfile = f"{args.auxfile}[{extension_auxfile}]"
111
+ else:
112
+ args.auxfile = None
118
113
  root.destroy()
119
114
 
120
- # Check that input files, and the corresponding extensions, exist
121
- if not os.path.isfile(args.input_fits):
122
- print(f"Error: File '{args.input_fits}' does not exist.")
123
- exit(1)
124
- if args.auxfile is not None and not os.path.isfile(args.auxfile):
125
- print(f"Error: Auxiliary file '{args.auxfile}' does not exist.")
115
+ # Check that input file exists
116
+ if "[" in args.input_fits:
117
+ input_fits = args.input_fits[: args.input_fits.index("[")]
118
+ extension = args.input_fits[args.input_fits.index("[") + 1 : args.input_fits.index("]")]
119
+ else:
120
+ input_fits = args.input_fits
121
+ extension = "0"
122
+ if not os.path.isfile(input_fits):
123
+ print(f"Error: File '{input_fits}' does not exist.")
126
124
  exit(1)
127
125
 
126
+ # Process auxiliary files if provided
127
+ auxfile_list = []
128
+ extension_auxfile_list = []
129
+ if args.auxfile is not None:
130
+ # Check several auxiliary files separated by commas
131
+ if "\n" in args.auxfile:
132
+ if "?" in args.auxfile or "*" in args.auxfile:
133
+ print("Error: Cannot combine newlines and wildcards in --auxfile specification.")
134
+ exit(1)
135
+ # Replace newlines by commas to allow:
136
+ # --auxfile="`ls file??.fits`" -> without extensions
137
+ # --auxfile="`for f in file??.fits; do echo "${f}[primary]"; done`" -> with extension name (the same for all files!)
138
+ args.auxfile = args.auxfile.replace("\n", ",")
139
+ elif "?" in args.auxfile or "*" in args.auxfile:
140
+ # Handle wildcards to allow:
141
+ # --auxfile="file??.fits" -> without extensions
142
+ # --auxfile="file??.fits[primary]" -> with extension name (the same for all files!)
143
+ if "," in args.auxfile:
144
+ print("Error: Cannot combine wildcards with commas in --auxfile specification.")
145
+ exit(1)
146
+ # Expand possible wildcards in the auxiliary file specification
147
+ if "[" in args.auxfile:
148
+ s = args.auxfile.strip()
149
+ ext = None
150
+ if s.endswith("]"):
151
+ ext = s[s.rfind("[") + 1 : s.rfind("]")]
152
+ s = s[: s.rfind("[")]
153
+ matched_files = sorted(glob.glob(s))
154
+ if len(matched_files) == 0:
155
+ print(f"Error: No files matched the pattern '{s}'.")
156
+ exit(1)
157
+ args.auxfile = ",".join([f"{fname}[{ext}]" if ext is not None else fname for fname in matched_files])
158
+ else:
159
+ matched_files = sorted(glob.glob(args.auxfile))
160
+ if len(matched_files) == 0:
161
+ print(f"Error: No files matched the pattern '{args.auxfile}'.")
162
+ exit(1)
163
+ args.auxfile = ",".join(matched_files)
164
+ # Now process the comma-separated auxiliary files
165
+ for item in args.auxfile.split(","):
166
+ # Extract possible [ext] from filename
167
+ if "[" in item:
168
+ # Separate filename and extension, removing blanks
169
+ fname = item[: item.index("[")].strip()
170
+ # Check that file exists
171
+ if not os.path.isfile(fname):
172
+ print(f"Error: File '{fname}' does not exist.")
173
+ exit(1)
174
+ auxfile_list.append(fname)
175
+ extension_auxfile_list.append(item[item.index("[") + 1 : item.index("]")])
176
+ else:
177
+ auxfile_list.append(item.strip())
178
+ extension_auxfile_list.append("0")
179
+
128
180
  # Initialize Tkinter root
129
181
  try:
130
182
  root = tk.Tk()
@@ -149,10 +201,10 @@ def main():
149
201
  # Create and run the application
150
202
  CosmicRayCleanerApp(
151
203
  root=root,
152
- input_fits=args.input_fits,
153
- extension=args.extension,
154
- auxfile=args.auxfile,
155
- extension_auxfile=args.extension_auxfile,
204
+ input_fits=input_fits,
205
+ extension=extension,
206
+ auxfile_list=auxfile_list,
207
+ extension_auxfile_list=extension_auxfile_list,
156
208
  fontfamily=args.fontfamily,
157
209
  fontsize=args.fontsize,
158
210
  width=args.width,
@@ -0,0 +1,126 @@
1
+ #
2
+ # Copyright 2026 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
+ import tkinter as tk
11
+ from tkinter import simpledialog, ttk
12
+
13
+
14
+ class ThreeButtonListDialog(simpledialog.Dialog):
15
+ """
16
+ A modal dialog that shows a list of strings and returns:
17
+ - 1..N : the highlighted item index (1-based) when pressing OK (or double-click / Enter)
18
+ - 0 : when pressing Cancel or closing the window (Esc or window X)
19
+ - N+1 : when pressing New (N = len(items))
20
+ """
21
+
22
+ def __init__(self, parent, title, prompt, items):
23
+ self.prompt = prompt
24
+ self.items = list(items)
25
+ self.result = None
26
+ self._force_new = False # used to bypass selection validation for "New"
27
+ super().__init__(parent, title)
28
+
29
+ # ---------- Core layout ----------
30
+ def body(self, master):
31
+ ttk.Label(master, text=self.prompt).grid(row=0, column=0, columnspan=1, sticky="w", padx=8, pady=(8, 4))
32
+
33
+ self.listbox = tk.Listbox(master, height=min(12, max(4, len(self.items))), activestyle="dotbox")
34
+ for s in self.items:
35
+ self.listbox.insert(tk.END, s)
36
+ self.listbox.grid(row=1, column=0, sticky="nsew", padx=(8, 0), pady=4)
37
+
38
+ scrollbar = ttk.Scrollbar(master, orient="vertical", command=self.listbox.yview)
39
+ self.listbox.configure(yscrollcommand=scrollbar.set)
40
+ scrollbar.grid(row=1, column=1, sticky="ns", pady=4, padx=(0, 8))
41
+
42
+ # Keyboard bindings
43
+ self.listbox.bind("<Double-Button-1>", lambda e: self.ok()) # OK on double-click
44
+ self.bind("<Return>", lambda e: self.ok()) # Enter = OK
45
+ self.bind("<Escape>", lambda e: self.cancel()) # Esc = Cancel (→ 0)
46
+
47
+ # Resize behavior
48
+ master.grid_columnconfigure(0, weight=1)
49
+ master.grid_rowconfigure(1, weight=1)
50
+
51
+ if self.items:
52
+ self.listbox.selection_set(0)
53
+ self.listbox.activate(0)
54
+
55
+ return self.listbox # initial focus on listbox
56
+
57
+ # ---------- Buttons (OK, Cancel, New) ----------
58
+ def buttonbox(self):
59
+ box = ttk.Frame(self)
60
+
61
+ self.ok_button = ttk.Button(box, text="OK", width=10, command=self.ok)
62
+ self.ok_button.grid(row=0, column=0, padx=5, pady=8)
63
+
64
+ self.cancel_button = ttk.Button(box, text="Cancel", width=10, command=self.cancel)
65
+ self.cancel_button.grid(row=0, column=1, padx=5, pady=8)
66
+
67
+ self.new_button = ttk.Button(box, text="New", width=10, command=self._on_new)
68
+ self.new_button.grid(row=0, column=2, padx=5, pady=8)
69
+
70
+ # Bindings for Return/Escape already set in body()
71
+ self.bind("<Return>", lambda e: self.ok())
72
+ self.bind("<Escape>", lambda e: self.cancel())
73
+
74
+ box.pack(side="bottom", fill="x")
75
+
76
+ # ---------- Dialog flow ----------
77
+ def validate(self):
78
+ """
79
+ Called by OK to decide whether dialog can close.
80
+ - If 'New' was pressed, allow closing without selection.
81
+ - Otherwise, ensure there is a highlighted item.
82
+ """
83
+ if self._force_new:
84
+ return True
85
+ cur = self.listbox.curselection()
86
+ if not cur:
87
+ self.bell()
88
+ return False
89
+ self._selected_index = cur[0]
90
+ return True
91
+
92
+ def apply(self):
93
+ """
94
+ Called after validate(). Set the final result here.
95
+ """
96
+ if self._force_new:
97
+ self.result = len(self.items) + 1 # N+1 (New)
98
+ else:
99
+ self.result = self._selected_index + 1 # 1..N (index of selection)
100
+
101
+ def _on_new(self):
102
+ """
103
+ Trigger New: returns N+1.
104
+ This must bypass selection validation.
105
+ """
106
+ self._force_new = True
107
+ self.ok()
108
+
109
+ # Important: Don't overwrite OK/New results when closing via Dialog.ok().
110
+ # Dialog.ok() calls validate() -> apply() -> cancel().
111
+ # Only set 0 in cancel() if result wasn't already set.
112
+ def cancel(self, event=None):
113
+ if self.result is None:
114
+ self.result = 0
115
+ super().cancel(event)
116
+
117
+
118
+ def choose_index_with_three_buttons(parent, title, prompt, items):
119
+ """
120
+ Show the dialog and return:
121
+ - 1..N : highlighted item index (OK)
122
+ - 0 : Cancel/close
123
+ - N+1 : New
124
+ """
125
+ dlg = ThreeButtonListDialog(parent, title, prompt, items)
126
+ return dlg.result
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright 2025 Universidad Complutense de Madrid
2
+ # Copyright 2025-2026 Universidad Complutense de Madrid
3
3
  #
4
4
  # This file is part of teareduce
5
5
  #
@@ -69,6 +69,7 @@ from rich import print
69
69
 
70
70
  from .askextension import ask_extension_input_image
71
71
  from .centerchildparent import center_on_parent
72
+ from .choose_index_with_three_buttons import choose_index_with_three_buttons
72
73
  from .definitions import cosmicconn_default_dict
73
74
  from .definitions import deepcr_default_dict
74
75
  from .definitions import lacosmic_default_dict
@@ -117,8 +118,8 @@ class CosmicRayCleanerApp(ImageDisplay):
117
118
  root,
118
119
  input_fits,
119
120
  extension="0",
120
- auxfile=None,
121
- extension_auxfile="0",
121
+ auxfile_list=[],
122
+ extension_auxfile_list=[],
122
123
  fontfamily=DEFAULT_FONT_FAMILY,
123
124
  fontsize=DEFAULT_FONT_SIZE,
124
125
  width=DEFAULT_TK_WINDOW_SIZE_X,
@@ -136,10 +137,10 @@ class CosmicRayCleanerApp(ImageDisplay):
136
137
  Path to the FITS file to be cleaned.
137
138
  extension : str, optional
138
139
  FITS extension to use (default is "0").
139
- auxfile : str, optional
140
- Path to an auxiliary FITS file (default is None).
141
- extension_auxfile : str, optional
142
- FITS extension for auxiliary file (default is "0").
140
+ auxfile_list : list of str, optional
141
+ List of paths to auxiliary FITS files (default is empty list).
142
+ extension_auxfile_list : list of str, optional
143
+ List of FITS extensions for auxiliary files (default is empty list).
143
144
  fontfamily : str, optional
144
145
  Font family for the GUI (default is "Helvetica").
145
146
  fontsize : int, optional
@@ -222,12 +223,18 @@ class CosmicRayCleanerApp(ImageDisplay):
222
223
  FITS extension to use.
223
224
  data : np.ndarray
224
225
  The image data from the FITS file.
225
- auxfile : str
226
- Path to an auxiliary FITS file.
227
- extension_auxfile : int
228
- FITS extension for auxiliary file.
229
- auxdata : np.ndarray
230
- The image data from the auxiliary FITS file.
226
+ auxfile_list : list of str
227
+ List of Paths to auxiliary FITS files.
228
+ extension_auxfile_list : list of int
229
+ List of FITS extensions for auxiliary files.
230
+ auxdata : list of np.ndarray
231
+ The list of image data from the auxiliary FITS files.
232
+ auxdata_mean : np.ndarray
233
+ The mean image data computed from the auxiliary FITS files.
234
+ auxdata_median : np.ndarray
235
+ The median image data computed from the auxiliary FITS files.
236
+ auxdata_min : np.ndarray
237
+ The minimum image data computed from the auxiliary FITS files.
231
238
  overplot_cr_pixels : bool
232
239
  Flag to indicate whether to overlay cosmic ray pixels.
233
240
  mask_crfound : np.ndarray
@@ -300,9 +307,12 @@ class CosmicRayCleanerApp(ImageDisplay):
300
307
  self.input_fits = input_fits
301
308
  self.extension = extension
302
309
  self.data = None
303
- self.auxfile = auxfile
304
- self.extension_auxfile = extension_auxfile
305
- self.auxdata = None
310
+ self.auxfile_list = auxfile_list
311
+ self.extension_auxfile_list = extension_auxfile_list
312
+ self.auxdata = []
313
+ self.auxdata_mean = None
314
+ self.auxdata_median = None
315
+ self.auxdata_min = None
306
316
  self.overplot_cr_pixels = True
307
317
  self.mask_crfound = None
308
318
  self.load_fits_file()
@@ -452,7 +462,7 @@ class CosmicRayCleanerApp(ImageDisplay):
452
462
  help_text="Toggle the display of auxiliary data.",
453
463
  )
454
464
  self.toggle_auxdata_button.pack(side=tk.LEFT, padx=5)
455
- if self.auxdata is None:
465
+ if len(self.auxdata) == 0 or self.auxdata is None:
456
466
  self.toggle_auxdata_button.config(state=tk.DISABLED)
457
467
  else:
458
468
  self.toggle_auxdata_button.config(state=tk.NORMAL)
@@ -578,14 +588,14 @@ class CosmicRayCleanerApp(ImageDisplay):
578
588
  ylabel = "Y pixel (from 1 to NAXIS2)"
579
589
  extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
580
590
  self.image_aspect = "equal"
581
- self.displaying_auxdata = False
591
+ self.displaying_auxdata = 0 # 0: main data, > 0: auxdata
582
592
  self.image, _, _ = imshow(
583
593
  fig=self.fig,
584
594
  ax=self.ax,
585
595
  data=self.data,
586
596
  vmin=vmin,
587
597
  vmax=vmax,
588
- title=f"data: {os.path.basename(self.input_fits)}",
598
+ title=f"data: {os.path.basename(self.input_fits)}[{self.extension}]",
589
599
  xlabel=xlabel,
590
600
  ylabel=ylabel,
591
601
  extent=extent,
@@ -694,8 +704,8 @@ class CosmicRayCleanerApp(ImageDisplay):
694
704
  Notes
695
705
  -----
696
706
  This method loads the FITS file specified by `self.input_fits` and
697
- reads the data from the specified extension. If an auxiliary file is
698
- provided, it also loads the auxiliary data from the specified extension.
707
+ reads the data from the specified extension. If auxiliary files are
708
+ provided, it also loads the auxiliary data from the specified extensions.
699
709
  The loaded data is stored in `self.data` and `self.auxdata` attributes.
700
710
  """
701
711
  # check if extension is compatible with an integer
@@ -725,43 +735,67 @@ class CosmicRayCleanerApp(ImageDisplay):
725
735
  self.mask_crfound = np.zeros(self.data.shape, dtype=bool)
726
736
  naxis2, naxis1 = self.data.shape
727
737
  self.region = SliceRegion2D(f"[1:{naxis1}, 1:{naxis2}]", mode="fits").python
738
+
728
739
  # Read auxiliary file if provided
729
- if self.auxfile is not None:
730
- # check if extension_auxfile is compatible with an integer
731
- try:
732
- extnum_aux = int(self.extension_auxfile)
733
- self.extension_auxfile = extnum_aux
734
- except ValueError:
735
- # Keep as string (delaying checking until opening the file)
736
- self.extension_auxfile = self.extension_auxfile.upper() # Convert to uppercase
737
- try:
738
- with fits.open(self.auxfile, mode="readonly") as hdul_aux:
739
- if isinstance(self.extension_auxfile, int):
740
- if self.extension_auxfile < 0 or self.extension_auxfile >= len(hdul_aux):
741
- raise IndexError(f"Extension index {self.extension_auxfile} out of range.")
742
- else:
743
- if self.extension_auxfile not in hdul_aux:
744
- raise KeyError(f"Extension name '{self.extension_auxfile}' not found.")
745
- print(
746
- f"Reading auxiliary file [bold green]{self.auxfile}[/bold green], extension {self.extension_auxfile}"
747
- )
748
- self.auxdata = hdul_aux[self.extension_auxfile].data
749
- if self.auxdata.shape != self.data.shape:
750
- print(f"data shape...: {self.data.shape}")
751
- print(f"auxdata shape: {self.auxdata.shape}")
752
- raise ValueError("Auxiliary file has different shape.")
753
- except Exception as e:
754
- sys.exit(f"Error loading auxiliary FITS file: {e}")
740
+ naux = len(self.auxfile_list)
741
+ if naux > 0:
742
+ for i in range(naux):
743
+ # check if extension_auxfile is compatible with an integer
744
+ try:
745
+ extnum_aux = int(self.extension_auxfile_list[i])
746
+ self.extension_auxfile_list[i] = extnum_aux
747
+ except ValueError:
748
+ # Keep as string (delaying checking until opening the file)
749
+ self.extension_auxfile_list[i] = self.extension_auxfile_list[i].upper() # Convert to uppercase
750
+ try:
751
+ with fits.open(self.auxfile_list[i], mode="readonly") as hdul_aux:
752
+ if isinstance(self.extension_auxfile_list[i], int):
753
+ if self.extension_auxfile_list[i] < 0 or self.extension_auxfile_list[i] >= len(hdul_aux):
754
+ raise IndexError(f"Extension index {self.extension_auxfile_list[i]} out of range.")
755
+ else:
756
+ if self.extension_auxfile_list[i] not in hdul_aux:
757
+ raise KeyError(f"Extension name '{self.extension_auxfile_list[i]}' not found.")
758
+ print(
759
+ f"Reading auxiliary file [bold green]{self.auxfile_list[i]}[/bold green], extension {self.extension_auxfile_list[i]}"
760
+ )
761
+ self.auxdata.append(hdul_aux[self.extension_auxfile_list[i]].data)
762
+ if self.auxdata[i].shape != self.data.shape:
763
+ print(f"data shape...: {self.data.shape}")
764
+ print(f"auxdata shape: {self.auxdata[i].shape}")
765
+ raise ValueError("Auxiliary file has different shape.")
766
+ except Exception as e:
767
+ sys.exit(f"Error loading auxiliary FITS file {self.auxfile_list[i]}:\n{e}")
768
+ # Compute mean, median, min if multiple auxdata are loaded
769
+ if naux > 1:
770
+ data3d = np.zeros((len(self.auxdata),) + self.data.shape, dtype=self.data.dtype)
771
+ for i in range(len(self.auxdata)):
772
+ data3d[i, :, :] = self.auxdata[i]
773
+ self.auxdata_mean = np.mean(data3d, axis=0)
774
+ self.auxdata_median = np.median(data3d, axis=0)
775
+ self.auxdata_min = np.min(data3d, axis=0)
776
+ else:
777
+ self.auxdata_mean = self.auxdata[0].copy()
778
+ self.auxdata_median = self.auxdata[0].copy()
779
+ self.auxdata_min = self.auxdata[0].copy()
755
780
 
756
781
  def load_auxdata_from_file(self):
757
782
  """Load auxiliary data from a FITS file."""
758
- if self.auxfile is not None:
759
- overwrite = messagebox.askyesno(
760
- "Overwrite Auxiliary Data",
761
- f"An auxiliary file is already loaded:\n\n{self.auxfile}\n\n" "Do you want to overwrite it?",
783
+ if len(self.auxfile_list) > 0:
784
+ nchoice = choose_index_with_three_buttons(
785
+ parent=self.root,
786
+ title="Select Auxiliary Data Index",
787
+ prompt="Select the auxiliary data to overwrite\nor create new auxiliary data:",
788
+ items=[
789
+ f"Aux. data #{i+1}: {os.path.basename(self.auxfile_list[i])}[{self.extension_auxfile_list[i]}]"
790
+ for i in range(len(self.auxfile_list))
791
+ ],
762
792
  )
763
- if not overwrite:
793
+ if nchoice == 0:
764
794
  return
795
+ else:
796
+ aux_number = nchoice - 1
797
+ else:
798
+ aux_number = 0
765
799
  auxfile = filedialog.askopenfilename(
766
800
  initialdir=os.getcwd(),
767
801
  title="Select auxiliary FITS file",
@@ -782,9 +816,26 @@ class CosmicRayCleanerApp(ImageDisplay):
782
816
  print(f"data shape...: {self.data.shape}")
783
817
  print(f"auxdata shape: {auxdata_loaded.shape}")
784
818
  raise ValueError("Auxiliary file has different shape.")
785
- self.auxfile = auxfile
786
- self.auxdata = auxdata_loaded
787
- self.extension_auxfile = extension
819
+ if aux_number == len(self.auxfile_list):
820
+ self.auxfile_list.append(auxfile)
821
+ self.extension_auxfile_list.append(extension)
822
+ self.auxdata.append(auxdata_loaded)
823
+ else:
824
+ self.auxfile_list[aux_number] = auxfile
825
+ self.auxdata[aux_number] = auxdata_loaded
826
+ self.extension_auxfile_list[aux_number] = extension
827
+ # Compute mean, median, min if multiple auxdata are loaded
828
+ if len(self.auxdata) > 1:
829
+ data3d = np.zeros((len(self.auxdata),) + self.data.shape, dtype=self.data.dtype)
830
+ for i in range(len(self.auxdata)):
831
+ data3d[i, :, :] = self.auxdata[i]
832
+ self.auxdata_mean = np.mean(data3d, axis=0)
833
+ self.auxdata_median = np.median(data3d, axis=0)
834
+ self.auxdata_min = np.min(data3d, axis=0)
835
+ else:
836
+ self.auxdata_mean = self.auxdata[0].copy()
837
+ self.auxdata_median = self.auxdata[0].copy()
838
+ self.auxdata_min = self.auxdata[0].copy()
788
839
  print(f"Loaded auxiliary data from {auxfile}")
789
840
  self.toggle_auxdata_button.config(state=tk.NORMAL)
790
841
  except Exception as e:
@@ -863,23 +914,61 @@ class CosmicRayCleanerApp(ImageDisplay):
863
914
  self.use_cursor_button.config(text="[c]ursor: OFF")
864
915
 
865
916
  def toggle_auxdata(self):
866
- """Toggle between main data and auxiliary data for display."""
867
- if self.displaying_auxdata:
917
+ """Toggle between main data and auxiliary data for display.
918
+
919
+ If multiple auxiliary data are loaded, cycle through them,
920
+ including mean, median, and min of all auxiliary data.
921
+ """
922
+ naux = len(self.auxdata)
923
+ if naux == 0:
924
+ self.displaying_auxdata = 0
925
+ elif naux == 1:
926
+ if self.displaying_auxdata == 0:
927
+ self.displaying_auxdata = 1
928
+ else:
929
+ self.displaying_auxdata = 0
930
+ else:
931
+ self.displaying_auxdata += 1
932
+ if self.displaying_auxdata > naux + 3:
933
+ self.displaying_auxdata = 0
934
+ if self.displaying_auxdata == 0:
868
935
  # Switch to main data
869
936
  vmin = self.get_vmin()
870
937
  vmax = self.get_vmax()
871
938
  self.image.set_data(self.data)
872
939
  self.image.set_clim(vmin=vmin, vmax=vmax)
873
- self.displaying_auxdata = False
874
- self.ax.set_title(f"data: {os.path.basename(self.input_fits)}")
875
- else:
940
+ self.displaying_auxdata = 0
941
+ self.ax.set_title(f"data: {os.path.basename(self.input_fits)}[{self.extension}]")
942
+ elif self.displaying_auxdata > 0 and self.displaying_auxdata <= naux:
876
943
  # Switch to auxiliary data
877
944
  vmin = self.get_vmin()
878
945
  vmax = self.get_vmax()
879
- self.image.set_data(self.auxdata)
946
+ self.image.set_data(self.auxdata[self.displaying_auxdata - 1])
947
+ self.image.set_clim(vmin=vmin, vmax=vmax)
948
+ self.ax.set_title(
949
+ f"auxdata: {os.path.basename(self.auxfile_list[self.displaying_auxdata - 1])}[{self.extension_auxfile_list[self.displaying_auxdata - 1]}]"
950
+ )
951
+ elif self.displaying_auxdata == naux + 1:
952
+ # Switch to auxiliary mean data
953
+ vmin = self.get_vmin()
954
+ vmax = self.get_vmax()
955
+ self.image.set_data(self.auxdata_mean)
956
+ self.image.set_clim(vmin=vmin, vmax=vmax)
957
+ self.ax.set_title("auxdata: MEAN of all loaded auxiliary data")
958
+ elif self.displaying_auxdata == naux + 2:
959
+ # Switch to auxiliary median data
960
+ vmin = self.get_vmin()
961
+ vmax = self.get_vmax()
962
+ self.image.set_data(self.auxdata_median)
963
+ self.image.set_clim(vmin=vmin, vmax=vmax)
964
+ self.ax.set_title("auxdata: MEDIAN of all loaded auxiliary data")
965
+ elif self.displaying_auxdata == naux + 3:
966
+ # Switch to auxiliary min data
967
+ vmin = self.get_vmin()
968
+ vmax = self.get_vmax()
969
+ self.image.set_data(self.auxdata_min)
880
970
  self.image.set_clim(vmin=vmin, vmax=vmax)
881
- self.displaying_auxdata = True
882
- self.ax.set_title(f"auxdata: {os.path.basename(self.auxfile)}")
971
+ self.ax.set_title("auxdata: MIN of all loaded auxiliary data")
883
972
  self.canvas.draw_idle()
884
973
 
885
974
  def toggle_aspect(self):
@@ -1337,7 +1426,8 @@ class CosmicRayCleanerApp(ImageDisplay):
1337
1426
  last_maskfill_operator=self.last_maskfill_operator,
1338
1427
  last_maskfill_smooth=self.last_maskfill_smooth,
1339
1428
  last_maskfill_verbose=self.last_maskfill_verbose,
1340
- auxdata=self.auxdata,
1429
+ auxfile_list=self.auxfile_list,
1430
+ extension_auxfile_list=self.extension_auxfile_list,
1341
1431
  cleandata_lacosmic=self.cleandata_lacosmic,
1342
1432
  cleandata_pycosmic=self.cleandata_pycosmic,
1343
1433
  cleandata_deepcr=self.cleandata_deepcr,
@@ -1422,11 +1512,33 @@ class CosmicRayCleanerApp(ImageDisplay):
1422
1512
  self.mask_crfound[mask_crfound_region] = False
1423
1513
  data_has_been_modified = True
1424
1514
  elif cleaning_method == "auxdata":
1425
- if self.auxdata is None:
1515
+ if len(self.auxdata) == 0:
1426
1516
  print("No auxiliary data available. Cleaning skipped!")
1427
1517
  return
1428
- # Replace detected CR pixels with auxiliary data values
1429
- self.data[mask_crfound_region] = self.auxdata[mask_crfound_region]
1518
+ inum = editor.auxiliary_data_index # 1-based index of auxiliary data
1519
+ if inum <= len(self.auxdata):
1520
+ print(
1521
+ f"Using auxiliary data: "
1522
+ f"{os.path.basename(self.auxfile_list[inum - 1])}"
1523
+ f"[{self.extension_auxfile_list[inum - 1]}]"
1524
+ )
1525
+ # Replace detected CR pixels with auxiliary data values
1526
+ self.data[mask_crfound_region] = self.auxdata[inum - 1][mask_crfound_region]
1527
+ elif inum == len(self.auxdata) + 1:
1528
+ print("Using auxiliary data: MEAN of all loaded auxiliary data")
1529
+ # Replace detected CR pixels with auxiliary mean data values
1530
+ self.data[mask_crfound_region] = self.auxdata_mean[mask_crfound_region]
1531
+ elif inum == len(self.auxdata) + 2:
1532
+ print("Using auxiliary data: MEDIAN of all loaded auxiliary data")
1533
+ # Replace detected CR pixels with auxiliary median data values
1534
+ self.data[mask_crfound_region] = self.auxdata_median[mask_crfound_region]
1535
+ elif inum == len(self.auxdata) + 3:
1536
+ print("Using auxiliary data: MIN of all loaded auxiliary data")
1537
+ # Replace detected CR pixels with auxiliary min data values
1538
+ self.data[mask_crfound_region] = self.auxdata_min[mask_crfound_region]
1539
+ else:
1540
+ print(f"Invalid auxiliary data index: {inum}. Cleaning skipped!")
1541
+ return
1430
1542
  # update mask_fixed to include the newly fixed pixels
1431
1543
  self.mask_fixed[mask_crfound_region] = True
1432
1544
  # upate mask_crfound by eliminating the cleaned pixels
@@ -1533,8 +1645,14 @@ class CosmicRayCleanerApp(ImageDisplay):
1533
1645
  root=review_window,
1534
1646
  root_width=self.width,
1535
1647
  root_height=self.height,
1648
+ input_fits=self.input_fits,
1536
1649
  data=self.data,
1537
1650
  auxdata=self.auxdata,
1651
+ auxfile_list=self.auxfile_list,
1652
+ extension_auxfile_list=self.extension_auxfile_list,
1653
+ auxdata_mean=self.auxdata_mean,
1654
+ auxdata_median=self.auxdata_median,
1655
+ auxdata_min=self.auxdata_min,
1538
1656
  cleandata_lacosmic=self.cleandata_lacosmic,
1539
1657
  cleandata_pycosmic=self.cleandata_pycosmic,
1540
1658
  cleandata_deepcr=self.cleandata_deepcr,
@@ -1551,8 +1669,14 @@ class CosmicRayCleanerApp(ImageDisplay):
1551
1669
  root=review_window,
1552
1670
  root_width=self.width,
1553
1671
  root_height=self.height,
1672
+ input_fits=self.input_fits,
1554
1673
  data=self.data,
1555
1674
  auxdata=self.auxdata,
1675
+ auxfile_list=self.auxfile_list,
1676
+ extension_auxfile_list=self.extension_auxfile_list,
1677
+ auxdata_mean=self.auxdata_mean,
1678
+ auxdata_median=self.auxdata_median,
1679
+ auxdata_min=self.auxdata_min,
1556
1680
  cleandata_lacosmic=self.cleandata_lacosmic,
1557
1681
  cleandata_pycosmic=self.cleandata_pycosmic,
1558
1682
  cleandata_deepcr=self.cleandata_deepcr,
@@ -1616,7 +1740,7 @@ class CosmicRayCleanerApp(ImageDisplay):
1616
1740
  self.set_cursor_onoff()
1617
1741
  elif event.key == "a":
1618
1742
  self.toggle_aspect()
1619
- elif event.key == "t" and self.auxdata is not None:
1743
+ elif event.key == "t" and len(self.auxdata) > 0:
1620
1744
  self.toggle_auxdata()
1621
1745
  elif event.key == ",":
1622
1746
  self.set_minmax()