teareduce 0.5.4__py3-none-any.whl → 0.5.7__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.
@@ -0,0 +1,58 @@
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
+ """Merge peak and tail masks for cosmic ray cleaning."""
11
+
12
+ import numpy as np
13
+ from scipy import ndimage
14
+
15
+
16
+ def merge_peak_tail_masks(mask_peaks, mask_tails):
17
+ """Merge peak and tail masks for cosmic ray cleaning.
18
+
19
+ Tail pixels are preserved only if they correspond to CR features
20
+ that are also present in the peak mask.
21
+
22
+ Parameters
23
+ ----------
24
+ mask_peaks : ndarray
25
+ Boolean array indicating the pixels identified as cosmic ray peaks.
26
+ mask_tails : ndarray
27
+ Boolean array indicating the pixels identified as cosmic ray tails.
28
+
29
+ Returns
30
+ -------
31
+ merged_mask : ndarray
32
+ Boolean array indicating the merged cosmic ray mask.
33
+ """
34
+ # check that input masks are numpy arrays
35
+ if not isinstance(mask_peaks, np.ndarray) or not isinstance(mask_tails, np.ndarray):
36
+ raise TypeError("Input masks must be numpy arrays.")
37
+ # check that input masks have the same shape
38
+ if mask_peaks.shape != mask_tails.shape:
39
+ raise ValueError("Input masks must have the same shape.")
40
+ # check that input masks are boolean arrays
41
+ if mask_peaks.dtype != bool or mask_tails.dtype != bool:
42
+ raise TypeError("Input masks must be boolean arrays.")
43
+
44
+ # find structures in tail mask
45
+ structure = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
46
+ cr_labels_tails, num_crs_tails = ndimage.label(mask_tails, structure=structure)
47
+ # generate mask of ones at peak pixels
48
+ mask_peaks_ones = np.zeros(mask_peaks.shape, dtype=float)
49
+ mask_peaks_ones[mask_peaks] = 1.0
50
+ # preserve only those tail pixels that are flagged as peaks
51
+ cr_labels_tails_preserved = mask_peaks_ones * cr_labels_tails
52
+ # generate new mask with preserved tail pixels
53
+ merged_mask = np.zeros_like(mask_peaks, dtype=bool)
54
+ for icr in np.unique(cr_labels_tails_preserved):
55
+ if icr > 0:
56
+ merged_mask[cr_labels_tails == icr] = True
57
+
58
+ return merged_mask
@@ -0,0 +1,182 @@
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
+ """Module defining a progress bar widget for Tkinter."""
11
+
12
+ import tkinter as tk
13
+ from tkinter import ttk
14
+ import time
15
+
16
+
17
+ class ModalProgressBar:
18
+ def __init__(
19
+ self, parent, iterable=None, total=None, desc="Processing", completion_msg="Processing completed successfully!"
20
+ ):
21
+ self.parent = parent
22
+ self.iterable = iterable
23
+ self.total = total if total is not None else (len(iterable) if iterable is not None else 100)
24
+ self.current = 0
25
+ self.start_time = time.time()
26
+ self.window = None
27
+ self.desc = desc
28
+ self.completion_msg = completion_msg
29
+ self.continue_clicked = False
30
+
31
+ def __enter__(self):
32
+ # Create the modal window when entering context
33
+ self.window = tk.Toplevel(self.parent)
34
+ self.window.title("Progress")
35
+
36
+ # Make it modal
37
+ self.window.transient(self.parent)
38
+ self.window.grab_set()
39
+ self.window.protocol("WM_DELETE_WINDOW", lambda: None)
40
+
41
+ # Set geometry and force it
42
+ minwinsize_x = 400
43
+ minwinsize_y = 120
44
+ self.window.minsize(minwinsize_x, minwinsize_y)
45
+ self.window.update_idletasks()
46
+ self.window.update()
47
+
48
+ # Center on parent
49
+ self._center_on_parent()
50
+
51
+ # UI elements
52
+ default_font = tk.font.nametofont("TkDefaultFont")
53
+ bold_font = default_font.copy()
54
+ bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
55
+ self.desc_label = tk.Label(self.window, text=self.desc, font=bold_font)
56
+ self.desc_label.pack(padx=10, pady=5)
57
+
58
+ self.progress = ttk.Progressbar(self.window, length=minwinsize_x - 20, mode="determinate", maximum=self.total)
59
+ self.progress.pack(padx=10, pady=10)
60
+
61
+ self.status_label = tk.Label(self.window, text=f"0/{self.total} (0.0%)")
62
+ self.status_label.pack(padx=10, pady=2)
63
+
64
+ self.time_label = tk.Label(self.window, text="Elapsed: 0s | ETA: --")
65
+ self.time_label.pack(padx=10, pady=2)
66
+
67
+ # Continue button (to close the dialog after completion; hidden until done)
68
+ self.continue_button = tk.Button(self.window, text="Continue", command=self._on_continue)
69
+ self.continue_button.pack(padx=10, pady=10)
70
+ self.continue_button.pack_forget() # Hide initially
71
+
72
+ # Force another update
73
+ self.window.update_idletasks()
74
+ self.window.update()
75
+
76
+ return self
77
+
78
+ def __exit__(self, exc_type, exc_val, exc_tb):
79
+ if self.window:
80
+ # Show completion screen instead of closing
81
+ self._show_completion()
82
+
83
+ # Wait for user to click Continue
84
+ self.window.wait_variable(self._continue_var)
85
+
86
+ # Now close
87
+ self._destroy()
88
+ return False
89
+
90
+ def __iter__(self):
91
+ """Allow iteration like tqdm"""
92
+ if self.iterable is None:
93
+ raise ValueError("No iterable provided for iteration")
94
+
95
+ for item in self.iterable:
96
+ yield item
97
+ self.update(1)
98
+
99
+ def _center_on_parent(self):
100
+ self.window.update_idletasks()
101
+ x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.window.winfo_width() // 2)
102
+ y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.window.winfo_height() // 2)
103
+ self.window.geometry(f"+{x}+{y}")
104
+
105
+ def update(self, n=1):
106
+ self.current += n
107
+ self.progress["value"] = self.current
108
+ percentage = (self.current / self.total) * 100
109
+
110
+ elapsed = time.time() - self.start_time
111
+
112
+ if self.current > 0:
113
+ rate = self.current / elapsed
114
+ remaining = self.total - self.current
115
+ eta_seconds = remaining / rate if rate > 0 else 0
116
+
117
+ elapsed_str = self._format_time(elapsed)
118
+ eta_str = self._format_time(eta_seconds)
119
+ total_str = self._format_time(elapsed + eta_seconds)
120
+ rate_str = f"{rate:.2f} CR/s" if rate >= 1 else f"{1/rate:.2f} s/CR"
121
+
122
+ self.status_label.config(text=f"{self.current}/{self.total} ({percentage:.1f}%) | {rate_str}")
123
+ self.time_label.config(text=f"Expected Total: {total_str} | Elapsed: {elapsed_str} | ETA: {eta_str}")
124
+ self.window.update_idletasks()
125
+ self.window.update()
126
+
127
+ def _format_time(self, seconds):
128
+ if seconds < 60:
129
+ return f"{seconds:.1f} s"
130
+ elif seconds < 3600:
131
+ minutes = int(seconds // 60)
132
+ secs = int(seconds % 60)
133
+ return f"{minutes}m {secs} s"
134
+ else:
135
+ hours = int(seconds // 3600)
136
+ minutes = int((seconds % 3600) // 60)
137
+ return f"{hours}h {minutes} m"
138
+
139
+ def _show_completion(self):
140
+ """Transform the window into a completion dialog"""
141
+ elapsed = time.time() - self.start_time
142
+ elapsed_str = self._format_time(elapsed)
143
+
144
+ # Update title
145
+ self.window.title("Completed")
146
+
147
+ # Update description to show completion message
148
+ default_font = tk.font.nametofont("TkDefaultFont")
149
+ bold_font = default_font.copy()
150
+ bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
151
+ self.desc_label.config(text=self.completion_msg, fg="green", font=bold_font)
152
+
153
+ # Hide progress bar
154
+ self.progress.pack_forget()
155
+
156
+ # Update status to show final stats
157
+ avg_rate = self.current / elapsed if elapsed > 0 else 0
158
+ rate_str = f"{avg_rate:.2f} CR/s" if avg_rate >= 1 else f"{1/avg_rate:.2f} s/CR"
159
+ self.status_label.config(text=f"Processed {self.current} CRs | {rate_str}")
160
+
161
+ # Update time label
162
+ self.time_label.config(text=f"Total time: {elapsed_str}")
163
+
164
+ # Show the Continue button
165
+ self.continue_button.pack(padx=10, pady=15)
166
+
167
+ # Create a variable to track when Continue is clicked
168
+ self._continue_var = tk.BooleanVar(value=False)
169
+
170
+ # Re-enable close button to work like Continue
171
+ self.window.protocol("WM_DELETE_WINDOW", self._on_continue)
172
+
173
+ self.window.update()
174
+ self._center_on_parent()
175
+
176
+ def _on_continue(self):
177
+ """Called when Continue button is clicked"""
178
+ self._continue_var.set(True)
179
+
180
+ def _destroy(self):
181
+ self.window.grab_release()
182
+ self.window.destroy()
@@ -13,11 +13,13 @@ import tkinter as tk
13
13
  from tkinter import ttk
14
14
  from tkinter import messagebox
15
15
 
16
+ from .centerchildparent import center_on_parent
16
17
  from .definitions import lacosmic_default_dict
17
18
 
18
19
 
19
20
  class ParameterEditor:
20
21
  """A dialog to edit L.A.Cosmic parameters."""
22
+
21
23
  def __init__(self, root, param_dict, window_title, xmin, xmax, ymin, ymax, imgshape):
22
24
  """Initialize the parameter editor dialog.
23
25
 
@@ -31,12 +33,16 @@ class ParameterEditor:
31
33
  Title of the dialog window.
32
34
  xmin : int
33
35
  Minimum x-coordinate of the region to be examined.
36
+ From 1 to NAXIS1.
34
37
  xmax : int
35
38
  Maximum x-coordinate of the region to be examined.
39
+ From 1 to NAXIS1.
36
40
  ymin : int
37
41
  Minimum y-coordinate of the region to be examined.
42
+ From 1 to NAXIS2.
38
43
  ymax : int
39
44
  Maximum y-coordinate of the region to be examined.
45
+ From 1 to NAXIS2.
40
46
  imgshape : tuple
41
47
  Shape of the image (height, width).
42
48
 
@@ -70,16 +76,17 @@ class ParameterEditor:
70
76
  self.root.title(window_title)
71
77
  self.param_dict = param_dict
72
78
  # Set default region values
73
- self.param_dict['xmin']['value'] = xmin
74
- self.param_dict['xmax']['value'] = xmax
75
- self.param_dict['ymin']['value'] = ymin
76
- self.param_dict['ymax']['value'] = ymax
79
+ self.param_dict["xmin"]["value"] = xmin
80
+ self.param_dict["xmax"]["value"] = xmax
81
+ self.param_dict["ymin"]["value"] = ymin
82
+ self.param_dict["ymax"]["value"] = ymax
77
83
  self.imgshape = imgshape
78
- self.entries = {'run1': {}, 'run2': {}} # dictionary to hold entry widgets
84
+ self.entries = {"run1": {}, "run2": {}} # dictionary to hold entry widgets
79
85
  self.result_dict = None
80
86
 
81
87
  # Create the form
82
88
  self.create_widgets()
89
+ center_on_parent(child=self.root, parent=self.root.master)
83
90
 
84
91
  def create_widgets(self):
85
92
  """Create the widgets for the dialog."""
@@ -90,97 +97,124 @@ class ParameterEditor:
90
97
  row = 0
91
98
 
92
99
  # Subtitle for L.A.Cosmic parameters
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=4, pady=(0, 15))
100
+ default_font = tk.font.nametofont("TkDefaultFont")
101
+ bold_font = default_font.copy()
102
+ bold_font.configure(weight="bold", size=default_font.cget("size") + 2)
103
+ subtitle_label = tk.Label(main_frame, text="L.A.Cosmic Parameters", font=bold_font)
104
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
95
105
  row += 1
96
106
 
97
107
  # Create labels and entry fields for each parameter.
108
+ bold_font_subheader = default_font.copy()
109
+ bold_font_subheader.configure(weight="bold", size=default_font.cget("size") + 1)
110
+ label = tk.Label(main_frame, text="Parameter", font=bold_font_subheader, anchor="w", fg="gray")
111
+ label.grid(row=row, column=0, sticky="e", pady=0)
112
+ label = tk.Label(main_frame, text="Run 1", font=bold_font_subheader, anchor="w", fg="gray", width=10)
113
+ label.grid(row=row, column=1, sticky="w", padx=10, pady=0)
114
+ label = tk.Label(main_frame, text="Run 2", font=bold_font_subheader, anchor="w", fg="gray", width=10)
115
+ label.grid(row=row, column=2, sticky="w", padx=10, pady=0)
116
+ label = tk.Label(main_frame, text="Type", font=bold_font_subheader, anchor="w", fg="gray", width=10)
117
+ label.grid(row=row, column=3, sticky="w", pady=0)
118
+ row += 1
98
119
  # Note: here we are using entry_vars to trace changes in the entries
99
120
  # so that we can update the color of run2 entries if they differ from run1.
100
121
  self.entry_vars = {}
101
122
  for key, info in self.param_dict.items():
102
- if not key.startswith('run1_'):
123
+ if not key.startswith("run1_"):
103
124
  continue
104
125
  # 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)
126
+ label = tk.Label(main_frame, text=f"{key[5:]}:", anchor="e", width=15)
127
+ label.grid(row=row, column=0, sticky="w", pady=5)
107
128
  # Entry field for run1
108
129
  self.entry_vars[key] = tk.StringVar()
109
- self.entry_vars[key].trace_add('write', lambda *args: self.update_colour_param_run1_run2())
130
+ self.entry_vars[key].trace_add("write", lambda *args: self.update_colour_param_run1_run2())
110
131
  entry = tk.Entry(main_frame, textvariable=self.entry_vars[key], width=10)
111
- entry.insert(0, str(info['value']))
132
+ entry.insert(0, str(info["value"]))
112
133
  entry.grid(row=row, column=1, padx=10, pady=5)
113
134
  self.entries[key] = entry # dictionary to hold entry widgets
114
135
  # Entry field for run2
115
- key2 = 'run2_' + key[5:]
136
+ key2 = "run2_" + key[5:]
116
137
  self.entry_vars[key2] = tk.StringVar()
117
- self.entry_vars[key2].trace_add('write', lambda *args: self.update_colour_param_run1_run2())
138
+ self.entry_vars[key2].trace_add("write", lambda *args: self.update_colour_param_run1_run2())
118
139
  entry = tk.Entry(main_frame, textvariable=self.entry_vars[key2], width=10)
119
- entry.insert(0, str(self.param_dict[key2]['value']))
140
+ entry.insert(0, str(self.param_dict[key2]["value"]))
120
141
  entry.grid(row=row, column=2, padx=10, pady=5)
121
- self.entries['run2_'+key[5:]] = entry # dictionary to hold entry widgets
142
+ self.entries["run2_" + key[5:]] = entry # dictionary to hold entry widgets
122
143
  # 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)
144
+ type_label = tk.Label(main_frame, text=f"({info['type'].__name__})", fg="gray", anchor="w", width=10)
145
+ type_label.grid(row=row, column=3, sticky="w", pady=5)
125
146
  row += 1
126
147
  # self.update_colour_param_run1_run2()
127
148
 
128
149
  # Separator
129
- separator1 = ttk.Separator(main_frame, orient='horizontal')
130
- separator1.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
150
+ separator1 = ttk.Separator(main_frame, orient="horizontal")
151
+ separator1.grid(row=row, column=0, columnspan=4, sticky="ew", pady=(10, 10))
131
152
  row += 1
132
153
 
133
154
  # Subtitle for additional parameters
134
- subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=("Arial", 14, "bold"))
135
- subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
155
+ subtitle_label = tk.Label(main_frame, text="Additional Parameters", font=bold_font)
156
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
136
157
  row += 1
137
158
 
138
159
  # Dilation label and entry
139
- label = tk.Label(main_frame, text="Dilation:", anchor='e', width=15)
140
- label.grid(row=row, column=0, sticky='w', pady=5)
160
+ label = tk.Label(main_frame, text="Dilation:", anchor="e", width=15)
161
+ label.grid(row=row, column=0, sticky="w", pady=5)
162
+ entry = tk.Entry(main_frame, width=10)
163
+ entry.insert(0, str(self.param_dict["dilation"]["value"]))
164
+ entry.grid(row=row, column=1, padx=10, pady=5)
165
+ self.entries["dilation"] = entry
166
+ type_label = tk.Label(
167
+ main_frame, text=f"({self.param_dict['dilation']['type'].__name__})", fg="gray", anchor="w", width=10
168
+ )
169
+ type_label.grid(row=row, column=2, sticky="w", pady=5)
170
+ row += 1
171
+
172
+ label = tk.Label(main_frame, text="Border Padding:", anchor="e", width=15)
173
+ label.grid(row=row, column=0, sticky="w", pady=5)
141
174
  entry = tk.Entry(main_frame, width=10)
142
- entry.insert(0, str(self.param_dict['dilation']['value']))
175
+ entry.insert(0, str(self.param_dict["borderpadd"]["value"]))
143
176
  entry.grid(row=row, column=1, padx=10, pady=5)
144
- self.entries['dilation'] = entry
145
- type_label = tk.Label(main_frame, text=f"({self.param_dict['dilation']['type'].__name__})",
146
- fg='gray', anchor='w', width=10)
147
- type_label.grid(row=row, column=2, sticky='w', pady=5)
177
+ self.entries["borderpadd"] = entry
178
+ type_label = tk.Label(
179
+ main_frame, text=f"({self.param_dict['borderpadd']['type'].__name__})", fg="gray", anchor="w", width=10
180
+ )
181
+ type_label.grid(row=row, column=2, sticky="w", pady=5)
148
182
  row += 1
149
183
 
150
184
  # Separator
151
- separator2 = ttk.Separator(main_frame, orient='horizontal')
152
- separator2.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
185
+ separator2 = ttk.Separator(main_frame, orient="horizontal")
186
+ separator2.grid(row=row, column=0, columnspan=4, sticky="ew", pady=(10, 10))
153
187
  row += 1
154
188
 
155
189
  # Subtitle for region to be examined
156
- subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=("Arial", 14, "bold"))
157
- subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 15))
190
+ subtitle_label = tk.Label(main_frame, text="Region to be Examined", font=bold_font)
191
+ subtitle_label.grid(row=row, column=0, columnspan=4, pady=(0, 10))
158
192
  row += 1
159
193
 
160
194
  # Region to be examined label and entries
161
195
  for key, info in self.param_dict.items():
162
- if key.lower() in ['xmin', 'xmax', 'ymin', 'ymax']:
196
+ if key.lower() in ["xmin", "xmax", "ymin", "ymax"]:
163
197
  # Parameter name label
164
- label = tk.Label(main_frame, text=f"{key}:", anchor='e', width=15)
165
- label.grid(row=row, column=0, sticky='w', pady=5)
198
+ label = tk.Label(main_frame, text=f"{key}:", anchor="e", width=15)
199
+ label.grid(row=row, column=0, sticky="w", pady=5)
166
200
  # Entry field
167
201
  entry = tk.Entry(main_frame, width=10)
168
- entry.insert(0, str(info['value']))
202
+ entry.insert(0, str(info["value"]))
169
203
  entry.grid(row=row, column=1, padx=10, pady=5)
170
204
  self.entries[key] = entry # dictionary to hold entry widgets
171
205
  # Type label
172
206
  dumtext = f"({info['type'].__name__})"
173
- if key.lower() in ['xmax', 'ymax']:
207
+ if key.lower() in ["xmin", "xmax"]:
174
208
  dumtext += f" --> [1, {self.imgshape[1]}]"
175
209
  else:
176
210
  dumtext += f" --> [1, {self.imgshape[0]}]"
177
- type_label = tk.Label(main_frame, text=dumtext, fg='gray', anchor='w', width=15)
178
- type_label.grid(row=row, column=2, sticky='w', pady=5)
211
+ type_label = tk.Label(main_frame, text=dumtext, fg="gray", anchor="w", width=15)
212
+ type_label.grid(row=row, column=2, sticky="w", pady=5)
179
213
  row += 1
180
214
 
181
215
  # Separator
182
- separator3 = ttk.Separator(main_frame, orient='horizontal')
183
- separator3.grid(row=row, column=0, columnspan=4, sticky='ew', pady=(10, 10))
216
+ separator3 = ttk.Separator(main_frame, orient="horizontal")
217
+ separator3.grid(row=row, column=0, columnspan=4, sticky="ew", pady=(10, 10))
184
218
  row += 1
185
219
 
186
220
  # Button frame
@@ -189,15 +223,15 @@ class ParameterEditor:
189
223
 
190
224
  # OK button
191
225
  ok_button = tk.Button(button_frame, text="OK", width=5, command=self.on_ok)
192
- ok_button.pack(side='left', padx=5)
226
+ ok_button.pack(side="left", padx=5)
193
227
 
194
228
  # Cancel button
195
229
  cancel_button = tk.Button(button_frame, text="Cancel", width=5, command=self.on_cancel)
196
- cancel_button.pack(side='left', padx=5)
230
+ cancel_button.pack(side="left", padx=5)
197
231
 
198
232
  # Reset button
199
233
  reset_button = tk.Button(button_frame, text="Reset", width=5, command=self.on_reset)
200
- reset_button.pack(side='left', padx=5)
234
+ reset_button.pack(side="left", padx=5)
201
235
 
202
236
  def on_ok(self):
203
237
  """Validate and save the updated values"""
@@ -205,61 +239,60 @@ class ParameterEditor:
205
239
  updated_dict = {}
206
240
 
207
241
  for key, info in self.param_dict.items():
208
- if key == 'nruns':
242
+ if key == "nruns":
209
243
  continue
210
244
  entry_value = self.entries[key].get()
211
- value_type = info['type']
245
+ value_type = info["type"]
212
246
 
213
247
  # Convert string to appropriate type
214
248
  if value_type == bool:
215
249
  # Handle boolean conversion
216
- if entry_value.lower() in ['true', '1', 'yes']:
250
+ if entry_value.lower() in ["true", "1", "yes"]:
217
251
  converted_value = True
218
- elif entry_value.lower() in ['false', '0', 'no']:
252
+ elif entry_value.lower() in ["false", "0", "no"]:
219
253
  converted_value = False
220
254
  else:
221
255
  raise ValueError(f"Invalid boolean value for {key}")
222
256
  else:
223
257
  converted_value = value_type(entry_value)
224
- if 'positive' in info and info['positive'] and converted_value < 0:
258
+ if "positive" in info and info["positive"] and converted_value < 0:
225
259
  raise ValueError(f"Value for {key} must be positive")
226
260
 
227
- updated_dict[key] = {
228
- 'value': converted_value,
229
- 'type': value_type
230
- }
261
+ updated_dict[key] = {"value": converted_value, "type": value_type}
231
262
 
232
263
  # Check whether any run1 and run2 parameters differ
233
264
  nruns = 1
234
265
  for key in self.param_dict.keys():
235
- if key.startswith('run1_'):
266
+ if key.startswith("run1_"):
236
267
  parname = key[5:]
237
- key2 = 'run2_' + parname
238
- if updated_dict[key]['value'] != updated_dict[key2]['value']:
268
+ key2 = "run2_" + parname
269
+ if updated_dict[key]["value"] != updated_dict[key2]["value"]:
239
270
  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)")
271
+ print(
272
+ f"Parameter '{parname}' differs between run1 and run2: "
273
+ f"{updated_dict[key]['value']} (run1) vs {updated_dict[key2]['value']} (run2)"
274
+ )
242
275
 
243
276
  # Additional validation for region limits
244
277
  try:
245
- if updated_dict['xmax']['value'] <= updated_dict['xmin']['value']:
278
+ if updated_dict["xmax"]["value"] <= updated_dict["xmin"]["value"]:
246
279
  raise ValueError("xmax must be greater than xmin")
247
- if updated_dict['ymax']['value'] <= updated_dict['ymin']['value']:
280
+ if updated_dict["ymax"]["value"] <= updated_dict["ymin"]["value"]:
248
281
  raise ValueError("ymax must be greater than ymin")
249
282
  self.result_dict = updated_dict
250
- self.result_dict['nruns'] = {'value': nruns, 'type': int, 'positive': True}
283
+ self.result_dict["nruns"] = {"value": nruns, "type": int, "positive": True}
251
284
  if nruns not in [1, 2]:
252
285
  raise ValueError("nruns must be 1 or 2")
253
286
  self.root.destroy()
254
287
  except ValueError as e:
255
- messagebox.showerror("Invalid Inputs",
256
- "Error in region limits:\n"
257
- f"{str(e)}\n\nPlease check your inputs.")
288
+ messagebox.showerror(
289
+ "Invalid Inputs", "Error in region limits:\n" f"{str(e)}\n\nPlease check your inputs."
290
+ )
258
291
 
259
292
  except ValueError as e:
260
- messagebox.showerror("Invalid Inputs",
261
- f"Error converting value for {key}:\n{str(e)}\n\n"
262
- "Please check your inputs.")
293
+ messagebox.showerror(
294
+ "Invalid Inputs", f"Error converting value for {key}:\n{str(e)}\n\n" "Please check your inputs."
295
+ )
263
296
 
264
297
  def on_cancel(self):
265
298
  """Close without saving"""
@@ -269,15 +302,15 @@ class ParameterEditor:
269
302
  def on_reset(self):
270
303
  """Reset all fields to original values"""
271
304
  self.param_dict = lacosmic_default_dict.copy()
272
- self.param_dict['xmin']['value'] = 1
273
- self.param_dict['xmax']['value'] = self.imgshape[1]
274
- self.param_dict['ymin']['value'] = 1
275
- self.param_dict['ymax']['value'] = self.imgshape[0]
305
+ self.param_dict["xmin"]["value"] = 1
306
+ self.param_dict["xmax"]["value"] = self.imgshape[1]
307
+ self.param_dict["ymin"]["value"] = 1
308
+ self.param_dict["ymax"]["value"] = self.imgshape[0]
276
309
  for key, info in self.param_dict.items():
277
- if key == 'nruns':
310
+ if key == "nruns":
278
311
  continue
279
312
  self.entries[key].delete(0, tk.END)
280
- self.entries[key].insert(0, str(info['value']))
313
+ self.entries[key].insert(0, str(info["value"]))
281
314
 
282
315
  def get_result(self):
283
316
  """Return the updated dictionary"""
@@ -287,10 +320,10 @@ class ParameterEditor:
287
320
  """Update the foreground color of run1 and run2 entries."""
288
321
  # Highlight run2 parameter if different from run1
289
322
  for key in self.param_dict.keys():
290
- if key.startswith('run1_'):
323
+ if key.startswith("run1_"):
291
324
  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')
325
+ if key in self.entries and "run2_" + parname in self.entries:
326
+ if self.entries[key].get() != self.entries["run2_" + parname].get():
327
+ self.entries["run2_" + parname].config(fg="red")
295
328
  else:
296
- self.entries['run2_'+parname].config(fg='black')
329
+ self.entries["run2_" + parname].config(fg="black")