plotastrodata 1.9.6__tar.gz → 1.9.8__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.6/plotastrodata.egg-info → plotastrodata-1.9.8}/PKG-INFO +1 -1
  2. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/__init__.py +1 -1
  3. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/analysis_utils.py +155 -141
  4. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/fits_utils.py +145 -140
  5. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/plot_utils.py +1 -1
  6. {plotastrodata-1.9.6 → plotastrodata-1.9.8/plotastrodata.egg-info}/PKG-INFO +1 -1
  7. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/LICENSE +0 -0
  8. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/MANIFEST.in +0 -0
  9. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/README.md +0 -0
  10. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/const_utils.py +0 -0
  11. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/coord_utils.py +0 -0
  12. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/ext_utils.py +0 -0
  13. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/fft_utils.py +0 -0
  14. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/fitting_utils.py +0 -0
  15. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/los_utils.py +0 -0
  16. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/matrix_utils.py +0 -0
  17. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/noise_utils.py +0 -0
  18. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata/other_utils.py +0 -0
  19. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata.egg-info/SOURCES.txt +0 -0
  20. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata.egg-info/dependency_links.txt +0 -0
  21. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata.egg-info/not-zip-safe +0 -0
  22. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata.egg-info/requires.txt +0 -0
  23. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/plotastrodata.egg-info/top_level.txt +0 -0
  24. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/setup.cfg +0 -0
  25. {plotastrodata-1.9.6 → plotastrodata-1.9.8}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.6
3
+ Version: 1.9.8
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.6'
4
+ __version__ = '1.9.8'
@@ -575,6 +575,26 @@ class AstroData():
575
575
  fitsimage=fitsimage)
576
576
 
577
577
 
578
+ def _as_list(value, n: int, isbeam: bool = False):
579
+ if isbeam:
580
+ return [value] * n if np.ndim(value) == 1 else value
581
+ else:
582
+ return value if isinstance(value, list) else [value] * n
583
+
584
+
585
+ def _scalar_if_single(value, n: int):
586
+ return value[0] if n == 1 else value
587
+
588
+
589
+ def _get_gridsep(axis: np.ndarray | None):
590
+ return axis[1] - axis[0] if axis is not None and len(axis) > 1 else None
591
+
592
+
593
+ ASTRODATA_ARGS = ["fitsimage", "data", "Tb", "sigma", "center", "restfreq",
594
+ "cfactor", "bunit", "fitsimage_org", "sigma_org",
595
+ "beam_org", "fitsheader", "pv", "pvpa"]
596
+
597
+
578
598
  @dataclass
579
599
  class AstroFrame():
