Spec7DT 0.9.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.
spec7dt-0.9.0/PKG-INFO ADDED
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.2
2
+ Name: Spec7DT
3
+ Version: 0.9.0
4
+ Summary: Spectral image handling package for 7-Dimensional Telescope users by Won-Hyeong Lee
5
+ Home-page: https://github.com/Yicircle/Spec7DT
6
+ Author: Won-Hyeong Lee
7
+ Author-email: wohy1220@gmail.com
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: numpy
13
+ Requires-Dist: astropy
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: seaborn
16
+ Requires-Dist: pathlib
17
+ Requires-Dist: photutils
18
+ Requires-Dist: reproject
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: home-page
23
+ Dynamic: keywords
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
File without changes
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
spec7dt-0.9.0/setup.py ADDED
@@ -0,0 +1,30 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name='Spec7DT',
5
+ version='0.9.0',
6
+ description='Spectral image handling package for 7-Dimensional Telescope users by Won-Hyeong Lee',
7
+ author='Won-Hyeong Lee',
8
+ author_email='wohy1220@gmail.com',
9
+ url='https://github.com/Yicircle/Spec7DT',
10
+ install_requires=[
11
+ 'numpy',
12
+ 'astropy',
13
+ 'matplotlib',
14
+ 'seaborn',
15
+ 'pathlib',
16
+ 'photutils',
17
+ 'reproject',
18
+ ],
19
+ packages=find_packages(where='src'),
20
+ package_dir={'': 'src'},
21
+ keywords=[''],
22
+ python_requires='>=3.10',
23
+ package_data={},
24
+ zip_safe=False,
25
+ classifiers=[
26
+ 'Programming Language :: Python :: 3.10',
27
+ 'Programming Language :: Python :: 3.11',
28
+ 'Programming Language :: Python :: 3.12'
29
+ ],
30
+ )
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.2
2
+ Name: Spec7DT
3
+ Version: 0.9.0
4
+ Summary: Spectral image handling package for 7-Dimensional Telescope users by Won-Hyeong Lee
5
+ Home-page: https://github.com/Yicircle/Spec7DT
6
+ Author: Won-Hyeong Lee
7
+ Author-email: wohy1220@gmail.com
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: numpy
13
+ Requires-Dist: astropy
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: seaborn
16
+ Requires-Dist: pathlib
17
+ Requires-Dist: photutils
18
+ Requires-Dist: reproject
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: classifier
22
+ Dynamic: home-page
23
+ Dynamic: keywords
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
@@ -0,0 +1,24 @@
1
+ README.md
2
+ setup.py
3
+ src/Spec7DT.egg-info/PKG-INFO
4
+ src/Spec7DT.egg-info/SOURCES.txt
5
+ src/Spec7DT.egg-info/dependency_links.txt
6
+ src/Spec7DT.egg-info/not-zip-safe
7
+ src/Spec7DT.egg-info/requires.txt
8
+ src/Spec7DT.egg-info/top_level.txt
9
+ src/division/__init__.py
10
+ src/division/binning.py
11
+ src/division/cutout.py
12
+ src/manipulation/__init__.py
13
+ src/manipulation/mask.py
14
+ src/manipulation/reddening.py
15
+ src/manipulation/sky_interpolate.py
16
+ src/reduction/PSF.py
17
+ src/reduction/__init__.py
18
+ src/reduction/background.py
19
+ src/utils/__init__.py
20
+ src/utils/file_generator.py
21
+ src/utils/file_handler.py
22
+ src/utils/pipeline.py
23
+ src/utils/unit.py
24
+ src/utils/utility.py
@@ -0,0 +1,7 @@
1
+ numpy
2
+ astropy
3
+ matplotlib
4
+ seaborn
5
+ pathlib
6
+ photutils
7
+ reproject
@@ -0,0 +1,4 @@
1
+ division
2
+ manipulation
3
+ reduction
4
+ utils
File without changes
@@ -0,0 +1,20 @@
1
+ class Bin:
2
+ def __init__(self):
3
+ pass
4
+
5
+ @classmethod
6
+ def do_binning(cls, bin_size, image_data, error_data, galaxy_name, observatory, band, image_set):
7
+ # if bin_size is None:
8
+ # bin_size = image_data.shape[0]
9
+ binned_img = cls.binning(image_data, bin_size, bin_size)
10
+ binned_err = cls.binning_err(error_data, bin_size, bin_size)
11
+ image_set.update_data(binned_img, galaxy_name, observatory, band)
12
+ image_set.update_error(binned_err, galaxy_name, observatory, band)
13
+
14
+ def binning(image, bin_x, bin_y):
15
+ return image.reshape(bin_x, image.shape[0] // bin_x, bin_y, image.shape[1] // bin_y).sum(3).sum(1)
16
+
17
+ def binning_err(image, bin_x, bin_y):
18
+ image = image ** 2
19
+ image = image.reshape(bin_x, image.shape[0] // bin_x, bin_y, image.shape[1] // bin_y).sum(3).sum(1)
20
+ return image ** (0.5)
@@ -0,0 +1,64 @@
1
+ import numpy as np
2
+
3
+ class CutRegion:
4
+
5
+ @classmethod
6
+ def cutout_region(cls, box_size, image_data, error_data, galaxy_name, observatory, band, image_set):
7
+ cut_img, cut_error = cls.get_cutout(image_data, error_data, box_size, 'ellipse')
8
+ image_set.update_data(cut_img, galaxy_name, observatory, band)
9
+ image_set.update_error(cut_error, galaxy_name, observatory, band)
10
+
11
+ def get_cutout(img, error, size, _shape: str='box'):
12
+ """
13
+ Mask out everything except a central region of shape 'box', 'circle', or 'ellipse'.
14
+
15
+ Parameters
16
+ ----------
17
+ img : 2D ndarray
18
+ Input image.
19
+ size : float or tuple of floats
20
+ - If _shape in {'box','circle'}: scalar = side‐length (box) or diameter (circle).
21
+ - If _shape=='ellipse': either
22
+ * scalar = major and minor axes
23
+ * tuple (width, height) in pixels for major/minor axes.
24
+ _shape : {'box','circle','ellipse'}
25
+ Shape of the kept region.
26
+
27
+ Returns
28
+ -------
29
+ cutout : 2D ndarray
30
+ Same shape as `img`, with pixels **outside** the requested region set to zero.
31
+ """
32
+ ny, nx = img.shape
33
+ cx, cy = nx // 2, ny // 2 # center coordinates
34
+
35
+ if _shape == 'box':
36
+ hs = int(size // 2)
37
+ cut = np.zeros_like(img)
38
+ cut[cy-hs:cy+hs, cx-hs:cx+hs] = img[cy-hs:cy+hs, cx-hs:cx+hs]
39
+ return cut
40
+
41
+ elif _shape == 'circle':
42
+ r = size / 2
43
+ y, x = np.ogrid[:ny, :nx]
44
+ mask = (x - cx)**2 + (y - cy)**2 <= r**2
45
+ return img * mask.astype(img.dtype)
46
+
47
+ elif _shape == 'ellipse':
48
+ # Determine semiaxes in pixels
49
+ if isinstance(size, (list, tuple, np.ndarray)):
50
+ w, h = size
51
+ else:
52
+ w = h = size
53
+ a = w / 2 # semimajor axis
54
+ b = h / 2 # semiminor axis
55
+
56
+ # Create an ellipse mask
57
+ y, x = np.ogrid[:ny, :nx]
58
+ # Standard ellipse equation (centered at cx,cy)
59
+ mask = ((x - cx)**2 / a**2 + (y - cy)**2 / b**2) <= 1.0
60
+
61
+ return img * mask.astype(img.dtype), error * mask.astype(error.dtype)
62
+
63
+ else:
64
+ raise ValueError(f"Unknown shape '{_shape}'. Choose 'box', 'circle', or 'ellipse'.")
File without changes
@@ -0,0 +1,87 @@
1
+ import numpy as np
2
+ from astroquery.ipac.ned import Ned
3
+ from astropy.wcs import WCS
4
+ from photutils.segmentation import detect_sources
5
+ from photutils.background import Background2D, MedianBackground
6
+ from photutils.detection import DAOStarFinder
7
+ from astropy.stats import sigma_clipped_stats
8
+ from photutils.psf import PSFPhotometry, MoffatPSF
9
+
10
+ class Masking:
11
+ def __init__(self):
12
+ pass
13
+
14
+ @classmethod
15
+ def adapt_mask(cls, image_data, header, error_data, galaxy_name, observatory, band, image_set):
16
+ mask_image, masked_image, _ = cls.make_mask(cls, image_data, header, galaxy_name)
17
+ masked_err = np.where(mask_image, 999.0, error_data)
18
+
19
+ image_set.update_data(masked_image, galaxy_name, observatory, band)
20
+ image_set.update_error(masked_err, galaxy_name, observatory, band)
21
+
22
+
23
+ def make_mask(self, image, header, galaxy):
24
+ ra, dec = Ned.query_object(galaxy)['RA', 'DEC'][0]
25
+
26
+ wcs = WCS(header)
27
+ x, y = wcs.all_world2pix(ra, dec, 0)
28
+
29
+ bkg_estimator = MedianBackground()
30
+ try:
31
+ bkg = Background2D(image, (500, 500), filter_size=(13, 13), bkg_estimator=bkg_estimator)
32
+ except ValueError:
33
+ print('ValueError occured. Try Smaller Background size.')
34
+ bkg = Background2D(image, (200, 200), filter_size=(13, 13), bkg_estimator=bkg_estimator)
35
+ threshold = 1.5*bkg.background_rms
36
+
37
+ segment_map = detect_sources(image, threshold, npixels=5)
38
+ if segment_map == None:
39
+ return image, image, image
40
+
41
+ sky_map = np.nonzero(segment_map.data)
42
+ sky_image = image.copy()
43
+ sky_image[sky_map] = np.nan
44
+
45
+ label_main = segment_map.data[int(y), int(x)]
46
+ if label_main != 0:
47
+ segment_map.remove_labels([label_main])
48
+
49
+ mask = np.nonzero(segment_map.data)
50
+ masked_image = image.copy()
51
+ masked_image[mask] = np.nan
52
+
53
+ mask_image = np.zeros_like(image)
54
+ mask_image[mask] = image[mask]
55
+
56
+ mean, median, std = sigma_clipped_stats(image, sigma=3.0)
57
+ daofind = DAOStarFinder(fwhm=6.0, threshold=100.*std)
58
+ sources = daofind(masked_image - mean) # Table with 'xcentroid', 'ycentroid', etc.
59
+
60
+ # 2. Compute center and a small exclusion radius (in pixels)
61
+ excl_radius = 200.0 # e.g. 5 pixels
62
+
63
+ # 3. Measure distance of each source to center
64
+ dx = sources['xcentroid'] - x
65
+ dy = sources['ycentroid'] - y
66
+ dist = np.hypot(dx, dy)
67
+
68
+ # 4. Filter out the central source(s)
69
+ good = dist > excl_radius
70
+ filtered_sources = sources[good]
71
+
72
+ psf_model = MoffatPSF(alpha=5.0)
73
+ psf_model.alpha.fixed = False
74
+ psf_model.flux.fixed = False
75
+ fit_shape = (31, 31)
76
+ psfphot = PSFPhotometry(psf_model, fit_shape,
77
+ aperture_radius=12.0)
78
+ phot = psfphot(masked_image - mean, init_params=filtered_sources["xcentroid", "ycentroid", "flux"])
79
+ if phot is None:
80
+ print('No PSF photometry found.')
81
+ return masked_image, masked_image, masked_image
82
+
83
+ resid = psfphot.make_residual_image(masked_image - mean)
84
+ mask_image = np.where(masked_image - mean - resid > 0, 0, 1)
85
+ masked_image = resid.copy()
86
+
87
+ return mask_image, masked_image, sky_image
@@ -0,0 +1,223 @@
1
+ import numpy as np
2
+ from astropy.coordinates import SkyCoord
3
+ from astroquery.ipac.ned import Ned
4
+ from dustmaps.planck import PlanckQuery
5
+ from pathlib import Path
6
+
7
+ class Reddening:
8
+ def __init__(self):
9
+ from dustmaps.config import config
10
+ config.reset()
11
+
12
+ import dustmaps.planck
13
+ dustmaps.planck.fetch()
14
+
15
+
16
+ def dered(self, image_data, error_data, galaxy_name, observatory, band, image_set):
17
+ self.obj, self.obs, self.filt = galaxy_name, observatory, band
18
+ self.filter_file = f'{self.obs}.{self.filt}.dat'
19
+ self.ra, self.dec = Ned.query_object(self.obj)['RA', 'DEC'][0]
20
+ coords = SkyCoord(self.ra, self.dec, unit='deg', frame='icrs')
21
+ planck = PlanckQuery()
22
+ self.ebv = planck(coords)
23
+ wave, resp = self.get_resp_curve()
24
+ # Check if filter is valid for CCM98 model
25
+ if (max(wave) > 3.3 * 1e4) | (min(wave) < 9.1 * 1e2):
26
+ print('Filter is not valid for CCM98 model')
27
+ return 0
28
+
29
+ A_mean = self.median_reddening(wave, resp)
30
+ deredden_img = image_data * 10 ** (0.4 * A_mean)
31
+ deredden_err = error_data * 10 ** (0.4 * A_mean)
32
+
33
+ image_set.update_data(deredden_img, galaxy_name, observatory, band)
34
+ image_set.update_error(deredden_err, galaxy_name, observatory, band)
35
+
36
+
37
+ def get_resp_curve(self):
38
+ base_dir = Path(__file__).resolve().parents[2]
39
+ filter_dir = base_dir / "src" / "reference" / "filter_curves"
40
+ dat_path = filter_dir / f"{self.filter_file}"
41
+
42
+ f_g = np.genfromtxt(dat_path, skip_header = 3 , delimiter = ' ' , dtype = float)
43
+ wave = f_g[:, 0]
44
+ resp = f_g[:, 1]
45
+
46
+ mask = (resp != 0)
47
+
48
+ return wave[mask], resp[mask]
49
+
50
+ def reddening_ccm(self, wave, ebv=None, a_v=None, r_v=3.1, model='ccm89'):
51
+ """
52
+ Not used in FIREFLY
53
+ Determines a CCM reddening curve.
54
+
55
+ Parameters
56
+ ----------
57
+ wave: ~numpy.ndarray
58
+ wavelength in Angstroms
59
+ flux: ~numpy.ndarray
60
+ ebv: float
61
+ E(B-V) differential extinction; specify either this or a_v.
62
+ a_v: float
63
+ A(V) extinction; specify either this or ebv.
64
+ r_v: float, optional
65
+ defaults to standard Milky Way average of 3.1
66
+ model: {'ccm89', 'gcc09'}, optional
67
+ * 'ccm89' is the default Cardelli, Clayton, & Mathis (1989) [1]_, but
68
+ does include the O'Donnell (1994) parameters to match IDL astrolib.
69
+ * 'gcc09' is Gordon, Cartledge, & Clayton (2009) [2]_. This paper has
70
+ incorrect parameters for the 2175A bump; not yet corrected here.
71
+
72
+ Returns
73
+ -------
74
+ reddening_curve: ~numpy.ndarray
75
+ Multiply to deredden flux, divide to redden.
76
+
77
+ Notes
78
+ -----
79
+ Cardelli, Clayton, & Mathis (1989) [1]_ parameterization is used for all
80
+ models. The default parameter values are from CCM except in the optical
81
+ range, where the updated parameters of O'Donnell (1994) [3]_ are used
82
+ (matching the Goddard IDL astrolib routine CCM_UNRED).
83
+
84
+ The function is works between 910 A and 3.3 microns, although note the
85
+ default ccm89 model is scientifically valid only at >1250 A.
86
+
87
+ Model gcc09 uses the updated UV coefficients of Gordon, Cartledge, & Clayton
88
+ (2009) [2]_, and is valid from 910 A to 3030 A. This function will use CCM89
89
+ at longer wavelengths if GCC09 is selected, but note that the two do not
90
+ connect perfectly smoothly. There is a small discontinuity at 3030 A. Note
91
+ that GCC09 equations 14 and 15 apply to all x>5.9 (the GCC09 paper
92
+ mistakenly states they do not apply at x>8; K. Gordon, priv. comm.).
93
+
94
+ References
95
+ ----------
96
+ [1] Cardelli, J. A., Clayton, G. C., & Mathis, J. S. 1989, ApJ, 345, 245
97
+ [2] Gordon, K. D., Cartledge, S., & Clayton, G. C. 2009, ApJ, 705, 1320
98
+ [3] O'Donnell, J. E. 1994, ApJ, 422, 158O
99
+
100
+ """
101
+
102
+ import warnings
103
+
104
+ model = model.lower()
105
+ if model not in ['ccm89','gcc09']:
106
+ raise ValueError('model must be ccm89 or gcc09')
107
+ if (a_v is None) and (ebv is None):
108
+ raise ValueError('Must specify either a_v or ebv')
109
+ if (a_v is not None) and (ebv is not None):
110
+ raise ValueError('Cannot specify both a_v and ebv')
111
+ if a_v is not None:
112
+ ebv = a_v / r_v
113
+
114
+ if model == 'gcc09':
115
+ raise ValueError('TEMPORARY: gcc09 currently does 2175A bump '+
116
+ 'incorrectly')
117
+
118
+ x = 1e4 / wave # inverse microns
119
+ if any(x < 0.3) or any(x > 11):
120
+ raise ValueError('ccm_dered valid only for wavelengths from 910 A to '+
121
+ '3.3 microns')
122
+ if any(x > 8) and (model == 'ccm89'):
123
+ warnings.warn('CCM89 should not be used below 1250 A.')
124
+ # if any(x < 3.3) and any(x > 3.3) and (model == 'gcc09'):
125
+ # warnings.warn('GCC09 has a discontinuity at 3030 A.')
126
+
127
+ a = np.zeros(x.size)
128
+ b = np.zeros(x.size)
129
+
130
+ # NIR
131
+ valid = (0.3 <= x) & (x < 1.1)
132
+ a[valid] = 0.574 * x[valid]**1.61
133
+ b[valid] = -0.527 * x[valid]**1.61
134
+
135
+ # optical, using O'Donnell (1994) values
136
+ valid = (1.1 <= x) & (x < 3.3)
137
+ y = x[valid] - 1.82
138
+ coef_a = np.array([-0.505, 1.647, -0.827, -1.718, 1.137, 0.701, -0.609,
139
+ 0.104, 1.])
140
+ coef_b = np.array([3.347, -10.805, 5.491, 11.102, -7.985, -3.989, 2.908,
141
+ 1.952, 0.])
142
+ a[valid] = np.polyval(coef_a,y)
143
+ b[valid] = np.polyval(coef_b,y)
144
+
145
+ # UV
146
+ valid = (3.3 <= x) & (x < 8)
147
+ y = x[valid]
148
+ f_a = np.zeros(y.size)
149
+ f_b = np.zeros(y.size)
150
+ select = (y >= 5.9)
151
+ yselect = y[select] - 5.9
152
+
153
+ f_a[select] = -0.04473 * yselect**2 - 0.009779 * yselect**3
154
+ f_b[select] = 0.2130 * yselect**2 + 0.1207 * yselect**3
155
+ a[valid] = 1.752 - 0.316*y - (0.104 / ((y-4.67)**2 + 0.341)) + f_a
156
+ b[valid] = -3.090 + 1.825*y + (1.206 / ((y-4.62)**2 + 0.263)) + f_b
157
+
158
+ # far-UV CCM89 extrapolation
159
+ valid = (8 <= x) & (x < 11)
160
+ y = x[valid] - 8.
161
+ coef_a = np.array([-0.070, 0.137, -0.628, -1.073])
162
+ coef_b = np.array([0.374, -0.420, 4.257, 13.670])
163
+ a[valid] = np.polyval(coef_a,y)
164
+ b[valid] = np.polyval(coef_b,y)
165
+
166
+ # Overwrite UV with GCC09 model if applicable. Not an extrapolation.
167
+ if model == 'gcc09':
168
+ valid = (3.3 <= x) & (x < 11)
169
+ y = x[valid]
170
+ f_a = np.zeros(y.size)
171
+ f_b = np.zeros(y.size)
172
+ select = (5.9 <= y)
173
+ yselect = y[select] - 5.9
174
+ f_a[select] = -0.110 * yselect**2 - 0.0099 * yselect**3
175
+ f_b[select] = 0.537 * yselect**2 + 0.0530 * yselect**3
176
+ a[valid] = 1.896 - 0.372*y - (0.0108 / ((y-4.57)**2 + 0.0422)) + f_a
177
+ b[valid] = -3.503 + 2.057*y + (0.718 / ((y-4.59)**2 + 0.0530*3.1)) + f_b
178
+
179
+ if isinstance(ebv, np.ndarray):
180
+ if (len(ebv.shape) > 1):
181
+ a = a[np.newaxis, np.newaxis, :]; b = b[np.newaxis, np.newaxis, :]
182
+ ebv = ebv[:, :, np.newaxis]
183
+
184
+ a_v = ebv * r_v
185
+ a_lambda = a_v * (a + b/r_v)
186
+ reddening_curve = 10**(0.4 * a_lambda)
187
+
188
+ return reddening_curve
189
+
190
+ def median_reddening(self, wavelength, response):
191
+ """
192
+ Compute the mean wavelength given arrays of wavelength (wavelength) and transmission (response).
193
+
194
+ Parameters
195
+ ----------
196
+ wavelength : array_like
197
+ Wavelengths (e.g., in Angstroms).
198
+ response : array_like
199
+ Filter transmission values corresponding to wavelength.
200
+
201
+ Returns
202
+ -------
203
+ lambda_mean : float
204
+ The mean wavelength.
205
+ """
206
+ # Ensure the inputs are numpy arrays
207
+ wavelength = np.asarray(wavelength)
208
+ response = np.asarray(response) / np.max(np.asarray(response))
209
+
210
+ # Get reddening curve
211
+ red_curve = self.reddening_ccm(wavelength, ebv=self.ebv, a_v=None, r_v=3.1, model='ccm89')
212
+ A_lambda = 2.5 * np.log10(red_curve)
213
+
214
+ # Numerically integrate T(lambda)*lambda over lambda for the numerator
215
+ numerator = np.trapz(response * A_lambda, x=wavelength)
216
+
217
+ # Numerically integrate response(lambda)/lambda over lambda for the denominator
218
+ denominator = np.trapz(response, x=wavelength)
219
+
220
+ # Compute the pivot wavelength as the square root of the ratio
221
+ A_mean = numerator / denominator
222
+ return A_mean
223
+
@@ -0,0 +1,6 @@
1
+ from astropy.convolution import convolve, Gaussian2DKernel
2
+
3
+ def interpolate_sky(image_data, galaxy_name, observatory, band, image_set):
4
+ gauss_kernal = Gaussian2DKernel(x_stddev=1)
5
+ interp_img = convolve(image_data, gauss_kernal, normalize_kernel=True, nan_treatment='interpolate')
6
+ image_set.update_data(interp_img, galaxy_name, observatory, band)
@@ -0,0 +1,123 @@
1
+ import numpy as np
2
+ from scipy.stats import mode
3
+
4
+ from photutils.detection import DAOStarFinder
5
+ from astropy.stats import sigma_clipped_stats
6
+ from astropy.modeling import models, fitting
7
+ from astropy.convolution import convolve_fft, Gaussian2DKernel
8
+
9
+ import warnings
10
+ from astropy.utils.exceptions import AstropyWarning
11
+ warnings.simplefilter('ignore', category=AstropyWarning)
12
+
13
+ class PointSpreadFunction:
14
+
15
+ def __call__(self):
16
+ pass
17
+
18
+ @classmethod
19
+ def extract(cls, image_data, header, galaxy_name, observatory, band, image_set):
20
+ fwhm_val = cls.measure_psf_fwhm(cls, image_data, header, threshold_sigma=15)
21
+
22
+ image_set.set_psf(galaxy_name, observatory, band, fwhm_val) # in " fwhm * pixel_scale
23
+
24
+ @classmethod
25
+ def convolution(cls, image_data, header, error_data, galaxy_name, observatory, band, image_set):
26
+ """
27
+ Convolve `image` with a Gaussian kernel of width `sigma_extra_pix` (pixels).
28
+ If sigma_extra_pix==0, return original image.
29
+ """
30
+ pixel_scale = np.abs(header.get("CD1_1", 1.1e-4)) * 3600 # in "
31
+
32
+ psf_list = image_set.get_psf(galaxy_name)
33
+ sig_i = image_set.psf[galaxy_name][observatory][band]
34
+ sig_t = np.max(psf_list)
35
+ sigma_extra = np.sqrt(sig_t**2 - sig_i**2) / pixel_scale
36
+
37
+ if sigma_extra <= 0:
38
+ return image_data.copy()
39
+ kernel = Gaussian2DKernel(x_stddev=sigma_extra)
40
+ convolved_img = convolve_fft(
41
+ image_data, kernel,
42
+ normalize_kernel=True,
43
+ nan_treatment='interpolate'
44
+ )
45
+ convolved_err = convolve_fft(
46
+ error_data, kernel,
47
+ normalize_kernel=True,
48
+ nan_treatment='interpolate'
49
+ )
50
+ image_set.update_data(convolved_img, galaxy_name, observatory, band)
51
+ image_set.update_error(convolved_err, galaxy_name, observatory, band)
52
+
53
+
54
+ def measure_fwhm_gaussian(image, x_center, y_center, box_size=21):
55
+ """
56
+ Measure FWHM by fitting 2D Gaussian to PSF
57
+ """
58
+ # Extract cutout around star
59
+ y, x = np.ogrid[:box_size, :box_size]
60
+ x_start = int(x_center - box_size//2)
61
+ y_start = int(y_center - box_size//2)
62
+
63
+ cutout = image[y_start:y_start+box_size, x_start:x_start+box_size]
64
+
65
+ # Create coordinate grids
66
+ y_grid, x_grid = np.mgrid[:box_size, :box_size]
67
+
68
+ # Initial parameter guess
69
+ amplitude = np.max(cutout)
70
+ x_mean = box_size // 2
71
+ y_mean = box_size // 2
72
+
73
+ # Fit 2D Gaussian
74
+ g_init = models.Gaussian2D(amplitude=amplitude,
75
+ x_mean=x_mean, y_mean=y_mean,
76
+ x_stddev=box_size * 0.05, y_stddev=box_size * 0.05)
77
+ fit_g = fitting.TRFLSQFitter()
78
+ g = fit_g(g_init, x_grid, y_grid, cutout)
79
+
80
+ # Convert stddev to FWHM
81
+ fwhm_x = 2.355 * g.x_stddev.value
82
+ fwhm_y = 2.355 * g.y_stddev.value
83
+ fwhm_avg = (fwhm_x + fwhm_y) / 2
84
+
85
+ return fwhm_avg
86
+
87
+ def measure_psf_fwhm(self, image, header, threshold_sigma=15):
88
+ """
89
+ Complete pipeline: detect stars and measure FWHM
90
+ """
91
+ im_x, im_y = image.shape
92
+ box_size = int((im_x + im_y) * 0.1 / 2)
93
+ mean, median, std = sigma_clipped_stats(image, sigma=10.0)
94
+
95
+ # Star detection
96
+ daofind = DAOStarFinder(fwhm=3.0, threshold=threshold_sigma*std)
97
+ sources = daofind(image)
98
+
99
+ if sources is None:
100
+ return -1.0
101
+
102
+ # Measure FWHM for each detected star
103
+ fwhm_measurements = []
104
+
105
+ for source in sources:
106
+ x, y = source['xcentroid'], source['ycentroid']
107
+
108
+ margin = 0.1
109
+ # Skip stars too close to edges
110
+ if (x > im_x * margin and x < im_x * (1 - margin) and
111
+ y > im_y * margin and y < im_y * (1 - margin)):
112
+
113
+ try:
114
+ fwhm = self.measure_fwhm_gaussian(image, x, y, box_size=box_size)
115
+ if not np.isnan(fwhm) and fwhm > 0:
116
+ fwhm_measurements.append(fwhm)
117
+ except:
118
+ continue
119
+
120
+ fwhm, _ = mode(fwhm_measurements, nan_policy='omit')
121
+ pixel_scale = np.abs(header.get("CD1_1", 1.1e-4)) * 3600 # in "
122
+
123
+ return fwhm * pixel_scale
File without changes