lightstack 0.1.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.
- lightstack/__init__.py +104 -0
- lightstack/crop.py +310 -0
- lightstack/datacube.py +493 -0
- lightstack/plot.py +310 -0
- lightstack/psf.py +336 -0
- lightstack/utils.py +223 -0
- lightstack-0.1.0.dist-info/METADATA +13 -0
- lightstack-0.1.0.dist-info/RECORD +10 -0
- lightstack-0.1.0.dist-info/WHEEL +5 -0
- lightstack-0.1.0.dist-info/top_level.txt +1 -0
lightstack/datacube.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from astropy.io import fits
|
|
5
|
+
from astropy.wcs import WCS
|
|
6
|
+
|
|
7
|
+
from reproject import reproject_interp, reproject_exact
|
|
8
|
+
|
|
9
|
+
from .utils import find_ext, infer_filter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def align_reproject_fits(fits_list, ref_file, method="interp", crop=1):
|
|
13
|
+
"""
|
|
14
|
+
Aligns and reprojects FITS images to a common WCS using a reference FITS file.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
fits_list : list of tuples
|
|
19
|
+
List in the form [(fits_path, filter_name), ...].
|
|
20
|
+
|
|
21
|
+
ref_file : str
|
|
22
|
+
Path to the FITS file used as WCS reference.
|
|
23
|
+
|
|
24
|
+
method : str, optional
|
|
25
|
+
Reprojection method:
|
|
26
|
+
- "interp" (default): faster, interpolates values
|
|
27
|
+
- "exact": slower, conserves flux --> but reproject_exact has precision issues with resolutions below ~0.05 arcsec, so the results may not be accurate.
|
|
28
|
+
|
|
29
|
+
crop : int, optional
|
|
30
|
+
Number of pixels to remove from each border after reprojection
|
|
31
|
+
(helps remove edge artifacts from interpolation).
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
aligned_list : list of tuples
|
|
36
|
+
List in the form [(aligned_fits_path, filter_name), ...].
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Choose reprojection method
|
|
40
|
+
if method == "exact":
|
|
41
|
+
reproj_func = reproject_exact
|
|
42
|
+
elif method == "interp":
|
|
43
|
+
reproj_func = reproject_interp
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("method must be 'interp' or 'exact'")
|
|
46
|
+
|
|
47
|
+
# Open reference FITS
|
|
48
|
+
with fits.open(ref_file) as hdul_ref:
|
|
49
|
+
ext_ref = find_ext(hdul_ref)
|
|
50
|
+
if ext_ref is None:
|
|
51
|
+
raise ValueError(f"No valid image data in reference file '{ref_file}'.")
|
|
52
|
+
|
|
53
|
+
ref_header = hdul_ref[ext_ref].header
|
|
54
|
+
ref_wcs = WCS(ref_header)
|
|
55
|
+
shape_out = hdul_ref[ext_ref].data.shape
|
|
56
|
+
|
|
57
|
+
aligned_list = []
|
|
58
|
+
|
|
59
|
+
# Loop over all FITS
|
|
60
|
+
for fpath, filt in fits_list:
|
|
61
|
+
|
|
62
|
+
with fits.open(fpath) as hdul:
|
|
63
|
+
ext = find_ext(hdul)
|
|
64
|
+
if ext is None:
|
|
65
|
+
print(f"No data extension found in {fpath}. Skipping.")
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
data = hdul[ext].data
|
|
69
|
+
header = hdul[ext].header
|
|
70
|
+
wcs_in = WCS(header)
|
|
71
|
+
|
|
72
|
+
unit = header.get('BUNIT', 'unknown')
|
|
73
|
+
print(f"Filter {filt}: unit = {unit}")
|
|
74
|
+
|
|
75
|
+
# Reproject
|
|
76
|
+
data_aligned, footprint = reproj_func(
|
|
77
|
+
(data, wcs_in),
|
|
78
|
+
ref_wcs,
|
|
79
|
+
shape_out=shape_out)
|
|
80
|
+
|
|
81
|
+
# Crop border (interp method)
|
|
82
|
+
if crop > 0:
|
|
83
|
+
data_aligned = data_aligned[crop:-crop, crop:-crop]
|
|
84
|
+
|
|
85
|
+
# Adjust WCS
|
|
86
|
+
wcs_out = ref_wcs.deepcopy()
|
|
87
|
+
wcs_out.wcs.crpix -= crop
|
|
88
|
+
else:
|
|
89
|
+
wcs_out = ref_wcs
|
|
90
|
+
|
|
91
|
+
# Header
|
|
92
|
+
header_aligned = wcs_out.to_header()
|
|
93
|
+
|
|
94
|
+
ref_filter = infer_filter(ref_file)
|
|
95
|
+
header_aligned.add_history(
|
|
96
|
+
f"Reprojected to {ref_filter} using {method} method (reproject package)")
|
|
97
|
+
|
|
98
|
+
if crop > 0:
|
|
99
|
+
header_aligned.add_history(f"Cropped {crop} pixels from each border")
|
|
100
|
+
|
|
101
|
+
# Save
|
|
102
|
+
suffix = f"_aligned_{method}"
|
|
103
|
+
out_name = os.path.splitext(fpath)[0] + f"{suffix}.fits"
|
|
104
|
+
|
|
105
|
+
fits.PrimaryHDU(data_aligned, header=header_aligned).writeto(
|
|
106
|
+
out_name, overwrite=True)
|
|
107
|
+
|
|
108
|
+
aligned_list.append((out_name, filt))
|
|
109
|
+
print(f"Saved: {out_name}")
|
|
110
|
+
|
|
111
|
+
return aligned_list
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def build_datacube(aligned_fits_files, reference_file, output_path):
|
|
115
|
+
"""
|
|
116
|
+
Builds a 3D datacube from aligned 2D FITS images.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
aligned_fits_files : list of tuples
|
|
121
|
+
[(filename, filter_name), ...]
|
|
122
|
+
reference_file : str
|
|
123
|
+
FITS file to define WCS and shape.
|
|
124
|
+
output_path : str
|
|
125
|
+
Path to save the 3D datacube.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
None
|
|
130
|
+
Saves the cut datacube.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
# Open FITS
|
|
134
|
+
with fits.open(reference_file) as hdul_ref:
|
|
135
|
+
ext_ref = find_ext(hdul_ref)
|
|
136
|
+
if ext_ref is None:
|
|
137
|
+
raise ValueError(f"No 2D data in reference file {reference_file}.")
|
|
138
|
+
ref_header = hdul_ref[ext_ref].header
|
|
139
|
+
ny, nx = hdul_ref[ext_ref].data.shape
|
|
140
|
+
wcs_2d = WCS(ref_header, naxis=2)
|
|
141
|
+
|
|
142
|
+
cube_images = []
|
|
143
|
+
filter_names = []
|
|
144
|
+
units = []
|
|
145
|
+
|
|
146
|
+
# Construct cube
|
|
147
|
+
for file, filt in aligned_fits_files:
|
|
148
|
+
with fits.open(file) as hdul:
|
|
149
|
+
ext = find_ext(hdul)
|
|
150
|
+
if ext is None:
|
|
151
|
+
print(f"No 2D data in {file}. Skipping.")
|
|
152
|
+
continue
|
|
153
|
+
data = hdul[ext].data
|
|
154
|
+
cube_images.append(data)
|
|
155
|
+
filter_names.append(filt)
|
|
156
|
+
units.append(hdul[ext].header.get('BUNIT', 'unknown'))
|
|
157
|
+
|
|
158
|
+
cube = np.array(cube_images)
|
|
159
|
+
|
|
160
|
+
# Write header
|
|
161
|
+
wcs_3d = WCS(naxis=3)
|
|
162
|
+
wcs_3d.wcs.crpix[0] = wcs_2d.wcs.crpix[0]
|
|
163
|
+
wcs_3d.wcs.crpix[1] = wcs_2d.wcs.crpix[1]
|
|
164
|
+
wcs_3d.wcs.crval[0] = wcs_2d.wcs.crval[0]
|
|
165
|
+
wcs_3d.wcs.crval[1] = wcs_2d.wcs.crval[1]
|
|
166
|
+
wcs_3d.wcs.cdelt[0] = wcs_2d.wcs.cdelt[0]
|
|
167
|
+
wcs_3d.wcs.cdelt[1] = wcs_2d.wcs.cdelt[1]
|
|
168
|
+
wcs_3d.wcs.ctype[0] = wcs_2d.wcs.ctype[0]
|
|
169
|
+
wcs_3d.wcs.ctype[1] = wcs_2d.wcs.ctype[1]
|
|
170
|
+
wcs_3d.wcs.cunit[0] = wcs_2d.wcs.cunit[0]
|
|
171
|
+
wcs_3d.wcs.cunit[1] = wcs_2d.wcs.cunit[1]
|
|
172
|
+
|
|
173
|
+
if wcs_2d.wcs.has_cd():
|
|
174
|
+
cd3 = np.zeros((3,3))
|
|
175
|
+
cd3[0:2,0:2] = wcs_2d.wcs.cd.copy()
|
|
176
|
+
cd3[2,2] = 1.0
|
|
177
|
+
wcs_3d.wcs.cd = cd3
|
|
178
|
+
|
|
179
|
+
wcs_3d.wcs.crpix[2] = 1
|
|
180
|
+
wcs_3d.wcs.crval[2] = 0
|
|
181
|
+
wcs_3d.wcs.cdelt[2] = 1
|
|
182
|
+
wcs_3d.wcs.ctype[2] = 'FILTER'
|
|
183
|
+
wcs_3d.wcs.cunit[2] = ''
|
|
184
|
+
|
|
185
|
+
header = wcs_3d.to_header()
|
|
186
|
+
header['NAXIS'] = 3
|
|
187
|
+
header['NAXIS1'] = nx
|
|
188
|
+
header['NAXIS2'] = ny
|
|
189
|
+
header['NAXIS3'] = len(filter_names)
|
|
190
|
+
|
|
191
|
+
with fits.open(aligned_fits_files[0][0]) as hdul0:
|
|
192
|
+
ext0 = find_ext(hdul0)
|
|
193
|
+
header_meta = hdul0[ext0].header
|
|
194
|
+
|
|
195
|
+
if "HISTORY" in header_meta:
|
|
196
|
+
for h in header_meta["HISTORY"]:
|
|
197
|
+
header.add_history(h)
|
|
198
|
+
|
|
199
|
+
if all(u == units[0] for u in units):
|
|
200
|
+
header['BUNIT'] = units[0]
|
|
201
|
+
else:
|
|
202
|
+
header['BUNIT'] = 'unknown'
|
|
203
|
+
|
|
204
|
+
for i, filt in enumerate(filter_names):
|
|
205
|
+
header[f'FILTER{i+1}'] = filt
|
|
206
|
+
|
|
207
|
+
header['NFILTERS'] = len(filter_names)
|
|
208
|
+
header['FILTERS'] = ",".join(filter_names)
|
|
209
|
+
|
|
210
|
+
# Save datacube
|
|
211
|
+
fits.PrimaryHDU(cube, header=header).writeto(output_path, overwrite=True)
|
|
212
|
+
print(f"Datacube saved at {output_path}.")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def cut_region_datacube(cube_fits_file, x_start, x_end, y_start, y_end, output_path):
|
|
216
|
+
"""
|
|
217
|
+
Cuts a spatial region from a datacube.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
cube_fits_file : str
|
|
222
|
+
Path to the input 3D fits datacube.
|
|
223
|
+
x_start, x_end : int
|
|
224
|
+
Pixel indices for the x axis.
|
|
225
|
+
y_start, y_end : int
|
|
226
|
+
Pixel indices for the y axis.
|
|
227
|
+
output_filename : str
|
|
228
|
+
Path to the output fits file.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
None
|
|
233
|
+
Saves the cut datacube.
|
|
234
|
+
"""
|
|
235
|
+
# Open datacube
|
|
236
|
+
with fits.open(cube_fits_file) as hdul:
|
|
237
|
+
ext = find_ext(hdul)
|
|
238
|
+
if ext is None:
|
|
239
|
+
raise ValueError(f"No image data found in {cube_fits_file}")
|
|
240
|
+
|
|
241
|
+
cube_data = hdul[ext].data
|
|
242
|
+
cube_header = hdul[ext].header
|
|
243
|
+
|
|
244
|
+
# Cut the data array
|
|
245
|
+
cut_data = cube_data[:, y_start:y_end, x_start:x_end]
|
|
246
|
+
|
|
247
|
+
# Get and update WCS
|
|
248
|
+
wcs_3d = WCS(cube_header)
|
|
249
|
+
wcs_3d.wcs.crpix[0] -= x_start
|
|
250
|
+
wcs_3d.wcs.crpix[1] -= y_start
|
|
251
|
+
|
|
252
|
+
# Create new header with updated size and WCS
|
|
253
|
+
new_header = wcs_3d.to_header()
|
|
254
|
+
new_header['NAXIS'] = 3
|
|
255
|
+
new_header['NAXIS1'] = x_end - x_start
|
|
256
|
+
new_header['NAXIS2'] = y_end - y_start
|
|
257
|
+
new_header['NAXIS3'] = cube_data.shape[0]
|
|
258
|
+
|
|
259
|
+
# Filter info
|
|
260
|
+
for key in cube_header:
|
|
261
|
+
if key.startswith('FILTER'):
|
|
262
|
+
new_header[key] = cube_header[key]
|
|
263
|
+
|
|
264
|
+
# Crop window
|
|
265
|
+
new_header['XMINPIX'] = x_start
|
|
266
|
+
new_header['XMAXPIX'] = x_end
|
|
267
|
+
new_header['YMINPIX'] = y_start
|
|
268
|
+
new_header['YMAXPIX'] = y_end
|
|
269
|
+
|
|
270
|
+
# Save cut datacube
|
|
271
|
+
fits.PrimaryHDU(cut_data, header=new_header).writeto(output_path, overwrite=True)
|
|
272
|
+
print(f"Cut datacube saved to '{output_path}'")
|
|
273
|
+
|
|
274
|
+
def build_valid_datacube(cube_fits_file, output_cube, threshold=0.0, frac_valid=0.01):
|
|
275
|
+
"""
|
|
276
|
+
Remove empty filters in a datacube, saves the new datacube and returns the valid filter names.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
cube_fits_file : str
|
|
281
|
+
Path to the input 3D fits datacube.
|
|
282
|
+
output_cube: str
|
|
283
|
+
Path to save the filtered fits.
|
|
284
|
+
threshold : float
|
|
285
|
+
Minimum flux value to consider a pixel valid.
|
|
286
|
+
frac_valid : float
|
|
287
|
+
Minimum fraction of pixels above the threshold to consider the filter valid.
|
|
288
|
+
|
|
289
|
+
Returns
|
|
290
|
+
-------
|
|
291
|
+
cube_filtered : np.ndarray
|
|
292
|
+
Datacube with only valid filters.
|
|
293
|
+
filters_valid : list
|
|
294
|
+
List of valid filter names.
|
|
295
|
+
|
|
296
|
+
Saves the new datacube.
|
|
297
|
+
"""
|
|
298
|
+
|
|
299
|
+
# Open FITS
|
|
300
|
+
with fits.open(cube_fits_file) as hdul:
|
|
301
|
+
ext = find_ext(hdul)
|
|
302
|
+
if ext is None:
|
|
303
|
+
raise ValueError(f"No image data found in {cube_fits_file}")
|
|
304
|
+
|
|
305
|
+
cube = hdul[ext].data
|
|
306
|
+
header = hdul[ext].header
|
|
307
|
+
|
|
308
|
+
n_filters = header.get("NAXIS3", cube.shape[0])
|
|
309
|
+
filters = [header[f"FILTER{i+1}"].strip() for i in range(n_filters)]
|
|
310
|
+
|
|
311
|
+
# Keep only valid filters
|
|
312
|
+
valid_indices = []
|
|
313
|
+
for i, img in enumerate(cube):
|
|
314
|
+
valid = np.isfinite(img)
|
|
315
|
+
n_total = valid.sum()
|
|
316
|
+
if n_total == 0:
|
|
317
|
+
continue
|
|
318
|
+
n_above = np.sum(img[valid] > threshold)
|
|
319
|
+
if (n_above / n_total) >= frac_valid:
|
|
320
|
+
valid_indices.append(i)
|
|
321
|
+
|
|
322
|
+
cube_filtered = cube[valid_indices]
|
|
323
|
+
filters_valid = [filters[i] for i in valid_indices]
|
|
324
|
+
|
|
325
|
+
# Write new header
|
|
326
|
+
new_header = header.copy()
|
|
327
|
+
new_header["NAXIS3"] = len(filters_valid)
|
|
328
|
+
new_header['NFILTERS'] = len(filters_valid)
|
|
329
|
+
new_header['FILTERS'] = ",".join(filters_valid)
|
|
330
|
+
|
|
331
|
+
for i, f in enumerate(filters_valid):
|
|
332
|
+
new_header[f"FILTER{i+1}"] = f
|
|
333
|
+
|
|
334
|
+
for i in range(len(filters_valid), n_filters):
|
|
335
|
+
key = f"FILTER{i+1}"
|
|
336
|
+
if key in new_header:
|
|
337
|
+
del new_header[key]
|
|
338
|
+
|
|
339
|
+
# Save filtered datacube
|
|
340
|
+
hdu = fits.PrimaryHDU(cube_filtered, header=new_header)
|
|
341
|
+
hdu.writeto(output_cube, overwrite=True)
|
|
342
|
+
print(f"Filtered datacube saved at '{output_cube}'")
|
|
343
|
+
|
|
344
|
+
return cube_filtered, filters_valid
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def remove_filter(cube_fits_file, output_cube, filter_to_remove):
|
|
348
|
+
"""
|
|
349
|
+
Removes a specific filter from a 3D datacube.
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
cube_fits_file : str
|
|
354
|
+
Path to the original datacube.
|
|
355
|
+
output_cube : str
|
|
356
|
+
Path to save the new datacube.
|
|
357
|
+
filter_to_remove : str
|
|
358
|
+
Name of the filter to be removed (e.g., ‘F115W’).
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
# Open FITS
|
|
362
|
+
with fits.open(cube_fits_file) as hdul:
|
|
363
|
+
ext = find_ext(hdul)
|
|
364
|
+
if ext is None:
|
|
365
|
+
raise ValueError(f"No image data found in {cube_fits_file}")
|
|
366
|
+
|
|
367
|
+
cube = hdul[ext].data
|
|
368
|
+
header = hdul[ext].header
|
|
369
|
+
|
|
370
|
+
n_filters = header.get("NAXIS3", cube.shape[0])
|
|
371
|
+
filters = [header[f"FILTER{i+1}"].strip() for i in range(n_filters)]
|
|
372
|
+
|
|
373
|
+
# Keep filters
|
|
374
|
+
keep_indices = [i for i, f in enumerate(filters) if f != filter_to_remove]
|
|
375
|
+
|
|
376
|
+
if len(keep_indices) == len(filters):
|
|
377
|
+
print(f"This filter '{filter_to_remove}' is not in the datacube.")
|
|
378
|
+
else:
|
|
379
|
+
print(f"Removing filter: {filter_to_remove}")
|
|
380
|
+
|
|
381
|
+
# New cube
|
|
382
|
+
cube_new = cube[keep_indices]
|
|
383
|
+
filters_new = [filters[i] for i in keep_indices]
|
|
384
|
+
|
|
385
|
+
# Write new header
|
|
386
|
+
new_header = header.copy()
|
|
387
|
+
new_header["NAXIS3"] = len(filters_new)
|
|
388
|
+
new_header['NFILTERS'] = len(filters_new)
|
|
389
|
+
new_header['FILTERS'] = ",".join(filters_new)
|
|
390
|
+
|
|
391
|
+
for i, f in enumerate(filters_new):
|
|
392
|
+
new_header[f"FILTER{i+1}"] = f
|
|
393
|
+
|
|
394
|
+
for i in range(len(filters_new), n_filters):
|
|
395
|
+
key = f"FILTER{i+1}"
|
|
396
|
+
if key in new_header:
|
|
397
|
+
del new_header[key]
|
|
398
|
+
|
|
399
|
+
# Save
|
|
400
|
+
hdu = fits.PrimaryHDU(cube_new, header=new_header)
|
|
401
|
+
hdu.writeto(output_cube, overwrite=True)
|
|
402
|
+
print(f"New datacube without '{filter_to_remove}' saved at {output_cube}")
|
|
403
|
+
|
|
404
|
+
return cube_new, filters_new
|
|
405
|
+
|
|
406
|
+
def update_cube_header(
|
|
407
|
+
cube_fits_file,
|
|
408
|
+
output_file=None,
|
|
409
|
+
redshift=None,
|
|
410
|
+
ra_center=None,
|
|
411
|
+
dec_center=None,
|
|
412
|
+
use_brightest_pixel=False,
|
|
413
|
+
overwrite=False):
|
|
414
|
+
"""
|
|
415
|
+
Update a datacube header with astrophysical metadata.
|
|
416
|
+
|
|
417
|
+
By default, creates a new file with suffix '_more.fits'
|
|
418
|
+
instead of overwriting the original.
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
cube_fits_file : str
|
|
423
|
+
Path to input datacube.
|
|
424
|
+
|
|
425
|
+
output_file : str or None
|
|
426
|
+
Output FITS file. If None, creates '<original>_more.fits'.
|
|
427
|
+
|
|
428
|
+
redshift : float or None
|
|
429
|
+
Galaxy redshift.
|
|
430
|
+
|
|
431
|
+
ra_center, dec_center : float or None
|
|
432
|
+
Galaxy center coordinates (deg).
|
|
433
|
+
|
|
434
|
+
use_brightest_pixel : bool
|
|
435
|
+
If True, estimate center from brightest pixel.
|
|
436
|
+
|
|
437
|
+
overwrite : bool
|
|
438
|
+
Overwrite output file if it already exists.
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
# Define output file
|
|
442
|
+
if output_file is None:
|
|
443
|
+
base, ext = os.path.splitext(cube_fits_file)
|
|
444
|
+
output_file = f"{base}_more{ext}"
|
|
445
|
+
|
|
446
|
+
# Open FITS
|
|
447
|
+
with fits.open(cube_fits_file) as hdul:
|
|
448
|
+
ext = find_ext(hdul)
|
|
449
|
+
if ext is None:
|
|
450
|
+
raise ValueError(f"No valid data extension in {cube_fits_file}")
|
|
451
|
+
|
|
452
|
+
datacube = hdul[ext].data
|
|
453
|
+
header = hdul[ext].header.copy()
|
|
454
|
+
|
|
455
|
+
wcs = WCS(header)
|
|
456
|
+
if wcs.pixel_n_dim > 2:
|
|
457
|
+
wcs = wcs.celestial
|
|
458
|
+
|
|
459
|
+
# Redshift
|
|
460
|
+
if redshift is not None and not np.isnan(redshift):
|
|
461
|
+
header["REDSHIFT"] = (redshift, "Galaxy redshift")
|
|
462
|
+
else:
|
|
463
|
+
header["REDSHIFT"] = ("UNKNOWN", "Galaxy redshift not provided")
|
|
464
|
+
|
|
465
|
+
# Galaxy center
|
|
466
|
+
if use_brightest_pixel:
|
|
467
|
+
collapsed = np.nansum(datacube, axis=0)
|
|
468
|
+
|
|
469
|
+
y_max, x_max = np.unravel_index(
|
|
470
|
+
np.nanargmax(collapsed), collapsed.shape)
|
|
471
|
+
|
|
472
|
+
ra_peak, dec_peak = wcs.wcs_pix2world(x_max, y_max, 0)
|
|
473
|
+
|
|
474
|
+
header["GAL_XCEN"] = (x_max, "Galaxy center X (pixel)")
|
|
475
|
+
header["GAL_YCEN"] = (y_max, "Galaxy center Y (pixel)")
|
|
476
|
+
header["GAL_RA"] = (ra_peak, "Galaxy center RA (deg)")
|
|
477
|
+
header["GAL_DEC"] = (dec_peak, "Galaxy center Dec (deg)")
|
|
478
|
+
header["CEN_TYPE"] = ("BRIGHTEST", "Center definition")
|
|
479
|
+
|
|
480
|
+
elif (ra_center is not None) and (dec_center is not None):
|
|
481
|
+
header["GAL_RA"] = (ra_center, "Galaxy center RA (deg)")
|
|
482
|
+
header["GAL_DEC"] = (dec_center, "Galaxy center Dec (deg)")
|
|
483
|
+
header["CEN_TYPE"] = ("USER", "Center definition")
|
|
484
|
+
|
|
485
|
+
else:
|
|
486
|
+
header["CEN_TYPE"] = ("UNKNOWN", "Center not defined")
|
|
487
|
+
|
|
488
|
+
# Save
|
|
489
|
+
with fits.open(cube_fits_file) as hdul:
|
|
490
|
+
hdul[ext].header = header
|
|
491
|
+
hdul.writeto(output_file, overwrite=overwrite)
|
|
492
|
+
|
|
493
|
+
print(f"Updated header saved to: {output_file}")
|