天文データ解析入門 その18 (速度チャンネルマップを作る)

本記事では、速度チャンネルマップ (velocity channel map) を作ります。

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

また、 _Spitzer_ の 8.0 µm データを使用します。
https://irsa.ipac.caltech.edu/data/SPITZER/GLIMPSE/images/I/1.2_mosaics_v3.5/GLON_30-53/
から GLM_03000+0000_mosaic_I4.fits をダウンロードしました。

速度チャンネルマップは、aplpy.FITSFigure の subplot を指定すれば比較的簡単に作れます (綺麗にするのは少し難しいです)。
まず必要なものを import します。

```python
from astropy.io import fits
import numpy as np
import matplotlib.pyplot as plt
import aplpy
from matplotlib import cm
```
そして、積分強度を作る & fits を切り取って軽くするために、[前回の記事](https://qiita.com/Shinji_Fujita/items/8e2370758a5eb3214fc4) などで紹介した関数を定義します。

```python
def xyv2ch(x, y, v, w):#km/s
    x_ch, y_ch, v_ch   = w.wcs_world2pix(x, y, v*1000.0, 0)
    x_ch = int(round(float(x_ch), 0))
    y_ch = int(round(float(y_ch), 0))
    v_ch = int(round(float(v_ch), 0))
    return x_ch, y_ch, v_ch

def xy2ch(x, y, w):
    x_ch, y_ch   = w.wcs_world2pix(x, y, 0)
    x_ch = int(round(float(x_ch), 0))
    y_ch = int(round(float(y_ch), 0))
    return x_ch, y_ch

def v2ch(v, w):#km/s
    x_tempo, y_tempo, v_tempo   = w.wcs_world2pix(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 ch2v(ch, w):#km/s
    x, y, v   = w.wcs_pix2world(0, 0, ch, 0)
    return v/1000.0

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_ch(hdu, v_start_ch, v_end_ch, w): # 積分強度のhduを作る
    data = hdu.data
    header = hdu.header
    new_data = np.nansum(data[v_start_ch:v_end_ch], 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

def xy_cut(fits_name, x_start, x_end, y_start, y_end):
    from astropy.io import fits
    from astropy.wcs import WCS

    save_name = fits_name[:-5]+".xy_cut.fits"
    hdu = fits.open(fits_name)
    h = hdu[0].header
    d = hdu[0].data
    w = WCS(h)
    if d.ndim==3:
        x_start_ch, y_start_ch, v_tempo = xyv2ch(x_start, y_start, 0.0, w)
        x_end_ch, y_end_ch, v_tempo = xyv2ch(x_end, y_end, 0.0, w)  
        if x_start_ch<0 or y_start_ch<0 or x_end_ch>h["NAXIS1"]-2 or y_end_ch>h["NAXIS2"]-2:
            print("x_start_ch, x_end_ch = ", x_start_ch, x_end_ch)
            print("y_start_ch, y_end_ch = ", y_start_ch, y_end_ch)
            print("out of fits.")
            return
        d = d[:, y_start_ch:y_end_ch+1, x_start_ch:x_end_ch+1] # +1 してます
    elif d.ndim==2:
        x_start_ch, y_start_ch = xy2ch(x_start, y_start, w)
        x_end_ch, y_end_ch = xy2ch(x_end, y_end, w) 
        if x_start_ch<0 or y_start_ch<0 or x_end_ch>h["NAXIS1"]-2 or y_end_ch>h["NAXIS2"]-2:
            print("x_start_ch, x_end_ch = ", x_start_ch, x_end_ch)
            print("y_start_ch, y_end_ch = ", y_start_ch, y_end_ch)
            print("out of fits.")
            return
        d = d[y_start_ch:y_end_ch+1, x_start_ch:x_end_ch+1] # +1 してます
    else:
        print("data.ndim must be 2 or 3. ")
        return

    h['CRPIX1'] = h['CRPIX1'] - x_start_ch
    h['CRPIX2'] = h['CRPIX2'] - y_start_ch

    new_fits = fits.PrimaryHDU(d, h)
    new_fits.writeto(save_name, overwrite=True)
    return
```
xy_cut を使用します。fits がそもそも重くない方はスキップしても大丈夫です。

```python
fits_name = "FGN_03100+0000_2x2_12CO_v1.00_cube.fits"
mapcenter_x, mapcenter_y, mapsize_x, mapsize_y = 30.75, 0.0, 0.5, 0.5 # degree で、map の中心と幅, 高さ
x_start, x_end = mapcenter_x + mapsize_x/2.0, mapcenter_x - mapsize_x/2.0 # degree で、左-右の順で入れる
y_start, y_end =  mapcenter_y - mapsize_y/2.0, mapcenter_y + mapsize_y/2.0 # degree で、下-上の順で入れる
xy_cut(fits_name, x_start, x_end, y_start, y_end)
# FGN_03100+0000_2x2_12CO_v1.00_cube.xy_cut.fits ができる

fits_name = "GLM_03000+0000_mosaic_I4.fits"
mapcenter_x, mapcenter_y, mapsize_x, mapsize_y = 30.75, 0.0, 0.5, 0.5 # degree で、map の中心と幅, 高さ
x_start, x_end = mapcenter_x + mapsize_x/2.0, mapcenter_x - mapsize_x/2.0 # degree で、左-右の順で入れる
y_start, y_end =  mapcenter_y - mapsize_y/2.0, mapcenter_y + mapsize_y/2.0 # degree で、下-上の順で入れる
xy_cut(fits_name, x_start, x_end, y_start, y_end)
# GLM_03000+0000_mosaic_I4.xy_cut.fits ができる
```
ここから本題です。
パネル数やスタートする速度などを与えるとマップが出力される関数です。適当に書いた関数なので、引数が多くて使いづらいかもしれません。

```python
def func_chmap_integ_nxm(fits_name, v_start, ch_range, color_min, color_max, mapcenter_x, mapcenter_y, mapsize_x, mapsize_y, figsize, n, m, cmap, stretch, cb_label):
    from astropy.io import fits
    from astropy.wcs import WCS
    import numpy as np
    import matplotlib
    import matplotlib.pyplot as plt
    import aplpy
    from matplotlib.colorbar import ColorbarBase
    #from mpl_toolkits.axes_grid1 import make_axes_locatable
    from matplotlib.colors import LogNorm
    import math
    plt.rcParams['xtick.direction'] = 'in'
    plt.rcParams['ytick.direction'] = 'in'
    
    save_name = "chmap_%.1f_kms_%ich_%ix%i"%(v_start, ch_range, n, m)
    hdu = fits.open(fits_name)[0]
    h = hdu.header
    d = hdu.data
    w = WCS(h)
    ch_start = v2ch(v_start, w)
    position_addedlabel_x, position_addedlabel_y = mapcenter_x + mapsize_x*0.1, mapcenter_y + mapsize_y*0.43 # label の位置です。調整してください。

    fig = plt.figure(figsize=figsize)

    for i in range(n*m):
        ch_start_i, ch_end_i = int(ch_start+i*ch_range), int(ch_start+(i+1)*ch_range)
        v_start, v_end = ch2v(ch_start_i, w), ch2v(ch_end_i-1, w)
        hdu_color = make_new_hdu_integ_ch(hdu, ch_start_i, ch_end_i , w)
        
        f = aplpy.FITSFigure(hdu_color, slices=[0], figure=fig, subplot=[0.1+(0.8/float(n)*math.floor(i%n)),0.9-(0.8/float(m)*math.floor(i/n+1)), 0.8/float(n), 0.8/float(m)], convention='wells')
        f.recenter(mapcenter_x, mapcenter_y, width=mapsize_x, height=mapsize_y)
        f.show_colorscale(vmin=color_min, vmax=color_max, cmap=cmap, stretch=stretch)
        f.ticks.set_color("k")
        v_start_label, v_end_label = ch2v(ch_start_i, w)-h["CDELT3"]/1000.0/2.0, ch2v(ch_end_i-1, w)+h["CDELT3"]/1000.0/2.0
        f.add_label(position_addedlabel_x, position_addedlabel_y, "%.1f - %.1f km s$^{-1}$"%(v_start_label, v_end_label), color="k", size=14, family='serif', weight='bold') # 速度ラベル
        if int(i/n) != (m-1):
            f.tick_labels.hide_x()
            f.axis_labels.hide_x()
        if int(i%n) != 0:
            f.tick_labels.hide_y()
            f.axis_labels.hide_y()
        print(i+1, "/", n*m)
    #fig.canvas.draw()
    ax = fig.add_subplot(1, 1, 1)
    if stretch=="log":
        cb = ColorbarBase(ax=ax, cmap=cmap, norm=LogNorm(vmin=color_min, vmax=color_max), orientation="vertical")
    else:
        cb = ColorbarBase(ax=ax, cmap=cmap, norm=matplotlib.colors.Normalize(vmin=color_min, vmax=color_max), orientation="vertical")
    cb.ax.set_position([0.92, 0.1, 0.05, 0.8])
    cb.set_label(cb_label, size=20, family="serif")
    f.save(save_name+'.png', dpi=150)
```
以下のように使います。

```python
fits_name = "~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.xy_cut.fits"
mapcenter_x, mapcenter_y, mapsize_x, mapsize_y = 30.75, 0.0, 0.5, 0.5 # degree で、map の中心と幅, 高さ
v_start = 75.0 # km/s
ch_range = 5 # 1パネルに使うチャンネル数
color_min, color_max = 0.0, 100
n, m = 3, 4 # 列数, 行数
figsize = (n*4, m*4) # figure のサイズ
cmap = cm.gist_earth_r # colormap
stretch = "linear" # linear or log
cb_label = "$^{12}$CO [K km s$^{-1}$]"
func_chmap_integ_nxm(fits_name, v_start, ch_range, color_min, color_max, mapcenter_x, mapcenter_y, mapsize_x, mapsize_y, figsize, n, m, cmap, stretch, cb_label)
```
![ダウンロード (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117814/6c6ff6ed-9d21-e0fb-bbb2-c38bac349653.png)

カラーバーの太さや文字の大きさやその他諸々は適宜変更してください。保存形式も、 eps や pdf に変更できます。
（※ Jupyter では plot にすごい時間がかかる場合があります。どうしても遅くて我慢できない時は script にして (テキストファイルにして) ターミナルから実行してください。少し速くなります。）

例えば、赤外線の上に CO のコントアを乗せたい場合は以下のように関数を変更します。
(速度範囲やコントアレベルは今てきとうにやっています。)

```python
def func_chmap_integ_nxm_2(fits_name_contour, fits_name_color, v_start, ch_range, color_min, 
                           color_max, mapcenter_x, mapcenter_y, mapsize_x, mapsize_y, figsize, n, m, cmap, stretch, cb_labe, levels):
    from astropy.io import fits
    from astropy.wcs import WCS
    import numpy as np
    import matplotlib
    import matplotlib.pyplot as plt
    import aplpy
    from matplotlib.colorbar import ColorbarBase
    #from mpl_toolkits.axes_grid1 import make_axes_locatable
    from matplotlib.colors import LogNorm
    import math
    plt.rcParams['xtick.direction'] = 'in'
    plt.rcParams['ytick.direction'] = 'in'
    
    save_name = "chmap_2_%.1f_kms_%ich_%ix%i"%(v_start, ch_range, n, m)
    hdu = fits.open(fits_name_contour)[0]
    h = hdu.header
    d = hdu.data
    w = WCS(h)
    ch_start = v2ch(v_start, w)
    position_addedlabel_x, position_addedlabel_y = mapcenter_x + mapsize_x*0.1, mapcenter_y + mapsize_y*0.43 # label の位置です。調整してください。

    fig = plt.figure(figsize=fig_size)

    for i in range(n*m):
        ch_start_i, ch_end_i = int(ch_start+i*ch_range), int(ch_start+(i+1)*ch_range)
        v_start, v_end = ch2v(ch_start_i, w), ch2v(ch_end_i-1, w)
        hdu_contour = make_new_hdu_integ_ch(hdu, ch_start_i, ch_end_i , w)
        
        f = aplpy.FITSFigure(fits_name_color, slices=[0], figure=fig, subplot=[0.1+(0.8/float(n)*math.floor(i%n)),0.9-(0.8/float(m)*math.floor(i/n+1)), 0.8/float(n), 0.8/float(m)], convention='wells')
        f.recenter(mapcenter_x, mapcenter_y, width=mapsize_x, height=mapsize_y)
        f.show_colorscale(vmin=color_min, vmax=color_max, cmap=cmap, stretch=stretch)
        f.ticks.set_color("k")
        v_start_label, v_end_label = ch2v(ch_start_i, w)-h["CDELT3"]/1000.0/2.0, ch2v(ch_end_i-1, w)+h["CDELT3"]/1000.0/2.0
        f.add_label(position_addedlabel_x, position_addedlabel_y, "%.1f - %.1f km s$^{-1}$"%(v_start_label, v_end_label), color="k", size=14, family='serif', weight='bold') # 速度ラベル
        f.show_contour(hdu_contour, levels=levels, colors="k")
        if int(i/n) != (m-1):
            f.tick_labels.hide_x()
            f.axis_labels.hide_x()
        if int(i%n) != 0:
            f.tick_labels.hide_y()
            f.axis_labels.hide_y()
        print(i+1, "/", n*m)
    #fig.canvas.draw()
    ax = fig.add_subplot(1, 1, 1)
    if stretch=="log":
        cb = ColorbarBase(ax=ax, cmap=cmap, norm=LogNorm(vmin=color_min, vmax=color_max), orientation="vertical")
    else:
        cb = ColorbarBase(ax=ax, cmap=cmap, norm=matplotlib.colors.Normalize(vmin=color_min, vmax=color_max), orientation="vertical")
    cb.ax.set_position([0.92, 0.1, 0.05, 0.8])
    cb.set_label(cb_label, size=20, family="serif")
    f.save(save_name+'.png', dpi=150)
```

```python
fits_name_contour = "FGN_03100+0000_2x2_12CO_v1.00_cube.xy_cut.fits"
fits_name_color = "GLM_03000+0000_mosaic_I4.xy_cut.fits"
mapcenter_x, mapcenter_y, mapsize_x, mapsize_y = 30.75, 0.0, 0.5, 0.5 # degree で、map の中心と幅, 高さ
v_start = 85.0
ch_range = 5 # 1パネルに使うチャンネル数
color_min, color_max = 10.0, 2000
n, m = 3, 4 # 列数, 行数
fig_size = (n*4, m*4) # figure のサイズ
cmap = cm.Greens # colormap
stretch = "log" # linear or log
cb_label = "8$\mu$m [MJy str$^{-1}$]"
levels = np.array([1,2,3,4,5,6,7,8])*50
func_chmap_integ_nxm_2(fits_name_contour, fits_name_color, v_start, ch_range, color_min, color_max, mapcenter_x, mapcenter_y, mapsize_x, mapsize_y, figsize, n, m, cmap, stretch, cb_label, levels)
```
![ダウンロード (7).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/117814/d12adbcf-32e9-f9b3-8182-03c8d24e8ba6.png)

以上です。

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