【Python】風データから渦度を計算してみる

# 概要
気象分野には，**渦度**という物理量があります．

渦度とは，簡単に言うと「局所[^0]領域における流れの回転の強さを表す量」です．

[^0]:"局所"とわざわざつけているのは，例えば全体として渦を巻いているように見えても渦度がゼロになる（各流体粒子は回転していない）場合があるからです．

これは風のデータから簡単に計算することができるので，Pythonを使って渦度を計算してみよう，というのが本記事の内容です．

# 渦度とは
流体力学では渦度ベクトル$\boldsymbol{\omega}$を，流速ベクトル$\boldsymbol{v}=(u,v,w)$の回転で定義します．

```math
\boldsymbol{\omega} \equiv \text{rot}\boldsymbol{v} 
= \left(
\frac{\partial w}{\partial y} - \frac{\partial v}{\partial z},
\frac{\partial u}{\partial z} - \frac{\partial w}{\partial x},
\frac{\partial v}{\partial x} - \frac{\partial u}{\partial y}
\right)
```

一方，気象学で「渦度」と言ったときには，**鉛直渦度**，すなわち渦度ベクトルの鉛直成分


```math
\frac{\partial v}{\partial x} - \frac{\partial u}{\partial y}\tag{1}
```
を指すことが多いです．

今回は，この鉛直渦度を計算していきます．
これ以降，鉛直渦度のことを渦度と呼ぶことにします．

# 計算方法

渦度は式$(1)$のとおりなのですが，偏微分の形をしているため，このままでは具体的に計算することができません．

そこで，式$(1)$を離散化します．

```math
\frac{v_2-v_1}{\Delta x} - \frac{u_2-u_1}{\Delta y}\tag{2}
```

ここで，$v_2-v_1$は$x$軸方向（東西方向）の$v$の変化量，$u_2-u_1$は$y$軸方向（南北方向）の$u$の変化量です．

こちらの「色と形で気象予報士！」さんのブログ記事[^1]にあった図がとてもわかりやすいかと思います．

[^1]:https://irokata7.com/2021/09/01/r9-uzudo-shiki/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1900599/bbb2bc37-19f5-f6f7-e0c7-5e3167c1c2af.png)

あとは，離散化した式$(2)$にデータを代入して計算するだけです．

# Pythonによる実装
風のデータは，大気再解析データから取ってきました．


>- データ元：京都大学生存圏研究所のサイト内にある，「[解析値を中心に再構成したNetCDFデータ](http://database.rish.kyoto-u.ac.jp/arch/jmadata/gpv-netcdf.html)」からダウンロード
>- ファイル場所：[MSM-P/2023/](http://database.rish.kyoto-u.ac.jp/arch/jmadata/data/gpv/netcdf/MSM-P/2023/)1205.nc
>- `1205.nc`は100MB以上あるので，必要な風データのみを抽出して`1205_uv.nc`として保存しました

使用ライブラリは以下です．

```python
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
import cartopy.crs as ccrs
```

## データの読み込み
再解析データはNetCDFという形式のファイルなので，`xarray`というライブラリを使って読み込みます．

```python
data = xr.open_mfdataset('./1205_uv.nc').load()
```

風の東西成分と南北成分を，それぞれ変数`u`と`v`に格納します．
今回は，2023年12月5日9時（UTC）の500hPa面の渦度を計算することにします．

```python
TIME = '2023-12-05T00:00:00'
P = 500

u = data['u'].sel(time=TIME, p=P).values
v = data['v'].sel(time=TIME, p=P).values
```

あと，緯度経度座標も取得しておきます．
式$(2)$の$\Delta x, \Delta y$の計算に必要だからです[^2]．


[^2]:今回のデータの分解能は緯度方向0.1°，経度方向0.125°ですが，緯度や経度によってグリッドサイズが異なることに注意．

```python
lon, lat = np.meshgrid(data['lon'].values, data['lat'].values)
```

ある2地点の緯度経度が与えられたときの距離の算出方法は，以前私が書いた記事を参考にしてください．

https://qiita.com/matsuda_tkm/items/4eba5632535ca2f699b4

## 渦度の計算
あとは，式$(2)$に値をぶち込んでいくだけです．


```math
\frac{v_2-v_1}{\Delta x} - \frac{u_2-u_1}{\Delta y}\tag{2}
```

なお，愚直に二重forループで実装しました[^3]．

```python
zeta = np.zeros_like(u)

for i in range(1,lat.shape[0]-1):
    for j in range(1,lat.shape[1]-1):
        u1 = u[i+1][j]
        u2 = u[i-1][j]
        v1 = v[i][j-1]
        v2 = v[i][j+1]
        dx = dist((lat[i][j+1], lon[i][j+1]), (lat[i][j-1], lon[i][j-1]))
        dy = dist((lat[i+1][j], lon[i+1][j]), (lat[i-1][j], lon[i-1][j]))
        zeta[i][j] = (v2-v1)/(2*dx) - (u2-u1)/(2*dy)
```

## 結果の可視化

では，計算結果を可視化してみましょう．

地図の描画方法については，以前記事を書いていますので参考にしてください．

https://qiita.com/matsuda_tkm/items/4566b81f7341b155688a

```python
ax = plt.axes(projection=ccrs.PlateCarree())
ax.set_extent([lon.min(), lon.max(), lat.min(), lat.max()], crs=ccrs.PlateCarree())
ax.coastlines()
ax.gridlines(
    draw_labels=['left','bottom'],
    xlocs=np.arange(120, 150, 2.5),
    ylocs=np.arange(20, 50, 2.5),
)

plt.imshow(
    zeta*1e6, 
    cmap='bwr', vmax=600, vmin=-600,
    extent=[lon.min(), lon.max(), lat.min(), lat.max()], 
    transform=ccrs.PlateCarree())
cbar = plt.colorbar()
cbar.set_label(f'Vorticity at {P}hPa ($10^{-6}$ /s)')
plt.title(TIME + '(UTC)')

plt.tight_layout()
plt.savefig('vorticity.png', dpi=300)
plt.show()
```

![vorticity.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1900599/fcd3e860-e507-84e1-7cc4-2e5d970cb3c5.png)


いい感じですね．

念のため，答え合わせをしてみましょう．
下図はウェザーニューズ社提供のLabs Ch.から取ってきた，同時刻同圧力面における渦度（など）の値を示しています．

![R2312050000_000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1900599/46f2e63d-9ee7-7349-1cde-e3df9d8b7dd9.png)

解像度は違いますが，だいたい一致していますね．

黄海にのびる500hPaトラフに対応する高渦度領域を見てもらえるとわかりやすいかと思います．

[^3]:頑張ればforループ使わなくてもイケたかもしれませんが，数秒で計算が終わる量だったので素直にループです．

# ソースコード
Qiitaアドベントカレンダーで使ったコードはこちらのレポジトリにまとめています．

本記事のコードは`07-vorticity`の中に入っています．

https://github.com/matsuda-tkm/qiita-calender-2023