580
600
  """Parameter set to limit and reshape the data in the AstroData format.
@@ -666,6 +686,133 @@ class AstroFrame():
666
686
  x[i], y[i] = rel2abs(*p, self.Xlim, self.Ylim)
667
687
  return np.array([x, y])
668
688
 
689
+ def _get_restfreq(self, header: dict):
690
+ """Extract rest frequency from FITS header."""
691
+ if "RESTFRQ" in header:
692
+ return header["RESTFRQ"]
693
+ if "RESTFREQ" in header:
694
+ return header["RESTFREQ"]
695
+ if "NAXIS3" in header and header["NAXIS3"] == 1 and not self.pv:
696
+ return header["CRVAL3"]
697
+ return None
698
+
699
+ def _read_fitsimage(self, d: AstroData, i: int, grid: list) -> list:
700
+ """Read FITS-derived values into d and return the FITS grid."""
701
+ if d.fitsimage[i] is None:
702
+ return grid
703
+
704
+ fd = FitsData(d.fitsimage[i])
705
+ if d.fitsheader[i] is None:
706
+ d.fitsheader[i] = fd.get_header()
707
+ if d.center[i] is None and not self.pv:
708
+ d.center[i] = fd.get_center()
709
+ if d.restfreq[i] is None:
710
+ d.restfreq[i] = self._get_restfreq(d.fitsheader[i])
711
+ d.data[i] = fd.get_data()
712
+ grid = fd.get_grid(center=d.center[i], dist=self.dist,
713
+ restfreq=d.restfreq[i], vsys=self.vsys,
714
+ pv=self.pv)
715
+ if fd.wcsrot:
716
+ d.center[i] = fd.get_center()
717
+ d.beam[i] = fd.get_beam(dist=self.dist)
718
+ d.bunit[i] = fd.get_header("BUNIT")
719
+ return grid
720
+
721
+ def _shift_center(self, d: AstroData, i: int, grid: list) -> list:
722
+ corg = d.center[i]
723
+ cnew = self.center
724
+ if self.pv or cnew is None or corg is None or corg == cnew:
725
+ return grid
726
+
727
+ cx, cy = coord2xy(corg, cnew) * 3600
728
+ grid[0] = grid[0] + cx # Don't use += cx.
729
+ grid[1] = grid[1] + cy # Don't use += cy.
730
+ d.center[i] = cnew
731
+ return grid
732
+
733
+ def _ascending_v(self, d: AstroData, i: int, v: np.ndarray | None):
734
+ if v is not None and len(v) > 1 and v[1] < v[0]:
735
+ d.data[i], v = d.data[i][::-1], v[::-1]
736
+ print('Velocity has been inverted.')
737
+ d.v = v
738
+
739
+ def _xyskip(self, d: AstroData, i: int,
740
+ x: np.ndarray | None, y: np.ndarray | None):
741
+ d.x = x[::self.xskip]
742
+ d.y = y[::self.yskip]
743
+ data = np.moveaxis(d.data[i], [-2, -1], [0, 1])
744
+ data = data[::self.yskip, ::self.xskip]
745
+ d.data[i] = np.moveaxis(data, [0, 1], [-2, -1])
746
+
747
+ def _trim_skip(self, d: AstroData, i: int, grid: list):
748
+ d.data[i], grid = trim(data=d.data[i],
749
+ x=grid[0], y=grid[1], v=grid[2],
750
+ xlim=self.xlim, ylim=self.ylim,
751
+ vlim=self.vlim, pv=self.pv)
752
+ self._ascending_v(d, i, v=grid[2])
753
+ grid = [grid[0], d.v] if self.pv else [grid[0], grid[1]]
754
+ if self.swapxy:
755
+ grid.reverse()
756
+ d.data[i] = np.moveaxis(d.data[i], 1, 0)
757
+ self._xyskip(d, i, x=grid[0], y=grid[1])
758
+ if self.pv:
759
+ d.v = d.y
760
+ for axis in ['x', 'y', 'v']:
761
+ setattr(d, f"d{axis}", _get_gridsep(getattr(d, axis)))
762
+
763
+ def _convert_to_Tb(self, d: AstroData, i: int):
764
+ """Convert Jy/beam data to brightness temperature if requested."""
765
+ if not d.Tb[i]:
766
+ return
767
+
768
+ dx = d.dy if self.swapxy else d.dx
769
+ header = {"CDELT1": dx / 3600,
770
+ "CUNIT1": "DEG",
771
+ "RESTFREQ": d.restfreq[i]}
772
+ if None not in d.beam[i]:
773
+ header["BMAJ"] = d.beam[i][0] / 3600 / self.dist
774
+ header["BMIN"] = d.beam[i][1] / 3600 / self.dist
775
+ factor = Jy2K(header=header)
776
+ d.data[i] = d.data[i] * factor
777
+ if d.sigma[i] is not None:
778
+ d.sigma[i] = d.sigma[i] * factor
779
+
780
+ def _set_pv_beam(self, d: AstroData, i: int):
781
+ """Set effective PV beam."""
782
+ if not self.pv or d.pv[i] or None in d.beam[i]:
783
+ return
784
+
785
+ bmaj, bmin, bpa = d.beam_org[i] = d.beam[i]
786
+ if d.pvpa[i] is None:
787
+ d.pvpa[i] = bpa
788
+ print("pvpa is not specified. pvpa=bpa is assumed.")
789
+ angle = np.radians(bpa - d.pvpa[i])
790
+ beam_incut = 1 / np.hypot(np.cos(angle) / bmaj, np.sin(angle) / bmin)
791
+ d.beam[i] = np.array([np.abs(d.dv), beam_incut, 0])
792
+
793
+ def _read_one(self, d: AstroData, i: int):
794
+ if d.center[i] == 'common':
795
+ d.center[i] = self.center
796
+ d.sigma_org[i] = d.sigma[i]
797
+ grid = self._read_fitsimage(d, i, grid=[d.x, d.y, d.v])
798
+ if d.data[i] is not None:
799
+ d.sigma[i] = estimate_rms(d.data[i], d.sigma[i])
800
+ grid = self._shift_center(d, i, grid)
801
+ self._trim_skip(d, i, grid)
802
+ if self.quadrants is not None:
803
+ d.data[i], d.x, d.y \
804
+ = quadrantmean(d.data[i], d.x, d.y, self.quadrants)
805
+ d.data[i] = d.data[i] * d.cfactor[i]
806
+ if d.sigma[i] is not None:
807
+ d.sigma[i] = d.sigma[i] * d.cfactor[i]
808
+ self._convert_to_Tb(d, i)
809
+ self._set_pv_beam(d, i)
810
+ d.pv[i] = self.pv
811
+ d.Tb[i] = False
812
+ d.cfactor[i] = 1
813
+ d.fitsimage_org[i] = d.fitsimage[i]
814
+ d.fitsimage[i] = None
815
+
669
816
  def read(self, d: AstroData, xskip: int = 1, yskip: int = 1):
670
817
  """Get data, grid, sigma, beam, and bunit from AstroData, which is a part of the input of add_color, add_contour, add_segment, and add_rgb.
