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 +26 -0
- spec7dt-0.9.0/README.md +0 -0
- spec7dt-0.9.0/setup.cfg +4 -0
- spec7dt-0.9.0/setup.py +30 -0
- spec7dt-0.9.0/src/Spec7DT.egg-info/PKG-INFO +26 -0
- spec7dt-0.9.0/src/Spec7DT.egg-info/SOURCES.txt +24 -0
- spec7dt-0.9.0/src/Spec7DT.egg-info/dependency_links.txt +1 -0
- spec7dt-0.9.0/src/Spec7DT.egg-info/not-zip-safe +1 -0
- spec7dt-0.9.0/src/Spec7DT.egg-info/requires.txt +7 -0
- spec7dt-0.9.0/src/Spec7DT.egg-info/top_level.txt +4 -0
- spec7dt-0.9.0/src/division/__init__.py +0 -0
- spec7dt-0.9.0/src/division/binning.py +20 -0
- spec7dt-0.9.0/src/division/cutout.py +64 -0
- spec7dt-0.9.0/src/manipulation/__init__.py +0 -0
- spec7dt-0.9.0/src/manipulation/mask.py +87 -0
- spec7dt-0.9.0/src/manipulation/reddening.py +223 -0
- spec7dt-0.9.0/src/manipulation/sky_interpolate.py +6 -0
- spec7dt-0.9.0/src/reduction/PSF.py +123 -0
- spec7dt-0.9.0/src/reduction/__init__.py +0 -0
- spec7dt-0.9.0/src/reduction/background.py +16 -0
- spec7dt-0.9.0/src/utils/__init__.py +0 -0
- spec7dt-0.9.0/src/utils/file_generator.py +140 -0
- spec7dt-0.9.0/src/utils/file_handler.py +336 -0
- spec7dt-0.9.0/src/utils/pipeline.py +126 -0
- spec7dt-0.9.0/src/utils/unit.py +116 -0
- spec7dt-0.9.0/src/utils/utility.py +142 -0
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
|
spec7dt-0.9.0/README.md
ADDED
|
File without changes
|
spec7dt-0.9.0/setup.cfg
ADDED
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
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
|