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,1304 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib as mpl
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from matplotlib.patches import Ellipse, Rectangle
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from plotastrodata.other_utils import coord2xy, xy2coord, listing
|
|
8
|
+
from plotastrodata.analysis_utils import AstroData, AstroFrame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
plt.ioff() # force to turn off interactive mode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_rcparams(fontsize: int = 18, nancolor: str = 'w',
|
|
15
|
+
dpi: int = 256) -> None:
|
|
16
|
+
"""Nice rcParams for figures.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
fontsize (int, optional): plt.rcParams['font.size']. Defaults to 18.
|
|
20
|
+
nancolor (str, optional): plt.rcParams['axes.facecolor']. Defaults to 'w'.
|
|
21
|
+
dpi (int, optional): plt.rcParams['savefig.dpi']. Defaults to 256.
|
|
22
|
+
"""
|
|
23
|
+
# plt.rcParams['font.family'] = 'arial'
|
|
24
|
+
plt.rcParams['axes.facecolor'] = nancolor
|
|
25
|
+
plt.rcParams['font.size'] = fontsize
|
|
26
|
+
plt.rcParams['savefig.dpi'] = dpi
|
|
27
|
+
plt.rcParams['legend.fontsize'] = 15
|
|
28
|
+
plt.rcParams['axes.linewidth'] = 1.5
|
|
29
|
+
plt.rcParams['xtick.direction'] = 'inout'
|
|
30
|
+
plt.rcParams['ytick.direction'] = 'inout'
|
|
31
|
+
plt.rcParams['xtick.top'] = True
|
|
32
|
+
plt.rcParams['ytick.right'] = True
|
|
33
|
+
plt.rcParams['xtick.major.size'] = 10
|
|
34
|
+
plt.rcParams['ytick.major.size'] = 10
|
|
35
|
+
plt.rcParams['xtick.minor.size'] = 6
|
|
36
|
+
plt.rcParams['ytick.minor.size'] = 6
|
|
37
|
+
plt.rcParams['xtick.major.width'] = 1.5
|
|
38
|
+
plt.rcParams['ytick.major.width'] = 1.5
|
|
39
|
+
plt.rcParams['xtick.minor.width'] = 1.5
|
|
40
|
+
plt.rcParams['ytick.minor.width'] = 1.5
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def logticks(ticks: list[float], lim: list[float, float]
|
|
44
|
+
) -> tuple[list[float], list[str]]:
|
|
45
|
+
"""Make nice ticks for a log axis.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
ticks (list): List of ticks.
|
|
49
|
+
lim (list): [min, max].
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
tuple: (new ticks, new labels).
|
|
53
|
+
"""
|
|
54
|
+
order = int(np.floor((np.log10(lim[0]))))
|
|
55
|
+
a = (lim[0] // 10**order + 1) * 10**order
|
|
56
|
+
a = np.round(a, max(-order, 0))
|
|
57
|
+
order = int(np.floor((np.log10(lim[1]))))
|
|
58
|
+
b = (lim[1] // 10**order) * 10**order
|
|
59
|
+
b = np.round(b, max(-order, 0))
|
|
60
|
+
newticks = np.sort(np.r_[a, ticks, b])
|
|
61
|
+
newlabels = [str(t if t < 1 else int(t)) for t in newticks]
|
|
62
|
+
return newticks, newlabels
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class PlotAxes2D():
|
|
67
|
+
"""Use Axes.set_* to adjust x and y axes.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
samexy (bool, optional): True supports same ticks between x and y. Defaults to True.
|
|
71
|
+
loglog (float, optional): If a float is given, plot on a log-log plane, and xim=(xmax / loglog, xmax) and so does ylim. Defaults to None.
|
|
72
|
+
xscale (str, optional): Defaults to None.
|
|
73
|
+
yscale (str, optional): Defaults to None.
|
|
74
|
+
xlim (list, optional): Defaults to None.
|
|
75
|
+
ylim (list, optional): Defaults to None.
|
|
76
|
+
xlabel (str, optional): Defaults to None.
|
|
77
|
+
ylabel (str, optional): Defaults to None.
|
|
78
|
+
xticks (list, optional): Defaults to None.
|
|
79
|
+
yticks (list, optional): Defaults to None.
|
|
80
|
+
xticklabels (list, optional): Defaults to None.
|
|
81
|
+
yticklabels (list, optional): Defaults to None.
|
|
82
|
+
xticksminor (list or int, optional): If int, int times more than xticks. Defaults to None.
|
|
83
|
+
yticksminor (list ot int, optional): Defaults to None. If int, int times more than xticks. Defaults to None.
|
|
84
|
+
grid (dict, optional): True means merely grid(). Defaults to None.
|
|
85
|
+
aspect (float, optional): Defaults to None.
|
|
86
|
+
"""
|
|
87
|
+
samexy: bool = True
|
|
88
|
+
loglog: bool | None = None
|
|
89
|
+
xscale: str = 'linear'
|
|
90
|
+
yscale: str = 'linear'
|
|
91
|
+
xlim: list | None = None
|
|
92
|
+
ylim: list | None = None
|
|
93
|
+
xlabel: str | None = None
|
|
94
|
+
ylabel: str | None = None
|
|
95
|
+
xticks: list | None = None
|
|
96
|
+
yticks: list | None = None
|
|
97
|
+
xticklabels: list | None = None
|
|
98
|
+
yticklabels: list | None = None
|
|
99
|
+
xticksminor: list | int = None
|
|
100
|
+
yticksminor: list | int = None
|
|
101
|
+
grid: dict | None = None
|
|
102
|
+
aspect: float | None = None
|
|
103
|
+
|
|
104
|
+
def set_xyaxes(self, ax):
|
|
105
|
+
if self.loglog is not None:
|
|
106
|
+
self.xscale = 'log'
|
|
107
|
+
self.yscale = 'log'
|
|
108
|
+
self.samexy = True
|
|
109
|
+
if self.xlim is not None:
|
|
110
|
+
self.xlim[0] = self.xlim[1] / self.loglog
|
|
111
|
+
if self.ylim is not None:
|
|
112
|
+
self.ylim[0] = self.ylim[1] / self.loglog
|
|
113
|
+
ax.set_xscale(self.xscale)
|
|
114
|
+
ax.set_yscale(self.yscale)
|
|
115
|
+
if self.samexy:
|
|
116
|
+
ax.set_xticks(ax.get_yticks())
|
|
117
|
+
ax.set_yticks(ax.get_xticks())
|
|
118
|
+
ax.set_aspect(1)
|
|
119
|
+
if self.xticks is None:
|
|
120
|
+
self.xticks = ax.get_xticks()
|
|
121
|
+
if self.yticks is None:
|
|
122
|
+
self.yticks = ax.get_yticks()
|
|
123
|
+
if self.xscale == 'log':
|
|
124
|
+
self.xticks, self.xticklabels = logticks(self.xticks, self.xlim)
|
|
125
|
+
if self.yscale == 'log':
|
|
126
|
+
self.yticks, self.yticklabels = logticks(self.yticks, self.ylim)
|
|
127
|
+
ax.set_xticks(self.xticks)
|
|
128
|
+
ax.set_yticks(self.yticks)
|
|
129
|
+
if self.xticksminor is not None:
|
|
130
|
+
if type(self.xticksminor) is int:
|
|
131
|
+
t = ax.get_xticks()
|
|
132
|
+
dt = t[1] - t[0]
|
|
133
|
+
t = np.r_[t[0] - dt, t, t[-1] + dt]
|
|
134
|
+
num = self.xticksminor * (len(t) - 1) + 1
|
|
135
|
+
self.xticksminor = np.linspace(t[0], t[-1], num)
|
|
136
|
+
ax.set_xticks(self.xticksminor, minor=True)
|
|
137
|
+
if self.yticksminor is not None:
|
|
138
|
+
if type(self.yticksminor) is int:
|
|
139
|
+
t = ax.get_yticks()
|
|
140
|
+
dt = t[1] - t[0]
|
|
141
|
+
t = np.r_[t[0] - dt, t, t[-1] + dt]
|
|
142
|
+
num = self.yticksminor * (len(t) - 1) + 1
|
|
143
|
+
self.yticksminor = np.linspace(t[0], t[-1], num)
|
|
144
|
+
ax.set_yticks(self.yticksminor, minor=True)
|
|
145
|
+
if self.xticklabels is not None:
|
|
146
|
+
ax.set_xticklabels(self.xticklabels)
|
|
147
|
+
if self.yticklabels is not None:
|
|
148
|
+
ax.set_yticklabels(self.yticklabels)
|
|
149
|
+
if self.xlabel is not None:
|
|
150
|
+
ax.set_xlabel(self.xlabel)
|
|
151
|
+
if self.ylabel is not None:
|
|
152
|
+
ax.set_ylabel(self.ylabel)
|
|
153
|
+
if self.xlim is not None:
|
|
154
|
+
ax.set_xlim(*self.xlim)
|
|
155
|
+
if self.ylim is not None:
|
|
156
|
+
ax.set_ylim(*self.ylim)
|
|
157
|
+
if self.grid is not None:
|
|
158
|
+
ax.grid(**({} if self.grid is True else self.grid))
|
|
159
|
+
if self.aspect is not None:
|
|
160
|
+
ax.set_aspect(self.aspect)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def set_minmax(data: np.ndarray, stretch: str, stretchscale: float,
|
|
164
|
+
stretchpower: float,
|
|
165
|
+
rms: float, kw: dict) -> np.ndarray:
|
|
166
|
+
"""Set vmin and vmax for color pcolormesh and RGB maps.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
data (np.ndarray): Plotted data.
|
|
170
|
+
stretch (str): 'log', 'asinh', 'power'. Any other means linear.
|
|
171
|
+
stretchscale (float): For the arcsinh strech.
|
|
172
|
+
stretchpower (float): For the power strech.
|
|
173
|
+
rms (float): RMS noise level.
|
|
174
|
+
kw (dict): Probably like {'vmin':0, 'vmax':1}.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
np.ndarray: Data clipped with the vmin and vmax.
|
|
178
|
+
"""
|
|
179
|
+
if type(stretch) is str:
|
|
180
|
+
data = [data]
|
|
181
|
+
rms = [rms]
|
|
182
|
+
stretch = [stretch]
|
|
183
|
+
stretchscale = [stretchscale]
|
|
184
|
+
if 'vmin' in kw:
|
|
185
|
+
kw['vmin'] = [kw['vmin']]
|
|
186
|
+
if 'vmax' in kw:
|
|
187
|
+
kw['vmax'] = [kw['vmax']]
|
|
188
|
+
z = (data, stretch, stretchscale, rms)
|
|
189
|
+
for i, (c, st, stsc, r) in enumerate(zip(*z)):
|
|
190
|
+
if stsc is None:
|
|
191
|
+
stsc = r
|
|
192
|
+
if st == 'log':
|
|
193
|
+
if np.any(c > 0):
|
|
194
|
+
c = np.log10(c.clip(np.nanmin(c[c > 0]), None))
|
|
195
|
+
elif st == 'asinh':
|
|
196
|
+
c = np.arcsinh(c / stsc)
|
|
197
|
+
elif st == 'power':
|
|
198
|
+
cmin = kw['min'][i] if 'vmin' in kw else r
|
|
199
|
+
c = c.clip(cmin, None)
|
|
200
|
+
c = ((c / cmin)**(1 - stretchpower) - 1) \
|
|
201
|
+
/ (1 - stretchpower) / np.log(10)
|
|
202
|
+
data[i] = c
|
|
203
|
+
n = len(data)
|
|
204
|
+
for m in ['vmin', 'vmax']:
|
|
205
|
+
if m in kw:
|
|
206
|
+
for i, (c, st, stsc, _) in enumerate(zip(*z)):
|
|
207
|
+
if st == 'log':
|
|
208
|
+
kw[m][i] = np.log10(kw[m][i])
|
|
209
|
+
elif st == 'asinh':
|
|
210
|
+
kw[m][i] = np.arcsinh(kw[m][i] / stsc)
|
|
211
|
+
elif st == 'power':
|
|
212
|
+
kw[m][i] = ((kw[m][i]/c.min())**(1 - stretchpower) - 1) \
|
|
213
|
+
/ (1 - stretchpower) / np.log(10)
|
|
214
|
+
else:
|
|
215
|
+
kw[m] = [None] * n
|
|
216
|
+
for i, (c, st, _, r) in enumerate(zip(*z)):
|
|
217
|
+
if m == 'vmin':
|
|
218
|
+
|
|
219
|
+
kw[m][i] = np.log10(r) if st == 'log' else np.nanmin(c)
|
|
220
|
+
else:
|
|
221
|
+
kw[m][i] = np.nanmax(c)
|
|
222
|
+
data = [c.clip(a, b) for c, a, b in zip(data, kw['vmin'], kw['vmax'])]
|
|
223
|
+
if n == 1:
|
|
224
|
+
data = data[0]
|
|
225
|
+
kw['vmin'] = kw['vmin'][0]
|
|
226
|
+
kw['vmax'] = kw['vmax'][0]
|
|
227
|
+
return data
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def kwargs2AstroData(kw: dict) -> AstroData:
|
|
231
|
+
"""Get AstroData and remove its arguments from kwargs.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
kw (dict): Parameters to make AstroData.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
AstroData: AstroData made from the parameters in kwargs.
|
|
238
|
+
"""
|
|
239
|
+
tmp = {}
|
|
240
|
+
d = AstroData(data=np.zeros((2, 2)))
|
|
241
|
+
for k in vars(d):
|
|
242
|
+
if k in kw:
|
|
243
|
+
tmp[k] = kw[k]
|
|
244
|
+
del kw[k]
|
|
245
|
+
if tmp == {}:
|
|
246
|
+
print('No argument given.')
|
|
247
|
+
return None
|
|
248
|
+
else:
|
|
249
|
+
d = AstroData(**tmp)
|
|
250
|
+
return d
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def kwargs2AstroFrame(kw: dict) -> AstroFrame:
|
|
254
|
+
"""Get AstroFrame from kwargs.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
kw (dict): Parameters to make AstroFrame.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
AstroFrame: AstroFrame made from the parameters in kwargs.
|
|
261
|
+
"""
|
|
262
|
+
tmp = {}
|
|
263
|
+
f = AstroFrame()
|
|
264
|
+
for k in vars(f):
|
|
265
|
+
if k in kw:
|
|
266
|
+
tmp[k] = kw[k]
|
|
267
|
+
f = AstroFrame(**tmp)
|
|
268
|
+
return f
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def kwargs2PlotAxes2D(kw: dict) -> PlotAxes2D:
|
|
272
|
+
"""Get PlotAxes2D and remove its arguments from kwargs.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
kw (dict): Parameters to make PlotAxes2D.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
PlotAxes2D: PlotAxes2D made from the parameters in kwargs.
|
|
279
|
+
"""
|
|
280
|
+
tmp = {}
|
|
281
|
+
d = PlotAxes2D()
|
|
282
|
+
for k in vars(d):
|
|
283
|
+
if k in kw:
|
|
284
|
+
tmp[k] = kw[k]
|
|
285
|
+
del kw[k]
|
|
286
|
+
d = PlotAxes2D(**tmp)
|
|
287
|
+
return d
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class PlotAstroData(AstroFrame):
|
|
291
|
+
"""Make a figure from 2D/3D FITS files or 2D/3D arrays.
|
|
292
|
+
|
|
293
|
+
Basic rules --- For 3D data, a 1D velocity array or a FITS file
|
|
294
|
+
with a velocity axis must be given to set up channels in each page.
|
|
295
|
+
For 2D/3D data, the spatial center can be read from a FITS file
|
|
296
|
+
or manually given.
|
|
297
|
+
len(v)=1 (default) means to make a 2D figure.
|
|
298
|
+
Spatial lengths are in the unit of arcsec, or au if dist (!= 1) is given.
|
|
299
|
+
Angles are in the unit of degree.
|
|
300
|
+
For region, line, arrow, label, and marker,
|
|
301
|
+
a single input can be treated without a list, e.g., anglelist=60,
|
|
302
|
+
as well as anglelist=[60].
|
|
303
|
+
Each element of poslist supposes a text coordinate
|
|
304
|
+
like '01h23m45.6s 01d23m45.6s' or a list of relative x and y
|
|
305
|
+
like [0.2, 0.3] (0 is left or bottom, 1 is right or top).
|
|
306
|
+
Parameters for original methods in matplotlib.axes.Axes can be
|
|
307
|
+
used as kwargs; see the default _kw for reference.
|
|
308
|
+
Position-velocity diagrams (pv=True) does not yet suppot region, line,
|
|
309
|
+
arrow, and segment because the units of abscissa and ordinate
|
|
310
|
+
are different.
|
|
311
|
+
|
|
312
|
+
kwargs is the arguments of AstroFrame to define plotting ranges.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
v (np.ndarray, optional): Used to set up channels if fitsimage not given. Defaults to [0].
|
|
316
|
+
vskip (int, optional): How many channels are skipped. Defaults to 1.
|
|
317
|
+
veldigit (int, optional): How many digits after the decimal point. Defaults to 2.
|
|
318
|
+
restfreq (float, optional): Used for velocity and brightness T. Defaults to None.
|
|
319
|
+
channelnumber (int, optional): Specify a channel number to make 2D maps. Defaults to None.
|
|
320
|
+
nrows (int, optional): Used for channel maps. Defaults to 4.
|
|
321
|
+
ncols (int, optional): Used for channel maps. Defaults to 6.
|
|
322
|
+
fontsize (int, optional): rc_Params['font.size']. None means 18 (2D) or 12 (3D). Defaults to None.
|
|
323
|
+
nancolor (str, optional): Color for masked regions. Defaults to white.
|
|
324
|
+
dpi (int, optional): Dot per inch for plotting an image. Defaults to 256.
|
|
325
|
+
figsize (tuple, optional): Defaults to None.
|
|
326
|
+
fig (optional): External plt.figure(). Defaults to None.
|
|
327
|
+
ax (optional): External fig.add_subplot(). Defaults to None.
|
|
328
|
+
"""
|
|
329
|
+
def __init__(self, v: np.ndarray = np.array([0]), vskip: int = 1,
|
|
330
|
+
veldigit: int = 2, restfreq: float | None = None,
|
|
331
|
+
channelnumber: int | None = None, nrows: int = 4, ncols: int = 6,
|
|
332
|
+
fontsize: int | None = None, nancolor: str = 'w', dpi: int = 256,
|
|
333
|
+
figsize: tuple[float, float] | None = None,
|
|
334
|
+
fig: object | None = None, ax: object | None = None, **kwargs) -> None:
|
|
335
|
+
super().__init__(**kwargs)
|
|
336
|
+
internalfig = fig is None
|
|
337
|
+
internalax = ax is None
|
|
338
|
+
if type(channelnumber) is int:
|
|
339
|
+
nrows = ncols = 1
|
|
340
|
+
if self.fitsimage is not None:
|
|
341
|
+
self.read(d := AstroData(fitsimage=self.fitsimage,
|
|
342
|
+
restfreq=restfreq, sigma=None))
|
|
343
|
+
v = d.v
|
|
344
|
+
if len(v) > 1:
|
|
345
|
+
dv = v[1] - v[0]
|
|
346
|
+
k0 = int(round((self.vmin - v[0]) / dv))
|
|
347
|
+
if k0 < 0:
|
|
348
|
+
vpre = v[0] - (1 + np.arange(-k0)[::-1]) * dv
|
|
349
|
+
v = np.append(vpre, v)
|
|
350
|
+
else:
|
|
351
|
+
v = v[k0:]
|
|
352
|
+
k1 = len(v) + int(round((self.vmax - v[-1]) / dv))
|
|
353
|
+
if k1 > len(v):
|
|
354
|
+
vpost = v[-1] + (1 + np.arange(k1 - len(v))) * dv
|
|
355
|
+
v = np.append(v, vpost)
|
|
356
|
+
else:
|
|
357
|
+
v = v[:k1]
|
|
358
|
+
if self.pv or v is None or len(v) == 1:
|
|
359
|
+
nv = nrows = ncols = npages = nchan = 1
|
|
360
|
+
else:
|
|
361
|
+
nv = len(v := v[::vskip])
|
|
362
|
+
npages = int(np.ceil(nv / nrows / ncols))
|
|
363
|
+
nchan = npages * nrows * ncols
|
|
364
|
+
v = np.r_[v, v[-1] + (np.arange(nchan-nv)+1) * (v[1] - v[0])]
|
|
365
|
+
if type(channelnumber) is int:
|
|
366
|
+
nchan = npages = 1
|
|
367
|
+
|
|
368
|
+
def nij2ch(n: int, i: int, j: int):
|
|
369
|
+
return n*nrows*ncols + i*ncols + j
|
|
370
|
+
|
|
371
|
+
def ch2nij(ch: int) -> tuple:
|
|
372
|
+
n = ch // (nrows*ncols)
|
|
373
|
+
i = (ch - n*nrows*ncols) // ncols
|
|
374
|
+
j = ch % ncols
|
|
375
|
+
return n, i, j
|
|
376
|
+
|
|
377
|
+
if fontsize is None:
|
|
378
|
+
fontsize = 18 if nchan == 1 else 12
|
|
379
|
+
set_rcparams(fontsize=fontsize, nancolor=nancolor, dpi=dpi)
|
|
380
|
+
ax = np.empty(nchan, dtype='object') if internalax else [ax]
|
|
381
|
+
for ch in range(nchan):
|
|
382
|
+
n, i, j = ch2nij(ch)
|
|
383
|
+
if figsize is None:
|
|
384
|
+
sqrt_a = (self.ymax - self.ymin) / (self.xmax - self.xmin)
|
|
385
|
+
sqrt_a = np.sqrt(np.abs(sqrt_a))
|
|
386
|
+
if nchan == 1:
|
|
387
|
+
figsize = (7 / sqrt_a, 5 * sqrt_a)
|
|
388
|
+
else:
|
|
389
|
+
figsize = (ncols * 2 / sqrt_a, max(nrows*2, 3) * sqrt_a)
|
|
390
|
+
if internalfig:
|
|
391
|
+
fig = plt.figure(n, figsize=figsize)
|
|
392
|
+
sharex = ax[nij2ch(n, i - 1, j)] if i > 0 else None
|
|
393
|
+
sharey = ax[nij2ch(n, i, j - 1)] if j > 0 else None
|
|
394
|
+
if internalax:
|
|
395
|
+
ax[ch] = fig.add_subplot(nrows, ncols, i*ncols + j + 1,
|
|
396
|
+
sharex=sharex, sharey=sharey)
|
|
397
|
+
if nchan > 1 or type(channelnumber) is int:
|
|
398
|
+
fig.subplots_adjust(hspace=0, wspace=0, right=0.87, top=0.87)
|
|
399
|
+
vellabel = v[ch] if channelnumber is None else v[channelnumber]
|
|
400
|
+
ax[ch].text(0.9 * self.rmax, 0.7 * self.rmax,
|
|
401
|
+
rf'${vellabel:.{veldigit:d}f}$', color='black',
|
|
402
|
+
backgroundcolor='white', zorder=20)
|
|
403
|
+
self.fig = None if internalfig else fig
|
|
404
|
+
self.ax = ax
|
|
405
|
+
self.rowcol = nrows * ncols
|
|
406
|
+
self.npages = npages
|
|
407
|
+
self.allchan = np.arange(nchan if channelnumber is None else nv)
|
|
408
|
+
self.bottomleft = nij2ch(np.arange(npages), nrows - 1, 0)
|
|
409
|
+
self.channelnumber = channelnumber
|
|
410
|
+
|
|
411
|
+
def vskipfill(c: np.ndarray, v_in: np.ndarray = None) -> np.ndarray:
|
|
412
|
+
"""Skip and fill channels with nan.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
c (np.ndarray): 2D or 3D arrays.
|
|
416
|
+
v_in (np.ndarray): 1D array.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
np.ndarray: 3D arrays skipped and filled with nan.
|
|
420
|
+
"""
|
|
421
|
+
if np.ndim(c) == 3:
|
|
422
|
+
if v_in is not None:
|
|
423
|
+
if (k0 := np.argmin(np.abs(v - v_in[0]))) > 0:
|
|
424
|
+
prenan = np.full((k0, *np.shape(c)[1:]), np.nan)
|
|
425
|
+
d = np.append(prenan, c, axis=0)
|
|
426
|
+
else:
|
|
427
|
+
d = c
|
|
428
|
+
d = d[::vskip]
|
|
429
|
+
else:
|
|
430
|
+
d = np.full((nv, *np.shape(c)), c)
|
|
431
|
+
n = nchan if channelnumber is None else nv
|
|
432
|
+
shape = (n - len(d), len(d[0]), len(d[0, 0]))
|
|
433
|
+
dnan = np.full(shape, d[0] * np.nan)
|
|
434
|
+
return np.concatenate((d, dnan), axis=0)
|
|
435
|
+
self.vskipfill = vskipfill
|
|
436
|
+
|
|
437
|
+
def add_region(self, patch: str = 'ellipse',
|
|
438
|
+
poslist: list[str | list[float, float]] = [],
|
|
439
|
+
majlist: list[float] = [], minlist: list[float] = [],
|
|
440
|
+
palist: list[float] = [],
|
|
441
|
+
include_chan: list[int] | None = None,
|
|
442
|
+
**kwargs) -> None:
|
|
443
|
+
"""Use add_patch() and Rectangle or Ellipse of matplotlib.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
patch (str, optional): 'ellipse' or 'rectangle'. Defaults to 'ellipse'.
|
|
447
|
+
poslist (list, optional): Text or relative center. Defaults to [].
|
|
448
|
+
majlist (list, optional): Ellipse major axis. Defaults to [].
|
|
449
|
+
minlist (list, optional): Ellipse minor axis. Defaults to [].
|
|
450
|
+
palist (list, optional): Position angle (north to east). Defaults to [].
|
|
451
|
+
include_chan (list, optional): None means all. Defaults to None.
|
|
452
|
+
"""
|
|
453
|
+
_kw = {'facecolor': 'none', 'edgecolor': 'gray',
|
|
454
|
+
'linewidth': 1.5, 'zorder': 10}
|
|
455
|
+
_kw.update(kwargs)
|
|
456
|
+
if include_chan is None:
|
|
457
|
+
include_chan = self.allchan
|
|
458
|
+
if not (patch in ['rectangle', 'ellipse']):
|
|
459
|
+
print('Only patch=\'rectangle\' or \'ellipse\' supported. ')
|
|
460
|
+
return -1
|
|
461
|
+
for x, y, width, height, angle in zip(*self.pos2xy(poslist),
|
|
462
|
+
*listing(minlist, majlist, palist)):
|
|
463
|
+
for ch, axnow in enumerate(self.ax):
|
|
464
|
+
if type(self.channelnumber) is int:
|
|
465
|
+
ch = self.channelnumber
|
|
466
|
+
if not (ch in include_chan):
|
|
467
|
+
continue
|
|
468
|
+
if self.fig is None:
|
|
469
|
+
plt.figure(ch // self.rowcol)
|
|
470
|
+
if patch == 'rectangle':
|
|
471
|
+
a = np.radians(angle)
|
|
472
|
+
xp = x - (width*np.cos(a) + height*np.sin(a)) / 2.
|
|
473
|
+
yp = y - (-width*np.sin(a) + height*np.cos(a)) / 2.
|
|
474
|
+
p = Rectangle
|
|
475
|
+
else:
|
|
476
|
+
xp, yp = x, y
|
|
477
|
+
p = Ellipse
|
|
478
|
+
p = p((xp, yp), width=width, height=height,
|
|
479
|
+
angle=angle * self.xdir, **_kw)
|
|
480
|
+
axnow.add_patch(p)
|
|
481
|
+
|
|
482
|
+
def add_beam(self,
|
|
483
|
+
beam: list[float | None, float | None, float | None] = [None, None, None],
|
|
484
|
+
beamcolor: str = 'gray',
|
|
485
|
+
poslist: list[str | list[float, float]] | None = None) -> None:
|
|
486
|
+
"""Use add_region().
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
beam (list, optional): [bmaj, bmin, bpa]. Defaults to [None, None, None].
|
|
490
|
+
beamcolor (str, optional): matplotlib color. Defaults to 'gray'.
|
|
491
|
+
poslist (list, optional): text or relative. Defaults to None.
|
|
492
|
+
"""
|
|
493
|
+
if None in beam:
|
|
494
|
+
print('No beam to plot.')
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
if poslist is None:
|
|
498
|
+
poslist = [max(0.35 * beam[0] / self.rmax, 0.1)] * 2
|
|
499
|
+
include_chan = self.bottomleft if self.channelnumber is None else self.allchan
|
|
500
|
+
self.add_region('ellipse', poslist, *beam,
|
|
501
|
+
include_chan=include_chan,
|
|
502
|
+
facecolor=beamcolor, edgecolor=None)
|
|
503
|
+
|
|
504
|
+
def add_marker(self, poslist: list[str | list[float, float]] = [],
|
|
505
|
+
include_chan: list[int] | None = None, **kwargs) -> None:
|
|
506
|
+
"""Use Axes.plot of matplotlib.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
poslist (list, optional): Text or relative. Defaults to [].
|
|
510
|
+
include_chan (list, optional): None means all. Defaults to None.
|
|
511
|
+
"""
|
|
512
|
+
_kw = {'marker': '+', 'ms': 10, 'mfc': 'gray',
|
|
513
|
+
'mec': 'gray', 'mew': 2, 'alpha': 1, 'zorder': 10}
|
|
514
|
+
_kw.update(kwargs)
|
|
515
|
+
if include_chan is None:
|
|
516
|
+
include_chan = self.allchan
|
|
517
|
+
for ch, axnow in enumerate(self.ax):
|
|
518
|
+
if type(self.channelnumber) is int:
|
|
519
|
+
ch = self.channelnumber
|
|
520
|
+
if not (ch in include_chan):
|
|
521
|
+
continue
|
|
522
|
+
for x, y in zip(*self.pos2xy(poslist)):
|
|
523
|
+
axnow.plot(x, y, **_kw)
|
|
524
|
+
|
|
525
|
+
def add_text(self, poslist: list[str | list[float, float]] = [],
|
|
526
|
+
slist: list[str] = [],
|
|
527
|
+
include_chan: list[int] | None = None, **kwargs) -> None:
|
|
528
|
+
"""Use Axes.text of matplotlib.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
poslist (list, optional): Text or relative. Defaults to [].
|
|
532
|
+
slist (list, optional): List of text. Defaults to [].
|
|
533
|
+
include_chan (list, optional): None means all. Defaults to None.
|
|
534
|
+
"""
|
|
535
|
+
_kw = {'color': 'gray', 'fontsize': 15, 'ha': 'center',
|
|
536
|
+
'va': 'center', 'zorder': 10}
|
|
537
|
+
_kw.update(kwargs)
|
|
538
|
+
if include_chan is None:
|
|
539
|
+
include_chan = self.allchan
|
|
540
|
+
for ch, axnow in enumerate(self.ax):
|
|
541
|
+
if type(self.channelnumber) is int:
|
|
542
|
+
ch = self.channelnumber
|
|
543
|
+
if not (ch in include_chan):
|
|
544
|
+
continue
|
|
545
|
+
for x, y, s in zip(*self.pos2xy(poslist), listing(slist)):
|
|
546
|
+
axnow.text(x=x, y=y, s=s, **_kw)
|
|
547
|
+
|
|
548
|
+
def add_line(self, poslist: list[str | list[float, float]] = [],
|
|
549
|
+
anglelist: list[float] = [],
|
|
550
|
+
rlist: list[float] = [], include_chan: list[int] | None = None,
|
|
551
|
+
**kwargs) -> None:
|
|
552
|
+
"""Use Axes.plot of matplotlib.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
poslist (list, optional): Text or relative. Defaults to [].
|
|
556
|
+
anglelist (list, optional): North to east. Defaults to [].
|
|
557
|
+
rlist (list, optional): List of radius. Defaults to [].
|
|
558
|
+
include_chan (list, optional): None means all. Defaults to None.
|
|
559
|
+
"""
|
|
560
|
+
_kw = {'color': 'gray', 'linewidth': 1.5,
|
|
561
|
+
'linestyle': '-', 'zorder': 10}
|
|
562
|
+
_kw.update(kwargs)
|
|
563
|
+
if include_chan is None:
|
|
564
|
+
include_chan = self.allchan
|
|
565
|
+
for ch, axnow in enumerate(self.ax):
|
|
566
|
+
if type(self.channelnumber) is int:
|
|
567
|
+
ch = self.channelnumber
|
|
568
|
+
if not (ch in include_chan):
|
|
569
|
+
continue
|
|
570
|
+
alist = np.radians(anglelist)
|
|
571
|
+
for x, y, a, r in zip(*self.pos2xy(poslist), *listing(alist, rlist)):
|
|
572
|
+
axnow.plot([x, x + r * np.sin(a)],
|
|
573
|
+
[y, y + r * np.cos(a)], **_kw)
|
|
574
|
+
|
|
575
|
+
def add_arrow(self, poslist: list[str | list[float, float]] = [],
|
|
576
|
+
anglelist: list[float] = [],
|
|
577
|
+
rlist: list[float] = [], include_chan: list[int] | None = None,
|
|
578
|
+
**kwargs) -> None:
|
|
579
|
+
"""Use Axes.quiver of matplotlib.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
poslist (list, optional): Text or relative. Defaults to [].
|
|
583
|
+
anglelist (list, optional): North to east. Defaults to [].
|
|
584
|
+
rlist (list, optional): List of radius. Defaults to [].
|
|
585
|
+
include_chan (list, optional): None means all. Defaults to None.
|
|
586
|
+
"""
|
|
587
|
+
_kw = {'color': 'gray', 'width': 0.012,
|
|
588
|
+
'headwidth': 5, 'headlength': 5, 'zorder': 10}
|
|
589
|
+
_kw.update(kwargs)
|
|
590
|
+
if include_chan is None:
|
|
591
|
+
include_chan = self.allchan
|
|
592
|
+
for ch, axnow in enumerate(self.ax):
|
|
593
|
+
if type(self.channelnumber) is int:
|
|
594
|
+
ch = self.channelnumber
|
|
595
|
+
if not (ch in include_chan):
|
|
596
|
+
continue
|
|
597
|
+
alist = np.radians(anglelist)
|
|
598
|
+
for x, y, a, r in zip(*self.pos2xy(poslist), *listing(alist, rlist)):
|
|
599
|
+
axnow.quiver(x, y, r * np.sin(a), r * np.cos(a),
|
|
600
|
+
angles='xy', scale_units='xy', scale=1,
|
|
601
|
+
**_kw)
|
|
602
|
+
|
|
603
|
+
def add_scalebar(self, length: float = 0, label: str = '',
|
|
604
|
+
color: str = 'gray', barpos: tuple[float, float] = (0.8, 0.12),
|
|
605
|
+
fontsize: float = None, linewidth: float = 3,
|
|
606
|
+
bbox: dict = {'alpha': 0}) -> None:
|
|
607
|
+
"""Use Axes.text and Axes.plot of matplotlib.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
length (float, optional): In the unit of arcsec. Defaults to 0.
|
|
611
|
+
label (str, optional): Text like '100 au'. Defaults to ''.
|
|
612
|
+
color (str, optional): Same for bar and label. Defaults to 'gray'.
|
|
613
|
+
barpos (tuple, optional): Relative position. Defaults to (0.8, 0.12).
|
|
614
|
+
fontsize (float, optional): None means 15 if one channel else 20. Defaults to None.
|
|
615
|
+
linewidth (float, optional): Width of the bar. Defaults to 3.
|
|
616
|
+
"""
|
|
617
|
+
if length == 0 or label == '':
|
|
618
|
+
print('Please input length and label.')
|
|
619
|
+
return -1
|
|
620
|
+
if fontsize is None:
|
|
621
|
+
fontsize = 20 if len(self.ax) == 1 else 15
|
|
622
|
+
for ch, axnow in enumerate(self.ax):
|
|
623
|
+
if not (ch in self.bottomleft):
|
|
624
|
+
continue
|
|
625
|
+
x, y = self.pos2xy([barpos[0], barpos[1] - 0.012])
|
|
626
|
+
axnow.text(x[0], y[0], label, color=color, size=fontsize,
|
|
627
|
+
ha='center', va='top', bbox=bbox, zorder=10)
|
|
628
|
+
x, y = self.pos2xy([barpos[0], barpos[1] + 0.012])
|
|
629
|
+
axnow.plot([x[0] - length/2., x[0] + length/2.], [y[0], y[0]],
|
|
630
|
+
'-', linewidth=linewidth, color=color)
|
|
631
|
+
|
|
632
|
+
def add_color(self, xskip: int = 1, yskip: int = 1,
|
|
633
|
+
stretch: str = 'linear',
|
|
634
|
+
stretchscale: float | None = None,
|
|
635
|
+
stretchpower: float = 0,
|
|
636
|
+
show_cbar: bool = True, cblabel: str | None = None,
|
|
637
|
+
cbformat: float = '%.1e', cbticks: list[float] | None = None,
|
|
638
|
+
cbticklabels: list[str] | None = None, cblocation: str = 'right',
|
|
639
|
+
show_beam: bool = True, beamcolor: str = 'gray',
|
|
640
|
+
**kwargs) -> None:
|
|
641
|
+
"""Use Axes.pcolormesh of matplotlib. kwargs must include the arguments of AstroData to specify the data to be plotted.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
xskip, yskip (int, optional): Spatial pixel skip. Defaults to 1.
|
|
645
|
+
stretch (str, optional): 'log' means the mapped data are logarithmic. 'asinh' means the mapped data are arc sin hyperbolic. Defaults to 'linear'.
|
|
646
|
+
stretchscale (float, optional): color scale is asinh(data / stretchscale). Defaults to None.
|
|
647
|
+
stretchpower (float, optional): color scale is ((data / vmin)**(1 - stretchpower) - 1) / (1 - stretchpower) / ln(10). 0 means the linear scale. 1 means the logarithmic scale. Defaults to 0.
|
|
648
|
+
show_cbar (bool, optional): Show color bar. Defaults to True.
|
|
649
|
+
cblabel (str, optional): Colorbar label. Defaults to None.
|
|
650
|
+
cbformat (float, optional): Format for ticklabels of colorbar. Defaults to '%.1e'.
|
|
651
|
+
cbticks (list, optional): Ticks of colorbar. Defaults to None.
|
|
652
|
+
cbticklabels (list, optional): Ticklabels of colorbar. Defaults to None.
|
|
653
|
+
cblocation (str, optional): 'left', 'top', 'left', 'right'. Only for 2D images. Defaults to 'right'.
|
|
654
|
+
show_beam (bool, optional): Defaults to True.
|
|
655
|
+
beamcolor (str, optional): Matplotlib color. Defaults to 'gray'.
|
|
656
|
+
"""
|
|
657
|
+
_kw = {'cmap': 'cubehelix', 'alpha': 1, 'edgecolors': 'none', 'zorder': 1}
|
|
658
|
+
_kw.update(kwargs)
|
|
659
|
+
d = kwargs2AstroData(_kw)
|
|
660
|
+
self.read(d, xskip, yskip)
|
|
661
|
+
c, x, y, v, beam, sigma = d.data, d.x, d.y, d.v, d.beam, d.sigma
|
|
662
|
+
bunit = d.bunit
|
|
663
|
+
self.beam = beam
|
|
664
|
+
self.sigma = sigma
|
|
665
|
+
if stretchscale is None:
|
|
666
|
+
stretchscale = sigma
|
|
667
|
+
cmin_org = _kw['vmin'] if 'vmin' in _kw else sigma
|
|
668
|
+
c = set_minmax(c, stretch, stretchscale, stretchpower, sigma, _kw)
|
|
669
|
+
c = self.vskipfill(c, v)
|
|
670
|
+
if type(self.channelnumber) is int:
|
|
671
|
+
c = [c[self.channelnumber]]
|
|
672
|
+
for axnow, cnow in zip(self.ax, c):
|
|
673
|
+
p = axnow.pcolormesh(x, y, cnow, **_kw)
|
|
674
|
+
for ch in self.bottomleft:
|
|
675
|
+
if not show_cbar:
|
|
676
|
+
break
|
|
677
|
+
if cblabel is None:
|
|
678
|
+
cblabel = bunit
|
|
679
|
+
if self.fig is None:
|
|
680
|
+
fig = plt.figure(ch // self.rowcol)
|
|
681
|
+
else:
|
|
682
|
+
fig = self.fig
|
|
683
|
+
if len(self.ax) == 1:
|
|
684
|
+
ax = self.ax[0]
|
|
685
|
+
cb = fig.colorbar(p, ax=ax, label=cblabel,
|
|
686
|
+
format=cbformat, location=cblocation)
|
|
687
|
+
else:
|
|
688
|
+
cax = plt.axes([0.88, 0.105, 0.015, 0.77])
|
|
689
|
+
cb = fig.colorbar(p, cax=cax, label=cblabel, format=cbformat)
|
|
690
|
+
cb.ax.tick_params(labelsize=14)
|
|
691
|
+
font = mpl.font_manager.FontProperties(size=16)
|
|
692
|
+
cb.ax.yaxis.label.set_font_properties(font)
|
|
693
|
+
if cbticks is not None:
|
|
694
|
+
if stretch == 'log':
|
|
695
|
+
cbticks = np.log10(cbticks)
|
|
696
|
+
elif stretch == 'asinh':
|
|
697
|
+
cbticks = np.arcsinh(np.array(cbticks) / stretchscale)
|
|
698
|
+
elif stretch == 'power':
|
|
699
|
+
cbticks = (np.array(cbticks) / cmin_org)**(1 - stretchpower)
|
|
700
|
+
cbticks = (cbticks - 1) / (1 - stretchpower) / np.log(10)
|
|
701
|
+
cb.set_ticks(cbticks)
|
|
702
|
+
if cbticklabels is not None:
|
|
703
|
+
cb.set_ticklabels(cbticklabels)
|
|
704
|
+
elif stretch in ['log', 'asinh', 'power']:
|
|
705
|
+
t = cb.get_ticks()
|
|
706
|
+
t = t[(_kw['vmin'] < t) * (t < _kw['vmax'])]
|
|
707
|
+
cb.set_ticks(t)
|
|
708
|
+
if stretch == 'log':
|
|
709
|
+
ticklin = 10**t
|
|
710
|
+
elif stretch == 'asinh':
|
|
711
|
+
ticklin = np.sinh(t) * stretchscale
|
|
712
|
+
elif stretch == 'power':
|
|
713
|
+
ticklin = 1 + (1 - stretchpower) * np.log(10) * t
|
|
714
|
+
ticklin = cmin_org * ticklin**(1 / (1 - stretchpower))
|
|
715
|
+
cb.set_ticklabels([f'{d:{cbformat[1:]}}' for d in ticklin])
|
|
716
|
+
if show_beam and not self.pv:
|
|
717
|
+
self.add_beam(beam, beamcolor)
|
|
718
|
+
|
|
719
|
+
def add_contour(self, xskip: int = 1, yskip: int = 1,
|
|
720
|
+
levels: list[float] = [-12, -6, -3, 3, 6, 12, 24, 48, 96, 192, 384],
|
|
721
|
+
show_beam: bool = True, beamcolor: str = 'gray',
|
|
722
|
+
**kwargs) -> None:
|
|
723
|
+
"""Use Axes.contour of matplotlib. kwargs must include the arguments of AstroData to specify the data to be plotted.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
xskip, yskip (int, optional): Spatial pixel skip. Defaults to 1.
|
|
727
|
+
levels (list, optional): Contour levels in the unit of sigma. Defaults to [-12,-6,-3,3,6,12,24,48,96,192,384].
|
|
728
|
+
show_beam (bool, optional): Defaults to True.
|
|
729
|
+
beamcolor (str, optional): Matplotlib color. Defaults to 'gray'.
|
|
730
|
+
"""
|
|
731
|
+
_kw = {'colors': 'gray', 'linewidths': 1.0, 'zorder': 2}
|
|
732
|
+
_kw.update(kwargs)
|
|
733
|
+
d = kwargs2AstroData(_kw)
|
|
734
|
+
self.read(d, xskip, yskip)
|
|
735
|
+
c, x, y, v, beam, sigma = d.data, d.x, d.y, d.v, d.beam, d.sigma
|
|
736
|
+
self.beam = beam
|
|
737
|
+
self.sigma = sigma
|
|
738
|
+
c = self.vskipfill(c, v)
|
|
739
|
+
if type(self.channelnumber) is int:
|
|
740
|
+
c = [c[self.channelnumber]]
|
|
741
|
+
for axnow, cnow in zip(self.ax, c):
|
|
742
|
+
axnow.contour(x, y, cnow, np.sort(levels) * sigma, **_kw)
|
|
743
|
+
if show_beam and not self.pv:
|
|
744
|
+
self.add_beam(beam, beamcolor)
|
|
745
|
+
|
|
746
|
+
def add_segment(self, ampfits: str = None, angfits: str = None,
|
|
747
|
+
Ufits: str = None, Qfits: str = None,
|
|
748
|
+
xskip: int = 1, yskip: int = 1,
|
|
749
|
+
amp: list[np.ndarray] | None = None,
|
|
750
|
+
ang: list[np.ndarray] | None = None,
|
|
751
|
+
stU: list[np.ndarray] | None = None,
|
|
752
|
+
stQ: list[np.ndarray] | None = None,
|
|
753
|
+
ampfactor: float = 1., angonly: bool = False,
|
|
754
|
+
rotation: float = 0.,
|
|
755
|
+
cutoff: float = 3.,
|
|
756
|
+
show_beam: bool = True, beamcolor: str = 'gray',
|
|
757
|
+
**kwargs) -> None:
|
|
758
|
+
"""Use Axes.quiver of matplotlib. kwargs must include the arguments of AstroData to specify the data to be plotted. fitsimage = [ampfits, angfits, Ufits, Qfits]. data = [amp, ang, stU, stQ].
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
ampfits (str, optional): In put fits name. Length of segment. Defaults to None.
|
|
762
|
+
angfits (str, optional): In put fits name. North to east. Defaults to None.
|
|
763
|
+
Ufits (str, optional): In put fits name. Stokes U. Defaults to None.
|
|
764
|
+
Qfits (str, optional): In put fits name. Stokes Q. Defaults to None.
|
|
765
|
+
xskip, yskip (int, optional): Spatial pixel skip. Defaults to 1.
|
|
766
|
+
amp (list, optional): Length of segment. Defaults to None.
|
|
767
|
+
ang (list, optional): North to east. Defaults to None.
|
|
768
|
+
stU (list, optional): Stokes U. Defaults to None.
|
|
769
|
+
stQ (list, optional): Stokes Q. Defaults to None.
|
|
770
|
+
ampfactor (float, optional): Length of segment is amp times ampfactor. Defaults to 1..
|
|
771
|
+
angonly (bool, optional): True means amp=1 for all. Defaults to False.
|
|
772
|
+
rotation (float, optional): Segment angle is ang + rotation. Defaults to 0..
|
|
773
|
+
cutoff (float, optional): Used when amp and ang are calculated from Stokes U and Q. In the unit of sigma. Defaults to 3..
|
|
774
|
+
sigma (str or float, optional): Noise level or method for measuring it. Defaults to 'out'.
|
|
775
|
+
show_beam (bool, optional): Defaults to True.
|
|
776
|
+
beamcolor (str, optional): Matplotlib color. Defaults to 'gray'.
|
|
777
|
+
"""
|
|
778
|
+
_kw = {'angles': 'xy', 'scale_units': 'xy', 'color': 'gray',
|
|
779
|
+
'pivot': 'mid', 'headwidth': 0, 'headlength': 0,
|
|
780
|
+
'headaxislength': 0, 'width': 0.007, 'zorder': 3}
|
|
781
|
+
_kw.update(kwargs)
|
|
782
|
+
_kw['data'] = [amp, ang, stU, stQ]
|
|
783
|
+
_kw['fitsimage'] = [ampfits, angfits, Ufits, Qfits]
|
|
784
|
+
d = kwargs2AstroData(_kw)
|
|
785
|
+
self.read(d, xskip, yskip)
|
|
786
|
+
c, x, y, v, beam, sigma = d.data, d.x, d.y, d.v, d.beam, d.sigma
|
|
787
|
+
amp, ang, stU, stQ = c
|
|
788
|
+
sigmaU, sigmaQ = sigma[2:]
|
|
789
|
+
self.beam = beam
|
|
790
|
+
beam = [beam[i] for i in range(4) if beam[i][0] is not None][0]
|
|
791
|
+
if stU is not None and stQ is not None:
|
|
792
|
+
self.sigma = sigma = (sigmaU + sigmaQ) / 2.
|
|
793
|
+
ang = np.degrees(np.arctan2(stU, stQ) / 2.)
|
|
794
|
+
amp = np.hypot(stU, stQ)
|
|
795
|
+
amp[amp < cutoff * sigma] = np.nan
|
|
796
|
+
if amp is None:
|
|
797
|
+
amp = np.ones_like(ang)
|
|
798
|
+
if angonly:
|
|
799
|
+
amp = np.sign(amp)**2
|
|
800
|
+
amp = amp / np.nanmax(amp)
|
|
801
|
+
U = ampfactor * amp * np.sin(np.radians(ang + rotation))
|
|
802
|
+
V = ampfactor * amp * np.cos(np.radians(ang + rotation))
|
|
803
|
+
U = self.vskipfill(U, v)
|
|
804
|
+
V = self.vskipfill(V, v)
|
|
805
|
+
if type(self.channelnumber) is int:
|
|
806
|
+
U = [U[self.channelnumber]]
|
|
807
|
+
V = [V[self.channelnumber]]
|
|
808
|
+
_kw['scale'] = 1 if len(x) == 1 else 1. / np.abs(x[1] - x[0])
|
|
809
|
+
for axnow, unow, vnow in zip(self.ax, U, V):
|
|
810
|
+
axnow.quiver(x, y, unow, vnow, **_kw)
|
|
811
|
+
if show_beam and not self.pv:
|
|
812
|
+
self.add_beam(beam, beamcolor)
|
|
813
|
+
|
|
814
|
+
def add_rgb(self, xskip: int = 1, yskip: int = 1,
|
|
815
|
+
stretch: list[str, str, str] = ['linear'] * 3,
|
|
816
|
+
stretchscale: list[float | None, float | None, float | None] = [None] * 3,
|
|
817
|
+
stretchpower: float = 0,
|
|
818
|
+
show_beam: bool = True,
|
|
819
|
+
beamcolor: list[str, str, str] = ['red', 'green', 'blue'],
|
|
820
|
+
**kwargs) -> None:
|
|
821
|
+
"""Use PIL.Image and imshow of matplotlib. kwargs must include the arguments of AstroData to specify the data to be plotted. A three-element array ([red, green, blue]) is supposed for all arguments, except for xskip, yskip and show_beam, including vmax and vmin.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
xskip, yskip (int, optional): Spatial pixel skip. Defaults to 1.
|
|
825
|
+
stretch (str, optional): 'log' means the mapped data are logarithmic. 'asinh' means the mapped data are arc sin hyperbolic. Defaults to 'linear'.
|
|
826
|
+
stretchscale (float, optional): color scale is asinh(data / stretchscale). Defaults to None.
|
|
827
|
+
stretchpower (float, optional): color scale is ((data / vmin)**(1 - stretchpower) - 1) / (1 - stretchpower) / ln(10). 0 means the linear scale. 1 means the logarithmic scale. Defaults to 0.
|
|
828
|
+
show_beam (bool, optional): Defaults to True.
|
|
829
|
+
beamcolor (str, optional): Matplotlib color. Defaults to 'gray'.
|
|
830
|
+
"""
|
|
831
|
+
from PIL import Image
|
|
832
|
+
|
|
833
|
+
_kw = {}
|
|
834
|
+
_kw.update(kwargs)
|
|
835
|
+
d = kwargs2AstroData(_kw)
|
|
836
|
+
self.read(d, xskip, yskip)
|
|
837
|
+
c, x, y, v, beam, sigma = d.data, d.x, d.y, d.v, d.beam, d.sigma
|
|
838
|
+
self.beam = beam
|
|
839
|
+
self.sigma = sigma
|
|
840
|
+
for i in range(len(stretchscale)):
|
|
841
|
+
if stretchscale[i] is None:
|
|
842
|
+
stretchscale[i] = sigma[i]
|
|
843
|
+
c = set_minmax(c, stretch, stretchscale, stretchpower, sigma, _kw)
|
|
844
|
+
if not (np.shape(c[0]) == np.shape(c[1]) == np.shape(c[2])):
|
|
845
|
+
print('RGB shapes mismatch. Skip add_rgb.')
|
|
846
|
+
return -1
|
|
847
|
+
|
|
848
|
+
for i in range(3):
|
|
849
|
+
c[i] = (c[i] - _kw['vmin'][i]) \
|
|
850
|
+
/ (_kw['vmax'][i] - _kw['vmin'][i]) * 255
|
|
851
|
+
c[i] = self.vskipfill(c[i], v)
|
|
852
|
+
size = np.shape(c[0][0])
|
|
853
|
+
for axnow, red, green, blue in zip(self.ax, *c):
|
|
854
|
+
im = Image.new('RGB', size[::-1], (128, 128, 128))
|
|
855
|
+
rgb = [red[::-1, :], green[::-1, :], blue[::-1, :]]
|
|
856
|
+
for j in range(size[0]):
|
|
857
|
+
for i in range(size[1]):
|
|
858
|
+
value = tuple(int(a[j, i]) for a in rgb)
|
|
859
|
+
im.putpixel((i, j), value)
|
|
860
|
+
axnow.imshow(im, extent=[x[0], x[-1], y[0], y[-1]])
|
|
861
|
+
axnow.set_aspect(np.abs((x[-1]-x[0]) / (y[-1]-y[0])))
|
|
862
|
+
if show_beam and not self.pv:
|
|
863
|
+
for i in range(3):
|
|
864
|
+
self.add_beam(beam[i], beamcolor[i])
|
|
865
|
+
|
|
866
|
+
def set_axis(self, title: dict | str | None = None, **kwargs) -> None:
|
|
867
|
+
"""Use Axes.set_* of matplotlib. kwargs can include the arguments of PlotAxes2D to adjust x and y axis.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
title (dict, optional): str means set_title(str) for 2D or fig.suptitle(str) for 3D. Defaults to None.
|
|
871
|
+
"""
|
|
872
|
+
_kw = {}
|
|
873
|
+
_kw.update(kwargs)
|
|
874
|
+
offunit = '(arcsec)' if self.dist == 1 else '(au)'
|
|
875
|
+
if self.pv:
|
|
876
|
+
offlabel = f'Offset {offunit}'
|
|
877
|
+
vellabel = r'Velocity (km s$^{-1})$'
|
|
878
|
+
if 'xlabel' not in _kw:
|
|
879
|
+
_kw['xlabel'] = vellabel if self.swapxy else offlabel
|
|
880
|
+
if 'ylabel' not in _kw:
|
|
881
|
+
_kw['ylabel'] = offlabel if self.swapxy else vellabel
|
|
882
|
+
_kw['samexy'] = False
|
|
883
|
+
else:
|
|
884
|
+
ralabel, declabel = f'R.A. {offunit}', f'Dec. {offunit}'
|
|
885
|
+
if 'xlabel' not in _kw:
|
|
886
|
+
_kw['xlabel'] = declabel if self.swapxy else ralabel
|
|
887
|
+
if 'ylabel' not in _kw:
|
|
888
|
+
_kw['ylabel'] = ralabel if self.swapxy else declabel
|
|
889
|
+
if 'xlim' not in _kw:
|
|
890
|
+
_kw['xlim'] = self.Xlim
|
|
891
|
+
if 'ylim' not in _kw:
|
|
892
|
+
_kw['ylim'] = self.Ylim
|
|
893
|
+
pa2 = kwargs2PlotAxes2D(_kw)
|
|
894
|
+
for ch, axnow in enumerate(self.ax):
|
|
895
|
+
pa2.set_xyaxes(axnow)
|
|
896
|
+
if not (ch in self.bottomleft):
|
|
897
|
+
plt.setp(axnow.get_xticklabels(), visible=False)
|
|
898
|
+
plt.setp(axnow.get_yticklabels(), visible=False)
|
|
899
|
+
axnow.set_xlabel('')
|
|
900
|
+
axnow.set_ylabel('')
|
|
901
|
+
if len(self.ax) == 1:
|
|
902
|
+
if self.fig is None:
|
|
903
|
+
plt.figure(0).tight_layout()
|
|
904
|
+
if title is not None:
|
|
905
|
+
if len(self.ax) > 1:
|
|
906
|
+
t = {'y': 0.9}
|
|
907
|
+
t_in = {'t': title} if type(title) is str else title
|
|
908
|
+
t.update(t_in)
|
|
909
|
+
for i in range(self.npages):
|
|
910
|
+
fig = plt.figure(i)
|
|
911
|
+
fig.suptitle(**t)
|
|
912
|
+
else:
|
|
913
|
+
t = {'label': title} if type(title) is str else title
|
|
914
|
+
axnow.set_title(**t)
|
|
915
|
+
|
|
916
|
+
def set_axis_radec(self, center: str | None = None,
|
|
917
|
+
xlabel: str = 'R.A. (ICRS)',
|
|
918
|
+
ylabel: str = 'Dec. (ICRS)',
|
|
919
|
+
nticksminor: int = 2,
|
|
920
|
+
grid: dict | None = None, title: dict | None = None) -> None:
|
|
921
|
+
"""Use ax.set_* of matplotlib.
|
|
922
|
+
|
|
923
|
+
Args:
|
|
924
|
+
center (str, optional): Defaults to None, initial one.
|
|
925
|
+
xlabel (str, optional): Defaults to 'R.A. (ICRS)'.
|
|
926
|
+
ylabel (str, optional): Defaults to 'Dec. (ICRS)'.
|
|
927
|
+
nticksminor (int, optional): Interval ratio of major and minor ticks. Defaults to 2.
|
|
928
|
+
grid (dict, optional): True means merely grid(). Defaults to None.
|
|
929
|
+
title (dict, optional): str means set_title(str) for 2D or fig.suptitle(str) for 3D. Defaults to None.
|
|
930
|
+
"""
|
|
931
|
+
if self.rmax > 50.:
|
|
932
|
+
print('WARNING: set_axis_radec() is not supported '
|
|
933
|
+
+ 'with rmax>50 yet.')
|
|
934
|
+
if center is None:
|
|
935
|
+
center = self.center
|
|
936
|
+
if center is None:
|
|
937
|
+
center = '00h00m00s 00d00m00s'
|
|
938
|
+
dec = np.radians(coord2xy(center)[1])
|
|
939
|
+
|
|
940
|
+
def get_sec(x, i):
|
|
941
|
+
return x.split(' ')[i].split('m')[1].strip('s')
|
|
942
|
+
|
|
943
|
+
def get_hmdm(x, i):
|
|
944
|
+
return x.split(' ')[i].split('m')[0]
|
|
945
|
+
|
|
946
|
+
ra_s = get_sec(center, 0)
|
|
947
|
+
dec_s = get_sec(center, 1)
|
|
948
|
+
log2r = np.log10(2. * self.rmax)
|
|
949
|
+
n = np.array([-3, -2, -1, 0, 1, 2, 3])
|
|
950
|
+
|
|
951
|
+
def makegrid(second, mode):
|
|
952
|
+
second = float(second)
|
|
953
|
+
if mode == 'ra':
|
|
954
|
+
scale, factor, sec = 1.5, 15 * np.cos(dec), r'$^{\rm s}$'
|
|
955
|
+
else:
|
|
956
|
+
scale, factor, sec = 0.5, 1, r'$^{\rm \prime\prime}$'
|
|
957
|
+
sec = r'.$\hspace{-0.4}$' + sec
|
|
958
|
+
dorder = log2r - scale - (order := np.floor(log2r - scale))
|
|
959
|
+
if 0.00 < dorder <= 0.33:
|
|
960
|
+
g = 1
|
|
961
|
+
elif 0.33 < dorder <= 0.68:
|
|
962
|
+
g = 2
|
|
963
|
+
elif 0.68 < dorder <= 1.00:
|
|
964
|
+
g = 5
|
|
965
|
+
g *= 10**order
|
|
966
|
+
decimals = max(-int(order), -1)
|
|
967
|
+
rounded = round(second, decimals)
|
|
968
|
+
lastdigit = round(rounded // 10**(-decimals-1) % 100 / 10) % 10
|
|
969
|
+
rounded -= lastdigit * 10**(-decimals) % g
|
|
970
|
+
ticks = (n*g - second + rounded) * factor
|
|
971
|
+
ticksminor = np.linspace(ticks[0], ticks[-1], 6*nticksminor + 1)
|
|
972
|
+
decimals = max(decimals, 0)
|
|
973
|
+
if mode == 'ra':
|
|
974
|
+
xy, i = [ticks / 3600., ticks * 0], 0
|
|
975
|
+
else:
|
|
976
|
+
xy, i = [ticks * 0, ticks / 3600.], 1
|
|
977
|
+
tickvalues = xy2coord(xy, center)
|
|
978
|
+
tickvalues = [float(get_sec(t, i)) for t in tickvalues]
|
|
979
|
+
tickvalues = np.divmod(tickvalues, 1)
|
|
980
|
+
ticklabels = [f'{int(i):02d}{sec}' + f'{j:.{decimals:d}f}'[2:]
|
|
981
|
+
for i, j in zip(*tickvalues)]
|
|
982
|
+
return ticks, ticksminor, ticklabels
|
|
983
|
+
|
|
984
|
+
xticks, xticksminor, xticklabels = makegrid(ra_s, 'ra')
|
|
985
|
+
yticks, yticksminor, yticklabels = makegrid(dec_s, 'dec')
|
|
986
|
+
ra_hm = get_hmdm(xy2coord([xticks[3] / 3600., 0], center), 0)
|
|
987
|
+
dec_dm = get_hmdm(xy2coord([0, yticks[3] / 3600.], center), 1)
|
|
988
|
+
ra_hm = ra_hm.replace('h', r'$^{\rm h}$') + r'$^{\rm m}$'
|
|
989
|
+
dec_dm = dec_dm.replace('d', r'$^{\circ}$') + r'$^{\prime}$'
|
|
990
|
+
xticklabels[3] = ra_hm + xticklabels[3]
|
|
991
|
+
yticklabels[3] = dec_dm + '\n' + yticklabels[3]
|
|
992
|
+
pa2 = PlotAxes2D(True, None, 'linear', 'linear', self.Xlim, self.Ylim,
|
|
993
|
+
xlabel, ylabel, xticks, yticks, xticklabels,
|
|
994
|
+
yticklabels, xticksminor, yticksminor, grid)
|
|
995
|
+
for ch, axnow in enumerate(self.ax):
|
|
996
|
+
pa2.set_xyaxes(axnow)
|
|
997
|
+
if not (ch in self.bottomleft):
|
|
998
|
+
plt.setp(axnow.get_xticklabels(), visible=False)
|
|
999
|
+
plt.setp(axnow.get_yticklabels(), visible=False)
|
|
1000
|
+
axnow.set_xlabel('')
|
|
1001
|
+
axnow.set_ylabel('')
|
|
1002
|
+
if len(self.ax) == 1:
|
|
1003
|
+
if self.fig is None:
|
|
1004
|
+
plt.figure(0).tight_layout()
|
|
1005
|
+
if title is not None:
|
|
1006
|
+
if len(self.ax) > 1:
|
|
1007
|
+
t = {'y': 0.9}
|
|
1008
|
+
t_in = {'t': title} if type(title) is str else title
|
|
1009
|
+
t.update(t_in)
|
|
1010
|
+
for i in range(self.npages):
|
|
1011
|
+
fig = plt.figure(i)
|
|
1012
|
+
fig.suptitle(**t)
|
|
1013
|
+
else:
|
|
1014
|
+
t = {'label': title} if type(title) is str else title
|
|
1015
|
+
axnow.set_title(**t)
|
|
1016
|
+
|
|
1017
|
+
def savefig(self, filename: str | None = None,
|
|
1018
|
+
show: bool = False, **kwargs) -> None:
|
|
1019
|
+
"""Use savefig of matplotlib.
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
filename (str, optional): Output image file name. Defaults to None.
|
|
1023
|
+
show (bool, optional): True means doing plt.show(). Defaults to False.
|
|
1024
|
+
"""
|
|
1025
|
+
_kw = {'transparent': True, 'bbox_inches': 'tight'}
|
|
1026
|
+
_kw.update(kwargs)
|
|
1027
|
+
for axnow in self.ax:
|
|
1028
|
+
axnow.set_xlim(*self.Xlim)
|
|
1029
|
+
axnow.set_ylim(*self.Ylim)
|
|
1030
|
+
if type(filename) is str:
|
|
1031
|
+
ext = filename.split('.')[-1]
|
|
1032
|
+
for i in range(self.npages):
|
|
1033
|
+
ver = '' if self.npages == 1 else f'_{i:d}'
|
|
1034
|
+
fig = plt.figure(i)
|
|
1035
|
+
fig.patch.set_alpha(0)
|
|
1036
|
+
fname = filename.replace(f'.{ext}', f'{ver}.{ext}')
|
|
1037
|
+
fig.savefig(fname, **_kw)
|
|
1038
|
+
if show:
|
|
1039
|
+
plt.show()
|
|
1040
|
+
plt.close()
|
|
1041
|
+
|
|
1042
|
+
def get_figax(self) -> tuple[object, object]:
|
|
1043
|
+
"""Output the external fig and ax after plotting.
|
|
1044
|
+
|
|
1045
|
+
Returns:
|
|
1046
|
+
tuple: (fig, ax)
|
|
1047
|
+
"""
|
|
1048
|
+
if len(self.ax) > 1:
|
|
1049
|
+
print('get_figax is not supported with channel maps')
|
|
1050
|
+
return -1
|
|
1051
|
+
return self.fig, self.ax[0]
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def plotprofile(coords: list[str] | str = [],
|
|
1055
|
+
xlist: list[float] = [], ylist: list[float] = [],
|
|
1056
|
+
ellipse: list[float, float, float] | None = None,
|
|
1057
|
+
ninterp: int = 1,
|
|
1058
|
+
flux: bool = False, width: int = 1,
|
|
1059
|
+
gaussfit: bool = False, gauss_kwargs: dict = {},
|
|
1060
|
+
title: list[str] | None = None, text: list[str] | None = None,
|
|
1061
|
+
dist: float = 1., vsys: float = 0.,
|
|
1062
|
+
nrows: int = 0, ncols: int = 1, fig=None, ax=None,
|
|
1063
|
+
getfigax: bool = False,
|
|
1064
|
+
savefig: dict = None, show: bool = True,
|
|
1065
|
+
**kwargs) -> tuple[object, object]:
|
|
1066
|
+
"""Use Axes.plot of matplotlib to plot line profiles at given coordinates. kwargs must include the arguments of AstroData to specify the data to be plotted. kwargs can include the arguments of PlotAxes2D to adjust x and y axes.
|
|
1067
|
+
|
|
1068
|
+
Args:
|
|
1069
|
+
coords (list, optional): Coordinates. Defaults to [].
|
|
1070
|
+
xlist (list, optional): Offset from the center. Defaults to [].
|
|
1071
|
+
ylist (list, optional): Offset from the center. Defaults to [].
|
|
1072
|
+
ellipse (list, optional): [major, minor, pa], For average. Defaults to None.
|
|
1073
|
+
ninterp (int, optional): Number of points for interpolation. Defaults to 1.
|
|
1074
|
+
flux (bool, optional): y axis is flux density. Defaults to False.
|
|
1075
|
+
width (int, optional): Rebinning step along v. Defaults to 1.
|
|
1076
|
+
gaussfit (bool, optional): Fit the profiles. Defaults to False.
|
|
1077
|
+
gauss_kwargs (dict, optional): Kwargs for Axes.plot. Defaults to {}.
|
|
1078
|
+
title (list, optional): For each plot. Defaults to None.
|
|
1079
|
+
text (list, optional): For each plot. Defaults to None.
|
|
1080
|
+
|
|
1081
|
+
Returns:
|
|
1082
|
+
tuple: (fig, ax), where ax is a list, if getfigax=True. Otherwise, no return.
|
|
1083
|
+
"""
|
|
1084
|
+
_kw = {'drawstyle': 'steps-mid', 'color': 'k'}
|
|
1085
|
+
_kw.update(kwargs)
|
|
1086
|
+
_kwgauss = {'drawstyle': 'default', 'color': 'g'}
|
|
1087
|
+
_kwgauss.update(gauss_kwargs)
|
|
1088
|
+
savefig0 = {'bbox_inches': 'tight', 'transparent': True}
|
|
1089
|
+
if type(coords) is str:
|
|
1090
|
+
coords = [coords]
|
|
1091
|
+
vmin, vmax = _kw['xlim'] if 'xlim' in _kw else [-1e10, 1e10]
|
|
1092
|
+
f = AstroFrame(dist=dist, vsys=vsys, vmin=vmin, vmax=vmax)
|
|
1093
|
+
d = kwargs2AstroData(_kw)
|
|
1094
|
+
Tb = d.Tb
|
|
1095
|
+
f.read(d)
|
|
1096
|
+
d.binning([width, 1, 1])
|
|
1097
|
+
v, prof, gfitres = d.profile(coords=coords, xlist=xlist, ylist=ylist,
|
|
1098
|
+
ellipse=ellipse, ninterp=ninterp,
|
|
1099
|
+
flux=flux, gaussfit=gaussfit)
|
|
1100
|
+
nprof = len(prof)
|
|
1101
|
+
if 'ylabel' in _kw:
|
|
1102
|
+
ylabel = _kw['ylabel']
|
|
1103
|
+
elif Tb:
|
|
1104
|
+
ylabel = r'$T_b$ (K)'
|
|
1105
|
+
elif flux:
|
|
1106
|
+
ylabel = 'Flux (Jy)'
|
|
1107
|
+
else:
|
|
1108
|
+
ylabel = d.bunit
|
|
1109
|
+
if type(ylabel) is str:
|
|
1110
|
+
ylabel = [ylabel] * nprof
|
|
1111
|
+
|
|
1112
|
+
def gauss(x, p, c, w):
|
|
1113
|
+
return p * np.exp(-4. * np.log(2.) * ((x - c) / w)**2)
|
|
1114
|
+
|
|
1115
|
+
set_rcparams(20, 'w')
|
|
1116
|
+
if ncols == 1:
|
|
1117
|
+
nrows = nprof
|
|
1118
|
+
if fig is None:
|
|
1119
|
+
fig = plt.figure(figsize=(6 * ncols, 3 * nrows))
|
|
1120
|
+
if nprof > 1 and ax is not None:
|
|
1121
|
+
print('External ax is supported only when len(coords)=1.')
|
|
1122
|
+
ax = None
|
|
1123
|
+
ax = np.empty(nprof, dtype='object') if ax is None else [ax]
|
|
1124
|
+
if 'xlabel' not in _kw:
|
|
1125
|
+
_kw['xlabel'] = 'Velocity (km s$^{-1}$)'
|
|
1126
|
+
if 'xlim' not in _kw:
|
|
1127
|
+
_kw['xlim'] = [v.min(), v.max()]
|
|
1128
|
+
_kw['samexy'] = False
|
|
1129
|
+
pa2d = kwargs2PlotAxes2D(_kw)
|
|
1130
|
+
for i in range(nprof):
|
|
1131
|
+
sharex = None if i < nrows - 1 else ax[i - 1]
|
|
1132
|
+
ax[i] = fig.add_subplot(nrows, ncols, i + 1, sharex=sharex)
|
|
1133
|
+
if gaussfit:
|
|
1134
|
+
ax[i].plot(v, gauss(v, *gfitres['best'][i]), **_kwgauss)
|
|
1135
|
+
ax[i].plot(v, prof[i], **_kw)
|
|
1136
|
+
ax[i].hlines([0], v.min(), v.max(), linestyle='dashed', color='k')
|
|
1137
|
+
ax[i].set_ylabel(ylabel[i])
|
|
1138
|
+
pa2d.set_xyaxes(ax[i])
|
|
1139
|
+
if text is not None:
|
|
1140
|
+
ax[i].text(**text[i])
|
|
1141
|
+
if title is not None:
|
|
1142
|
+
if type(title[i]) is str:
|
|
1143
|
+
title[i] = {'label': title[i]}
|
|
1144
|
+
ax[i].set_title(**title[i])
|
|
1145
|
+
if i <= nprof - ncols - 1:
|
|
1146
|
+
plt.setp(ax[i].get_xticklabels(), visible=False)
|
|
1147
|
+
if getfigax:
|
|
1148
|
+
return fig, ax
|
|
1149
|
+
fig.tight_layout()
|
|
1150
|
+
if savefig is not None:
|
|
1151
|
+
s = {'fname': savefig} if type(savefig) is str else savefig
|
|
1152
|
+
savefig0.update(s)
|
|
1153
|
+
fig.savefig(**savefig0)
|
|
1154
|
+
if show:
|
|
1155
|
+
plt.show()
|
|
1156
|
+
plt.close()
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def plotslice(length: float, dx: float | None = None, pa: float = 0,
|
|
1160
|
+
dist: float = 1, xoff: float = 0, yoff: float = 0,
|
|
1161
|
+
xflip: bool = True, yflip: bool = False,
|
|
1162
|
+
txtfile: str | None = None,
|
|
1163
|
+
fig: object | None = None, ax: object | None = None,
|
|
1164
|
+
getfigax: bool = False,
|
|
1165
|
+
savefig: str | dict | None = None, show: bool = False,
|
|
1166
|
+
**kwargs) -> None:
|
|
1167
|
+
"""Use Axes.plot of matplotlib to plot a 1D spatial slice in a 2D map. kwargs must include the arguments of AstroData to specify the data to be plotted. kwargs can include the arguments of PlotAxes2D to adjust x and y axes.
|
|
1168
|
+
|
|
1169
|
+
Args:
|
|
1170
|
+
length (float): Slice length.
|
|
1171
|
+
dx (float, optional): Grid increment. Defaults to None.
|
|
1172
|
+
pa (float, optional): Degree. Position angle. Defaults to 0.
|
|
1173
|
+
fitsimage to show: same as in PlotAstroData.
|
|
1174
|
+
"""
|
|
1175
|
+
_kw = {'linestyle': '-', 'marker': 'o'}
|
|
1176
|
+
_kw.update(kwargs)
|
|
1177
|
+
savefig0 = {'bbox_inches': 'tight', 'transparent': True}
|
|
1178
|
+
center = _kw['center'] if 'center' in _kw else None
|
|
1179
|
+
f = AstroFrame(rmax=length / 2, dist=dist, xoff=xoff, yoff=yoff,
|
|
1180
|
+
xflip=xflip, yflip=yflip, center=center)
|
|
1181
|
+
d = kwargs2AstroData(_kw)
|
|
1182
|
+
Tb = d.Tb
|
|
1183
|
+
f.read(d)
|
|
1184
|
+
if np.ndim(d.data) > 2:
|
|
1185
|
+
print('Only 2D map is supported.')
|
|
1186
|
+
return -1
|
|
1187
|
+
|
|
1188
|
+
r, z = d.slice(length=length, pa=pa, dx=dx)
|
|
1189
|
+
xunit = 'arcsec' if dist == 1 else 'au'
|
|
1190
|
+
yunit = 'K' if Tb else d.bunit
|
|
1191
|
+
yquantity = 'Tb' if Tb else 'intensity'
|
|
1192
|
+
|
|
1193
|
+
if txtfile is not None:
|
|
1194
|
+
np.savetxt(txtfile, np.c_[r, z],
|
|
1195
|
+
header=f'x ({xunit}), {yquantity} ({yunit}); '
|
|
1196
|
+
+ f'positive x is pa={pa:.2f} deg.')
|
|
1197
|
+
set_rcparams()
|
|
1198
|
+
if fig is None:
|
|
1199
|
+
fig = plt.figure()
|
|
1200
|
+
if ax is None:
|
|
1201
|
+
ax = fig.add_subplot(1, 1, 1)
|
|
1202
|
+
if 'xlabel' not in _kw:
|
|
1203
|
+
_kw['xlabel'] = f'Offset ({xunit})'
|
|
1204
|
+
if 'ylabel' not in _kw:
|
|
1205
|
+
_kw['ylabel'] = f'Intensity ({yunit})'
|
|
1206
|
+
if 'xlim' not in _kw:
|
|
1207
|
+
_kw['xlim'] = [r.min(), r.max()]
|
|
1208
|
+
_kw['samexy'] = False
|
|
1209
|
+
pa2d = kwargs2PlotAxes2D(_kw)
|
|
1210
|
+
ax.plot(r, z, **_kw)
|
|
1211
|
+
if d.sigma is not None:
|
|
1212
|
+
ax.plot(r, r * 0 + 3 * d.sigma, 'k--')
|
|
1213
|
+
pa2d.set_xyaxes(ax)
|
|
1214
|
+
if getfigax:
|
|
1215
|
+
return fig, ax
|
|
1216
|
+
fig.tight_layout()
|
|
1217
|
+
if savefig is not None:
|
|
1218
|
+
s = {'fname': savefig} if type(savefig) is str else savefig
|
|
1219
|
+
savefig0.update(s)
|
|
1220
|
+
fig.savefig(**savefig0)
|
|
1221
|
+
if show:
|
|
1222
|
+
plt.show()
|
|
1223
|
+
plt.close()
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def plot3d(levels: list[float] = [3, 6, 12], cmap: str = 'Jet',
|
|
1227
|
+
xlabel: str = 'R.A. (arcsec)',
|
|
1228
|
+
ylabel: str = 'Dec. (arcsec)',
|
|
1229
|
+
vlabel: str = 'Velocity (km/s)',
|
|
1230
|
+
xskip: int = 1, yskip: int = 1,
|
|
1231
|
+
eye_p: float = 0, eye_i: float = 180,
|
|
1232
|
+
outname: str = 'plot3d', show: bool = False,
|
|
1233
|
+
**kwargs) -> None:
|
|
1234
|
+
"""Use Plotly. kwargs must include the arguments of AstroData to specify the data to be plotted. kwargs must include the arguments of AstroFrame to specify the ranges and so on for plotting.
|
|
1235
|
+
|
|
1236
|
+
Args:
|
|
1237
|
+
levels (list, optional): Contour levels. Defaults to [3,6,12].
|
|
1238
|
+
cmap (str, optional): Color map name. Defaults to 'Jet'.
|
|
1239
|
+
xlabel (str, optional): Defaults to 'R.A. (arcsec)'.
|
|
1240
|
+
ylabel (str, optional): Defaults to 'Dec. (arcsec)'.
|
|
1241
|
+
vlabel (str, optional): Defaults to 'Velocity (km/s)'.
|
|
1242
|
+
xskip (int, optional): Number of pixel to skip. Defaults to 1.
|
|
1243
|
+
yskip (int, optional): Number of pixel to skip. Defaults to 1.
|
|
1244
|
+
eye_p (float, optional): Azimuthal angle of camera. Defaults to 0.
|
|
1245
|
+
eye_i (float, optional): Inclination angle of camera. Defaults to 180.
|
|
1246
|
+
outname (str, optional): Output file name. Defaults to 'plot3d'.
|
|
1247
|
+
show (bool, optional): auto_open in plotly. Defaults to False.
|
|
1248
|
+
"""
|
|
1249
|
+
import plotly.offline as po
|
|
1250
|
+
import plotly.graph_objs as go
|
|
1251
|
+
from skimage import measure
|
|
1252
|
+
|
|
1253
|
+
f = kwargs2AstroFrame(kwargs)
|
|
1254
|
+
d = kwargs2AstroData(kwargs)
|
|
1255
|
+
f.read(d, xskip, yskip)
|
|
1256
|
+
volume, x, y, v, sigma = d.data, d.x, d.y, d.v, d.sigma
|
|
1257
|
+
dx, dy, dv = x[1] - x[0], y[1] - y[0], v[1] - v[0]
|
|
1258
|
+
volume[np.isnan(volume)] = 0
|
|
1259
|
+
if dx < 0:
|
|
1260
|
+
x, dx, volume = x[::-1], -dx, volume[:, :, ::-1]
|
|
1261
|
+
if dy < 0:
|
|
1262
|
+
y, dy, volume = y[::-1], -dy, volume[:, ::-1, :]
|
|
1263
|
+
if dv < 0:
|
|
1264
|
+
v, dv, volume = v[::-1], -dv, volume[::-1, :, :]
|
|
1265
|
+
s, ds = [x, y, v], [dx, dy, dv]
|
|
1266
|
+
deg = np.radians(1)
|
|
1267
|
+
xeye = -np.sin(eye_i * deg) * np.sin(eye_p * deg)
|
|
1268
|
+
yeye = -np.sin(eye_i * deg) * np.cos(eye_p * deg)
|
|
1269
|
+
zeye = np.cos(eye_i * deg)
|
|
1270
|
+
margin = dict(l=0, r=0, b=0, t=0)
|
|
1271
|
+
camera = dict(eye=dict(x=xeye, y=yeye, z=zeye), up=dict(x=0, y=1, z=0))
|
|
1272
|
+
xaxis = dict(range=[x[0], x[-1]], title=xlabel)
|
|
1273
|
+
yaxis = dict(range=[y[0], y[-1]], title=ylabel)
|
|
1274
|
+
zaxis = dict(range=[v[0], v[-1]], title=vlabel)
|
|
1275
|
+
scene = dict(aspectmode='cube', camera=camera,
|
|
1276
|
+
xaxis=xaxis, yaxis=yaxis, zaxis=zaxis)
|
|
1277
|
+
layout = go.Layout(margin=margin, scene=scene, showlegend=False)
|
|
1278
|
+
|
|
1279
|
+
data = []
|
|
1280
|
+
for lev in levels:
|
|
1281
|
+
if lev * sigma > np.max(volume):
|
|
1282
|
+
continue
|
|
1283
|
+
vertices, simplices, _, _ = measure.marching_cubes(volume, lev * sigma)
|
|
1284
|
+
Xg, Yg, Zg = [t[0] + i * dt for t, i, dt
|
|
1285
|
+
in zip(s, vertices.T[::-1], ds)]
|
|
1286
|
+
i, j, k = simplices.T
|
|
1287
|
+
mesh = dict(type='mesh3d', x=Xg, y=Yg, z=Zg, i=i, j=j, k=k,
|
|
1288
|
+
intensity=Zg * 0 + lev,
|
|
1289
|
+
colorscale=cmap, reversescale=False,
|
|
1290
|
+
cmin=np.min(levels), cmax=np.max(levels),
|
|
1291
|
+
opacity=0.08, name='', showscale=False)
|
|
1292
|
+
data.append(mesh)
|
|
1293
|
+
Xe, Ye, Ze = [], [], []
|
|
1294
|
+
for t in vertices[simplices]:
|
|
1295
|
+
Xe += [x[0] + dx * t[k % 3][2] for k in range(4)] + [None]
|
|
1296
|
+
Ye += [y[0] + dy * t[k % 3][1] for k in range(4)] + [None]
|
|
1297
|
+
Ze += [v[0] + dv * t[k % 3][0] for k in range(4)] + [None]
|
|
1298
|
+
lines = dict(type='scatter3d', x=Xe, y=Ye, z=Ze,
|
|
1299
|
+
mode='lines', opacity=0.04, visible=True,
|
|
1300
|
+
name='', line=dict(color='rgb(0,0,0)', width=1))
|
|
1301
|
+
data.append(lines)
|
|
1302
|
+
|
|
1303
|
+
fig = dict(data=data, layout=layout)
|
|
1304
|
+
po.plot(fig, filename=outname + '.html', auto_open=show)
|