slide-lsst 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.
- slide_lsst/__init__.py +41 -0
- slide_lsst/_version.py +4 -0
- slide_lsst/image_processing.py +344 -0
- slide_lsst/lsst_utils.py +312 -0
- slide_lsst/reference_download.py +330 -0
- slide_lsst/subtraction.py +347 -0
- slide_lsst-0.1.0.dist-info/LICENSE +21 -0
- slide_lsst-0.1.0.dist-info/METADATA +101 -0
- slide_lsst-0.1.0.dist-info/RECORD +11 -0
- slide_lsst-0.1.0.dist-info/WHEEL +5 -0
- slide_lsst-0.1.0.dist-info/top_level.txt +1 -0
slide_lsst/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SLIDE: Subtracting LSST Images with DECam Exposures
|
|
3
|
+
|
|
4
|
+
This package provides tools for performing image subtraction on LSST images using DECam templates,
|
|
5
|
+
including PSF modeling, reference image downloading, and image subtraction.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "Yize Dong"
|
|
10
|
+
|
|
11
|
+
from .lsst_utils import query_lsst_visits, lsst_visit_to_ccddata, lsst_visit_to_psf
|
|
12
|
+
from .image_processing import (
|
|
13
|
+
read_with_datasec,
|
|
14
|
+
get_ccd_bbox,
|
|
15
|
+
make_psf,
|
|
16
|
+
refine_wcs,
|
|
17
|
+
assemble_reference
|
|
18
|
+
)
|
|
19
|
+
from .reference_download import (
|
|
20
|
+
download_des_reference,
|
|
21
|
+
download_decals_reference,
|
|
22
|
+
gaia3cat,
|
|
23
|
+
)
|
|
24
|
+
from .subtraction import perform_image_subtraction, lsst_decam_data_load, load_usesr_decam
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
'gaia3cat',
|
|
28
|
+
'query_lsst_visits',
|
|
29
|
+
'read_with_datasec',
|
|
30
|
+
'get_ccd_bbox',
|
|
31
|
+
'make_psf',
|
|
32
|
+
'refine_wcs',
|
|
33
|
+
'assemble_reference',
|
|
34
|
+
'download_des_reference',
|
|
35
|
+
'download_decals_reference',
|
|
36
|
+
'lsst_visit_to_ccddata',
|
|
37
|
+
'lsst_visit_to_psf',
|
|
38
|
+
'perform_image_subtraction',
|
|
39
|
+
'lsst_decam_data_load',
|
|
40
|
+
'load_usesr_decam'
|
|
41
|
+
]
|
slide_lsst/_version.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image processing utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from reproject import reproject_interp, reproject_adaptive
|
|
8
|
+
from astropy.io import fits
|
|
9
|
+
from astropy.nddata import CCDData, NDData
|
|
10
|
+
from astropy.wcs import WCS, _wcs
|
|
11
|
+
from astropy.visualization import PercentileInterval, ImageNormalize
|
|
12
|
+
from photutils.psf import EPSFBuilder, extract_stars
|
|
13
|
+
from astropy.coordinates import SkyCoord
|
|
14
|
+
from astropy.wcs.utils import skycoord_to_pixel
|
|
15
|
+
import scipy.optimize
|
|
16
|
+
import warnings
|
|
17
|
+
from astropy.wcs.utils import fit_wcs_from_points
|
|
18
|
+
import sep
|
|
19
|
+
from .lsst_utils import astropy_world_to_pixel, lsst_world_to_pixel, lsst_pixel_to_world
|
|
20
|
+
|
|
21
|
+
def read_with_datasec(filename, hdu=0):
|
|
22
|
+
"""
|
|
23
|
+
Read FITS file with optional DATASEC handling.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
filename : str
|
|
28
|
+
Path to FITS file
|
|
29
|
+
hdu : int, optional
|
|
30
|
+
HDU index to read (default: 0)
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
CCDData
|
|
35
|
+
Image data with WCS information
|
|
36
|
+
"""
|
|
37
|
+
ccddata = CCDData.read(filename, format='fits', unit='adu', hdu=hdu)
|
|
38
|
+
if 'datasec' in ccddata.meta:
|
|
39
|
+
jmin, jmax, imin, imax = eval(ccddata.meta['datasec'].replace(':', ','))
|
|
40
|
+
ccddata = ccddata[imin-1:imax, jmin-1:jmax]
|
|
41
|
+
return ccddata
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_ccd_bbox(ccddata):
|
|
45
|
+
"""
|
|
46
|
+
Get bounding box coordinates for a CCD image.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
ccddata : CCDData
|
|
51
|
+
Image data with WCS
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
tuple
|
|
56
|
+
(ra_min, dec_min, ra_max, dec_max) in degrees
|
|
57
|
+
"""
|
|
58
|
+
corners = [[0.], [0.5], [1.]] * np.array(ccddata.shape)[::-1]
|
|
59
|
+
(ra_min, dec_min), (ra_ctr, dec_ctr), (ra_max, dec_max) = ccddata.wcs.all_pix2world(corners, 0.)
|
|
60
|
+
if ra_min > ra_max:
|
|
61
|
+
ra_min, ra_max = ra_max, ra_min
|
|
62
|
+
if dec_min > dec_max:
|
|
63
|
+
dec_min, dec_max = dec_max, dec_min
|
|
64
|
+
max_size_dec = 0.199
|
|
65
|
+
if dec_max - dec_min > max_size_dec:
|
|
66
|
+
dec_min = dec_ctr - max_size_dec / 2.
|
|
67
|
+
dec_max = dec_ctr + max_size_dec / 2.
|
|
68
|
+
return ra_min, dec_min, ra_max, dec_max
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def make_psf(data, catalog, show=False, boxsize=25, oversampling=1, center_accuracy=0.001):
|
|
72
|
+
"""
|
|
73
|
+
Create PSF model from catalog stars.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
data : CCDData
|
|
78
|
+
Image data
|
|
79
|
+
catalog : astropy.table.Table
|
|
80
|
+
Star catalog with ra, dec columns
|
|
81
|
+
show : bool, optional
|
|
82
|
+
Show PSF visualization (default: False)
|
|
83
|
+
boxsize : int, optional
|
|
84
|
+
Size of star cutouts (default: 25)
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
tuple
|
|
89
|
+
(epsf, fitted_stars)
|
|
90
|
+
"""
|
|
91
|
+
_catalog = catalog.copy()
|
|
92
|
+
coords = SkyCoord(_catalog['ra'], _catalog['dec'], unit='deg')
|
|
93
|
+
_catalog['x'], _catalog['y'] = skycoord_to_pixel(coords, data.wcs, origin=0)
|
|
94
|
+
|
|
95
|
+
good_mask = []
|
|
96
|
+
for x, y in zip(_catalog['x'], _catalog['y']):
|
|
97
|
+
x = int(np.round(x))
|
|
98
|
+
y = int(np.round(y))
|
|
99
|
+
# If any pixel in the cutout is masked, reject it
|
|
100
|
+
if data.mask is not None and np.any(data.mask[y-1:y+1,
|
|
101
|
+
x-1:x+1]):
|
|
102
|
+
good_mask.append(False)
|
|
103
|
+
else:
|
|
104
|
+
good_mask.append(True)
|
|
105
|
+
_catalog = _catalog[good_mask]
|
|
106
|
+
|
|
107
|
+
bkg = np.nanmedian(data.data)
|
|
108
|
+
nddata = NDData(data.data - bkg)
|
|
109
|
+
|
|
110
|
+
stars = extract_stars(nddata, _catalog, size=boxsize)
|
|
111
|
+
epsf_builder = EPSFBuilder(oversampling=oversampling, center_accuracy=center_accuracy)
|
|
112
|
+
epsf, fitted_stars = epsf_builder(stars)
|
|
113
|
+
|
|
114
|
+
if show:
|
|
115
|
+
plt.figure()
|
|
116
|
+
plt.imshow(epsf.data)
|
|
117
|
+
plot_stars(fitted_stars)
|
|
118
|
+
|
|
119
|
+
return epsf, fitted_stars
|
|
120
|
+
|
|
121
|
+
def find_catalog_stars(image, wcs, catalog):
|
|
122
|
+
try:
|
|
123
|
+
data = image.astype(np.float32)
|
|
124
|
+
except:
|
|
125
|
+
data = image.byteswap().newbyteorder()
|
|
126
|
+
bkg = sep.Background(data)
|
|
127
|
+
data_sub = data - bkg
|
|
128
|
+
|
|
129
|
+
# 2. Extract all sources
|
|
130
|
+
sources = sep.extract(data_sub, thresh=3, err=bkg.globalrms)
|
|
131
|
+
|
|
132
|
+
known_x, known_y = astropy_world_to_pixel(catalog['ra'], catalog['dec'], wcs)
|
|
133
|
+
|
|
134
|
+
matched_sources = []
|
|
135
|
+
for x0, y0 in zip(known_x, known_y):
|
|
136
|
+
distances = np.hypot(sources['x'] - x0, sources['y'] - y0)
|
|
137
|
+
closest_idx = np.argmin(distances)
|
|
138
|
+
matched_sources.append(sources[closest_idx])
|
|
139
|
+
|
|
140
|
+
matched_sources = np.array(matched_sources)
|
|
141
|
+
return matched_sources
|
|
142
|
+
|
|
143
|
+
def plot_stars(stars):
|
|
144
|
+
"""
|
|
145
|
+
Plot extracted stars for visualization.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
stars : list
|
|
150
|
+
List of star cutouts
|
|
151
|
+
"""
|
|
152
|
+
nrows = int(np.ceil(len(stars) ** 0.5))
|
|
153
|
+
fig, axarr = plt.subplots(nrows, nrows, figsize=(20, 20), squeeze=True)
|
|
154
|
+
for ax, star in zip(axarr.ravel(), stars):
|
|
155
|
+
ax.imshow(star)
|
|
156
|
+
ax.plot(star.cutout_center[0], star.cutout_center[1], 'r+')
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def update_wcs(wcs, p):
|
|
160
|
+
"""
|
|
161
|
+
Update WCS with transformation parameters.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
wcs : WCS
|
|
166
|
+
WCS object to update
|
|
167
|
+
p : array-like
|
|
168
|
+
Transformation parameters [dx, dy, rotation, scale]
|
|
169
|
+
"""
|
|
170
|
+
wcs.wcs.crval += p[:2]
|
|
171
|
+
c, s = np.cos(p[2]), np.sin(p[2])
|
|
172
|
+
if wcs.wcs.has_cd():
|
|
173
|
+
wcs.wcs.cd = wcs.wcs.cd @ np.array([[c, -s], [s, c]]) * p[3]
|
|
174
|
+
if wcs.wcs.has_pc():
|
|
175
|
+
wcs.wcs.pc = wcs.wcs.pc @ np.array([[c, -s], [s, c]]) * p[3]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def wcs_offset(p, radec, xy, origwcs):
|
|
180
|
+
"""
|
|
181
|
+
Calculate RMS offset between catalog and detected positions.
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
p : array-like
|
|
186
|
+
Transformation parameters
|
|
187
|
+
radec : array-like
|
|
188
|
+
Catalog coordinates
|
|
189
|
+
xy : array-like
|
|
190
|
+
Detected pixel coordinates
|
|
191
|
+
origwcs : WCS
|
|
192
|
+
Original WCS
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
float
|
|
197
|
+
RMS offset in pixels
|
|
198
|
+
"""
|
|
199
|
+
wcs = origwcs.deepcopy()
|
|
200
|
+
update_wcs(wcs, p)
|
|
201
|
+
sky = SkyCoord(radec[:, 0], radec[:, 1], unit='deg')
|
|
202
|
+
test_xy = np.column_stack(skycoord_to_pixel(sky, wcs))
|
|
203
|
+
rms = (np.sum((test_xy - xy)**2) / len(radec))**0.5
|
|
204
|
+
return rms
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def refine_wcs(wcs, stars, catalog, use_sep=False):
|
|
208
|
+
"""
|
|
209
|
+
Refine WCS using star positions.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
wcs : WCS
|
|
214
|
+
WCS to refine
|
|
215
|
+
stars : object
|
|
216
|
+
Detected stars
|
|
217
|
+
catalog : astropy.table.Table
|
|
218
|
+
Reference catalog
|
|
219
|
+
use_sep : bool, optional
|
|
220
|
+
Use SEP detection format (default: False)
|
|
221
|
+
"""
|
|
222
|
+
if use_sep:
|
|
223
|
+
xy = np.array([[star['x'], star['y']] for star in stars])
|
|
224
|
+
t_match = catalog[stars['i']]
|
|
225
|
+
else:
|
|
226
|
+
xy = np.array([star.center for star in stars.all_good_stars])
|
|
227
|
+
t_match = catalog[[star.id_label - 1 for star in stars.all_good_stars]]
|
|
228
|
+
radec = np.array([t_match['raMean'], t_match['decMean']]).T
|
|
229
|
+
|
|
230
|
+
res = scipy.optimize.minimize(wcs_offset, [0., 0., 0., 1.], args=(radec, xy, wcs),
|
|
231
|
+
bounds=[(-0.01, 0.01), (-0.01, 0.01), (-0.1, 0.1), (0.9, 1.1)])
|
|
232
|
+
|
|
233
|
+
orig_rms = wcs_offset([0., 0., 0., 1.], radec, xy, wcs)
|
|
234
|
+
print(' orig_fun: {}'.format(orig_rms))
|
|
235
|
+
print(res)
|
|
236
|
+
update_wcs(wcs, res.x)
|
|
237
|
+
|
|
238
|
+
def refine_multiple_stars(image, wcs, catalog, box_size=25, fwhm=5.0, threshold=5):
|
|
239
|
+
from astropy.stats import sigma_clipped_stats
|
|
240
|
+
from astropy.nddata import Cutout2D
|
|
241
|
+
from photutils.detection import DAOStarFinder
|
|
242
|
+
|
|
243
|
+
known_x, known_y = astropy_world_to_pixel(catalog['ra'], catalog['dec'], wcs)
|
|
244
|
+
refined_xy = {}
|
|
245
|
+
refined_xy['x'] = []
|
|
246
|
+
refined_xy['y'] = []
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
for (rough_x, rough_y) in zip(known_x, known_y):
|
|
250
|
+
try:
|
|
251
|
+
cutout = Cutout2D(image, (rough_x, rough_y), size=box_size)
|
|
252
|
+
mean, median, std = sigma_clipped_stats(cutout.data)
|
|
253
|
+
finder = DAOStarFinder(fwhm=fwhm, threshold=threshold*std)
|
|
254
|
+
sources = finder(cutout.data - median)
|
|
255
|
+
|
|
256
|
+
if sources is None or len(sources) == 0:
|
|
257
|
+
#refined_xy_list.append((np.nan, np.nan))
|
|
258
|
+
refined_xy['x'].append(np.nan)
|
|
259
|
+
refined_xy['y'].append(np.nan)
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
# Choose nearest star to center
|
|
263
|
+
dx = sources['xcentroid'] - box_size/2
|
|
264
|
+
dy = sources['ycentroid'] - box_size/2
|
|
265
|
+
distances = np.hypot(dx, dy)
|
|
266
|
+
idx = np.argmin(distances)
|
|
267
|
+
|
|
268
|
+
x_refined = sources['xcentroid'][idx] + cutout.origin_original[0]
|
|
269
|
+
y_refined = sources['ycentroid'][idx] + cutout.origin_original[1]
|
|
270
|
+
#refined_xy_list.append((x_refined, y_refined))
|
|
271
|
+
refined_xy['x'].append(x_refined)
|
|
272
|
+
refined_xy['y'].append(y_refined)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
print(f"Warning: failed to refine star at ({rough_x:.1f}, {rough_y:.1f}): {e}")
|
|
275
|
+
#refined_xy_list.append((np.nan, np.nan))
|
|
276
|
+
refined_xy['x'].append(np.nan)
|
|
277
|
+
refined_xy['y'].append(np.nan)
|
|
278
|
+
|
|
279
|
+
return refined_xy
|
|
280
|
+
|
|
281
|
+
def refine_wcs_astropy(image, wcs, catalog, fwhm=5.0, projection='TAN', fit_distortion=None):
|
|
282
|
+
#matched_sources = find_catalog_stars(image, wcs, catalog)
|
|
283
|
+
matched_sources = refine_multiple_stars(image, wcs, catalog, box_size=25, fwhm=fwhm)
|
|
284
|
+
|
|
285
|
+
valid_mask = np.isfinite(matched_sources['x']) & np.isfinite(matched_sources['y'])
|
|
286
|
+
matched_x = np.array(matched_sources['x'])[valid_mask]
|
|
287
|
+
matched_y = np.array(matched_sources['y'])[valid_mask]
|
|
288
|
+
|
|
289
|
+
catalog = catalog[valid_mask]
|
|
290
|
+
|
|
291
|
+
sky_coords = SkyCoord(ra=catalog['ra'], dec=catalog['dec'], unit='deg')
|
|
292
|
+
# matched pixel positions and sky positions
|
|
293
|
+
new_wcs = fit_wcs_from_points(xy=(matched_x, matched_y), world_coords=sky_coords, projection=projection, sip_degree=fit_distortion)
|
|
294
|
+
return new_wcs, catalog
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def assemble_reference(refdatas, wcs, shape, ref_global_bkg=0, order='bicubic'):
|
|
298
|
+
"""
|
|
299
|
+
Reproject and stack reference images to match science image.
|
|
300
|
+
|
|
301
|
+
Parameters
|
|
302
|
+
----------
|
|
303
|
+
refdatas : list
|
|
304
|
+
List of reference CCDData objects
|
|
305
|
+
wcs : WCS
|
|
306
|
+
Target WCS
|
|
307
|
+
shape : tuple
|
|
308
|
+
Target image shape
|
|
309
|
+
ref_global_bkg : float, optional
|
|
310
|
+
Background value for masked pixels (default: 0)
|
|
311
|
+
order : str, optional
|
|
312
|
+
Interpolation order (default: 'bicubic')
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
CCDData
|
|
317
|
+
Assembled reference image
|
|
318
|
+
"""
|
|
319
|
+
refdatas_reprojected = []
|
|
320
|
+
mask_stack = []
|
|
321
|
+
refdata_foot = np.zeros(shape, float)
|
|
322
|
+
for data in refdatas:
|
|
323
|
+
#reprojected, foot = reproject_interp((data.data, data.wcs), wcs, shape, order=order)
|
|
324
|
+
reprojected, foot = reproject_adaptive((data.data, data.wcs), wcs, shape, conserve_flux=True)
|
|
325
|
+
|
|
326
|
+
refdatas_reprojected.append(reprojected)
|
|
327
|
+
refdata_foot += foot
|
|
328
|
+
|
|
329
|
+
if data.mask is not None:
|
|
330
|
+
# Use nearest-neighbor interpolation for boolean mask
|
|
331
|
+
mask_reproj, _ = reproject_interp((data.mask.astype(float), data.wcs), wcs, shape)
|
|
332
|
+
mask_stack.append(mask_reproj > 0.5)
|
|
333
|
+
# Combine masks if available
|
|
334
|
+
if mask_stack:
|
|
335
|
+
combined_mask = np.any(mask_stack, axis=0)
|
|
336
|
+
else:
|
|
337
|
+
combined_mask = np.zeros(shape, dtype=bool)
|
|
338
|
+
refdata_reproj = np.nanmedian(refdatas_reprojected, axis=0)
|
|
339
|
+
if np.all(np.isnan(refdata_reproj)):
|
|
340
|
+
raise ValueError("All reprojected reference data is NaN.")
|
|
341
|
+
refdata_reproj[np.isnan(refdata_reproj)] = ref_global_bkg
|
|
342
|
+
final_mask = (refdata_foot == 0.) | combined_mask
|
|
343
|
+
refdata = CCDData(refdata_reproj, wcs=wcs, mask=final_mask, unit='adu')
|
|
344
|
+
return refdata
|
slide_lsst/lsst_utils.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
from lsst.daf.butler import Butler, Timespan
|
|
2
|
+
from lsst.rsp import get_tap_service
|
|
3
|
+
import lsst.geom
|
|
4
|
+
import lsst.afw.display as afwDisplay
|
|
5
|
+
from astropy.time import Time
|
|
6
|
+
import numpy as np
|
|
7
|
+
import lsst.geom as geom
|
|
8
|
+
from astroquery.gaia import Gaia
|
|
9
|
+
from astropy.coordinates import SkyCoord
|
|
10
|
+
from astropy.wcs.utils import skycoord_to_pixel, pixel_to_skycoord
|
|
11
|
+
from astropy.nddata import CCDData
|
|
12
|
+
from astropy.io import fits
|
|
13
|
+
from astropy.wcs import WCS
|
|
14
|
+
from photutils.aperture import SkyCircularAnnulus, SkyCircularAperture, ApertureStats
|
|
15
|
+
from photutils.background import Background2D
|
|
16
|
+
from photutils.psf import ImagePSF
|
|
17
|
+
from astropy.table import QTable
|
|
18
|
+
from photutils.psf import IterativePSFPhotometry
|
|
19
|
+
from photutils.background import LocalBackground, MMMBackground
|
|
20
|
+
from photutils.detection import DAOStarFinder
|
|
21
|
+
from photutils.psf import PSFPhotometry
|
|
22
|
+
from astropy.nddata import Cutout2D
|
|
23
|
+
from astropy.wcs.utils import fit_wcs_from_points
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def query_lsst_visits(butler, ra, dec, band, time1, time2):
|
|
27
|
+
"""
|
|
28
|
+
Query LSST Butler for visit images within a time range and sky region.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
butler : lsst.daf.butler.Butler
|
|
33
|
+
Butler instance for data access
|
|
34
|
+
ra : float
|
|
35
|
+
Right ascension in degrees
|
|
36
|
+
dec : float
|
|
37
|
+
Declination in degrees
|
|
38
|
+
band : str
|
|
39
|
+
Filter band name
|
|
40
|
+
time1 : astropy.time.Time
|
|
41
|
+
Start time
|
|
42
|
+
time2 : astropy.time.Time
|
|
43
|
+
End time
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
list
|
|
48
|
+
Dataset references matching the criteria
|
|
49
|
+
"""
|
|
50
|
+
#butler = Butler("dp1", collections="LSSTComCam/DP1")
|
|
51
|
+
time1 = Time(time1, format="isot", scale="tai")
|
|
52
|
+
time2 = Time(time2, format="isot", scale="tai")
|
|
53
|
+
timespan = Timespan(time1, time2)
|
|
54
|
+
|
|
55
|
+
dataset_refs = butler.query_datasets("visit_image",
|
|
56
|
+
where="band.name = :band AND \
|
|
57
|
+
visit.timespan OVERLAPS :timespan AND \
|
|
58
|
+
visit_detector_region.region OVERLAPS POINT(:ra, :dec)",
|
|
59
|
+
bind={"band": band, "timespan": timespan,
|
|
60
|
+
"ra": ra, "dec": dec},
|
|
61
|
+
order_by=["visit.timespan.begin"])
|
|
62
|
+
return dataset_refs
|
|
63
|
+
|
|
64
|
+
def lsst_bad_mask(visit_image, mask_type = ['']):
|
|
65
|
+
mask = visit_image.mask.array
|
|
66
|
+
if len(mask_type) == 0:
|
|
67
|
+
bitmask = visit_image.mask.getPlaneBitMask([
|
|
68
|
+
"BAD", # detector bad pixel
|
|
69
|
+
"SAT", # saturated pixel
|
|
70
|
+
"INTRP", # interpolated pixel
|
|
71
|
+
"CR", # cosmic ray
|
|
72
|
+
"EDGE", # edge of the detector
|
|
73
|
+
#"DETECTED", # source detected
|
|
74
|
+
#"DETECTED_NEGATIVE", # negative source detected
|
|
75
|
+
"SUSPECT", # suspicious pixel
|
|
76
|
+
"NO_DATA", # no data available
|
|
77
|
+
"SENSOR_EDGE", # sensor edge
|
|
78
|
+
"CLIPPED", # pixel in clipped area
|
|
79
|
+
"CROSSTALK", # crosstalk
|
|
80
|
+
#"NOT_DEBLENDED",# not deblended
|
|
81
|
+
"UNMASKEDNAN", # unmasked NaN
|
|
82
|
+
#"VIGNETTED", # vignetting
|
|
83
|
+
"STREAK", # streak from satellite or airplane
|
|
84
|
+
])
|
|
85
|
+
else:
|
|
86
|
+
bitmask = visit_image.mask.getPlaneBitMask(mask_type)
|
|
87
|
+
image_mask = (visit_image.mask.array & bitmask) != 0
|
|
88
|
+
return image_mask
|
|
89
|
+
|
|
90
|
+
def lsst_visit_to_ccddata(visit_image, mask_type = ["BAD", "SAT", "INTRP", "CR", "EDGE", "SUSPECT", "NO_DATA", "SENSOR_EDGE", "CLIPPED", "CROSSTALK", "UNMASKEDNAN", "STREAK"], saveas=None):
|
|
91
|
+
data = visit_image.image.array
|
|
92
|
+
#header = fits.Header(visit_image.getWcs().getFitsMetadata().toDict())
|
|
93
|
+
#wcs = WCS(header)
|
|
94
|
+
wcs = lsst_refine_wcs_astropy(visit_image, n_points=10)
|
|
95
|
+
mask = lsst_bad_mask(visit_image, mask_type=mask_type)
|
|
96
|
+
|
|
97
|
+
sat_mask = lsst_bad_mask(visit_image, mask_type = ['SAT'])
|
|
98
|
+
masked_values = data[sat_mask]
|
|
99
|
+
finite_values = masked_values[np.isfinite(masked_values)]
|
|
100
|
+
SATURATE = np.nanmax(finite_values) if finite_values.size > 0 else 60000
|
|
101
|
+
|
|
102
|
+
ccddata = CCDData(data, wcs=wcs, unit='adu')
|
|
103
|
+
ccddata.mask = mask
|
|
104
|
+
ccddata.meta['SATURATE'] = SATURATE
|
|
105
|
+
if saveas is not None:
|
|
106
|
+
visit_image.writeFits(saveas)
|
|
107
|
+
return ccddata
|
|
108
|
+
|
|
109
|
+
def get_visit_fwhm(visit_image, ra, dec):
|
|
110
|
+
SIGMA_TO_FWHM = 2.0*np.sqrt(2.0*np.log(2.0))
|
|
111
|
+
x, y = lsst_world_to_pixel(ra, dec, visit_image)
|
|
112
|
+
|
|
113
|
+
info_calexp = visit_image.getInfo()
|
|
114
|
+
psf_calexp = info_calexp.getPsf()
|
|
115
|
+
|
|
116
|
+
point_image = lsst.geom.Point2D(x, y)
|
|
117
|
+
psf_shape = psf_calexp.computeShape(point_image)
|
|
118
|
+
psf_sigma = psf_shape.getDeterminantRadius()
|
|
119
|
+
psf_fwhm = psf_sigma * SIGMA_TO_FWHM
|
|
120
|
+
return psf_fwhm
|
|
121
|
+
|
|
122
|
+
def lsst_visit_to_psf(visit_image, ra, dec):
|
|
123
|
+
x, y = lsst_world_to_pixel(ra, dec, visit_image)
|
|
124
|
+
psf = visit_image.getPsf()
|
|
125
|
+
xy = lsst.geom.Point2D(x, y)
|
|
126
|
+
psf_image = psf.computeImage(xy)
|
|
127
|
+
ccddata = CCDData(psf_image.array, unit='adu')
|
|
128
|
+
return ccddata
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def lsst_visit_to_psf_median(visit_image, ra, dec, cutout_size=(2000, 2000), sample_number=20):
|
|
132
|
+
if isinstance(cutout_size, (int, float)):
|
|
133
|
+
cutout_size = (int(cutout_size), int(cutout_size))
|
|
134
|
+
else:
|
|
135
|
+
cutout_size = (int(cutout_size[0]), int(cutout_size[1]))
|
|
136
|
+
|
|
137
|
+
data = visit_image.image.array
|
|
138
|
+
ny, nx = data.shape
|
|
139
|
+
|
|
140
|
+
# Get WCS and center in pixel coordinates
|
|
141
|
+
header = fits.Header(visit_image.getWcs().getFitsMetadata().toDict())
|
|
142
|
+
wcs = WCS(header)
|
|
143
|
+
xcen, ycen = astropy_world_to_pixel(ra, dec, wcs)
|
|
144
|
+
|
|
145
|
+
# Shift center inward if too close to edge
|
|
146
|
+
half_x, half_y = cutout_size[0] // 2, cutout_size[1] // 2
|
|
147
|
+
xcen = np.clip(xcen, half_x, nx - half_x)
|
|
148
|
+
ycen = np.clip(ycen, half_y, ny - half_y)
|
|
149
|
+
|
|
150
|
+
# Sample PSFs
|
|
151
|
+
psf = visit_image.getPsf()
|
|
152
|
+
psf_stars = []
|
|
153
|
+
|
|
154
|
+
for dx in np.linspace(-cutout_size[0] / 2, cutout_size[0] / 2, sample_number):
|
|
155
|
+
for dy in np.linspace(-cutout_size[1] / 2, cutout_size[1] / 2, sample_number):
|
|
156
|
+
x_sample, y_sample = xcen + dx, ycen + dy
|
|
157
|
+
xy = lsst.geom.Point2D(x_sample, y_sample)
|
|
158
|
+
psf_image = psf.computeImage(xy).getArray()
|
|
159
|
+
psf_stars.append(psf_image)
|
|
160
|
+
|
|
161
|
+
if len(psf_stars) == 0:
|
|
162
|
+
raise RuntimeError("No valid PSFs found in the region.")
|
|
163
|
+
|
|
164
|
+
# Stack and normalize
|
|
165
|
+
stacked = np.median(np.stack(psf_stars, axis=0), axis=0)
|
|
166
|
+
psf_final = stacked / np.sum(stacked)
|
|
167
|
+
|
|
168
|
+
ccddata = CCDData(psf_final, unit='adu')
|
|
169
|
+
|
|
170
|
+
return ccddata
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def lsst_world_to_pixel(ra, dec, visit_image):
|
|
175
|
+
wcs = visit_image.wcs
|
|
176
|
+
if isinstance(ra, (int, float)):
|
|
177
|
+
spherePoint = lsst.geom.SpherePoint(ra*geom.degrees, dec*geom.degrees)
|
|
178
|
+
pixel_coord = wcs.skyToPixel(spherePoint)
|
|
179
|
+
x = pixel_coord.getX()
|
|
180
|
+
y = pixel_coord.getY()
|
|
181
|
+
else:
|
|
182
|
+
sphere_points = [geom.SpherePoint(_ra, _dec, geom.degrees) for _ra, _dec in zip(ra, dec)]
|
|
183
|
+
pixel_coords = [wcs.skyToPixel(sp) for sp in sphere_points]
|
|
184
|
+
x = [pt.getX() for pt in pixel_coords]
|
|
185
|
+
y = [pt.getY() for pt in pixel_coords]
|
|
186
|
+
return x, y
|
|
187
|
+
|
|
188
|
+
def lsst_pixel_to_world(x, y, visit_image):
|
|
189
|
+
wcs = visit_image.wcs
|
|
190
|
+
if isinstance(x, (int, float)):
|
|
191
|
+
pixel = geom.Point2D(x, y)
|
|
192
|
+
sky_coord = wcs.pixelToSky(pixel)
|
|
193
|
+
ra = sky_coord.getRa().asDegrees()
|
|
194
|
+
dec = sky_coord.getDec().asDegrees()
|
|
195
|
+
else:
|
|
196
|
+
pixel_coords = [geom.Point2D(px, py) for px, py in zip(x, y)]
|
|
197
|
+
sky_coords = [wcs.pixelToSky(p) for p in pixel_coords]
|
|
198
|
+
ra = [c.getRa().asDegrees() for c in sky_coords]
|
|
199
|
+
dec = [c.getDec().asDegrees() for c in sky_coords]
|
|
200
|
+
return ra, dec
|
|
201
|
+
|
|
202
|
+
def astropy_world_to_pixel(ra, dec, wcs, origin = 0):
|
|
203
|
+
coord = SkyCoord(ra, dec, unit='deg')
|
|
204
|
+
x, y = skycoord_to_pixel(coord, wcs, origin=origin)
|
|
205
|
+
return x, y
|
|
206
|
+
|
|
207
|
+
def astropy_pixel_to_world(x, y, wcs, origin=0):
|
|
208
|
+
skycoord = pixel_to_skycoord(x, y, wcs, origin=origin)
|
|
209
|
+
return skycoord.ra.deg, skycoord.dec.deg
|
|
210
|
+
|
|
211
|
+
def lsst_refine_wcs_astropy(visit_image, n_points=10, projection='TAN', fit_distortion=None):
|
|
212
|
+
|
|
213
|
+
ny, nx = visit_image.image.array.shape
|
|
214
|
+
# Sample pixel grid
|
|
215
|
+
x_vals = np.linspace(25, nx - 25, n_points)
|
|
216
|
+
y_vals = np.linspace(25, ny - 25, n_points)
|
|
217
|
+
xx, yy = np.meshgrid(x_vals, y_vals)
|
|
218
|
+
matched_x = xx.ravel()
|
|
219
|
+
matched_y = yy.ravel()
|
|
220
|
+
|
|
221
|
+
ra, dec = lsst_pixel_to_world(matched_x, matched_y, visit_image)
|
|
222
|
+
|
|
223
|
+
sky_coords = SkyCoord(ra=ra, dec=dec, unit='deg')
|
|
224
|
+
#matched_x, matched_y = lsst_world_to_pixel(catalog['ra'], catalog['dec'], visit_image)
|
|
225
|
+
#matched_x = np.asarray(matched_x)
|
|
226
|
+
#matched_y = np.asarray(matched_y)
|
|
227
|
+
new_wcs = fit_wcs_from_points(xy=(matched_x, matched_y), world_coords=sky_coords, projection=projection, sip_degree=fit_distortion)
|
|
228
|
+
return new_wcs
|
|
229
|
+
|
|
230
|
+
def safe_cutout2d(visit_image, ra, dec, cutout_size=(2000, 2000), mask_type = ["BAD", "SAT", "INTRP", "CR", "EDGE", "SUSPECT", "NO_DATA", "SENSOR_EDGE", "CLIPPED", "CROSSTALK", "UNMASKEDNAN", "STREAK"]):
|
|
231
|
+
"""
|
|
232
|
+
Create a Cutout2D that contains the original RA/Dec, shifting inward if near the edge.
|
|
233
|
+
|
|
234
|
+
Parameters:
|
|
235
|
+
data (2D ndarray): Image array.
|
|
236
|
+
wcs (astropy.wcs.WCS): WCS for the image.
|
|
237
|
+
ra, dec (float): Center coordinates in degrees.
|
|
238
|
+
cutout_size (tuple): Desired cutout size in pixels.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Cutout2D object.
|
|
242
|
+
"""
|
|
243
|
+
if isinstance(cutout_size, (int, float)):
|
|
244
|
+
cutout_size = (int(cutout_size), int(cutout_size))
|
|
245
|
+
else:
|
|
246
|
+
cutout_size = (int(cutout_size[0]), int(cutout_size[1]))
|
|
247
|
+
|
|
248
|
+
data = visit_image.image.array
|
|
249
|
+
mask = lsst_bad_mask(visit_image, mask_type = mask_type)
|
|
250
|
+
sat_mask = lsst_bad_mask(visit_image, mask_type = ['SAT'])
|
|
251
|
+
masked_values = data[sat_mask]
|
|
252
|
+
finite_values = masked_values[np.isfinite(masked_values)]
|
|
253
|
+
SATURATE = np.nanmax(finite_values) if finite_values.size > 0 else 60000
|
|
254
|
+
#SATURATE = np.max(data[sat_mask==True])
|
|
255
|
+
#header = fits.Header(visit_image.getWcs().getFitsMetadata().toDict())
|
|
256
|
+
#wcs = WCS(header)
|
|
257
|
+
wcs = lsst_refine_wcs_astropy(visit_image, n_points=10)
|
|
258
|
+
|
|
259
|
+
xcen, ycen = astropy_world_to_pixel(ra, dec, wcs)
|
|
260
|
+
|
|
261
|
+
# Half size
|
|
262
|
+
half_x = cutout_size[0] // 2
|
|
263
|
+
half_y = cutout_size[1] // 2
|
|
264
|
+
ny, nx = data.shape
|
|
265
|
+
|
|
266
|
+
# Shift center inward if near edges
|
|
267
|
+
xcen = np.clip(xcen, half_x, nx - half_x)
|
|
268
|
+
ycen = np.clip(ycen, half_y, ny - half_y)
|
|
269
|
+
|
|
270
|
+
pixel_center = (xcen, ycen)
|
|
271
|
+
cutout = Cutout2D(data, position=pixel_center, size=cutout_size, wcs=wcs, mode='trim')
|
|
272
|
+
cutout_mask = Cutout2D(mask, position=pixel_center, size=cutout_size, wcs=wcs, mode='trim')
|
|
273
|
+
|
|
274
|
+
ccddata = CCDData(cutout.data, wcs=cutout.wcs, unit='adu')
|
|
275
|
+
ccddata.mask = cutout_mask.data
|
|
276
|
+
ccddata.meta['SATURATE'] = SATURATE
|
|
277
|
+
return ccddata
|
|
278
|
+
|
|
279
|
+
def lsst_cutout_to_ccddata(cutout, saveas=None):
|
|
280
|
+
ccddata = CCDData(cutout.data, wcs=cutout.wcs, unit='adu')
|
|
281
|
+
return ccddata
|
|
282
|
+
|
|
283
|
+
def forced_phot(ra, dec, image, wcs, psf_data):
|
|
284
|
+
xx, yy = np.mgrid[:psf_data.shape[0], :psf_data.shape[1]]
|
|
285
|
+
psf_model = ImagePSF(psf_data/np.sum(psf_data), x_0=psf_data.shape[0]/2, y_0=psf_data.shape[1]/2, flux=1)
|
|
286
|
+
bkgstat = MMMBackground()
|
|
287
|
+
localbkg_estimator = LocalBackground(5, 10, bkgstat)
|
|
288
|
+
init_params = QTable()
|
|
289
|
+
_x, _y = astropy_world_to_pixel(ra, dec, wcs)
|
|
290
|
+
init_params['x'] = [_x]
|
|
291
|
+
init_params['y'] = [_y]
|
|
292
|
+
fit_shape = (5,5)
|
|
293
|
+
#bkgstat = MMMBackground()
|
|
294
|
+
#localbkg_estimator = LocalBackground(5, 10, bkgstat)
|
|
295
|
+
psfphot = PSFPhotometry(psf_model, fit_shape,
|
|
296
|
+
aperture_radius=7,
|
|
297
|
+
localbkg_estimator=None)
|
|
298
|
+
phot = psfphot(image.data, init_params=init_params)
|
|
299
|
+
if phot[0]['flux_fit'] / phot[0]['flux_err'] <= 3 or phot[0]['flux_err'] == 0:
|
|
300
|
+
psf_model.x_0.fixed = True
|
|
301
|
+
psf_model.y_0.fixed = True
|
|
302
|
+
psfphot = PSFPhotometry(psf_model, fit_shape,
|
|
303
|
+
aperture_radius=7,
|
|
304
|
+
localbkg_estimator=None)
|
|
305
|
+
phot = psfphot(image.data, init_params=init_params)
|
|
306
|
+
flux_njy = phot[0]['flux_fit']
|
|
307
|
+
flux_err = phot[0]['flux_err']
|
|
308
|
+
mag = -2.5 * np.log10(flux_njy / 3631e9)
|
|
309
|
+
magerr = (2.5 / np.log(10)) * (flux_err / flux_njy)
|
|
310
|
+
upper_limit = -2.5 * np.log10(flux_err * 5 / 3631e9)
|
|
311
|
+
|
|
312
|
+
return flux_njy, flux_err, mag, magerr, upper_limit
|