cubespa 0.0.9__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.
- cubespa-0.0.9/PKG-INFO +20 -0
- cubespa-0.0.9/README.md +3 -0
- cubespa-0.0.9/cubespa/__init__.py +15 -0
- cubespa-0.0.9/cubespa/analysis/__init__.py +2 -0
- cubespa-0.0.9/cubespa/analysis/flux_analysis.py +145 -0
- cubespa-0.0.9/cubespa/cubespa.py +184 -0
- cubespa-0.0.9/cubespa/data.py +173 -0
- cubespa-0.0.9/cubespa/modeling/__init__.py +3 -0
- cubespa-0.0.9/cubespa/modeling/velocity_map.py +102 -0
- cubespa-0.0.9/cubespa/moment_maps.py +82 -0
- cubespa-0.0.9/cubespa/overlays.py +30 -0
- cubespa-0.0.9/cubespa/plotting/__init__.py +11 -0
- cubespa-0.0.9/cubespa/plotting/channel_map_plots.py +67 -0
- cubespa-0.0.9/cubespa/plotting/mommap_plots.py +95 -0
- cubespa-0.0.9/cubespa/plotting/overlay_plots.py +121 -0
- cubespa-0.0.9/cubespa/plotting/pvd_plots.py +43 -0
- cubespa-0.0.9/cubespa/plotting/spectra_plots.py +220 -0
- cubespa-0.0.9/cubespa/plotting/util_plots.py +67 -0
- cubespa-0.0.9/cubespa/plotting/utils.py +62 -0
- cubespa-0.0.9/cubespa/pvds.py +20 -0
- cubespa-0.0.9/cubespa/spectra.py +137 -0
- cubespa-0.0.9/cubespa/utils.py +271 -0
- cubespa-0.0.9/cubespa.egg-info/PKG-INFO +20 -0
- cubespa-0.0.9/cubespa.egg-info/SOURCES.txt +29 -0
- cubespa-0.0.9/cubespa.egg-info/dependency_links.txt +1 -0
- cubespa-0.0.9/cubespa.egg-info/top_level.txt +3 -0
- cubespa-0.0.9/personal/__init__.py +0 -0
- cubespa-0.0.9/setup.cfg +4 -0
- cubespa-0.0.9/setup.py +40 -0
- cubespa-0.0.9/tests/__init__.py +8 -0
- cubespa-0.0.9/tests/util_tests.py +26 -0
cubespa-0.0.9/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: cubespa
|
|
3
|
+
Version: 0.0.9
|
|
4
|
+
Summary: Cube SPectral Analysis
|
|
5
|
+
Home-page: https://github.com/HSouch/cubespa
|
|
6
|
+
Author: Harrison Souchereau
|
|
7
|
+
Author-email: harrison.souchereau@yale.edu
|
|
8
|
+
License: BSD-3-Clause
|
|
9
|
+
Keywords: galaxies datacubes spectra
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# CUBE SPectral Analysis
|
|
19
|
+
|
|
20
|
+
Fully-featured, aesthetically pleasing 2D and 3D analysis of astronomical datacubes.
|
cubespa-0.0.9/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
from .analysis import * # Plots for flux analysis
|
|
3
|
+
from .modeling import *
|
|
4
|
+
|
|
5
|
+
from .cubespa import * # Module with all CubeSPA object functions
|
|
6
|
+
from .data import * # Data handling modules
|
|
7
|
+
from .moment_maps import * # Module for generating moment maps using masking
|
|
8
|
+
from .overlays import * # Module for reprojection and creating overlays
|
|
9
|
+
from .pvds import * # Module for position velocity diagrams (PVDs)
|
|
10
|
+
from .spectra import * # Module for generating and analyzing spectra
|
|
11
|
+
from .utils import * # All miscellaneous utility modules
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__version__ = "0.0.9"
|
|
15
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .. import utils
|
|
3
|
+
|
|
4
|
+
from matplotlib import pyplot as plt
|
|
5
|
+
|
|
6
|
+
from photutils.aperture import EllipticalAnnulus
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def quadrants(q1_angle, offset = 90, clockwise=False):
|
|
10
|
+
mult = -1 if clockwise else 1
|
|
11
|
+
return np.array([(q1_angle + i * 90 * mult + offset) for i in range(4)])
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def halfline_points(cent, length, angle):
|
|
15
|
+
|
|
16
|
+
p1 = (cent[0] , cent[0] + length * np.cos(angle))
|
|
17
|
+
p2 = (cent[1], cent[1] + length * np.sin(angle))
|
|
18
|
+
|
|
19
|
+
return (p1, p2)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def line_points(cent, length, angle):
|
|
23
|
+
p1 = (cent[0] - length * np.cos(angle), cent[0] + length * np.cos(angle))
|
|
24
|
+
p2 = (cent[1] - length * np.sin(angle), cent[1] + length * np.sin(angle))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
return (p1, p2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def theta_list(theta_0, clockwise=False, n_samples=90, plot=False, **kwargs):
|
|
31
|
+
outname = utils.check_kwarg("outname", None, kwargs)
|
|
32
|
+
|
|
33
|
+
theta_max = theta_0 + 2*np.pi
|
|
34
|
+
|
|
35
|
+
if clockwise:
|
|
36
|
+
_ = theta_0
|
|
37
|
+
theta_0, theta_max = theta_max, _
|
|
38
|
+
|
|
39
|
+
print(theta_0, theta_max)
|
|
40
|
+
|
|
41
|
+
angles = np.linspace(theta_0, theta_max, n_samples) % (2 * np.pi)
|
|
42
|
+
|
|
43
|
+
if plot:
|
|
44
|
+
thetas_norm = np.linspace(0, 2*np.pi, n_samples)
|
|
45
|
+
xs, ys = np.cos(angles), np.sin(angles)
|
|
46
|
+
plt.scatter(xs, ys, c=np.rad2deg(thetas_norm))
|
|
47
|
+
plt.colorbar()
|
|
48
|
+
plt.tight_layout()
|
|
49
|
+
if outname is not None:
|
|
50
|
+
plt.savefig(outname, dpi=100)
|
|
51
|
+
plt.close()
|
|
52
|
+
else:
|
|
53
|
+
plt.show()
|
|
54
|
+
|
|
55
|
+
return angles
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def theta_range(shape, x0, y0, theta, dtheta):
|
|
59
|
+
ys, xs = np.mgrid[:shape[0], :shape[1]].astype(float)
|
|
60
|
+
xs -= x0
|
|
61
|
+
ys -= y0
|
|
62
|
+
|
|
63
|
+
thetas = (np.arctan2(ys, xs) ) % (2 * np.pi)
|
|
64
|
+
|
|
65
|
+
theta_mask = np.ones(thetas.shape)
|
|
66
|
+
|
|
67
|
+
theta_min = (theta - dtheta / 2) % (2 * np.pi)
|
|
68
|
+
theta_max = (theta + dtheta / 2) % (2 * np.pi)
|
|
69
|
+
|
|
70
|
+
# Adjust for cases where theta_max < theta_min due to wrapping around 2pi
|
|
71
|
+
if theta_max < theta_min:
|
|
72
|
+
# In this case, we need to handle the wrap around.
|
|
73
|
+
theta_mask = np.logical_or(thetas >= theta_min, thetas <= theta_max)
|
|
74
|
+
else:
|
|
75
|
+
# Regular case where the range does not wrap around
|
|
76
|
+
theta_mask = np.logical_and(thetas >= theta_min, thetas <= theta_max)
|
|
77
|
+
|
|
78
|
+
return theta_mask.astype(int)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def process_azimuth(data, radius, pa, eps, thetas, stellar_x=0, stellar_y=0, annulus_width=50, trim_nans=False):
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
ell_annulus = EllipticalAnnulus((stellar_x, stellar_y), radius, radius + annulus_width,
|
|
85
|
+
(radius + annulus_width) * (1-eps), theta = pa)
|
|
86
|
+
mask = ell_annulus.to_mask(method='center').to_image(shape=data.shape)
|
|
87
|
+
|
|
88
|
+
data_init_mask = np.copy(data)
|
|
89
|
+
data_init_mask[mask == 0] = np.nan
|
|
90
|
+
|
|
91
|
+
fluxes, flux_stds = [], []
|
|
92
|
+
|
|
93
|
+
for i, theta in enumerate(thetas[:]):
|
|
94
|
+
|
|
95
|
+
theta_mask = theta_range(data.shape, stellar_x, stellar_y, theta, 5 * np.pi / 180)
|
|
96
|
+
|
|
97
|
+
this_data = np.copy(data_init_mask)
|
|
98
|
+
this_data[theta_mask == 0] = np.nan
|
|
99
|
+
try:
|
|
100
|
+
med, std = np.nanmedian(this_data), np.nanstd(this_data)
|
|
101
|
+
except RuntimeWarning:
|
|
102
|
+
med, std = np.nan, np.nan
|
|
103
|
+
|
|
104
|
+
fluxes.append(med)
|
|
105
|
+
flux_stds.append(std)
|
|
106
|
+
|
|
107
|
+
if trim_nans:
|
|
108
|
+
fluxes = np.array(fluxes)
|
|
109
|
+
fluxes[np.isnan(fluxes)] = 0
|
|
110
|
+
|
|
111
|
+
flux_stds = np.array(flux_stds)
|
|
112
|
+
flux_stds[np.isnan(flux_stds)] = 0
|
|
113
|
+
|
|
114
|
+
return (np.array(fluxes), np.array(flux_stds), ell_annulus)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def segment_sum(cubespa_obj, data, radius, annulus_width, theta_0, plot=False, clockwise=False):
|
|
118
|
+
stellar_x, stellar_y = cubespa_obj.center
|
|
119
|
+
|
|
120
|
+
eps, pa = cubespa_obj.eps, cubespa_obj.position_angle
|
|
121
|
+
|
|
122
|
+
# print(stellar_x, stellar_y, eps, pa)
|
|
123
|
+
ell_annulus = EllipticalAnnulus((stellar_x, stellar_y),
|
|
124
|
+
radius, radius + annulus_width,
|
|
125
|
+
(radius + annulus_width) * (1-eps),
|
|
126
|
+
theta = pa)
|
|
127
|
+
mask = ell_annulus.to_mask(method='center').to_image(shape=data.shape)
|
|
128
|
+
|
|
129
|
+
data_init_mask = np.copy(data)
|
|
130
|
+
data_init_mask[mask == 0] = np.nan
|
|
131
|
+
|
|
132
|
+
theta_mask = theta_range(data.shape, stellar_x, stellar_y, np.deg2rad(theta_0) + np.pi/4, np.pi / 2)
|
|
133
|
+
|
|
134
|
+
data_init_mask[theta_mask == 0] = np.nan
|
|
135
|
+
pixel_sum = np.nansum(data_init_mask)
|
|
136
|
+
|
|
137
|
+
if plot:
|
|
138
|
+
plt.imshow(data, origin="lower", cmap="Greys")
|
|
139
|
+
plt.imshow(data_init_mask, cmap="viridis", origin="lower", alpha=0.7)
|
|
140
|
+
plt.imshow(theta_mask, alpha=0.2, cmap="Greys", origin="lower")
|
|
141
|
+
ell_annulus.plot(color="black")
|
|
142
|
+
plt.title(f'{radius:.1f} {theta_0}\n{pixel_sum:.2f}', fontsize=10)
|
|
143
|
+
plt.show()
|
|
144
|
+
|
|
145
|
+
return pixel_sum
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from . import data, utils, spectra
|
|
2
|
+
|
|
3
|
+
from . import plotting
|
|
4
|
+
|
|
5
|
+
import os, logging
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CubeSPA:
|
|
11
|
+
""" Base input class for a CubeSPA object
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, cube, data_index=0,
|
|
15
|
+
psf = None,
|
|
16
|
+
mom_maps=None, additional_maps = [],
|
|
17
|
+
center = None, position_angle = 0, eps=0,
|
|
18
|
+
vsys = 0,
|
|
19
|
+
limits = None,
|
|
20
|
+
plot_dir = None,
|
|
21
|
+
**kwargs) -> None:
|
|
22
|
+
""" CubeSPA object
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
cube (ndarray or str): Input cube or filename that points to cube.
|
|
26
|
+
data_index (int, optional): If loading cube, index of cube data. Defaults to 0.
|
|
27
|
+
mom_maps (str, optional): maskmoment-formatted moment map filename prefix. Defaults to None.
|
|
28
|
+
additional_maps (list, optional): Additional data.DataSet objects. Defaults to [].
|
|
29
|
+
center (tuple, optional): Central region, typically defined by stellar light isophotal
|
|
30
|
+
center. Defaults to None.
|
|
31
|
+
position_angle (float, optional): Position angle of disk. Defaults to None.
|
|
32
|
+
eps (float, optional): Ellipticity (1 - b/a) of disk. Defaults to None.
|
|
33
|
+
limits (array or str, optional): Bounding box containing relevant data. Defaults to None.
|
|
34
|
+
If "auto", will try to automatically generate from moment maps.
|
|
35
|
+
plot_dir (str, optional): Directory to place plots. Defaults to None.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
self.cube = data.handle_data(cube, handler=data.load_data, data_index=data_index)
|
|
39
|
+
|
|
40
|
+
if self.cube is not None:
|
|
41
|
+
self.cube_noise_level, self.cube_rms = utils.estimate_rms(self.cube.data,
|
|
42
|
+
utils.check_kwarg("cmin", 5, kwargs),
|
|
43
|
+
utils.check_kwarg("cmax", 5, kwargs))
|
|
44
|
+
|
|
45
|
+
self.psf = data.handle_data(psf, handler=data.load_data, data_index=data_index) if psf is not None else psf
|
|
46
|
+
|
|
47
|
+
# TODO implement better handling of cx and cy. Right now assuming a 3D cube with Stokes. Can be better!
|
|
48
|
+
self.center = center
|
|
49
|
+
if self.center is not None:
|
|
50
|
+
center_x, center_y, center_type = center
|
|
51
|
+
if center_type == "radec":
|
|
52
|
+
cx, cy = self.cube.wcs.wcs_world2pix(center_x, center_y, 0, 0, 0)[:2]
|
|
53
|
+
self.center = (float(cx), float(cy))
|
|
54
|
+
else:
|
|
55
|
+
self.center = (center_x, center_y)
|
|
56
|
+
|
|
57
|
+
# Position angle and ellipticty (1 - b/a) of the stellar disk, for flux analysis
|
|
58
|
+
self.position_angle = np.deg2rad(position_angle)
|
|
59
|
+
self.eps = eps
|
|
60
|
+
|
|
61
|
+
self.velocities = self.velocities_from_wcs(vsys=vsys)
|
|
62
|
+
self.vsys = vsys
|
|
63
|
+
|
|
64
|
+
# TODO Add this to a separate function with error handling
|
|
65
|
+
try:
|
|
66
|
+
self.beam = [self.cube.header["BMIN"], self.cube.header["BMAJ"], self.cube.header["BPA"]]
|
|
67
|
+
self.beam_pix = self.get_beam_pix()
|
|
68
|
+
|
|
69
|
+
self.beam_area = self.get_beam_area()
|
|
70
|
+
self.beam_area_arcsec = self.get_beam_area(in_pixels=False)
|
|
71
|
+
except KeyError:
|
|
72
|
+
logging.warn(f"Unable to get beam for {cube}")
|
|
73
|
+
self.beam = self.beam_pix = self.beam_area_arcsec = None
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
self.mom_maps = data.handle_data(mom_maps, handler=data.load_moment_maps, data_index=data_index)
|
|
77
|
+
self.additional_maps = additional_maps
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logging.warn(f'Failed to load moment maps for {cube}\n{e}')
|
|
80
|
+
|
|
81
|
+
if limits == "auto":
|
|
82
|
+
if "padding" in kwargs.keys():
|
|
83
|
+
padding = kwargs["padding"]
|
|
84
|
+
else:
|
|
85
|
+
padding = 10
|
|
86
|
+
|
|
87
|
+
self.limits = utils.bounds_from_moment_map(self.mom_maps.mom0.data, padding = padding)
|
|
88
|
+
|
|
89
|
+
if plot_dir is not None:
|
|
90
|
+
self.plot_dir = plot_dir
|
|
91
|
+
self.load_dir(plot_dir)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def load_dir(path):
|
|
95
|
+
if not os.path.isdir(path):
|
|
96
|
+
os.makedirs(path, exist_ok=True)
|
|
97
|
+
else:
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def velocities_from_wcs(self, vsys=0):
|
|
102
|
+
vmin = self.cube.wcs.wcs.crval[2]
|
|
103
|
+
vdelt = self.cube.wcs.wcs.cdelt[2]
|
|
104
|
+
|
|
105
|
+
return (np.array([vmin + i * vdelt for i in range(len(self.cube.data))]) / 1000) - vsys
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_beam_area(self, in_pixels=True):
|
|
109
|
+
try:
|
|
110
|
+
header, w = self.cube.header, self.cube.wcs
|
|
111
|
+
|
|
112
|
+
delt = np.mean(np.abs(w.wcs.cdelt[:2]))
|
|
113
|
+
|
|
114
|
+
if in_pixels:
|
|
115
|
+
bmin, bmaj = header["BMIN"] / delt , header["BMAJ"] / delt
|
|
116
|
+
else:
|
|
117
|
+
bmin, bmaj = header["BMIN"] * 3600 , header["BMAJ"] * 3600
|
|
118
|
+
|
|
119
|
+
area = np.pi * bmaj * bmin / (4 * np.log(2))
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print("Failed to get beam area:", e)
|
|
122
|
+
area = 1
|
|
123
|
+
|
|
124
|
+
return area
|
|
125
|
+
|
|
126
|
+
def get_beam_pix(self):
|
|
127
|
+
try:
|
|
128
|
+
header, w = self.cube.header, self.cube.wcs
|
|
129
|
+
|
|
130
|
+
delt = np.mean(np.abs(w.wcs.cdelt[:2]))
|
|
131
|
+
bmaj, bmin, bpa = self.beam
|
|
132
|
+
bmaj /= delt
|
|
133
|
+
bmin /= delt
|
|
134
|
+
return bmaj, bmin, bpa
|
|
135
|
+
except:
|
|
136
|
+
return 1, 1
|
|
137
|
+
|
|
138
|
+
def get_beam_coords(self, x0=0, y0=0):
|
|
139
|
+
try:
|
|
140
|
+
header, w = self.cube.header, self.cube.wcs
|
|
141
|
+
delt = np.mean(np.abs(w.wcs.cdelt[:2]))
|
|
142
|
+
|
|
143
|
+
bmin, bmaj, bpa = header["BMIN"] / delt , header["BMAJ"] / delt, np.deg2rad(header["BPA"] + 90)
|
|
144
|
+
return utils.ellipse_coords(x0, y0, bmaj / 2, bmin / 2, bpa)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print("Failed to get beam coordinates", e)
|
|
148
|
+
|
|
149
|
+
# UTILITY FUNCTIONS
|
|
150
|
+
def plot_moment_maps(self, use_limits=True, **kwargs):
|
|
151
|
+
outname = utils.check_kwarg("outname", None, kwargs)
|
|
152
|
+
|
|
153
|
+
plotting.moment_map_plot(self, use_limits=use_limits, outname=outname, kwargs=kwargs)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_spectra(self, position, size, return_products=False, plot=False):
|
|
157
|
+
aper = spectra.create_aperture(self, position, size)
|
|
158
|
+
spectrum = spectra.get_spectra(self.cube.data, aper)
|
|
159
|
+
if plot:
|
|
160
|
+
plotting.spectra_plot(self, aper, spectrum)
|
|
161
|
+
if return_products:
|
|
162
|
+
return aper, spectrum
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class CubeComparison:
|
|
166
|
+
|
|
167
|
+
def __init__(self, cube1: CubeSPA, cube2: CubeSPA) -> None:
|
|
168
|
+
self.cube1 = cube1
|
|
169
|
+
self.cube2 = cube2
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create_spectra(self, position, size, return_products=False, plot=False):
|
|
173
|
+
aper = spectra.create_aperture(self, position, size)
|
|
174
|
+
spectrum = spectra.get_spectra(self.cube1.data, aper)
|
|
175
|
+
|
|
176
|
+
if plot:
|
|
177
|
+
plotting.spectra_plot(self, aper, spectrum)
|
|
178
|
+
if return_products:
|
|
179
|
+
return aper, spectrum
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def readout(self):
|
|
183
|
+
print(f'Cube 1: {self.cube1.cube.data.shape} Cube 2: {self.cube2.cube.data.shape}')
|
|
184
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import logging
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
from . import utils
|
|
6
|
+
|
|
7
|
+
from astropy.wcs import WCS
|
|
8
|
+
from astropy.convolution import Tophat2DKernel
|
|
9
|
+
from astropy.io import fits
|
|
10
|
+
|
|
11
|
+
from scipy.signal import convolve2d
|
|
12
|
+
|
|
13
|
+
from . import plotting
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataSet:
|
|
17
|
+
def __init__(self, data=None, wcs=None, header=None, label=None, dtype=None):
|
|
18
|
+
self.data = data
|
|
19
|
+
self.wcs = wcs
|
|
20
|
+
self.header = header
|
|
21
|
+
self.label = label
|
|
22
|
+
|
|
23
|
+
self.dtype = dtype
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RGBImage(DataSet):
|
|
27
|
+
def __init__(self, data=None, wcs=None, header=None, label=None):
|
|
28
|
+
super().__init__(data, wcs, header, label)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MomentMaps:
|
|
33
|
+
def __init__(self, mom0=None, mom1=None, mom2=None, data_index=0):
|
|
34
|
+
if type(mom0) == str:
|
|
35
|
+
self.mom0 = load_data(mom0, data_index=data_index)
|
|
36
|
+
if type(mom1) == str:
|
|
37
|
+
self.mom1 = load_data(mom1, data_index=data_index)
|
|
38
|
+
if type(mom2) == str:
|
|
39
|
+
self.mom2 = load_data(mom2, data_index=data_index)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_data(filename, data_index=0, rgb_index = None, label=None):
|
|
43
|
+
try:
|
|
44
|
+
with fits.open(filename) as HDUList:
|
|
45
|
+
hdu = HDUList[data_index]
|
|
46
|
+
|
|
47
|
+
if rgb_index is None:
|
|
48
|
+
data = hdu.data
|
|
49
|
+
if len(data.shape) == 4: # Drop Stokes parameter (could remove this later maybe)
|
|
50
|
+
data = data[0]
|
|
51
|
+
wcs = WCS(hdu.header)
|
|
52
|
+
else:
|
|
53
|
+
data = hdu.data[rgb_index]
|
|
54
|
+
wcs = WCS(hdu.header).dropaxis(2)
|
|
55
|
+
|
|
56
|
+
out = DataSet(data=data, wcs=wcs, header=hdu.header, label=label)
|
|
57
|
+
return out
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f"Error loading data: {e}")
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_moment_maps(topdir, data_index=0):
|
|
64
|
+
try:
|
|
65
|
+
mom_0_fn = topdir + ".mom0.fits.gz"
|
|
66
|
+
mom_1_fn = topdir + ".mom1.fits.gz"
|
|
67
|
+
mom_2_fn = topdir + ".mom2.fits.gz"
|
|
68
|
+
|
|
69
|
+
moment_maps = MomentMaps(mom0=mom_0_fn, mom1=mom_1_fn, mom2=mom_2_fn, data_index=data_index)
|
|
70
|
+
return moment_maps
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"Error loading Moment Maps: {e}")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def handle_data(data, handler, data_index=0):
|
|
78
|
+
""" Handle incoming data for the CubeSPA object.
|
|
79
|
+
If not a string, CubeSPA will return the data enclosed in a dataset without additional info.
|
|
80
|
+
Args:
|
|
81
|
+
data (str or cubespa.DataSet): Incoming data. If str, CubeSPA will automatically load it in.
|
|
82
|
+
handler (method): Method to handle data, either set to load_data or load_moment_maps
|
|
83
|
+
data_index (int, optional): Index to find data in. Defaults to 0.
|
|
84
|
+
Returns:
|
|
85
|
+
utils.DataSet : The output DataSet object.
|
|
86
|
+
"""
|
|
87
|
+
if type(data) == str:
|
|
88
|
+
return handler(data, data_index=data_index)
|
|
89
|
+
elif type(data) == np.ndarray:
|
|
90
|
+
logging.warn("Naked data loaded. No WCS or header included!")
|
|
91
|
+
return utils.DataSet(data=data)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def gen_cutout(data, limits):
|
|
95
|
+
""" Generates a cutout from an image based on a set of limits.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
data (ndarray): Input image data array
|
|
99
|
+
limits (arr): Limits, assuming [xmin, xmax, ymin, ymax] format
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
_type_: _description_
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
xmin, xmax, ymin, ymax = limits
|
|
106
|
+
|
|
107
|
+
return data[ymin:ymax, xmin:xmax]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def gen_cutout(cubespa_obj, cent, size, show_bbox=False):
|
|
111
|
+
|
|
112
|
+
# TODO: change WCS coordinates to match new cutout location
|
|
113
|
+
|
|
114
|
+
cutout = copy.deepcopy(cubespa_obj)
|
|
115
|
+
|
|
116
|
+
if type(size) is int:
|
|
117
|
+
size = (size, size)
|
|
118
|
+
|
|
119
|
+
xmin, xmax = cent[1] - size[0], cent[1] + size[0]
|
|
120
|
+
ymin, ymax = cent[0] - size[1], cent[0] + size[1]
|
|
121
|
+
|
|
122
|
+
if show_bbox:
|
|
123
|
+
plotting.plot_bbox(cubespa_obj, [ymin, ymax, xmin, xmax])
|
|
124
|
+
|
|
125
|
+
new_cube = cubespa_obj.cube.data[:, xmin:xmax, ymin:ymax]
|
|
126
|
+
if cubespa_obj.mom_maps is not None:
|
|
127
|
+
new_mom0 = cubespa_obj.mom_maps.mom0.data[xmin:xmax, ymin:ymax]
|
|
128
|
+
new_mom1 = cubespa_obj.mom_maps.mom1.data[xmin:xmax, ymin:ymax]
|
|
129
|
+
new_mom2 = cubespa_obj.mom_maps.mom2.data[xmin:xmax, ymin:ymax]
|
|
130
|
+
|
|
131
|
+
for i, additional in enumerate(cubespa_obj.additional_maps):
|
|
132
|
+
cutout.additional_maps[i].data = additional.data[xmin:xmax, ymin:ymax]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
cutout.cube.data = new_cube
|
|
136
|
+
|
|
137
|
+
cutout.limits = [ymin, ymax, xmin, xmax]
|
|
138
|
+
|
|
139
|
+
cutout.mom_maps.mom0.data = new_mom0
|
|
140
|
+
cutout.mom_maps.mom1.data = new_mom1
|
|
141
|
+
cutout.mom_maps.mom2.data = new_mom2
|
|
142
|
+
|
|
143
|
+
return cutout
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def gen_bg_mask(a, thresh_min=None, dilate=None, dilate_thresh = 0.7):
|
|
147
|
+
"""Generate mask from a map, assuming emission is either nan or below some threshold value.
|
|
148
|
+
Mask can be dilated using the dilate parameter, and be further adjusted with dilate_thresh
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
a (ndarray): Input data array
|
|
152
|
+
thresh_min (float, optional): Minimum threshold to keep unmasked. Defaults to None.
|
|
153
|
+
dilate (int, optional): Side of tophat kernel to dilate mask. Defaults to None.
|
|
154
|
+
dilate_thresh (float, optional): Threshold (between 0 and 1) to include "edge areas" in the dilation mask.
|
|
155
|
+
A higher value corresponds to a stronger dilation. Defaults to 0.7.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
ndarray: Boolean mask with same shape as input array.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
mask = np.zeros(a.shape)
|
|
162
|
+
mask[np.isnan(a)] = 1
|
|
163
|
+
|
|
164
|
+
if thresh_min is not None:
|
|
165
|
+
mask[a < thresh_min] = 1
|
|
166
|
+
|
|
167
|
+
if dilate is not None:
|
|
168
|
+
kernel = Tophat2DKernel(dilate)
|
|
169
|
+
mask = convolve2d(mask, kernel, mode="same")
|
|
170
|
+
mask[mask < dilate_thresh] = 0
|
|
171
|
+
mask[mask >= dilate_thresh] = 1
|
|
172
|
+
|
|
173
|
+
return mask.astype(bool)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .. import utils
|
|
3
|
+
|
|
4
|
+
from astropy.io import fits
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def brandt_curve(rs, kwargs):
|
|
8
|
+
Rmax = utils.check_kwarg("rmax", 50, kwargs)
|
|
9
|
+
Vmax = utils.check_kwarg("vmax", 50, kwargs)
|
|
10
|
+
n = utils.check_kwarg("n", 1, kwargs)
|
|
11
|
+
|
|
12
|
+
R_norm = rs/Rmax
|
|
13
|
+
|
|
14
|
+
return Vmax * R_norm * (1/3 + (2/3)*(R_norm ** n)) ** (-3 / (2 * n))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def df_to_of(x, y, alpha, i):
|
|
18
|
+
""" Convert the disk frame to the observer's frame.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
x (_type_): _description_
|
|
22
|
+
y (_type_): _description_
|
|
23
|
+
alpha (_type_): _description_
|
|
24
|
+
i (_type_): _description_
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
_type_: _description_
|
|
28
|
+
"""
|
|
29
|
+
X = x * np.cos(alpha) - y * np.sin(alpha) * np.cos(i)
|
|
30
|
+
Y = x * np.sin(alpha) + y * np.cos(alpha) * np.cos(i)
|
|
31
|
+
return X, Y
|
|
32
|
+
|
|
33
|
+
def of_to_df(X, Y, alpha, i):
|
|
34
|
+
""" Convert the observer's frame to the disk frame.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
X (_type_): _description_
|
|
38
|
+
Y (_type_): _description_
|
|
39
|
+
alpha (_type_): _description_
|
|
40
|
+
i (_type_): _description_
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
_type_: _description_
|
|
44
|
+
"""
|
|
45
|
+
x = X * np.cos(alpha) + Y * np.sin(alpha)
|
|
46
|
+
y = -X * np.sin(alpha) * np.cos(i) + Y * np.cos(alpha) * np.cos(i)
|
|
47
|
+
|
|
48
|
+
return x, y
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class VelocityModel:
|
|
52
|
+
def __init__(self, shape, x0=None, y0=None, pa=None, inc=None, vsys=0, rmax=50):
|
|
53
|
+
|
|
54
|
+
if type(shape) == int:
|
|
55
|
+
shape = (shape, shape)
|
|
56
|
+
|
|
57
|
+
self.shape = shape
|
|
58
|
+
|
|
59
|
+
self.x0 = x0 if x0 is not None else int(shape[1] / 2)
|
|
60
|
+
self.y0 = y0 if y0 is not None else int(shape[0] / 2)
|
|
61
|
+
self.pa = np.deg2rad(pa) if pa is not None else 0
|
|
62
|
+
self.inc = np.deg2rad(inc) if inc is not None else np.deg2rad(30)
|
|
63
|
+
self.vsys = vsys if vsys is not None else 0
|
|
64
|
+
self.rmax = rmax
|
|
65
|
+
|
|
66
|
+
self.vr = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def gen_model(self, velocity_model, fill_value=np.nan, **vmod_kwargs):
|
|
71
|
+
xs, ys = np.arange(0, self.shape[0]).astype(float), np.arange(0, self.shape[1]).astype(float)
|
|
72
|
+
Y, X = np.meshgrid(ys, xs)
|
|
73
|
+
|
|
74
|
+
X -= self.x0
|
|
75
|
+
Y -= self.y0
|
|
76
|
+
|
|
77
|
+
x, y = of_to_df(X, Y, self.pa, self.inc)
|
|
78
|
+
|
|
79
|
+
r = np.sqrt(x ** 2 + y ** 2)
|
|
80
|
+
|
|
81
|
+
vcirc = velocity_model(r, vmod_kwargs)
|
|
82
|
+
theta = np.arctan2(x, y)
|
|
83
|
+
|
|
84
|
+
vr = self.vsys + vcirc * np.sin(self.inc) * np.cos(theta)
|
|
85
|
+
vr[r > self.rmax * 2] = fill_value
|
|
86
|
+
self.vr = vr
|
|
87
|
+
return self.vr
|
|
88
|
+
|
|
89
|
+
def write_model(self, outname, to_fits=False, overwrite=True):
|
|
90
|
+
""" Save the velocity model to a FITS file or Pickle file, depending on user preferences
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
filename (str): Output filename
|
|
94
|
+
to_fits (bool, optional): Whether to save to FITS format. Defaults to False.
|
|
95
|
+
overwrite (bool, optional): If saving to fits format, whether to overwrite output if already saved. Defaults to True.
|
|
96
|
+
"""
|
|
97
|
+
if to_fits:
|
|
98
|
+
hdul = fits.HDUList()
|
|
99
|
+
hdul.append(fits.ImageHDU(data=self.vr))
|
|
100
|
+
hdul.writeto(outname, overwrite=overwrite)
|
|
101
|
+
else:
|
|
102
|
+
utils.save_pickle(self.vr, outname)
|