lightstack 0.1.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.0/PKG-INFO +12 -0
- lightstack-0.1.0/README.md +18 -0
- lightstack-0.1.0/pyproject.toml +22 -0
- lightstack-0.1.0/setup.cfg +4 -0
- lightstack-0.1.0/src/lightstack/__init__.py +104 -0
- lightstack-0.1.0/src/lightstack/crop.py +310 -0
- lightstack-0.1.0/src/lightstack/datacube.py +493 -0
- lightstack-0.1.0/src/lightstack/plot.py +310 -0
- lightstack-0.1.0/src/lightstack/psf.py +336 -0
- lightstack-0.1.0/src/lightstack/utils.py +223 -0
- lightstack-0.1.0/src/lightstack.egg-info/PKG-INFO +12 -0
- lightstack-0.1.0/src/lightstack.egg-info/SOURCES.txt +13 -0
- lightstack-0.1.0/src/lightstack.egg-info/dependency_links.txt +1 -0
- lightstack-0.1.0/src/lightstack.egg-info/requires.txt +7 -0
- lightstack-0.1.0/src/lightstack.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: lightstack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tools for building and processing multi-filter astrophysical datacubes
|
|
5
|
+
Author: Andressa Wille, Thallis Pessi
|
|
6
|
+
Requires-Dist: numpy
|
|
7
|
+
Requires-Dist: astropy
|
|
8
|
+
Requires-Dist: matplotlib
|
|
9
|
+
Requires-Dist: reproject
|
|
10
|
+
Requires-Dist: regions
|
|
11
|
+
Requires-Dist: scipy
|
|
12
|
+
Requires-Dist: photutils
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<img src="images/logo.png" width="120">
|
|
2
|
+
|
|
3
|
+
# Lightstack
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
This is a code with functions for:
|
|
7
|
+
- cropping FITS images
|
|
8
|
+
- building photometric datacubes
|
|
9
|
+
- PSF matching
|
|
10
|
+
|
|
11
|
+
(only JWST and HST for now)
|
|
12
|
+
|
|
13
|
+
pip install git+https://github.com/AndressaWille/lightstack.git
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Documentation: https://lightstack.readthedocs.io/en/latest/
|
|
17
|
+
|
|
18
|
+
See the tutorial notebook!
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "lightstack"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Tools for building and processing multi-filter astrophysical datacubes"
|
|
5
|
+
authors = [{name = "Andressa Wille"}, {name="Thallis Pessi"}]
|
|
6
|
+
dependencies = [
|
|
7
|
+
"numpy",
|
|
8
|
+
"astropy",
|
|
9
|
+
"matplotlib",
|
|
10
|
+
"reproject",
|
|
11
|
+
"regions",
|
|
12
|
+
"scipy",
|
|
13
|
+
"photutils"
|
|
14
|
+
]
|
|
15
|
+
license = {file = "LICENSE"}
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["setuptools", "wheel"]
|
|
19
|
+
build-backend = "setuptools.build_meta"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightstack: Tools for processing, PSF matching, and visualization
|
|
3
|
+
of multi-filter photometric datacubes using astronomical imaging (HST and JWST).
|
|
4
|
+
|
|
5
|
+
Main functionalities include:
|
|
6
|
+
- Crop regions
|
|
7
|
+
- Image alignment and photometric datacube construction
|
|
8
|
+
- PSF matching using convolution kernels
|
|
9
|
+
- Visualization of FITS images and datacubes
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# =========================
|
|
13
|
+
# Cropping
|
|
14
|
+
# =========================
|
|
15
|
+
from .crop import (
|
|
16
|
+
get_sky_bbox_from_cutout,
|
|
17
|
+
crop_from_radec,
|
|
18
|
+
crop_using_reference,
|
|
19
|
+
crop_reg,
|
|
20
|
+
cut_region_2d,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# =========================
|
|
25
|
+
# Datacube / alignment
|
|
26
|
+
# =========================
|
|
27
|
+
from .datacube import (
|
|
28
|
+
align_reproject_fits,
|
|
29
|
+
build_datacube,
|
|
30
|
+
cut_region_datacube,
|
|
31
|
+
build_valid_datacube,
|
|
32
|
+
remove_filter,
|
|
33
|
+
update_cube_header,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# =========================
|
|
37
|
+
# PSF matching
|
|
38
|
+
# =========================
|
|
39
|
+
from .psf import (
|
|
40
|
+
build_kernel,
|
|
41
|
+
save_kernel,
|
|
42
|
+
apply_kernel,
|
|
43
|
+
psf_match_datacube,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# =========================
|
|
47
|
+
# Visualization
|
|
48
|
+
# =========================
|
|
49
|
+
from .plot import (
|
|
50
|
+
visualize_fits,
|
|
51
|
+
plot_datacube_filters,
|
|
52
|
+
plot_psf_grid,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# =========================
|
|
56
|
+
# Utils
|
|
57
|
+
# =========================
|
|
58
|
+
from .utils import (
|
|
59
|
+
find_ext,
|
|
60
|
+
infer_filter,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# =========================
|
|
64
|
+
# Public API
|
|
65
|
+
# =========================
|
|
66
|
+
__all__ = [
|
|
67
|
+
# crop
|
|
68
|
+
"get_sky_bbox_from_cutout",
|
|
69
|
+
"crop_from_radec",
|
|
70
|
+
"crop_using_reference",
|
|
71
|
+
"crop_reg",
|
|
72
|
+
"cut_region_2d",
|
|
73
|
+
|
|
74
|
+
# datacube
|
|
75
|
+
"align_reproject_fits",
|
|
76
|
+
"build_datacube",
|
|
77
|
+
"cut_region_datacube",
|
|
78
|
+
"build_valid_datacube",
|
|
79
|
+
"remove_filter",
|
|
80
|
+
"update_cube_header",
|
|
81
|
+
|
|
82
|
+
# psf
|
|
83
|
+
"build_kernel",
|
|
84
|
+
"save_kernel",
|
|
85
|
+
"apply_kernel",
|
|
86
|
+
"psf_match_datacube",
|
|
87
|
+
|
|
88
|
+
# plot
|
|
89
|
+
"visualize_fits",
|
|
90
|
+
"plot_datacube_filters",
|
|
91
|
+
"plot_psf_grid",
|
|
92
|
+
|
|
93
|
+
# utils
|
|
94
|
+
"find_ext",
|
|
95
|
+
"infer_filter",
|
|
96
|
+
"pick_folder",
|
|
97
|
+
"get_filter",
|
|
98
|
+
"filter_id",
|
|
99
|
+
"save_fits",
|
|
100
|
+
"sort_fits",
|
|
101
|
+
"MJy_sr_to_jy",
|
|
102
|
+
"get_pixel_scale",
|
|
103
|
+
|
|
104
|
+
]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from astropy.io import fits
|
|
4
|
+
from astropy.wcs import WCS
|
|
5
|
+
from astropy.wcs.utils import proj_plane_pixel_scales, skycoord_to_pixel
|
|
6
|
+
from astropy.coordinates import SkyCoord
|
|
7
|
+
import astropy.units as u
|
|
8
|
+
|
|
9
|
+
from .utils import find_ext
|
|
10
|
+
|
|
11
|
+
def get_sky_bbox_from_cutout(fits_cut):
|
|
12
|
+
"""
|
|
13
|
+
Get sky bounding box (RA, Dec) from a FITS cutout.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
fits_cut : str
|
|
18
|
+
Path to FITS file
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
ra, dec : arrays
|
|
23
|
+
RA and Dec of the four corners
|
|
24
|
+
"""
|
|
25
|
+
# Read FITS
|
|
26
|
+
with fits.open(fits_cut) as hdul:
|
|
27
|
+
ext = find_ext(hdul)
|
|
28
|
+
if ext is None:
|
|
29
|
+
raise ValueError(f"No 2D image found in {fits_cut}")
|
|
30
|
+
|
|
31
|
+
data = hdul[ext].data
|
|
32
|
+
header = hdul[ext].header
|
|
33
|
+
|
|
34
|
+
wcs = WCS(header)
|
|
35
|
+
|
|
36
|
+
ny, nx = data.shape
|
|
37
|
+
|
|
38
|
+
# Define pixel coordinates of the four corners
|
|
39
|
+
corners_pix = np.array([
|
|
40
|
+
[0, 0],
|
|
41
|
+
[nx-1, 0],
|
|
42
|
+
[0, ny-1],
|
|
43
|
+
[nx-1, ny-1]])
|
|
44
|
+
|
|
45
|
+
# Convert pixel coordinates to sky (RA, Dec)
|
|
46
|
+
ra, dec = wcs.wcs_pix2world(
|
|
47
|
+
corners_pix[:, 0],
|
|
48
|
+
corners_pix[:, 1],
|
|
49
|
+
0 # origin = 0 (Python convention)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return ra, dec
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def crop_from_radec(fits_path, ra, dec, size_arcsec, output_path=None):
|
|
56
|
+
"""
|
|
57
|
+
Extract a square cutout from a FITS image centered on a given sky position.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
fits_path : str
|
|
62
|
+
Path to the FITS image.
|
|
63
|
+
|
|
64
|
+
ra : float
|
|
65
|
+
Right Ascension of the center (in degrees).
|
|
66
|
+
|
|
67
|
+
dec : float
|
|
68
|
+
Declination of the center (in degrees).
|
|
69
|
+
|
|
70
|
+
size_arcsec : float
|
|
71
|
+
Size of the cutout (in arcseconds). The cutout is square.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
data_cut : 2D numpy array
|
|
76
|
+
Cropped image data.
|
|
77
|
+
|
|
78
|
+
header_cut : FITS header
|
|
79
|
+
Updated header with corrected WCS reference (CRPIX).
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
# Open FITS
|
|
83
|
+
with fits.open(fits_path) as hdul:
|
|
84
|
+
ext = find_ext(hdul)
|
|
85
|
+
if ext is None:
|
|
86
|
+
raise ValueError(f"No 2D image data found in {fits_path}")
|
|
87
|
+
|
|
88
|
+
data = hdul[ext].data
|
|
89
|
+
header = hdul[ext].header
|
|
90
|
+
wcs = WCS(header)
|
|
91
|
+
|
|
92
|
+
# Convert sky coordinates (RA, Dec) to pixel coordinates
|
|
93
|
+
coord = SkyCoord(ra=ra * u.deg, dec=dec * u.deg)
|
|
94
|
+
x_center, y_center = skycoord_to_pixel(coord, wcs)
|
|
95
|
+
|
|
96
|
+
# Get pixel scale (arcsec/pixel)
|
|
97
|
+
pixel_scales = proj_plane_pixel_scales(wcs) * 3600.0 # deg → arcsec
|
|
98
|
+
|
|
99
|
+
pixscale = np.mean(pixel_scales)
|
|
100
|
+
|
|
101
|
+
# Convert size from arcsec to pixels
|
|
102
|
+
half_size_pix = (size_arcsec / pixscale) / 2.0
|
|
103
|
+
|
|
104
|
+
# Define integer pixel bounds
|
|
105
|
+
x_min = int(np.floor(x_center - half_size_pix))
|
|
106
|
+
x_max = int(np.ceil(x_center + half_size_pix))
|
|
107
|
+
y_min = int(np.floor(y_center - half_size_pix))
|
|
108
|
+
y_max = int(np.ceil(y_center + half_size_pix))
|
|
109
|
+
|
|
110
|
+
ny, nx = data.shape
|
|
111
|
+
x_min = max(0, x_min)
|
|
112
|
+
x_max = min(nx, x_max)
|
|
113
|
+
y_min = max(0, y_min)
|
|
114
|
+
y_max = min(ny, y_max)
|
|
115
|
+
|
|
116
|
+
# Extract cutout
|
|
117
|
+
data_cut = data[y_min:y_max, x_min:x_max]
|
|
118
|
+
|
|
119
|
+
# Update WCS
|
|
120
|
+
header_cut = header.copy()
|
|
121
|
+
header_cut['CRPIX1'] -= x_min
|
|
122
|
+
header_cut['CRPIX2'] -= y_min
|
|
123
|
+
|
|
124
|
+
if output_path is not None:
|
|
125
|
+
fits.PrimaryHDU(data_cut, header=header_cut).writeto(
|
|
126
|
+
output_path,
|
|
127
|
+
overwrite=True)
|
|
128
|
+
|
|
129
|
+
return data_cut, header_cut
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def crop_using_reference(fits_path, ref_cutout, output_path=None):
|
|
133
|
+
"""
|
|
134
|
+
Crop a FITS image using the sky footprint of a reference cutout.
|
|
135
|
+
|
|
136
|
+
This function extracts a region from a target image such that it matches
|
|
137
|
+
the sky coverage (RA/Dec bounding box) of a reference FITS cutout.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
fits_path : str
|
|
142
|
+
Path to the target FITS image to be cropped.
|
|
143
|
+
|
|
144
|
+
ref_cutout : str
|
|
145
|
+
Path to the reference FITS cutout defining the sky region.
|
|
146
|
+
|
|
147
|
+
output_path : str, optional
|
|
148
|
+
If provided, saves the cropped FITS to this path.
|
|
149
|
+
|
|
150
|
+
Returns
|
|
151
|
+
-------
|
|
152
|
+
data_cut : 2D numpy array
|
|
153
|
+
Cropped image data.
|
|
154
|
+
|
|
155
|
+
header_cut : FITS header
|
|
156
|
+
Updated header with corrected WCS reference (CRPIX).
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
# Get RA/Dec bounding box from reference cutout
|
|
160
|
+
ra, dec = get_sky_bbox_from_cutout(ref_cutout)
|
|
161
|
+
|
|
162
|
+
# Open FITS
|
|
163
|
+
with fits.open(fits_path) as hdul:
|
|
164
|
+
ext = find_ext(hdul)
|
|
165
|
+
if ext is None:
|
|
166
|
+
raise ValueError(f"No 2D image found in {fits_path}")
|
|
167
|
+
|
|
168
|
+
data = hdul[ext].data
|
|
169
|
+
header = hdul[ext].header
|
|
170
|
+
wcs = WCS(header)
|
|
171
|
+
|
|
172
|
+
# Convert sky coordinates (RA/Dec) to pixel coordinates
|
|
173
|
+
x, y = wcs.wcs_world2pix(ra, dec, 0)
|
|
174
|
+
|
|
175
|
+
# Define bounding box in pixel space
|
|
176
|
+
x_min = int(np.floor(np.min(x)))
|
|
177
|
+
x_max = int(np.ceil(np.max(x)))
|
|
178
|
+
y_min = int(np.floor(np.min(y)))
|
|
179
|
+
y_max = int(np.ceil(np.max(y)))
|
|
180
|
+
|
|
181
|
+
ny, nx = data.shape
|
|
182
|
+
x_min = max(0, x_min)
|
|
183
|
+
x_max = min(nx, x_max)
|
|
184
|
+
y_min = max(0, y_min)
|
|
185
|
+
y_max = min(ny, y_max)
|
|
186
|
+
|
|
187
|
+
# Extract cutout
|
|
188
|
+
data_cut = data[y_min:y_max, x_min:x_max]
|
|
189
|
+
|
|
190
|
+
# Update WCS
|
|
191
|
+
header_cut = header.copy()
|
|
192
|
+
header_cut['CRPIX1'] -= x_min
|
|
193
|
+
header_cut['CRPIX2'] -= y_min
|
|
194
|
+
|
|
195
|
+
# Save
|
|
196
|
+
if output_path is not None:
|
|
197
|
+
fits.PrimaryHDU(data_cut, header=header_cut).writeto(
|
|
198
|
+
output_path,
|
|
199
|
+
overwrite=True)
|
|
200
|
+
|
|
201
|
+
return data_cut, header_cut
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def crop_reg(fits_path, region):
|
|
205
|
+
"""
|
|
206
|
+
Crops a FITS image using a DS9 region.
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
fits_path : str
|
|
211
|
+
Path to the FITS file.
|
|
212
|
+
region : regions.Region
|
|
213
|
+
Region object read from a DS9 region file.
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
data_cut : numpy.ndarray
|
|
218
|
+
Cropped image data.
|
|
219
|
+
header_cut : astropy.io.fits.Header
|
|
220
|
+
Updated FITS header with adjusted CRPIX keywords.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
ValueError
|
|
225
|
+
If no image data extension is found in the FITS file.
|
|
226
|
+
"""
|
|
227
|
+
# Open fits
|
|
228
|
+
with fits.open(fits_path) as hdul:
|
|
229
|
+
ext = find_ext(hdul)
|
|
230
|
+
if ext is None:
|
|
231
|
+
raise ValueError(f"No image data found in {fits_path}")
|
|
232
|
+
|
|
233
|
+
data = hdul[ext].data
|
|
234
|
+
header = hdul[ext].header
|
|
235
|
+
wcs = WCS(header)
|
|
236
|
+
|
|
237
|
+
# Define region limits
|
|
238
|
+
pix_region = region.to_pixel(wcs)
|
|
239
|
+
bbox = pix_region.bounding_box
|
|
240
|
+
|
|
241
|
+
x_min, x_max = int(bbox.ixmin), int(bbox.ixmax)
|
|
242
|
+
y_min, y_max = int(bbox.iymin), int(bbox.iymax)
|
|
243
|
+
|
|
244
|
+
# Crop the image
|
|
245
|
+
data_cut = data[y_min:y_max, x_min:x_max]
|
|
246
|
+
|
|
247
|
+
# Update reference pixel in the header
|
|
248
|
+
header_cut = header.copy()
|
|
249
|
+
header_cut['CRPIX1'] -= x_min
|
|
250
|
+
header_cut['CRPIX2'] -= y_min
|
|
251
|
+
|
|
252
|
+
return data_cut, header_cut
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def cut_region_2d(fits_file, x_start, x_end, y_start, y_end, output_path):
|
|
256
|
+
"""
|
|
257
|
+
Cuts a spatial region from a 2D fits image.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
fits_file : str
|
|
262
|
+
Path to the input 2D fits image.
|
|
263
|
+
x_start, x_end : int
|
|
264
|
+
Pixel indices for the x axis.
|
|
265
|
+
y_start, y_end : int
|
|
266
|
+
Pixel indices for the y axis.
|
|
267
|
+
output_path : str
|
|
268
|
+
Path to the output fits file.
|
|
269
|
+
|
|
270
|
+
Returns
|
|
271
|
+
-------
|
|
272
|
+
None
|
|
273
|
+
Saves the cut fits image.
|
|
274
|
+
"""
|
|
275
|
+
# Open FITS
|
|
276
|
+
with fits.open(fits_file) as hdul:
|
|
277
|
+
ext = find_ext(hdul)
|
|
278
|
+
if ext is None:
|
|
279
|
+
raise ValueError(f"No image data found in {fits_path}")
|
|
280
|
+
|
|
281
|
+
data = hdul[ext].data
|
|
282
|
+
header = hdul[ext].header
|
|
283
|
+
|
|
284
|
+
# Cut the data
|
|
285
|
+
cut_data = data[y_start:y_end, x_start:x_end]
|
|
286
|
+
|
|
287
|
+
# Update WCS
|
|
288
|
+
wcs_2d = WCS(header)
|
|
289
|
+
wcs_2d.wcs.crpix[0] -= x_start
|
|
290
|
+
wcs_2d.wcs.crpix[1] -= y_start
|
|
291
|
+
|
|
292
|
+
# Build new header
|
|
293
|
+
new_header = wcs_2d.to_header()
|
|
294
|
+
new_header['NAXIS'] = 2
|
|
295
|
+
new_header['NAXIS1'] = x_end - x_start
|
|
296
|
+
new_header['NAXIS2'] = y_end - y_start
|
|
297
|
+
|
|
298
|
+
# Preserve unit if exists
|
|
299
|
+
if 'BUNIT' in header:
|
|
300
|
+
new_header['BUNIT'] = header['BUNIT']
|
|
301
|
+
|
|
302
|
+
# Optional: store crop info
|
|
303
|
+
new_header['XMINPIX'] = x_start
|
|
304
|
+
new_header['XMAXPIX'] = x_end
|
|
305
|
+
new_header['YMINPIX'] = y_start
|
|
306
|
+
new_header['YMAXPIX'] = y_end
|
|
307
|
+
|
|
308
|
+
# Save cut image
|
|
309
|
+
fits.PrimaryHDU(cut_data, header=new_header).writeto(output_path, overwrite=True)
|
|
310
|
+
print(f"Cut 2D fits saved to '{output_path}'")
|