plotastrodata 1.9.7__tar.gz → 1.9.9__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.7/plotastrodata.egg-info → plotastrodata-1.9.9}/PKG-INFO +1 -1
  2. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/__init__.py +1 -1
  3. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/analysis_utils.py +10 -12
  4. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/fits_utils.py +144 -139
  5. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/plot_utils.py +127 -108
  6. {plotastrodata-1.9.7 → plotastrodata-1.9.9/plotastrodata.egg-info}/PKG-INFO +1 -1
  7. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/LICENSE +0 -0
  8. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/MANIFEST.in +0 -0
  9. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/README.md +0 -0
  10. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/const_utils.py +0 -0
  11. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/coord_utils.py +0 -0
  12. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/ext_utils.py +0 -0
  13. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/fft_utils.py +0 -0
  14. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/fitting_utils.py +0 -0
  15. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/los_utils.py +0 -0
  16. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/matrix_utils.py +0 -0
  17. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/noise_utils.py +0 -0
  18. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata/other_utils.py +0 -0
  19. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata.egg-info/SOURCES.txt +0 -0
  20. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata.egg-info/dependency_links.txt +0 -0
  21. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata.egg-info/not-zip-safe +0 -0
  22. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata.egg-info/requires.txt +0 -0
  23. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/plotastrodata.egg-info/top_level.txt +0 -0
  24. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/setup.cfg +0 -0
  25. {plotastrodata-1.9.7 → plotastrodata-1.9.9}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.7
3
+ Version: 1.9.9
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.7'
4
+ __version__ = '1.9.9'
@@ -688,12 +688,12 @@ class AstroFrame():
688
688
 
689
689
  def _get_restfreq(self, header: dict):
690
690
  """Extract rest frequency from FITS header."""
691
- if "NAXIS3" in header and header["NAXIS3"] == 1 and not self.pv:
692
- return header["CRVAL3"]
693
691
  if "RESTFRQ" in header:
694
692
  return header["RESTFRQ"]
695
693
  if "RESTFREQ" in header:
696
694
  return header["RESTFREQ"]
695
+ if "NAXIS3" in header and header["NAXIS3"] == 1 and not self.pv:
696
+ return header["CRVAL3"]
697
697
  return None
698
698
 
699
699
  def _read_fitsimage(self, d: AstroData, i: int, grid: list) -> list:
@@ -718,20 +718,16 @@ class AstroFrame():
718
718
  d.bunit[i] = fd.get_header("BUNIT")
719
719
  return grid
720
720
 
721
- def _no_newcenter(self, center: str | None):
722
- return (self.pv
723
- or self.center is None
724
- or center is None
725
- or center == self.center)
726
-
727
721
  def _shift_center(self, d: AstroData, i: int, grid: list) -> list:
728
- if self._no_newcenter(d.center[i]):
722
+ corg = d.center[i]
723
+ cnew = self.center
724
+ if self.pv or cnew is None or corg is None or corg == cnew:
729
725
  return grid
730
726
 
731
- cx, cy = coord2xy(d.center[i], self.center) * 3600
727
+ cx, cy = coord2xy(corg, cnew) * 3600
732
728
  grid[0] = grid[0] + cx # Don't use += cx.
733
729
  grid[1] = grid[1] + cy # Don't use += cy.
734
- d.center[i] = self.center
730
+ d.center[i] = cnew
735
731
  return grid
736
732
 
737
733
  def _ascending_v(self, d: AstroData, i: int, v: np.ndarray | None):
@@ -756,9 +752,11 @@ class AstroFrame():
756
752
  self._ascending_v(d, i, v=grid[2])
757
753
  grid = [grid[0], d.v] if self.pv else [grid[0], grid[1]]
758
754
  if self.swapxy:
759
- grid = [grid[1], grid[0]]
755
+ grid.reverse()
760
756
  d.data[i] = np.moveaxis(d.data[i], 1, 0)
761
757
  self._xyskip(d, i, x=grid[0], y=grid[1])
758
+ if self.pv:
759
+ d.v = d.y
762
760
  for axis in ['x', 'y', 'v']:
763
761
  setattr(d, f"d{axis}", _get_gridsep(getattr(d, axis)))
764
762
 
@@ -177,6 +177,137 @@ class FitsData:
177
177
  self.gen_data(**kwargs)
178
178
  return self.data
179
179
 
