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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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}'")