plotastrodata 1.3.2__tar.gz → 1.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/PKG-INFO +1 -1
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/__init__.py +1 -1
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/analysis_utils.py +10 -91
- plotastrodata-1.4.1/plotastrodata/coord_utils.py +149 -0
- plotastrodata-1.4.1/plotastrodata/ext_utils.py +49 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/fits_utils.py +50 -2
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/los_utils.py +1 -30
- plotastrodata-1.4.1/plotastrodata/matrix_utils.py +74 -0
- plotastrodata-1.4.1/plotastrodata/other_utils.py +290 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/plot_utils.py +2 -1
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/PKG-INFO +1 -1
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/SOURCES.txt +3 -0
- plotastrodata-1.3.2/plotastrodata/other_utils.py +0 -439
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/LICENSE +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/README.md +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/const_utils.py +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/fft_utils.py +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/fitting_utils.py +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/dependency_links.txt +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/not-zip-safe +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/requires.txt +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/top_level.txt +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/setup.cfg +0 -0
- {plotastrodata-1.3.2 → plotastrodata-1.4.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: plotastrodata
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: plotastrodata is a tool for astronomers to create figures from FITS files and perform fundamental data analyses with ease.
|
|
5
5
|
Home-page: https://github.com/yusukeaso-astron/plotastrodata
|
|
6
6
|
Download-URL: https://github.com/yusukeaso-astron/plotastrodata
|
|
@@ -4,31 +4,16 @@ from scipy.interpolate import RegularGridInterpolator as RGI
|
|
|
4
4
|
from scipy.optimize import curve_fit
|
|
5
5
|
from scipy.signal import convolve
|
|
6
6
|
|
|
7
|
-
from plotastrodata.
|
|
8
|
-
|
|
7
|
+
from plotastrodata.coord_utils import coord2xy, rel2abs
|
|
8
|
+
from plotastrodata.matrix_utils import Mfac, Mrot, dot2d
|
|
9
|
+
from plotastrodata.other_utils import (estimate_rms, trim,
|
|
10
|
+
gaussian2d, isdeg,
|
|
11
|
+
RGIxy, RGIxyv, to4dim)
|
|
9
12
|
from plotastrodata.fits_utils import FitsData, data2fits, Jy2K
|
|
10
13
|
from plotastrodata import const_utils as cu
|
|
11
14
|
from plotastrodata.fitting_utils import EmceeCorner
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
def to4dim(data: np.ndarray) -> np.ndarray:
|
|
15
|
-
"""Change a 2D, 3D, or 4D array to a 4D array.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
data (np.ndarray): Input data. 2D, 3D, or 4D.
|
|
19
|
-
|
|
20
|
-
Returns:
|
|
21
|
-
np.ndarray: Output 4D array.
|
|
22
|
-
"""
|
|
23
|
-
if np.ndim(data) == 2:
|
|
24
|
-
d = np.array([[data]])
|
|
25
|
-
elif np.ndim(data) == 3:
|
|
26
|
-
d = np.array([data])
|
|
27
|
-
else:
|
|
28
|
-
d = np.array(data)
|
|
29
|
-
return d
|
|
30
|
-
|
|
31
|
-
|
|
32
17
|
def quadrantmean(data: np.ndarray, x: np.ndarray, y: np.ndarray,
|
|
33
18
|
quadrants: str = '13') -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
34
19
|
"""Take mean between 1st and 3rd (or 2nd and 4th) quadrants.
|
|
@@ -62,74 +47,6 @@ def quadrantmean(data: np.ndarray, x: np.ndarray, y: np.ndarray,
|
|
|
62
47
|
return datanew[ny:, nx:], xnew[nx:], ynew[ny:]
|
|
63
48
|
|
|
64
49
|
|
|
65
|
-
def RGIxy(y: np.ndarray, x: np.ndarray, data: np.ndarray,
|
|
66
|
-
yxnew: tuple[np.ndarray, np.ndarray] | None = None,
|
|
67
|
-
**kwargs) -> object | np.ndarray:
|
|
68
|
-
"""RGI for x and y at each channel.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
y (np.ndarray): 1D array. Second coordinate.
|
|
72
|
-
x (np.ndarray): 1D array. First coordinate.
|
|
73
|
-
data (np.ndarray): 2D, 3D, or 4D array.
|
|
74
|
-
yxnew (tuple, optional): (ynew, xnew), where ynew and xnew are 1D or 2D arrays. Defaults to None.
|
|
75
|
-
|
|
76
|
-
Returns:
|
|
77
|
-
np.ndarray: The RGI function or the interpolated array.
|
|
78
|
-
"""
|
|
79
|
-
if not np.ndim(data) in [2, 3, 4]:
|
|
80
|
-
print('data must be 2D, 3D, or 4D.')
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
_kw = {'bounds_error': False, 'fill_value': np.nan,
|
|
84
|
-
'method': 'linear'}
|
|
85
|
-
_kw.update(kwargs)
|
|
86
|
-
c4d = to4dim(data)
|
|
87
|
-
c4d[np.isnan(c4d)] = 0
|
|
88
|
-
f = [[RGI((y, x), c2d, **_kw)
|
|
89
|
-
for c2d in c3d] for c3d in c4d]
|
|
90
|
-
if yxnew is None:
|
|
91
|
-
if len(f) == 1:
|
|
92
|
-
f = f[0]
|
|
93
|
-
if len(f) == 1:
|
|
94
|
-
f = f[0]
|
|
95
|
-
return f
|
|
96
|
-
else:
|
|
97
|
-
return np.squeeze([[f2d(tuple(yxnew)) for f2d in f3d] for f3d in f])
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def RGIxyv(v: np.ndarray, y: np.ndarray, x: np.ndarray, data: np.ndarray,
|
|
101
|
-
vyxnew: tuple[np.ndarray, np.ndarray, np.ndarray] | None = None,
|
|
102
|
-
**kwargs) -> object | np.ndarray:
|
|
103
|
-
"""RGI in the x-y-v space.
|
|
104
|
-
|
|
105
|
-
Args:
|
|
106
|
-
v (np.ndarray): 1D array. Third coordinate.
|
|
107
|
-
y (np.ndarray): 1D array. Second coordinate.
|
|
108
|
-
x (np.ndarray): 1D array. First coordinate.
|
|
109
|
-
data (np.ndarray): 3D or 4D array.
|
|
110
|
-
vyxnew (tuple, optional): (vnew, ynew, xnew), where vnew, ynew, and xnew are 1D or 2D arrays. Defaults to None.
|
|
111
|
-
|
|
112
|
-
Returns:
|
|
113
|
-
np.ndarray: The RGI function or the interpolated array.
|
|
114
|
-
"""
|
|
115
|
-
if not np.ndim(data) in [3, 4]:
|
|
116
|
-
print('data must be 3D or 4D.')
|
|
117
|
-
return
|
|
118
|
-
|
|
119
|
-
_kw = {'bounds_error': False, 'fill_value': np.nan,
|
|
120
|
-
'method': 'linear'}
|
|
121
|
-
_kw.update(kwargs)
|
|
122
|
-
c4d = to4dim(data)
|
|
123
|
-
c4d[np.isnan(c4d)] = 0
|
|
124
|
-
f = [RGI((v, y, x), c3d, **_kw) for c3d in c4d]
|
|
125
|
-
if vyxnew is None:
|
|
126
|
-
if len(f) == 1:
|
|
127
|
-
f = f[0]
|
|
128
|
-
return f
|
|
129
|
-
else:
|
|
130
|
-
return np.squeeze([f3d(tuple(vyxnew)) for f3d in f])
|
|
131
|
-
|
|
132
|
-
|
|
133
50
|
def filled2d(data: np.ndarray, x: np.ndarray, y: np.ndarray, n: int = 1,
|
|
134
51
|
**kwargs) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
135
52
|
"""Fill 2D data, 1D x, and 1D y by a factor of n using RGI.
|
|
@@ -580,7 +497,7 @@ class AstroData():
|
|
|
580
497
|
h['BMAJ'] = self.beam_org[0] / 3600
|
|
581
498
|
h['BMIN'] = self.beam_org[1] / 3600
|
|
582
499
|
h['BPA'] = self.beam_org[2]
|
|
583
|
-
else:
|
|
500
|
+
else:
|
|
584
501
|
h['BMAJ'] = self.beam[0] / 3600
|
|
585
502
|
h['BMIN'] = self.beam[1] / 3600
|
|
586
503
|
h['BPA'] = self.beam[2]
|
|
@@ -750,6 +667,8 @@ class AstroFrame():
|
|
|
750
667
|
grid = fd.get_grid(center=d.center[i], dist=self.dist,
|
|
751
668
|
restfreq=d.restfreq[i], vsys=self.vsys,
|
|
752
669
|
pv=self.pv)
|
|
670
|
+
if fd.wcsrot:
|
|
671
|
+
d.center[i] = fd.get_center() # for WCS rotation
|
|
753
672
|
d.beam[i] = fd.get_beam(dist=self.dist)
|
|
754
673
|
d.bunit[i] = fd.get_header('BUNIT')
|
|
755
674
|
if d.data[i] is not None:
|
|
@@ -788,8 +707,8 @@ class AstroFrame():
|
|
|
788
707
|
'CUNIT1': 'DEG',
|
|
789
708
|
'RESTFREQ': d.restfreq[i]}
|
|
790
709
|
if None not in d.beam[i]:
|
|
791
|
-
header['BMAJ'] = d.beam[i][0] / 3600
|
|
792
|
-
header['BMIN'] = d.beam[i][1] / 3600
|
|
710
|
+
header['BMAJ'] = d.beam[i][0] / 3600 / self.dist
|
|
711
|
+
header['BMIN'] = d.beam[i][1] / 3600 / self.dist
|
|
793
712
|
d.data[i] = d.data[i] * Jy2K(header=header)
|
|
794
713
|
d.sigma[i] = d.sigma[i] * Jy2K(header=header)
|
|
795
714
|
if self.pv and None not in d.beam[i]:
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from astropy.coordinates import SkyCoord, FK5, FK4
|
|
3
|
+
from astropy import units
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _getframe(coord: str, s: str = '') -> tuple:
|
|
7
|
+
"""Internal function to pick up the frame name from the coordinates.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
coord (str): something like "J2000 01h23m45.6s 01d23m45.6s"
|
|
11
|
+
s (str, optional): To distinguish coord and coordorg. Defaults to ''.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
tuple: updated coord and frame. frame is FK5(equinox='J2000), FK4(equinox='B1950'), or 'icrs'.
|
|
15
|
+
"""
|
|
16
|
+
if len(c := coord.split()) == 3:
|
|
17
|
+
coord = f'{c[1]} {c[2]}'
|
|
18
|
+
if 'J2000' in c[0]:
|
|
19
|
+
frame = FK5(equinox='J2000')
|
|
20
|
+
elif 'FK5' in c[0]:
|
|
21
|
+
frame = FK5(equinox='J2000')
|
|
22
|
+
elif 'B1950' in c[0]:
|
|
23
|
+
frame = FK4(equinox='B1950')
|
|
24
|
+
elif 'FK4' in c[0]:
|
|
25
|
+
frame = FK4(equinox='B1950')
|
|
26
|
+
elif 'ICRS' in c[0]:
|
|
27
|
+
frame = 'icrs'
|
|
28
|
+
else:
|
|
29
|
+
print(f'Unknown equinox found in coord{s}. ICRS is used')
|
|
30
|
+
frame = 'icrs'
|
|
31
|
+
else:
|
|
32
|
+
frame = None
|
|
33
|
+
return coord, frame
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _updateframe(frame: str) -> str:
|
|
37
|
+
"""Internal function to str frame to astropy frame.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
frame (str): _description_
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: frame as is, FK5(equinox='J2000'), FK4(equinox='B1950'), or 'icrs'.
|
|
44
|
+
"""
|
|
45
|
+
if 'ICRS' in frame:
|
|
46
|
+
a = 'icrs'
|
|
47
|
+
elif 'J2000' in frame or 'FK5' in frame:
|
|
48
|
+
a = FK5(equinox='J2000')
|
|
49
|
+
elif 'B1950' in frame or 'FK4' in frame:
|
|
50
|
+
a = FK4(equinox='B1950')
|
|
51
|
+
else:
|
|
52
|
+
a = frame
|
|
53
|
+
return a
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def coord2xy(coords: str | list, coordorg: str = '00h00m00s 00d00m00s',
|
|
57
|
+
frame: str | None = None, frameorg: str | None = None,
|
|
58
|
+
) -> np.ndarray:
|
|
59
|
+
"""Transform R.A.-Dec. to relative (deg, deg).
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
coords (str, list): something like '01h23m45.6s 01d23m45.6s'. The input can be a list of str in an arbitrary shape.
|
|
63
|
+
coordorg (str, optional): something like '01h23m45.6s 01d23m45.6s'. The origin of the relative (deg, deg). Defaults to '00h00m00s 00d00m00s'.
|
|
64
|
+
frame (str, optional): coordinate frame. Defaults to None.
|
|
65
|
+
frameorg (str, optional): coordinate frame of the origin. Defaults to None.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
np.ndarray: [(array of) alphas, (array of) deltas] in degree. The shape of alphas and deltas is the input shape. With a single input, the output is [alpha0, delta0].
|
|
69
|
+
"""
|
|
70
|
+
coordorg, frameorg_c = _getframe(coordorg, 'org')
|
|
71
|
+
frameorg = frameorg_c if frameorg is None else _updateframe(frameorg)
|
|
72
|
+
if type(coords) is list:
|
|
73
|
+
for i in range(len(coords)):
|
|
74
|
+
coords[i], frame_c = _getframe(coords[i])
|
|
75
|
+
else:
|
|
76
|
+
coords, frame_c = _getframe(coords)
|
|
77
|
+
frame = frame_c if frame is None else _updateframe(frame)
|
|
78
|
+
if frame is None and frameorg is not None:
|
|
79
|
+
frame = frameorg
|
|
80
|
+
if frame is not None and frameorg is None:
|
|
81
|
+
frameorg = frame
|
|
82
|
+
if frame is None and frameorg is None:
|
|
83
|
+
frame = frameorg = 'icrs'
|
|
84
|
+
clist = SkyCoord(coords, frame=frame)
|
|
85
|
+
c0 = SkyCoord(coordorg, frame=frameorg)
|
|
86
|
+
c0 = c0.transform_to(frame=frame)
|
|
87
|
+
xy = c0.spherical_offsets_to(clist)
|
|
88
|
+
return np.array([xy[0].degree, xy[1].degree])
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def xy2coord(xy: list, coordorg: str = '00h00m00s 00d00m00s',
|
|
92
|
+
frame: str | None = None, frameorg: str | None = None,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Transform relative (deg, deg) to R.A.-Dec.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
xy (list): [(array of) alphas, (array of) deltas] in degree. alphas and deltas can have an arbitrary shape.
|
|
98
|
+
coordorg (str): something like '01h23m45.6s 01d23m45.6s'. The origin of the relative (deg, deg). Defaults to '00h00m00s 00d00m00s'.
|
|
99
|
+
frame (str): coordinate frame. Defaults to None.
|
|
100
|
+
frameorg (str): coordinate frame of the origin. Defaults to None.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
str: something like '01h23m45.6s 01d23m45.6s'. With multiple inputs, the output has the input shape.
|
|
104
|
+
"""
|
|
105
|
+
coordorg, frameorg_c = _getframe(coordorg, 'org')
|
|
106
|
+
frameorg = frameorg_c if frameorg is None else _updateframe(frameorg)
|
|
107
|
+
if frameorg is None:
|
|
108
|
+
frameorg = 'icrs'
|
|
109
|
+
frame = frameorg if frame is None else _updateframe(frame)
|
|
110
|
+
c0 = SkyCoord(coordorg, frame=frameorg)
|
|
111
|
+
coords = c0.spherical_offsets_by(*xy * units.degree)
|
|
112
|
+
coords = coords.transform_to(frame=frame)
|
|
113
|
+
return coords.to_string('hmsdms')
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def rel2abs(xrel: float, yrel: float,
|
|
117
|
+
x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
118
|
+
"""Transform relative coordinates to absolute ones.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
xrel (float): 0 <= xrel <= 1. 0 and 1 correspond to x[0] and x[-1], respectively. Arbitrary shape.
|
|
122
|
+
yrel (float): same as xrel.
|
|
123
|
+
x (np.ndarray): [x0, x0+dx, x0+2dx, ...]
|
|
124
|
+
y (np.ndarray): [y0, y0+dy, y0+2dy, ...]
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
np.ndarray: [xabs, yabs]. Each has the input's shape.
|
|
128
|
+
"""
|
|
129
|
+
xabs = (1. - xrel)*x[0] + xrel*x[-1]
|
|
130
|
+
yabs = (1. - yrel)*y[0] + yrel*y[-1]
|
|
131
|
+
return np.array([xabs, yabs])
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def abs2rel(xabs: float, yabs: float,
|
|
135
|
+
x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
136
|
+
"""Transform absolute coordinates to relative ones.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
xabs (float): In the same frame of x.
|
|
140
|
+
yabs (float): In the same frame of y.
|
|
141
|
+
x (np.ndarray): [x0, x0+dx, x0+2dx, ...]
|
|
142
|
+
y (np.ndarray): [y0, y0+dy, y0+2dy, ...]
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
ndarray: [xrel, yrel]. Each has the input's shape. 0 <= xrel, yrel <= 1. 0 and 1 correspond to x[0] and x[-1], respectively.
|
|
146
|
+
"""
|
|
147
|
+
xrel = (xabs - x[0]) / (x[-1] - x[0])
|
|
148
|
+
yrel = (yabs - y[0]) / (y[-1] - y[0])
|
|
149
|
+
return np.array([xrel, yrel])
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import shlex
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from plotastrodata import const_utils as cu
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def terminal(cmd: str, **kwargs) -> None:
|
|
9
|
+
"""Run a terminal command through subprocess.run.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
cmd (str): Terminal command.
|
|
13
|
+
"""
|
|
14
|
+
subprocess.run(shlex.split(cmd), **kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def runpython(filename: str, **kwargs) -> None:
|
|
18
|
+
"""Run a python file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
filename (str): Python file name.
|
|
22
|
+
"""
|
|
23
|
+
terminal(f'python {filename}', **kwargs)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def BnuT(T: float = 30, nu: float = 230e9) -> float:
|
|
27
|
+
"""Planck function.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
T (float, optional): Temperature in the unit of K. Defaults to 30.
|
|
31
|
+
nu (float, optional): Frequency in the unit of Hz. Defaults to 230e9.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
float: Planck function in the SI units.
|
|
35
|
+
"""
|
|
36
|
+
return 2 * cu.h * nu**3 / cu.c**2 / (np.exp(cu.h * nu / cu.k_B / T) - 1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def JnuT(T: float = 30, nu: float = 230e9) -> float:
|
|
40
|
+
"""Brightness templerature from the Planck function.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
T (float, optional): Temperature in the unit of K. Defaults to 30.
|
|
44
|
+
nu (float, optional): Frequency in the unit of Hz. Defaults to 230e9.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
float: Brightness temperature of Planck function in the unit of K.
|
|
48
|
+
"""
|
|
49
|
+
return cu.h * nu / cu.k_B / (np.exp(cu.h * nu / cu.k_B / T) - 1)
|
|
@@ -2,8 +2,10 @@ import numpy as np
|
|
|
2
2
|
from astropy.io import fits
|
|
3
3
|
from astropy import units, wcs
|
|
4
4
|
|
|
5
|
-
from plotastrodata.
|
|
6
|
-
|
|
5
|
+
from plotastrodata.coord_utils import coord2xy, xy2coord
|
|
6
|
+
from plotastrodata.matrix_utils import dot2d
|
|
7
|
+
from plotastrodata.other_utils import (estimate_rms, trim, isdeg,
|
|
8
|
+
RGIxy)
|
|
7
9
|
from plotastrodata import const_utils as cu
|
|
8
10
|
|
|
9
11
|
|
|
@@ -204,6 +206,28 @@ class FitsData:
|
|
|
204
206
|
restfreq = h['RESTFREQ']
|
|
205
207
|
self.x, self.y, self.v = None, None, None
|
|
206
208
|
self.dx, self.dy, self.dv = None, None, None
|
|
209
|
+
# WCS rotation (Calabretta & Greisen 2002, Astronomy & Astrophysics, 395, 1077)
|
|
210
|
+
self.wcsrot = False
|
|
211
|
+
cdij = ['CD1_1', 'CD1_2', 'CD2_1', 'CD2_2']
|
|
212
|
+
if np.all([s in list(h.keys()) for s in cdij]):
|
|
213
|
+
self.wcsrot = True
|
|
214
|
+
cd11, cd12, cd21, cd22 = [h[s] for s in cdij]
|
|
215
|
+
cdelt1cdelt2 = cd11 * cd22 - cd12 * cd21
|
|
216
|
+
sin_2rho = 2 * cd21 * cd22 / cdelt1cdelt2
|
|
217
|
+
cos_2rho = (cd11 * cd22 + cd12 * cd21) / cdelt1cdelt2
|
|
218
|
+
crota2 = np.arctan2(sin_2rho, cos_2rho) / 2.
|
|
219
|
+
sin_rho = np.sin(crota2)
|
|
220
|
+
cos_rho = np.cos(crota2)
|
|
221
|
+
cdelt1 = cd11 * cos_rho + cd21 * sin_rho
|
|
222
|
+
cdelt2 = -cd12 * sin_rho + cd22 * cos_rho
|
|
223
|
+
crota2 = np.degrees(crota2)
|
|
224
|
+
h['CDELT1'] = cdelt1
|
|
225
|
+
h['CDELT2'] = cdelt2
|
|
226
|
+
del h['CD1_1']
|
|
227
|
+
del h['CD1_2']
|
|
228
|
+
del h['CD2_1']
|
|
229
|
+
del h['CD2_2']
|
|
230
|
+
print(f'WCS rotation was found (CROTA2 = {crota2:f} deg).')
|
|
207
231
|
|
|
208
232
|
def get_list(i: int, crval=False) -> np.ndarray:
|
|
209
233
|
s = np.arange(h[f'NAXIS{i:d}'])
|
|
@@ -271,6 +295,30 @@ class FitsData:
|
|
|
271
295
|
if h['NAXIS'] > 2 and h['NAXIS3'] > 1:
|
|
272
296
|
gen_v(get_list(3, True))
|
|
273
297
|
|
|
298
|
+
if self.wcsrot:
|
|
299
|
+
data = self.get_data()
|
|
300
|
+
self.header['CRPIX1'] = ic = len(self.x) // 2
|
|
301
|
+
self.header['CRPIX2'] = jc = len(self.y) // 2
|
|
302
|
+
xc = self.x[ic] / self.dx
|
|
303
|
+
yc = self.y[jc] / self.dy
|
|
304
|
+
Mcd = [[cd11, cd12], [cd21, cd22]]
|
|
305
|
+
xc, yc = dot2d(Mcd, [xc, yc])
|
|
306
|
+
newcenter = xy2coord(xy=[xc, yc], coordorg=self.get_center())
|
|
307
|
+
xc, yc = coord2xy(coords=newcenter, coordorg='00h00m00s 00d00m00s')
|
|
308
|
+
self.header['CRVAL1'] = xc
|
|
309
|
+
self.header['CRVAL2'] = yc
|
|
310
|
+
self.x = self.x - self.x[ic]
|
|
311
|
+
self.y = self.y - self.y[jc]
|
|
312
|
+
x = self.x / (3600 if isdeg(h['CUNIT1']) else 1)
|
|
313
|
+
y = self.y / (3600 if isdeg(h['CUNIT2']) else 1)
|
|
314
|
+
X, Y = np.meshgrid(x, y)
|
|
315
|
+
cdinv = np.linalg.inv([[cd11, cd12], [cd21, cd22]])
|
|
316
|
+
xnew, ynew = dot2d(cdinv, [X, Y])
|
|
317
|
+
datanew = RGIxy(self.y / self.dy, self.x / self.dx,
|
|
318
|
+
data, (ynew, xnew))
|
|
319
|
+
self.data = datanew
|
|
320
|
+
print('Data values were interpolated for WCS rotation.')
|
|
321
|
+
|
|
274
322
|
def get_grid(self, **kwargs) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
275
323
|
"""Output the grids, [x, y, v]. This method can take the arguments of gen_grid().
|
|
276
324
|
|
|
@@ -1,35 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
def Mrot3d(t: float, axis: int = 3) -> np.ndarray:
|
|
5
|
-
"""3D rotation matrix around a specified axis.
|
|
6
|
-
|
|
7
|
-
This function creates a 3x3 rotation matrix for rotating coordinates around
|
|
8
|
-
the x-axis (axis=1), y-axis (axis=2), or z-axis (axis=3) by t degrees.
|
|
9
|
-
|
|
10
|
-
Args:
|
|
11
|
-
t (float): Rotation angle in degrees.
|
|
12
|
-
axis (int, optional): Axis to rotate around - 1 for x-axis, 2 for y-axis, 3 for z-axis. Defaults to 3.
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
np.ndarray: 3x3 rotation matrix that rotates coordinates around the specified axis by t degrees.
|
|
16
|
-
"""
|
|
17
|
-
cos_t = np.cos(np.radians(t))
|
|
18
|
-
sin_t = np.sin(np.radians(t))
|
|
19
|
-
match axis:
|
|
20
|
-
case 1:
|
|
21
|
-
m = [[1, 0, 0],
|
|
22
|
-
[0, cos_t, -sin_t],
|
|
23
|
-
[0, sin_t, cos_t]]
|
|
24
|
-
case 2:
|
|
25
|
-
m = [[cos_t, 0, sin_t],
|
|
26
|
-
[0, 1, 0],
|
|
27
|
-
[-sin_t, 0, cos_t]]
|
|
28
|
-
case 3:
|
|
29
|
-
m = [[cos_t, -sin_t, 0],
|
|
30
|
-
[sin_t, cos_t, 0],
|
|
31
|
-
[0, 0, 1]]
|
|
32
|
-
return m
|
|
3
|
+
from plotastrodata.matrix_utils import Mrot3d
|
|
33
4
|
|
|
34
5
|
|
|
35
6
|
def obs2sys(xobs: np.ndarray, yobs: np.ndarray, zobs: np.ndarray,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def Mfac(f0: float = 1, f1: float = 1) -> np.ndarray:
|
|
5
|
+
"""2 x 2 matrix for (x,y) --> (f0 * x, f1 * y).
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
f0 (float, optional): Defaults to 1.
|
|
9
|
+
f1 (float, optional): Defaults to 1.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
np.ndarray: Matrix for the multiplication.
|
|
13
|
+
"""
|
|
14
|
+
return np.array([[f0, 0], [0, f1]])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def Mrot(pa: float = 0) -> np.ndarray:
|
|
18
|
+
"""2 x 2 matrix for rotation.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
pa (float, optional): How many degrees are the image rotated by. Defaults to 0.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
np.ndarray: Matrix for the rotation.
|
|
25
|
+
"""
|
|
26
|
+
p = np.radians(pa)
|
|
27
|
+
return np.array([[np.cos(p), -np.sin(p)], [np.sin(p), np.cos(p)]])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dot2d(M: np.ndarray = [[1, 0], [0, 1]],
|
|
31
|
+
a: np.ndarray = [0, 0]) -> np.ndarray:
|
|
32
|
+
"""To maltiply a 2 x 2 matrix to (x,y) with arrays of x and y.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
M (np.ndarray, optional): 2 x 2 matrix. Defaults to [[1, 0], [0, 1]].
|
|
36
|
+
a (np.ndarray, optional): 2D vector (of 1D arrays). Defaults to [0].
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
np.ndarray: The 2D vector after the matrix multiplied.
|
|
40
|
+
"""
|
|
41
|
+
x = M[0][0] * np.array(a[0]) + M[0][1] * np.array(a[1])
|
|
42
|
+
y = M[1][0] * np.array(a[0]) + M[1][1] * np.array(a[1])
|
|
43
|
+
return np.array([x, y])
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def Mrot3d(t: float, axis: int = 3) -> np.ndarray:
|
|
47
|
+
"""3D rotation matrix around a specified axis.
|
|
48
|
+
|
|
49
|
+
This function creates a 3x3 rotation matrix for rotating coordinates around
|
|
50
|
+
the x-axis (axis=1), y-axis (axis=2), or z-axis (axis=3) by t degrees.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
t (float): Rotation angle in degrees.
|
|
54
|
+
axis (int, optional): Axis to rotate around - 1 for x-axis, 2 for y-axis, 3 for z-axis. Defaults to 3.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
np.ndarray: 3x3 rotation matrix that rotates coordinates around the specified axis by t degrees.
|
|
58
|
+
"""
|
|
59
|
+
cos_t = np.cos(np.radians(t))
|
|
60
|
+
sin_t = np.sin(np.radians(t))
|
|
61
|
+
match axis:
|
|
62
|
+
case 1:
|
|
63
|
+
m = [[1, 0, 0],
|
|
64
|
+
[0, cos_t, -sin_t],
|
|
65
|
+
[0, sin_t, cos_t]]
|
|
66
|
+
case 2:
|
|
67
|
+
m = [[cos_t, 0, sin_t],
|
|
68
|
+
[0, 1, 0],
|
|
69
|
+
[-sin_t, 0, cos_t]]
|
|
70
|
+
case 3:
|
|
71
|
+
m = [[cos_t, -sin_t, 0],
|
|
72
|
+
[sin_t, cos_t, 0],
|
|
73
|
+
[0, 0, 1]]
|
|
74
|
+
return m
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import curve_fit
|
|
3
|
+
from scipy.special import erf
|
|
4
|
+
from scipy.interpolate import RegularGridInterpolator as RGI
|
|
5
|
+
|
|
6
|
+
from plotastrodata.matrix_utils import Mrot, dot2d
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def listing(*args) -> list:
|
|
10
|
+
"""Output a list of the input when the input is string or number.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
list: With a single non-list input, the output is a list like ['a'], rather than [['a']].
|
|
14
|
+
"""
|
|
15
|
+
nums = [float, int, np.float64, np.int64, np.float32, np.int32]
|
|
16
|
+
b = [None] * len(args)
|
|
17
|
+
for i, a in enumerate(args):
|
|
18
|
+
b[i] = [a] if type(a) in (nums + [str]) else a
|
|
19
|
+
if len(args) == 1:
|
|
20
|
+
b = b[0]
|
|
21
|
+
return b
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def isdeg(s: str) -> bool:
|
|
25
|
+
"""Whether the given string means degree.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
s (str): The string to be checked.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
bool: Whether the given string means degree.
|
|
32
|
+
"""
|
|
33
|
+
if type(s) is str:
|
|
34
|
+
return s.strip() in ['deg', 'DEG', 'degree', 'DEGREE']
|
|
35
|
+
else:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def estimate_rms(data: np.ndarray, sigma: float | str | None = 'hist') -> float:
|
|
40
|
+
"""Estimate a noise level of a N-D array.
|
|
41
|
+
When a float number or None is given, this function just outputs it.
|
|
42
|
+
Following methos are acceptable.
|
|
43
|
+
'edge': use data[0] and data[-1].
|
|
44
|
+
'neg': use only negative values.
|
|
45
|
+
'med': use the median of data^2 assuming Gaussian.
|
|
46
|
+
'iter': exclude outliers.
|
|
47
|
+
'out': exclude inner 60% about axes=-2 and -1.
|
|
48
|
+
'hist': fit histgram with Gaussian.
|
|
49
|
+
'hist-pbcor': fit histgram with PB-corrected Gaussian.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
data (np.ndarray): N-D array.
|
|
53
|
+
sigma (float or str): One of the methods above. Defaults to 'hist'.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
float: the estimated root mean square of noise.
|
|
57
|
+
"""
|
|
58
|
+
if sigma is None:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
nums = [float, int, np.float64, np.int64, np.float32, np.int32]
|
|
62
|
+
if type(sigma) in nums:
|
|
63
|
+
noise = sigma
|
|
64
|
+
elif np.ndim(np.squeeze(data)) == 0:
|
|
65
|
+
print('sigma cannot be estimated from only one pixel.')
|
|
66
|
+
noise = 0.0
|
|
67
|
+
elif sigma == 'edge':
|
|
68
|
+
ave = np.nanmean(data[::len(data) - 1])
|
|
69
|
+
noise = np.nanstd(data[::len(data) - 1])
|
|
70
|
+
if np.abs(ave) > 0.2 * noise:
|
|
71
|
+
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
72
|
+
elif sigma == 'neg':
|
|
73
|
+
noise = np.sqrt(np.nanmean(data[data < 0]**2))
|
|
74
|
+
elif sigma == 'med':
|
|
75
|
+
noise = np.sqrt(np.nanmedian(data**2) / 0.454936)
|
|
76
|
+
elif sigma == 'iter':
|
|
77
|
+
n = data.copy()
|
|
78
|
+
for _ in range(5):
|
|
79
|
+
ave, sig = np.nanmean(n), np.nanstd(n)
|
|
80
|
+
n = n - ave
|
|
81
|
+
n = n[np.abs(n) < 3.5 * sig]
|
|
82
|
+
ave = np.nanmean(n)
|
|
83
|
+
noise = np.nanstd(n)
|
|
84
|
+
if np.abs(ave) > 0.2 * noise:
|
|
85
|
+
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
86
|
+
elif sigma == 'out':
|
|
87
|
+
n, n0, n1 = data.copy(), len(data), len(data[0])
|
|
88
|
+
n = np.moveaxis(n, [-2, -1], [0, 1])
|
|
89
|
+
n[n0//5: n0*4//5, n1//5: n1*4//5] = np.nan
|
|
90
|
+
if np.all(np.isnan(n)):
|
|
91
|
+
print('sigma=\'neg\' instead of \'out\' because'
|
|
92
|
+
+ ' the outer region is filled with nan.')
|
|
93
|
+
noise = np.sqrt(np.nanmean(data[data < 0]**2))
|
|
94
|
+
else:
|
|
95
|
+
ave = np.nanmean(n)
|
|
96
|
+
noise = np.nanstd(n)
|
|
97
|
+
if np.abs(ave) > 0.2 * noise:
|
|
98
|
+
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
99
|
+
elif sigma[:4] == 'hist':
|
|
100
|
+
m0, s0 = np.nanmean(data), np.nanstd(data)
|
|
101
|
+
hist, hbin = np.histogram(data[~np.isnan(data)],
|
|
102
|
+
bins=100, density=True,
|
|
103
|
+
range=(m0 - s0 * 5, m0 + s0 * 5))
|
|
104
|
+
hist, hbin = hist * s0, (hbin[:-1] + hbin[1:]) / 2 / s0
|
|
105
|
+
if sigma[4:] == '-pbcor':
|
|
106
|
+
def g(x, s, c, R):
|
|
107
|
+
xn = (x - c) / np.sqrt(2) / s
|
|
108
|
+
return (erf(xn) - erf(xn * np.exp(-R**2))) / (2 * (x-c) * R**2)
|
|
109
|
+
else:
|
|
110
|
+
def g(x, s, c, R):
|
|
111
|
+
return np.exp(-((x-c)/s)**2 / 2) / np.sqrt(2*np.pi) / s
|
|
112
|
+
popt, _ = curve_fit(g, hbin, hist, p0=[1, 0, 1])
|
|
113
|
+
ave = popt[1]
|
|
114
|
+
noise = popt[0]
|
|
115
|
+
if np.abs(ave) > 0.2 * noise:
|
|
116
|
+
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
117
|
+
noise = noise * s0
|
|
118
|
+
return noise
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def trim(data: np.ndarray | None = None, x: np.ndarray | None = None,
|
|
122
|
+
y: np.ndarray | None = None, v: np.ndarray | None = None,
|
|
123
|
+
xlim: list[float, float] | None = None,
|
|
124
|
+
ylim: list[float, float] | None = None,
|
|
125
|
+
vlim: list[float, float] | None = None,
|
|
126
|
+
pv: bool = False) -> tuple[np.ndarray, list[np.ndarray, np.ndarray, np.ndarray]]:
|
|
127
|
+
"""Trim 2D or 3D data by given coordinates and their limits.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
data (np.ndarray, optional): 2D or 3D array. Defaults to None.
|
|
131
|
+
x (np.ndarray, optional): 1D array. Defaults to None.
|
|
132
|
+
y (np.ndarray, optional): 1D array. Defaults to None.
|
|
133
|
+
v (np.ndarray, optional): 1D array. Defaults to None.
|
|
134
|
+
xlim (list, optional): [xmin, xmax]. Defaults to None.
|
|
135
|
+
ylim (list, optional): [ymin, ymax]. Defaults to None.
|
|
136
|
+
vlim (list, optional): [vmin, vmax]. Defaults to None.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
tuple: Trimmed (data, [x,y,v]).
|
|
140
|
+
"""
|
|
141
|
+
xout, yout, vout, dataout = x, y, v, data
|
|
142
|
+
i0 = j0 = k0 = 0
|
|
143
|
+
i1 = j1 = k1 = 100000
|
|
144
|
+
if not (x is None or xlim is None):
|
|
145
|
+
if not (None in xlim):
|
|
146
|
+
x0 = np.max([np.min(x), xlim[0]])
|
|
147
|
+
x1 = np.min([np.max(x), xlim[1]])
|
|
148
|
+
i0 = np.argmin(np.abs(x - x0))
|
|
149
|
+
i1 = np.argmin(np.abs(x - x1))
|
|
150
|
+
i0, i1 = sorted([i0, i1])
|
|
151
|
+
xout = x[i0:i1+1]
|
|
152
|
+
if not (y is None or ylim is None):
|
|
153
|
+
if not (None in ylim):
|
|
154
|
+
y0 = np.max([np.min(y), ylim[0]])
|
|
155
|
+
y1 = np.min([np.max(y), ylim[1]])
|
|
156
|
+
j0 = np.argmin(np.abs(y - y0))
|
|
157
|
+
j1 = np.argmin(np.abs(y - y1))
|
|
158
|
+
j0, j1 = sorted([j0, j1])
|
|
159
|
+
yout = y[j0:j1+1]
|
|
160
|
+
if not (v is None or vlim is None):
|
|
161
|
+
if not (None in vlim):
|
|
162
|
+
v0 = np.max([np.min(v), vlim[0]])
|
|
163
|
+
v1 = np.min([np.max(v), vlim[1]])
|
|
164
|
+
k0 = np.argmin(np.abs(v - v0))
|
|
165
|
+
k1 = np.argmin(np.abs(v - v1))
|
|
166
|
+
k0, k1 = sorted([k0, k1])
|
|
167
|
+
vout = v[k0:k1+1]
|
|
168
|
+
if data is not None:
|
|
169
|
+
d = np.squeeze(data)
|
|
170
|
+
if np.ndim(d) == 0:
|
|
171
|
+
print('data has only one pixel.')
|
|
172
|
+
d = data
|
|
173
|
+
if np.ndim(d) == 2:
|
|
174
|
+
if pv:
|
|
175
|
+
j0, j1 = k0, k1
|
|
176
|
+
dataout = d[j0:j1+1, i0:i1+1]
|
|
177
|
+
else:
|
|
178
|
+
d = np.moveaxis(d, [-3, -2, -1], [0, 1, 2])
|
|
179
|
+
d = d[k0:k1+1, j0:j1+1, i0:i1+1]
|
|
180
|
+
d = np.moveaxis(d, [0, 1, 2], [-3, -2, -1])
|
|
181
|
+
dataout = d
|
|
182
|
+
return dataout, [xout, yout, vout]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def to4dim(data: np.ndarray) -> np.ndarray:
|
|
186
|
+
"""Change a 2D, 3D, or 4D array to a 4D array.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
data (np.ndarray): Input data. 2D, 3D, or 4D.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
np.ndarray: Output 4D array.
|
|
193
|
+
"""
|
|
194
|
+
if np.ndim(data) == 2:
|
|
195
|
+
d = np.array([[data]])
|
|
196
|
+
elif np.ndim(data) == 3:
|
|
197
|
+
d = np.array([data])
|
|
198
|
+
else:
|
|
199
|
+
d = np.array(data)
|
|
200
|
+
return d
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def RGIxy(y: np.ndarray, x: np.ndarray, data: np.ndarray,
|
|
204
|
+
yxnew: tuple[np.ndarray, np.ndarray] | None = None,
|
|
205
|
+
**kwargs) -> object | np.ndarray:
|
|
206
|
+
"""RGI for x and y at each channel.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
y (np.ndarray): 1D array. Second coordinate.
|
|
210
|
+
x (np.ndarray): 1D array. First coordinate.
|
|
211
|
+
data (np.ndarray): 2D, 3D, or 4D array.
|
|
212
|
+
yxnew (tuple, optional): (ynew, xnew), where ynew and xnew are 1D or 2D arrays. Defaults to None.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
np.ndarray: The RGI function or the interpolated array.
|
|
216
|
+
"""
|
|
217
|
+
if not np.ndim(data) in [2, 3, 4]:
|
|
218
|
+
print('data must be 2D, 3D, or 4D.')
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
_kw = {'bounds_error': False, 'fill_value': np.nan,
|
|
222
|
+
'method': 'linear'}
|
|
223
|
+
_kw.update(kwargs)
|
|
224
|
+
c4d = to4dim(data)
|
|
225
|
+
c4d[np.isnan(c4d)] = 0
|
|
226
|
+
f = [[RGI((y, x), c2d, **_kw)
|
|
227
|
+
for c2d in c3d] for c3d in c4d]
|
|
228
|
+
if yxnew is None:
|
|
229
|
+
if len(f) == 1:
|
|
230
|
+
f = f[0]
|
|
231
|
+
if len(f) == 1:
|
|
232
|
+
f = f[0]
|
|
233
|
+
return f
|
|
234
|
+
else:
|
|
235
|
+
return np.squeeze([[f2d(tuple(yxnew)) for f2d in f3d] for f3d in f])
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def RGIxyv(v: np.ndarray, y: np.ndarray, x: np.ndarray, data: np.ndarray,
|
|
239
|
+
vyxnew: tuple[np.ndarray, np.ndarray, np.ndarray] | None = None,
|
|
240
|
+
**kwargs) -> object | np.ndarray:
|
|
241
|
+
"""RGI in the x-y-v space.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
v (np.ndarray): 1D array. Third coordinate.
|
|
245
|
+
y (np.ndarray): 1D array. Second coordinate.
|
|
246
|
+
x (np.ndarray): 1D array. First coordinate.
|
|
247
|
+
data (np.ndarray): 3D or 4D array.
|
|
248
|
+
vyxnew (tuple, optional): (vnew, ynew, xnew), where vnew, ynew, and xnew are 1D or 2D arrays. Defaults to None.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
np.ndarray: The RGI function or the interpolated array.
|
|
252
|
+
"""
|
|
253
|
+
if not np.ndim(data) in [3, 4]:
|
|
254
|
+
print('data must be 3D or 4D.')
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
_kw = {'bounds_error': False, 'fill_value': np.nan,
|
|
258
|
+
'method': 'linear'}
|
|
259
|
+
_kw.update(kwargs)
|
|
260
|
+
c4d = to4dim(data)
|
|
261
|
+
c4d[np.isnan(c4d)] = 0
|
|
262
|
+
f = [RGI((v, y, x), c3d, **_kw) for c3d in c4d]
|
|
263
|
+
if vyxnew is None:
|
|
264
|
+
if len(f) == 1:
|
|
265
|
+
f = f[0]
|
|
266
|
+
return f
|
|
267
|
+
else:
|
|
268
|
+
return np.squeeze([f3d(tuple(vyxnew)) for f3d in f])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def gaussian2d(xy: np.ndarray,
|
|
272
|
+
amplitude: float, xo: float, yo: float,
|
|
273
|
+
fwhm_major: float, fwhm_minor: float, pa: float) -> np.ndarray:
|
|
274
|
+
"""Two dimensional Gaussian function.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
xy (np.ndarray): A pair of (x, y).
|
|
278
|
+
amplitude (float): Peak value.
|
|
279
|
+
xo (float): Offset in the x direction.
|
|
280
|
+
yo (float): Offset in the y direction.
|
|
281
|
+
fwhm_major (float): Full width at half maximum in the major axis (but can be shorter than the minor axis).
|
|
282
|
+
fwhm_minor (float): Full width at half maximum in the minor axis (but can be longer then the major axis).
|
|
283
|
+
pa (float): Position angle of the major axis from the +y axis to the +x axis in the unit of degree.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
g (np.ndarray): 2D numpy array.
|
|
287
|
+
"""
|
|
288
|
+
s, t = dot2d(Mrot(-pa), [xy[1] - yo, xy[0] - xo])
|
|
289
|
+
g = amplitude * np.exp2(-4 * ((s / fwhm_major)**2 + (t / fwhm_minor)**2))
|
|
290
|
+
return g
|
|
@@ -4,7 +4,8 @@ import matplotlib.pyplot as plt
|
|
|
4
4
|
from matplotlib.patches import Ellipse, Rectangle
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
|
-
from plotastrodata.
|
|
7
|
+
from plotastrodata.coord_utils import coord2xy, xy2coord
|
|
8
|
+
from plotastrodata.other_utils import listing, estimate_rms
|
|
8
9
|
from plotastrodata.analysis_utils import AstroData, AstroFrame
|
|
9
10
|
|
|
10
11
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: plotastrodata
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: plotastrodata is a tool for astronomers to create figures from FITS files and perform fundamental data analyses with ease.
|
|
5
5
|
Home-page: https://github.com/yusukeaso-astron/plotastrodata
|
|
6
6
|
Download-URL: https://github.com/yusukeaso-astron/plotastrodata
|
|
@@ -5,10 +5,13 @@ setup.py
|
|
|
5
5
|
plotastrodata/__init__.py
|
|
6
6
|
plotastrodata/analysis_utils.py
|
|
7
7
|
plotastrodata/const_utils.py
|
|
8
|
+
plotastrodata/coord_utils.py
|
|
9
|
+
plotastrodata/ext_utils.py
|
|
8
10
|
plotastrodata/fft_utils.py
|
|
9
11
|
plotastrodata/fits_utils.py
|
|
10
12
|
plotastrodata/fitting_utils.py
|
|
11
13
|
plotastrodata/los_utils.py
|
|
14
|
+
plotastrodata/matrix_utils.py
|
|
12
15
|
plotastrodata/other_utils.py
|
|
13
16
|
plotastrodata/plot_utils.py
|
|
14
17
|
plotastrodata.egg-info/PKG-INFO
|
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
import shlex
|
|
3
|
-
import numpy as np
|
|
4
|
-
from astropy.coordinates import SkyCoord, FK5, FK4
|
|
5
|
-
from astropy import units
|
|
6
|
-
from scipy.optimize import curve_fit
|
|
7
|
-
from scipy.special import erf
|
|
8
|
-
|
|
9
|
-
from plotastrodata import const_utils as cu
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def terminal(cmd: str, **kwargs) -> None:
|
|
13
|
-
"""Run a terminal command through subprocess.run.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
cmd (str): Terminal command.
|
|
17
|
-
"""
|
|
18
|
-
subprocess.run(shlex.split(cmd), **kwargs)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def runpython(filename: str, **kwargs) -> None:
|
|
22
|
-
"""Run a python file.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
filename (str): Python file name.
|
|
26
|
-
"""
|
|
27
|
-
terminal(f'python {filename}', **kwargs)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def listing(*args) -> list:
|
|
31
|
-
"""Output a list of the input when the input is string or number.
|
|
32
|
-
|
|
33
|
-
Returns:
|
|
34
|
-
list: With a single non-list input, the output is a list like ['a'], rather than [['a']].
|
|
35
|
-
"""
|
|
36
|
-
nums = [float, int, np.float64, np.int64, np.float32, np.int32]
|
|
37
|
-
b = [None] * len(args)
|
|
38
|
-
for i, a in enumerate(args):
|
|
39
|
-
b[i] = [a] if type(a) in (nums + [str]) else a
|
|
40
|
-
if len(args) == 1:
|
|
41
|
-
b = b[0]
|
|
42
|
-
return b
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def isdeg(s: str) -> bool:
|
|
46
|
-
"""Whether the given string means degree.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
s (str): The string to be checked.
|
|
50
|
-
|
|
51
|
-
Returns:
|
|
52
|
-
bool: Whether the given string means degree.
|
|
53
|
-
"""
|
|
54
|
-
if type(s) is str:
|
|
55
|
-
return s.strip() in ['deg', 'DEG', 'degree', 'DEGREE']
|
|
56
|
-
else:
|
|
57
|
-
return False
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _getframe(coord: str, s: str = '') -> tuple:
|
|
61
|
-
"""Internal function to pick up the frame name from the coordinates.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
coord (str): something like "J2000 01h23m45.6s 01d23m45.6s"
|
|
65
|
-
s (str, optional): To distinguish coord and coordorg. Defaults to ''.
|
|
66
|
-
|
|
67
|
-
Returns:
|
|
68
|
-
tuple: updated coord and frame. frame is FK5(equinox='J2000), FK4(equinox='B1950'), or 'icrs'.
|
|
69
|
-
"""
|
|
70
|
-
if len(c := coord.split()) == 3:
|
|
71
|
-
coord = f'{c[1]} {c[2]}'
|
|
72
|
-
if 'J2000' in c[0]:
|
|
73
|
-
frame = FK5(equinox='J2000')
|
|
74
|
-
elif 'FK5' in c[0]:
|
|
75
|
-
frame = FK5(equinox='J2000')
|
|
76
|
-
elif 'B1950' in c[0]:
|
|
77
|
-
frame = FK4(equinox='B1950')
|
|
78
|
-
elif 'FK4' in c[0]:
|
|
79
|
-
frame = FK4(equinox='B1950')
|
|
80
|
-
elif 'ICRS' in c[0]:
|
|
81
|
-
frame = 'icrs'
|
|
82
|
-
else:
|
|
83
|
-
print(f'Unknown equinox found in coord{s}. ICRS is used')
|
|
84
|
-
frame = 'icrs'
|
|
85
|
-
else:
|
|
86
|
-
frame = None
|
|
87
|
-
return coord, frame
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _updateframe(frame: str) -> str:
|
|
91
|
-
"""Internal function to str frame to astropy frame.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
frame (str): _description_
|
|
95
|
-
|
|
96
|
-
Returns:
|
|
97
|
-
str: frame as is, FK5(equinox='J2000'), FK4(equinox='B1950'), or 'icrs'.
|
|
98
|
-
"""
|
|
99
|
-
if 'ICRS' in frame:
|
|
100
|
-
a = 'icrs'
|
|
101
|
-
elif 'J2000' in frame or 'FK5' in frame:
|
|
102
|
-
a = FK5(equinox='J2000')
|
|
103
|
-
elif 'B1950' in frame or 'FK4' in frame:
|
|
104
|
-
a = FK4(equinox='B1950')
|
|
105
|
-
else:
|
|
106
|
-
a = frame
|
|
107
|
-
return a
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def coord2xy(coords: str | list, coordorg: str = '00h00m00s 00d00m00s',
|
|
111
|
-
frame: str | None = None, frameorg: str | None = None,
|
|
112
|
-
) -> np.ndarray:
|
|
113
|
-
"""Transform R.A.-Dec. to relative (deg, deg).
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
coords (str, list): something like '01h23m45.6s 01d23m45.6s'. The input can be a list of str in an arbitrary shape.
|
|
117
|
-
coordorg (str, optional): something like '01h23m45.6s 01d23m45.6s'. The origin of the relative (deg, deg). Defaults to '00h00m00s 00d00m00s'.
|
|
118
|
-
frame (str, optional): coordinate frame. Defaults to None.
|
|
119
|
-
frameorg (str, optional): coordinate frame of the origin. Defaults to None.
|
|
120
|
-
|
|
121
|
-
Returns:
|
|
122
|
-
np.ndarray: [(array of) alphas, (array of) deltas] in degree. The shape of alphas and deltas is the input shape. With a single input, the output is [alpha0, delta0].
|
|
123
|
-
"""
|
|
124
|
-
coordorg, frameorg_c = _getframe(coordorg, 'org')
|
|
125
|
-
frameorg = frameorg_c if frameorg is None else _updateframe(frameorg)
|
|
126
|
-
if type(coords) is list:
|
|
127
|
-
for i in range(len(coords)):
|
|
128
|
-
coords[i], frame_c = _getframe(coords[i])
|
|
129
|
-
else:
|
|
130
|
-
coords, frame_c = _getframe(coords)
|
|
131
|
-
frame = frame_c if frame is None else _updateframe(frame)
|
|
132
|
-
if frame is None and frameorg is not None:
|
|
133
|
-
frame = frameorg
|
|
134
|
-
if frame is not None and frameorg is None:
|
|
135
|
-
frameorg = frame
|
|
136
|
-
if frame is None and frameorg is None:
|
|
137
|
-
frame = frameorg = 'icrs'
|
|
138
|
-
clist = SkyCoord(coords, frame=frame)
|
|
139
|
-
c0 = SkyCoord(coordorg, frame=frameorg)
|
|
140
|
-
c0 = c0.transform_to(frame=frame)
|
|
141
|
-
xy = c0.spherical_offsets_to(clist)
|
|
142
|
-
return np.array([xy[0].degree, xy[1].degree])
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def xy2coord(xy: list, coordorg: str = '00h00m00s 00d00m00s',
|
|
146
|
-
frame: str | None = None, frameorg: str | None = None,
|
|
147
|
-
) -> str:
|
|
148
|
-
"""Transform relative (deg, deg) to R.A.-Dec.
|
|
149
|
-
|
|
150
|
-
Args:
|
|
151
|
-
xy (list): [(array of) alphas, (array of) deltas] in degree. alphas and deltas can have an arbitrary shape.
|
|
152
|
-
coordorg (str): something like '01h23m45.6s 01d23m45.6s'. The origin of the relative (deg, deg). Defaults to '00h00m00s 00d00m00s'.
|
|
153
|
-
frame (str): coordinate frame. Defaults to None.
|
|
154
|
-
frameorg (str): coordinate frame of the origin. Defaults to None.
|
|
155
|
-
|
|
156
|
-
Returns:
|
|
157
|
-
str: something like '01h23m45.6s 01d23m45.6s'. With multiple inputs, the output has the input shape.
|
|
158
|
-
"""
|
|
159
|
-
coordorg, frameorg_c = _getframe(coordorg, 'org')
|
|
160
|
-
frameorg = frameorg_c if frameorg is None else _updateframe(frameorg)
|
|
161
|
-
if frameorg is None:
|
|
162
|
-
frameorg = 'icrs'
|
|
163
|
-
frame = frameorg if frame is None else _updateframe(frame)
|
|
164
|
-
c0 = SkyCoord(coordorg, frame=frameorg)
|
|
165
|
-
coords = c0.spherical_offsets_by(*xy * units.degree)
|
|
166
|
-
coords = coords.transform_to(frame=frame)
|
|
167
|
-
return coords.to_string('hmsdms')
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def rel2abs(xrel: float, yrel: float,
|
|
171
|
-
x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
172
|
-
"""Transform relative coordinates to absolute ones.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
xrel (float): 0 <= xrel <= 1. 0 and 1 correspond to x[0] and x[-1], respectively. Arbitrary shape.
|
|
176
|
-
yrel (float): same as xrel.
|
|
177
|
-
x (np.ndarray): [x0, x0+dx, x0+2dx, ...]
|
|
178
|
-
y (np.ndarray): [y0, y0+dy, y0+2dy, ...]
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
np.ndarray: [xabs, yabs]. Each has the input's shape.
|
|
182
|
-
"""
|
|
183
|
-
xabs = (1. - xrel)*x[0] + xrel*x[-1]
|
|
184
|
-
yabs = (1. - yrel)*y[0] + yrel*y[-1]
|
|
185
|
-
return np.array([xabs, yabs])
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def abs2rel(xabs: float, yabs: float,
|
|
189
|
-
x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
|
190
|
-
"""Transform absolute coordinates to relative ones.
|
|
191
|
-
|
|
192
|
-
Args:
|
|
193
|
-
xabs (float): In the same frame of x.
|
|
194
|
-
yabs (float): In the same frame of y.
|
|
195
|
-
x (np.ndarray): [x0, x0+dx, x0+2dx, ...]
|
|
196
|
-
y (np.ndarray): [y0, y0+dy, y0+2dy, ...]
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
ndarray: [xrel, yrel]. Each has the input's shape. 0 <= xrel, yrel <= 1. 0 and 1 correspond to x[0] and x[-1], respectively.
|
|
200
|
-
"""
|
|
201
|
-
xrel = (xabs - x[0]) / (x[-1] - x[0])
|
|
202
|
-
yrel = (yabs - y[0]) / (y[-1] - y[0])
|
|
203
|
-
return np.array([xrel, yrel])
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def estimate_rms(data: np.ndarray, sigma: float | str | None = 'hist') -> float:
|
|
207
|
-
"""Estimate a noise level of a N-D array.
|
|
208
|
-
When a float number or None is given, this function just outputs it.
|
|
209
|
-
Following methos are acceptable.
|
|
210
|
-
'edge': use data[0] and data[-1].
|
|
211
|
-
'neg': use only negative values.
|
|
212
|
-
'med': use the median of data^2 assuming Gaussian.
|
|
213
|
-
'iter': exclude outliers.
|
|
214
|
-
'out': exclude inner 60% about axes=-2 and -1.
|
|
215
|
-
'hist': fit histgram with Gaussian.
|
|
216
|
-
'hist-pbcor': fit histgram with PB-corrected Gaussian.
|
|
217
|
-
|
|
218
|
-
Args:
|
|
219
|
-
data (np.ndarray): N-D array.
|
|
220
|
-
sigma (float or str): One of the methods above. Defaults to 'hist'.
|
|
221
|
-
|
|
222
|
-
Returns:
|
|
223
|
-
float: the estimated root mean square of noise.
|
|
224
|
-
"""
|
|
225
|
-
if sigma is None:
|
|
226
|
-
return None
|
|
227
|
-
|
|
228
|
-
nums = [float, int, np.float64, np.int64, np.float32, np.int32]
|
|
229
|
-
if type(sigma) in nums:
|
|
230
|
-
noise = sigma
|
|
231
|
-
elif np.ndim(np.squeeze(data)) == 0:
|
|
232
|
-
print('sigma cannot be estimated from only one pixel.')
|
|
233
|
-
noise = 0.0
|
|
234
|
-
elif sigma == 'edge':
|
|
235
|
-
ave = np.nanmean(data[::len(data) - 1])
|
|
236
|
-
noise = np.nanstd(data[::len(data) - 1])
|
|
237
|
-
if np.abs(ave) > 0.2 * noise:
|
|
238
|
-
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
239
|
-
elif sigma == 'neg':
|
|
240
|
-
noise = np.sqrt(np.nanmean(data[data < 0]**2))
|
|
241
|
-
elif sigma == 'med':
|
|
242
|
-
noise = np.sqrt(np.nanmedian(data**2) / 0.454936)
|
|
243
|
-
elif sigma == 'iter':
|
|
244
|
-
n = data.copy()
|
|
245
|
-
for _ in range(5):
|
|
246
|
-
ave, sig = np.nanmean(n), np.nanstd(n)
|
|
247
|
-
n = n - ave
|
|
248
|
-
n = n[np.abs(n) < 3.5 * sig]
|
|
249
|
-
ave = np.nanmean(n)
|
|
250
|
-
noise = np.nanstd(n)
|
|
251
|
-
if np.abs(ave) > 0.2 * noise:
|
|
252
|
-
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
253
|
-
elif sigma == 'out':
|
|
254
|
-
n, n0, n1 = data.copy(), len(data), len(data[0])
|
|
255
|
-
n = np.moveaxis(n, [-2, -1], [0, 1])
|
|
256
|
-
n[n0//5: n0*4//5, n1//5: n1*4//5] = np.nan
|
|
257
|
-
if np.all(np.isnan(n)):
|
|
258
|
-
print('sigma=\'neg\' instead of \'out\' because'
|
|
259
|
-
+ ' the outer region is filled with nan.')
|
|
260
|
-
noise = np.sqrt(np.nanmean(data[data < 0]**2))
|
|
261
|
-
else:
|
|
262
|
-
ave = np.nanmean(n)
|
|
263
|
-
noise = np.nanstd(n)
|
|
264
|
-
if np.abs(ave) > 0.2 * noise:
|
|
265
|
-
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
266
|
-
elif sigma[:4] == 'hist':
|
|
267
|
-
m0, s0 = np.nanmean(data), np.nanstd(data)
|
|
268
|
-
hist, hbin = np.histogram(data[~np.isnan(data)],
|
|
269
|
-
bins=100, density=True,
|
|
270
|
-
range=(m0 - s0 * 5, m0 + s0 * 5))
|
|
271
|
-
hist, hbin = hist * s0, (hbin[:-1] + hbin[1:]) / 2 / s0
|
|
272
|
-
if sigma[4:] == '-pbcor':
|
|
273
|
-
def g(x, s, c, R):
|
|
274
|
-
xn = (x - c) / np.sqrt(2) / s
|
|
275
|
-
return (erf(xn) - erf(xn * np.exp(-R**2))) / (2 * (x-c) * R**2)
|
|
276
|
-
else:
|
|
277
|
-
def g(x, s, c, R):
|
|
278
|
-
return np.exp(-((x-c)/s)**2 / 2) / np.sqrt(2*np.pi) / s
|
|
279
|
-
popt, _ = curve_fit(g, hbin, hist, p0=[1, 0, 1])
|
|
280
|
-
ave = popt[1]
|
|
281
|
-
noise = popt[0]
|
|
282
|
-
if np.abs(ave) > 0.2 * noise:
|
|
283
|
-
print('Warning: The intensity offset is larger than 0.2 sigma.')
|
|
284
|
-
noise = noise * s0
|
|
285
|
-
return noise
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def trim(data: np.ndarray | None = None, x: np.ndarray | None = None,
|
|
289
|
-
y: np.ndarray | None = None, v: np.ndarray | None = None,
|
|
290
|
-
xlim: list[float, float] | None = None,
|
|
291
|
-
ylim: list[float, float] | None = None,
|
|
292
|
-
vlim: list[float, float] | None = None,
|
|
293
|
-
pv: bool = False) -> tuple[np.ndarray, list[np.ndarray, np.ndarray, np.ndarray]]:
|
|
294
|
-
"""Trim 2D or 3D data by given coordinates and their limits.
|
|
295
|
-
|
|
296
|
-
Args:
|
|
297
|
-
data (np.ndarray, optional): 2D or 3D array. Defaults to None.
|
|
298
|
-
x (np.ndarray, optional): 1D array. Defaults to None.
|
|
299
|
-
y (np.ndarray, optional): 1D array. Defaults to None.
|
|
300
|
-
v (np.ndarray, optional): 1D array. Defaults to None.
|
|
301
|
-
xlim (list, optional): [xmin, xmax]. Defaults to None.
|
|
302
|
-
ylim (list, optional): [ymin, ymax]. Defaults to None.
|
|
303
|
-
vlim (list, optional): [vmin, vmax]. Defaults to None.
|
|
304
|
-
|
|
305
|
-
Returns:
|
|
306
|
-
tuple: Trimmed (data, [x,y,v]).
|
|
307
|
-
"""
|
|
308
|
-
xout, yout, vout, dataout = x, y, v, data
|
|
309
|
-
i0 = j0 = k0 = 0
|
|
310
|
-
i1 = j1 = k1 = 100000
|
|
311
|
-
if not (x is None or xlim is None):
|
|
312
|
-
if not (None in xlim):
|
|
313
|
-
x0 = np.max([np.min(x), xlim[0]])
|
|
314
|
-
x1 = np.min([np.max(x), xlim[1]])
|
|
315
|
-
i0 = np.argmin(np.abs(x - x0))
|
|
316
|
-
i1 = np.argmin(np.abs(x - x1))
|
|
317
|
-
i0, i1 = sorted([i0, i1])
|
|
318
|
-
xout = x[i0:i1+1]
|
|
319
|
-
if not (y is None or ylim is None):
|
|
320
|
-
if not (None in ylim):
|
|
321
|
-
y0 = np.max([np.min(y), ylim[0]])
|
|
322
|
-
y1 = np.min([np.max(y), ylim[1]])
|
|
323
|
-
j0 = np.argmin(np.abs(y - y0))
|
|
324
|
-
j1 = np.argmin(np.abs(y - y1))
|
|
325
|
-
j0, j1 = sorted([j0, j1])
|
|
326
|
-
yout = y[j0:j1+1]
|
|
327
|
-
if not (v is None or vlim is None):
|
|
328
|
-
if not (None in vlim):
|
|
329
|
-
v0 = np.max([np.min(v), vlim[0]])
|
|
330
|
-
v1 = np.min([np.max(v), vlim[1]])
|
|
331
|
-
k0 = np.argmin(np.abs(v - v0))
|
|
332
|
-
k1 = np.argmin(np.abs(v - v1))
|
|
333
|
-
k0, k1 = sorted([k0, k1])
|
|
334
|
-
vout = v[k0:k1+1]
|
|
335
|
-
if data is not None:
|
|
336
|
-
d = np.squeeze(data)
|
|
337
|
-
if np.ndim(d) == 0:
|
|
338
|
-
print('data has only one pixel.')
|
|
339
|
-
d = data
|
|
340
|
-
if np.ndim(d) == 2:
|
|
341
|
-
if pv:
|
|
342
|
-
j0, j1 = k0, k1
|
|
343
|
-
dataout = d[j0:j1+1, i0:i1+1]
|
|
344
|
-
else:
|
|
345
|
-
d = np.moveaxis(d, [-3, -2, -1], [0, 1, 2])
|
|
346
|
-
d = d[k0:k1+1, j0:j1+1, i0:i1+1]
|
|
347
|
-
d = np.moveaxis(d, [0, 1, 2], [-3, -2, -1])
|
|
348
|
-
dataout = d
|
|
349
|
-
return dataout, [xout, yout, vout]
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def Mfac(f0: float = 1, f1: float = 1) -> np.ndarray:
|
|
353
|
-
"""2 x 2 matrix for (x,y) --> (f0 * x, f1 * y).
|
|
354
|
-
|
|
355
|
-
Args:
|
|
356
|
-
f0 (float, optional): Defaults to 1.
|
|
357
|
-
f1 (float, optional): Defaults to 1.
|
|
358
|
-
|
|
359
|
-
Returns:
|
|
360
|
-
np.ndarray: Matrix for the multiplication.
|
|
361
|
-
"""
|
|
362
|
-
return np.array([[f0, 0], [0, f1]])
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def Mrot(pa: float = 0) -> np.ndarray:
|
|
366
|
-
"""2 x 2 matrix for rotation.
|
|
367
|
-
|
|
368
|
-
Args:
|
|
369
|
-
pa (float, optional): How many degrees are the image rotated by. Defaults to 0.
|
|
370
|
-
|
|
371
|
-
Returns:
|
|
372
|
-
np.ndarray: Matrix for the rotation.
|
|
373
|
-
"""
|
|
374
|
-
p = np.radians(pa)
|
|
375
|
-
return np.array([[np.cos(p), -np.sin(p)], [np.sin(p), np.cos(p)]])
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def dot2d(M: np.ndarray = [[1, 0], [0, 1]],
|
|
379
|
-
a: np.ndarray = [0, 0]) -> np.ndarray:
|
|
380
|
-
"""To maltiply a 2 x 2 matrix to (x,y) with arrays of x and y.
|
|
381
|
-
|
|
382
|
-
Args:
|
|
383
|
-
M (np.ndarray, optional): 2 x 2 matrix. Defaults to [[1, 0], [0, 1]].
|
|
384
|
-
a (np.ndarray, optional): 2D vector (of 1D arrays). Defaults to [0].
|
|
385
|
-
|
|
386
|
-
Returns:
|
|
387
|
-
np.ndarray: The 2D vector after the matrix multiplied.
|
|
388
|
-
"""
|
|
389
|
-
x = M[0, 0] * np.array(a[0]) + M[0, 1] * np.array(a[1])
|
|
390
|
-
y = M[1, 0] * np.array(a[0]) + M[1, 1] * np.array(a[1])
|
|
391
|
-
return np.array([x, y])
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
def BnuT(T: float = 30, nu: float = 230e9) -> float:
|
|
395
|
-
"""Planck function.
|
|
396
|
-
|
|
397
|
-
Args:
|
|
398
|
-
T (float, optional): Temperature in the unit of K. Defaults to 30.
|
|
399
|
-
nu (float, optional): Frequency in the unit of Hz. Defaults to 230e9.
|
|
400
|
-
|
|
401
|
-
Returns:
|
|
402
|
-
float: Planck function in the SI units.
|
|
403
|
-
"""
|
|
404
|
-
return 2 * cu.h * nu**3 / cu.c**2 / (np.exp(cu.h * nu / cu.k_B / T) - 1)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
def JnuT(T: float = 30, nu: float = 230e9) -> float:
|
|
408
|
-
"""Brightness templerature from the Planck function.
|
|
409
|
-
|
|
410
|
-
Args:
|
|
411
|
-
T (float, optional): Temperature in the unit of K. Defaults to 30.
|
|
412
|
-
nu (float, optional): Frequency in the unit of Hz. Defaults to 230e9.
|
|
413
|
-
|
|
414
|
-
Returns:
|
|
415
|
-
float: Brightness temperature of Planck function in the unit of K.
|
|
416
|
-
"""
|
|
417
|
-
return cu.h * nu / cu.k_B / (np.exp(cu.h * nu / cu.k_B / T) - 1)
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
def gaussian2d(xy: np.ndarray,
|
|
421
|
-
amplitude: float, xo: float, yo: float,
|
|
422
|
-
fwhm_major: float, fwhm_minor: float, pa: float) -> np.ndarray:
|
|
423
|
-
"""Two dimensional Gaussian function.
|
|
424
|
-
|
|
425
|
-
Args:
|
|
426
|
-
xy (np.ndarray): A pair of (x, y).
|
|
427
|
-
amplitude (float): Peak value.
|
|
428
|
-
xo (float): Offset in the x direction.
|
|
429
|
-
yo (float): Offset in the y direction.
|
|
430
|
-
fwhm_major (float): Full width at half maximum in the major axis (but can be shorter than the minor axis).
|
|
431
|
-
fwhm_minor (float): Full width at half maximum in the minor axis (but can be longer then the major axis).
|
|
432
|
-
pa (float): Position angle of the major axis from the +y axis to the +x axis in the unit of degree.
|
|
433
|
-
|
|
434
|
-
Returns:
|
|
435
|
-
g (np.ndarray): 2D numpy array.
|
|
436
|
-
"""
|
|
437
|
-
s, t = dot2d(Mrot(-pa), [xy[1] - yo, xy[0] - xo])
|
|
438
|
-
g = amplitude * np.exp2(-4 * ((s / fwhm_major)**2 + (t / fwhm_minor)**2))
|
|
439
|
-
return g
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|