plotastrodata 1.9.10__tar.gz → 1.9.12__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 (25) hide show
  1. {plotastrodata-1.9.10/plotastrodata.egg-info → plotastrodata-1.9.12}/PKG-INFO +1 -1
  2. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/__init__.py +1 -1
  3. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/analysis_utils.py +113 -101
  4. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/coord_utils.py +1 -1
  5. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/fft_utils.py +67 -23
  6. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/fits_utils.py +1 -1
  7. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/fitting_utils.py +99 -75
  8. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/noise_utils.py +2 -2
  9. {plotastrodata-1.9.10 → plotastrodata-1.9.12/plotastrodata.egg-info}/PKG-INFO +1 -1
  10. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/LICENSE +0 -0
  11. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/MANIFEST.in +0 -0
  12. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/README.md +0 -0
  13. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/const_utils.py +0 -0
  14. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/ext_utils.py +0 -0
  15. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/los_utils.py +0 -0
  16. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/matrix_utils.py +0 -0
  17. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/other_utils.py +0 -0
  18. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata/plot_utils.py +0 -0
  19. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata.egg-info/SOURCES.txt +0 -0
  20. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata.egg-info/dependency_links.txt +0 -0
  21. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata.egg-info/not-zip-safe +0 -0
  22. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata.egg-info/requires.txt +0 -0
  23. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/plotastrodata.egg-info/top_level.txt +0 -0
  24. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/setup.cfg +0 -0
  25. {plotastrodata-1.9.10 → plotastrodata-1.9.12}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.10