180
+ def _read_cd(self):
181
+ h = self.header
182
+ cdij = ['CD1_1', 'CD1_2', 'CD2_1', 'CD2_2']
183
+ if not np.all([k in list(h.keys()) for k in cdij]):
184
+ self.wcsrot = False
185
+ self.Mcd = None
186
+ return
187
+
188
+ self.wcsrot = True
189
+ cd11, cd12, cd21, cd22 = [h[k] for k in cdij]
190
+ self.Mcd = [[cd11, cd12], [cd21, cd22]]
191
+ if cd21 == 0:
192
+ rho_a = 0
193
+ else:
194
+ rho_a = np.arctan2(np.abs(cd21), np.sign(cd21) * cd11)
195
+ if cd12 == 0:
196
+ rho_b = 0
197
+ else:
198
+ rho_b = np.arctan2(np.abs(cd12), -np.sign(cd12) * cd22)
199
+ if (drho := np.abs(np.degrees(rho_a - rho_b))) > 1.0:
200
+ print('Angles from (CD21, CD11) and (CD12, CD22)'
201
+ + f' are different by {drho:.2} degrees.')
202
+ crota2 = (rho_a + rho_b) / 2.
203
+ sin_rho = np.sin(crota2)
204
+ cos_rho = np.cos(crota2)
205
+ cdelt1 = cd11 * cos_rho + cd21 * sin_rho
206
+ cdelt2 = -cd12 * sin_rho + cd22 * cos_rho
207
+ crota2 = np.degrees(crota2)
208
+ h['CDELT1'] = cdelt1
209
+ h['CDELT2'] = cdelt2
210
+ for k in cdij:
211
+ del h[k]
212
+ print(f'WCS rotation was found (CROTA2 = {crota2:f} deg).')
213
+
214
+ def _rotate_cd(self):
215
+ h = self.header
216
+ data = self.get_data()
217
+ ic = len(self.x) // 2
218
+ jc = len(self.y) // 2
219
+ h['CRPIX1'] = ic + 1
220
+ h['CRPIX2'] = jc + 1
221
+ xc = self.x[ic] / self.dx
222
+ yc = self.y[jc] / self.dy
223
+ xc, yc = dot2d(self.Mcd, [xc, yc])
224
+ newcenter = xy2coord([xc, yc], coordorg=self.get_center())
225
+ xc, yc = coord2xy(coords=newcenter)
226
+ h['CRVAL1'] = xc
227
+ h['CRVAL2'] = yc
228
+ self.x = self.x - self.x[ic]
229
+ self.y = self.y - self.y[jc]
230
+ x = self.x / (3600 if isdeg(h['CUNIT1']) else 1)
231
+ y = self.y / (3600 if isdeg(h['CUNIT2']) else 1)
232
+ X, Y = np.meshgrid(x, y)
233
+ Mcdinv = np.linalg.inv(self.Mcd)
234
+ xnew, ynew = dot2d(Mcdinv, [X, Y])
235
+ datanew = RGIxy(self.y / self.dy, self.x / self.dx,
236
+ data, (ynew, xnew))
237
+ self.data = datanew
238
+ print('Data values were interpolated for WCS rotation.')
239
+
240
+ def _get_genx_geny(self, center: str, dist: float):
241
+ h = self.header
242
+ cxy = (0, 0)
243
+ if center is not None and not self.wcsrot:
244
+ coordorg = xy2coord([h["CRVAL1"], h["CRVAL2"]])
245
+ if (radesys := h.get("RADESYS")) is not None:
246
+ coordorg = f"{radesys} {coordorg}"
247
+ cxy = coord2xy(center, coordorg)
248
+ slabel = ["x", "y"]
249
+
250
+ def wrapper(i: int):
251
+ def gen_s(s_in: np.ndarray | None) -> None:
252
+ if h.get(f"NAXIS{i+1}") is None or s_in is None:
253
+ s, ds = None, None
254
+ else:
255
+ s = (s_in - cxy[i]) * dist
256
+ if isdeg(h[f"CUNIT{i+1}"]):
257
+ s *= 3600.
258
+ ds = None if len(s) == 0 else s[1] - s[0]
259
+ setattr(self, f"{slabel[i]}", s)
260
+ setattr(self, f"d{slabel[i]}", ds)
261
+ return gen_s
262
+
263
+ return wrapper(0), wrapper(1)
264
+
265
+ def _get_genv(self, restfreq: float | None, vsys: float, pv: bool):
266
+ h = self.header
267
+
268
+ def gen_v(v_in: np.ndarray) -> None:
269
+ vaxis = "2" if pv else "3"
270
+ if h.get(f"NAXIS{vaxis}") is None or v_in is None:
271
+ self.v, self.dv = None, None
272
+ return
273
+
274
+ if restfreq is None:
275
+ freq = np.mean(v_in)
276
+ print("restfreq is assumed to be the center.")
277
+ else:
278
+ freq = restfreq
279
+ v = v_in + h[f"CRVAL{vaxis}"]
280
+ key = f'CUNIT{vaxis}'
281
+ cunitv = h[key].strip()
282
+ match cunitv:
283
+ case "Hz" | "HZ":
284
+ if freq == 0:
285
+ print("v is read as is, because restfreq=0.")
286
+ else:
287
+ v = (1 - v / freq) * cu.c_kms - vsys
288
+ case "m/s" | "M/S":
289
+ print(f'{key}={cunitv} found.')
290
+ v = v * 1e-3 - vsys
291
+ case 'km/s' | "KM/S":
292
+ print(f'{key}={cunitv} found.')
293
+ v = v - vsys
294
+ case _:
295
+ print(f'Unknown CUNIT3 {cunitv} found.'
296
+ + ' v is read as is.')
297
+ dv = None if len(v) == 0 else v[1] - v[0]
298
+ self.v, self.dv = v, dv
299
+
300
+ return gen_v
301
+
302
+ def _get_array(self, i: int) -> np.ndarray:
303
+ h = self.header
304
+ n = h.get(f"NAXIS{i:d}")
305
+ if n is None:
306
+ return None
307
+
308
+ s = (np.arange(n) - h[f"CRPIX{i:d}"] + 1) * h[f"CDELT{i:d}"]
309
+ return s
310
+
180
311
  def gen_grid(self, center: str | None = None, dist: float = 1.,
181
312
  restfreq: float | None = None, vsys: float = 0.,
182
313
  pv: bool = False) -> None:
