plotastrodata 1.0.1__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.
- plotastrodata/__init__.py +5 -0
- plotastrodata/analysis_utils.py +739 -0
- plotastrodata/fft_utils.py +304 -0
- plotastrodata/fits_utils.py +325 -0
- plotastrodata/fitting_utils.py +350 -0
- plotastrodata/los_utils.py +135 -0
- plotastrodata/other_utils.py +355 -0
- plotastrodata/plot_utils.py +1304 -0
- plotastrodata-1.0.1.dist-info/LICENSE +674 -0
- plotastrodata-1.0.1.dist-info/METADATA +108 -0
- plotastrodata-1.0.1.dist-info/RECORD +13 -0
- plotastrodata-1.0.1.dist-info/WHEEL +5 -0
- plotastrodata-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
|
|
4
|
+
from plotastrodata.fits_utils import fits2data
|
|
5
|
+
from plotastrodata.plot_utils import set_rcparams
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def shiftphase(F: np.ndarray, u: np.ndarray, xoff: float = 0) -> np.ndarray:
|
|
9
|
+
"""Shift the phase of 1D FFT by xoff.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
F (np.ndarray): 1D FFT.
|
|
13
|
+
u (np.ndarray): 1D array. The first frequency coordinate.
|
|
14
|
+
xoff (float): From old to new center. Defaults to 0.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
np.ndarray: phase-shifted FFT.
|
|
18
|
+
"""
|
|
19
|
+
return F * np.exp(1j * 2 * np.pi * u * xoff)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def shiftphase2(F: np.ndarray, u: np.ndarray, v: np.ndarray,
|
|
23
|
+
xoff: float = 0, yoff: float = 0) -> np.ndarray:
|
|
24
|
+
"""Shift the phase of 2D FFT by (xoff, yoff).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
F (np.ndarray): 2D FFT.
|
|
28
|
+
u (np.ndarray): 1D or 2D array. The first frequency coordinate.
|
|
29
|
+
v (np.ndarray): 1D or 2D array. The second frequency coordinate. Defaults to None.
|
|
30
|
+
xoff (float): From old to new center. Defaults to 0.
|
|
31
|
+
yoff (float): From old to new center. Defaults to 0.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
np.ndarray: phase-shifted FFT.
|
|
35
|
+
"""
|
|
36
|
+
(U, V) = np.meshgrid(u, v) if np.ndim(u) == 1 else (u, v)
|
|
37
|
+
return F * np.exp(1j * 2 * np.pi * (U * xoff + V * yoff))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fftcentering(f: np.ndarray, x: np.ndarray | None = None,
|
|
41
|
+
xcenter: float = 0
|
|
42
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
43
|
+
"""FFT with the phase referring to a specific point.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
f (np.ndarray): 1D array for FFT.
|
|
47
|
+
x (np.ndarray, optional): 1D array. The spatial coordinate. Defaults to None.
|
|
48
|
+
xcenter (float, optional): x of phase reference. Defaults to 0.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
tuple: (F, u). F is FFT of f. u is a 1D array of the frequency coordinate.
|
|
52
|
+
"""
|
|
53
|
+
nx = np.shape(f)[0]
|
|
54
|
+
if x is None:
|
|
55
|
+
x = np.arange(nx)
|
|
56
|
+
X = x[0, :] if np.ndim(x) == 2 else x
|
|
57
|
+
dx = X[1] - X[0]
|
|
58
|
+
u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
|
|
59
|
+
F = np.fft.fftshift(np.fft.fft(f))
|
|
60
|
+
F = shiftphase(F, u=u, xoff=xcenter - X[-1] - dx)
|
|
61
|
+
return F, u
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def fftcentering2(f: np.ndarray,
|
|
65
|
+
x: np.ndarray | None = None, y: np.ndarray | None = None,
|
|
66
|
+
xcenter: float = 0, ycenter: float = 0
|
|
67
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
68
|
+
"""FFT with the phase referring to a specific point.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
f (np.ndarray): 2D array for FFT.
|
|
72
|
+
x (np.ndarray, optional): 1D or 2D array. The first spatial coordinate. Defaults to None.
|
|
73
|
+
y (np.ndarray, optional): 1D or 2D array. The second spatial coordinate. Defaults to None.
|
|
74
|
+
xcenter (float, optional): x of phase reference. Defaults to 0.
|
|
75
|
+
ycenter (float, optional): y of phase reference. Defaults to 0.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
tuple: (F, u, v). F is FFT of f. u and v are 1D arrays of the frequency coordinates.
|
|
79
|
+
"""
|
|
80
|
+
ny, nx = np.shape(f)
|
|
81
|
+
if x is None:
|
|
82
|
+
x = np.arange(nx)
|
|
83
|
+
if y is None:
|
|
84
|
+
y = np.arange(ny)
|
|
85
|
+
X = x[0, :] if np.ndim(x) == 2 else x
|
|
86
|
+
Y = y[:, 0] if np.ndim(y) == 2 else y
|
|
87
|
+
dx, dy = X[1] - X[0], Y[1] - Y[0]
|
|
88
|
+
u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
|
|
89
|
+
v = np.fft.fftshift(np.fft.fftfreq(ny, d=dy))
|
|
90
|
+
F = np.fft.fftshift(np.fft.fft2(f))
|
|
91
|
+
F = shiftphase2(F, u, v, xcenter - X[-1] - dx, ycenter - Y[-1] - dy)
|
|
92
|
+
return F, u, v
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def ifftcentering(F: np.ndarray, u: np.ndarray | None = None,
|
|
96
|
+
xcenter: float = 0, x0: float = None, outreal: bool = True
|
|
97
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
98
|
+
"""inverse FFT with the phase referring to a specific point.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
F (np.ndarray): 1D array. A result of FFT.
|
|
102
|
+
u (np.ndarray, optional): 1D array. The frequency coordinate. Defaults to None.
|
|
103
|
+
xcenter (float, optional): x of phase reference (used in fftcentering). Defaults to 0.
|
|
104
|
+
x0 (float, optional): spatial coordinate of x[0]. Defaults to None.
|
|
105
|
+
outreal (bool, optional): whether output only the real part. Defaults to True.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
tuple: (f, x). f is iFFT of F. x is a 1D array of the spatial coordinate.
|
|
109
|
+
"""
|
|
110
|
+
nx = np.shape(F)[0]
|
|
111
|
+
if u is None:
|
|
112
|
+
u = np.fft.fftshift(np.fft.fftfreq(nx, d=1))
|
|
113
|
+
x = (np.arange(nx) - (nx-1)/2.) / (u[1]-u[0]) / nx + xcenter
|
|
114
|
+
if x0 is not None:
|
|
115
|
+
x = x - x[0] + x0
|
|
116
|
+
dx = x[1] - x[0]
|
|
117
|
+
F = shiftphase(F, u=u, xoff=x[-1] + dx - xcenter)
|
|
118
|
+
f = np.fft.ifft(np.fft.ifftshift(F))
|
|
119
|
+
if outreal:
|
|
120
|
+
f = np.real(f)
|
|
121
|
+
return f, x
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def ifftcentering2(F: np.ndarray,
|
|
125
|
+
u: np.ndarray | None = None, v: np.ndarray | None = None,
|
|
126
|
+
xcenter: float = 0, ycenter: float = 0,
|
|
127
|
+
x0: float | None = None, y0: float | None = None,
|
|
128
|
+
outreal: bool = True
|
|
129
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
130
|
+
"""inverse FFT with the phase referring to a specific point.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
F (np.ndarray): 2D array. A result of FFT.
|
|
134
|
+
u (np.ndarray, optional): 1D or 2D array. The first frequency coordinate. Defaults to None.
|
|
135
|
+
v (np.ndarray, optional): 1D or 2D array. The second frequency cooridnate. Defaults to None.
|
|
136
|
+
xcenter (float, optional): x of phase reference (used in fftcentering2). Defaults to 0.
|
|
137
|
+
ycenter (float, optional): y of phase reference (used in fftcentering2). Defaults to 0.
|
|
138
|
+
x0 (float, optional): spatial coordinate of x[0]. Defaults to None.
|
|
139
|
+
y0 (float, optional): spatial coordinate of y[0]. Defaults to None.
|
|
140
|
+
outreal (bool, optional): whether output only the real part. Defaults to True.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
tuple: (f, x, y). f is iFFT of F. x and y are 1D arrays of the spatial coordinates.
|
|
144
|
+
"""
|
|
145
|
+
ny, nx = np.shape(F)
|
|
146
|
+
if u is None:
|
|
147
|
+
u = np.fft.fftshift(np.fft.fftfreq(nx, d=1))
|
|
148
|
+
if v is None:
|
|
149
|
+
v = np.fft.fftshift(np.fft.fftfreq(ny, d=1))
|
|
150
|
+
x = (np.arange(nx) - (nx-1)/2.) / (u[1]-u[0]) / nx + xcenter
|
|
151
|
+
y = (np.arange(ny) - (ny-1)/2.) / (v[1]-v[0]) / ny + ycenter
|
|
152
|
+
if x0 is not None:
|
|
153
|
+
x = x - x[0] + x0
|
|
154
|
+
if y0 is not None:
|
|
155
|
+
y = y - y[0] + y0
|
|
156
|
+
dx, dy = x[1] - x[0], y[1] - y[0]
|
|
157
|
+
F = shiftphase(F, u, v, x[-1] + dx - xcenter, y[-1] + dy - ycenter)
|
|
158
|
+
f = np.fft.ifft2(np.fft.ifftshift(F))
|
|
159
|
+
if outreal:
|
|
160
|
+
f = np.real(f)
|
|
161
|
+
return f, x, y
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def zeropadding(f: np.ndarray, x: np.ndarray, y: np.ndarray,
|
|
165
|
+
xlim: list, ylim: list
|
|
166
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
167
|
+
"""Pad an outer region with zero.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
f (np.ndarray): Input 2D array.
|
|
171
|
+
x (np.ndarray): 1D array.
|
|
172
|
+
y (np.ndarray): 1D array.
|
|
173
|
+
xlim (list): range of x after the zero padding.
|
|
174
|
+
ylim (list): range of y after the zero padding.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
tuple: (fnew, xnew, ynew). fnew is an 2D array and xnew and ynew are 1D arrays after the zero padding.
|
|
178
|
+
"""
|
|
179
|
+
nx, ny = len(x), len(y)
|
|
180
|
+
dx, dy = x[1] - x[0], y[1] - y[0]
|
|
181
|
+
if dx < 0:
|
|
182
|
+
xlim = [xlim[1], xlim[0]]
|
|
183
|
+
if dy < 0:
|
|
184
|
+
ylim = [ylim[1], ylim[0]]
|
|
185
|
+
nx0 = max(int((x[0] - xlim[0]) / dx), 0)
|
|
186
|
+
nx1 = max(int((xlim[1] - x[-1]) / dx), 0)
|
|
187
|
+
nxnew = nx0 + nx + nx1
|
|
188
|
+
xnew = np.linspace(x[0] - nx0*dx, x[-1] + nx1*dx, nxnew)
|
|
189
|
+
ny0 = max(int((y[0] - ylim[0]) / dy), 0)
|
|
190
|
+
ny1 = max(int((ylim[1] - y[-1]) / dy), 0)
|
|
191
|
+
nynew = ny0 + ny + ny1
|
|
192
|
+
ynew = np.linspace(y[0] - ny0*dy, y[-1] + ny1*dy, nynew)
|
|
193
|
+
fnew = np.zeros((nynew, nxnew))
|
|
194
|
+
fnew[ny0:ny0 + ny, nx0:nx0 + nx] = f
|
|
195
|
+
return fnew, xnew, ynew
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def fftfits(fitsimage: str, center: str | None = None, lam: float = 1,
|
|
199
|
+
xlim: list | None = None, ylim: list | None = None,
|
|
200
|
+
plot: bool = False) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
201
|
+
"""FFT a fits image with the phase referring to a specific point.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
fitsimage (str): Input fits name in the unit of Jy/pixel.
|
|
205
|
+
center (str, optional): Text coordinate. Defaults to None.
|
|
206
|
+
lam (float, optional): Return u * lam and v * lam. Defaults to 1.
|
|
207
|
+
xlim (list, optional): Range of x for zero padding in arcsec.
|
|
208
|
+
ylim (list, optional): Range of y for zero padding in arcsec.
|
|
209
|
+
plot (bool, optional): Check F through images.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
tuple: (F, u, v). F is FFT of f in the unit of Jy. u and v are 1D arrays in the unit of lambda or meter if lam it not unity.
|
|
213
|
+
"""
|
|
214
|
+
f, (x, y, v), _, _, _ = fits2data(fitsimage, center=center)
|
|
215
|
+
if xlim is not None and ylim is not None:
|
|
216
|
+
f, x, y = zeropadding(f, x, y, xlim, ylim)
|
|
217
|
+
arcsec = np.radians(1) / 3600.
|
|
218
|
+
F, u, v = fftcentering2(f, x * arcsec, y * arcsec)
|
|
219
|
+
u, v = u * lam, v * lam
|
|
220
|
+
if plot:
|
|
221
|
+
set_rcparams()
|
|
222
|
+
fig = plt.figure(figsize=(10, 8))
|
|
223
|
+
ax = fig.add_subplot(2, 2, 1)
|
|
224
|
+
m = ax.pcolormesh(u, v, np.real(F), shading='nearest', cmap='jet')
|
|
225
|
+
fig.colorbar(m, ax=ax, label='Real', format='%.1e')
|
|
226
|
+
ax.set_ylabel(r'v ($\lambda$)')
|
|
227
|
+
ax = fig.add_subplot(2, 2, 2)
|
|
228
|
+
m = ax.pcolormesh(u, v, np.imag(F), shading='nearest', cmap='jet')
|
|
229
|
+
fig.colorbar(m, ax=ax, label='Imaginary', format='%.1e')
|
|
230
|
+
ax = fig.add_subplot(2, 2, 3)
|
|
231
|
+
m = ax.pcolormesh(u, v, np.abs(F), shading='nearest', cmap='jet')
|
|
232
|
+
fig.colorbar(m, ax=ax, label='Amplitude', format='%.1e')
|
|
233
|
+
ax.set_xlabel(r'u ($\lambda$)')
|
|
234
|
+
ax.set_ylabel(r'v ($\lambda$)')
|
|
235
|
+
ax = fig.add_subplot(2, 2, 4)
|
|
236
|
+
m = ax.pcolormesh(u, v, np.angle(F) * np.degrees(1),
|
|
237
|
+
shading='nearest', cmap='jet')
|
|
238
|
+
fig.colorbar(m, ax=ax, label='Phase (deg)', format='%.0f')
|
|
239
|
+
ax.set_xlabel(r'u ($\lambda$)')
|
|
240
|
+
fig.tight_layout()
|
|
241
|
+
plt.show()
|
|
242
|
+
plt.close()
|
|
243
|
+
return F, u, v
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def findindex(u: np.ndarray | None = None, v: np.ndarray | None = None,
|
|
247
|
+
uobs: np.ndarray | None = None, vobs: np.ndarray | None = None
|
|
248
|
+
) -> np.ndarray:
|
|
249
|
+
"""Find indicies of the observed visibility points.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
u (np.ndarray, optional): 1D array. The first frequency coordinate. Defaults to None.
|
|
253
|
+
v (np.ndarray, optional): 1D array. The second frequency cooridnate. Defaults to None.
|
|
254
|
+
uobs (np.ndarray, optional): 1D array. Observed u. Defaults to None.
|
|
255
|
+
vobs (np.ndarray, optional): 1D array. Observed v. Defaults to None.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
np.ndarray: Indicies or an array of indicies.
|
|
259
|
+
"""
|
|
260
|
+
if u is not None:
|
|
261
|
+
Nu, du = len(u), u[1] - u[0]
|
|
262
|
+
if v is not None:
|
|
263
|
+
Nv, dv = len(v), v[1] - v[0]
|
|
264
|
+
idx_u, idx_v = None, None
|
|
265
|
+
if uobs is not None:
|
|
266
|
+
idx_u = np.round(uobs / du + Nu // 2).astype(np.int64)
|
|
267
|
+
if vobs is not None:
|
|
268
|
+
idx_v = np.round(vobs / dv + Nv // 2).astype(np.int64)
|
|
269
|
+
if idx_u is not None and idx_v is not None:
|
|
270
|
+
return np.array([idx_u, idx_v])
|
|
271
|
+
if idx_u is not None:
|
|
272
|
+
return idx_u
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def fftfitssample(fitsimage: str, center: str | None = None,
|
|
276
|
+
index_u: np.ndarray | None = None,
|
|
277
|
+
index_v: np.ndarray | None = None,
|
|
278
|
+
xlim: list | None = None, ylim: list | None = None,
|
|
279
|
+
getindex: bool = False,
|
|
280
|
+
u_sample: np.ndarray | None = None,
|
|
281
|
+
v_sample: np.ndarray | None = None) -> np.ndarray:
|
|
282
|
+
"""Find indicies or the visibilities on them from an image fits file.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
fitsimage (str): Input fits name in the unit of Jy/pixel.
|
|
286
|
+
center (str, optional): Text coordinate. Defaults to None.
|
|
287
|
+
index_u (np.ndarray, optional): Indicies. Output from the getindex mode. Defaults to None.
|
|
288
|
+
index_v (np.ndarray, optional): Indicies. Output from the getindex mode. Defaults to None.
|
|
289
|
+
xlim (list, optional): Range of x for zero padding in arcsec.
|
|
290
|
+
ylim (list, optional): Range of y for zero padding in arcsec.
|
|
291
|
+
getindex (bool, optional): True outputs [index_u, index_v]. Defaults to False.
|
|
292
|
+
u_sample (np.ndarray, optional): 1D array. Observed u. Defaults to None.
|
|
293
|
+
v_sample (np.ndarray, optional): 1D array. Observed u. Defaults to None.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
np.ndarray: Array of indicies or sampled FFT.
|
|
297
|
+
"""
|
|
298
|
+
F, u, v = fftfits(fitsimage=fitsimage, center=center, xlim=xlim, ylim=ylim)
|
|
299
|
+
if index_u is None or index_v is None:
|
|
300
|
+
index_u, index_v = findindex(u, v, u_sample, v_sample)
|
|
301
|
+
if getindex:
|
|
302
|
+
return np.array([index_u, index_v])
|
|
303
|
+
else:
|
|
304
|
+
return F[index_v, index_u]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from astropy.io import fits
|
|
3
|
+
from astropy import constants, units, wcs
|
|
4
|
+
|
|
5
|
+
from plotastrodata.other_utils import coord2xy, xy2coord, estimate_rms, trim
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def Jy2K(header=None, bmaj: float | None = None, bmin: float | None = None,
|
|
9
|
+
restfreq: float | None = None) -> float:
|
|
10
|
+
"""Calculate a conversion factor in the unit of K/Jy.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
header (optional): astropy.io.fits.open('a.fits')[0].header. Defaults to None.
|
|
14
|
+
bmaj (float, optional): beam major axis in degree. Defaults to None.
|
|
15
|
+
bmin (float, optional): beam minor axis in degree. Defaults to None.
|
|
16
|
+
freq (float, optional): rest frequency in Hz. Defaults to None.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
float: the conversion factor in the unit of K/Jy.
|
|
20
|
+
"""
|
|
21
|
+
freq = None
|
|
22
|
+
if header is not None:
|
|
23
|
+
if 'BMAJ' in header and 'BMIN' in header:
|
|
24
|
+
bmaj, bmin = header['BMAJ'] * 3600, header['BMIN'] * 3600
|
|
25
|
+
else:
|
|
26
|
+
print('Use CDELT1^2 for Tb conversion.')
|
|
27
|
+
bmaj = bmin = header['CDELT1'] * np.sqrt(4*np.log(2)/np.pi) * 3600
|
|
28
|
+
if header['CUNIT1'] == 'arcsec':
|
|
29
|
+
bmaj, bmin = bmaj / 3600, bmin / 3600
|
|
30
|
+
if 'RESTFREQ' in header:
|
|
31
|
+
freq = header['RESTFREQ']
|
|
32
|
+
if 'RESTFRQ' in header:
|
|
33
|
+
freq = header['RESTFRQ']
|
|
34
|
+
if restfreq is not None:
|
|
35
|
+
freq = restfreq
|
|
36
|
+
if freq is None:
|
|
37
|
+
print('Please input restfreq.')
|
|
38
|
+
return 1
|
|
39
|
+
omega = bmaj * bmin * units.arcsec**2 * np.pi / 4. / np.log(2.)
|
|
40
|
+
equiv = units.brightness_temperature(freq * units.Hz, beam_area=omega)
|
|
41
|
+
T = (1 * units.Jy / units.beam).to(units.K, equivalencies=equiv)
|
|
42
|
+
return T.value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class FitsData:
|
|
46
|
+
"""For practical treatment of data in a FITS file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
fitsimage (str): Input FITS file name.
|
|
50
|
+
"""
|
|
51
|
+
def __init__(self, fitsimage: str):
|
|
52
|
+
self.fitsimage = fitsimage
|
|
53
|
+
|
|
54
|
+
def gen_hdu(self) -> None:
|
|
55
|
+
"""Generate self.hdu. fits.open()[0].
|
|
56
|
+
"""
|
|
57
|
+
hdu = fits.open(self.fitsimage)
|
|
58
|
+
self.hdu = hdu[0]
|
|
59
|
+
if 'BEAMS' in hdu:
|
|
60
|
+
print('Beam table found in HDU list. Use median beam.')
|
|
61
|
+
b = hdu['BEAMS'].data
|
|
62
|
+
area = b['BMAJ'] * b['BMIN'] # arcsec^2?
|
|
63
|
+
imed = np.nanargmin(np.abs(area - np.nanmedian(area)))
|
|
64
|
+
self.hdubeam = b['BMAJ'][imed], b['BMIN'][imed], b['BPA'][imed]
|
|
65
|
+
|
|
66
|
+
def gen_header(self) -> None:
|
|
67
|
+
"""Generate self.header. fits.open()[0].header.
|
|
68
|
+
"""
|
|
69
|
+
if not hasattr(self, 'hdu'):
|
|
70
|
+
self.gen_hdu()
|
|
71
|
+
self.header = self.hdu.header
|
|
72
|
+
|
|
73
|
+
def get_header(self, key: str | None = None) -> dict | float:
|
|
74
|
+
"""Output the entire header or a value when a key is given.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
key (str, optional): Key name of the FITS header. Defaults to None.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
dict or float: The entire header or a value.
|
|
81
|
+
"""
|
|
82
|
+
if not hasattr(self, 'header'):
|
|
83
|
+
self.gen_header()
|
|
84
|
+
if key is None:
|
|
85
|
+
return self.header
|
|
86
|
+
if key in self.header:
|
|
87
|
+
return self.header[key]
|
|
88
|
+
print(f'{key} is not in the header.')
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def gen_beam(self, dist: float = 1.) -> None:
|
|
92
|
+
"""Generate sef.bmaj, self.bmin, self.bpa from header['BMAJ'], etc.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
dist (float, optional): bmaj and bmin are multiplied by dist. Defaults to 1..
|
|
96
|
+
"""
|
|
97
|
+
if hasattr(self, 'hdubeam'):
|
|
98
|
+
bmaj, bmin, bpa = self.hdubeam
|
|
99
|
+
bmaj = bmaj * dist
|
|
100
|
+
bmin = bmin * dist
|
|
101
|
+
else:
|
|
102
|
+
bmaj = self.get_header('BMAJ')
|
|
103
|
+
bmin = self.get_header('BMIN')
|
|
104
|
+
bpa = self.get_header('BPA')
|
|
105
|
+
if bmaj is not None:
|
|
106
|
+
bmaj = bmaj * 3600 * dist
|
|
107
|
+
if bmin is not None:
|
|
108
|
+
bmin = bmin * 3600 * dist
|
|
109
|
+
self.bmaj, self.bmin, self.bpa = bmaj, bmin, bpa
|
|
110
|
+
|
|
111
|
+
def get_beam(self, dist: float = 1.) -> np.ndarray:
|
|
112
|
+
"""Output the beam array of [bmaj, bmin, bpa].
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
dist (float, optional): bmaj and bmin are multiplied by dist. Defaults to 1..
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
np.ndarray: [bmaj, bmin, bpa].
|
|
119
|
+
"""
|
|
120
|
+
if not hasattr(self, 'bmaj'):
|
|
121
|
+
self.gen_beam(dist)
|
|
122
|
+
return np.array([self.bmaj, self.bmin, self.bpa])
|
|
123
|
+
|
|
124
|
+
def get_center(self) -> str:
|
|
125
|
+
"""Output the central coordinates as text.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: The central coordinates.
|
|
129
|
+
"""
|
|
130
|
+
ra_deg = self.get_header('CRVAL1')
|
|
131
|
+
dec_deg = self.get_header('CRVAL2')
|
|
132
|
+
return xy2coord([ra_deg, dec_deg])
|
|
133
|
+
|
|
134
|
+
def gen_data(self, Tb: bool = False, log: bool = False,
|
|
135
|
+
drop: bool = True, restfreq: float = None) -> None:
|
|
136
|
+
"""Generate data, which may be brightness temperature.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
Tb (bool, optional): True means the data are brightness temperatures. Defaults to False.
|
|
140
|
+
log (bool, optional): True means the data are after taking the logarithm to the base 10. Defaults to False.
|
|
141
|
+
drop (bool, optional): True means the data are after using np.squeeze. Defaults to True.
|
|
142
|
+
restfreq (float, optional): Rest frequency for calculating the brightness temperature. Defaults to None.
|
|
143
|
+
"""
|
|
144
|
+
self.data = None
|
|
145
|
+
if not hasattr(self, 'hdu'):
|
|
146
|
+
self.gen_hdu()
|
|
147
|
+
h, d = self.hdu.header, self.hdu.data
|
|
148
|
+
if drop:
|
|
149
|
+
d = np.squeeze(d)
|
|
150
|
+
if Tb:
|
|
151
|
+
d *= Jy2K(header=h, restfreq=restfreq)
|
|
152
|
+
if log:
|
|
153
|
+
d = np.log10(d.clip(np.min(d[d > 0]), None))
|
|
154
|
+
self.data = d
|
|
155
|
+
|
|
156
|
+
def get_data(self, **kwargs) -> np.ndarray:
|
|
157
|
+
"""Output data. This method can take the arguments of gen_data().
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
np.ndarray: data in the format of np.ndarray.
|
|
161
|
+
"""
|
|
162
|
+
if not hasattr(self, 'data'):
|
|
163
|
+
self.gen_data(**kwargs)
|
|
164
|
+
return self.data
|
|
165
|
+
|
|
166
|
+
def gen_grid(self, center: str | None = None, dist: float = 1.,
|
|
167
|
+
restfreq: float | None = None, vsys: float = 0.,
|
|
168
|
+
pv: bool = False) -> None:
|
|
169
|
+
"""Generate grids relative to the center and vsys.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
center (str, optional): Center for the spatial grids. Defaults to None.
|
|
173
|
+
dist (float, optional): The spatial grids are multiplied by dist. Defaults to 1..
|
|
174
|
+
restfreq (float, optional): Rest frequency for converting the frequencies to velocities. Defaults to None.
|
|
175
|
+
vsys (float, optional): The velocity is relative to vsys. Defaults to 0..
|
|
176
|
+
pv (bool, optional): Mode for position-velocity diagram. Defaults to False.
|
|
177
|
+
"""
|
|
178
|
+
h = self.get_header()
|
|
179
|
+
# spatial center
|
|
180
|
+
if center is not None:
|
|
181
|
+
c0 = xy2coord([h['CRVAL1'], h['CRVAL2']])
|
|
182
|
+
cx, cy = coord2xy(coords=center, coordorg=c0)
|
|
183
|
+
else:
|
|
184
|
+
cx, cy = 0, 0
|
|
185
|
+
# rest frequency
|
|
186
|
+
if restfreq is None:
|
|
187
|
+
if 'RESTFRQ' in h:
|
|
188
|
+
restfreq = h['RESTFRQ']
|
|
189
|
+
if 'RESTFREQ' in h:
|
|
190
|
+
restfreq = h['RESTFREQ']
|
|
191
|
+
self.x, self.y, self.v = None, None, None
|
|
192
|
+
self.dx, self.dy, self.dv = None, None, None
|
|
193
|
+
|
|
194
|
+
def get_list(i: int, crval=False) -> np.ndarray:
|
|
195
|
+
s = np.arange(h[f'NAXIS{i:d}'])
|
|
196
|
+
s = (s - h[f'CRPIX{i:d}'] + 1) * h[f'CDELT{i:d}']
|
|
197
|
+
if crval:
|
|
198
|
+
s = s + h[f'CRVAL{i:d}']
|
|
199
|
+
return s
|
|
200
|
+
|
|
201
|
+
def gen_x(s: np.ndarray) -> None:
|
|
202
|
+
s = (s - cx) * dist
|
|
203
|
+
if h['CUNIT1'].strip() in ['deg', 'DEG', 'degree', 'DEGREE']:
|
|
204
|
+
s *= 3600.
|
|
205
|
+
self.x, self.dx = s, s[1] - s[0]
|
|
206
|
+
|
|
207
|
+
def gen_y(s: np.ndarray) -> None:
|
|
208
|
+
s = (s - cy) * dist
|
|
209
|
+
if h['CUNIT2'].strip() in ['deg', 'DEG', 'degree', 'DEGREE']:
|
|
210
|
+
s *= 3600.
|
|
211
|
+
self.y, self.dy = s, s[1] - s[0]
|
|
212
|
+
|
|
213
|
+
def gen_v(s: np.ndarray) -> None:
|
|
214
|
+
if restfreq is None:
|
|
215
|
+
freq = np.mean(s)
|
|
216
|
+
print('restfreq is assumed to be the center.')
|
|
217
|
+
else:
|
|
218
|
+
freq = restfreq
|
|
219
|
+
if freq == 0:
|
|
220
|
+
print('v is frequency because restfreq=0.')
|
|
221
|
+
else:
|
|
222
|
+
s = (1 - s / freq) * constants.c.to('km*s**(-1)').value - vsys
|
|
223
|
+
self.v, self.dv = s, s[1] - s[0]
|
|
224
|
+
|
|
225
|
+
if h['NAXIS'] > 0 and h['NAXIS1'] > 1:
|
|
226
|
+
gen_x(get_list(1))
|
|
227
|
+
if h['NAXIS'] > 1 and h['NAXIS2'] > 1:
|
|
228
|
+
gen_v(get_list(2, True)) if pv else gen_y(get_list(2))
|
|
229
|
+
if h['NAXIS'] > 2 and h['NAXIS3'] > 1:
|
|
230
|
+
gen_v(get_list(3, True))
|
|
231
|
+
|
|
232
|
+
def get_grid(self, **kwargs) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
233
|
+
"""Output the grids, [x, y, v]. This method can take the arguments of gen_grid().
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
tuple: (x, y, v).
|
|
237
|
+
"""
|
|
238
|
+
if not hasattr(self, 'x') or not hasattr(self, 'y'):
|
|
239
|
+
self.gen_grid(**kwargs)
|
|
240
|
+
return self.x, self.y, self.v
|
|
241
|
+
|
|
242
|
+
def trim(self, rmax: float = 1e10, xoff: float = 0., yoff: float = 0.,
|
|
243
|
+
vmin: float = -1e10, vmax: float = 1e10,
|
|
244
|
+
pv: bool = False) -> None:
|
|
245
|
+
"""Trim the data and grids. The data range will be from xoff - rmax, yoff - rmax, vmin to xoff + rmax, yoff + rmax, vmax.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
rmax (float, optional): Defaults to 1e10.
|
|
249
|
+
xoff (float, optional): Defaults to 0..
|
|
250
|
+
yoff (float, optional): Defaults to 0..
|
|
251
|
+
vmin (float, optional): Defaults to -1e10.
|
|
252
|
+
vmax (float, optional): Defaults to 1e10.
|
|
253
|
+
pv (bool, optional): Mode for position-velocity diagram. Defaults to False.
|
|
254
|
+
"""
|
|
255
|
+
data = self.data if hasattr(self, 'data') else None
|
|
256
|
+
x = self.x if hasattr(self, 'x') else None
|
|
257
|
+
y = self.y if hasattr(self, 'y') else None
|
|
258
|
+
v = self.v if hasattr(self, 'v') else None
|
|
259
|
+
self.data, grid = trim(data=data, x=x, y=y, v=v,
|
|
260
|
+
xlim=[xoff - rmax, xoff + rmax],
|
|
261
|
+
ylim=[yoff - rmax, yoff + rmax],
|
|
262
|
+
vlim=[vmin, vmax], pv=pv)
|
|
263
|
+
self.x, self.y, self.v = grid
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def fits2data(fitsimage: str, Tb: bool = False, log: bool = False,
|
|
267
|
+
dist: float = 1., sigma: str | None = None,
|
|
268
|
+
restfreq: float | None = None, center: str | None = None,
|
|
269
|
+
vsys: float = 0., pv: bool = False, **kwargs
|
|
270
|
+
) -> tuple[np.ndarray, tuple[np.ndarray, np.ndarray, np.ndarray],
|
|
271
|
+
tuple[float, float, float], float, float]:
|
|
272
|
+
"""Extract data from a fits file. kwargs are arguments of FitsData.trim().
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
fitsimage (str): Input fits name.
|
|
276
|
+
Tb (bool, optional): True means ouput data are brightness temperature. Defaults to False.
|
|
277
|
+
log (bool, optional): True means output data are logarhismic. Defaults to False.
|
|
278
|
+
dist (float, optional): Change x and y in arcsec to au. Defaults to 1..
|
|
279
|
+
sigma (str, optional): Noise level or method for measuring it. Defaults to None.
|
|
280
|
+
restfreq (float, optional): Used for velocity and brightness temperature. Defaults to None.
|
|
281
|
+
center (str, optional): Text coordinates. Defaults to None.
|
|
282
|
+
vsys (float, optional): In the unit of km/s. Defaults to 0.
|
|
283
|
+
pv (bool, optional): True means PV fits file. Defaults to False.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
tuple: (data, (x, y, v), (bmaj, bmin, bpa), bunit, rms)
|
|
287
|
+
"""
|
|
288
|
+
fd = FitsData(fitsimage)
|
|
289
|
+
fd.gen_data(Tb=Tb, log=log, drop=True, restfreq=restfreq)
|
|
290
|
+
rms = estimate_rms(fd.data, sigma)
|
|
291
|
+
fd.gen_grid(center=center, dist=dist, restfreq=restfreq, vsys=vsys, pv=pv)
|
|
292
|
+
fd.trim(pv=pv, **kwargs)
|
|
293
|
+
beam = fd.get_beam(dist=dist)
|
|
294
|
+
bunit = fd.get_header('BUNIT')
|
|
295
|
+
return fd.data, (fd.x, fd.y, fd.v), beam, bunit, rms
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def data2fits(d: np.ndarray | None = None, h: dict = {},
|
|
299
|
+
templatefits: str | None = None,
|
|
300
|
+
fitsimage: str = 'test') -> None:
|
|
301
|
+
"""Make a fits file from a N-D array.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
d (np.ndarray, optional): N-D array. Defaults to None.
|
|
305
|
+
h (dict, optional): Fits header. Defaults to {}.
|
|
306
|
+
templatefits (str, optional): Fits file to copy header. Defaults to None.
|
|
307
|
+
fitsimage (str, optional): Output name. Defaults to 'test'.
|
|
308
|
+
"""
|
|
309
|
+
ctype0 = ["RA---SIN", "DEC--SIN", "VELOCITY"]
|
|
310
|
+
naxis = np.ndim(d)
|
|
311
|
+
w = wcs.WCS(naxis=naxis)
|
|
312
|
+
_h = {} if templatefits is None else FitsData(templatefits).get_header()
|
|
313
|
+
_h.update(h)
|
|
314
|
+
if _h == {}:
|
|
315
|
+
w.wcs.crpix = [0] * naxis
|
|
316
|
+
w.wcs.crval = [0] * naxis
|
|
317
|
+
w.wcs.cdelt = [1] * naxis
|
|
318
|
+
w.wcs.ctype = ctype0[:naxis]
|
|
319
|
+
header = w.to_header()
|
|
320
|
+
hdu = fits.PrimaryHDU(d, header=header)
|
|
321
|
+
for k in _h:
|
|
322
|
+
if not ('COMMENT' in k or 'HISTORY' in k):
|
|
323
|
+
hdu.header[k] = _h[k]
|
|
324
|
+
hdu = fits.HDUList([hdu])
|
|
325
|
+
hdu.writeto(fitsimage.replace('.fits', '') + '.fits', overwrite=True)
|