lightstack 0.3.2__tar.gz → 0.4.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.3.2/src/lightstack.egg-info → lightstack-0.4.0}/PKG-INFO +4 -4
- {lightstack-0.3.2 → lightstack-0.4.0}/README.md +3 -3
- {lightstack-0.3.2 → lightstack-0.4.0}/pyproject.toml +1 -1
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack/__init__.py +10 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack/datacube.py +127 -1
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack/psf.py +182 -39
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack/utils.py +402 -28
- {lightstack-0.3.2 → lightstack-0.4.0/src/lightstack.egg-info}/PKG-INFO +4 -4
- {lightstack-0.3.2 → lightstack-0.4.0}/LICENSE +0 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/setup.cfg +0 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack/crop.py +0 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack/plot.py +0 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack.egg-info/SOURCES.txt +0 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack.egg-info/dependency_links.txt +0 -0
- {lightstack-0.3.2 → lightstack-0.4.0}/src/lightstack.egg-info/requires.txt +0 -0
- {lightstack-0.3.2 → lightstack-0.4.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.4.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
|
|
@@ -36,13 +36,13 @@ Requires-Dist: scipy
|
|
|
36
36
|
Requires-Dist: photutils
|
|
37
37
|
Requires-Dist: pandas
|
|
38
38
|
|
|
39
|
-
[](https://doi.org/10.5281/zenodo.
|
|
39
|
+
[](https://doi.org/10.5281/zenodo.22148981)
|
|
40
40
|
|
|
41
41
|
<img src="https://raw.githubusercontent.com/AndressaWille/lightstack/master/images/logo.png" width="120">
|
|
42
42
|
|
|
43
43
|
# Lightstack
|
|
44
44
|
|
|
45
|
-
Tools for building and processing multi-filter astrophysical datacubes.
|
|
45
|
+
Tools for building and processing multi-filter (and multi-servey) astrophysical datacubes.
|
|
46
46
|
|
|
47
47
|
## Features
|
|
48
48
|
|
|
@@ -60,4 +60,4 @@ pip install lightstack
|
|
|
60
60
|
|
|
61
61
|
https://lightstack.readthedocs.io/en/latest/
|
|
62
62
|
|
|
63
|
-
See the tutorial
|
|
63
|
+
See the tutorial notebooks (in the "examples" folder)!
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
[](https://doi.org/10.5281/zenodo.
|
|
1
|
+
[](https://doi.org/10.5281/zenodo.22148981)
|
|
2
2
|
|
|
3
3
|
<img src="https://raw.githubusercontent.com/AndressaWille/lightstack/master/images/logo.png" width="120">
|
|
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 (and multi-servey) astrophysical datacubes.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -22,4 +22,4 @@ pip install lightstack
|
|
|
22
22
|
|
|
23
23
|
https://lightstack.readthedocs.io/en/latest/
|
|
24
24
|
|
|
25
|
-
See the tutorial
|
|
25
|
+
See the tutorial notebooks (in the "examples" folder)!
|
|
@@ -34,6 +34,7 @@ from .datacube import (
|
|
|
34
34
|
build_valid_datacube,
|
|
35
35
|
remove_filter,
|
|
36
36
|
update_cube_header,
|
|
37
|
+
apply_reference_nan_mask,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
# =========================
|
|
@@ -52,6 +53,8 @@ from .psf import (
|
|
|
52
53
|
oversample_psf,
|
|
53
54
|
add_arcsec_axis,
|
|
54
55
|
encircled_energy,
|
|
56
|
+
measure_psf_fwhm,
|
|
57
|
+
print_psf_fwhm,
|
|
55
58
|
clean_stellar_cutouts_radially,
|
|
56
59
|
plot_stellar_radial_profiles,
|
|
57
60
|
plot_psf_stars,
|
|
@@ -83,6 +86,7 @@ from .plot import (
|
|
|
83
86
|
# =========================
|
|
84
87
|
from .utils import (
|
|
85
88
|
identify_filter,
|
|
89
|
+
check_filenames,
|
|
86
90
|
get_filter_info,
|
|
87
91
|
select_filters,
|
|
88
92
|
add_custom_dataset,
|
|
@@ -94,6 +98,7 @@ from .utils import (
|
|
|
94
98
|
MJy_sr_to_jy,
|
|
95
99
|
get_pixel_scale,
|
|
96
100
|
get_pixel_scale_from_wcs,
|
|
101
|
+
print_fits_spatial_info,
|
|
97
102
|
)
|
|
98
103
|
|
|
99
104
|
# =========================
|
|
@@ -117,6 +122,7 @@ __all__ = [
|
|
|
117
122
|
"build_valid_datacube",
|
|
118
123
|
"remove_filter",
|
|
119
124
|
"update_cube_header",
|
|
125
|
+
"apply_reference_nan_mask",
|
|
120
126
|
|
|
121
127
|
# psf
|
|
122
128
|
"get_psf_input_files",
|
|
@@ -131,6 +137,8 @@ __all__ = [
|
|
|
131
137
|
"oversample_psf",
|
|
132
138
|
"add_arcsec_axis",
|
|
133
139
|
"encircled_energy",
|
|
140
|
+
"measure_psf_fwhm",
|
|
141
|
+
"print_psf_fwhm",
|
|
134
142
|
"clean_stellar_cutouts_radially",
|
|
135
143
|
"plot_stellar_radial_profiles",
|
|
136
144
|
"plot_psf_stars",
|
|
@@ -155,6 +163,7 @@ __all__ = [
|
|
|
155
163
|
# utils
|
|
156
164
|
"find_ext",
|
|
157
165
|
"identify_filter",
|
|
166
|
+
"check_filenames",
|
|
158
167
|
"get_filter_info",
|
|
159
168
|
"select_filters",
|
|
160
169
|
"add_custom_dataset",
|
|
@@ -165,5 +174,6 @@ __all__ = [
|
|
|
165
174
|
"MJy_sr_to_jy",
|
|
166
175
|
"get_pixel_scale",
|
|
167
176
|
"get_pixel_scale_from_wcs",
|
|
177
|
+
"print_fits_spatial_info",
|
|
168
178
|
|
|
169
179
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import os
|
|
3
3
|
import glob
|
|
4
|
+
import re
|
|
4
5
|
|
|
5
6
|
from astropy.io import fits
|
|
6
7
|
from astropy.wcs import WCS
|
|
@@ -41,6 +42,25 @@ def load_image_fits(folder_path):
|
|
|
41
42
|
return fits_files
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
def _is_aligned_output(fpath):
|
|
46
|
+
"""
|
|
47
|
+
Check whether a FITS file was written by ``align_reproject_fits``
|
|
48
|
+
(i.e. its name ends in ``_aligned_interp`` or ``_aligned_exact``).
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
fpath : str or pathlib.Path
|
|
53
|
+
FITS filename.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
bool
|
|
58
|
+
True if the file is an output of ``align_reproject_fits``.
|
|
59
|
+
"""
|
|
60
|
+
stem = os.path.splitext(os.path.basename(fpath))[0]
|
|
61
|
+
return re.search(r"_aligned_(interp|exact)$", stem) is not None
|
|
62
|
+
|
|
63
|
+
|
|
44
64
|
def align_reproject_fits(
|
|
45
65
|
fits_list,
|
|
46
66
|
ref_file,
|
|
@@ -50,6 +70,11 @@ def align_reproject_fits(
|
|
|
50
70
|
"""
|
|
51
71
|
Align and optionally reproject FITS images to a common WCS.
|
|
52
72
|
|
|
73
|
+
Files produced by a previous run of this function (names ending in
|
|
74
|
+
``_aligned_interp.fits`` or ``_aligned_exact.fits``) are skipped, so
|
|
75
|
+
that only the original images are aligned and previous outputs are
|
|
76
|
+
overwritten instead of being aligned again.
|
|
77
|
+
|
|
53
78
|
Parameters
|
|
54
79
|
----------
|
|
55
80
|
fits_list : list of tuples
|
|
@@ -57,7 +82,8 @@ def align_reproject_fits(
|
|
|
57
82
|
[(fits_path, dataset, filter_name), ...]
|
|
58
83
|
|
|
59
84
|
ref_file : str
|
|
60
|
-
Reference FITS file.
|
|
85
|
+
Reference FITS file. It must be an original image, not a previous
|
|
86
|
+
``*_aligned_*`` output.
|
|
61
87
|
|
|
62
88
|
method : str, optional
|
|
63
89
|
Reprojection method: "interp" or "exact".
|
|
@@ -86,6 +112,28 @@ def align_reproject_fits(
|
|
|
86
112
|
"method must be 'interp' or 'exact'"
|
|
87
113
|
)
|
|
88
114
|
|
|
115
|
+
# Skip outputs of previous runs to avoid duplicated filters
|
|
116
|
+
if _is_aligned_output(ref_file):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Reference file '{os.path.basename(ref_file)}' is the output "
|
|
119
|
+
"of a previous alignment. Use the original image instead."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
skipped = [fpath for fpath, _, _ in fits_list if _is_aligned_output(fpath)]
|
|
123
|
+
|
|
124
|
+
if skipped:
|
|
125
|
+
print(
|
|
126
|
+
f"Skipping {len(skipped)} previously aligned file(s) "
|
|
127
|
+
"(they will be overwritten):"
|
|
128
|
+
)
|
|
129
|
+
for fpath in skipped:
|
|
130
|
+
print(f" {os.path.basename(fpath)}")
|
|
131
|
+
|
|
132
|
+
fits_list = [
|
|
133
|
+
entry for entry in fits_list
|
|
134
|
+
if not _is_aligned_output(entry[0])
|
|
135
|
+
]
|
|
136
|
+
|
|
89
137
|
# --------------------------------------------------
|
|
90
138
|
# 2. Open reference FITS
|
|
91
139
|
# --------------------------------------------------
|
|
@@ -688,6 +736,14 @@ def remove_filter(cube_fits_file, output_cube, filter_to_remove):
|
|
|
688
736
|
Path to save the new datacube.
|
|
689
737
|
filter_to_remove : str
|
|
690
738
|
Name of the filter to be removed.
|
|
739
|
+
|
|
740
|
+
Returns
|
|
741
|
+
-------
|
|
742
|
+
cube : ndarray
|
|
743
|
+
Datacube without the removed filter. If the filter is not found,
|
|
744
|
+
the original datacube is returned and no file is written.
|
|
745
|
+
filters : list of str
|
|
746
|
+
Filter names of the returned datacube.
|
|
691
747
|
"""
|
|
692
748
|
|
|
693
749
|
# Open FITS
|
|
@@ -840,3 +896,73 @@ def update_cube_header(
|
|
|
840
896
|
hdul.writeto(output_file, overwrite=overwrite)
|
|
841
897
|
|
|
842
898
|
print(f"Updated header saved to: {output_file}")
|
|
899
|
+
|
|
900
|
+
def apply_reference_nan_mask(
|
|
901
|
+
cube_file,
|
|
902
|
+
reference_slice=0,
|
|
903
|
+
output_path=None,
|
|
904
|
+
):
|
|
905
|
+
"""
|
|
906
|
+
Propagate the NaN mask of a reference slice to all slices
|
|
907
|
+
of a datacube.
|
|
908
|
+
|
|
909
|
+
Parameters
|
|
910
|
+
----------
|
|
911
|
+
cube_file : str or pathlib.Path
|
|
912
|
+
Input datacube FITS file.
|
|
913
|
+
|
|
914
|
+
reference_slice : int, optional
|
|
915
|
+
Index of the slice whose valid-pixel footprint will be used
|
|
916
|
+
as the reference mask. Default is 0.
|
|
917
|
+
|
|
918
|
+
output_path : str or pathlib.Path, optional
|
|
919
|
+
Output FITS file. If None, the cube is not saved.
|
|
920
|
+
|
|
921
|
+
Returns
|
|
922
|
+
-------
|
|
923
|
+
cube : ndarray
|
|
924
|
+
Cube with the reference NaN mask propagated to all slices.
|
|
925
|
+
|
|
926
|
+
header : fits.Header
|
|
927
|
+
FITS header from the input cube.
|
|
928
|
+
"""
|
|
929
|
+
|
|
930
|
+
with fits.open(cube_file) as hdul:
|
|
931
|
+
|
|
932
|
+
cube = hdul[0].data.copy()
|
|
933
|
+
header = hdul[0].header.copy()
|
|
934
|
+
|
|
935
|
+
# Validate reference slice
|
|
936
|
+
if not 0 <= reference_slice < cube.shape[0]:
|
|
937
|
+
raise ValueError(
|
|
938
|
+
f"reference_slice={reference_slice} is outside the "
|
|
939
|
+
f"cube range (0-{cube.shape[0] - 1})."
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
# Reference mask
|
|
943
|
+
ref_mask = np.isfinite(
|
|
944
|
+
cube[reference_slice]
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
if not np.any(ref_mask):
|
|
948
|
+
raise ValueError(
|
|
949
|
+
f"Reference slice {reference_slice} contains no "
|
|
950
|
+
"valid pixels."
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
# Propagate mask
|
|
954
|
+
for i in range(cube.shape[0]):
|
|
955
|
+
cube[i][~ref_mask] = np.nan
|
|
956
|
+
|
|
957
|
+
# Save
|
|
958
|
+
if output_path is not None:
|
|
959
|
+
|
|
960
|
+
fits.PrimaryHDU(
|
|
961
|
+
cube,
|
|
962
|
+
header=header,
|
|
963
|
+
).writeto(
|
|
964
|
+
output_path,
|
|
965
|
+
overwrite=True,
|
|
966
|
+
)
|
|
967
|
+
|
|
968
|
+
return cube, header
|
|
@@ -3,6 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import os
|
|
6
|
+
import warnings
|
|
6
7
|
import matplotlib.pyplot as plt
|
|
7
8
|
|
|
8
9
|
from astropy.visualization import simple_norm
|
|
@@ -12,7 +13,9 @@ from astropy.convolution import convolve_fft
|
|
|
12
13
|
from photutils.centroids import centroid_com, centroid_quadratic
|
|
13
14
|
from scipy.ndimage import shift, zoom, gaussian_filter, affine_transform
|
|
14
15
|
from astropy.nddata import Cutout2D
|
|
15
|
-
from astropy.stats import sigma_clip, sigma_clipped_stats
|
|
16
|
+
from astropy.stats import gaussian_sigma_to_fwhm, sigma_clip, sigma_clipped_stats
|
|
17
|
+
from astropy.modeling import fitting, models
|
|
18
|
+
from astropy.utils.exceptions import AstropyWarning
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
from .utils import (
|
|
@@ -43,7 +46,10 @@ def get_psf_input_files(data_dir, dataset):
|
|
|
43
46
|
"""
|
|
44
47
|
data_dir = Path(data_dir)
|
|
45
48
|
|
|
46
|
-
fits_files = sorted(
|
|
49
|
+
fits_files = sorted(
|
|
50
|
+
f for f in data_dir.glob("*.fits")
|
|
51
|
+
if not f.name.lower().startswith("empirical_psf_")
|
|
52
|
+
)
|
|
47
53
|
|
|
48
54
|
if not fits_files:
|
|
49
55
|
raise FileNotFoundError(
|
|
@@ -81,7 +87,6 @@ def get_psf_input_files(data_dir, dataset):
|
|
|
81
87
|
|
|
82
88
|
return inputs
|
|
83
89
|
|
|
84
|
-
|
|
85
90
|
def extract_cutouts(image, stars, size):
|
|
86
91
|
"""
|
|
87
92
|
Extract square cutouts centered on selected stars.
|
|
@@ -166,28 +171,87 @@ def subtract_background(cutout, border, sigma=3, clip_negative=True, noise_thres
|
|
|
166
171
|
|
|
167
172
|
return corrected, background, background_std
|
|
168
173
|
|
|
169
|
-
def measure_centers(cutouts):
|
|
174
|
+
def measure_centers(cutouts, fallback="peak"):
|
|
170
175
|
"""
|
|
171
|
-
Measure
|
|
176
|
+
Measure stellar centers using quadratic centroids, with an optional
|
|
177
|
+
peak-based fallback.
|
|
172
178
|
|
|
173
179
|
Parameters
|
|
174
180
|
----------
|
|
175
181
|
cutouts : dict
|
|
176
|
-
|
|
182
|
+
Dictionary of stellar cutouts.
|
|
183
|
+
|
|
184
|
+
fallback : str or None, optional
|
|
185
|
+
Method used when the quadratic centroid fails.
|
|
186
|
+
Currently supports ``"peak"`` or ``None``.
|
|
177
187
|
|
|
178
188
|
Returns
|
|
179
189
|
-------
|
|
180
|
-
dict
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
centers : dict
|
|
191
|
+
Dictionary containing the measured x and y centers and
|
|
192
|
+
the method used for each star.
|
|
183
193
|
"""
|
|
194
|
+
|
|
184
195
|
centers = {}
|
|
196
|
+
|
|
185
197
|
for name, image in cutouts.items():
|
|
186
|
-
|
|
187
|
-
|
|
198
|
+
|
|
199
|
+
image = np.asarray(image, dtype=float)
|
|
200
|
+
|
|
201
|
+
# Try quadratic centroid
|
|
202
|
+
x_center, y_center = centroid_quadratic(image)
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
np.isfinite(x_center)
|
|
206
|
+
and np.isfinite(y_center)
|
|
207
|
+
):
|
|
208
|
+
centers[name] = {
|
|
209
|
+
"x": float(x_center),
|
|
210
|
+
"y": float(y_center),
|
|
211
|
+
"method": "quadratic",
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Fallback
|
|
217
|
+
if fallback == "peak":
|
|
218
|
+
|
|
219
|
+
if not np.any(np.isfinite(image)):
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"Cannot determine center for star '{name}': "
|
|
222
|
+
"cutout contains no finite pixels."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
y_peak, x_peak = np.unravel_index(
|
|
226
|
+
np.nanargmax(image),
|
|
227
|
+
image.shape,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
centers[name] = {
|
|
231
|
+
"x": float(x_peak),
|
|
232
|
+
"y": float(y_peak),
|
|
233
|
+
"method": "peak",
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
elif fallback is None:
|
|
237
|
+
|
|
238
|
+
centers[name] = {
|
|
239
|
+
"x": np.nan,
|
|
240
|
+
"y": np.nan,
|
|
241
|
+
"method": "failed",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
else:
|
|
245
|
+
|
|
246
|
+
raise ValueError(
|
|
247
|
+
f"Unknown fallback method: '{fallback}'. "
|
|
248
|
+
"Use 'peak' or None."
|
|
249
|
+
)
|
|
250
|
+
|
|
188
251
|
return centers
|
|
189
252
|
|
|
190
253
|
|
|
254
|
+
|
|
191
255
|
def recenter_cutouts(cutouts, centers, order=3):
|
|
192
256
|
"""
|
|
193
257
|
Shift cutouts so their measured centroids match their geometric centres.
|
|
@@ -414,6 +478,85 @@ def encircled_energy(image):
|
|
|
414
478
|
radius, flux = radius[valid][order], flux[valid][order]
|
|
415
479
|
return radius, np.cumsum(flux) / np.nansum(flux)
|
|
416
480
|
|
|
481
|
+
def measure_psf_fwhm(psf):
|
|
482
|
+
"""
|
|
483
|
+
Measure the FWHM of a PSF by fitting a 2D Gaussian to its core.
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
psf : 2D ndarray
|
|
488
|
+
PSF image. Non-finite pixels are ignored.
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
float
|
|
493
|
+
FWHM in pixels, averaged over the two axes of the fitted Gaussian.
|
|
494
|
+
"""
|
|
495
|
+
data = np.nan_to_num(np.asarray(psf, dtype=float))
|
|
496
|
+
|
|
497
|
+
yc, xc = np.unravel_index(np.argmax(data), data.shape)
|
|
498
|
+
|
|
499
|
+
# Rough FWHM from the area above half maximum, used to size the fit box
|
|
500
|
+
fwhm0 = 2 * np.sqrt(np.sum(data > data[yc, xc] / 2) / np.pi)
|
|
501
|
+
half_size = int(np.ceil(max(5, 3 * fwhm0)))
|
|
502
|
+
|
|
503
|
+
y, x = np.mgrid[
|
|
504
|
+
max(yc - half_size, 0):yc + half_size + 1,
|
|
505
|
+
max(xc - half_size, 0):xc + half_size + 1,
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
model = models.Gaussian2D(
|
|
509
|
+
amplitude=data[yc, xc], x_mean=xc, y_mean=yc,
|
|
510
|
+
x_stddev=fwhm0 / gaussian_sigma_to_fwhm,
|
|
511
|
+
y_stddev=fwhm0 / gaussian_sigma_to_fwhm,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
with warnings.catch_warnings():
|
|
515
|
+
warnings.simplefilter("ignore", AstropyWarning)
|
|
516
|
+
fitted = fitting.LevMarLSQFitter()(model, x, y, data[y, x])
|
|
517
|
+
|
|
518
|
+
sigma = (abs(fitted.x_stddev.value) + abs(fitted.y_stddev.value)) / 2
|
|
519
|
+
|
|
520
|
+
return sigma * gaussian_sigma_to_fwhm
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def print_psf_fwhm(psf_files, pixel_scale=None):
|
|
524
|
+
"""
|
|
525
|
+
Measure and print the FWHM of a set of PSF FITS files.
|
|
526
|
+
|
|
527
|
+
Parameters
|
|
528
|
+
----------
|
|
529
|
+
psf_files : iterable of str or pathlib.Path
|
|
530
|
+
PSF FITS files.
|
|
531
|
+
pixel_scale : float, optional
|
|
532
|
+
Pixel scale in arcsec/pixel. If None, it is read from the
|
|
533
|
+
``PIXSCALE`` or ``PIXELSCL`` header keyword, if present.
|
|
534
|
+
|
|
535
|
+
Returns
|
|
536
|
+
-------
|
|
537
|
+
dict
|
|
538
|
+
FWHM in pixels for each filename.
|
|
539
|
+
"""
|
|
540
|
+
results = {}
|
|
541
|
+
|
|
542
|
+
for psf_file in psf_files:
|
|
543
|
+
|
|
544
|
+
name = Path(psf_file).name
|
|
545
|
+
|
|
546
|
+
with fits.open(psf_file) as hdul:
|
|
547
|
+
hdu = hdul[find_ext(hdul)]
|
|
548
|
+
fwhm = measure_psf_fwhm(hdu.data)
|
|
549
|
+
scale = pixel_scale or hdu.header.get("PIXSCALE", hdu.header.get("PIXELSCL"))
|
|
550
|
+
|
|
551
|
+
results[name] = fwhm
|
|
552
|
+
|
|
553
|
+
if scale:
|
|
554
|
+
print(f"{name}: FWHM = {fwhm:.2f} px = {fwhm * scale:.4f} arcsec")
|
|
555
|
+
else:
|
|
556
|
+
print(f"{name}: FWHM = {fwhm:.2f} px")
|
|
557
|
+
|
|
558
|
+
return results
|
|
559
|
+
|
|
417
560
|
def clean_stellar_cutouts_radially(
|
|
418
561
|
cutouts,
|
|
419
562
|
background_stds,
|
|
@@ -1303,19 +1446,19 @@ def build_empirical_psf(
|
|
|
1303
1446
|
noise during background subtraction. Default is 0.5.
|
|
1304
1447
|
|
|
1305
1448
|
radial_cleaning : bool, optional
|
|
1306
|
-
If True,
|
|
1307
|
-
|
|
1308
|
-
False.
|
|
1449
|
+
If True, set to zero the pixels of each stellar cutout outside a
|
|
1450
|
+
cleaning radius determined from its radial profile (see
|
|
1451
|
+
``clean_stellar_cutouts_radially``). Default is False.
|
|
1309
1452
|
|
|
1310
1453
|
cleaning_sigma : float, optional
|
|
1311
|
-
|
|
1312
|
-
radial
|
|
1313
|
-
|
|
1454
|
+
Used only if ``radial_cleaning`` is True. The cleaning radius is
|
|
1455
|
+
where the radial profile falls below ``cleaning_sigma`` times the
|
|
1456
|
+
background standard deviation. Default is 1.0.
|
|
1314
1457
|
|
|
1315
1458
|
cleaning_consecutive_bins : int, optional
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1459
|
+
Used only if ``radial_cleaning`` is True. Number of consecutive
|
|
1460
|
+
radial bins that must stay below the threshold to define the
|
|
1461
|
+
cleaning radius. Default is 3.
|
|
1319
1462
|
|
|
1320
1463
|
normalization_radius : float, optional
|
|
1321
1464
|
Radius used to normalize each star. Its unit is controlled by
|
|
@@ -1342,23 +1485,24 @@ def build_empirical_psf(
|
|
|
1342
1485
|
is used.
|
|
1343
1486
|
|
|
1344
1487
|
plot_stars : bool, optional
|
|
1345
|
-
If True,
|
|
1346
|
-
|
|
1488
|
+
If True, plot the stellar cutouts used to build the PSF (see
|
|
1489
|
+
``plot_psf_stars``). Default is False.
|
|
1347
1490
|
|
|
1348
1491
|
plot_diagnostics : bool, optional
|
|
1349
|
-
If True,
|
|
1350
|
-
|
|
1492
|
+
If True, plot diagnostic figures of the final PSF (see
|
|
1493
|
+
``plot_psf_diagnostics``). Default is False.
|
|
1351
1494
|
|
|
1352
1495
|
save_figures : bool, optional
|
|
1353
|
-
If True,
|
|
1354
|
-
Default is False.
|
|
1496
|
+
If True, save the figures enabled by ``plot_stars`` and
|
|
1497
|
+
``plot_diagnostics``. Requires ``figure_dir``. Default is False.
|
|
1355
1498
|
|
|
1356
1499
|
show_figures : bool, optional
|
|
1357
|
-
If True, display the
|
|
1500
|
+
If True, display the figures enabled by ``plot_stars`` and
|
|
1501
|
+
``plot_diagnostics``. Default is True.
|
|
1358
1502
|
|
|
1359
1503
|
figure_dir : str or pathlib.Path or None, optional
|
|
1360
|
-
Directory
|
|
1361
|
-
|
|
1504
|
+
Directory where figures are saved when ``save_figures`` is True.
|
|
1505
|
+
Default is None.
|
|
1362
1506
|
|
|
1363
1507
|
Returns
|
|
1364
1508
|
-------
|
|
@@ -1394,19 +1538,13 @@ def build_empirical_psf(
|
|
|
1394
1538
|
Aperture fluxes used for normalization.
|
|
1395
1539
|
|
|
1396
1540
|
``cutouts``
|
|
1397
|
-
|
|
1541
|
+
Background-subtracted stellar cutouts.
|
|
1398
1542
|
|
|
1399
1543
|
``cutouts_centered``
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
``cutouts_cleaned``
|
|
1403
|
-
Recentered cutouts after optional radial cleaning.
|
|
1544
|
+
Recentered stellar cutouts.
|
|
1404
1545
|
|
|
1405
1546
|
``cutouts_normalized``
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
``cleaning_radii``
|
|
1409
|
-
Radial-cleaning radii in pixels, when radial cleaning is enabled.
|
|
1547
|
+
Normalized stellar cutouts.
|
|
1410
1548
|
|
|
1411
1549
|
``psf_raw``
|
|
1412
1550
|
Cropped stacked PSF before final normalization.
|
|
@@ -1538,7 +1676,11 @@ def build_empirical_psf(
|
|
|
1538
1676
|
background_stds[name] = background_std
|
|
1539
1677
|
|
|
1540
1678
|
# Measure and correct centroids
|
|
1541
|
-
|
|
1679
|
+
|
|
1680
|
+
centers = measure_centers(
|
|
1681
|
+
cutouts_subtracted,
|
|
1682
|
+
fallback="peak",
|
|
1683
|
+
)
|
|
1542
1684
|
|
|
1543
1685
|
cutouts_centered = recenter_cutouts(
|
|
1544
1686
|
cutouts_subtracted,
|
|
@@ -1715,6 +1857,7 @@ def build_empirical_psf(
|
|
|
1715
1857
|
}
|
|
1716
1858
|
|
|
1717
1859
|
|
|
1860
|
+
|
|
1718
1861
|
def pad_psf_for_resampling(
|
|
1719
1862
|
psf_path,
|
|
1720
1863
|
target_pixel_scale,
|
|
@@ -67,10 +67,10 @@ FILTER_DATABASE = {
|
|
|
67
67
|
# ACS SBC
|
|
68
68
|
#"F435W": {"lambda": 1518.84, "instrument": "ACS-SBC", "group": "ACS-SBC"},
|
|
69
69
|
|
|
70
|
-
# WFC3 UVIS
|
|
71
|
-
"F225W": {"lambda": 2357.65, "instrument": "WFC3", "group": "UVIS"},
|
|
72
|
-
"F275W": {"lambda": 2702.92, "instrument": "WFC3", "group": "UVIS"},
|
|
73
|
-
"F336W": {"lambda": 3354.60, "instrument": "WFC3", "group": "UVIS"},
|
|
70
|
+
# WFC3 UVIS ("aliases" covers the grizli "u" suffix, e.g. f225wu)
|
|
71
|
+
"F225W": {"lambda": 2357.65, "instrument": "WFC3", "group": "UVIS", "aliases": ["F225WU"]},
|
|
72
|
+
"F275W": {"lambda": 2702.92, "instrument": "WFC3", "group": "UVIS", "aliases": ["F275WU"]},
|
|
73
|
+
"F336W": {"lambda": 3354.60, "instrument": "WFC3", "group": "UVIS", "aliases": ["F336WU"]},
|
|
74
74
|
"F390W": {"lambda": 3920.70, "instrument": "WFC3", "group": "UVIS"},
|
|
75
75
|
"F680N": {"lambda": 6877.61, "instrument": "WFC3", "group": "UVIS"},
|
|
76
76
|
|
|
@@ -203,10 +203,99 @@ FILTER_DATABASE = {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
|
|
206
|
+
def _naming_hint(dataset):
|
|
207
|
+
"""
|
|
208
|
+
Explain the expected FITS filename convention for a dataset.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
dataset : str
|
|
213
|
+
Dataset key in ``FILTER_DATABASE`` (upper case).
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
str
|
|
218
|
+
Message with the naming convention, examples and valid filters.
|
|
219
|
+
"""
|
|
220
|
+
example_filter = next(iter(FILTER_DATABASE[dataset]["filters"]))
|
|
221
|
+
valid_filters = ", ".join(FILTER_DATABASE[dataset]["filters"])
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
"Expected filename convention: the dataset name and the filter name "
|
|
225
|
+
"must appear as separate words, delimited by '_', '-' or '.' "
|
|
226
|
+
"(case-insensitive), e.g. "
|
|
227
|
+
f"'{dataset}_{example_filter}.fits' or "
|
|
228
|
+
f"'mytarget_{dataset}_{example_filter}_drz.fits'.\n"
|
|
229
|
+
"Other information is allowed in the filename, but whenever possible "
|
|
230
|
+
"keep only the dataset and the filter names "
|
|
231
|
+
f"(e.g. '{dataset}_{example_filter}.fits').\n"
|
|
232
|
+
f"Valid {dataset} filters: {valid_filters}."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _has_word(name, word):
|
|
237
|
+
"""
|
|
238
|
+
Check whether ``word`` appears in ``name`` as a separate word, i.e. not
|
|
239
|
+
glued to other letters or digits (case-insensitive).
|
|
240
|
+
|
|
241
|
+
Parameters
|
|
242
|
+
----------
|
|
243
|
+
name : str
|
|
244
|
+
Text to search, usually a filename without extension.
|
|
245
|
+
word : str
|
|
246
|
+
Word to look for, e.g. a dataset or filter name.
|
|
247
|
+
|
|
248
|
+
Returns
|
|
249
|
+
-------
|
|
250
|
+
bool
|
|
251
|
+
True if ``word`` is found as a separate word.
|
|
252
|
+
"""
|
|
253
|
+
pattern = rf"(?<![A-Z0-9]){re.escape(word.upper())}(?![A-Z0-9])"
|
|
254
|
+
return re.search(pattern, name.upper()) is not None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _filters_in_name(name, dataset):
|
|
258
|
+
"""
|
|
259
|
+
Return the dataset filters whose name, or one of its ``aliases``,
|
|
260
|
+
appears in ``name`` as a separate word.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
name : str
|
|
265
|
+
Text to search, usually a filename without extension.
|
|
266
|
+
dataset : str
|
|
267
|
+
Dataset key in ``FILTER_DATABASE`` (upper case).
|
|
268
|
+
|
|
269
|
+
Returns
|
|
270
|
+
-------
|
|
271
|
+
list of str
|
|
272
|
+
Canonical names of the filters found.
|
|
273
|
+
"""
|
|
274
|
+
filters = FILTER_DATABASE[dataset]["filters"]
|
|
275
|
+
|
|
276
|
+
return [
|
|
277
|
+
filt
|
|
278
|
+
for filt, meta in filters.items()
|
|
279
|
+
if any(
|
|
280
|
+
_has_word(name, alias)
|
|
281
|
+
for alias in [filt, *meta.get("aliases", [])]
|
|
282
|
+
)
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
|
|
206
286
|
def identify_filter(fits_file, dataset):
|
|
207
287
|
"""
|
|
208
288
|
Identify a FITS file's filter using its filename and a dataset registry.
|
|
209
289
|
|
|
290
|
+
The dataset name and the filter name (or one of its ``aliases``) must
|
|
291
|
+
appear in the filename as separate words, delimited by ``_``, ``-`` or
|
|
292
|
+
``.`` (case-insensitive). For example, ``LSST_r.fits`` and
|
|
293
|
+
``abell370_LSST_r_drz.fits`` are identified as LSST ``r``, while
|
|
294
|
+
``abell370_LSST_rband.fits`` is not. Other information is allowed in the
|
|
295
|
+
filename, but whenever possible keep only the dataset and the filter
|
|
296
|
+
names (e.g. ``LSST_r.fits``); see ``simplify_fits_filename`` and
|
|
297
|
+
``check_filenames``.
|
|
298
|
+
|
|
210
299
|
Parameters
|
|
211
300
|
----------
|
|
212
301
|
fits_file : str or pathlib.Path
|
|
@@ -222,12 +311,13 @@ def identify_filter(fits_file, dataset):
|
|
|
222
311
|
Raises
|
|
223
312
|
------
|
|
224
313
|
ValueError
|
|
225
|
-
If the dataset is unknown, absent from the filename, or
|
|
226
|
-
filter
|
|
314
|
+
If the dataset is unknown, absent from the filename, or if zero or
|
|
315
|
+
more than one registered filter is found in the filename.
|
|
227
316
|
"""
|
|
228
317
|
|
|
229
318
|
dataset = dataset.upper()
|
|
230
|
-
filename =
|
|
319
|
+
filename = Path(fits_file).name
|
|
320
|
+
stem = Path(fits_file).stem
|
|
231
321
|
|
|
232
322
|
if dataset not in FILTER_DATABASE:
|
|
233
323
|
raise ValueError(
|
|
@@ -236,22 +326,93 @@ def identify_filter(fits_file, dataset):
|
|
|
236
326
|
)
|
|
237
327
|
|
|
238
328
|
# Check that the dataset appears in the filename
|
|
239
|
-
if
|
|
329
|
+
if not _has_word(stem, dataset):
|
|
240
330
|
raise ValueError(
|
|
241
|
-
f"Dataset '{dataset}' not found in filename
|
|
242
|
-
f"{
|
|
331
|
+
f"Dataset '{dataset}' not found in filename '{filename}'.\n"
|
|
332
|
+
f"{_naming_hint(dataset)}"
|
|
243
333
|
)
|
|
244
334
|
|
|
245
|
-
|
|
335
|
+
matches = _filters_in_name(stem, dataset)
|
|
246
336
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
337
|
+
if not matches:
|
|
338
|
+
raise ValueError(
|
|
339
|
+
f"Could not identify the {dataset} filter in filename "
|
|
340
|
+
f"'{filename}'.\n"
|
|
341
|
+
f"{_naming_hint(dataset)}"
|
|
342
|
+
)
|
|
250
343
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
344
|
+
if len(matches) > 1:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"Multiple {dataset} filters found in filename "
|
|
347
|
+
f"'{filename}': {matches}.\n"
|
|
348
|
+
f"{_naming_hint(dataset)}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
return matches[0]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def check_filenames(fits_files, dataset, verbose=True):
|
|
355
|
+
"""
|
|
356
|
+
Check which FITS filenames follow the naming convention expected by
|
|
357
|
+
``identify_filter``, before running the rest of the pipeline.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
fits_files : iterable of str or pathlib.Path
|
|
362
|
+
FITS filenames to check.
|
|
363
|
+
dataset : str
|
|
364
|
+
Dataset key in ``FILTER_DATABASE``. Matching is case-insensitive.
|
|
365
|
+
verbose : bool, optional
|
|
366
|
+
If True, print a report with the identified filter of each file and
|
|
367
|
+
the reason why the others were not identified.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
valid : list of tuple
|
|
372
|
+
``(fits_path, filter_name)`` for files whose filter was identified.
|
|
373
|
+
invalid : list of tuple
|
|
374
|
+
``(fits_path, reason)`` for files whose filter was not identified.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
dataset = dataset.upper()
|
|
378
|
+
|
|
379
|
+
if dataset not in FILTER_DATABASE:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f"Unknown dataset '{dataset}'. "
|
|
382
|
+
f"Available datasets: {list(FILTER_DATABASE)}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
valid = []
|
|
386
|
+
invalid = []
|
|
387
|
+
|
|
388
|
+
for fpath in fits_files:
|
|
389
|
+
try:
|
|
390
|
+
valid.append((fpath, identify_filter(fpath, dataset)))
|
|
391
|
+
except ValueError as err:
|
|
392
|
+
# Keep only the first line; the naming hint is printed once below
|
|
393
|
+
invalid.append((fpath, str(err).splitlines()[0]))
|
|
394
|
+
|
|
395
|
+
if verbose:
|
|
396
|
+
print(f"{dataset}: {len(valid)} file(s) identified, "
|
|
397
|
+
f"{len(invalid)} not identified.")
|
|
398
|
+
|
|
399
|
+
for fpath, filt in valid:
|
|
400
|
+
print(f" OK {filt:>8s} {Path(fpath).name}")
|
|
401
|
+
|
|
402
|
+
for fpath, reason in invalid:
|
|
403
|
+
print(f" FAIL {Path(fpath).name}: {reason}")
|
|
404
|
+
|
|
405
|
+
filters_found = [filt for _, filt in valid]
|
|
406
|
+
repeated = sorted(
|
|
407
|
+
{filt for filt in filters_found if filters_found.count(filt) > 1}
|
|
408
|
+
)
|
|
409
|
+
if repeated:
|
|
410
|
+
print(f" Warning: more than one file for filter(s) {repeated}.")
|
|
411
|
+
|
|
412
|
+
if invalid:
|
|
413
|
+
print(_naming_hint(dataset))
|
|
414
|
+
|
|
415
|
+
return valid, invalid
|
|
255
416
|
|
|
256
417
|
|
|
257
418
|
def get_filter_info(filter_name, dataset):
|
|
@@ -377,10 +538,16 @@ def add_custom_dataset(
|
|
|
377
538
|
"lambda": 5500.0,
|
|
378
539
|
"instrument": "TelescopeCam",
|
|
379
540
|
"group": "broad",
|
|
541
|
+
"aliases": ["FILTER1_ALT"], # optional
|
|
380
542
|
},
|
|
381
543
|
...
|
|
382
544
|
}
|
|
383
545
|
|
|
546
|
+
The optional ``aliases`` list contains alternative names under which
|
|
547
|
+
the filter may appear in filenames (see ``identify_filter``).
|
|
548
|
+
Filter names and aliases are matched case-insensitively, so they
|
|
549
|
+
must be unique within the dataset regardless of case.
|
|
550
|
+
|
|
384
551
|
overwrite : bool, optional
|
|
385
552
|
If True, overwrite existing dataset.
|
|
386
553
|
|
|
@@ -418,6 +585,26 @@ def add_custom_dataset(
|
|
|
418
585
|
f"Filter '{filt}': lambda must be numeric."
|
|
419
586
|
)
|
|
420
587
|
|
|
588
|
+
aliases = meta.get("aliases", [])
|
|
589
|
+
|
|
590
|
+
if isinstance(aliases, str) or not all(
|
|
591
|
+
isinstance(alias, str) for alias in aliases
|
|
592
|
+
):
|
|
593
|
+
raise TypeError(
|
|
594
|
+
f"Filter '{filt}': aliases must be a list of strings."
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
# Filenames are matched case-insensitively, so names must not collide
|
|
598
|
+
seen = {}
|
|
599
|
+
for filt, meta in filters.items():
|
|
600
|
+
for name in [filt, *meta.get("aliases", [])]:
|
|
601
|
+
if name.upper() in seen and seen[name.upper()] != filt:
|
|
602
|
+
raise ValueError(
|
|
603
|
+
f"Name '{name}' of filter '{filt}' collides with filter "
|
|
604
|
+
f"'{seen[name.upper()]}' (names are case-insensitive)."
|
|
605
|
+
)
|
|
606
|
+
seen[name.upper()] = filt
|
|
607
|
+
|
|
421
608
|
FILTER_DATABASE[dataset_name] = {
|
|
422
609
|
"filters": filters
|
|
423
610
|
}
|
|
@@ -429,6 +616,12 @@ def simplify_fits_filename(fits_file, dataset):
|
|
|
429
616
|
Create a simplified FITS filename containing the dataset
|
|
430
617
|
and filter name.
|
|
431
618
|
|
|
619
|
+
This is the recommended filename format for ``identify_filter``.
|
|
620
|
+
Unlike ``identify_filter``, the original filename does not need to
|
|
621
|
+
contain the dataset name, but the filter name (or one of its
|
|
622
|
+
``aliases``) must appear as a separate word, delimited by ``_``, ``-``
|
|
623
|
+
or ``.`` (case-insensitive).
|
|
624
|
+
|
|
432
625
|
Parameters
|
|
433
626
|
----------
|
|
434
627
|
fits_file : str or pathlib.Path
|
|
@@ -445,27 +638,27 @@ def simplify_fits_filename(fits_file, dataset):
|
|
|
445
638
|
fits_file = Path(fits_file)
|
|
446
639
|
dataset = dataset.upper()
|
|
447
640
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
641
|
+
if dataset not in FILTER_DATABASE:
|
|
642
|
+
raise ValueError(
|
|
643
|
+
f"Unknown dataset '{dataset}'. "
|
|
644
|
+
f"Available datasets: {list(FILTER_DATABASE)}"
|
|
645
|
+
)
|
|
451
646
|
|
|
452
|
-
matches =
|
|
453
|
-
filter_name
|
|
454
|
-
for filter_name in filters
|
|
455
|
-
if filter_name.lower() in filename
|
|
456
|
-
]
|
|
647
|
+
matches = _filters_in_name(fits_file.stem, dataset)
|
|
457
648
|
|
|
458
649
|
if not matches:
|
|
459
650
|
raise ValueError(
|
|
460
651
|
f"Could not identify a filter from filename "
|
|
461
|
-
f"'{fits_file.name}' for dataset '{dataset}'
|
|
652
|
+
f"'{fits_file.name}' for dataset '{dataset}'.\n"
|
|
653
|
+
f"{_naming_hint(dataset)}"
|
|
462
654
|
)
|
|
463
655
|
|
|
464
656
|
if len(matches) > 1:
|
|
465
657
|
raise ValueError(
|
|
466
658
|
f"Multiple filters found in filename "
|
|
467
659
|
f"'{fits_file.name}' for dataset '{dataset}': "
|
|
468
|
-
f"{matches}"
|
|
660
|
+
f"{matches}.\n"
|
|
661
|
+
f"{_naming_hint(dataset)}"
|
|
469
662
|
)
|
|
470
663
|
|
|
471
664
|
filter_name = matches[0]
|
|
@@ -824,3 +1017,184 @@ def get_pixel_scale_from_wcs(wcs):
|
|
|
824
1017
|
distortion.
|
|
825
1018
|
"""
|
|
826
1019
|
return proj_plane_pixel_scales(wcs)[0] * 3600.0
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def print_fits_spatial_info(fits_files):
|
|
1023
|
+
"""
|
|
1024
|
+
Print pixel scale and field of view for a set of FITS images.
|
|
1025
|
+
|
|
1026
|
+
The pixel scale is derived from the FITS WCS using the following
|
|
1027
|
+
hierarchy:
|
|
1028
|
+
|
|
1029
|
+
1. CD matrix
|
|
1030
|
+
2. PC matrix + CDELT
|
|
1031
|
+
3. PC matrix
|
|
1032
|
+
4. CDELT
|
|
1033
|
+
|
|
1034
|
+
Parameters
|
|
1035
|
+
----------
|
|
1036
|
+
fits_files : list of str or pathlib.Path
|
|
1037
|
+
FITS files to inspect.
|
|
1038
|
+
|
|
1039
|
+
Notes
|
|
1040
|
+
-----
|
|
1041
|
+
Pixel scales are reported in arcsec/pixel and FOVs in arcsec.
|
|
1042
|
+
"""
|
|
1043
|
+
|
|
1044
|
+
print(
|
|
1045
|
+
f"{'File':40} "
|
|
1046
|
+
f"{'PixScale X':>12} {'PixScale Y':>12} "
|
|
1047
|
+
f"{'FOV X':>12} {'FOV Y':>12}"
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
print(
|
|
1051
|
+
f"{'':40} "
|
|
1052
|
+
f"{'(arcsec/pix)':>12} {'(arcsec/pix)':>12} "
|
|
1053
|
+
f"{'(arcsec)':>12} {'(arcsec)':>12}"
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
print("-" * 92)
|
|
1057
|
+
|
|
1058
|
+
for fits_file in fits_files:
|
|
1059
|
+
|
|
1060
|
+
fits_file = Path(fits_file)
|
|
1061
|
+
|
|
1062
|
+
with fits.open(fits_file) as hdul:
|
|
1063
|
+
|
|
1064
|
+
ext = find_ext(hdul)
|
|
1065
|
+
|
|
1066
|
+
if ext is None:
|
|
1067
|
+
print(
|
|
1068
|
+
f"{fits_file.name:40} "
|
|
1069
|
+
f"{'No image extension':>12}"
|
|
1070
|
+
)
|
|
1071
|
+
continue
|
|
1072
|
+
|
|
1073
|
+
hdr = hdul[ext].header
|
|
1074
|
+
data = hdul[ext].data
|
|
1075
|
+
|
|
1076
|
+
if data is None or data.ndim < 2:
|
|
1077
|
+
print(
|
|
1078
|
+
f"{fits_file.name:40} "
|
|
1079
|
+
f"{'Invalid image':>12}"
|
|
1080
|
+
)
|
|
1081
|
+
continue
|
|
1082
|
+
|
|
1083
|
+
ny, nx = data.shape[-2:]
|
|
1084
|
+
|
|
1085
|
+
# CD matrix
|
|
1086
|
+
if all(
|
|
1087
|
+
k in hdr
|
|
1088
|
+
for k in [
|
|
1089
|
+
"CD1_1",
|
|
1090
|
+
"CD1_2",
|
|
1091
|
+
"CD2_1",
|
|
1092
|
+
"CD2_2",
|
|
1093
|
+
]
|
|
1094
|
+
):
|
|
1095
|
+
|
|
1096
|
+
pixscale_x = 3600 * np.sqrt(
|
|
1097
|
+
hdr["CD1_1"]**2
|
|
1098
|
+
+ hdr["CD2_1"]**2
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
pixscale_y = 3600 * np.sqrt(
|
|
1102
|
+
hdr["CD1_2"]**2
|
|
1103
|
+
+ hdr["CD2_2"]**2
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
# PC + CDELT
|
|
1107
|
+
elif (
|
|
1108
|
+
all(
|
|
1109
|
+
k in hdr
|
|
1110
|
+
for k in [
|
|
1111
|
+
"PC1_1",
|
|
1112
|
+
"PC1_2",
|
|
1113
|
+
"PC2_1",
|
|
1114
|
+
"PC2_2",
|
|
1115
|
+
]
|
|
1116
|
+
)
|
|
1117
|
+
and "CDELT1" in hdr
|
|
1118
|
+
and "CDELT2" in hdr
|
|
1119
|
+
):
|
|
1120
|
+
|
|
1121
|
+
cd11 = (
|
|
1122
|
+
hdr["PC1_1"]
|
|
1123
|
+
* hdr["CDELT1"]
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
cd12 = (
|
|
1127
|
+
hdr["PC1_2"]
|
|
1128
|
+
* hdr["CDELT1"]
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
cd21 = (
|
|
1132
|
+
hdr["PC2_1"]
|
|
1133
|
+
* hdr["CDELT2"]
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
cd22 = (
|
|
1137
|
+
hdr["PC2_2"]
|
|
1138
|
+
* hdr["CDELT2"]
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
pixscale_x = 3600 * np.sqrt(
|
|
1142
|
+
cd11**2 + cd21**2
|
|
1143
|
+
)
|
|
1144
|
+
|
|
1145
|
+
pixscale_y = 3600 * np.sqrt(
|
|
1146
|
+
cd12**2 + cd22**2
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
# PC only
|
|
1150
|
+
elif all(
|
|
1151
|
+
k in hdr
|
|
1152
|
+
for k in [
|
|
1153
|
+
"PC1_1",
|
|
1154
|
+
"PC1_2",
|
|
1155
|
+
"PC2_1",
|
|
1156
|
+
"PC2_2",
|
|
1157
|
+
]
|
|
1158
|
+
):
|
|
1159
|
+
|
|
1160
|
+
pixscale_x = 3600 * np.sqrt(
|
|
1161
|
+
hdr["PC1_1"]**2
|
|
1162
|
+
+ hdr["PC2_1"]**2
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
pixscale_y = 3600 * np.sqrt(
|
|
1166
|
+
hdr["PC1_2"]**2
|
|
1167
|
+
+ hdr["PC2_2"]**2
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
# CDELT
|
|
1171
|
+
elif (
|
|
1172
|
+
"CDELT1" in hdr
|
|
1173
|
+
and "CDELT2" in hdr
|
|
1174
|
+
):
|
|
1175
|
+
|
|
1176
|
+
pixscale_x = (
|
|
1177
|
+
abs(hdr["CDELT1"]) * 3600
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
pixscale_y = (
|
|
1181
|
+
abs(hdr["CDELT2"]) * 3600
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
# No valid WCS scale
|
|
1185
|
+
else:
|
|
1186
|
+
|
|
1187
|
+
pixscale_x = np.nan
|
|
1188
|
+
pixscale_y = np.nan
|
|
1189
|
+
|
|
1190
|
+
# FOV
|
|
1191
|
+
fov_x = nx * pixscale_x
|
|
1192
|
+
fov_y = ny * pixscale_y
|
|
1193
|
+
|
|
1194
|
+
print(
|
|
1195
|
+
f"{fits_file.name:40} "
|
|
1196
|
+
f"{pixscale_x:12.4f} "
|
|
1197
|
+
f"{pixscale_y:12.4f} "
|
|
1198
|
+
f"{fov_x:12.1f} "
|
|
1199
|
+
f"{fov_y:12.1f}"
|
|
1200
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lightstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -36,13 +36,13 @@ Requires-Dist: scipy
|
|
|
36
36
|
Requires-Dist: photutils
|
|
37
37
|
Requires-Dist: pandas
|
|
38
38
|
|
|
39
|
-
[](https://doi.org/10.5281/zenodo.
|
|
39
|
+
[](https://doi.org/10.5281/zenodo.22148981)
|
|
40
40
|
|
|
41
41
|
<img src="https://raw.githubusercontent.com/AndressaWille/lightstack/master/images/logo.png" width="120">
|
|
42
42
|
|
|
43
43
|
# Lightstack
|
|
44
44
|
|
|
45
|
-
Tools for building and processing multi-filter astrophysical datacubes.
|
|
45
|
+
Tools for building and processing multi-filter (and multi-servey) astrophysical datacubes.
|
|
46
46
|
|
|
47
47
|
## Features
|
|
48
48
|
|
|
@@ -60,4 +60,4 @@ pip install lightstack
|
|
|
60
60
|
|
|
61
61
|
https://lightstack.readthedocs.io/en/latest/
|
|
62
62
|
|
|
63
|
-
See the tutorial
|
|
63
|
+
See the tutorial notebooks (in the "examples" folder)!
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|