teareduce 0.4.6__tar.gz → 0.4.8__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 (43) hide show
  1. {teareduce-0.4.6/src/teareduce.egg-info → teareduce-0.4.8}/PKG-INFO +1 -1
  2. {teareduce-0.4.6 → teareduce-0.4.8}/pyproject.toml +1 -1
  3. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/__init__.py +1 -0
  4. teareduce-0.4.8/src/teareduce/cleanest/__main__.py +49 -0
  5. teareduce-0.4.8/src/teareduce/cleanest/cosmicraycleanerapp.py +203 -0
  6. teareduce-0.4.6/src/teareduce/cleanest.py → teareduce-0.4.8/src/teareduce/cleanest/reviewcosmicray.py +4 -212
  7. teareduce-0.4.8/src/teareduce/cookbook/__init__.py +0 -0
  8. teareduce-0.4.8/src/teareduce/cookbook/get_cookbook_file.py +34 -0
  9. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/sliceregion.py +85 -15
  10. teareduce-0.4.8/src/teareduce/tests/__init__.py +0 -0
  11. teareduce-0.4.8/src/teareduce/tests/test_sliceregion.py +116 -0
  12. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/version.py +1 -1
  13. {teareduce-0.4.6 → teareduce-0.4.8/src/teareduce.egg-info}/PKG-INFO +1 -1
  14. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce.egg-info/SOURCES.txt +6 -1
  15. teareduce-0.4.8/src/teareduce.egg-info/entry_points.txt +2 -0
  16. teareduce-0.4.6/src/teareduce/tests/test_sliceregion.py +0 -49
  17. teareduce-0.4.6/src/teareduce.egg-info/entry_points.txt +0 -2
  18. {teareduce-0.4.6 → teareduce-0.4.8}/LICENSE.txt +0 -0
  19. {teareduce-0.4.6 → teareduce-0.4.8}/README.md +0 -0
  20. {teareduce-0.4.6 → teareduce-0.4.8}/setup.cfg +0 -0
  21. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/avoid_astropy_warnings.py +0 -0
  22. {teareduce-0.4.6/src/teareduce/tests → teareduce-0.4.8/src/teareduce/cleanest}/__init__.py +0 -0
  23. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/correct_pincushion_distortion.py +0 -0
  24. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/cosmicrays.py +0 -0
  25. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/ctext.py +0 -0
  26. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/draw_rectangle.py +0 -0
  27. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/elapsed_time.py +0 -0
  28. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/histogram1d.py +0 -0
  29. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/imshow.py +0 -0
  30. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/numsplines.py +0 -0
  31. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/peaks_spectrum.py +0 -0
  32. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/polfit.py +0 -0
  33. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/robust_std.py +0 -0
  34. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/sdistortion.py +0 -0
  35. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/simulateccdexposure.py +0 -0
  36. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/statsummary.py +0 -0
  37. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/tests/test_version.py +0 -0
  38. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/wavecal.py +0 -0
  39. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/write_array_to_fits.py +0 -0
  40. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce/zscale.py +0 -0
  41. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce.egg-info/dependency_links.txt +0 -0
  42. {teareduce-0.4.6 → teareduce-0.4.8}/src/teareduce.egg-info/requires.txt +0 -0
  43. {teareduce-0.4.6 → teareduce-0.4.8}/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.4.6
3
+ Version: 0.4.8
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
@@ -47,7 +47,7 @@ Homepage = "https://github.com/nicocardiel/teareduce"
47
47
  Repository = "https://github.com/nicocardiel/teareduce.git"
48
48
 
49
49
  [project.scripts]
50
- tea-cleanest = "teareduce.cleanest:main"
50
+ tea-cleanest = "teareduce.cleanest.__main__:main"
51
51
 
52
52
  [tool.setuptools.dynamic]
53
53
  version = {attr = "teareduce.version.VERSION"}
@@ -8,6 +8,7 @@
8
8
  #
9
9
 
10
10
  from .avoid_astropy_warnings import avoid_astropy_warnings
11
+ from .cookbook.get_cookbook_file import get_cookbook_file
11
12
  from .correct_pincushion_distortion import correct_pincushion_distortion
12
13
  from .cosmicrays import cr2images, apply_cr2images_ccddata, crmedian
13
14
  from .ctext import ctext
