IT pass HikiWiki - [Exp2021]Fortran を用いた NetCDF データの解析 Diff

  • Added parts are displayed like this.
  • Deleted parts are displayed like this.

((<"スケジュール表・各回資料 (08/30)"|[Exp2021](08/13)"|[Exp2021]スケジュール表・各回資料#08-2F13>))

{{toc}}


= はじめに


ここまでに, ruby や GPhys を使って NetCDF 形式のデータの簡単な解析について説明しました.
ruby や GPhys に習熟すれば, 既に説明した以上に複雑なデータ解析・処理も行うことができます.
しかしながら, ruby や GPhys での複雑な処理は若干敷居が高いかもしれません.
それに比べると, ((<惑星学実験実習の基礎 II|惑星学実験実習の基礎II>))で Fortran を既に学んでいますので, データの入出力さえできれば Fortran でのデータ解析・処理は比較的容易です.
したがって, ここでは Fortran を用いた NetCDF データの入出力について説明します.
(Fortran での演算の方法については, ((<惑星学実験実習の基礎 II|惑星学実験実習の基礎II>)) の内容を復習すると良いでしょう.)


#------------------------------------------------------------------------------
= Fortran プログラムでの NetCDF データの入出力の概要

NetCDF データはメタデータを含む独自の形式ですから, Fortran の標準的な命令である read/write では入出力することができません.
したがって, NetCDF データを Fortran プログラムで入出力するためのプログラムが必要です.

NetCDF の開発元である Unidata は, Fortran のプログラムから NetCDF ファイルを入出力するためのライブラリを提供しています.
(「ライブラリ」は一連のプログラムを集めたものです. 例えば, Fortran であれば read/write に相当する, NetCDF ファイルを入出力するためのサブルーチンや関数をまとめたプログラム群のことです.
なお, Unidata は C, C++, Java のためのライブラリも提供しています.)
その Unidata が提供する NetCDF の Fortran ライブラリを用いることで, 自分で読み込みサブルーチン・関数を新たに作ることなく Fortran プログラムで NetCDF データの入出力が可能です.

しかし, Unidata が提供するライブラリは考えられるすべての入出力ができるものの, 「よくある使い方」だけでよいユーザにとっては使いにくいかもしれません.
そこでここでは, ユーザの利便性を考慮して((<地球対電脳倶楽部|URL:http://www.gfd-dennou.org/>))の有志が開発した NetCDF データ入出力のための Fortran ライブラリ (((<gtool5|URL:http://www.gfd-dennou.org/library/gtool/gtool5.htm.ja>))) を用いた入出力方法について説明します.
gtool5 は, 内部で Unidata 提供のライブラリを使用しています.



#------------------------------------------------------------------------------
= gtool5 のインストール

gtool5 は下のコマンドでインストールすることができます.

  $ sudo apt install gtool5



#------------------------------------------------------------------------------
= Fortran プログラムでの NetCDF ファイルの入力

ここでは, 既にダウンロードした NetCDF ファイル air.2019.nc を用いて
Fortran での入力を説明します.

下のプログラムを実行してみましょう.

((<nc_i.f90|URL:/exp/fy2021/210813/practice_gphys/nc_i.f90>))

  01  ! Sample program for gtool_history/gtool5
  02  !
  03  program nc_i
  04
  05    use gtool_history ! モジュール指定
  06
  07    implicit none
  08
  09    character(128)     :: InFileName               ! 入力ファイル名
  10
  11    integer, parameter :: NLon   = 144             ! 経度格子点数
  12    integer, parameter :: NLat   =  73             ! 緯度格子点数
  13    integer, parameter :: NLevel =  17             ! 高度(圧力)格子点数
  14    integer, parameter :: NTime  = 365             ! 時間格子点数
  15
  16    real(4)            :: x_Lon      (NLon  )             ! 経度配列
  17    real(4)            :: y_Lat      (NLat  )             ! 緯度配列
  18    real(4)            :: z_Level    (NLevel)             ! 高度(圧力)配列
  19    real(4)            :: t_Time     (NTime )             ! 時間配列
  20    real(4)            :: xyz_Temp   (NLon, NLat, NLevel) ! 温度配列
  21
  22    integer            :: it
  23    integer            :: i
  24    integer            :: j
  25    integer            :: k
  26
  27    character(64)      :: range
  28
  29
  30    InFileName  = "air.2019.nc"   ! 入力ファイル名の設定
  31
  32
  33    call HistoryGet(InFileName, 'lon'  , x_Lon  ) ! [入力] 経度を読む
  34    call HistoryGet(InFileName, 'lat'  , y_Lat  ) ! [入力] 緯度を読む
  35    call HistoryGet(InFileName, 'level', z_Level) ! [入力] 圧力を読む
  36    call HistoryGet(InFileName, 'time' , t_Time ) ! [入力] 時間を読む
  37
  38
  39    do it = 1, NTime
  40      write( 6, * ) it, t_Time(it)
  41
  42      write( range, '(a,i5.5)' ) 'time=^', it  ! 出力用の時刻情報文字列の 作成
  43      write( 6, * ) range
  44
  45      ! [入力] it 番目のデータを読む
  46      call HistoryGet(InFileName, 'air', xyz_Temp, range = range)
  47
  48      ! i=NLon/2, j=NLat/2, k=1 における温度の出力
  49      i = NLon/2
  50      j = NLat/2
  51      k = 1
  52      write( 6, * ) t_Time(it), x_Lon(i), y_Lat(j), z_Level(k), xyz_Temp(i,j,k)
  53
  54    end do
  55
  56    stop
  57  end program nc_i

プログラムを実行するには,

  $ gt5frt -o nc_i nc_i.f90

でコンパイルし,

  $ ./nc_i

で実行します. 実行すると下のように表示されるでしょう.

$ ./nc_i
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@lon
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@lat
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@level
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@time
           1   1919712.00
time=^00001
*** MESSAGE [HistoryGetReal3] ***  Input air.2019.nc@air,time=1919712.
   1919712.00       177.500000       2.50000000       1000.00000       300.649963
           2   1919736.00
time=^00002
*** MESSAGE [HistoryGetReal3] ***  Input air.2019.nc@air,time=1919736.
   1919736.00       177.500000       2.50000000       1000.00000       301.649994
           3   1919760.00
time=^00003
*** MESSAGE [HistoryGetReal3] ***  Input air.2019.nc@air,time=1919760.
   1919760.00       177.500000       2.50000000       1000.00000       301.625000

(中略)

         365   1928448.00
time=^00365
*** MESSAGE [HistoryGetReal3] ***  Input air.2019.nc@air,time=1928448.
   1928448.00       177.500000       2.50000000       1000.00000       301.024963


== [入力] プログラムの説明

上のプログラムは, air.2019.nc を読み込み, i=NLon/2, j=NLat/2, k=1 の場所の温度を表示します.


=== [入力] プログラムの内容の大まかな説明

ここではプログラムの内容を大雑把に説明します.

プログラムでは下のような処理をしています. (行頭の数字は行番号です.)

* 05-27:
  使用するモジュールや変数を準備しています.
* 30:
  入力ファイル名を指定しています.
* 33-36:
  入力ファイルから経度軸, 緯度軸, 圧力(高度)軸, 時間軸のデータを入力しています.
* 39-54:
  入力ファイルから時間軸方向に一つのデータずつを読み, 温度を画面に表示しています.
  * 42:
    何番目の時刻のデータを入出力するかを指定するための「ラベル」文字列を用意します.
  * 46:
    入力ファイルから時間一つ分の温度データ (3 次元) を読んでいます.
  * 52:
    読んだ温度のある場所の値を画面に出力しています.

例えばこのプログラムを基にして, 39-54 行目を変更することで自分がやりたいデータ処理ができるでしょう.


=== [入力] プログラムの詳細な説明

ここではプログラムの内容をより詳しく説明します.

プログラムでは下のような処理をしています. (行頭の数字は行番号です.)

* 05:
  使用するモジュールの指定. ここでは gtool5 のモジュールを使うことを宣言しています.
* 09-27:
  変数を宣言しています.
  * 20:
    読むデータを格納する配列を宣言しています. このとき, 次元の順番に気を付けましょう.
    今回読むデータの次元は 'lon', 'lat', 'level', 'time' ですが, netcdf ファイルの内容を ncdump で確認すると, 次元は 'time', 'level', 'lat', 'lon' の順番に書かれるでしょう.
    しかし, Fortran の配列での順番は逆順になりますので注意しましょう.
    (なお, C 言語では Fortran とは逆 (ncdump の表示の順) となります.)
* 30:
  入力ファイル名, 出力ファイル名を指定しています.
* 33-36:
  入力ファイルから経度軸, 緯度軸, 圧力(高度)軸, 時間軸のデータを入力しています.
* 39-54:
  入力ファイルから時間軸方向に一つのデータずつを読み, 温度を画面に表示しています.
  * 入力 NetCDF ファイルに保存されているすべての時間のデータを一度に読み込もうとするとメモリサイズに収まらずにエラーになることが少なくありません.
    時間 1 つのデータごとに読み込み, 演算して, 出力することを繰り返すことで, メモリ不足を回避することができます.
  * 各行は下のような処理を行っています.
    * 42:
      何番目の時刻のデータを入出力するかを指定するための「ラベル」文字列を用意します.
      * write は端末に数値や文字を出力するときに使うことが多いでしょう.
        例えば, 端末に出力する際には,

          write( 6, * ) ...

        のように使いました.
        * "6" は出力先を表し, 端末 (画面) に対応するのでした.
        * "*" は「適当に」出力することの指定でした. (詳細は((<惑星学実験実習の基礎 II|惑星学実験実習の基礎II>))の資料を参照してください.)
        この出力先の指定には他のものを指定することができ, 上のプログラムでは文字列 "range" を指定することで, 文字列変数に出力, つまり, 文字列変数に書き込み = 代入しています.
      * ここで指定しているラベルは下のような形式です.

          time=^N

        ここで, N は配列の要素の番号 (インデクス) です. "^" は要素番号を表します.
        "^" を付けない場合には, 配列に収められた値 (ここでは時間の値) を表します.
    * 46:
      入力ファイルから時間一つ分の温度データ (3 次元) を読んでいます.
    * 52:
      読んだ温度のある場所の値を画面に出力しています.


例えばこのプログラムを基にして, 39-54 行目を変更することで自分がやりたいデータ処理ができるでしょう.
もし複数の NetCDF ファイルから複数の物理量を読んでデータ処理をする必要があれば, 上で使われている HistoryGet を参考にして読み込みを追加すると良いでしょう.



#------------------------------------------------------------------------------
= Fortran プログラムでの NetCDF ファイルの入出力

ここでは, 上のプログラムに出力を追加して,
Fortran での入出力を説明します.

下のプログラムを実行してみましょう.

((<nc_io.f90|URL:/exp/fy2021/210813/practice_gphys/nc_io.f90>))

  01  ! Sample program for gtool_history/gtool5
  02  !
  03  program nc_io
  04
  05    use gtool_history ! モジュール指定
  06
  07    implicit none
  08
  09    character(128)     :: InFileName               ! 入力ファイル名
  10    character(128)     :: OutFileName              ! 出力ファイル名
  11
  12    integer, parameter :: NLon   = 144             ! 経度格子点数
  13    integer, parameter :: NLat   =  73             ! 緯度格子点数
  14    integer, parameter :: NLevel =  17             ! 高度(圧力)格子点数
  15    integer, parameter :: NTime  = 365             ! 時間格子点数
  16
  17    real(4)            :: x_Lon      (NLon  )             ! 経度配列
  18    real(4)            :: y_Lat      (NLat  )             ! 緯度配列
  19    real(4)            :: z_Level    (NLevel)             ! 高度(圧力)配列
  20    real(4)            :: t_Time     (NTime )             ! 時間配列
  21    real(4)            :: xyz_Temp   (NLon, NLat, NLevel) ! 温度配列
  22    real(4)            :: xyz_PotTemp(NLon, NLat, NLevel) ! 温位配列
  23
  24    real(4)            :: Kappa = 0.29     ! R/Cp
  25    real(4)            :: P0    = 1000.0   ! 基準圧力
  26
  27    integer            :: it
  28    integer            :: i
  29    integer            :: j
  30    integer            :: k
  31
  32    character(64)      :: range
  33
  34
  35    InFileName  = "air.2019.nc"   ! 入力ファイル名の設定
  36    OutFileName = "output.nc"     ! 出力ファイル名の設定
  37
  38
  39    call HistoryGet(InFileName, 'lon'  , x_Lon  ) ! [入力] 経度を読む
  40    call HistoryGet(InFileName, 'lat'  , y_Lat  ) ! [入力] 緯度を読む
  41    call HistoryGet(InFileName, 'level', z_Level) ! [入力] 圧力を読む
  42    call HistoryGet(InFileName, 'time' , t_Time ) ! [入力] 時間を読む
  43
  44
  45    ! [出力] 出力ファイル作成 (開く)
  46    call HistoryCreate(                                                 &
  47      & file=OutFileName, title='atmospheric temperature',              &
  48      & source='NCEP/NCAR Reanalysis data',                             &
  49      & institution='Department of Planetology, Kobe University',       &
  50      & dims=(/'lon  ','lat  ','level','time '/),                       &
  51      & dimsizes=(/NLon,NLat,NLevel,NTime/),                            &
  52      & longnames=(/'longitude','latitude ','level    ', 'time     '/), &
  53      & units=(/'degrees_east                    ',                     &
  54      &         'degrees_north                   ',                     &
  55      &         'hPa                             ',                     &
  56      &         'hours since 1800-01-01 00:00:0.0'/)                    &
  57      & )
  58
  59    call HistoryPut('lon'  ,x_Lon  )            ! [出力] 次元変数出力, 経度
  60    call HistoryPut('lat'  ,y_Lat  )            ! [出力] 次元変数出力, 緯度
  61    call HistoryPut('level',z_Level)            ! [出力] 次元変数出力, 高度(圧力)
  62    call HistoryPut('time' ,t_Time )            ! [出力] 次元変数出力, 時間
  63
  64
  65    ! [出力] 変数定義, 温位
  66    call HistoryAddVariable(                                         &
  67      & varname='pottemp', dims=(/'lon  ','lat  ','level','time '/), &
  68      & longname='potential temperature', units='K', xtype='real'    &
  69      & )
  70
  71    do it = 1, NTime
  72      write( 6, * ) it, t_Time(it)
  73
  74      write( range, '(a,i5.5)' ) 'time=^', it  ! 出力用の時刻情報文字列の作 成
  75      write( 6, * ) range
  76
  77      ! [入力] it 番目のデータを読む
  78      call HistoryGet(InFileName, 'air', xyz_Temp, range = range)
  79
  80      ! 温位の計算
  81      do k = 1, NLevel
  82        do j = 1, NLat
  83          do i = 1, NLon
  84            xyz_PotTemp(i,j,k) = xyz_Temp(i,j,k) * ( P0 / z_Level(k) )**Kappa
  85          end do
  86        end do
  87      end do
  88
  89      ! [出力] it 番目のデータを出力, 温位
  90      call HistoryPut('pottemp', xyz_PotTemp, range=range)
  91    end do
  92
  93    call HistoryClose  ! [出力] 出力ファイルを閉じる
  94
  95    stop
  96  end program nc_io

プログラムを実行するには,

  $ gt5frt -o nc_io nc_io.f90

でコンパイルし,

  $ ./nc_io

で実行します. 実行すると下のように表示されるでしょう.

$ ./nc_io
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@lon
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@lat
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@level
*** MESSAGE [HistoryGetReal1] ***  Input air.2019.nc@time
*** MESSAGE [GDNcFileOpen] ***  "output.nc" is overwritten.
*** MESSAGE [HistoryCreate1] ***  "output.nc" is created (origin=0. [])
           1   1919712.00
time=^00001
*** MESSAGE [HistoryGetReal3] ***  Input air.2019.nc@air,time=1919712.
           2   1919736.00
time=^00002

(中略)

         364   1928424.00
time=^00364
*** MESSAGE [HistoryGetReal3] ***  Input air.2019.nc@air,time=1928424.
*** MESSAGE [HistoryClose] ***  "output.nc" is closed


== [入出力] プログラムの説明

上のプログラムは, air.2019.nc を読み込み, その中に保存されている温度を使って温位を計算して NetCDF ファイル (ここでは output.nc) に保存します.

* 温位についてはここでは詳しく説明しませんが, エントロピーに相当する物理量です. 大気科学では「大気塊を断熱的に基準圧力 (地表面) まで移動させたときの温度」という意味で説明されることが多いでしょう.

=== [入出力] プログラムの内容の大まかな説明

ここではプログラムの内容を大雑把に説明します.

プログラムでは下のような処理をしています. (行頭の数字は行番号です.)

* 05-32:
  使用するモジュールや変数を準備しています.
* 35-36:
  入力ファイル名, 出力ファイル名を指定しています.
* 39-42:
  入力ファイルから経度軸, 緯度軸, 圧力(高度)軸, 時間軸のデータを入力しています.
* 45-69:
  出力ファイルを準備しています.
  * 45-57:
    出力 NetCDF ファイルを作成しています.
  * 59-62:
    出力ファイルに経度軸, 緯度軸, 圧力(高度)軸, 時間軸のデータを出力しています.
  * 65-69:
    出力する温位データを登録し, メタデータを設定しています.
* 71-91:
  入力ファイルから時間軸方向に一つのデータずつを読み, 温位を計算して出力しています.
  * 74:
    何番目の時刻のデータを入出力するかを指定するための「ラベル」文字列を用意します.
  * 78:
    入力ファイルから時間一つ分の温度データ (3 次元) を読んでいます.
  * 81-87:
    入力ファイルから読んだ温度を使って温位を計算しています.
  * 90:
    計算した温位を NetCDF ファイルに出力しています.
* 93:
  出力 NetCDF ファイルを閉じています.


例えばこのプログラムを基にして, 71-91 行目を変更することで自分がやりたいデータ処理ができるでしょう.


=== [入出力] プログラムの詳細な説明

ここではプログラムの内容をより詳しく説明します.

プログラムでは下のような処理をしています. (行頭の数字は行番号です.)

* 05:
  使用するモジュールの指定. ここでは gtool5 のモジュールを使うことを宣言しています.
* 09-32:
  変数を宣言しています.
  * 21-22:
    読むデータを格納する配列を宣言しています. このとき, 次元の順番に気を付けましょう.
    今回読むデータの次元は 'lon', 'lat', 'level', 'time' ですが, netcdf ファイルの内容を ncdump で確認すると, 次元は 'time', 'level', 'lat', 'lon' の順番に書かれるでしょう.
    しかし, Fortran の配列での順番は逆順になりますので注意しましょう.
    (なお, C 言語では Fortran とは逆 (ncdump の表示の順) となります.)
* 35-36:
  入力ファイル名, 出力ファイル名を指定しています.
* 39-42:
  入力ファイルから経度軸, 緯度軸, 圧力(高度)軸, 時間軸のデータを入力しています.
* 45-57:
  出力 NetCDF ファイルを準備しています.
  * NetCDF ファイルに出力する軸情報などのメタデータは下のオプションで指定します.
    * file : 出力 NetCDF ファイル名
    * title : データのタイトル
    * source : データの出どころ
    * institution : データ作成機関 / データ作成者の所属機関
    * dims : NetCDF ファイルが含む軸/次元
      * (/'lon  ','lat  ','level','time '/) は配列になっていて, このファイルが 4 つの軸, lon, lat, level, time を含むことを表しています.
      * なお, 'lon  ' のように "n" の後に空白を二つ入力しているのは, すべての軸の名前の長さを揃えるためです. 今回の場合は level が 5 文字の為, 他の軸の名前にも空白を入れて 5 文字に揃えています.
      * もし (/'lon','lat','level','time'/) のように文字列を揃えないと, コンパイル時に下のようなエラーが出ます.
          Error: Different CHARACTER lengths (4/5) in array constructor at (1)
    * dimsizes : dims で与えた軸/次元のそれぞれの長さ
      * これも (/NLon,NLat,NLevel,NTime/) は配列になっています.
    * longnames : 軸/次元の名前のより詳しい名前
      * これも (/'longitude','latitude ','level    ', 'time     '/) は配列になっており, すべての名前の文字数を揃えるために空白を使っています.
    * units : 軸/次元の単位
      * これも配列になっており, すべての名前の文字数を揃えるために空白を使っています. 今回はそれぞれが長いため, "&" を使って改行しています.
* 59-62:
  出力ファイルに経度軸, 緯度軸, 圧力(高度)軸, 時間軸のデータを出力しています.
* 65-69:
  出力する温位データを登録し, メタデータを設定しています.
* 71-91:
  入力ファイルから時間軸方向に一つのデータずつを読み, 温位を計算して出力しています.
  * 入力 NetCDF ファイルに保存されているすべての時間のデータを一度に読み込もうとするとメモリサイズに収まらずにエラーになることが少なくありません.
    時間 1 つのデータごとに読み込み, 演算して, 出力することを繰り返すことで, メモリ不足を回避することができます.
  * 各行は下のような処理を行っています.
    * 74:
      何番目の時刻のデータを入出力するかを指定するための「ラベル」文字列を用意します.
      * write は端末に数値や文字を出力するときに使うことが多いでしょう.
        例えば, 端末に出力する際には,

          write( 6, * ) ...

        のように使いました.
        * "6" は出力先を表し, 端末 (画面) に対応するのでした.
        * "*" は「適当に」出力することの指定でした. (詳細は((<惑星学実験実習の基礎 II|惑星学実験実習の基礎II>))の資料を参照してください.)
        この出力先の指定には他のものを指定することができ, 上のプログラムでは文字列 "range" を指定することで, 文字列変数に出力, つまり, 文字列変数に書き込み = 代入しています.
      * ここで指定しているラベルは下のような形式です.

          time=^N

        ここで, N は配列の要素の番号 (インデクス) です. "^" は要素番号を表します.
        "^" を付けない場合には, 配列に収められた値 (ここでは時間の値) を表します.
    * 78:
      入力ファイルから時間一つ分の温度データ (3 次元) を読んでいます.
    * 81-87:
      入力ファイルから読んだ温度を使って温位を計算しています.
    * 90:
      計算した温位を NetCDF ファイルに出力しています.
* 93:
  出力 NetCDF ファイルを閉じています.


例えばこのプログラムを基にして, 71-91 行目を変更することで自分がやりたいデータ処理ができるでしょう.
もし複数の NetCDF ファイルから複数の物理量を読んでデータ処理をする必要があれば, 上で使われている HistoryGet を参考にして読み込みを追加すると良いでしょう.


#------------------------------------------------------------------------------
= gt5frt についての補足

上の説明ではプログラムをコンパイルするために gt5frt を使いました.

これは Fortran コンパイラ gfortran に, NetCDF ライブラリや gtool5 ライブラリを用いるためのオプションを付けた「別名」コマンドです.
具体的には下のようなオプションを指定しています.
* -I/usr/include
* -I/usr/lib/x86_64-linux-gnu/gtool5/include
* -L/usr/lib/x86_64-linux-gnu/gtool5/lib
* -L/usr/lib/x86_64-linux-gnu
* -lgtool5
* -lnetcdf
* -lnetcdff
* -fPIC
これらのオプションは, -I, -L, -l -f にオプションの「値」を組み合わせた形になっています.
興味があれば -I, -L, -l, -f の意味を調べてみると良いでしょう.
(man コマンドを使うのも良いでしょうし, 「gfortran」「オプション」などをキーワードにして検索してみるのも良いでしょう.)


== より詳しい情報

gtool についてのより詳しい情報は下の場所で見つけることができるでしょう.
* ((<gtool5 オフィシャルチュートリアル|URL:http://www.gfd-dennou.org/library/gtool/gtool5/gtool5_current/doc/tutorial/>))
* ((<gtool5 Fortran 90/95 ライブラリ|URL:http://www.gfd-dennou.org/library/gtool/gtool5.htm.ja>))