3
+ Version: 1.9.12
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', FutureWarning)
4
- __version__ = '1.9.10'
4
+ __version__ = '1.9.12'
@@ -33,18 +33,19 @@ def quadrantmean(data: np.ndarray, x: np.ndarray, y: np.ndarray,
33
33
  print('data must be 2D.')
34
34
  return
35
35
 
36
- dx, dy = x[1] - x[0], y[1] - y[0]
37
- nx = int(np.floor(max(np.abs(x[0]), np.abs(x[-1])) / dx))
38
- ny = int(np.floor(max(np.abs(y[0]), np.abs(y[-1])) / dy))
39
- xnew = np.linspace(-nx * dx, nx * dx, 2 * nx + 1)
40
- ynew = np.linspace(-ny * dy, ny * dy, 2 * ny + 1)
41
- Xnew, Ynew = np.meshgrid(xnew, ynew)
42
- if quadrants in ['13', '24']:
43
- s = 1 if quadrants == '13' else -1
44
- f = RGI((y, s * x), data, bounds_error=False, fill_value=np.nan)
45
- datanew = f((Ynew, Xnew))
46
- else:
47
- print('quadrants must be \'13\' or \'24\'.')
36
+ if quadrants not in ['13', '24']:
37
+ print("quadrants must be '13' or '24'.")
38
+ return
39
+
40
+ dx = x[1] - x[0]
41
+ dy = y[1] - y[0]
42
+ nx = int(np.ceil(np.max(np.abs(x)) / dx))
43
+ ny = int(np.ceil(np.max(np.abs(y)) / dy))
44
+ xnew = np.linspace(-nx, nx, 2 * nx + 1) * dx
45
+ ynew = np.linspace(-ny, ny, 2 * ny + 1) * dy
46
+ s = 1 if quadrants == '13' else -1
47
+ f = RGI((y, s * x), data, bounds_error=False, fill_value=np.nan)
48
+ datanew = f(np.meshgrid(ynew, xnew, indexing='ij'))
48
49
  datanew = (datanew + datanew[::-1, ::-1]) / 2.
49
50
  return datanew[ny:, nx:], xnew[nx:], ynew[ny:]
50
51
 
@@ -70,12 +71,12 @@ def filled2d(data: np.ndarray, x: np.ndarray, y: np.ndarray, n: int = 1,
70
71
 
71
72
 
72
73
  def _need_multipixels(method):
73
- def wrapper(self, *args, **kwargs):
74
- singlepixel = self.dx is None or self.dy is None
74
+ def wrapper(cls, *args, **kwargs):
75
+ singlepixel = cls.dx is None or cls.dy is None
75
76
  if singlepixel:
76
77
  print('No pixel size.')
77
78
  return
78
- return method(self, *args, **kwargs)
79
+ return method(cls, *args, **kwargs)
79
80
  return wrapper
80
81
 
81
82
 
@@ -140,57 +141,60 @@ class AstroData():
140
141
  self.beam_org = None
141
142
  self.fitsheader = None
142
143
 
143
- def binning(self, width: list[int, int, int] = [1, 1, 1]):
144
+ def _binning_one(self, t: str, width: float):
145
+ grid = getattr(self, t)
146
+ if width == 1 or grid is None:
147
+ return
148
+
149
+ dt = f'd{t}'
150
+ sep = getattr(self, dt)
151
+ if sep is None:
152
+ s = f'Skip binning in the {t}-axis because {dt} is None.'
153
+ warnings.warn(s, UserWarning)
154
+ return
155
+
156
+ i = {'v': 1, 'y': 2, 'x': 3}[t]
157
+ sizenew = self.size[i] // width
158
+ self.size[i] = sizenew
159
+ data = np.moveaxis(self.data, i, 0)
160
+ datanew = np.moveaxis(np.zeros(self.size), i, 0)
161
+ gridnew = np.zeros(sizenew)
162
+ for start in range(width):
163
+ stop = start + sizenew * width
164
+ datanew += data[start:stop:width]
165
+ gridnew += grid[start:stop:width]
166
+ self.data = np.moveaxis(datanew, 0, i) / width
167
+ setattr(self, t, gridnew / width)
168
+ setattr(self, dt, sep * width)
169
+
170
+ def binning(self, width: list[int] = [1, 1, 1]):
144
171
  """Binning up neighboring pixels in the v, y, and x domain.
145
172
 
146
173
  Args:
147
174
  width (list, optional): Number of channels, y-pixels, and x-pixels for binning. Defaults to [1, 1, 1].
148
175
  """
149
- w = [1] * (4 - len(width)) + list(width)
176
+ w = np.array([1] * (3 - len(width)) + list(width), dtype=int)
150
177
  if self.pv:
151
- w[2] = max(w[1], w[2])
152
- w[1] = 1
153
- d = to4dim(self.data)
154
- size = np.array(np.shape(d))
155
- w = np.array(w, dtype=int)
156
- if np.any(w > size):
157
- w = np.minimum(w, size)
158
- ws = ', '.join([f'{s:d}' for s in w[1:]])
178
+ w[1] = max(w[0], w[1])
179
+ w[2] = 1
180
+ self.data = to4dim(self.data)
181
+ size = np.array(np.shape(self.data))
182
+ if np.any(w > size[1:]):
183
+ w = np.minimum(w, size[1:])
184
+ ws = ', '.join([f'{s:d}' for s in w])
159
185
  print(f'width was changed to [{ws}].')
160
- newsize = size // w
161
- if (not self.pv and w[1] > 1) or (self.pv and w[2] > 1):
162
- width_v = w[2] if self.pv else w[1]
186
+ if (not self.pv and w[0] > 1) or (self.pv and w[1] > 1):
187
+ width_v = w[1] if self.pv else w[0]
163
188
  print(f'sigma has been divided by sqrt({width_v:d})'
164
189
  + ' because of binning in the v-axis.')
165
190
  self.sigma = self.sigma / np.sqrt(width_v)
166
- if (not self.pv and w[2] > 1) or w[3] > 1:
191
+ if (not self.pv and w[1] > 1) or w[2] > 1:
167
192
  print('Binning in the x- or y-axis does not update sigma.')
168
- grid = [None, self.v, self.y, self.x]
169
- dgrid = [None, self.dv, self.dy, self.dx]
170
- for n in range(1, 4):
171
- if w[n] == 1 or grid[n] is None:
172
- continue
173
- if dgrid[n] is None:
174
- t = ['v', 'y', 'x'][n - 1]
175
- s = f'Skip binning in the {t}-axis' \
176
- + f' because d{t} is None.'
177
- warnings.warn(s, UserWarning)
178
- continue
179
- size[n] = newsize[n]
180
- olddata = np.moveaxis(d, n, 0)
181
- newdata = np.moveaxis(np.zeros(size), n, 0)
182
- t = np.zeros(newsize[n])
183
- for i in range(w[n]):
184
- i_stop = i + newsize[n] * w[n]
185
- i_step = w[n]
186
- t += grid[n][i:i_stop:i_step]
187
- newdata += olddata[i:i_stop:i_step]
188
- grid[n] = t / w[n]
189
- dgrid[n] = dgrid[n] * w[n]
190
- d = np.moveaxis(newdata, 0, n) / w[n]
191
- self.data = np.squeeze(d)
192
- _, self.v, self.y, self.x = grid
193
- _, self.dv, self.dy, self.dx = dgrid
193
+ self.size = size
194
+ for t, ww in zip(['v', 'y', 'x'], w):
195
+ self._binning_one(t, ww)
196
+ self.data = np.squeeze(self.data)
197
+ del self.size
194
198
  if self.pv:
195
199
  self.v = self.y
196
200
  self.dv = self.dy
@@ -245,16 +249,14 @@ class AstroData():
245
249
  nx = len(self.x) if len(self.x) % 2 == 1 else len(self.x) - 1
246
250
  ny = len(self.y) if len(self.y) % 2 == 1 else len(self.y) - 1
247
251
  y = np.linspace(-(ny-1) / 2, (ny-1) / 2, ny) * np.abs(self.dy)
248
- g1 = np.exp(-4*np.log(2) * y**2 / (bmaj**2 - bmin**2))
252
+ g1 = np.exp(-4 * np.log(2) * y**2 / (bmaj**2 - bmin**2))
249
253
  e = np.sqrt(1 - bmin**2 / bmaj**2)
250
254
  g1 /= np.sqrt(np.pi / 4 / np.log(2) * bmin * e)
251
255
  g = np.zeros((ny, nx))
252
256
  g[:, (nx - 1) // 2] = g1
253
257
  d = self.data.copy()
254
258
  d[np.isnan(d)] = 0
255
- d = to4dim(d)
256
- d = [[convolve(c, g, mode='same') for c in cc] for cc in d]
257
- self.data = np.squeeze(d)
259
+ self.data = np.squeeze(convolve(to4dim(d), [[g]], mode='same'))
258
260
  self.rotate(bpa)
259
261
  self.beam[1] = self.beam[0]
260
262
  self.beam[2] = 0
@@ -272,8 +274,10 @@ class AstroData():
272
274
  self.data = RGIxy(self.y, self.x, self.data, yxnew, **kwargs)
273
275
  if None not in self.beam:
274
276
  bmaj, bmin, bpa = self.beam
275
- a, b = np.linalg.multi_dot([Mfac(1/bmaj, 1/bmin), Mrot(pa-bpa),
276
- Mfac(1, ci), Mrot(-pa)]).T
277
+ a, b = np.linalg.multi_dot([Mfac(1 / bmaj, 1 / bmin),
278
+ Mrot(pa - bpa),
279
+ Mfac(1, ci),
280
+ Mrot(-pa)]).T
277
281
  alpha = (np.dot(a, a) + np.dot(b, b)) / 2
278
282
  beta = np.dot(a, b)
279
283
  gamma = (np.dot(a, a) - np.dot(b, b)) / 2
@@ -406,6 +410,21 @@ class AstroData():
406
410
  if len(excludepix) == 2:
407
411
  self.data[(excludepix[0] < mask) * (mask < excludepix[1])] = np.nan
408
412
 
413
+ def _gfit_profile(self, prof: list, gaussfit: bool):
414
+ if not gaussfit:
415
+ return {}
416
+
417
+ gfitres = {}
418
+ nprof = len(prof)
419
+ res = [None] * nprof
420
+ for i in range(nprof):
421
+ res[i] = gaussfit1d(xdata=self.v, ydata=prof[i],
422
+ sigma=None, show=True,
423
+ nwalkersperdim=8)
424
+ gfitres['best'] = [a['popt'][:3] for a in res]
425
+ gfitres['error'] = [a['perr'][:3] for a in res]
426
+ return gfitres
427
+
409
428
  def profile(self, coords: list[str] = [],
410
429
  xlist: list[float] = [], ylist: list[float] = [],
411
430
  ellipse: list[float, float, float] | None = None,
@@ -430,13 +449,12 @@ class AstroData():
430
449
  print('Data must be 3D with the v, y, and x axes.')
431
450
  return
432
451
 
433
- v = self.v
434
452
  data, xf, yf = filled2d(self.data, self.x, self.y, ninterp)
435
453
  x, y = np.meshgrid(xf, yf)
436
454
  if len(coords) > 0:
437
455
  xlist, ylist = coord2xy(coords, self.center) * 3600.
438
456
  nprof = len(xlist)
439
- prof = np.empty((nprof, len(v)))
457
+ prof = np.empty((nprof, len(self.v)))
440
458
  ellipse = ellipse or [[0, 0, 0]] * nprof
441
459
  calc = np.sum if flux else np.mean
442
460
  for i, (xc, yc, e) in enumerate(zip(xlist, ylist, ellipse)):
@@ -455,16 +473,8 @@ class AstroData():
455
473
  else:
456
474
  Omega = np.pi * self.beam[0] * self.beam[1] / 4. / np.log(2.)
457
475
  prof *= np.abs(self.dx * self.dy) / Omega
458
- gfitres = {}
459
- if gaussfit:
460
- res = [None] * nprof
461
- for i in range(nprof):
462
- res[i] = gaussfit1d(xdata=v, ydata=prof[i],
463
- sigma=None, show=True,
464
- nwalkersperdim=8)
465
- gfitres['best'] = [a['popt'][:3] for a in res]
466
- gfitres['error'] = [a['perr'][:3] for a in res]
467
- return v, prof, gfitres
476
+ gfitres = self._gfit_profile(prof, gaussfit)
477
+ return self.v, prof, gfitres
468
478
 
469
479
  def rotate(self, pa: float = 0, **kwargs):
470
480
  """Counter clockwise rotation with respect to the center.
@@ -515,6 +525,31 @@ class AstroData():
515
525
  'bunit': self.bunit}
516
526
  return d
517
527
 
528
+ def _put_header(self, h: dict, t: str, crpix: int, crval: float,
529
+ cdelt: float | None = None) -> None:
530
+ fhd = self.fitsheader
531
+ axis = {'x': 1, 'y': 2, 'v': 2 if self.pv else 3}[t]
532
+ u = f'CUNIT{axis}'
533
+ indeg = fhd is None or u not in fhd or isdeg(fhd[u])
534
+ h[f'NAXIS{axis}'] = len(getattr(self, t))
535
+ h[f'CRPIX{axis}'] = int(crpix)
536
+ h[f'CRVAL{axis}'] = float(crval)
537
+ if cdelt is None:
538
+ cdelt = getattr(self, f'd{t}') / (3600 if indeg else 1)
539
+ h[f'CDELT{axis}'] = float(cdelt)
540
+
541
+ def _get_cvdv_in_freq(self, ck: int) -> tuple[float, float]:
542
+ cv = self.v[ck]
543
+ dv = self.dv
544
+ if self.restfreq is None or self.restfreq == 0:
545
+ s = 'No valid restfreq. The velocity axis is saved as is.'
546
+ warnings.warn(s, UserWarning)
547
+ return cv, dv
548
+
549
+ cv = (1 - cv / cu.c_kms) * self.restfreq
550
+ dv = -dv / cu.c_kms * self.restfreq
551
+ return cv, dv
552
+
518
553
  @_need_multipixels
519
554
  def writetofits(self, fitsimage: str = 'out.fits',
520
555
  header: dict = {}) -> None:
@@ -524,7 +559,6 @@ class AstroData():
524
559
  fitsimage (str, optional): Output FITS file name. Defaults to 'out.fits'.
525
560
  header (dict, optional): Header dictionary. Defaults to {}.
526
561
  """
527
- fhd = self.fitsheader
528
562
  h = {}
529
563
  ci = nearest_index(self.x)
530
564
  cx = 0
@@ -536,35 +570,13 @@ class AstroData():
536
570
  xy = [self.x[ci] / 3600, self.y[cj] / 3600]
537
571
  cx, cy = coord2xy(xy2coord(xy, self.center))
538
572
 
539
- def indeg(s):
540
- u = f'CUNIT{s}'
541
- return fhd is None or u not in fhd or isdeg(fhd[u])
542
-
543
- h['NAXIS1'] = len(self.x)
544
- h['CRPIX1'] = int(ci + 1)
545
- h['CRVAL1'] = float(cx)
546
- h['CDELT1'] = float(self.dx / (3600 if indeg('1') else 1))
573
+ self._put_header(h, 'x', crpix=ci + 1, crval=cx)
574
+ if not self.pv:
575
+ self._put_header(h, 'y', crpix=cj + 1, crval=cy)
547
576
  if self.dv is not None:
548
- vaxis = '2' if self.pv else '3'
549
577
  ck = nearest_index(self.v)
550
- cv = self.v[ck]
551
- dv = self.dv
552
- if self.restfreq is None or self.restfreq == 0:
553
- s = 'No valid restfreq.' \
554
- + f' Axis {vaxis} is saved as is.'
555
- warnings.warn(s, UserWarning)
556
- else:
557
- cv = (1 - cv / cu.c_kms) * self.restfreq
558
- dv = -dv / cu.c_kms * self.restfreq
559
- h[f'NAXIS{vaxis}'] = len(self.v)
560
- h[f'CRPIX{vaxis}'] = int(ck + 1)
561
- h[f'CRVAL{vaxis}'] = float(cv)
562
- h[f'CDELT{vaxis}'] = float(dv)
563
- if not self.pv:
564
- h['NAXIS2'] = len(self.y)
565
- h['CRPIX2'] = int(cj + 1)
566
- h['CRVAL2'] = float(cy)
567
- h['CDELT2'] = float(self.dy / (3600 if indeg('2') else 1))
578
+ cv, dv = self._get_cvdv_in_freq(ck)
579
+ self._put_header(h, 'v', crpix=ck + 1, crval=cv, cdelt=dv)
568
580
  if None not in self.beam:
569
581
  beam = self.beam_org if self.pv else self.beam
570
582
  h['BMAJ'] = float(beam[0] / 3600)
@@ -3,7 +3,7 @@ from astropy import units
3
3
  from astropy.coordinates import FK4, FK5, SkyCoord
4
4
 
5
5
 
6
- def _updateframe(frame: str) -> str:
6
+ def _updateframe(frame: str | None) -> str:
7
7
  """Internal function to str frame to astropy frame.
8
8
 
9
9
  Args:
@@ -11,7 +11,7 @@ def shiftphase(F: np.ndarray, u: np.ndarray,
11
11
 
12
12
  Args:
13
13
  F (np.ndarray): 1D FFT.
14
- u (np.ndarray): 1D array. The first frequency coordinate.
14
+ u (np.ndarray): 1D array. The frequency coordinate.
15
15
  xoff (float): From old to new center. Defaults to 0.
16
16
 
17
17
  Returns:
@@ -27,7 +27,7 @@ def shiftphase2(F: np.ndarray, u: np.ndarray, v: np.ndarray,
27
27
  Args:
28
28
  F (np.ndarray): 2D FFT.
29
29
  u (np.ndarray): 1D or 2D array. The first frequency coordinate.
30
- v (np.ndarray): 1D or 2D array. The second frequency coordinate. Defaults to None.
30
+ v (np.ndarray): 1D or 2D array. The second frequency coordinate.
31
31
  xoff (float): From old to new center. Defaults to 0.
32
32
  yoff (float): From old to new center. Defaults to 0.
33
33
 
@@ -39,7 +39,7 @@ def shiftphase2(F: np.ndarray, u: np.ndarray, v: np.ndarray,
39
39
 
40
40
 
41
41
  def fftcentering(f: np.ndarray, x: np.ndarray | None = None,
42
- xcenter: float = 0
42
+ xcenter: float = 0, rfft: bool = False
43
43
  ) -> tuple[np.ndarray, np.ndarray]:
44
44
  """FFT with the phase referring to a specific point.
45
45
 
@@ -47,6 +47,7 @@ def fftcentering(f: np.ndarray, x: np.ndarray | None = None,
47
47
  f (np.ndarray): 1D array for FFT.
48
48
  x (np.ndarray, optional): 1D array. The spatial coordinate. Defaults to None.
49
49
  xcenter (float, optional): x of phase reference. Defaults to 0.
50
+ rfft (bool, optional): True means using rFFT. Defaults to False.
50
51
 
51
52
  Returns:
52
53
  tuple: (F, u). F is FFT of f. u is a 1D array of the frequency coordinate.
@@ -56,15 +57,19 @@ def fftcentering(f: np.ndarray, x: np.ndarray | None = None,
56
57
  x = np.arange(nx)
57
58
  X = x[0, :] if np.ndim(x) == 2 else x
58
59
  dx = X[1] - X[0]
59
- u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
60
- F = np.fft.fftshift(np.fft.fft(f))
60
+ if rfft:
61
+ u = np.fft.rfftfreq(nx, d=dx)
62
+ F = np.fft.rfft(f)
63
+ else:
64
+ u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
65
+ F = np.fft.fftshift(np.fft.fft(f))
61
66
  F = shiftphase(F, u=u, xoff=xcenter - X[0])
62
67
  return F, u
63
68
 
64
69
 
65
70
  def fftcentering2(f: np.ndarray,
66
71
  x: np.ndarray | None = None, y: np.ndarray | None = None,
67
- xcenter: float = 0, ycenter: float = 0
72
+ xcenter: float = 0, ycenter: float = 0, rfft: bool = False
68
73
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
69
74
  """FFT with the phase referring to a specific point.
70
75
 
@@ -74,6 +79,7 @@ def fftcentering2(f: np.ndarray,
74
79
  y (np.ndarray, optional): 1D or 2D array. The second spatial coordinate. Defaults to None.
75
80
  xcenter (float, optional): x of phase reference. Defaults to 0.
76
81
  ycenter (float, optional): y of phase reference. Defaults to 0.
82
+ rfft (bool, optional): True means using rFFT. Defaults to False.
77
83
 
78
84
  Returns:
79
85
  tuple: (F, u, v). F is FFT of f. u and v are 1D arrays of the frequency coordinates.
@@ -85,39 +91,60 @@ def fftcentering2(f: np.ndarray,
85
91
  y = np.arange(ny)
86
92
  X = x[0, :] if np.ndim(x) == 2 else x
87
93
  Y = y[:, 0] if np.ndim(y) == 2 else y
88
- dx, dy = X[1] - X[0], Y[1] - Y[0]
89
- u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
90
- v = np.fft.fftshift(np.fft.fftfreq(ny, d=dy))
91
- F = np.fft.fftshift(np.fft.fft2(f))
94
+ dx = X[1] - X[0]
95
+ dy = Y[1] - Y[0]
96
+ if rfft:
97
+ u = np.fft.rfftfreq(nx, d=dx)
98
+ v = np.fft.fftshift(np.fft.fftfreq(ny, d=dy))
99
+ F = np.fft.fftshift(np.fft.rfft2(f), axes=0)
100
+ else:
101
+ u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
102
+ v = np.fft.fftshift(np.fft.fftfreq(ny, d=dy))
103
+ F = np.fft.fftshift(np.fft.fft2(f))
92
104
  F = shiftphase2(F, u, v, xcenter - X[0], ycenter - Y[0])
93
105
  return F, u, v
94
106
 
95
107
 
96
108
  def ifftcentering(F: np.ndarray, u: np.ndarray | None = None,
97
- xcenter: float = 0,
98
- x0: float = None,
99
- outreal: bool = True
109
+ xcenter: float = 0, x0: float = None,
110
+ dx: float = 1,
111
+ outreal: bool = False, rfft: bool = False
100
112
  ) -> tuple[np.ndarray, np.ndarray]:
101
113
  """inverse FFT with the phase referring to a specific point.
102
114
 
103
115
  Args:
104
- F (np.ndarray): 1D array. A result of FFT.
116
+ F (np.ndarray): 1D array. An FFT result in the frequency domain.
105
117
  u (np.ndarray, optional): 1D array. The frequency coordinate. Defaults to None.
106
118
  xcenter (float, optional): x of phase reference (used in fftcentering). Defaults to 0.
107
119
  x0 (float, optional): spatial coordinate of x[0]. Defaults to None.
108
- outreal (bool, optional): whether output only the real part. Defaults to True.
120
+ dx (float, optional): spatial interval. Defaults to 1.
121
+ outreal (bool, optional): whether output only the real part. Defaults to False.
122
+ rfft (bool, optional): True means using rFFT. Defaults to False.
109
123
 
110
124
  Returns:
111
125
  tuple: (f, x). f is iFFT of F. x is a 1D array of the spatial coordinate.
112
126
  """
113
127
  nx = np.shape(F)[0]
114
128
  if u is None:
115
- u = np.fft.fftshift(np.fft.fftfreq(nx, d=1))
129
+ if rfft:
130
+ nx = 2 * (nx - 1) # Follow numpy.fft.irfft behavior.
131
+ u = np.fft.rfftfreq(nx, d=dx)
132
+ else:
133
+ u = np.fft.fftshift(np.fft.fftfreq(nx, d=dx))
134
+ else:
135
+ if rfft:
136
+ if np.isclose(u[-1], 1 / (2 * dx)):
137
+ nx = 2 * (nx - 1)
138
+ else:
139
+ nx = 2 * (nx - 1) + 1
116
140
  x = (np.arange(nx) - (nx-1)/2.) / (u[1]-u[0]) / nx + xcenter
117
141
  if x0 is not None:
118
142
  x = x - x[0] + x0
119
143
  F = shiftphase(F, u=u, xoff=x[0] - xcenter)
120
- f = np.fft.ifft(np.fft.ifftshift(F))
144
+ if rfft:
145
+ f = np.fft.irfft(F, n=nx)
146
+ else:
147
+ f = np.fft.ifft(np.fft.ifftshift(F))
121
148
  if outreal:
122
149
  f = np.real(f)
123
150
  return f, x
@@ -127,28 +154,42 @@ def ifftcentering2(F: np.ndarray,
127
154
  u: np.ndarray | None = None, v: np.ndarray | None = None,
128
155
  xcenter: float = 0, ycenter: float = 0,
129
156
  x0: float | None = None, y0: float | None = None,
130
- outreal: bool = True
157
+ dx: float = 1, dy: float = 1,
158
+ outreal: bool = False, rfft: bool = False
131
159
  ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
132
160
  """inverse FFT with the phase referring to a specific point.
133
161
 
134
162
  Args:
135
- F (np.ndarray): 2D array. A result of FFT.
163
+ F (np.ndarray): 2D array. An FFT result in the frequency domain.
136
164
  u (np.ndarray, optional): 1D or 2D array. The first frequency coordinate. Defaults to None.
137
165
  v (np.ndarray, optional): 1D or 2D array. The second frequency cooridnate. Defaults to None.
138
166
  xcenter (float, optional): x of phase reference (used in fftcentering2). Defaults to 0.
139
167
  ycenter (float, optional): y of phase reference (used in fftcentering2). Defaults to 0.
140
168
  x0 (float, optional): spatial coordinate of x[0]. Defaults to None.
141
169
  y0 (float, optional): spatial coordinate of y[0]. Defaults to None.
142
- outreal (bool, optional): whether output only the real part. Defaults to True.
170
+ dx (float, optional): spatial interval. Defaults to 1.
171
+ dy (float, optional): spatial interval. Defaults to 1.
172
+ outreal (bool, optional): whether output only the real part. Defaults to False.
173
+ rfft (bool, optional): True means using rFFT. Defaults to False.
143
174
 
144
175
  Returns:
145
176
  tuple: (f, x, y). f is iFFT of F. x and y are 1D arrays of the spatial coordinates.
146
177
  """
147
178
  ny, nx = np.shape(F)
148
179
  if u is None:
149
- u = np.fft.fftshift(np.fft.fftfreq(nx, d=1))
180
+ if rfft:
181
+ nx = 2 * (nx - 1) # Follow numpy.fft.irfft behavior.
182
+ u = np.fft.rfftfreq(nx, d=dx)
183
+ else:
184
+ u = np.fft.fftshift(np.fft.fftfreq(nx, d=dy))
185
+ else:
186
+ if rfft:
187
+ if np.isclose(u[-1], 1 / (2 * dx)):
188
+ nx = 2 * (nx - 1)
189
+ else:
190
+ nx = 2 * (nx - 1) + 1
150
191
  if v is None:
151
- v = np.fft.fftshift(np.fft.fftfreq(ny, d=1))
192
+ v = np.fft.fftshift(np.fft.fftfreq(ny, d=dy))
152
193
  x = (np.arange(nx) - (nx-1)/2.) / (u[1]-u[0]) / nx + xcenter
153
194
  y = (np.arange(ny) - (ny-1)/2.) / (v[1]-v[0]) / ny + ycenter
154
195
  if x0 is not None:
@@ -156,7 +197,10 @@ def ifftcentering2(F: np.ndarray,
156
197
  if y0 is not None:
157
198
  y = y - y[0] + y0
158
199
  F = shiftphase2(F, u, v, x[0] - xcenter, y[0] - ycenter)
159
- f = np.fft.ifft2(np.fft.ifftshift(F))
200
+ if rfft:
201
+ f = np.fft.irfft2(np.fft.ifftshift(F, axes=0), s=(ny, nx))
202
+ else:
203
+ f = np.fft.ifft2(np.fft.ifftshift(F))
160
204
  if outreal:
161
205
  f = np.real(f)
162
206
  return f, x, y
@@ -376,7 +376,7 @@ def fits2data(fitsimage: str, Tb: bool = False, log: bool = False,
376
376
  restfreq: float | None = None, center: str | None = None,
377
377
  vsys: float = 0., pv: bool = False, **kwargs
378
378
  ) -> tuple[np.ndarray, tuple[np.ndarray, np.ndarray, np.ndarray],
379
- tuple[float, float, float], float, float]:
379
+ np.ndarray, str, float]:
380
380
  """Extract data from a fits file. kwargs are arguments of FitsData.trim().
381
381
 
382
382
  Args:
@@ -281,6 +281,29 @@ class EmceeCorner():
281
281
  ax.set_xlabel('Step')
282
282
  close_figure(fig, savefig, show)
283
283
 
284
+ def _set_results_posteriorongrid(self) -> None:
285
+ p = self.p
286
+ p1d = self.p1d
287
+ pargrid = self.pargrid
288
+ if np.all(p == 0):
289
+ print('All posterior is below pcut.')
290
+ self.popt = np.full(self.dim, np.nan)
291
+ self.plow = np.full(self.dim, np.nan)
292
+ self.pmid = np.full(self.dim, np.nan)
293
+ self.phigh = np.full(self.dim, np.nan)
294
+ else:
295
+ i_max = np.unravel_index(np.argmax(p), np.shape(p))[::-1]
296
+ self.popt = np.array([p[i] for p, i in zip(pargrid, i_max)])
297
+
298
+ def getpercentile(percent: float):
299
+ a = [np.percentile(g, percent, method='inverted_cdf', weights=p)
300
+ for g, p in zip(pargrid, p1d)]
301
+ return np.array(a)
302
+
303
+ self.plow = getpercentile(self.percent[0])
304
+ self.pmid = getpercentile(50)
305
+ self.phigh = getpercentile(self.percent[1])
306
+
284
307
  def posteriorongrid(self, ngrid: list[int] | int = 100,
285
308
  log: list[bool] | bool = False, pcut: float = 0):
286
309
  """Calculate the posterior on a grid of ngrid x ngrid x ... x ngrid.
@@ -297,8 +320,8 @@ class EmceeCorner():
297
320
  pargrid = []
298
321
  inzip = [self.bounds[:, 0], self.bounds[:, 1], ngrid, log]
299
322
  for start, stop, num, uselog in zip(*inzip):
300
- args = [start, stop, num]
301
- pargrid.append(np.geomspace(*args) if uselog else np.linspace(*args))
323
+ spacer = np.geomspace if uselog else np.linspace
324
+ pargrid.append(spacer(start, stop, num))
302
325
  p = np.exp(self.logl(np.meshgrid(*pargrid[::-1], indexing='ij')[::-1]))
303
326
  p[p < pcut] = 0
304
327
  dpar = []
@@ -309,33 +332,74 @@ class EmceeCorner():
309
332
  else:
310
333
  dpar.append(pg * 0 + pg[1] - pg[0])
311
334
  vol = np.prod(np.meshgrid(*dpar[::-1], indexing='ij')[::-1], axis=0)
312
- adim = np.arange(self.dim)
313
- axlist = [tuple(np.delete(adim, i)) for i in adim[::-1]] # adim[::-1] is becuase the 0th parameter is the innermost axis.
335
+ arrdim = np.arange(self.dim)
336
+ arrdim_r = arrdim[::-1]
337
+ # arrdim[::-1] is becuase the 0th parameter is the innermost axis.
338
+ axlist = [tuple(np.delete(arrdim, i)) for i in arrdim_r]
314
339
  p1d = [np.sum(p * vol, axis=a) / np.sum(vol, axis=a) for a in axlist]
315
340
  evidence = np.sum(p * vol) / np.sum(vol)
316
- if np.all(p == 0):
317
- print('All posterior is below pcut.')
318
- self.popt = np.full(self.dim, np.nan)
319
- self.plow = np.full(self.dim, np.nan)
320
- self.pmid = np.full(self.dim, np.nan)
321
- self.phigh = np.full(self.dim, np.nan)
322
- else:
323
- i_max = np.unravel_index(np.argmax(p), np.shape(p))[::-1]
324
- self.popt = np.array([p[i] for p, i in zip(pargrid, i_max)])
325
-
326
- def getpercentile(percent: float):
327
- a = [np.percentile(g, percent, method='inverted_cdf', weights=p)
328
- for g, p in zip(pargrid, p1d)]
329
- return np.array(a)
330
-
331
- self.plow = getpercentile(self.percent[0])
332
- self.pmid = getpercentile(50)
333
- self.phigh = getpercentile(self.percent[1])
334
341
  self.p = p
335
342
  self.p1d = p1d
336
343
  self.pargrid = pargrid
337
344
  self.vol = vol
338
345
  self.evidence = evidence
346
+ self.arrdim = arrdim
347
+ self.arrdim_r = arrdim_r
348
+ self._set_results_posteriorongrid()
349
+
350
+ def _i_eq_j(self, fig, ax, i: int, k: int) -> None:
351
+ x = self.pargrid
352
+ y = self.p1d
353
+ s0 = self.pmid[i]
354
+ s1 = self.phigh[i] - self.pmid[i]
355
+ s2 = self.pmid[i] - self.plow[i]
356
+ s0 = f'{s0:.2f}'
357
+ s1 = '^{+' + f'{s1:.2f}' + '}'
358
+ s2 = '_{-' + f'{s2:.2f}' + '}'
359
+ s0 = s0 + s1 + s2
360
+ ax[k] = fig.add_subplot(self.dim, self.dim, k + 1)
361
+ ax[k].plot(x[i], y[i], 'k-', drawstyle='steps-mid')
362
+ ax[k].axvline(self.popt[i])
363
+ ax[k].axvline(self.plow[i], linestyle='--', color='k')
364
+ ax[k].axvline(self.pmid[i], linestyle='--', color='k')
365
+ ax[k].axvline(self.phigh[i], linestyle='--', color='k')
366
+ ax[k].set_title(rf'{self.labels[i]}=${s0}$')
367
+ ax[k].set_xlim(self.cornerrange[i])
368
+ ax[k].set_ylim([0, np.max(y[i]) * 1.2])
369
+ ax[k].set_yticks([])
370
+ if i == self.dim - 1:
371
+ plt.setp(ax[k].get_xticklabels(), rotation=45)
372
+ ax[k].set_xlabel(self.labels[i])
373
+ else:
374
+ plt.setp(ax[k].get_xticklabels(), visible=False)
375
+
376
+ def _i_neq_j(self, fig, ax, i: int, j: int, k: int) -> None:
377
+ x = self.pargrid
378
+ sharex = ax[self.dim * (i - 1) + j]
379
+ sharey = ax[self.dim * i + (j - 1)] if j > 1 else None
380
+ ax[k] = fig.add_subplot(self.dim, self.dim, k + 1,
381
+ sharex=sharex, sharey=sharey)
382
+ axis = tuple(np.delete(self.arrdim_r, [i, j]))
383
+ yy = np.sum(self.p * self.vol, axis=axis) \
384
+ / np.sum(self.vol, axis=axis)
385
+ ax[k].pcolormesh(x[j], x[i], yy, cmap=self.cmap)
386
+ ax[k].contour(x[j], x[i], yy, colors='k',
387
+ levels=self.levels * np.nanmax(yy))
388
+ ax[k].plot(self.popt[j], self.popt[i], 'o')
389
+ ax[k].axvline(self.popt[j])
390
+ ax[k].axhline(self.popt[i])
391
+ ax[k].set_xlim(self.cornerrange[j])
392
+ ax[k].set_ylim(self.cornerrange[i])
393
+ if j == 0:
394
+ ax[k].set_ylabel(self.labels[i])
395
+ plt.setp(ax[k].get_yticklabels(), rotation=45)
396
+ else:
397
+ plt.setp(ax[k].get_yticklabels(), visible=False)
398
+ if i == self.dim - 1:
399
+ ax[k].set_xlabel(self.labels[j])
400
+ plt.setp(ax[k].get_xticklabels(), rotation=45)
401
+ else:
402
+ plt.setp(ax[k].get_xticklabels(), visible=False)
339
403
 
340
404
  def plotongrid(self, show: bool = False, savefig: str | None = None,
341
405
  labels: list[str] = None,
@@ -352,70 +416,30 @@ class EmceeCorner():
352
416
  cmap: (str, optional): cmap for matplotlib.pyplot.plt.pcolormesh(). Defaults to 'binary'.
353
417
  levels: (list, optional): levels for matplotlib.pyplot.plt.contour() relative to the peak. Defaults to [exp(-0.5*3^2), exp(-0.5*2^2), exp(-0.5*1^2)].
354
418
  """
355
- adim = np.arange(self.dim)
356
419
  if labels is None:
357
- labels = [f'Par {i:d}' for i in adim]
420
+ labels = [f'Par {i:d}' for i in self.arrdim]
358
421
  if cornerrange is None:
359
422
  cornerrange = self.bounds
360
- x = self.pargrid
361
- y = self.p1d
423
+ self.labels = labels
424
+ self.cornerrange = cornerrange
425
+ self.cmap = cmap
426
+ self.levels = np.array(levels)
362
427
  fig = plt.figure(figsize=(2 * self.dim * 1.2, 2 * self.dim * 1.2))
363
428
  fig.subplots_adjust(hspace=0.05, wspace=0.05, top=0.87, right=0.87)
364
429
  ax = np.empty(self.dim * self.dim, dtype='object')
365
- for i in adim:
366
- for j in adim:
430
+ for i in self.arrdim:
431
+ for j in self.arrdim:
367
432
  if i < j:
368
433
  continue
369
434
  k = self.dim * i + j
370
435
  if i == j:
371
- s0 = self.pmid[i]
372
- s1 = self.phigh[i] - self.pmid[i]
373
- s2 = self.pmid[i] - self.plow[i]
374
- s0 = f'{s0:.2f}'
375
- s1 = '^{+' + f'{s1:.2f}' + '}'
376
- s2 = '_{-' + f'{s2:.2f}' + '}'
377
- s0 = s0 + s1 + s2
378
- ax[k] = fig.add_subplot(self.dim, self.dim, k + 1)
379
- ax[k].plot(x[i], y[i], 'k-', drawstyle='steps-mid')
380
- ax[k].axvline(self.popt[i])
381
- ax[k].axvline(self.plow[i], linestyle='--', color='k')
382
- ax[k].axvline(self.pmid[i], linestyle='--', color='k')
383
- ax[k].axvline(self.phigh[i], linestyle='--', color='k')
384
- ax[k].set_title(rf'{labels[i]}=${s0}$')
385
- ax[k].set_xlim(cornerrange[i])
386
- ax[k].set_ylim([0, np.max(y[i]) * 1.2])
387
- ax[k].set_yticks([])
388
- if i == self.dim - 1:
389
- plt.setp(ax[k].get_xticklabels(), rotation=45)
390
- ax[k].set_xlabel(labels[i])
391
- else:
392
- plt.setp(ax[k].get_xticklabels(), visible=False)
436
+ self._i_eq_j(fig, ax, i, k)
393
437
  else:
394
- sharex = ax[self.dim * (i - 1) + j]
395
- sharey = ax[self.dim * i + (j - 1)] if j > 1 else None
396
- ax[k] = fig.add_subplot(self.dim, self.dim, k + 1,
397
- sharex=sharex, sharey=sharey)
398
- axis = tuple(np.delete(adim[::-1], [i, j]))
399
- yy = np.sum(self.p * self.vol, axis=axis) \
400
- / np.sum(self.vol, axis=axis)
401
- ax[k].pcolormesh(x[j], x[i], yy, cmap=cmap)
402
- ax[k].contour(x[j], x[i], yy, colors='k',
403
- levels=np.array(levels) * np.nanmax(yy))
404
- ax[k].plot(self.popt[j], self.popt[i], 'o')
405
- ax[k].axvline(self.popt[j])
406
- ax[k].axhline(self.popt[i])
407
- ax[k].set_xlim(cornerrange[j])
408
- ax[k].set_ylim(cornerrange[i])
409
- if j == 0:
410
- ax[k].set_ylabel(labels[i])
411
- plt.setp(ax[k].get_yticklabels(), rotation=45)
412
- else:
413
- plt.setp(ax[k].get_yticklabels(), visible=False)
414
- if i == self.dim - 1:
415
- ax[k].set_xlabel(labels[j])
416
- plt.setp(ax[k].get_xticklabels(), rotation=45)
417
- else:
418
- plt.setp(ax[k].get_xticklabels(), visible=False)
438
+ self._i_neq_j(fig, ax, i, j, k)
439
+ del self.labels
440
+ del self.cornerrange
441
+ del self.cmap
442
+ del self.levels
419
443
  close_figure(fig, savefig, show, tight=False)
420
444
 
421
445
  def getDNSevidence(self, **kwargs):
@@ -74,7 +74,7 @@ def select_noise(data: np.ndarray, sigma: str) -> np.ndarray:
74
74
  Returns:
75
75
  np.ndarray: 1D array that includes only the selected pixels.
76
76
  """
77
- n = np.array(data) * 1
77
+ n = np.array(data).copy()
78
78
  if 'edge' in sigma:
79
79
  if np.ndim(n) <= 2:
80
80
  print('\'edge\' is ignored because ndim <= 2.')
@@ -215,6 +215,6 @@ def estimate_rms(data: np.ndarray,
215
215
  ave = n.m0
216
216
  noise = n.s0
217
217
  if np.abs(ave) > 0.2 * noise:
218
- s = 'Mean > 0.2 x standard deviation.'
218
+ s = '|mean| > 0.2sigma.'
219
219
  warnings.warn(s, UserWarning)
220
220
  return noise
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.10
3
+ Version: 1.9.12
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
File without changes
File without changes
File without changes
File without changes