@@ -0,0 +1,49 @@
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
+ """Interactive Cosmic Ray cleaning tool."""
11
+
12
+ import argparse
13
+ import tkinter as tk
14
+ import os
15
+
16
+ from .cosmicraycleanerapp import CosmicRayCleanerApp
17
+
18
+ import matplotlib
19
+ matplotlib.use("TkAgg")
20
+
21
+
22
+ def main():
23
+ parser = argparse.ArgumentParser(description="Interactive cosmic ray cleaner for FITS images.")
24
+ parser.add_argument("input_fits", help="Path to the FITS file to be cleaned.")
25
+ parser.add_argument("--extension", type=int, default=0,
26
+ help="FITS extension to use (default: 0).")
27
+ parser.add_argument("--output_fits", type=str, default=None,
28
+ help="Path to save the cleaned FITS file")
29
+ args = parser.parse_args()
30
+
31
+ if not os.path.isfile(args.input_fits):
32
+ print(f"Error: File '{args.input_fits}' does not exist.")
33
+ return
34
+ if args.output_fits is not None and os.path.isfile(args.output_fits):
35
+ print(f"Error: Output file '{args.output_fits}' already exists.")
36
+ return
37
+
38
+ # Initialize Tkinter root
39
+ root = tk.Tk()
40
+
41
+ # Create and run the application
42
+ CosmicRayCleanerApp(root, args.input_fits, args.extension, args.output_fits)
43
+
44
+ # Execute
45
+ root.mainloop()
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -0,0 +1,203 @@
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
+ """Define the CosmicRayCleanerApp class."""
11
+
12
+ import tkinter as tk
13
+ from tkinter import filedialog
14
+ from tkinter import simpledialog
15
+
16
+ from astropy.io import fits
17
+ from ccdproc import cosmicray_lacosmic
18
+ import matplotlib.pyplot as plt
19
+ from matplotlib.backend_bases import key_press_handler
20
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
21
+ import numpy as np
22
+ import os
23
+
24
+ from .reviewcosmicray import ReviewCosmicRay
25
+
26
+ from ..imshow import imshow
27
+ from ..zscale import zscale
28
+
29
+ import matplotlib
30
+ matplotlib.use("TkAgg")
31
+
32
+
33
+ class CosmicRayCleanerApp():
34
+ """Main application class for cosmic ray cleaning."""
35
+
36
+ def __init__(self, root, input_fits, extension=0, output_fits=None):
37
+ """
38
+ Initialize the application.
39
+
40
+ Parameters
41
+ ----------
42
+ root : tk.Tk
43
+ The main Tkinter window.
44
+ input_fits : str
45
+ Path to the FITS file to be cleaned.
46
+ extension : int, optional
47
+ FITS extension to use (default is 0).
48
+ output_fits : str, optional
49
+ Path to save the cleaned FITS file (default is None, which prompts
50
+ for a save location).
51
+ """
52
+ self.root = root
53
+ self.root.title("Cosmic Ray Cleaner")
54
+ self.root.geometry("800x700+50+0")
55
+ self.input_fits = input_fits
56
+ self.extension = extension
57
+ self.output_fits = output_fits
58
+ self.load_fits_file()
59
+ self.create_widgets()
60
+
61
+ def load_fits_file(self):
62
+ try:
63
+ with fits.open(self.input_fits, mode='readonly') as hdul:
64
+ self.data = hdul[self.extension].data
65
+ if 'CRMASK' in hdul:
66
+ self.mask_fixed = hdul['CRMASK'].data.astype(bool)
67
+ else:
68
+ self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
69
+ except Exception as e:
70
+ print(f"Error loading FITS file: {e}")
71
+
72
+ def save_fits_file(self):
73
+ if self.output_fits is None:
74
+ base, ext = os.path.splitext(self.input_fits)
75
+ suggested_name = f"{base}_cleaned"
76
+ else:
77
+ suggested_name, _ = os.path.splitext(self.output_fits)
78
+ self.output_fits = filedialog.asksaveasfilename(
79
+ initialdir=os.getcwd(),
80
+ title="Save cleaned FITS file",
81
+ defaultextension=".fits",
82
+ filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
83
+ initialfile=suggested_name
84
+ )
85
+ try:
86
+ with fits.open(self.input_fits, mode='readonly') as hdul:
87
+ hdul[self.extension].data = self.data
88
+ if 'CRMASK' in hdul:
89
+ hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
90
+ else:
91
+ crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name='CRMASK')
92
+ hdul.append(crmask_hdu)
93
+ hdul.writeto(self.output_fits, overwrite=True)
94
+ print(f"Cleaned data saved to {self.output_fits}")
95
+ except Exception as e:
96
+ print(f"Error saving FITS file: {e}")
97
+
98
+ def create_widgets(self):
99
+ # Row 1
100
+ self.button_frame1 = tk.Frame(self.root)
101
+ self.button_frame1.grid(row=0, column=0, pady=5)
102
+ self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
103
+ self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
104
+ self.save_button = tk.Button(self.button_frame1, text="Save cleaned FITS", command=self.save_fits_file)
105
+ self.save_button.pack(side=tk.LEFT, padx=5)
106
+
107
+ # Row 2
108
+ self.button_frame2 = tk.Frame(self.root)
109
+ self.button_frame2.grid(row=1, column=0, pady=5)
110
+ vmin, vmax = zscale(self.data)
111
+ self.vmin_button = tk.Button(self.button_frame2, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
112
+ self.vmin_button.pack(side=tk.LEFT, padx=5)
113
+ self.vmax_button = tk.Button(self.button_frame2, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
114
+ self.vmax_button.pack(side=tk.LEFT, padx=5)
115
+ self.stop_button = tk.Button(self.button_frame2, text="Stop program", command=self.stop_app)
116
+ self.stop_button.pack(side=tk.LEFT, padx=5)
117
+
118
+ # Main frame for figure and toolbar
119
+ self.main_frame = tk.Frame(self.root)
120
+ self.main_frame.grid(row=2, column=0, sticky="nsew")
121
+ self.root.grid_rowconfigure(2, weight=1)
122
+ self.root.grid_columnconfigure(0, weight=1)
123
+ self.main_frame.grid_rowconfigure(0, weight=1)
124
+ self.main_frame.grid_columnconfigure(0, weight=1)
125
+
126
+ # Create figure and axis
127
+ self.fig, self.ax = plt.subplots(figsize=(8, 6))
128
+ xlabel = 'X pixel (from 1 to NAXIS1)'
129
+ ylabel = 'Y pixel (from 1 to NAXIS2)'
130
+ extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
131
+ self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
132
+ xlabel=xlabel, ylabel=ylabel, extent=extent)
133
+ # Note: tight_layout should be called before defining the canvas
134
+ self.fig.tight_layout()
135
+
136
+ # Create canvas and toolbar
137
+ self.canvas = FigureCanvasTkAgg(self.fig, master=self.main_frame)
138
+ # The next two instructions prevent a segmentation fault when pressing "q"
139
+ self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
140
+ self.canvas.mpl_connect("key_press_event", self.on_key)
141
+ self.canvas.mpl_connect("button_press_event", self.on_click)
142
+ canvas_widget = self.canvas.get_tk_widget()
143
+ canvas_widget.grid(row=0, column=0, sticky="nsew")
144
+
145
+ # Matplotlib toolbar
146
+ self.toolbar_frame = tk.Frame(self.main_frame)
147
+ self.toolbar_frame.grid(row=1, column=0, sticky="ew")
148
+ self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
149
+ self.toolbar.update()
150
+
151
+ def set_vmin(self):
152
+ old_vmin = self.get_vmin()
153
+ new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
154
+ if new_vmin is None:
155
+ return
156
+ self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
157
+ self.image.set_clim(vmin=new_vmin)
158
+ self.canvas.draw()
159
+
160
+ def set_vmax(self):
161
+ old_vmax = self.get_vmax()
162
+ new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
163
+ if new_vmax is None:
164
+ return
165
+ self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
166
+ self.image.set_clim(vmax=new_vmax)
167
+ self.canvas.draw()
168
+
169
+ def get_vmin(self):
170
+ return float(self.vmin_button.cget("text").split(":")[1])
171
+
172
+ def get_vmax(self):
173
+ return float(self.vmax_button.cget("text").split(":")[1])
174
+
175
+ def run_lacosmic(self):
176
+ self.run_lacosmic_button.config(state=tk.DISABLED)
177
+ self.stop_button.config(state=tk.DISABLED)
178
+ # Parameters for L.A.Cosmic can be adjusted as needed
179
+ _, mask_crfound = cosmicray_lacosmic(self.data, sigclip=4.5, sigfrac=0.3, objlim=5.0, verbose=True)
180
+ ReviewCosmicRay(
181
+ root=self.root,
182
+ data=self.data,
183
+ mask_fixed=self.mask_fixed,
184
+ mask_crfound=mask_crfound
185
+ )
186
+ print("L.A.Cosmic cleaning applied.")
187
+ self.run_lacosmic_button.config(state=tk.NORMAL)
188
+ self.stop_button.config(state=tk.NORMAL)
189
+
190
+ def stop_app(self):
191
+ self.root.quit()
192
+ self.root.destroy()
193
+
194
+ def on_key(self, event):
195
+ if event.key == 'q':
196
+ pass # Ignore the "q" key to prevent closing the window
197
+ else:
198
+ print(f"Key pressed: {event.key}")
199
+
200
+ def on_click(self, event):
201
+ if event.inaxes:
202
+ x, y = event.xdata, event.ydata
203
+ print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
@@ -7,25 +7,20 @@
7
7
  # License-Filename: LICENSE.txt
8
8
  #
9
9
 
10
- """Interactive Cosmic Ray cleaning tool."""
10
+ """Define the ReviewCosmicRay class."""
11
11
 
12
- import argparse
13
12
  import tkinter as tk
14
- from tkinter import filedialog
15
13
  from tkinter import simpledialog
16
14
 
17
- from astropy.io import fits
18
- from ccdproc import cosmicray_lacosmic
19
15
  import matplotlib.pyplot as plt
20
16
  from matplotlib.backend_bases import key_press_handler
21
17
  from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
22
18
  import numpy as np
23
- import os
24
19
  from scipy import ndimage
25
20
 
26
- from .imshow import imshow
27
- from .sliceregion import SliceRegion2D
28
- from .zscale import zscale
21
+ from ..imshow import imshow
22
+ from ..sliceregion import SliceRegion2D
23
+ from ..zscale import zscale
29
24
 
30
25
  import matplotlib
31
26
  matplotlib.use("TkAgg")
@@ -489,206 +484,3 @@ class ReviewCosmicRay():
489
484
  self.remove_crosses_button.config(state=tk.NORMAL)
490
485
  # Update the display to reflect the change
491
486
  self.update_display()
492
-
493
-
494
- class CosmicRayCleanerApp():
495
- """Main application class for cosmic ray cleaning."""
496
-
497
- def __init__(self, root, input_fits, extension=0, output_fits=None):
498
- """
499
- Initialize the application.
500
-
501
- Parameters
502
- ----------
503
- root : tk.Tk
504
- The main Tkinter window.
505
- input_fits : str
506
- Path to the FITS file to be cleaned.
507
- extension : int, optional
508
- FITS extension to use (default is 0).
509
- output_fits : str, optional
510
- Path to save the cleaned FITS file (default is None, which prompts
511
- for a save location).
512
- """
513
- self.root = root
514
- self.root.title("Cosmic Ray Cleaner")
515
- self.root.geometry("800x700+50+0")
516
- self.input_fits = input_fits
517
- self.extension = extension
518
- self.output_fits = output_fits
519
- self.load_fits_file()
520
- self.create_widgets()
521
-
522
- def load_fits_file(self):
523
- try:
524
- with fits.open(self.input_fits, mode='readonly') as hdul:
525
- self.data = hdul[self.extension].data
526
- if 'CRMASK' in hdul:
527
- self.mask_fixed = hdul['CRMASK'].data.astype(bool)
528
- else:
529
- self.mask_fixed = np.zeros(self.data.shape, dtype=bool)
530
- except Exception as e:
531
- print(f"Error loading FITS file: {e}")
532
-
533
- def save_fits_file(self):
534
- if self.output_fits is None:
535
- base, ext = os.path.splitext(self.input_fits)
536
- suggested_name = f"{base}_cleaned"
537
- else:
538
- suggested_name, _ = os.path.splitext(self.output_fits)
539
- self.output_fits = filedialog.asksaveasfilename(
540
- initialdir=os.getcwd(),
541
- title="Save cleaned FITS file",
542
- defaultextension=".fits",
543
- filetypes=[("FITS files", "*.fits"), ("All files", "*.*")],
544
- initialfile=suggested_name
545
- )
546
- try:
547
- with fits.open(self.input_fits, mode='readonly') as hdul:
548
- hdul[self.extension].data = self.data
549
- if 'CRMASK' in hdul:
550
- hdul['CRMASK'].data = self.mask_fixed.astype(np.uint8)
551
- else:
552
- crmask_hdu = fits.ImageHDU(self.mask_fixed.astype(np.uint8), name='CRMASK')
553
- hdul.append(crmask_hdu)
554
- hdul.writeto(self.output_fits, overwrite=True)
555
- print(f"Cleaned data saved to {self.output_fits}")
556
- except Exception as e:
557
- print(f"Error saving FITS file: {e}")
558
-
559
- def create_widgets(self):
560
- # Row 1
561
- self.button_frame1 = tk.Frame(self.root)
562
- self.button_frame1.grid(row=0, column=0, pady=5)
563
- self.run_lacosmic_button = tk.Button(self.button_frame1, text="Run L.A.Cosmic", command=self.run_lacosmic)
564
- self.run_lacosmic_button.pack(side=tk.LEFT, padx=5)
565
- self.save_button = tk.Button(self.button_frame1, text="Save cleaned FITS", command=self.save_fits_file)
566
- self.save_button.pack(side=tk.LEFT, padx=5)
567
-
568
- # Row 2
569
- self.button_frame2 = tk.Frame(self.root)
570
- self.button_frame2.grid(row=1, column=0, pady=5)
571
- vmin, vmax = zscale(self.data)
572
- self.vmin_button = tk.Button(self.button_frame2, text=f"vmin: {vmin:.2f}", command=self.set_vmin)
573
- self.vmin_button.pack(side=tk.LEFT, padx=5)
574
- self.vmax_button = tk.Button(self.button_frame2, text=f"vmax: {vmax:.2f}", command=self.set_vmax)
575
- self.vmax_button.pack(side=tk.LEFT, padx=5)
576
- self.stop_button = tk.Button(self.button_frame2, text="Stop program", command=self.stop_app)
577
- self.stop_button.pack(side=tk.LEFT, padx=5)
578
-
579
- # Main frame for figure and toolbar
580
- self.main_frame = tk.Frame(self.root)
581
- self.main_frame.grid(row=2, column=0, sticky="nsew")
582
- self.root.grid_rowconfigure(2, weight=1)
583
- self.root.grid_columnconfigure(0, weight=1)
584
- self.main_frame.grid_rowconfigure(0, weight=1)
585
- self.main_frame.grid_columnconfigure(0, weight=1)
586
-
587
- # Create figure and axis
588
- self.fig, self.ax = plt.subplots(figsize=(8, 6))
589
- xlabel = 'X pixel (from 1 to NAXIS1)'
590
- ylabel = 'Y pixel (from 1 to NAXIS2)'
591
- extent = [0.5, self.data.shape[1] + 0.5, 0.5, self.data.shape[0] + 0.5]
592
- self.image, _, _ = imshow(self.fig, self.ax, self.data, vmin=vmin, vmax=vmax,
593
- xlabel=xlabel, ylabel=ylabel, extent=extent)
594
- # Note: tight_layout should be called before defining the canvas
595
- self.fig.tight_layout()
596
-
597
- # Create canvas and toolbar
598
- self.canvas = FigureCanvasTkAgg(self.fig, master=self.main_frame)
599
- # The next two instructions prevent a segmentation fault when pressing "q"
600
- self.canvas.mpl_disconnect(self.canvas.mpl_connect("key_press_event", key_press_handler))
601
- self.canvas.mpl_connect("key_press_event", self.on_key)
602
- self.canvas.mpl_connect("button_press_event", self.on_click)
603
- canvas_widget = self.canvas.get_tk_widget()
604
- canvas_widget.grid(row=0, column=0, sticky="nsew")
605
-
606
- # Matplotlib toolbar
607
- self.toolbar_frame = tk.Frame(self.main_frame)
608
- self.toolbar_frame.grid(row=1, column=0, sticky="ew")
609
- self.toolbar = NavigationToolbar2Tk(self.canvas, self.toolbar_frame)
610
- self.toolbar.update()
611
-
612
- def set_vmin(self):
613
- old_vmin = self.get_vmin()
614
- new_vmin = simpledialog.askfloat("Set vmin", "Enter new vmin:", initialvalue=old_vmin)
615
- if new_vmin is None:
616
- return
617
- self.vmin_button.config(text=f"vmin: {new_vmin:.2f}")
618
- self.image.set_clim(vmin=new_vmin)
619
- self.canvas.draw()
620
-
621
- def set_vmax(self):
622
- old_vmax = self.get_vmax()
623
- new_vmax = simpledialog.askfloat("Set vmax", "Enter new vmax:", initialvalue=old_vmax)
624
- if new_vmax is None:
625
- return
626
- self.vmax_button.config(text=f"vmax: {new_vmax:.2f}")
627
- self.image.set_clim(vmax=new_vmax)
628
- self.canvas.draw()
629
-
630
- def get_vmin(self):
631
- return float(self.vmin_button.cget("text").split(":")[1])
632
-
633
- def get_vmax(self):
634
- return float(self.vmax_button.cget("text").split(":")[1])
635
-
636
- def run_lacosmic(self):
637
- self.run_lacosmic_button.config(state=tk.DISABLED)
638
- self.stop_button.config(state=tk.DISABLED)
639
- # Parameters for L.A.Cosmic can be adjusted as needed
640
- _, mask_crfound = cosmicray_lacosmic(self.data, sigclip=4.5, sigfrac=0.3, objlim=5.0, verbose=True)
641
- ReviewCosmicRay(
642
- root=self.root,
643
- data=self.data,
644
- mask_fixed=self.mask_fixed,
645
- mask_crfound=mask_crfound
646
- )
647
- print("L.A.Cosmic cleaning applied.")
648
- self.run_lacosmic_button.config(state=tk.NORMAL)
649
- self.stop_button.config(state=tk.NORMAL)
650
-
651
- def stop_app(self):
652
- self.root.quit()
653
- self.root.destroy()
654
-
655
- def on_key(self, event):
656
- if event.key == 'q':
657
- pass # Ignore the "q" key to prevent closing the window
658
- else:
659
- print(f"Key pressed: {event.key}")
660
-
661
- def on_click(self, event):
662
- if event.inaxes:
663
- x, y = event.xdata, event.ydata
664
- print(f"Clicked at image coordinates: ({x:.2f}, {y:.2f})")
665
-
666
-
667
- def main():
668
- parser = argparse.ArgumentParser(description="Interactive cosmic ray cleaner for FITS images.")
669
- parser.add_argument("input_fits", help="Path to the FITS file to be cleaned.")
670
- parser.add_argument("--extension", type=int, default=0,
671
- help="FITS extension to use (default: 0).")
672
- parser.add_argument("--output_fits", type=str, default=None,
673
- help="Path to save the cleaned FITS file")
674
- args = parser.parse_args()
675
-
676
- if not os.path.isfile(args.input_fits):
677
- print(f"Error: File '{args.input_fits}' does not exist.")
678
- return
679
- if args.output_fits is not None and os.path.isfile(args.output_fits):
680
- print(f"Error: Output file '{args.output_fits}' already exists.")
681
- return
682
-
683
- # Initialize Tkinter root
684
- root = tk.Tk()
685
-
686
- # Create and run the application
687
- CosmicRayCleanerApp(root, args.input_fits, args.extension, args.output_fits)
688
-
689
- # Execute
690
- root.mainloop()
691
-
692
-
693
- if __name__ == "__main__":
694
- main()
File without changes
@@ -0,0 +1,34 @@
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
+ import os
10
+ import requests
11
+
12
+
13
+ def get_cookbook_file(file_path, verbose=True):
14
+ """
15
+ Retrieve the contents of an auxiliary file.
16
+
17
+ The file is downloaded from the teareduce-cookbook GitHub repository
18
+
19
+ Parameters
20
+ ----------
21
+ file_path : str
22
+ The path to the auxiliary file.
23
+ verbose : bool, optional
24
+ If True, print a message when the file is downloaded.
25
+ Default is True.
26
+ """
27
+
28
+ url = f"https://raw.githubusercontent.com/nicocardiel/teareduce-cookbook/main/{file_path}"
29
+ response = requests.get(url)
30
+ local_filename = os.path.basename(file_path)
31
+ with open(local_filename, "wb") as f:
32
+ f.write(response.content)
33
+ if verbose:
34
+ print(f"File {local_filename} downloaded!")
@@ -43,7 +43,7 @@ class SliceRegion1D:
43
43
  Check if slice 'other' is within the parent slice.
44
44
  """
45
45
 
46
- def __init__(self, region, mode=None):
46
+ def __init__(self, region, mode=None, naxis1=None):
47
47
  """Initialize SliceRegion1D.
48
48
 
49
49
  Parameters
@@ -54,6 +54,9 @@ class SliceRegion1D:
54
54
  mode : str
55
55
  Convention mode employed to define the slice.
56
56
  The two possible modes are 'fits' and 'python'.
57
+ naxis1 : int
58
+ The axis 1 size (length) of the data being sliced.
59
+ If provided, it is used to validate the slice region.
57
60
  """
58
61
  if isinstance(region, str):
59
62
  pattern = r'^\s*\[\s*\d+\s*:\s*\d+\s*\]\s*$'
@@ -82,17 +85,23 @@ class SliceRegion1D:
82
85
 
83
86
  if self.mode == 'fits':
84
87
  if region.stop < region.start:
85
- raise ValueError(f'Invalid {region!r}')
88
+ raise ValueError(f'Invalid {region!r}: stop must be >= start')
86
89
  self.fits = region
87
90
  self.python = slice(region.start-1, region.stop)
88
91
  elif self.mode == 'python':
89
92
  if region.stop <= region.start:
90
- raise ValueError(f'Invalid {region!r}')
93
+ raise ValueError(f'Invalid {region!r}: stop must be > start')
91
94
  self.fits = slice(region.start+1, region.stop)
92
95
  self.python = region
93
96
  else:
94
97
  raise ValueError(errmsg)
95
98
 
99
+ if naxis1 is not None:
100
+ if not (1 <= self.fits.start <= naxis1):
101
+ raise ValueError(f'Invalid start={self.fits.start} for naxis1={naxis1}')
102
+ if not (1 <= self.fits.stop <= naxis1+1):
103
+ raise ValueError(f'Invalid stop={self.fits.stop} for naxis1={naxis1}')
104
+
96
105
  s = self.fits
97
106
  self.fits_section = f'[{s.start}:{s.stop}]'
98
107
 
@@ -137,6 +146,10 @@ class SliceRegion1D:
137
146
  result = True
138
147
  return result
139
148
 
149
+ def length(self):
150
+ """Return the length of the slice."""
151
+ return self.python.stop - self.python.start
152
+
140
153
 
141
154
  class SliceRegion2D:
142
155
  """Store indices for slicing of 2D regions.
@@ -163,7 +176,7 @@ class SliceRegion2D:
163
176
  within(other)
164
177
  Check if slice 'other' is within the parent slice."""
165
178
 
166
- def __init__(self, region, mode=None):
179
+ def __init__(self, region, mode=None, naxis1=None, naxis2=None):
167
180
  """Initialize SliceRegion2D.
168
181
 
169
182
  Parameters
@@ -175,6 +188,14 @@ class SliceRegion2D:
175
188
  mode : str
176
189
  Convention mode employed to define the slice.
177
190
  The two possible modes are 'fits' and 'python'.
191
+ naxis1 : int
192
+ The axis 1 size (length) of the data being sliced,
193
+ assuming the FITS convention.
194
+ If provided, it is used to validate the slice region.
195
+ naxis2 : int
196
+ The axis 2 size (length) of the data being sliced,
197
+ assuming the FITS convention.
198
+ If provided, it is used to validate the slice region.
178
199
  """
179
200
  if isinstance(region, str):
180
201
  pattern = r'^\s*\[\s*\d+\s*:\s*\d+\s*,\s*\d+\s*:\s*\d+\s*\]\s*$'
@@ -206,16 +227,16 @@ class SliceRegion2D:
206
227
 
207
228
  if self.mode == 'fits':
208
229
  if s1.stop < s1.start:
209
- raise ValueError(f'Invalid {s1!r}')
230
+ raise ValueError(f'Invalid {s1!r}: stop must be >= start')
210
231
  if s2.stop < s2.start:
211
- raise ValueError(f'Invalid {s2!r}')
232
+ raise ValueError(f'Invalid {s2!r}: stop must be >= start')
212
233
  self.fits = region
213
234
  self.python = slice(s2.start-1, s2.stop), slice(s1.start-1, s1.stop)
214
235
  elif self.mode == 'python':
215
236
  if s1.stop <= s1.start:
216
- raise ValueError(f'Invalid {s1!r}')
237
+ raise ValueError(f'Invalid {s1!r}: stop must be > start')
217
238
  if s2.stop <= s2.start:
218
- raise ValueError(f'Invalid {s2!r}')
239
+ raise ValueError(f'Invalid {s2!r}: stop must be > start')
219
240
  self.fits = slice(s2.start+1, s2.stop), slice(s1.start+1, s1.stop)
220
241
  self.python = region
221
242
  else:
@@ -224,6 +245,17 @@ class SliceRegion2D:
224
245
  s1, s2 = self.fits
225
246
  self.fits_section = f'[{s1.start}:{s1.stop},{s2.start}:{s2.stop}]'
226
247
 
248
+ if naxis1 is not None:
249
+ if not (1 <= s1.start <= naxis1):
250
+ raise ValueError(f'Invalid start={s1.start} for naxis1={naxis1}')
251
+ if not (1 <= s1.stop <= naxis1):
252
+ raise ValueError(f'Invalid stop={s1.stop} for naxis1={naxis1}')
253
+ if naxis2 is not None:
254
+ if not (1 <= s2.start <= naxis2):
255
+ raise ValueError(f'Invalid start={s2.start} for naxis2={naxis2}')
256
+ if not (1 <= s2.stop <= naxis2):
257
+ raise ValueError(f'Invalid stop={s2.stop} for naxis2={naxis2}')
258
+
227
259
  def __eq__(self, other):
228
260
  return self.fits == other.fits and self.python == other.python
229
261
 
@@ -269,6 +301,11 @@ class SliceRegion2D:
269
301
  result = True
270
302
  return result
271
303
 
304
+ def area(self):
305
+ """Return the area of the slice."""
306
+ s1, s2 = self.python
307
+ return (s1.stop - s1.start) * (s2.stop - s2.start)
308
+
272
309
 
273
310
  class SliceRegion3D:
274
311
  """Store indices for slicing of 3D regions.
@@ -295,7 +332,7 @@ class SliceRegion3D:
295
332
  within(other)
296
333
  Check if slice 'other' is within the parent slice."""
297
334
 
298
- def __init__(self, region, mode=None):
335
+ def __init__(self, region, mode=None, naxis1=None, naxis2=None, naxis3=None):
299
336
  """Initialize SliceRegion3D.
300
337
 
301
338
  Parameters
@@ -307,6 +344,18 @@ class SliceRegion3D:
307
344
  mode : str
308
345
  Convention mode employed to define the slice.
309
346
  The two possible modes are 'fits' and 'python'.
347
+ naxis1 : int
348
+ The axis 1 size (length) of the data being sliced,
349
+ assuming the FITS convention.
350
+ If provided, it is used to validate the slice region.
351
+ naxis2 : int
352
+ The axis 2 size (length) of the data being sliced,
353
+ assuming the FITS convention.
354
+ If provided, it is used to validate the slice region.
355
+ naxis3 : int
356
+ The axis 3 size (length) of the data being sliced,
357
+ assuming the FITS convention.
358
+ If provided, it is used to validate the slice region.
310
359
  """
311
360
  if isinstance(region, str):
312
361
  pattern = r'^\s*\[\s*\d+\s*:\s*\d+\s*,\s*\d+\s*:\s*\d+\s*,\s*\d+\s*:\s*\d+\s*\]\s*$'
@@ -338,20 +387,20 @@ class SliceRegion3D:
338
387
 
339
388
  if self.mode == 'fits':
340
389
  if s1.stop < s1.start:
341
- raise ValueError(f'Invalid {s1!r}')
390
+ raise ValueError(f'Invalid {s1!r}: stop must be >= start')
342
391
  if s2.stop < s2.start:
343
- raise ValueError(f'Invalid {s2!r}')
392
+ raise ValueError(f'Invalid {s2!r}: stop must be >= start')
344
393
  if s3.stop < s3.start:
345
- raise ValueError(f'Invalid {s3!r}')
394
+ raise ValueError(f'Invalid {s3!r}: stop must be >= start')
346
395
  self.fits = region
347
396
  self.python = slice(s3.start-1, s3.stop), slice(s2.start-1, s2.stop), slice(s1.start-1, s1.stop)
348
397
  elif self.mode == 'python':
349
398
  if s1.stop <= s1.start:
350
- raise ValueError(f'Invalid {s1!r}')
399
+ raise ValueError(f'Invalid {s1!r}: stop must be > start')
351
400
  if s2.stop <= s2.start:
352
- raise ValueError(f'Invalid {s2!r}')
401
+ raise ValueError(f'Invalid {s2!r}: stop must be > start')
353
402
  if s3.stop <= s3.start:
354
- raise ValueError(f'Invalid {s3!r}')
403
+ raise ValueError(f'Invalid {s3!r}: stop must be > start')
355
404
  self.fits = slice(s3.start+1, s3.stop), slice(s2.start+1, s2.stop), slice(s1.start+1, s1.stop)
356
405
  self.python = region
357
406
  else:
@@ -360,6 +409,22 @@ class SliceRegion3D:
360
409
  s1, s2, s3 = self.fits
361
410
  self.fits_section = f'[{s1.start}:{s1.stop},{s2.start}:{s2.stop},{s3.start}:{s3.stop}]'
362
411
 
412
+ if naxis1 is not None:
413
+ if not (1 <= s1.start <= naxis1):
414
+ raise ValueError(f'Invalid start={s1.start} for naxis1={naxis1}')
415
+ if not (1 <= s1.stop <= naxis1):
416
+ raise ValueError(f'Invalid stop={s1.stop} for naxis1={naxis1}')
417
+ if naxis2 is not None:
418
+ if not (1 <= s2.start <= naxis2):
419
+ raise ValueError(f'Invalid start={s2.start} for naxis2={naxis2}')
420
+ if not (1 <= s2.stop <= naxis2):
421
+ raise ValueError(f'Invalid stop={s2.stop} for naxis2={naxis2}')
422
+ if naxis3 is not None:
423
+ if not (1 <= s3.start <= naxis3):
424
+ raise ValueError(f'Invalid start={s3.start} for naxis3={naxis3}')
425
+ if not (1 <= s3.stop <= naxis3):
426
+ raise ValueError(f'Invalid stop={s3.stop} for naxis3={naxis3}')
427
+
363
428
  def __eq__(self, other):
364
429
  return self.fits == other.fits and self.python == other.python
365
430
 
@@ -408,3 +473,8 @@ class SliceRegion3D:
408
473
  return result
409
474
  result = True
410
475
  return result
476
+
477
+ def volume(self):
478
+ """Return the volume of the slice."""
479
+ s1, s2, s3 = self.python
480
+ return (s1.stop - s1.start) * (s2.stop - s2.start) * (s3.stop - s3.start)
File without changes
@@ -0,0 +1,116 @@
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 SliceRegion class."""
11
+
12
+ import numpy as np
13
+
14
+ from ..sliceregion import SliceRegion1D, SliceRegion2D, SliceRegion3D
15
+
16
+
17
+ def test_slice_region_creation():
18
+ """Test the creation of a SliceRegion."""
19
+
20
+ region1d = SliceRegion1D(np.s_[1:10], mode='python')
21
+ assert region1d.fits == slice(2, 10, None)
22
+ assert region1d.python == slice(1, 10, None)
23
+ assert region1d.fits_section == '[2:10]'
24
+
25
+ region2d = SliceRegion2D(np.s_[1:10, 2:20], mode='python')
26
+ assert region2d.fits == (slice(3, 20, None), slice(2, 10, None))
27
+ assert region2d.python == (slice(1, 10, None), slice(2, 20, None))
28
+ assert region2d.fits_section == '[3:20,2:10]'
29
+
30
+ region3d = SliceRegion3D(np.s_[1:10, 2:20, 3:30], mode='python')
31
+ assert region3d.fits == (slice(4, 30, None), slice(3, 20, None), slice(2, 10, None))
32
+ assert region3d.python == (slice(1, 10, None), slice(2, 20, None), slice(3, 30, None))
33
+ assert region3d.fits_section == '[4:30,3:20,2:10]'
34
+
35
+
36
+ def test_slice_values():
37
+ """Test the values of the slices in different modes."""
38
+
39
+ array1d = np.arange(10)
40
+
41
+ region1d = SliceRegion1D(np.s_[1:3], mode='python')
42
+ assert np.all(array1d[region1d.python] == np.array([1, 2]))
43
+
44
+ array2d = np.arange(12).reshape(3, 4)
45
+ region2d = SliceRegion2D(np.s_[1:3, 2:3], mode='python')
46
+ assert np.all(array2d[region2d.python] == np.array([[6], [10]]))
47
+
48
+ array3d = np.arange(24).reshape(3, 4, 2)
49
+ region3d = SliceRegion3D(np.s_[1:3, 2:4, 1:2], mode='python')
50
+ assert np.all(array3d[region3d.python] == np.array([[[13], [15]], [[21], [23]]]))
51
+
52
+
53
+ def test_wrong_number_of_dimensions():
54
+ """Test the creation of a SliceRegion with wrong slices."""
55
+
56
+ try:
57
+ SliceRegion1D(np.s_[1:3, 2:4], mode='python')
58
+ assert False, "Expected ValueError for 1D slice with 2D input"
59
+ except ValueError:
60
+ pass
61
+
62
+ try:
63
+ SliceRegion2D(np.s_[1:3], mode='python')
64
+ assert False, "Expected ValueError for 2D slice with 1D input"
65
+ except ValueError:
66
+ pass
67
+
68
+ try:
69
+ SliceRegion3D(np.s_[1:3, 2:4], mode='python')
70
+ assert False, "Expected ValueError for 3D slice with 2D input"
71
+ except ValueError:
72
+ pass
73
+
74
+
75
+ def test_wrong_limits_order():
76
+ """Test the creation of a SliceRegion with wrong limits."""
77
+
78
+ try:
79
+ SliceRegion1D(np.s_[10:5], mode='python')
80
+ assert False, "Expected ValueError for 1D slice with start > stop"
81
+ except ValueError:
82
+ pass
83
+
84
+ try:
85
+ SliceRegion2D(np.s_[1:3, 5:2], mode='python')
86
+ assert False, "Expected ValueError for 2D slice with start > stop in second dimension"
87
+ except ValueError:
88
+ pass
89
+
90
+ try:
91
+ SliceRegion3D(np.s_[1:3, 2:4, 6:1], mode='python')
92
+ assert False, "Expected ValueError for 3D slice with start > stop in third dimension"
93
+ except ValueError:
94
+ pass
95
+
96
+
97
+ def test_limits_out_of_range():
98
+ """Test the creation of a SliceRegion with limits out of range."""
99
+
100
+ try:
101
+ SliceRegion1D(np.s_[-1:5], mode='python', naxis1=10)
102
+ assert False, "Expected ValueError for 1D slice with negative start"
103
+ except ValueError:
104
+ pass
105
+
106
+ try:
107
+ SliceRegion2D(np.s_[1:3, 2:25], mode='python', naxis1=10, naxis2=20)
108
+ assert False, "Expected ValueError for 2D slice with stop > naxis in second dimension"
109
+ except ValueError:
110
+ pass
111
+
112
+ try:
113
+ SliceRegion3D(np.s_[1:3, 2:4, 3:35], mode='python', naxis1=10, naxis2=20, naxis3=30)
114
+ assert False, "Expected ValueError for 3D slice with stop > naxis in third dimension"
115
+ except ValueError:
116
+ pass
@@ -9,7 +9,7 @@
9
9
  #
10
10
  """Module to define the version of the teareduce package."""
11
11
 
12
- VERSION = '0.4.6'
12
+ VERSION = '0.4.8'
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.4.6
3
+ Version: 0.4.8
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
@@ -3,7 +3,6 @@ README.md
3
3
  pyproject.toml
4
4
  src/teareduce/__init__.py
5
5
  src/teareduce/avoid_astropy_warnings.py
6
- src/teareduce/cleanest.py
7
6
  src/teareduce/correct_pincushion_distortion.py
8
7
  src/teareduce/cosmicrays.py
9
8
  src/teareduce/ctext.py
@@ -29,6 +28,12 @@ src/teareduce.egg-info/dependency_links.txt
29
28
  src/teareduce.egg-info/entry_points.txt
30
29
  src/teareduce.egg-info/requires.txt
31
30
  src/teareduce.egg-info/top_level.txt
31
+ src/teareduce/cleanest/__init__.py
32
+ src/teareduce/cleanest/__main__.py
33
+ src/teareduce/cleanest/cosmicraycleanerapp.py
34
+ src/teareduce/cleanest/reviewcosmicray.py
35
+ src/teareduce/cookbook/__init__.py
36
+ src/teareduce/cookbook/get_cookbook_file.py
32
37
  src/teareduce/tests/__init__.py
33
38
  src/teareduce/tests/test_sliceregion.py
34
39
  src/teareduce/tests/test_version.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tea-cleanest = teareduce.cleanest.__main__:main
@@ -1,49 +0,0 @@
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 SliceRegion class."""
11
-
12
- import numpy as np
13
-
14
- from ..sliceregion import SliceRegion1D, SliceRegion2D, SliceRegion3D
15
-
16
-
17
- def test_slice_region_creation():
18
- """Test the creation of a SliceRegion."""
19
-
20
- region1d = SliceRegion1D(np.s_[1:10], mode='python')
21
- assert region1d.fits == slice(2, 10, None)
22
- assert region1d.python == slice(1, 10, None)
23
- assert region1d.fits_section == '[2:10]'
24
-
25
- region2d = SliceRegion2D(np.s_[1:10, 2:20], mode='python')
26
- assert region2d.fits == (slice(3, 20, None), slice(2, 10, None))
27
- assert region2d.python == (slice(1, 10, None), slice(2, 20, None))
28
- assert region2d.fits_section == '[3:20,2:10]'
29
-
30
- region3d = SliceRegion3D(np.s_[1:10, 2:20, 3:30], mode='python')
31
- assert region3d.fits == (slice(4, 30, None), slice(3, 20, None), slice(2, 10, None))
32
- assert region3d.python == (slice(1, 10, None), slice(2, 20, None), slice(3, 30, None))
33
- assert region3d.fits_section == '[4:30,3:20,2:10]'
34
-
35
- def test_slice_values():
36
- """Test the values of the slices in different modes."""
37
-
38
- array1d = np.arange(10)
39
-
40
- region1d = SliceRegion1D(np.s_[1:3], mode='python')
41
- assert np.all(array1d[region1d.python] == np.array([1, 2]))
42
-
43
- array2d = np.arange(12).reshape(3, 4)
44
- region2d = SliceRegion2D(np.s_[1:3, 2:3], mode='python')
45
- assert np.all(array2d[region2d.python] == np.array([[6], [10]]))
46
-
47
- array3d = np.arange(24).reshape(3, 4, 2)
48
- region3d = SliceRegion3D(np.s_[1:3, 2:4, 1:2], mode='python')
49
- assert np.all(array3d[region3d.python] == np.array([[[13], [15]], [[21], [23]]]))
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- tea-cleanest = teareduce.cleanest:main
File without changes
File without changes
File without changes