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.
@@ -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)