@@ -191,146 +322,20 @@ class FitsData:
191
322
  """
192
323
  h = self.get_header()
193
324
  # WCS rotation (Calabretta & Greisen 2002, Astronomy & Astrophysics, 395, 1077)
194
- self.wcsrot = False
195
- cdij = ['CD1_1', 'CD1_2', 'CD2_1', 'CD2_2']
196
- if np.all([s in list(h.keys()) for s in cdij]):
197
- self.wcsrot = True
198
- cd11, cd12, cd21, cd22 = [h[s] for s in cdij]
199
- if cd21 == 0:
200
- rho_a = 0
201
- else:
202
- rho_a = np.arctan2(np.abs(cd21), np.sign(cd21) * cd11)
203
- if cd12 == 0:
204
- rho_b = 0
205
- else:
206
- rho_b = np.arctan2(np.abs(cd12), -np.sign(cd12) * cd22)
207
- if (drho := np.abs(np.degrees(rho_a - rho_b))) > 1.0:
208
- print('Angles from (CD21, CD11) and (CD12, CD22)'
209
- + f' are different by {drho:.2} degrees.')
210
- crota2 = (rho_a + rho_b) / 2.
211
- sin_rho = np.sin(crota2)
212
- cos_rho = np.cos(crota2)
213
- cdelt1 = cd11 * cos_rho + cd21 * sin_rho
214
- cdelt2 = -cd12 * sin_rho + cd22 * cos_rho
215
- crota2 = np.degrees(crota2)
216
- h['CDELT1'] = cdelt1
217
- h['CDELT2'] = cdelt2
218
- del h['CD1_1']
219
- del h['CD1_2']
220
- del h['CD2_1']
221
- del h['CD2_2']
222
- print(f'WCS rotation was found (CROTA2 = {crota2:f} deg).')
223
- # spatial center
224
- if center is None or self.wcsrot:
225
- cx, cy = 0, 0
325
+ self._read_cd()
326
+ gen_x, gen_y = self._get_genx_geny(center, dist)
327
+ restfreq = restfreq or h.get('RESTFRQ') or h.get('RESTFREQ')
328
+ gen_v = self._get_genv(restfreq, vsys, pv)
329
+ if pv:
330
+ gen_x(self._get_array(1))
331
+ gen_v(self._get_array(2))
332
+ self.y, self.dy = None, None
226
333
  else:
227
- c0 = xy2coord([h['CRVAL1'], h['CRVAL2']])
228
- if 'RADESYS' in h:
229
- radesys = h['RADESYS']
230
- c0 = f'{radesys} {c0}'
231
- cx, cy = coord2xy(center, c0)
232
- # rest frequency
233
- if restfreq is None:
234
- if 'RESTFRQ' in h:
235
- restfreq = h['RESTFRQ']
236
- if 'RESTFREQ' in h:
237
- restfreq = h['RESTFREQ']
238
- self.x, self.y, self.v = None, None, None
239
- self.dx, self.dy, self.dv = None, None, None
240
-
241
- def get_list(i: int, crval=False) -> np.ndarray:
242
- s = np.arange(h[f'NAXIS{i:d}'])
243
- s = (s - h[f'CRPIX{i:d}'] + 1) * h[f'CDELT{i:d}']
244
- if crval:
245
- s = s + h[f'CRVAL{i:d}']
246
- return s
247
-
248
- def gen_x(s_in: np.ndarray) -> None:
249
- s = (s_in - cx) * dist
250
- if isdeg(h['CUNIT1']):
251
- s *= 3600.
252
- self.x, self.dx = s, s[1] - s[0]
253
-
254
- def gen_y(s_in: np.ndarray) -> None:
255
- s = (s_in - cy) * dist
256
- if isdeg(h['CUNIT2']):
257
- s *= 3600.
258
- self.y, self.dy = s, s[1] - s[0]
259
-
260
- def gen_v(s_in: np.ndarray) -> None:
261
- if restfreq is None:
262
- freq = np.mean(s_in)
263
- print('restfreq is assumed to be the center.')
264
- else:
265
- freq = restfreq
266
-
267
- vaxis = '2' if pv else '3'
268
- key = f'CUNIT{vaxis}'
269
- cunitv = h[key]
270
- match cunitv.strip():
271
- case 'Hz':
272
- if freq == 0:
273
- print('v is frequency because restfreq=0.')
274
- s = s_in * 1
275
- else:
276
- s = (1 - s_in / freq) * cu.c_kms - vsys
277
- case 'HZ':
278
- if freq == 0:
279
- print('v is frequency because restfreq=0.')
280
- s = s_in * 1
281
- else:
282
- s = (1 - s_in / freq) * cu.c_kms - vsys
283
- case 'm/s':
284
- print(f'{key}=\'m/s\' found.')
285
- s = s_in * 1e-3 - vsys
286
- case 'M/S':
287
- print(f'{key}=\'M/S\' found.')
288
- s = s_in * 1e-3 - vsys
289
- case 'km/s':
290
- print(f'{key}=\'km/s\' found.')
291
- s = s_in - vsys
292
- case 'KM/S':
293
- print(f'{key}=\'KM/S\' found.')
294
- s = s_in - vsys
295
- case _:
296
- print(f'Unknown CUNIT3 {cunitv} found.'
297
- + ' v is read as is.')
298
- s = s_in - vsys
299
-
300
- self.v, self.dv = s, s[1] - s[0]
301
-
302
- if h['NAXIS'] > 0 and h['NAXIS1'] > 1:
303
- gen_x(get_list(1))
304
- if h['NAXIS'] > 1 and h['NAXIS2'] > 1:
305
- gen_v(get_list(2, True)) if pv else gen_y(get_list(2))
306
- if h['NAXIS'] > 2 and h['NAXIS3'] > 1:
307
- gen_v(get_list(3, True))
308
-
334
+ gen_x(self._get_array(1))
335
+ gen_y(self._get_array(2))
336
+ gen_v(self._get_array(3))
309
337
  if self.wcsrot:
310
- data = self.get_data()
311
- self.header['CRPIX1'] = ic = len(self.x) // 2
312
- self.header['CRPIX2'] = jc = len(self.y) // 2
313
- xc = self.x[ic] / self.dx
314
- yc = self.y[jc] / self.dy
315
- Mcd = [[cd11, cd12], [cd21, cd22]]
316
- xc, yc = dot2d(Mcd, [xc, yc])
317
- newcenter = xy2coord(xy=[xc, yc],
318
- coordorg=self.get_center())
319
- xc, yc = coord2xy(coords=newcenter,
320
- coordorg='00h00m00s 00d00m00s')
321
- self.header['CRVAL1'] = xc
322
- self.header['CRVAL2'] = yc
323
- self.x = self.x - self.x[ic]
324
- self.y = self.y - self.y[jc]
325
- x = self.x / (3600 if isdeg(h['CUNIT1']) else 1)
326
- y = self.y / (3600 if isdeg(h['CUNIT2']) else 1)
327
- X, Y = np.meshgrid(x, y)
328
- cdinv = np.linalg.inv([[cd11, cd12], [cd21, cd22]])
329
- xnew, ynew = dot2d(cdinv, [X, Y])
330
- datanew = RGIxy(self.y / self.dy, self.x / self.dx,
331
- data, (ynew, xnew))
332
- self.data = datanew
333
- print('Data values were interpolated for WCS rotation.')
338
+ self._rotate_cd()
334
339
 
335
340
  def get_grid(self, **kwargs) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
336
341
  """Output the grids, [x, y, v]. This method can take the arguments of gen_grid().
