lightstack 0.1.6__tar.gz → 0.2.0__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.
- {lightstack-0.1.6/src/lightstack.egg-info → lightstack-0.2.0}/PKG-INFO +3 -5
- {lightstack-0.1.6 → lightstack-0.2.0}/README.md +2 -4
- {lightstack-0.1.6 → lightstack-0.2.0}/pyproject.toml +1 -1
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack/datacube.py +220 -98
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack/plot.py +73 -27
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack/psf.py +1 -1
- lightstack-0.2.0/src/lightstack/utils.py +407 -0
- {lightstack-0.1.6 → lightstack-0.2.0/src/lightstack.egg-info}/PKG-INFO +3 -5
- lightstack-0.1.6/src/lightstack/utils.py +0 -231
- {lightstack-0.1.6 → lightstack-0.2.0}/LICENSE +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/setup.cfg +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack/__init__.py +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack/crop.py +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack.egg-info/SOURCES.txt +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack.egg-info/dependency_links.txt +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack.egg-info/requires.txt +0 -0
- {lightstack-0.1.6 → lightstack-0.2.0}/src/lightstack.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lightstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Tools for building and processing multi-filter astrophysical datacubes
|
|
5
5
|
Author: Andressa Wille, Thallis Pessi
|
|
6
6
|
License: MIT License
|
|
@@ -41,7 +41,7 @@ Requires-Dist: photutils
|
|
|
41
41
|
|
|
42
42
|
# Lightstack
|
|
43
43
|
|
|
44
|
-
Tools for building and processing multi-filter astrophysical datacubes.
|
|
44
|
+
Tools for building and processing multi-filter astrophysical datacubes.
|
|
45
45
|
|
|
46
46
|
## Features
|
|
47
47
|
|
|
@@ -49,8 +49,6 @@ Tools for building and processing multi-filter astrophysical datacubes.
|
|
|
49
49
|
- Building photometric datacubes
|
|
50
50
|
- PSF matching
|
|
51
51
|
|
|
52
|
-
(Currently supports JWST and HST)
|
|
53
|
-
|
|
54
52
|
## Installation
|
|
55
53
|
|
|
56
54
|
```bash
|
|
@@ -61,4 +59,4 @@ pip install lightstack
|
|
|
61
59
|
|
|
62
60
|
https://lightstack.readthedocs.io/en/latest/
|
|
63
61
|
|
|
64
|
-
See the tutorial notebook!
|
|
62
|
+
See the tutorial notebook (in the "examples" folder)!
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# Lightstack
|
|
6
6
|
|
|
7
|
-
Tools for building and processing multi-filter astrophysical datacubes.
|
|
7
|
+
Tools for building and processing multi-filter astrophysical datacubes.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -12,8 +12,6 @@ Tools for building and processing multi-filter astrophysical datacubes.
|
|
|
12
12
|
- Building photometric datacubes
|
|
13
13
|
- PSF matching
|
|
14
14
|
|
|
15
|
-
(Currently supports JWST and HST)
|
|
16
|
-
|
|
17
15
|
## Installation
|
|
18
16
|
|
|
19
17
|
```bash
|
|
@@ -24,4 +22,4 @@ pip install lightstack
|
|
|
24
22
|
|
|
25
23
|
https://lightstack.readthedocs.io/en/latest/
|
|
26
24
|
|
|
27
|
-
See the tutorial notebook!
|
|
25
|
+
See the tutorial notebook (in the "examples" folder)!
|
|
@@ -1,39 +1,56 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import os
|
|
3
|
+
import glob
|
|
3
4
|
|
|
4
5
|
from astropy.io import fits
|
|
5
6
|
from astropy.wcs import WCS
|
|
6
7
|
|
|
7
8
|
from reproject import reproject_interp, reproject_exact
|
|
8
9
|
|
|
9
|
-
from .utils import find_ext,
|
|
10
|
+
from .utils import find_ext, get_filter, get_pixel_scale_from_wcs
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_image_fits(folder_path):
|
|
14
|
+
|
|
15
|
+
fits_files = []
|
|
16
|
+
|
|
17
|
+
for f in glob.glob(os.path.join(folder_path, "*.fits")):
|
|
18
|
+
try:
|
|
19
|
+
if fits.getval(f, "NAXIS") != 2:
|
|
20
|
+
continue
|
|
21
|
+
fits_files.append(f)
|
|
22
|
+
except Exception:
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
return fits_files
|
|
10
26
|
|
|
11
27
|
|
|
12
28
|
def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
13
29
|
"""
|
|
14
|
-
|
|
30
|
+
Align and optionally reproject FITS images to a common WCS.
|
|
31
|
+
If an image already matches the reference WCS, shape and pixel scale, reprojection is skipped automatically.
|
|
32
|
+
Area correction is only applied when input and output pixel scales differ.
|
|
15
33
|
|
|
16
34
|
Parameters
|
|
17
35
|
----------
|
|
18
36
|
fits_list : list of tuples
|
|
19
|
-
|
|
37
|
+
[(fits_path, filter_name), ...]
|
|
20
38
|
|
|
21
39
|
ref_file : str
|
|
22
|
-
|
|
40
|
+
Reference FITS file.
|
|
23
41
|
|
|
24
42
|
method : str, optional
|
|
25
43
|
Reprojection method:
|
|
26
|
-
- "interp" (default)
|
|
27
|
-
- "exact"
|
|
28
|
-
with resolutions below ~0.05 arcsec.
|
|
44
|
+
- "interp" (default)
|
|
45
|
+
- "exact"
|
|
29
46
|
|
|
30
47
|
crop : int, optional
|
|
31
|
-
|
|
48
|
+
Pixels to crop from borders after reprojection.
|
|
32
49
|
|
|
33
50
|
Returns
|
|
34
51
|
-------
|
|
35
52
|
aligned_list : list of tuples
|
|
36
|
-
|
|
53
|
+
[(aligned_fits_path, filter_name), ...]
|
|
37
54
|
"""
|
|
38
55
|
|
|
39
56
|
# Choose reprojection method
|
|
@@ -56,12 +73,15 @@ def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
|
56
73
|
ref_wcs = WCS(ref_header)
|
|
57
74
|
shape_out = hdul_ref[ext_ref].data.shape
|
|
58
75
|
|
|
59
|
-
# Pixel scale of reference
|
|
60
76
|
scale_out = get_pixel_scale_from_wcs(ref_wcs)
|
|
61
|
-
|
|
62
77
|
aligned_list = []
|
|
63
78
|
|
|
64
|
-
#
|
|
79
|
+
# Reference filter name
|
|
80
|
+
ref_filter_name = next(
|
|
81
|
+
filt for f, filt in fits_list if f == ref_file
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Loop through files
|
|
65
85
|
for fpath, filt in fits_list:
|
|
66
86
|
|
|
67
87
|
with fits.open(fpath) as hdul:
|
|
@@ -74,50 +94,67 @@ def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
|
74
94
|
header = hdul[ext].header
|
|
75
95
|
wcs_in = WCS(header)
|
|
76
96
|
|
|
77
|
-
unit = header.get(
|
|
78
|
-
print(f"
|
|
97
|
+
unit = header.get("BUNIT", "unknown")
|
|
98
|
+
print(f"\nFilter {filt}: unit = {unit}")
|
|
79
99
|
|
|
80
|
-
# Pixel scale of input
|
|
81
100
|
scale_in = get_pixel_scale_from_wcs(wcs_in)
|
|
101
|
+
print(f"Pixel scale in/out: {scale_in:.4f} -> {scale_out:.4f}")
|
|
82
102
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
103
|
+
# Check whether reprojection is really needed
|
|
104
|
+
same_scale = np.isclose(scale_in, scale_out, rtol=1e-6)
|
|
105
|
+
same_shape = data.shape == shape_out
|
|
106
|
+
same_wcs = (
|
|
107
|
+
wcs_in.to_header_string()
|
|
108
|
+
== ref_wcs.to_header_string()
|
|
88
109
|
)
|
|
89
110
|
|
|
90
|
-
|
|
111
|
+
if same_scale and same_shape and same_wcs:
|
|
112
|
+
print("Already aligned with reference: skipping reprojection.")
|
|
91
113
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
data_aligned *= area_ratio
|
|
96
|
-
print(f"Applied area correction: {area_ratio:.4f}")
|
|
97
|
-
area_corrected = True
|
|
98
|
-
else:
|
|
99
|
-
print("Pixel scales identical: no area correction applied.")
|
|
114
|
+
data_aligned = data.copy()
|
|
115
|
+
wcs_out = wcs_in.deepcopy()
|
|
116
|
+
reprojected = False
|
|
100
117
|
area_corrected = False
|
|
101
118
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
else:
|
|
120
|
+
data_aligned, footprint = reproj_func(
|
|
121
|
+
(data, wcs_in),
|
|
122
|
+
ref_wcs,
|
|
123
|
+
shape_out=shape_out
|
|
124
|
+
)
|
|
105
125
|
|
|
106
|
-
# Adjust WCS
|
|
107
126
|
wcs_out = ref_wcs.deepcopy()
|
|
127
|
+
reprojected = True
|
|
128
|
+
|
|
129
|
+
# Area correction only if pixel scales differ
|
|
130
|
+
if not same_scale:
|
|
131
|
+
area_ratio = (scale_out / scale_in) ** 2
|
|
132
|
+
data_aligned *= area_ratio
|
|
133
|
+
print(f"Applied area correction: {area_ratio:.4f}")
|
|
134
|
+
area_corrected = True
|
|
135
|
+
else:
|
|
136
|
+
print("Pixel scales identical: no area correction applied.")
|
|
137
|
+
area_corrected = False
|
|
138
|
+
|
|
139
|
+
# Crop borders
|
|
140
|
+
if crop > 0:
|
|
141
|
+
data_aligned = data_aligned[crop:-crop, crop:-crop]
|
|
108
142
|
wcs_out.wcs.crpix -= crop
|
|
109
|
-
else:
|
|
110
|
-
wcs_out = ref_wcs
|
|
111
143
|
|
|
112
|
-
#
|
|
144
|
+
# Build output header
|
|
113
145
|
header_aligned = wcs_out.to_header()
|
|
114
|
-
header_aligned[
|
|
146
|
+
header_aligned["BUNIT"] = unit
|
|
115
147
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
if reprojected:
|
|
149
|
+
header_aligned.add_history(
|
|
150
|
+
f"Reprojected to {ref_filter_name} using "
|
|
151
|
+
f"{method} method (reproject package)"
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
header_aligned.add_history(
|
|
155
|
+
f"No reprojection needed "
|
|
156
|
+
f"(already aligned to {ref_filter_name})"
|
|
157
|
+
)
|
|
121
158
|
|
|
122
159
|
if area_corrected:
|
|
123
160
|
header_aligned.add_history(
|
|
@@ -125,8 +162,7 @@ def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
|
125
162
|
)
|
|
126
163
|
else:
|
|
127
164
|
header_aligned.add_history(
|
|
128
|
-
"No area correction applied
|
|
129
|
-
"(identical pixel scale)"
|
|
165
|
+
"No area correction applied"
|
|
130
166
|
)
|
|
131
167
|
|
|
132
168
|
if crop > 0:
|
|
@@ -134,7 +170,7 @@ def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
|
134
170
|
f"Cropped {crop} pixels from each border"
|
|
135
171
|
)
|
|
136
172
|
|
|
137
|
-
# Save
|
|
173
|
+
# Save output
|
|
138
174
|
suffix = f"_aligned_{method}"
|
|
139
175
|
out_name = os.path.splitext(fpath)[0] + f"{suffix}.fits"
|
|
140
176
|
|
|
@@ -143,41 +179,54 @@ def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
|
143
179
|
header=header_aligned
|
|
144
180
|
).writeto(out_name, overwrite=True)
|
|
145
181
|
|
|
146
|
-
# Flux sanity check
|
|
182
|
+
# Flux sanity check (only if reprojection happened and input not empty)
|
|
147
183
|
sum_in = np.nansum(data)
|
|
148
184
|
sum_out = np.nansum(data_aligned)
|
|
149
|
-
|
|
185
|
+
|
|
186
|
+
if reprojected and sum_in != 0:
|
|
187
|
+
print(f"Flux ratio (out/in): {sum_out / sum_in:.4f}")
|
|
188
|
+
elif sum_in == 0:
|
|
189
|
+
print("Input image is empty: skipping flux ratio check.")
|
|
150
190
|
|
|
151
191
|
aligned_list.append((out_name, filt))
|
|
152
192
|
print(f"Saved: {out_name}")
|
|
153
193
|
|
|
154
194
|
return aligned_list
|
|
155
|
-
|
|
195
|
+
|
|
156
196
|
|
|
157
197
|
def build_datacube(aligned_fits_files, reference_file, output_path):
|
|
158
198
|
"""
|
|
159
|
-
|
|
199
|
+
Build a 3D datacube from aligned 2D FITS images.
|
|
160
200
|
|
|
161
201
|
Parameters
|
|
162
202
|
----------
|
|
163
203
|
aligned_fits_files : list of tuples
|
|
164
|
-
[(filename, filter_name), ...]
|
|
204
|
+
List in format [(filename, filter_name), ...].
|
|
205
|
+
|
|
165
206
|
reference_file : str
|
|
166
|
-
FITS file to define WCS and shape.
|
|
207
|
+
FITS file used to define WCS and output shape.
|
|
208
|
+
|
|
167
209
|
output_path : str
|
|
168
|
-
Path to save the
|
|
169
|
-
|
|
210
|
+
Path to save the output datacube.
|
|
211
|
+
|
|
170
212
|
Returns
|
|
171
213
|
-------
|
|
172
214
|
None
|
|
173
|
-
Saves the
|
|
215
|
+
Saves the datacube to disk.
|
|
174
216
|
"""
|
|
175
|
-
|
|
176
|
-
#
|
|
217
|
+
|
|
218
|
+
# Safety check
|
|
219
|
+
if len(aligned_fits_files) == 0:
|
|
220
|
+
raise ValueError("No aligned FITS files provided.")
|
|
221
|
+
|
|
222
|
+
# Reference WCS
|
|
177
223
|
with fits.open(reference_file) as hdul_ref:
|
|
178
224
|
ext_ref = find_ext(hdul_ref)
|
|
179
225
|
if ext_ref is None:
|
|
180
|
-
raise ValueError(
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"No valid 2D data in reference file {reference_file}."
|
|
228
|
+
)
|
|
229
|
+
|
|
181
230
|
ref_header = hdul_ref[ext_ref].header
|
|
182
231
|
ny, nx = hdul_ref[ext_ref].data.shape
|
|
183
232
|
wcs_2d = WCS(ref_header, naxis=2)
|
|
@@ -186,51 +235,71 @@ def build_datacube(aligned_fits_files, reference_file, output_path):
|
|
|
186
235
|
filter_names = []
|
|
187
236
|
units = []
|
|
188
237
|
|
|
189
|
-
#
|
|
238
|
+
# Read aligned images
|
|
190
239
|
for file, filt in aligned_fits_files:
|
|
240
|
+
|
|
191
241
|
with fits.open(file) as hdul:
|
|
192
242
|
ext = find_ext(hdul)
|
|
243
|
+
|
|
193
244
|
if ext is None:
|
|
194
|
-
print(f"No 2D data in {file}. Skipping.")
|
|
245
|
+
print(f"No valid 2D data in {file}. Skipping.")
|
|
195
246
|
continue
|
|
247
|
+
|
|
196
248
|
data = hdul[ext].data
|
|
249
|
+
|
|
197
250
|
cube_images.append(data)
|
|
198
251
|
filter_names.append(filt)
|
|
199
|
-
units.append(
|
|
252
|
+
units.append(
|
|
253
|
+
hdul[ext].header.get("BUNIT", "unknown")
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Safety check after filtering invalid files
|
|
257
|
+
if len(cube_images) == 0:
|
|
258
|
+
raise ValueError("No valid 2D FITS images found.")
|
|
200
259
|
|
|
201
260
|
cube = np.array(cube_images)
|
|
202
261
|
|
|
203
|
-
#
|
|
262
|
+
# Build 3D WCS
|
|
204
263
|
wcs_3d = WCS(naxis=3)
|
|
264
|
+
|
|
265
|
+
# Spatial axes
|
|
205
266
|
wcs_3d.wcs.crpix[0] = wcs_2d.wcs.crpix[0]
|
|
206
267
|
wcs_3d.wcs.crpix[1] = wcs_2d.wcs.crpix[1]
|
|
268
|
+
|
|
207
269
|
wcs_3d.wcs.crval[0] = wcs_2d.wcs.crval[0]
|
|
208
270
|
wcs_3d.wcs.crval[1] = wcs_2d.wcs.crval[1]
|
|
271
|
+
|
|
209
272
|
wcs_3d.wcs.cdelt[0] = wcs_2d.wcs.cdelt[0]
|
|
210
273
|
wcs_3d.wcs.cdelt[1] = wcs_2d.wcs.cdelt[1]
|
|
274
|
+
|
|
211
275
|
wcs_3d.wcs.ctype[0] = wcs_2d.wcs.ctype[0]
|
|
212
276
|
wcs_3d.wcs.ctype[1] = wcs_2d.wcs.ctype[1]
|
|
277
|
+
|
|
213
278
|
wcs_3d.wcs.cunit[0] = wcs_2d.wcs.cunit[0]
|
|
214
279
|
wcs_3d.wcs.cunit[1] = wcs_2d.wcs.cunit[1]
|
|
215
280
|
|
|
281
|
+
# Preserve CD matrix if present
|
|
216
282
|
if wcs_2d.wcs.has_cd():
|
|
217
|
-
cd3 = np.zeros((3,3))
|
|
218
|
-
cd3[0:2,0:2] = wcs_2d.wcs.cd.copy()
|
|
219
|
-
cd3[2,2] = 1.0
|
|
283
|
+
cd3 = np.zeros((3, 3))
|
|
284
|
+
cd3[0:2, 0:2] = wcs_2d.wcs.cd.copy()
|
|
285
|
+
cd3[2, 2] = 1.0
|
|
220
286
|
wcs_3d.wcs.cd = cd3
|
|
221
287
|
|
|
288
|
+
# Filter axis
|
|
222
289
|
wcs_3d.wcs.crpix[2] = 1
|
|
223
290
|
wcs_3d.wcs.crval[2] = 0
|
|
224
291
|
wcs_3d.wcs.cdelt[2] = 1
|
|
225
|
-
wcs_3d.wcs.ctype[2] =
|
|
226
|
-
wcs_3d.wcs.cunit[2] =
|
|
292
|
+
wcs_3d.wcs.ctype[2] = "FILTER"
|
|
293
|
+
wcs_3d.wcs.cunit[2] = ""
|
|
227
294
|
|
|
228
295
|
header = wcs_3d.to_header()
|
|
229
|
-
|
|
230
|
-
header[
|
|
231
|
-
header[
|
|
232
|
-
header[
|
|
233
|
-
|
|
296
|
+
|
|
297
|
+
header["NAXIS"] = 3
|
|
298
|
+
header["NAXIS1"] = nx
|
|
299
|
+
header["NAXIS2"] = ny
|
|
300
|
+
header["NAXIS3"] = len(filter_names)
|
|
301
|
+
|
|
302
|
+
# Copy HISTORY from first valid file
|
|
234
303
|
with fits.open(aligned_fits_files[0][0]) as hdul0:
|
|
235
304
|
ext0 = find_ext(hdul0)
|
|
236
305
|
header_meta = hdul0[ext0].header
|
|
@@ -239,20 +308,26 @@ def build_datacube(aligned_fits_files, reference_file, output_path):
|
|
|
239
308
|
for h in header_meta["HISTORY"]:
|
|
240
309
|
header.add_history(h)
|
|
241
310
|
|
|
311
|
+
# Units
|
|
242
312
|
if all(u == units[0] for u in units):
|
|
243
|
-
header[
|
|
313
|
+
header["BUNIT"] = units[0]
|
|
244
314
|
else:
|
|
245
|
-
header[
|
|
315
|
+
header["BUNIT"] = "unknown"
|
|
246
316
|
|
|
317
|
+
# Filter metadata
|
|
247
318
|
for i, filt in enumerate(filter_names):
|
|
248
|
-
header[f
|
|
249
|
-
|
|
250
|
-
header['NFILTERS'] = len(filter_names)
|
|
251
|
-
header['FILTERS'] = ",".join(filter_names)
|
|
319
|
+
header[f"FILTER{i+1}"] = filt
|
|
252
320
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
321
|
+
header["NFILTERS"] = len(filter_names)
|
|
322
|
+
header["FILTERS"] = ",".join(filter_names)
|
|
323
|
+
|
|
324
|
+
# Save cube
|
|
325
|
+
fits.PrimaryHDU(
|
|
326
|
+
cube,
|
|
327
|
+
header=header
|
|
328
|
+
).writeto(output_path, overwrite=True)
|
|
329
|
+
|
|
330
|
+
print(f"Datacube saved at {output_path}")
|
|
256
331
|
|
|
257
332
|
|
|
258
333
|
def cut_region_datacube(cube_fits_file, x_start, x_end, y_start, y_end, output_path):
|
|
@@ -267,7 +342,7 @@ def cut_region_datacube(cube_fits_file, x_start, x_end, y_start, y_end, output_p
|
|
|
267
342
|
Pixel indices for the x axis.
|
|
268
343
|
y_start, y_end : int
|
|
269
344
|
Pixel indices for the y axis.
|
|
270
|
-
|
|
345
|
+
output_path : str
|
|
271
346
|
Path to the output fits file.
|
|
272
347
|
|
|
273
348
|
Returns
|
|
@@ -354,33 +429,62 @@ def build_valid_datacube(cube_fits_file, output_cube, threshold=0.0, frac_valid=
|
|
|
354
429
|
for i, img in enumerate(cube):
|
|
355
430
|
valid = np.isfinite(img)
|
|
356
431
|
n_total = valid.sum()
|
|
432
|
+
|
|
357
433
|
if n_total == 0:
|
|
358
434
|
continue
|
|
435
|
+
|
|
359
436
|
n_above = np.sum(img[valid] > threshold)
|
|
437
|
+
|
|
360
438
|
if (n_above / n_total) >= frac_valid:
|
|
361
439
|
valid_indices.append(i)
|
|
362
|
-
|
|
440
|
+
|
|
441
|
+
# Stop if all filters are invalid
|
|
442
|
+
if len(valid_indices) == 0:
|
|
443
|
+
raise ValueError(
|
|
444
|
+
f"No valid filters found in {cube_fits_file}. "
|
|
445
|
+
"Try lowering threshold or frac_valid."
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Report removed filters
|
|
449
|
+
removed_filters = [
|
|
450
|
+
filters[i] for i in range(n_filters)
|
|
451
|
+
if i not in valid_indices
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
if removed_filters:
|
|
455
|
+
print(f"Removed empty/invalid filters: {removed_filters}")
|
|
456
|
+
|
|
457
|
+
# Build filtered cube + valid filter names
|
|
363
458
|
cube_filtered = cube[valid_indices]
|
|
364
459
|
filters_valid = [filters[i] for i in valid_indices]
|
|
365
|
-
|
|
460
|
+
|
|
366
461
|
# Write new header
|
|
367
462
|
new_header = header.copy()
|
|
368
463
|
new_header["NAXIS3"] = len(filters_valid)
|
|
369
|
-
new_header[
|
|
370
|
-
new_header[
|
|
464
|
+
new_header["NFILTERS"] = len(filters_valid)
|
|
465
|
+
new_header["FILTERS"] = ",".join(filters_valid)
|
|
371
466
|
|
|
372
467
|
for i, f in enumerate(filters_valid):
|
|
373
468
|
new_header[f"FILTER{i+1}"] = f
|
|
374
469
|
|
|
470
|
+
# Remove obsolete FILTER keywords
|
|
375
471
|
for i in range(len(filters_valid), n_filters):
|
|
376
472
|
key = f"FILTER{i+1}"
|
|
377
473
|
if key in new_header:
|
|
378
474
|
del new_header[key]
|
|
379
475
|
|
|
380
|
-
#
|
|
476
|
+
# Add history entry
|
|
477
|
+
new_header.add_history(
|
|
478
|
+
f"Removed invalid filters "
|
|
479
|
+
f"(threshold={threshold}, frac_valid={frac_valid})"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Save filtered datacube
|
|
381
483
|
hdu = fits.PrimaryHDU(cube_filtered, header=new_header)
|
|
382
484
|
hdu.writeto(output_cube, overwrite=True)
|
|
485
|
+
|
|
383
486
|
print(f"Filtered datacube saved at '{output_cube}'")
|
|
487
|
+
print(f"Valid filters: {filters_valid}")
|
|
384
488
|
|
|
385
489
|
return cube_filtered, filters_valid
|
|
386
490
|
|
|
@@ -396,7 +500,7 @@ def remove_filter(cube_fits_file, output_cube, filter_to_remove):
|
|
|
396
500
|
output_cube : str
|
|
397
501
|
Path to save the new datacube.
|
|
398
502
|
filter_to_remove : str
|
|
399
|
-
Name of the filter to be removed
|
|
503
|
+
Name of the filter to be removed.
|
|
400
504
|
"""
|
|
401
505
|
|
|
402
506
|
# Open FITS
|
|
@@ -411,13 +515,20 @@ def remove_filter(cube_fits_file, output_cube, filter_to_remove):
|
|
|
411
515
|
n_filters = header.get("NAXIS3", cube.shape[0])
|
|
412
516
|
filters = [header[f"FILTER{i+1}"].strip() for i in range(n_filters)]
|
|
413
517
|
|
|
414
|
-
# Keep filters
|
|
415
|
-
keep_indices = [i for i, f in enumerate(filters)
|
|
518
|
+
# Keep filters except selected one
|
|
519
|
+
keep_indices = [i for i, f in enumerate(filters)
|
|
520
|
+
if f != filter_to_remove]
|
|
416
521
|
|
|
417
522
|
if len(keep_indices) == len(filters):
|
|
418
|
-
print(f"
|
|
419
|
-
|
|
420
|
-
|
|
523
|
+
print(f"Filter '{filter_to_remove}' not found in datacube.")
|
|
524
|
+
return cube, filters
|
|
525
|
+
|
|
526
|
+
if len(keep_indices) == 0:
|
|
527
|
+
raise ValueError(
|
|
528
|
+
"Cannot remove all filters. Datacube would be empty."
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
print(f"Removing filter: {filter_to_remove}")
|
|
421
532
|
|
|
422
533
|
# New cube
|
|
423
534
|
cube_new = cube[keep_indices]
|
|
@@ -426,23 +537,33 @@ def remove_filter(cube_fits_file, output_cube, filter_to_remove):
|
|
|
426
537
|
# Write new header
|
|
427
538
|
new_header = header.copy()
|
|
428
539
|
new_header["NAXIS3"] = len(filters_new)
|
|
429
|
-
new_header[
|
|
430
|
-
new_header[
|
|
540
|
+
new_header["NFILTERS"] = len(filters_new)
|
|
541
|
+
new_header["FILTERS"] = ",".join(filters_new)
|
|
431
542
|
|
|
432
543
|
for i, f in enumerate(filters_new):
|
|
433
544
|
new_header[f"FILTER{i+1}"] = f
|
|
434
545
|
|
|
546
|
+
# Remove old FILTER keywords
|
|
435
547
|
for i in range(len(filters_new), n_filters):
|
|
436
548
|
key = f"FILTER{i+1}"
|
|
437
549
|
if key in new_header:
|
|
438
550
|
del new_header[key]
|
|
439
551
|
|
|
552
|
+
# Add history
|
|
553
|
+
new_header.add_history(
|
|
554
|
+
f"Removed filter {filter_to_remove}"
|
|
555
|
+
)
|
|
556
|
+
|
|
440
557
|
# Save
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
558
|
+
fits.PrimaryHDU(cube_new, header=new_header).writeto(
|
|
559
|
+
output_cube,
|
|
560
|
+
overwrite=True
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
print(f"New datacube saved at {output_cube}")
|
|
564
|
+
|
|
445
565
|
return cube_new, filters_new
|
|
566
|
+
|
|
446
567
|
|
|
447
568
|
def update_cube_header(
|
|
448
569
|
cube_fits_file,
|
|
@@ -532,3 +653,4 @@ def update_cube_header(
|
|
|
532
653
|
hdul.writeto(output_file, overwrite=overwrite)
|
|
533
654
|
|
|
534
655
|
print(f"Updated header saved to: {output_file}")
|
|
656
|
+
|