天文データ解析入門 その6 (astrodendroの使い方: 基本編)

【以下追記】
numpy のバージョンが 1.23 以上だと、astrodendro 内のパラメータの計算の中で
```python
module 'numpy' has no attribute 'asscalar'
```
とエラーが出ます。
astrodendro はおそらくもうアップデートされないので、numpy のバージョンを下げる (もしくは自分でastrodendroの中身を修正する) 必要があります。例えば、一時的な解決策として、解析ディレクトリで
```
pip install 'numpy==1.22.4' -t ./
```
などとして対応してください。



また、Python3.10などでは、import 時に、
```python
ImportError: cannot import name 'Iterable' from 'collections' (/usr/lib/python3.10/collections/__init__.py)
```
のエラーが出ることがあります。
```python
import collections.abc
collections.Iterable = collections.abc.Iterable
import astrodendro
```
とすることで、一時的に対応できます。
【以上追記】


本記事では、[astrodendro](https://dendrograms.readthedocs.io/en/stable/) (Astronomical Dendrograms in Python) の基本的な使い方について記述します。

今回も、例として国立天文台の [FUGIN](https://nro-fugin.github.io/) プロジェクトで得られた野辺山45m電波望遠鏡の CO 輝線のアーカイブデータを用います。データは
http://jvo.nao.ac.jp/portal/nobeyama/
から
FGN_03100+0000_2x2_12CO_v1.00_cube.fits
をダウンロードします (重いです)。

# 2D fits の場合
まずは例によって必要なものを import し、fitsを読み込みます。

```python
from astropy.io import fits
import numpy as np
from matplotlib import pyplot as plt
import aplpy
from astropy.wcs import WCS

hdu = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.fits")[0] # 3D
w = WCS(hdu.header)
```

2D fitsにするため、以下のような関数を定義します。

```python
def v2ch(v, w): # v(km/s)をchに変える
    x_tempo, y_tempo, v_tempo   = w.wcs_pix2world(0, 0, 0, 0)
    x_ch, y_ch, v_ch   = w.wcs_world2pix(x_tempo, y_tempo, v*1000.0, 0)
    v_ch = int(round(float(v_ch), 0))
    return v_ch

def del_header_key(header, keys): # headerのkeyを消す
    import copy
    h = copy.deepcopy(header)
    for k in keys:
        try:
            del h[k]
        except:
            pass
    return h
def make_new_hdu_integ(hdu, v_start_wcs, v_end_wcs, w): # 積分強度のhduを作る
    data = hdu.data
    header = hdu.header
    start_ch, end_ch = v2ch(v_start_wcs, w), v2ch(v_end_wcs, w)
    new_data = np.nansum(data[start_ch:end_ch+1], axis=0)*header["CDELT3"]/1000.0
    header = del_header_key(header, ["CRVAL3", "CRPIX3", "CRVAL3", "CDELT3", "CUNIT3", "CTYPE3", "CROTA3", "NAXIS3"])
    header["NAXIS"] = 2
    new_hdu = fits.PrimaryHDU(new_data, header)
    return new_hdu
```

以下のように 2D fits を作成します。

```python
integ_hdu = make_new_hdu_integ(hdu, 25.0, 125.0, w)
```

Dendrogram と、統計量を出すためのものなどを import します。

```python
from astrodendro import Dendrogram
from astrodendro.analysis import PPStatistic
import datetime
```

以下のように、3つのパラメータを設定します。詳しくは[ドキュメント](https://dendrograms.readthedocs.io/en/stable/)を参照してください。

```python
dendro_min_value = 10
dendro_min_delta = 50
dendro_min_npix = 100
```
以下でDendrogramを実行します。結果は全て d に格納されます。
(一応時間を計測しています。)

```python
t1 = datetime.datetime.now()
print("[dendro] started......", t1)
d = Dendrogram.compute(integ_hdu.data, min_value=dendro_min_value, min_delta=dendro_min_delta, min_npix=dendro_min_npix, verbose=False)
t2 = datetime.datetime.now()
print("[dendro] finished.....", t2)
print("[dendro] total time... ", (t2 - t1).total_seconds(),"sec")
```

ここまでやっておいて今更言うのもあれですが、以上を ipython で実行していると

```python
d.viewer()
plt.show()
```
を実行することで GUI で結果を確認できます。(Jupyter だとうまく動きません。)

![スクリーンショット 2021-06-10 12.12.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117814/02911237-092a-9b3c-1567-14a08fd01ec3.png)

# カタログ化
同定した構造をカタログ化したいと思います。標準では

```python
from astrodendro import pp_catalog
```
というものが備わっているのですが、少し扱いづらいので、今回は pandas を使って独自にカタログを作る方法をとることにします。

```python
import pandas as pd
```

まず、どれだけの構造が同定されたか個数を確認します。

```python
print(len(d))
# 1002
```

d.trunk や d.leaves と実行するとそれぞれ trunk と leaf の structure のリストが返ってきます (詳細は[ドキュメント](https://dendrograms.readthedocs.io/en/stable/)を参照)。数を以下で確認します。

```python
print(len(d.trunk))
# 6
print(len(d.leaves))
# 504
```
それでは、全 leaf に対してパラメータを出してカタログを作ろうと思います。
(追記) 
trunk に対する vmax は、ドキュメントによれば、sub-structure を取り除いた pixel/voxel たちでの最大値だそうです。一方、 get_peak() はその structure 内での最大値の場所と値を返してくれます。

```python
w_integ = WCS(integ_hdu.header)
x_peak, y_peak, x_cen, y_cen, val_min, val_max = [], [], [], [], [], []
index_list, ancestor = [], []
n_vox, area_ellipse, area_exact, minor_sigma, major_sigma, position_angle = [], [], [], [], [], [] 
radius, total_flux = [], []
for s in d.leaves:
    stat = PPStatistic(s)
    index_list.append(s.idx)
    ancestor.append(str(s.ancestor.idx))
    y_ch, x_ch = s.get_peak()[0]
    x_peak_, y_peak_ = w_integ.wcs_pix2world(x_ch, y_ch, 0)
    x_peak.append(round(float(x_peak_), 6))
    y_peak.append(round(float(y_peak_), 6))
    x_cen_ch, y_cen_ch = stat.x_cen.value, stat.y_cen.value
    x_cen_, y_cen_ = w_integ.wcs_pix2world(x_cen_ch, y_cen_ch, 0)
    x_cen.append(round(float(x_cen_), 6))
    y_cen.append(round(float(y_cen_), 6))
    val_min.append(round(s.vmin, 6))
    #val_max.append(round(s.vmax, 6))
    val_max.append(round(s.get_peak()[1], 6))
    ind = s.indices()
    n_vox.append(len(ind[0]))
    area_ellipse.append(round(stat.area_ellipse.value, 6))
    area_exact.append(round(stat.area_exact.value, 0))
    minor_sigma.append(round(stat.minor_sigma.value, 6))
    major_sigma.append(round(stat.major_sigma.value, 6))
    position_angle.append(round(stat.position_angle.value, 6))
    radius.append(round(stat.radius.value, 6))
    total_flux.append(round(stat.stat.mom0(), 6))
    
results_leaves = pd.DataFrame({
            'id':index_list, # ID
            'ancestor':ancestor, # (最も)先祖のID
            'x_peak':x_peak, # peak の位置のx座標
            'y_peak':y_peak, # peak の位置のy座標
            'x_cen':x_cen, # 重心位置のx座標
            'y_cen':y_cen, # 重心位置のy座標
            'val_min':val_min, # 構造の持つ最小値
            'val_max':val_max, # 構造の持つ最大値
            'n_vox':n_vox, # 構造が使っている pixel 数
            'area_ellipse':area_ellipse, # 構造の重みつき 1 sigma の楕円面積 (pixel^2)
            'area_exact':area_exact, # 構造が実際に使っている面積 (pixel^2)
            'minor_sigma':minor_sigma, # 楕円の短軸 (pixel)
            'major_sigma':major_sigma, # 楕円の長軸 (pixel)
            'position_angle':position_angle, # 楕円の position angle (degree)
            'radius':radius, # 楕円の長軸短軸の相乗平均 (pixel)
            'total_flux':total_flux # 構造の持つ値の総和
        })
```

results_leaves という名前の pandas DataFrame が出来上がりました。
試しに何かplotしてみます。


```python
plt.scatter(results_leaves["area_exact"], results_leaves["total_flux"])
plt.xscale("log")
plt.yscale("log")
plt.xlabel("area_exact [pix^2]")
plt.ylabel("total_flux [K]")
plt.show()
```
![ダウンロード (12).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117814/39ac3d8e-3057-de79-e0cd-1680197efb64.png)

```python
results_leaves.to_csv("leaves_test.csv")
```
で保存できます。読み込みは

```python
results_leaves = pd.read_csv("leaves_test.csv")
```
です。


# 3D fits (cube) の場合
2D の場合と使用方法は変わりません。 (ただし、もちろん計算は重いです。)

カタログ化の際は

```python
from astrodendro.analysis import PPVStatistic
```
を使って、v に関するパラメータも格納できるように上のコードを適宜書き換えましょう。

# カタログの map 上への plot
aplpy と、aplpy の show_markers を使えば簡単にできます。

```python
fig = plt.figure(figsize=(8, 8))
f = aplpy.FITSFigure(integ_hdu, slices=[0], convention='wells', figure=fig)
f.show_colorscale(vmin=1, vmax=500, stretch='log', cmap="Greys", aspect="equal")
f.show_markers(results_leaves["x_peak"], results_leaves["y_peak"], marker="x", c="r")
plt.show()
```
![ダウンロード (13).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117814/5cd18e69-cbad-26e0-8b4a-3a6dcd67a2f5.png)

構造の輪郭のコントアを引きたい場合は、

```python
mask = np.zeros(integ_hdu.data.shape, dtype=bool)
for s in d.leaves:
    mask = mask | s.get_mask()
mask_hdu = fits.PrimaryHDU(mask.astype('short'), integ_hdu.header)
```
でコントアの hdu を作った後に、

```python
fig = plt.figure(figsize=(8, 8))
f = aplpy.FITSFigure(integ_hdu, slices=[0], convention='wells', figure=fig)
f.show_colorscale(vmin=1, vmax=800, stretch='log', cmap="Greys", aspect="equal")
f.show_contour(mask_hdu, colors='r', linewidths=0.5)
plt.show()
```
![ダウンロード (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117814/2bce8e6b-c67d-4162-7223-66ec0ab7705d.png)

以上です。

リンク
[目次](https://qiita.com/Shinji_Fujita/items/ecb869e5b6a9fa468483)