@@ -338,7 +343,7 @@ class FitsData:
338
343
  Returns:
339
344
  tuple: (x, y, v).
340
345
  """
341
- if not hasattr(self, 'x') or not hasattr(self, 'y'):
346
+ if not np.all([hasattr(self, s) for s in ["x", "y", "v"]]):
342
347
  self.gen_grid(**kwargs)
343
348
  return self.x, self.y, self.v
344
349
 
@@ -8,6 +8,7 @@ from typing import TypeVar
8
8
  from plotastrodata.analysis_utils import AstroData, AstroFrame
9
9
  from plotastrodata.coord_utils import (coord2xy, xy2coord,
10
10
  get_hmdm, get_min, get_sec)
11
+ from plotastrodata.fitting_utils import gaussian1d
11
12
  from plotastrodata.noise_utils import estimate_rms
12
13
  from plotastrodata.other_utils import (close_figure, listing,
13
14
  reform_grid, reform_data)
@@ -149,7 +150,7 @@ def _get_gridwidth(mode: str, rmax: float, cos_dec: float
149
150
  def _get_v(p, v: np.ndarray | None = None,
150
151
  restfreq: float | None = None,
151
152
  vskip: int = 1) -> np.ndarray:
152
- if p.fitsimage is not None:
153
+ if p.fitsimage is not None and v is None:
153
154
  p.read(d := AstroData(fitsimage=p.fitsimage,
154
155
  restfreq=restfreq, sigma=None))
155
156
  v = d.v
@@ -1199,6 +1200,55 @@ class PlotAstroData(AstroFrame):
1199
1200
  return fig, self.ax[0]
1200
1201
 
1201
1202
 
1203
+ def _get_ylabel_profile(_kw: dict, Tb: bool, flux: bool, bunit: str
1204
+ ) -> str:
1205
+ if "ylabel" in _kw:
1206
+ return _kw["ylabel"]
1207
+ if Tb:
1208
+ return r"$T_b$ (K)"
1209
+ if flux:
1210
+ return "Flux (Jy)"
1211
+ return bunit
1212
+
1213
+
1214
+ def _prep_plotprofile(width: int, coords: list | str,
1215
+ xlist: list, ylist: list, ellipse: list,
1216
+ ninterp: int, flux: bool, gaussfit: bool,
1217
+ _kw: dict) -> tuple:
1218
+ if isinstance(coords, str):
1219
+ coords = [coords]
1220
+ Tb = _kw.get("Tb", False)
1221
+ f = kwargs2instance(AstroFrame, _kw)
1222
+ d = kwargs2instance(AstroData, _kw)
1223
+ f.read(d)
1224
+ d.binning([width, 1, 1])
1225
+ v, prof, gfitres = d.profile(coords=coords, xlist=xlist, ylist=ylist,
1226
+ ellipse=ellipse, ninterp=ninterp,
1227
+ flux=flux, gaussfit=gaussfit)
1228
+ ylabel = _get_ylabel_profile(_kw, Tb, flux, d.bunit)
1229
+ if isinstance(ylabel, str):
1230
+ ylabel = [ylabel] * len(prof)
1231
+ _kw.setdefault("xlim", [v.min(), v.max()])
1232
+ pa2 = kwargs2instance(PlotAxes2D, _kw)
1233
+ return v, prof, gfitres, pa2, ylabel
1234
+
1235
+
1236
+ def _set_figax_plotprofile(fig, ax, nrows: int, ncols: int,
1237
+ nprof: int) -> tuple:
1238
+ if ncols == 1:
1239
+ nrows = nprof
1240
+ if fig is None:
1241
+ fig = plt.figure(figsize=(6 * ncols, 3 * nrows))
1242
+ if nprof > 1 and ax is not None:
1243
+ print("External ax is supported only when len(coords)=1.")
1244
+ ax = None
1245
+ ax = np.empty(nprof, dtype=object) if ax is None else [ax]
1246
+ for i in range(nprof):
1247
+ sharex = None if i < nrows - 1 else ax[i - 1]
1248
+ ax[i] = fig.add_subplot(nrows, ncols, i + 1, sharex=sharex)
1249
+ return fig, ax
1250
+
1251
+
1202
1252
  def plotprofile(coords: list[str] | str = [],
1203
1253
  xlist: list[float] = [], ylist: list[float] = [],
1204
1254
  ellipse: list[float, float, float] | None = None,
@@ -1208,11 +1258,9 @@ def plotprofile(coords: list[str] | str = [],
1208
1258
  title: list[str] | None = None,
1209
1259
  text: list[str] | None = None,
1210
1260
  nrows: int = 0, ncols: int = 1,
1211
- fig: object | None = None,
1212
- ax: object | None = None,
1261
+ fig: object | None = None, ax: object | None = None,
1213
1262
  getfigax: bool = False,
1214
- savefig: dict | str | None = None,
1215
- show: bool = False,
1263
+ savefig: dict | str | None = None, show: bool = False,
1216
1264
  **kwargs) -> tuple[object, object]:
1217
1265
  """Use Axes.plot of matplotlib to plot line profiles at given coordinates. kwargs must include the arguments of AstroData to specify the data to be plotted. kwargs must include the arguments of AstroFrame to specify the ranges and so on for plotting. kwargs can include the arguments of PlotAxes2D to adjust x and y axes.