671
818
 
@@ -673,145 +820,12 @@ class AstroFrame():
673
820
  d (AstroData): Dataclass for the add_* input.
674
821
  xskip, yskip (int): Spatial pixel skip. Defaults to 1.
675
822
  """
676
- if type(d.fitsimage) is not list:
677
- d.fitsimage = [d.fitsimage] * d.n
678
- if type(d.data) is not list:
679
- d.data = [d.data] * d.n
680
- if np.ndim(d.beam) == 1:
681
- d.beam = [d.beam] * d.n
682
- if type(d.Tb) is not list:
683
- d.Tb = [d.Tb] * d.n
684
- if type(d.sigma) is not list:
685
- d.sigma = [d.sigma] * d.n
686
- if type(d.center) is not list:
687
- d.center = [d.center] * d.n
688
- if type(d.restfreq) is not list:
689
- d.restfreq = [d.restfreq] * d.n
690
- if type(d.cfactor) is not list:
691
- d.cfactor = [d.cfactor] * d.n
692
- if type(d.bunit) is not list:
693
- d.bunit = [d.bunit] * d.n
694
- if type(d.fitsimage_org) is not list:
695
- d.fitsimage_org = [d.fitsimage_org] * d.n
696
- if type(d.sigma_org) is not list:
697
- d.sigma_org = [d.sigma_org] * d.n
698
- if type(d.beam_org) is not list:
699
- d.beam_org = [d.beam_org] * d.n
700
- if type(d.fitsheader) is not list:
701
- d.fitsheader = [d.fitsheader] * d.n
702
- if type(d.pv) is not list:
703
- d.pv = [d.pv] * d.n
704
- if type(d.pvpa) is not list:
705
- d.pvpa = [d.pvpa] * d.n
706
- grid0 = [d.x, d.y, d.v]
823
+ self.xskip = xskip
824
+ self.yskip = yskip
825
+ for name in ASTRODATA_ARGS:
826
+ setattr(d, name, _as_list(getattr(d, name), d.n))
827
+ d.beam = _as_list(d.beam, d.n, isbeam=True)
707
828
  for i in range(d.n):
708
- if d.center[i] == 'common':
709
- d.center[i] = self.center
710
- grid = grid0.copy()
711
- if d.fitsimage[i] is not None:
712
- fd = FitsData(d.fitsimage[i])
713
- if d.fitsheader[i] is None:
714
- d.fitsheader[i] = fd.get_header()
715
- if d.center[i] is None and not self.pv:
716
- d.center[i] = fd.get_center()
717
- if d.restfreq[i] is None:
718
- h = d.fitsheader[i]
719
- if 'NAXIS3' in h and h['NAXIS3'] == 1 and not self.pv:
720
- d.restfreq[i] = h['CRVAL3']
721
- elif 'RESTFRQ' in h:
722
- d.restfreq[i] = h['RESTFRQ']
723
- elif 'RESTFREQ' in h:
724
- d.restfreq[i] = h['RESTFREQ']
725
- d.data[i] = fd.get_data()
726
- grid = fd.get_grid(center=d.center[i], dist=self.dist,
727
- restfreq=d.restfreq[i], vsys=self.vsys,
728
- pv=self.pv)
729
- if fd.wcsrot:
730
- d.center[i] = fd.get_center() # for WCS rotation
731
- d.beam[i] = fd.get_beam(dist=self.dist)
732
- d.bunit[i] = fd.get_header('BUNIT')
733
- if d.data[i] is not None:
734
- d.sigma_org[i] = d.sigma[i]
735
- d.sigma[i] = estimate_rms(d.data[i], d.sigma[i])
736
- diffcent = (not self.pv
737
- and self.center is not None
738
- and d.center[i] is not None
739
- and d.center[i] != self.center)
740
- if diffcent:
741
- cx, cy = coord2xy(d.center[i], self.center) * 3600
742
- grid[0] = grid[0] + cx # Don't use += cx.
743
- grid[1] = grid[1] + cy # Don't use += cy.
744
- d.center[i] = self.center
745
- d.data[i], grid = trim(data=d.data[i],
746
- x=grid[0], y=grid[1], v=grid[2],
747
- xlim=self.xlim, ylim=self.ylim,
748
- vlim=self.vlim, pv=self.pv)
749
- v = grid[2]
750
- has_v = v is not None and len(v) > 1
751
- if has_v and v[1] < v[0]:
752
- d.data[i], v = d.data[i][::-1], v[::-1]
753
- print('Velocity has been inverted.')
754
- d.v = v
755
- d.dv = v[1] - v[0] if has_v else None
756
- grid = grid[:3:2] if self.pv else grid[:2]
757
- if self.swapxy:
758
- grid = [grid[1], grid[0]]
759
- d.data[i] = np.moveaxis(d.data[i], 1, 0)
760
- grid[0] = grid[0][::xskip]
761
- grid[1] = grid[1][::yskip]
762
- a = d.data[i]
763
- a = np.moveaxis(a, [-2, -1], [0, 1])
764
- a = a[::yskip, ::xskip]
765
- a = np.moveaxis(a, [0, 1], [-2, -1])
766
- d.data[i] = a
767
- x, y = d.x, d.y = grid
768
- has_x = x is not None and len(x) > 1
769
- d.dx = x[1] - x[0] if has_x else None
770
- has_y = y is not None and len(y) > 1
771
- d.dy = y[1] - y[0] if has_y else None
772
- if self.quadrants is not None:
773
- d.data[i], d.x, d.y \
774
- = quadrantmean(d.data[i], d.x, d.y, self.quadrants)
775
- d.data[i] = d.data[i] * d.cfactor[i]
776
- if d.sigma[i] is not None:
777
- d.sigma[i] = d.sigma[i] * d.cfactor[i]
778
- if d.Tb[i]:
779
- dx = d.dy if self.swapxy else d.dx
780
- header = {'CDELT1': dx / 3600,
781
- 'CUNIT1': 'DEG',
782
- 'RESTFREQ': d.restfreq[i]}
783
- if None not in d.beam[i]:
784
- header['BMAJ'] = d.beam[i][0] / 3600 / self.dist
785
- header['BMIN'] = d.beam[i][1] / 3600 / self.dist
786
- d.data[i] = d.data[i] * Jy2K(header=header)
787
- d.sigma[i] = d.sigma[i] * Jy2K(header=header)
788
- if self.pv and not d.pv[i] and None not in d.beam[i]:
789
- bmaj, bmin, bpa = d.beam_org[i] = d.beam[i]
790
- if d.pvpa[i] is None:
791
- d.pvpa[i] = bpa
792
- print('pvpa is not specified. pvpa=bpa is assumed.')
793
- p = np.radians(bpa - d.pvpa[i])
794
- b = 1 / np.hypot(np.cos(p) / bmaj, np.sin(p) / bmin)
795
- d.beam[i] = np.array([np.abs(d.dv), b, 0])
796
- d.pv[i] = self.pv
797
- d.Tb[i] = False
798
- d.cfactor[i] = 1
799
- if d.fitsimage[i] is not None:
800
- d.fitsimage_org[i] = d.fitsimage[i]
801
- d.fitsimage[i] = None
802
- if d.n == 1:
803
- d.data = d.data[0]
804
- d.beam = d.beam[0]
805
- d.fitsimage = d.fitsimage[0]
806
- d.Tb = d.Tb[0]
807
- d.sigma = d.sigma[0]
808
- d.center = d.center[0]
809
- d.restfreq = d.restfreq[0]
810
- d.cfactor = d.cfactor[0]
811
- d.bunit = d.bunit[0]
812
- d.fitsimage_org = d.fitsimage_org[0]
813
- d.sigma_org = d.sigma_org[0]
814
- d.beam_org = d.beam_org[0]
815
- d.fitsheader = d.fitsheader[0]
816
- d.pv = d.pv[0]
817
- d.pvpa = d.pvpa[0]
829
+ self._read_one(d, i)
830
+ for name in ASTRODATA_ARGS + ["beam"]:
831
+ setattr(d, name, _scalar_if_single(getattr(d, name), d.n))
@@ -29,7 +29,7 @@ def Jy2K(header=None, bmaj: float | None = None, bmin: float | None = None,
29
29
  else:
30
30
  print('Use CDELT1^2 for Tb conversion.')
31
31
  todiameter = np.sqrt(4 * np.log(2) / np.pi) * 3600
32
- bmaj = bmin = header['CDELT1'] * todiameter
32
+ bmaj = bmin = np.abs(header['CDELT1']) * todiameter
33
33
  if header['CUNIT1'] == 'arcsec':
34
34
  bmaj, bmin = bmaj / 3600, bmin / 3600
35
35
  if 'RESTFREQ' in header:
@@ -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
 
@@ -149,7 +149,7 @@ def _get_gridwidth(mode: str, rmax: float, cos_dec: float
149
149
  def _get_v(p, v: np.ndarray | None = None,
150
150
  restfreq: float | None = None,
151
151
  vskip: int = 1) -> np.ndarray:
152
- if p.fitsimage is not None:
152
+ if p.fitsimage is not None and v is None:
153
153
  p.read(d := AstroData(fitsimage=p.fitsimage,
154
154
  restfreq=restfreq, sigma=None))
155
155
  v = d.v
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotastrodata
3
- Version: 1.9.6
3
+ Version: 1.9.8
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