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.
Files changed (24) hide show
  1. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/PKG-INFO +1 -1
  2. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/__init__.py +1 -1
  3. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/analysis_utils.py +10 -91
  4. plotastrodata-1.4.1/plotastrodata/coord_utils.py +149 -0
  5. plotastrodata-1.4.1/plotastrodata/ext_utils.py +49 -0
  6. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/fits_utils.py +50 -2
  7. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/los_utils.py +1 -30
  8. plotastrodata-1.4.1/plotastrodata/matrix_utils.py +74 -0
  9. plotastrodata-1.4.1/plotastrodata/other_utils.py +290 -0
  10. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/plot_utils.py +2 -1
  11. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/PKG-INFO +1 -1
  12. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/SOURCES.txt +3 -0
  13. plotastrodata-1.3.2/plotastrodata/other_utils.py +0 -439
  14. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/LICENSE +0 -0
  15. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/README.md +0 -0
  16. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/const_utils.py +0 -0
  17. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/fft_utils.py +0 -0
  18. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata/fitting_utils.py +0 -0
  19. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/dependency_links.txt +0 -0
  20. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/not-zip-safe +0 -0
  21. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/requires.txt +0 -0
  22. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/plotastrodata.egg-info/top_level.txt +0 -0
  23. {plotastrodata-1.3.2 → plotastrodata-1.4.1}/setup.cfg +0 -0
  24. {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.2
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
@@ -1,4 +1,4 @@
1
1
  import warnings
2
2
 
3
3
  warnings.simplefilter('ignore', UserWarning)
4
- __version__ = '1.3.2'
4
+ __version__ = '1.4.1'
@@ -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.other_utils import (coord2xy, rel2abs, estimate_rms, trim,
8
- isdeg, Mfac, Mrot, dot2d, gaussian2d)
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.other_utils import (coord2xy, xy2coord,
6
- estimate_rms, trim, isdeg)
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.other_utils import coord2xy, xy2coord, listing, estimate_rms
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.2
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