1218
1266
 
@@ -1239,63 +1287,29 @@ def plotprofile(coords: list[str] | str = [],
1239
1287
  Returns:
1240
1288
  tuple: (fig, ax), where ax is a list, if getfigax=True. Otherwise, no return.
1241
1289
  """
1242
- _kw = {'drawstyle': 'steps-mid', 'color': 'k'}
1290
+ _kw = {"drawstyle": "steps-mid", "color": "k",
1291
+ "xlabel": r"Velocity (km s$^{-1}$)", "samexy": False}
1243
1292
  _kw.update(kwargs)
1244
1293
  _kwgauss = {'drawstyle': 'default', 'color': 'g'}
1245
1294
  _kwgauss.update(gauss_kwargs)
1246
- if type(coords) is str:
1247
- coords = [coords]
1248
- f = kwargs2instance(AstroFrame, _kw)
1249
- d = kwargs2instance(AstroData, _kw)
1250
- f.read(d)
1251
- d.binning([width, 1, 1])
1252
- v, prof, gfitres = d.profile(coords=coords, xlist=xlist, ylist=ylist,
1253
- ellipse=ellipse, ninterp=ninterp,
1254
- flux=flux, gaussfit=gaussfit)
1295
+ v, prof, gfitres, pa2, ylabel \
1296
+ = _prep_plotprofile(width, coords, xlist, ylist, ellipse,
1297
+ ninterp, flux, gaussfit, _kw)
1255
1298
  nprof = len(prof)
1256
- if 'ylabel' in _kw:
1257
- ylabel = _kw['ylabel']
1258
- elif d.Tb:
1259
- ylabel = r'$T_b$ (K)'
1260
- elif flux:
1261
- ylabel = 'Flux (Jy)'
1262
- else:
1263
- ylabel = d.bunit
1264
- if type(ylabel) is str:
1265
- ylabel = [ylabel] * nprof
1266
-
1267
- def gauss(x, p, c, w):
1268
- return p * np.exp(-4. * np.log(2.) * ((x - c) / w)**2)
1269
-
1270
- set_rcparams(20, 'w')
1271
- if ncols == 1:
1272
- nrows = nprof
1273
- if fig is None:
1274
- fig = plt.figure(figsize=(6 * ncols, 3 * nrows))
1275
- if nprof > 1 and ax is not None:
1276
- print('External ax is supported only when len(coords)=1.')
1277
- ax = None
1278
- ax = np.empty(nprof, dtype='object') if ax is None else [ax]
1279
- if 'xlabel' not in _kw:
1280
- _kw['xlabel'] = 'Velocity (km s$^{-1}$)'
1281
- if 'xlim' not in _kw:
1282
- _kw['xlim'] = [v.min(), v.max()]
1283
- _kw['samexy'] = False
1284
- pa2 = kwargs2instance(PlotAxes2D, _kw)
1299
+ set_rcparams(20, "w")
1300
+ fig, ax = _set_figax_plotprofile(fig, ax, nrows, ncols, nprof)
1285
1301
  for i in range(nprof):
1286
- sharex = None if i < nrows - 1 else ax[i - 1]
1287
- ax[i] = fig.add_subplot(nrows, ncols, i + 1, sharex=sharex)
1288
1302
  if gaussfit:
1289
- ax[i].plot(v, gauss(v, *gfitres['best'][i]), **_kwgauss)
1303
+ ax[i].plot(v, gaussian1d(v, *gfitres["best"][i]), **_kwgauss)
1290
1304
  ax[i].plot(v, prof[i], **_kw)
1291
- ax[i].hlines([0], v.min(), v.max(), linestyle='dashed', color='k')
1305
+ ax[i].hlines([0], v.min(), v.max(), linestyle="dashed", color="k")
1292
1306
  ax[i].set_ylabel(ylabel[i])
1293
1307
  pa2.set_xyaxes(ax[i])
1294
1308
  if text is not None:
1295
1309
  ax[i].text(**text[i])
1296
1310
  if title is not None:
1297
- if type(title[i]) is str:
1298
- title[i] = {'label': title[i]}
1311
+ if isinstance(title[i], str):
1312
+ title[i] = {"label": title[i]}
1299
1313
  ax[i].set_title(**title[i])
1300
1314
  if i <= nprof - ncols - 1:
1301
1315
  plt.setp(ax[i].get_xticklabels(), visible=False)
@@ -1369,8 +1383,69 @@ def plotslice(length: float, dx: float | None = None, pa: float = 0,
1369
1383
  close_figure(fig, savefig, show)
1370
1384
 
1371
1385
 
1386
+ def _plot_on_wall(d: AstroData, x: np.ndarray, y: np.ndarray, v: np.ndarray,
1387
+ measure: object, datalist: list,
1388
+ sign: int, axis: int, **kwargs):
1389
+ dx, dy, dv = x[1] - x[0], y[1] - y[0], v[1] - v[0]
1390
+ s, ds = [x, y, v], [dx, dy, dv]
1391
+ if kwargs == {}:
1392
+ return
1393
+
1394
+ match axis:
1395
+ case 2:
1396
+ shape = np.shape(d.data[:, :, 0])
1397
+ case 1:
1398
+ shape = np.shape(d.data[:, 0, :])
1399
+ case 0:
1400
+ shape = np.shape(d.data[0, :, :])
1401
+ if np.shape(kwargs['data']) != shape:
1402
+ print('The shape of the 2D data is inconsistent'
1403
+ + ' with the shape of the 3D data.')
1404
+ return
1405
+
1406
+ _kw = {'levels': [3, 6, 12, 24, 48, 96, 192, 384],
1407
+ 'sigma': 'hist', 'cmap': 'Jet', 'alpha': 0.3}
1408
+ _kw.update(kwargs)
1409
+ volume = _kw['data']
1410
+ levels = _kw['levels']
1411
+ cmap = _kw['cmap']
1412
+ alpha = _kw['alpha']
1413
+ sigma = estimate_rms(data=volume, sigma=_kw['sigma'])
1414
+ volume[np.isnan(volume)] = 0
1415
+ a = int(sign == -1)
1416
+ b = int(sign == 1)
1417
+ volume = np.moveaxis([volume * a, volume * b], 0, axis)
1418
+ if d.dx < 0:
1419
+ volume = volume[:, :, ::-1]
1420
+ if d.dy < 0:
1421
+ volume = volume[:, ::-1, :]
1422
+ if d.dv < 0:
1423
+ volume = volume[::-1, :, :]
1424
+ for lev in levels:
1425
+ if lev * sigma > np.max(volume):
1426
+ continue
1427
+ vertices, simplices, _, _ = measure.marching_cubes(volume, lev * sigma)
1428
+ Xg, Yg, Zg = [t[0] + i * dt for t, i, dt
1429
+ in zip(s, vertices.T[::-1], ds)]
1430
+ match axis:
1431
+ case 2:
1432
+ Xg = Xg * 0 + (x[-1] if sign == 1 else x[0])
1433
+ case 1:
1434
+ Yg = Yg * 0 + (y[-1] if sign == 1 else y[0])
1435
+ case 0:
1436
+ Zg = Zg * 0 + (v[-1] if sign == 1 else v[0])
1437
+ i, j, k = simplices.T
1438
+ mesh2d = dict(type='mesh3d', x=Xg, y=Yg, z=Zg,
1439
+ i=i, j=j, k=k,
1440
+ intensity=Zg * 0 + lev,
1441
+ colorscale=cmap, reversescale=False,
1442
+ cmin=np.min(levels), cmax=np.max(levels),
1443
+ opacity=alpha, name='', showscale=False)
1444
+ datalist.append(mesh2d)
1445
+
1446
+
1372
1447
  def plot3d(levels: list[float] = [3, 6, 12],
1373
- cmap: str = 'Jet', alpha: float = 0.08,
1448
+ cmap: str = 'jet', alpha: float = 0.08,
1374
1449
  xlabel: str = 'R.A. (arcsec)',
1375
1450
  ylabel: str = 'Dec. (arcsec)',
1376
1451
  vlabel: str = 'Velocity (km/s)',
@@ -1461,67 +1536,11 @@ def plot3d(levels: list[float] = [3, 6, 12],
1461
1536
  name='', line=dict(color='rgb(0,0,0)', width=1))
1462
1537
  data.append(lines)
1463
1538
 
1464
- def plot_on_wall(sign: int, axis: int, **kwargs):
1465
- if kwargs == {}:
1466
- return
1467
-
1468
- match axis:
1469
- case 2:
1470
- shape = np.shape(d.data[:, :, 0])
1471
- case 1:
1472
- shape = np.shape(d.data[:, 0, :])
1473
- case 0:
1474
- shape = np.shape(d.data[0, :, :])
1475
- if np.shape(kwargs['data']) != shape:
1476
- print('The shape of the 2D data is inconsistent'
1477
- + ' with the shape of the 3D data.')
1478
- return
1479
-
1480
- _kw = {'levels': [3, 6, 12, 24, 48, 96, 192, 384],
1481
- 'sigma': 'hist', 'cmap': 'Jet', 'alpha': 0.3}
1482
- _kw.update(kwargs)
1483
- volume = _kw['data']
1484
- levels = _kw['levels']
1485
- cmap = _kw['cmap']
1486
- alpha = _kw['alpha']
1487
- sigma = estimate_rms(data=volume, sigma=_kw['sigma'])
1488
- volume[np.isnan(volume)] = 0
1489
- a = int(sign == -1)
1490
- b = int(sign == 1)
1491
- volume = np.moveaxis([volume * a, volume * b], 0, axis)
1492
- if d.dx < 0:
1493
- volume = volume[:, :, ::-1]
1494
- if d.dy < 0:
1495
- volume = volume[:, ::-1, :]
1496
- if d.dv < 0:
1497
- volume = volume[::-1, :, :]
1498
- for lev in levels:
1499
- if lev * sigma > np.max(volume):
1500
- continue
1501
- vertices, simplices, _, _ = measure.marching_cubes(volume, lev * sigma)
1502
- Xg, Yg, Zg = [t[0] + i * dt for t, i, dt
1503
- in zip(s, vertices.T[::-1], ds)]
1504
- match axis:
1505
- case 2:
1506
- Xg = Xg * 0 + (x[-1] if sign == 1 else x[0])
1507
- case 1:
1508
- Yg = Yg * 0 + (y[-1] if sign == 1 else y[0])
1509
- case 0:
1510
- Zg = Zg * 0 + (v[-1] if sign == 1 else v[0])
1511
- i, j, k = simplices.T
1512
- mesh2d = dict(type='mesh3d', x=Xg, y=Yg, z=Zg,
1513
- i=i, j=j, k=k,
1514
- intensity=Zg * 0 + lev,
1515
- colorscale=cmap, reversescale=False,
1516
- cmin=np.min(levels), cmax=np.max(levels),
1517
- opacity=alpha, name='', showscale=False)
1518
- data.append(mesh2d)
1519
-
1520
1539
  klist = [xplus, xminus, yplus, yminus, vplus, vminus]
1521
1540
  slist = [1, -1, 1, -1, 1, -1]
1522
1541
  alist = [2, 2, 1, 1, 0, 0]
1523
1542
  for kw, sign, axis in zip(klist, slist, alist):
1524
- plot_on_wall(sign=sign, axis=axis, **kw)
1543
+ _plot_on_wall(d, x, y, v, measure, data, sign, axis, **kw)
1525
1544
 
1526
1545
  if return_data_layout:
1527
1546
  return {'data': data, 'layout': layout}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.7
3
+ Version: 1.9.9
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
File without changes